pi-weave 0.1.7 → 0.1.9
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 +123 -37
- package/package.json +17 -5
- package/skills/weave-notepad/SKILL.md +13 -4
- package/src/core/cache/workspace.ts +466 -0
- package/src/core/concurrency.ts +36 -0
- package/src/core/frontmatter.ts +270 -23
- package/src/core/git.ts +19 -0
- package/src/core/graph/build.ts +97 -5
- package/src/core/graph/current.ts +41 -28
- package/src/core/graph/mentions.ts +170 -0
- package/src/core/graph/model.ts +39 -0
- package/src/core/graph/wikilinks.ts +5 -1
- package/src/core/index.ts +14 -0
- package/src/core/openInEditor.ts +69 -0
- package/src/core/paths.ts +7 -0
- package/src/core/sessions.ts +929 -0
- package/src/core/summaries.ts +1 -19
- package/src/core/types.ts +40 -0
- package/src/core/vault.ts +739 -57
- package/src/core/view/cluster.ts +262 -0
- package/src/core/view/detail.ts +118 -0
- package/src/core/view/focus.ts +109 -0
- package/src/core/view/health.ts +156 -0
- package/src/core/view/index.ts +15 -0
- package/src/core/view/links.ts +105 -0
- package/src/core/view/time.ts +47 -0
- package/src/core/view/tree.ts +269 -0
- package/src/core/view/types.ts +39 -0
- package/src/core/workspace.ts +3 -3
- package/src/pi/index.ts +248 -48
- package/src/pi/sessionScan.ts +104 -0
- package/src/pi/summarize.ts +24 -4
- package/src/pi/tools/noteTool.ts +13 -4
- package/src/pi/viewer/tui/branding.ts +8 -7
- package/src/pi/viewer/tui/explorer.ts +5 -3
- package/src/pi/viewer/tui/model.ts +46 -667
- package/src/pi/viewer/tui/openNote.ts +7 -56
- package/src/pi/viewer/tui/surface/explore.ts +4 -2
- package/src/pi/viewer/web/run.ts +331 -0
- package/src/web/client/api.dom.ts +40 -0
- package/src/web/client/api.ts +472 -0
- package/src/web/client/bootstrap.ts +58 -0
- package/src/web/client/context/context.model.ts +313 -0
- package/src/web/client/dist/app.js +764 -0
- package/src/web/client/graph/Graph.tsx +209 -0
- package/src/web/client/graph/column.model.ts +372 -0
- package/src/web/client/graph/dynamics.ts +176 -0
- package/src/web/client/graph/graph.model.ts +546 -0
- package/src/web/client/graph/positions.ts +380 -0
- package/src/web/client/graph/project.ts +153 -0
- package/src/web/client/graph/renderer.dom.ts +52 -0
- package/src/web/client/graph/renderer.ts +339 -0
- package/src/web/client/graph/scheme.ts +44 -0
- package/src/web/client/live.model.ts +275 -0
- package/src/web/client/live.ts +151 -0
- package/src/web/client/main.tsx +27 -0
- package/src/web/client/note/Editor.tsx +102 -0
- package/src/web/client/note/Note.tsx +113 -0
- package/src/web/client/note/editor.controller.ts +151 -0
- package/src/web/client/note/editor.model.ts +636 -0
- package/src/web/client/note/note.model.ts +738 -0
- package/src/web/client/search/SearchPalette.tsx +105 -0
- package/src/web/client/search/search.model.ts +588 -0
- package/src/web/client/search/search.ts +107 -0
- package/src/web/client/selection.storage.ts +69 -0
- package/src/web/client/shell/Columns.tsx +161 -0
- package/src/web/client/shell/ContextRail.tsx +87 -0
- package/src/web/client/shell/Divider.tsx +44 -0
- package/src/web/client/shell/FocusTrap.tsx +56 -0
- package/src/web/client/shell/Header.tsx +64 -0
- package/src/web/client/shell/HelpOverlay.tsx +70 -0
- package/src/web/client/shell/Shell.tsx +210 -0
- package/src/web/client/shell/StatusBar.tsx +28 -0
- package/src/web/client/shell/cssvars.ts +70 -0
- package/src/web/client/shell/drag.model.ts +170 -0
- package/src/web/client/shell/focus.model.ts +100 -0
- package/src/web/client/shell/keys.model.ts +453 -0
- package/src/web/client/shell/keys.ts +59 -0
- package/src/web/client/shell/layout.model.ts +526 -0
- package/src/web/client/shell/shell.model.ts +333 -0
- package/src/web/client/shell/theme.ts +490 -0
- package/src/web/client/shell/viewport.ts +29 -0
- package/src/web/client/state.ts +89 -0
- package/src/web/client/tree/Tree.tsx +141 -0
- package/src/web/client/tree/tree.model.ts +674 -0
- package/src/web/client/workspace.ts +278 -0
- package/src/web/server/page.ts +258 -0
- package/src/web/server/routes.ts +987 -0
- package/src/web/server/security.ts +361 -0
- package/src/web/server/server.ts +275 -0
- package/src/web/server/sse.ts +321 -0
- package/src/web/server/watcher.ts +507 -0
- package/src/web/shared/graph.ts +213 -0
- package/src/web/shared/layout.ts +314 -0
- package/src/web/shared/logo.ts +11 -0
- package/src/web/shared/metrics.ts +136 -0
- package/src/web/shared/view.ts +200 -0
- package/src/web/shared/wire.ts +358 -0
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `mentions` edges: a note body that names a repository path → note → node
|
|
3
|
+
* (weave-workspace §4.4, §15.5).
|
|
4
|
+
*
|
|
5
|
+
* `EdgeKind` has declared `"mentions"` since the graph model was written and
|
|
6
|
+
* `buildGraph` had never emitted one, so every consumer carried a branch that
|
|
7
|
+
* could not be taken and the legend advertised a relationship the product did
|
|
8
|
+
* not have. This module is the implementation side of that choice.
|
|
9
|
+
*
|
|
10
|
+
* Pure and I/O-free, like its sibling `wikilinks.ts`: a regex over a string
|
|
11
|
+
* and a lookup in a map built from the repo index. No parsing, no LLM, no
|
|
12
|
+
* new dependency (§4.4 is explicit about all three).
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import type { RepoStructure } from "../types";
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* A repo-relative path candidate: two or more `/`-joined segments of word
|
|
19
|
+
* characters, dots and dashes.
|
|
20
|
+
*
|
|
21
|
+
* The lookbehind is the part doing real work. `(?<![\w/.@:-])` refuses to
|
|
22
|
+
* start a match immediately after a character that would make this the *tail*
|
|
23
|
+
* of something longer, which is what keeps `https://github.com/org/repo/src`
|
|
24
|
+
* and `user@host.com/path` from being read as repo paths. Without it every
|
|
25
|
+
* URL in every note becomes a candidate, and since candidates are only kept
|
|
26
|
+
* when they hit a real node the damage would be invisible until the day
|
|
27
|
+
* someone's repo happened to contain `org/repo`.
|
|
28
|
+
*
|
|
29
|
+
* At least one slash is required, so prose words are not candidates. Trailing
|
|
30
|
+
* sentence punctuation is stripped by {@link extractPathMentions} rather than
|
|
31
|
+
* excluded here, because `src/core.` at the end of a sentence should match
|
|
32
|
+
* `src/core` and a regex that refuses the dot cannot also accept
|
|
33
|
+
* `src/core/vault.ts`.
|
|
34
|
+
*/
|
|
35
|
+
const PATH_RE = /(?<![\w/.@:-])([\w.-]+(?:\/[\w.-]+)+)/g;
|
|
36
|
+
|
|
37
|
+
/** Punctuation that ends a sentence rather than a path. */
|
|
38
|
+
const TRAILING_PUNCT_RE = /[.,;:!?)\]}'"]+$/;
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Repo path → graph node id, for every path-addressable node the builder
|
|
42
|
+
* emits.
|
|
43
|
+
*
|
|
44
|
+
* Deliberately built from {@link RepoStructure} rather than by scanning the
|
|
45
|
+
* finished node list: both are derived from the same arrays, so mirroring the
|
|
46
|
+
* id construction here keeps the invariant that *every* value in this map is
|
|
47
|
+
* a node that exists, while reading it back off `GraphNode.detail.path` would
|
|
48
|
+
* mean re-parsing a display-only field (§4.2) to recover something structured
|
|
49
|
+
* we already had.
|
|
50
|
+
*/
|
|
51
|
+
export type PathIndex = ReadonlyMap<string, string>;
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Index the path-bearing nodes of a repository, exactly mirroring the ids
|
|
55
|
+
* `buildRepositorySide` emits.
|
|
56
|
+
*
|
|
57
|
+
* Insertion order is modules, then entry points, then packages, then `.okf`
|
|
58
|
+
* files. Later kinds do **not** overwrite earlier ones: a path that is both a
|
|
59
|
+
* module and an entry point resolves to the module, which is the coarser and
|
|
60
|
+
* more navigable of the two. The choice only has to be *stable*, and pinning
|
|
61
|
+
* it here is what makes it so.
|
|
62
|
+
*
|
|
63
|
+
* `(root)` is skipped. It is a display label for "files directly in the repo
|
|
64
|
+
* root", not a path, and no note body can spell it in a way that reaches
|
|
65
|
+
* here anyway.
|
|
66
|
+
*/
|
|
67
|
+
export function buildPathIndex(structure: RepoStructure): PathIndex {
|
|
68
|
+
const index = new Map<string, string>();
|
|
69
|
+
const put = (path: string, id: string): void => {
|
|
70
|
+
if (path.length > 0 && !index.has(path)) index.set(path, id);
|
|
71
|
+
};
|
|
72
|
+
for (const mod of structure.modules) {
|
|
73
|
+
if (mod.path === "(root)") continue;
|
|
74
|
+
put(mod.path, `module:${mod.path}`);
|
|
75
|
+
}
|
|
76
|
+
for (const entry of structure.entryPoints) put(entry, `entryPoint:${entry}`);
|
|
77
|
+
for (const pkg of structure.packages) put(pkg.manifestPath, `package:${pkg.manifestPath}`);
|
|
78
|
+
// `.okf` files are addressed by their repo-relative path (".okf/…"), while
|
|
79
|
+
// their node ids drop the prefix — the same rename `buildRepositorySide`
|
|
80
|
+
// performs.
|
|
81
|
+
for (const file of structure.okFiles ?? []) {
|
|
82
|
+
put(file, `okf:${file.replace(/^\.okf\//, "")}`);
|
|
83
|
+
}
|
|
84
|
+
return index;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Every distinct path-like token in a note body, in order of first
|
|
89
|
+
* appearance, with trailing sentence punctuation and trailing slashes
|
|
90
|
+
* removed.
|
|
91
|
+
*
|
|
92
|
+
* Exported for its own tests: the regex is the part of this feature most
|
|
93
|
+
* likely to be wrong, and testing it through `buildGraph` alone would mean
|
|
94
|
+
* every false-positive case needed a whole repo fixture.
|
|
95
|
+
*/
|
|
96
|
+
export function extractPathMentions(body: string): string[] {
|
|
97
|
+
const out: string[] = [];
|
|
98
|
+
const seen = new Set<string>();
|
|
99
|
+
for (const match of body.matchAll(PATH_RE)) {
|
|
100
|
+
// `?? ""` rather than an `undefined` guard: the group is mandatory, so it
|
|
101
|
+
// always participates and a guard would be a branch no input can take.
|
|
102
|
+
// An empty string falls out at the `includes("/")` check just below.
|
|
103
|
+
const path = (match[1] ?? "").replace(TRAILING_PUNCT_RE, "").replace(/\/+$/, "");
|
|
104
|
+
if (!path.includes("/") || seen.has(path)) continue;
|
|
105
|
+
seen.add(path);
|
|
106
|
+
out.push(path);
|
|
107
|
+
}
|
|
108
|
+
return out;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Resolve the repo nodes a note body mentions, as node ids in first-appearance
|
|
113
|
+
* order, deduplicated.
|
|
114
|
+
*
|
|
115
|
+
* ## Granularity: exact match, else the longest **enclosing** module
|
|
116
|
+
*
|
|
117
|
+
* This is the decision that keeps the edge count sane, and it runs in exactly
|
|
118
|
+
* one direction.
|
|
119
|
+
*
|
|
120
|
+
* *Never downward.* A note saying "see `src/core`" produces **one** edge, to
|
|
121
|
+
* `module:src/core`. It does not fan out to the files beneath it. Expanding a
|
|
122
|
+
* prefix downward is how a three-word sentence turns into forty edges and the
|
|
123
|
+
* graph column becomes unreadable — the explicit failure mode §4.4 warns
|
|
124
|
+
* about.
|
|
125
|
+
*
|
|
126
|
+
* *Upward, at most one step.* A note saying `src/core/vault.ts` is making a
|
|
127
|
+
* genuine reference, but most repository files are not graph nodes (only
|
|
128
|
+
* modules, entry points, packages and `.okf` files are), so an exact-match-only
|
|
129
|
+
* rule would silently drop the majority of real mentions and leave the feature
|
|
130
|
+
* looking broken. The fallback walks the path's ancestors longest-first and
|
|
131
|
+
* takes the first that is a **module**, so the mention lands on the nearest
|
|
132
|
+
* indexed container: `src/core/graph/build.ts` → `module:src/core`.
|
|
133
|
+
*
|
|
134
|
+
* The asymmetry is what makes this safe. Each distinct mentioned path resolves
|
|
135
|
+
* to **at most one** node, so the edges a note can contribute are bounded by
|
|
136
|
+
* the number of distinct paths it names, and the collapse only ever reduces
|
|
137
|
+
* that further — two files in the same module fold into a single edge because
|
|
138
|
+
* the result is deduplicated by target.
|
|
139
|
+
*
|
|
140
|
+
* The ancestor walk is restricted to modules on purpose: an entry point or a
|
|
141
|
+
* manifest is a *file*, and "this note mentions a path inside `package.json`"
|
|
142
|
+
* is not a relationship that exists.
|
|
143
|
+
*/
|
|
144
|
+
export function resolveMentions(body: string, paths: PathIndex): string[] {
|
|
145
|
+
const out: string[] = [];
|
|
146
|
+
const seen = new Set<string>();
|
|
147
|
+
for (const path of extractPathMentions(body)) {
|
|
148
|
+
const id = paths.get(path) ?? enclosingModule(path, paths);
|
|
149
|
+
if (id === null || id === undefined || seen.has(id)) continue;
|
|
150
|
+
seen.add(id);
|
|
151
|
+
out.push(id);
|
|
152
|
+
}
|
|
153
|
+
return out;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* The nearest ancestor directory of `path` that is an indexed module, or
|
|
158
|
+
* `null`. Longest first, so `src/core/graph/x.ts` prefers `src/core/graph`
|
|
159
|
+
* over `src/core` when both are modules.
|
|
160
|
+
*/
|
|
161
|
+
function enclosingModule(path: string, paths: PathIndex): string | null {
|
|
162
|
+
const segments = path.split("/");
|
|
163
|
+
for (let cut = segments.length - 1; cut > 0; cut--) {
|
|
164
|
+
const ancestor = segments.slice(0, cut).join("/");
|
|
165
|
+
const id = paths.get(ancestor);
|
|
166
|
+
// Only a module may absorb a mention of something inside it.
|
|
167
|
+
if (id !== undefined && id.startsWith("module:")) return id;
|
|
168
|
+
}
|
|
169
|
+
return null;
|
|
170
|
+
}
|
package/src/core/graph/model.ts
CHANGED
|
@@ -60,4 +60,43 @@ export interface GraphModel {
|
|
|
60
60
|
staleness: StalenessReport | null;
|
|
61
61
|
nodes: GraphNode[];
|
|
62
62
|
edges: GraphEdge[];
|
|
63
|
+
/**
|
|
64
|
+
* slug → wiki-link targets that resolved to no note in the graph
|
|
65
|
+
* (weave-workspace §4.2).
|
|
66
|
+
*
|
|
67
|
+
* A `[[target]]` that matches nothing is not an error — it is Obsidian's
|
|
68
|
+
* ghost node, the affordance that offers to create the missing note. The
|
|
69
|
+
* builder used to count these and throw the names away, leaving
|
|
70
|
+
* `detail["dangling links"] = "3"` as the only trace; a display string is
|
|
71
|
+
* not something a UI can navigate.
|
|
72
|
+
*
|
|
73
|
+
* Lives here rather than on {@link GraphNode} because `detail` is
|
|
74
|
+
* display-only by contract and must not grow structure (§4.2 is explicit
|
|
75
|
+
* about preferring the model-level map). Notes with nothing unresolved are
|
|
76
|
+
* absent rather than present-and-empty, so the common case costs no bytes.
|
|
77
|
+
*
|
|
78
|
+
* Keyed by **slug**, not by node id: the consumers of this map (the note
|
|
79
|
+
* column, the wire payload's `dangling` field) speak slugs, and `note:` is
|
|
80
|
+
* a graph-internal prefix.
|
|
81
|
+
*
|
|
82
|
+
* Insertion order follows note order and each target list follows
|
|
83
|
+
* first-appearance order in the body, so the JSON stays byte-stable for
|
|
84
|
+
* unchanged inputs.
|
|
85
|
+
*/
|
|
86
|
+
danglingLinks: Record<string, string[]>;
|
|
87
|
+
/**
|
|
88
|
+
* Content fingerprint of the note bodies the graph was built from
|
|
89
|
+
* (SHA-256 over `slug\0body-digest` pairs in slug order, truncated to
|
|
90
|
+
* 128 bits of hex).
|
|
91
|
+
*
|
|
92
|
+
* Exists so the wire payload's stamp — the digest the ETag and the SSE
|
|
93
|
+
* dedupe both key on — is sensitive to a **body-only** edit. The payload
|
|
94
|
+
* itself carries only display excerpts of a note (`detail.preview`, the
|
|
95
|
+
* first 240 flattened characters), so an edit below the fold with
|
|
96
|
+
* unchanged front matter used to leave the payload byte-identical: the
|
|
97
|
+
* frame was deduped away and the open note stayed stale until a manual
|
|
98
|
+
* reload. The digest is a function of content only, so it keeps the
|
|
99
|
+
* byte-determinism contract.
|
|
100
|
+
*/
|
|
101
|
+
contentDigest: string;
|
|
63
102
|
}
|
|
@@ -12,6 +12,10 @@ const WIKILINK_RE = /\[\[([^\][|]+)(?:\|[^\]]*)?\]\]/g;
|
|
|
12
12
|
* `[[some-note]]` and aliased `[[Some Note|alias]]` (alias ignored for
|
|
13
13
|
* linking). Targets are slugified so `[[Release Plan]]` matches the note
|
|
14
14
|
* `release-plan`. Duplicates are removed, order of first appearance kept.
|
|
15
|
+
*
|
|
16
|
+
* Path separators survive: a nested note's slug is its path relative to
|
|
17
|
+
* `notes/` (`sessions/foo`), so `[[sessions/foo]]` targets the session note,
|
|
18
|
+
* not a flattened name. Each path segment is slugified independently.
|
|
15
19
|
*/
|
|
16
20
|
export function extractWikilinks(body: string): string[] {
|
|
17
21
|
const out: string[] = [];
|
|
@@ -19,7 +23,7 @@ export function extractWikilinks(body: string): string[] {
|
|
|
19
23
|
for (const match of body.matchAll(WIKILINK_RE)) {
|
|
20
24
|
const raw = (match[1] ?? "").trim();
|
|
21
25
|
if (raw.length === 0) continue;
|
|
22
|
-
const slug = slugify(
|
|
26
|
+
const slug = raw.split("/").map((part) => slugify(part)).join("/");
|
|
23
27
|
if (seen.has(slug)) continue;
|
|
24
28
|
seen.add(slug);
|
|
25
29
|
out.push(slug);
|
package/src/core/index.ts
CHANGED
|
@@ -13,13 +13,27 @@ export * from "./git";
|
|
|
13
13
|
export * from "./vault";
|
|
14
14
|
export * from "./repoIndex";
|
|
15
15
|
export * from "./summaries";
|
|
16
|
+
export * from "./sessions";
|
|
17
|
+
export * from "./concurrency";
|
|
16
18
|
export * from "./workspace";
|
|
19
|
+
export * from "./openInEditor";
|
|
17
20
|
export * from "./graph/model";
|
|
18
21
|
export * from "./graph/wikilinks";
|
|
22
|
+
export * from "./view";
|
|
19
23
|
export { buildGraph, dataTimestamp, DEFAULT_MAX_NOTES, type BuildGraphInput } from "./graph/build";
|
|
20
24
|
export {
|
|
21
25
|
buildCurrentGraph,
|
|
22
26
|
readNoteForView,
|
|
23
27
|
readOkfFileForView,
|
|
28
|
+
readRepositorySide,
|
|
24
29
|
type ViewNote,
|
|
25
30
|
} from "./graph/current";
|
|
31
|
+
export {
|
|
32
|
+
classifyPath,
|
|
33
|
+
DEFAULT_STALENESS_TTL_MS,
|
|
34
|
+
WorkspaceCache,
|
|
35
|
+
type CacheStats,
|
|
36
|
+
type InvalidationScope,
|
|
37
|
+
type WorkspaceCacheOptions,
|
|
38
|
+
type WorkspaceSnapshot,
|
|
39
|
+
} from "./cache/workspace";
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Open a vault note in the user's editor.
|
|
3
|
+
*
|
|
4
|
+
* Originally `src/pi/viewer/tui/openNote.ts`, promoted into core because the
|
|
5
|
+
* browser workspace needs it too and `src/web/server/**` may not import
|
|
6
|
+
* `src/pi` (weave-workspace §2). It belongs here on the merits regardless:
|
|
7
|
+
* it is a pure fan-out over `core/vault` plus one `execFile`, with no
|
|
8
|
+
* harness and no terminal concept in it, and the same reasoning that put
|
|
9
|
+
* `readNoteForView` in core applies unchanged.
|
|
10
|
+
*
|
|
11
|
+
* The old path re-exports these two names, so no TUI file changed.
|
|
12
|
+
*
|
|
13
|
+
* Security: the slug is validated against the vault (traversal-safe) before
|
|
14
|
+
* any shell-out, and the command is invoked with `execFile`, not a shell, so
|
|
15
|
+
* neither the slug nor a `$EDITOR` containing metacharacters can compose a
|
|
16
|
+
* command. `$EDITOR` itself is trusted — it is the user's own configuration,
|
|
17
|
+
* and a hostile one already has whatever the user has.
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
import { execFile } from "node:child_process";
|
|
21
|
+
import { platform } from "node:os";
|
|
22
|
+
import { promisify } from "node:util";
|
|
23
|
+
import { getNote, resolveNotePath } from "./vault";
|
|
24
|
+
|
|
25
|
+
const execFileAsync = promisify(execFile);
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* The OS command used to open a note file in the user's editor. Respects
|
|
29
|
+
* $EDITOR / $VISUAL (which may carry args, e.g. "code --wait"); falls back
|
|
30
|
+
* to the platform default opener. `env` and `os` are injectable so the
|
|
31
|
+
* mapping is unit-testable on any host without stubbing globals.
|
|
32
|
+
*/
|
|
33
|
+
export function openNoteCommand(
|
|
34
|
+
notePath: string,
|
|
35
|
+
env: NodeJS.ProcessEnv = process.env,
|
|
36
|
+
os: NodeJS.Platform = platform(),
|
|
37
|
+
): { command: string; args: string[] } {
|
|
38
|
+
const editor = (env.EDITOR || env.VISUAL || "").trim();
|
|
39
|
+
if (editor) {
|
|
40
|
+
// editor is a trimmed non-empty string, so split+filter always yields a
|
|
41
|
+
// first segment — assert it rather than leave a dead `?? ""` branch.
|
|
42
|
+
const parts = editor.split(/\s+/).filter(Boolean);
|
|
43
|
+
const command = parts[0] as string;
|
|
44
|
+
return { command, args: [...parts.slice(1), notePath] };
|
|
45
|
+
}
|
|
46
|
+
if (os === "darwin") return { command: "open", args: [notePath] };
|
|
47
|
+
if (os === "win32") return { command: "cmd", args: ["/c", "start", "", notePath] };
|
|
48
|
+
return { command: "xdg-open", args: [notePath] };
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Open a note in the OS editor. The slug is validated against the vault
|
|
53
|
+
* (traversal-safe) before any shell-out; returns false when the note does
|
|
54
|
+
* not exist or the slug is unsafe.
|
|
55
|
+
*/
|
|
56
|
+
export async function openNoteInEditor(
|
|
57
|
+
vaultRoot: string,
|
|
58
|
+
slug: string,
|
|
59
|
+
openCommand: (notePath: string) => { command: string; args: string[] } = openNoteCommand,
|
|
60
|
+
): Promise<boolean> {
|
|
61
|
+
const path = resolveNotePath(vaultRoot, slug);
|
|
62
|
+
if (path === null) return false; // traversal-safe
|
|
63
|
+
const note = await getNote(vaultRoot, slug);
|
|
64
|
+
if (note === null) return false;
|
|
65
|
+
const { command, args } = openCommand(path);
|
|
66
|
+
if (!command) return false;
|
|
67
|
+
await execFileAsync(command, args);
|
|
68
|
+
return true;
|
|
69
|
+
}
|
package/src/core/paths.ts
CHANGED
|
@@ -14,6 +14,13 @@ import { join } from "node:path";
|
|
|
14
14
|
export const OKF_DIR = ".okf";
|
|
15
15
|
export const OKF_MANIFEST = "okf.json";
|
|
16
16
|
export const NOTES_DIR = "notes";
|
|
17
|
+
/**
|
|
18
|
+
* Vault collection for generated session memory (docs/session-scan.md).
|
|
19
|
+
* A sibling of `notes/` rather than a subdirectory of it: session notes are
|
|
20
|
+
* machine-derived memory, not hand-curated knowledge, so they stay out of
|
|
21
|
+
* the note graph's flat listing — and out of its slug namespace.
|
|
22
|
+
*/
|
|
23
|
+
export const SESSIONS_DIR = "sessions";
|
|
17
24
|
export const REPOSITORY_DIR = "repository";
|
|
18
25
|
export const VAULT_ENV_VAR = "PI_WEAVE_VAULT";
|
|
19
26
|
|