pi-weave 0.3.12 → 0.3.13
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 +1 -1
- package/src/core/index.ts +3 -0
- package/src/core/vault.ts +79 -5
- package/src/core/view/tree.ts +2 -2
- package/src/web/client/api.ts +4 -0
- package/src/web/client/dist/app.js +48 -48
- package/src/web/client/shell/shell.model.ts +3 -2
- package/src/web/client/tree/Tree.tsx +8 -6
- package/src/web/client/tree/tree.model.ts +22 -7
- package/src/web/server/routes.ts +25 -3
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-weave",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.13",
|
|
4
4
|
"description": "Local-first Markdown memory and knowledge graph for Pi and OpenCode: preserve decisions, recall sessions, and explore notes with code.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"sideEffects": false,
|
package/src/core/index.ts
CHANGED
package/src/core/vault.ts
CHANGED
|
@@ -764,6 +764,80 @@ export async function moveNote(root: string, slug: string, folder: string | null
|
|
|
764
764
|
});
|
|
765
765
|
}
|
|
766
766
|
|
|
767
|
+
/**
|
|
768
|
+
* Move a standalone HTML artifact to an existing vault folder, or to the vault
|
|
769
|
+
* root with `null`. Notes that link to it by path follow it, as they do a note.
|
|
770
|
+
*/
|
|
771
|
+
export async function moveHtmlArtifact(root: string, slug: string, folder: string | null): Promise<VaultMutationResult> {
|
|
772
|
+
const from = resolveHtmlPath(root, slug);
|
|
773
|
+
if (from === null) return { ok: false, reason: "invalid" };
|
|
774
|
+
const targetDir = folder === null ? join(root, NOTES_DIR) : resolveFolderPath(root, folder);
|
|
775
|
+
if (targetDir === null || !(await isDirectory(targetDir))) return { ok: false, reason: "missing" };
|
|
776
|
+
const target = folder === null ? basename(slug) : `${folder}/${basename(slug)}`;
|
|
777
|
+
const to = resolveHtmlPath(root, target)!;
|
|
778
|
+
return withVaultLock(root, async () => {
|
|
779
|
+
if (!(await exists(from))) return { ok: false, reason: "missing" };
|
|
780
|
+
if (from === to) return { ok: true, slug };
|
|
781
|
+
if (await exists(to)) return { ok: false, reason: "collision" };
|
|
782
|
+
await fs.rename(from, to);
|
|
783
|
+
await repointBacklinks(root, new Map([[slug, target]]));
|
|
784
|
+
return { ok: true, slug: target };
|
|
785
|
+
});
|
|
786
|
+
}
|
|
787
|
+
|
|
788
|
+
/**
|
|
789
|
+
* Rename a standalone HTML artifact in place. The new name sets the file name
|
|
790
|
+
* (slugified, extension kept) and the title the workspace shows, which lives
|
|
791
|
+
* in a leading `<!-- --- title: ... --- -->` comment or in `<title>`; a file
|
|
792
|
+
* with neither takes its title from the file name. Links follow the file.
|
|
793
|
+
*/
|
|
794
|
+
export async function renameHtmlArtifact(root: string, slug: string, name: string): Promise<VaultMutationResult> {
|
|
795
|
+
const from = resolveHtmlPath(root, slug);
|
|
796
|
+
const title = name.trim().replace(/\.html?$/i, "").trim();
|
|
797
|
+
if (from === null || title === "") return { ok: false, reason: "invalid" };
|
|
798
|
+
const ext = /\.html?$/i.exec(slug)![0];
|
|
799
|
+
const parent = slug.split("/").slice(0, -1).join("/");
|
|
800
|
+
const target = [...(parent === "" ? [] : [parent]), `${slugify(title)}${ext}`].join("/");
|
|
801
|
+
const to = resolveHtmlPath(root, target)!;
|
|
802
|
+
return withVaultLock(root, async () => {
|
|
803
|
+
let text: string;
|
|
804
|
+
try {
|
|
805
|
+
text = await fs.readFile(from, "utf8");
|
|
806
|
+
} catch {
|
|
807
|
+
return { ok: false, reason: "missing" };
|
|
808
|
+
}
|
|
809
|
+
if (from !== to && await exists(to)) return { ok: false, reason: "collision" };
|
|
810
|
+
await fs.writeFile(from, retitleHtml(text, title), "utf8");
|
|
811
|
+
if (from === to) return { ok: true, slug };
|
|
812
|
+
await fs.rename(from, to);
|
|
813
|
+
await repointBacklinks(root, new Map([[slug, target]]));
|
|
814
|
+
return { ok: true, slug: target };
|
|
815
|
+
});
|
|
816
|
+
}
|
|
817
|
+
|
|
818
|
+
/** Set the title `parseHtmlArtifact` reads, leaving every other byte alone. */
|
|
819
|
+
function retitleHtml(text: string, title: string): string {
|
|
820
|
+
const comment = /<!--[\s\S]*?---\n([\s\S]*?)\n---[\s\S]*?-->/m.exec(text);
|
|
821
|
+
if (comment !== null && /^title:.*$/m.test(comment[1]!)) {
|
|
822
|
+
// `-->` would close the comment early.
|
|
823
|
+
const fields = comment[1]!.replace(/^title:.*$/m, () => `title: ${quoteField(title.replace(/-->/g, "->"))}`);
|
|
824
|
+
return text.slice(0, comment.index) + comment[0].replace(comment[1]!, () => fields) + text.slice(comment.index + comment[0].length);
|
|
825
|
+
}
|
|
826
|
+
const escaped = title.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
|
|
827
|
+
return text.replace(/(<title\b[^>]*>)[\s\S]*?(<\/title>)/i, (_, open: string, close: string) => `${open}${escaped}${close}`);
|
|
828
|
+
}
|
|
829
|
+
|
|
830
|
+
/** Permanently delete one standalone HTML artifact. */
|
|
831
|
+
export async function deleteHtmlArtifact(root: string, slug: string): Promise<VaultMutationResult> {
|
|
832
|
+
const path = resolveHtmlPath(root, slug);
|
|
833
|
+
if (path === null) return { ok: false, reason: "invalid" };
|
|
834
|
+
return withVaultLock(root, async () => {
|
|
835
|
+
if (!(await exists(path))) return { ok: false, reason: "missing" };
|
|
836
|
+
await fs.unlink(path);
|
|
837
|
+
return { ok: true };
|
|
838
|
+
});
|
|
839
|
+
}
|
|
840
|
+
|
|
767
841
|
/** Permanently delete one note. */
|
|
768
842
|
export async function deleteNote(root: string, slug: string): Promise<VaultMutationResult> {
|
|
769
843
|
const path = resolveNotePath(root, slug);
|
|
@@ -789,12 +863,12 @@ export async function renameFolder(root: string, folder: string, name: string):
|
|
|
789
863
|
if (!(await isDirectory(from))) return { ok: false, reason: "missing" };
|
|
790
864
|
if (from !== to && await exists(to)) return { ok: false, reason: "collision" };
|
|
791
865
|
if (from === to) return { ok: true, path: target };
|
|
792
|
-
// Every note under the folder changes slug, so the whole
|
|
793
|
-
// backlinks move with it — collected before the rename, while
|
|
794
|
-
// paths still exist.
|
|
866
|
+
// Every note and HTML file under the folder changes slug, so the whole
|
|
867
|
+
// subtree's backlinks move with it — collected before the rename, while
|
|
868
|
+
// the old paths still exist. HTML slugs keep their extension.
|
|
795
869
|
const moved = (await listNoteFiles(root))
|
|
796
|
-
.filter((f) =>
|
|
797
|
-
.map((f) => f.slice(0, -".md".length));
|
|
870
|
+
.filter((f) => f.startsWith(`${folder}/`))
|
|
871
|
+
.map((f) => isMarkdown(f) ? f.slice(0, -".md".length) : f);
|
|
798
872
|
await fs.rename(from, to);
|
|
799
873
|
await moveHistoryFolder(root, folder, target);
|
|
800
874
|
await repointBacklinks(root, new Map(moved.map((s) => [s, `${target}/${s.slice(folder.length + 1)}`])));
|
package/src/core/view/tree.ts
CHANGED
|
@@ -273,8 +273,8 @@ export function treeRows(model: GraphModel, state: TreeState): TreeRow[] {
|
|
|
273
273
|
export function treeEmptyHint(model: GraphModel): string | null {
|
|
274
274
|
const vault = model.nodes.find((n) => n.kind === "vault");
|
|
275
275
|
if (!vault) return null;
|
|
276
|
-
// no
|
|
277
|
-
const hasNotes = model.nodes.some((n) => n.kind === "note");
|
|
276
|
+
// nothing in the vault (no note, HTML file or folder) and no repository
|
|
277
|
+
const hasNotes = model.nodes.some((n) => n.kind === "note" || n.id.startsWith("artifact:") || n.id.startsWith("vfolder:"));
|
|
278
278
|
const hasRepo = model.nodes.some((n) => n.kind === "repository");
|
|
279
279
|
if (!hasNotes && !hasRepo) return "no notes yet — add one with the weave_note tool";
|
|
280
280
|
return null;
|
package/src/web/client/api.ts
CHANGED
|
@@ -411,6 +411,7 @@ function mutation(fetchImpl: FetchLike, url: string, method: string, body?: unkn
|
|
|
411
411
|
|
|
412
412
|
const noteUrl = (slug: string, suffix = ""): string => `/api/note/${encodeURIComponent(slug)}${suffix}`;
|
|
413
413
|
const folderUrl = (path: string, suffix = ""): string => `/api/folder/${path.split("/").map(encodeURIComponent).join("/")}${suffix}`;
|
|
414
|
+
const artifactUrl = (path: string, suffix = ""): string => `/api/artifact/${path.split("/").map(encodeURIComponent).join("/")}${suffix}`;
|
|
414
415
|
|
|
415
416
|
/**
|
|
416
417
|
* `POST /api/note/:slug` — save the note's Markdown body. No revision
|
|
@@ -421,6 +422,9 @@ export const saveNote = (fetchImpl: FetchLike, slug: string, body: string, raw =
|
|
|
421
422
|
mutation(fetchImpl, noteUrl(slug), "POST", raw ? { body, raw: true } : { body });
|
|
422
423
|
export const renameNote = (fetchImpl: FetchLike, slug: string, name: string) => mutation(fetchImpl, noteUrl(slug, "/rename"), "POST", { name });
|
|
423
424
|
export const moveNote = (fetchImpl: FetchLike, slug: string, folder: string | null) => mutation(fetchImpl, noteUrl(slug, "/move"), "POST", { folder });
|
|
425
|
+
export const moveArtifact = (fetchImpl: FetchLike, slug: string, folder: string | null) => mutation(fetchImpl, artifactUrl(slug, "/move"), "POST", { folder });
|
|
426
|
+
export const renameArtifact = (fetchImpl: FetchLike, slug: string, name: string) => mutation(fetchImpl, artifactUrl(slug, "/rename"), "POST", { name });
|
|
427
|
+
export const deleteArtifact = (fetchImpl: FetchLike, slug: string) => mutation(fetchImpl, artifactUrl(slug), "DELETE");
|
|
424
428
|
export const deleteNote = (fetchImpl: FetchLike, slug: string) => mutation(fetchImpl, noteUrl(slug), "DELETE");
|
|
425
429
|
export const createFolder = (fetchImpl: FetchLike, path: string) => mutation(fetchImpl, folderUrl(path), "POST");
|
|
426
430
|
export const renameFolder = (fetchImpl: FetchLike, path: string, name: string) => mutation(fetchImpl, folderUrl(path, "/rename"), "POST", { name });
|