pi-weave 0.1.18 → 0.1.20

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-weave",
3
- "version": "0.1.18",
3
+ "version": "0.1.20",
4
4
  "description": "An agent-native knowledge workspace for your life and your code. Smart notepad + repository exploration, readable by humans and agents alike.",
5
5
  "type": "module",
6
6
  "sideEffects": false,
@@ -242,12 +242,19 @@ function canonicalBlock(meta: NoteMeta): string[] {
242
242
  function replayBlock(meta: NoteMeta, frontMatter: NoteFrontMatter): string[] {
243
243
  /** Top-level keys the file declares, in any syntax — including frozen ones. */
244
244
  const declared = new Set<string>();
245
+ /** Values of declared scalar keys, unquoted, to detect defaulted fallbacks. */
246
+ const declaredValues = new Map<string, string>();
245
247
  /** Owned keys already re-rendered, so a duplicate key collapses to one line. */
246
248
  const rendered = new Set<string>();
247
249
  const out: string[] = [];
248
250
 
249
251
  for (const line of scanFrontMatter(frontMatter)) {
250
- if (line.key !== null) declared.add(line.key);
252
+ if (line.key !== null) {
253
+ declared.add(line.key);
254
+ if (line.scalar) {
255
+ declaredValues.set(line.key, unquoteField(line.text.slice(line.text.indexOf(":") + 1).trim()));
256
+ }
257
+ }
251
258
  if (line.key === null || !line.scalar || !MANAGED.has(line.key)) {
252
259
  out.push(line.text); // carried verbatim — never interpreted, never reformatted
253
260
  continue;
@@ -260,8 +267,13 @@ function replayBlock(meta: NoteMeta, frontMatter: NoteFrontMatter): string[] {
260
267
  out.push(renderManaged(line.key as ManagedFrontMatterKey, meta));
261
268
  }
262
269
 
270
+ const dateVal = declaredValues.get("date");
271
+ const createdVal = declaredValues.get("created");
263
272
  for (const key of MANAGED_FRONT_MATTER_KEYS) {
264
273
  if (declared.has(key) || isDefaulted(key, meta)) continue;
274
+ if (key === "created" && (!declared.has("created") && !declared.has("date"))) continue;
275
+ if (key === "created" && dateVal !== undefined && meta.created === dateVal) continue;
276
+ if (key === "updated" && ((dateVal !== undefined && meta.updated === dateVal) || (createdVal !== undefined && meta.updated === createdVal))) continue;
265
277
  out.push(renderManaged(key, meta));
266
278
  }
267
279
  return out;
@@ -302,10 +314,14 @@ export function parseNoteFile(text: string): ParsedNoteFile {
302
314
  if (!title) {
303
315
  throw new Error("Front matter is missing required field: title");
304
316
  }
317
+ const createdRaw = parsed.fields.get("created") ?? parsed.fields.get("date");
318
+ const created = createdRaw ? unquoteField(createdRaw) : "";
319
+ const updatedRaw = parsed.fields.get("updated") ?? parsed.fields.get("date") ?? created;
320
+ const updated = updatedRaw ? unquoteField(updatedRaw) : "";
305
321
  const meta: NoteMeta = {
306
322
  title: unquoteField(title),
307
- created: parsed.fields.get("created") ?? "",
308
- updated: parsed.fields.get("updated") ?? "",
323
+ created,
324
+ updated,
309
325
  tags: parseTags(parsed.fields.get("tags") ?? "[]"),
310
326
  source: parseSource(parsed.fields.get("source") ?? "human"),
311
327
  };
@@ -160,15 +160,22 @@ function buildVaultSide(
160
160
  .filter((d) => d.length > 0)
161
161
  .sort();
162
162
  const notesIn = (dir: string): number => kept.filter((n) => n.slug.startsWith(`${dir}/`)).length;
163
+ const totalNotesIn = (dir: string): number => input.notes.filter((n) => n.slug.startsWith(`${dir}/`)).length;
163
164
  for (const dir of noteDirs) {
164
165
  const id = `vfolder:${dir}`;
165
166
  folderIds.set(dir, id);
167
+ const keptCount = notesIn(dir);
168
+ const totalCount = totalNotesIn(dir);
169
+ const folderDetail: Record<string, string> = { path: dir, notes: String(keptCount) };
170
+ if (totalCount > keptCount) {
171
+ folderDetail.warning = `${totalCount - keptCount} older note(s) in this folder omitted by note limit`;
172
+ }
166
173
  nodes.push({
167
174
  id,
168
175
  kind: "module",
169
176
  label: dir.split("/").pop() ?? dir,
170
177
  provenance: null,
171
- detail: { path: dir, notes: String(notesIn(dir)) },
178
+ detail: folderDetail,
172
179
  });
173
180
  const parentDir = dir.split("/").slice(0, -1).join("/");
174
181
  const parent = folderIds.get(parentDir) ?? "vault";
package/src/core/index.ts CHANGED
@@ -17,6 +17,7 @@ export { runDeepScan, type DeepScanOptions, type DeepScanResult, type SummarizeF
17
17
  export {
18
18
  addNote,
19
19
  appendToNote,
20
+ createFolder,
20
21
  deleteFolder,
21
22
  deleteNote,
22
23
  extractRawTail,
package/src/core/vault.ts CHANGED
@@ -129,6 +129,14 @@ export async function getNote(root: string, slug: string): Promise<Note | null>
129
129
  }
130
130
  try {
131
131
  const { meta, body, frontMatter } = parseNoteFile(text);
132
+ if (!meta.updated || !meta.created) {
133
+ const st = await fs.stat(path).catch(() => null);
134
+ if (st) {
135
+ const mtime = st.mtime.toISOString();
136
+ if (!meta.updated) meta.updated = mtime;
137
+ if (!meta.created) meta.created = mtime;
138
+ }
139
+ }
132
140
  return { slug, ...meta, body, frontMatter };
133
141
  } catch {
134
142
  return null;
@@ -438,6 +446,24 @@ export async function deleteFolder(root: string, folder: string): Promise<VaultM
438
446
  });
439
447
  }
440
448
 
449
+ /** Create a new vault folder. */
450
+ export async function createFolder(root: string, folder: string): Promise<VaultMutationResult> {
451
+ const trimmed = folder.trim();
452
+ if (trimmed === "") return { ok: false, reason: "invalid" };
453
+ const rawParts = trimmed.split("/").map((p) => p.trim()).filter((p) => p.length > 0);
454
+ if (rawParts.length === 0 || rawParts.some((p) => p === "." || p === "..")) return { ok: false, reason: "invalid" };
455
+ const parts = rawParts.map(slugify).filter((p) => p.length > 0);
456
+ if (parts.length === 0) return { ok: false, reason: "invalid" };
457
+ const target = parts.join("/");
458
+ const to = resolveFolderPath(root, target);
459
+ if (to === null) return { ok: false, reason: "invalid" };
460
+ return withVaultLock(root, async () => {
461
+ if (await exists(to)) return { ok: false, reason: "collision" };
462
+ await fs.mkdir(to, { recursive: true });
463
+ return { ok: true, path: target };
464
+ });
465
+ }
466
+
441
467
  async function listVaultEntries(root: string): Promise<{ files: string[]; folders: string[] }> {
442
468
  const dir = join(root, NOTES_DIR);
443
469
  const files: string[] = [];
@@ -326,5 +326,6 @@ const folderUrl = (path: string, suffix = ""): string => `/api/folder/${path.spl
326
326
  export const renameNote = (fetchImpl: FetchLike, slug: string, name: string) => mutation(fetchImpl, noteUrl(slug, "/rename"), "POST", { name });
327
327
  export const moveNote = (fetchImpl: FetchLike, slug: string, folder: string | null) => mutation(fetchImpl, noteUrl(slug, "/move"), "POST", { folder });
328
328
  export const deleteNote = (fetchImpl: FetchLike, slug: string) => mutation(fetchImpl, noteUrl(slug), "DELETE");
329
+ export const createFolder = (fetchImpl: FetchLike, path: string) => mutation(fetchImpl, folderUrl(path), "POST");
329
330
  export const renameFolder = (fetchImpl: FetchLike, path: string, name: string) => mutation(fetchImpl, folderUrl(path, "/rename"), "POST", { name });
330
331
  export const deleteFolder = (fetchImpl: FetchLike, path: string) => mutation(fetchImpl, folderUrl(path), "DELETE");