pi-weave 0.2.0 → 0.2.2

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/README.md CHANGED
@@ -147,15 +147,19 @@ Renaming or moving a note rewrites its inbound links automatically, so its backl
147
147
  The browser workspace has four connected views:
148
148
 
149
149
  - **Tree** — notes, folders, and repository structure, with text and provenance filters.
150
- - **Note** — rendered Markdown with clickable `[[wikilinks]]`, link previews, tags, authorship, and an action to open the source in
151
- `$EDITOR`.
150
+ - **Note** — rendered Markdown with clickable `[[wikilinks]]`, link previews, tags, authorship, click-to-edit, and an action to open the
151
+ source in `$EDITOR`.
152
152
  - **Graph** — a navigable map of notes, links, mentions, modules, and repository relationships. Selecting something updates every view.
153
153
  - **Context** — links, backlinks, tags, and code mentions for the current selection.
154
154
 
155
155
  Search with `⌘K` / `Ctrl K`. Press `?` for all shortcuts. The workspace updates as notes change on disk, so a note written by Pi appears
156
156
  without a reload. It follows the system theme by default and can be switched between light and dark.
157
157
 
158
- The browser is read-only. Edit with `$EDITOR`, Obsidian, or the `weave_note` tool; unknown front-matter fields remain preserved.
158
+ **Click the note text to edit it** — the prose swaps for its Markdown source; `⌘S` / `Ctrl S` saves, `Esc` or **Done** closes. Selecting
159
+ text to copy and clicking a `[[wikilink]]` do what they always did; neither opens the editor. A save rewrites the body and nothing
160
+ else — unknown front-matter fields, key order, and the append-only `## Raw` tail of a dictated note all survive untouched, so a note edited
161
+ here and a note edited in Obsidian stay byte-compatible. Editing with `$EDITOR`, Obsidian, or the `weave_note` tool works as before; the
162
+ last writer wins, and the workspace picks up outside changes within a couple of seconds.
159
163
 
160
164
  `/weave-view tui` is the smaller, read-only terminal explorer: tree, focused neighborhood, details, and link health over the same graph.
161
165
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-weave",
3
- "version": "0.2.0",
3
+ "version": "0.2.2",
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,
package/src/core/index.ts CHANGED
@@ -59,6 +59,7 @@ export {
59
59
  resolveHtmlPath,
60
60
  resolveNotePath,
61
61
  searchNotes,
62
+ setNoteBody,
62
63
  upsertNote,
63
64
  type LinkRepairResult,
64
65
  } from "./vault";
@@ -99,7 +99,7 @@ const RAW_HEADING_RE = new RegExp(`^${RAW_NOTES_HEADING}[ \\t]*\\r?$`, "m");
99
99
  * inside a ```` block would too — and each mistake ends the protected region
100
100
  * early, exposing example links in documentation to rewriting.
101
101
  */
102
- function fenceRanges(body: string): [number, number][] {
102
+ export function fenceRanges(body: string): [number, number][] {
103
103
  const ranges: [number, number][] = [];
104
104
  let open: { at: number; marker: string; length: number } | null = null;
105
105
  for (const match of body.matchAll(FENCE_RE)) {
@@ -134,7 +134,7 @@ function fenceRanges(body: string): [number, number][] {
134
134
  * refuse to repair — every link below it. Matching a whole heading line also
135
135
  * keeps `## Rawhide` from starting a tail.
136
136
  */
137
- function rawTailStart(body: string, fences: readonly [number, number][]): number {
137
+ export function rawTailStart(body: string, fences: readonly [number, number][]): number {
138
138
  let from = 0;
139
139
  for (;;) {
140
140
  const rest = body.slice(from);
package/src/core/vault.ts CHANGED
@@ -10,7 +10,7 @@ import {
10
10
  unquoteField,
11
11
  upsertFrontMatterFields,
12
12
  } from "./frontmatter";
13
- import { auditLinks, rewriteLinks, RAW_NOTES_HEADING, type LinkAudit, type LinkFix } from "./links/repair";
13
+ import { auditLinks, fenceRanges, rawTailStart, rewriteLinks, RAW_NOTES_HEADING, type LinkAudit, type LinkFix } from "./links/repair";
14
14
  import { withMutationQueue } from "./mutex";
15
15
  import { NOTES_DIR, OKF_MANIFEST } from "./paths";
16
16
  import { slugify, uniqueSlug } from "./slug";
@@ -596,6 +596,51 @@ export async function repairVaultLinks(root: string, options: { apply?: boolean
596
596
  });
597
597
  }
598
598
 
599
+ /**
600
+ * Keep the on-disk `## Raw` tail whatever the body says — it is append-only.
601
+ * `rawTailStart`, not `extractRawTail`: fence-aware, for untrusted input.
602
+ */
603
+ function preserveRawTail(currentBody: string, nextBody: string): string {
604
+ const tail = currentBody.slice(tailBoundary(currentBody)).trimEnd();
605
+ const head = nextBody.slice(0, tailBoundary(nextBody)).trim();
606
+ if (tail === "") return head;
607
+ // An emptied head leaves the tail alone, not a leading blank line.
608
+ return head === "" ? tail : `${head}\n\n${tail}`;
609
+ }
610
+
611
+ /** A `---` rule, alone on the line, with only blank space after it. */
612
+ const TAIL_RULE_RE = /(?:^|\n)-{3,}[ \t]*\r?\n\s*$/;
613
+
614
+ /**
615
+ * Where the tail's text begins — the `---` rule, not the heading
616
+ * `rawTailStart` returns. Splitting at the heading orphans the rule.
617
+ */
618
+ function tailBoundary(body: string): number {
619
+ const heading = rawTailStart(body, fenceRanges(body));
620
+ if (heading === body.length) return heading;
621
+ const rule = TAIL_RULE_RE.exec(body.slice(0, heading));
622
+ if (rule === null) return heading;
623
+ // `index` is the newline before the rule when there is one; step over it.
624
+ return rule.index === 0 && !body.startsWith("\n") ? 0 : rule.index + 1;
625
+ }
626
+
627
+ /**
628
+ * Replace a note's Markdown body; unknown front matter rides on
629
+ * `note.frontMatter` and only `updated` moves. Last write wins — no
630
+ * `expectedRevision`; docs/weave-workspace.md §11 P5.3 argues the trade.
631
+ */
632
+ export async function setNoteBody(root: string, slug: string, body: string, now = new Date()): Promise<VaultMutationResult> {
633
+ const path = resolveNotePath(root, slug);
634
+ if (path === null) return { ok: false, reason: "invalid" };
635
+ return withVaultLock(root, async () => {
636
+ const note = await getNote(root, slug);
637
+ if (note === null) return { ok: false, reason: "missing" };
638
+ const next = preserveRawTail(note.body, body);
639
+ await writeNote(path, slug, { ...note, updated: now.toISOString() }, next, note.frontMatter);
640
+ return { ok: true, slug };
641
+ });
642
+ }
643
+
599
644
  /** Rename a note in place and keep its front-matter title in sync. */
600
645
  export async function renameNote(root: string, slug: string, name: string, now = new Date()): Promise<VaultMutationResult> {
601
646
  const from = resolveNotePath(root, slug);
@@ -323,6 +323,12 @@ function mutation(fetchImpl: FetchLike, url: string, method: string, body?: unkn
323
323
  const noteUrl = (slug: string, suffix = ""): string => `/api/note/${encodeURIComponent(slug)}${suffix}`;
324
324
  const folderUrl = (path: string, suffix = ""): string => `/api/folder/${path.split("/").map(encodeURIComponent).join("/")}${suffix}`;
325
325
 
326
+ /**
327
+ * `POST /api/note/:slug` — save the note's Markdown body. No revision
328
+ * (last-write-wins, see core's `setNoteBody`), and a `MutationResult` rather
329
+ * than the note: the poll is what puts the new text back on screen.
330
+ */
331
+ export const saveNote = (fetchImpl: FetchLike, slug: string, body: string) => mutation(fetchImpl, noteUrl(slug), "POST", { body });
326
332
  export const renameNote = (fetchImpl: FetchLike, slug: string, name: string) => mutation(fetchImpl, noteUrl(slug, "/rename"), "POST", { name });
327
333
  export const moveNote = (fetchImpl: FetchLike, slug: string, folder: string | null) => mutation(fetchImpl, noteUrl(slug, "/move"), "POST", { folder });
328
334
  export const deleteNote = (fetchImpl: FetchLike, slug: string) => mutation(fetchImpl, noteUrl(slug), "DELETE");