list-notes.mjs
218 lines 7.6 KB
Raw
sha256:65ccb454656ea5acdea0a10e559b78bcde1eb6ff753ecc2911bc99d1c3d7cadd feat(calendar): enforce agent context tiers in retrieval AP… Human minor ⚠ breaking 1 day ago
1 /**
2 * List notes with filters. Single backend for CLI and MCP. Phase 9.
3 * Extracted from CLI runListNotes for reuse.
4 */
5
6 import { listMarkdownFiles, readNote, normalizeSlug, normalizeTags, effectiveProjectSlug } from './vault.mjs';
7 import { isApprovalLogNote } from './approval-log.mjs';
8
9 /**
10 * @param {string} d - date string
11 * @returns {string} YYYY-MM-DD slice for range comparison
12 */
13 function dateSlice(d) {
14 if (d == null || typeof d !== 'string') return '';
15 return d.trim().slice(0, 10) || '';
16 }
17
18 /**
19 * Get notes with metadata for listing.
20 * @param {string} vaultPath
21 * @param {{ ignore?: string[] }} config
22 * @returns {{ path: string, frontmatter: object, body: string, project?: string, tags?: string[], date?: string, updated?: string, causal_chain_id?: string, entity?: string[], episode_id?: string }[]}
23 */
24 export function getNotesWithMeta(vaultPath, config = {}) {
25 const paths = listMarkdownFiles(vaultPath, { ignore: config.ignore });
26 const notes = [];
27 for (const p of paths) {
28 try {
29 notes.push(readNote(vaultPath, p));
30 } catch (_) {
31 // skip unreadable
32 }
33 }
34 return notes;
35 }
36
37 /**
38 * Apply list-notes structural filters (folder, project, tag, dates, chain, entity, episode,
39 * content_scope, network, wallet_address, payment_status).
40 * Mutates no inputs; returns a new array.
41 * @param {Array<{ path: string, frontmatter?: object, body?: string, project?: string, tags?: string[], date?: string, updated?: string, causal_chain_id?: string, entity?: string[], episode_id?: string, network?: string, wallet_address?: string, payment_status?: string }>} notes
42 * @param {{
43 * folder?: string,
44 * project?: string,
45 * tag?: string,
46 * since?: string,
47 * until?: string,
48 * chain?: string,
49 * entity?: string,
50 * episode?: string,
51 * content_scope?: 'all'|'notes'|'approval_logs',
52 * network?: string,
53 * wallet_address?: string,
54 * payment_status?: string
55 * }} options
56 */
57 export function filterNotesByListOptions(notes, options = {}) {
58 let out = notes.slice();
59 if (options.folder) {
60 const prefix = options.folder.replace(/\\/g, '/').replace(/\/$/, '') + '/';
61 out = out.filter((n) => n.path === options.folder || n.path.startsWith(prefix));
62 }
63 if (options.project) {
64 const p = normalizeSlug(options.project);
65 out = out.filter((n) => effectiveProjectSlug(n.path, n.frontmatter) === p);
66 }
67 if (options.tag) {
68 const t = normalizeSlug(options.tag);
69 out = out.filter((n) => n.tags?.includes(t) || normalizeTags(n.frontmatter?.tags).includes(t));
70 }
71 if (options.since) {
72 const s = dateSlice(options.since);
73 if (s) out = out.filter((n) => dateSlice(n.date || n.updated) >= s);
74 }
75 if (options.until) {
76 const u = dateSlice(options.until);
77 if (u) out = out.filter((n) => dateSlice(n.date || n.updated) <= u);
78 }
79 if (options.chain) {
80 const c = normalizeSlug(options.chain);
81 out = out.filter((n) => n.causal_chain_id === c);
82 }
83 if (options.entity) {
84 const e = normalizeSlug(options.entity);
85 out = out.filter((n) => Array.isArray(n.entity) && n.entity.includes(e));
86 }
87 if (options.episode) {
88 const ep = normalizeSlug(options.episode);
89 out = out.filter((n) => n.episode_id === ep);
90 }
91 const cs = options.content_scope;
92 if (cs === 'notes') {
93 out = out.filter((n) => !isApprovalLogNote(n));
94 } else if (cs === 'approval_logs') {
95 out = out.filter((n) => isApprovalLogNote(n));
96 }
97 // Phase 12 — blockchain frontmatter filters
98 if (options.network) {
99 const net = String(options.network).trim().toLowerCase();
100 out = out.filter((n) => {
101 const nw = n.network ?? n.frontmatter?.network;
102 return nw != null && String(nw).trim().toLowerCase() === net;
103 });
104 }
105 if (options.wallet_address) {
106 const wa = String(options.wallet_address).trim().toLowerCase();
107 out = out.filter((n) => {
108 const addr = n.wallet_address ?? n.frontmatter?.wallet_address;
109 return addr != null && String(addr).trim().toLowerCase() === wa;
110 });
111 }
112 if (options.payment_status) {
113 const ps = String(options.payment_status).trim().toLowerCase();
114 out = out.filter((n) => {
115 const status = n.payment_status ?? n.frontmatter?.payment_status;
116 return status != null && String(status).trim().toLowerCase() === ps;
117 });
118 }
119 return out;
120 }
121
122 /**
123 * Run list-notes with filters. Returns SPEC §4.2 JSON shape.
124 * @param {{ vault_path: string, ignore?: string[] }} config
125 * @param {{
126 * folder?: string,
127 * project?: string,
128 * tag?: string,
129 * since?: string,
130 * until?: string,
131 * chain?: string,
132 * entity?: string,
133 * episode?: string,
134 * limit?: number,
135 * offset?: number,
136 * order?: 'date'|'date-asc'|string,
137 * fields?: 'path'|'path+metadata'|'full',
138 * countOnly?: boolean,
139 * content_scope?: 'all'|'notes'|'approval_logs',
140 * network?: string,
141 * wallet_address?: string,
142 * payment_status?: string
143 * }} options
144 * @returns {{ notes?: object[], total: number }}
145 */
146 export function runListNotes(config, options = {}) {
147 const limit = Math.max(0, options.limit ?? 20);
148 const offset = Math.max(0, options.offset ?? 0);
149 const order = options.order || 'date';
150 const fields = options.fields || 'path+metadata';
151 const countOnly = options.countOnly === true;
152
153 let notes = filterNotesByListOptions(getNotesWithMeta(config.vault_path, config), options);
154
155 if (order === 'date-asc') {
156 notes.sort((a, b) => (a.date || a.updated || '').localeCompare(b.date || b.updated || ''));
157 } else if (order === 'date') {
158 notes.sort((a, b) => (b.date || b.updated || '').localeCompare(a.date || a.updated || ''));
159 } else {
160 notes.sort((a, b) => a.path.localeCompare(b.path));
161 }
162
163 const total = notes.length;
164 const slice = notes.slice(offset, offset + limit);
165
166 if (countOnly) {
167 return { total };
168 }
169
170 const list = slice.map((n) => {
171 if (fields === 'path') return { path: n.path };
172 if (fields === 'full') return { path: n.path, frontmatter: n.frontmatter, body: n.body };
173 const fm = n.frontmatter || {};
174 return {
175 path: n.path,
176 title: fm.title ?? null,
177 project: n.project || null,
178 tags: n.tags || [],
179 date: n.date || null,
180 kind: fm.kind != null ? String(fm.kind) : null,
181 /** ISO timestamp for Hub UI calendar/list when `date` is unset (list response omits full frontmatter). */
182 knowtation_edited_at:
183 fm.knowtation_edited_at != null ? String(fm.knowtation_edited_at) : null,
184 };
185 });
186
187 return { notes: list, total };
188 }
189
190 /**
191 * Return facet values for filter dropdowns: projects, tags, folders, networks, wallets.
192 * @param {{ vault_path: string, ignore?: string[] }} config
193 * @returns {{ projects: string[], tags: string[], folders: string[], networks: string[], wallets: string[] }}
194 */
195 export function runFacets(config) {
196 const notes = getNotesWithMeta(config.vault_path, config);
197 const projects = new Set();
198 const tags = new Set();
199 const folders = new Set();
200 const networks = new Set();
201 const wallets = new Set();
202 for (const n of notes) {
203 if (n.project) projects.add(n.project);
204 for (const t of n.tags || []) if (t) tags.add(t);
205 const folder = n.path.includes('/') ? n.path.split('/').slice(0, -1).join('/') : '';
206 if (folder) folders.add(folder);
207 const fm = n.frontmatter || {};
208 if (fm.network != null && String(fm.network).trim()) networks.add(String(fm.network).trim());
209 if (fm.wallet_address != null && String(fm.wallet_address).trim()) wallets.add(String(fm.wallet_address).trim());
210 }
211 return {
212 projects: [...projects].sort(),
213 tags: [...tags].sort(),
214 folders: [...folders].sort(),
215 networks: [...networks].sort(),
216 wallets: [...wallets].sort(),
217 };
218 }
File History 2 commits
sha256:65ccb454656ea5acdea0a10e559b78bcde1eb6ff753ecc2911bc99d1c3d7cadd feat(calendar): enforce agent context tiers in retrieval AP… Human minor 1 day ago
sha256:9103f98c89257ed2b01c237cea895dabb3e85ea337dccb1161c175e4422355b6 docs: accept Calendar Events v0 spec with Phase 0 security … Human 1 day ago