imprnt 0.1.1
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/CLAUDE.md +173 -0
- package/LICENSE +21 -0
- package/README.md +176 -0
- package/dist/cli.js +2339 -0
- package/dist/imp.js +2342 -0
- package/package.json +46 -0
- package/templates/_tags.md +23 -0
- package/templates/hot.md +28 -0
- package/templates/index.md +33 -0
- package/templates/log.md +8 -0
- package/templates/pointer.md +12 -0
package/dist/cli.js
ADDED
|
@@ -0,0 +1,2339 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
var __esm = (fn, res) => () => (fn && (res = fn(fn = 0)), res);
|
|
3
|
+
|
|
4
|
+
// scripts/lib/resolve.ts
|
|
5
|
+
import { existsSync, readdirSync, readFileSync, appendFileSync, writeFileSync } from "node:fs";
|
|
6
|
+
import { join } from "node:path";
|
|
7
|
+
function personResolved(vault, slug, displayName) {
|
|
8
|
+
const dir = join(vault, "people");
|
|
9
|
+
if (existsSync(join(dir, `${slug}.md`)))
|
|
10
|
+
return true;
|
|
11
|
+
if (!existsSync(dir))
|
|
12
|
+
return false;
|
|
13
|
+
const needle = displayName.toLowerCase();
|
|
14
|
+
for (const f of readdirSync(dir)) {
|
|
15
|
+
if (!f.endsWith(".md"))
|
|
16
|
+
continue;
|
|
17
|
+
const fm = readFileSync(join(dir, f), "utf8").match(/^---\r?\n([\s\S]*?)\r?\n---/)?.[1] ?? "";
|
|
18
|
+
const aliases = (fm.match(/aliases:\s*\[(.*?)\]/i)?.[1] ?? "").toLowerCase();
|
|
19
|
+
if (aliases.split(",").some((a) => a.trim().replace(/["']/g, "") === needle))
|
|
20
|
+
return true;
|
|
21
|
+
}
|
|
22
|
+
return false;
|
|
23
|
+
}
|
|
24
|
+
function flagNeedsReview(vault, line) {
|
|
25
|
+
const p = join(vault, "_needs-review.md");
|
|
26
|
+
if (!existsSync(p))
|
|
27
|
+
writeFileSync(p, `---
|
|
28
|
+
type: needs-review
|
|
29
|
+
---
|
|
30
|
+
|
|
31
|
+
# Needs review
|
|
32
|
+
|
|
33
|
+
`);
|
|
34
|
+
const want = line.replace(/\n$/, "");
|
|
35
|
+
const present = readFileSync(p, "utf8").split(/\r?\n/).some((l) => l === want);
|
|
36
|
+
if (present)
|
|
37
|
+
return;
|
|
38
|
+
appendFileSync(p, line.endsWith(`
|
|
39
|
+
`) ? line : line + `
|
|
40
|
+
`);
|
|
41
|
+
}
|
|
42
|
+
function openNeedsReview(vault) {
|
|
43
|
+
const p = join(vault, "_needs-review.md");
|
|
44
|
+
if (!existsSync(p))
|
|
45
|
+
return [];
|
|
46
|
+
return readFileSync(p, "utf8").split(/\r?\n/).filter((l) => l.trim().startsWith("- [ ]"));
|
|
47
|
+
}
|
|
48
|
+
var init_resolve = () => {};
|
|
49
|
+
|
|
50
|
+
// scripts/lib/plugins.ts
|
|
51
|
+
import { existsSync as existsSync2, readdirSync as readdirSync2, readFileSync as readFileSync2, writeFileSync as writeFileSync2, statSync } from "node:fs";
|
|
52
|
+
import { join as join2, relative, resolve, sep } from "node:path";
|
|
53
|
+
function entryFor(spec) {
|
|
54
|
+
if (spec.includes("/"))
|
|
55
|
+
return `plugins/${spec}`;
|
|
56
|
+
return `plugins/${spec}/agent.md`;
|
|
57
|
+
}
|
|
58
|
+
function specError(root, spec) {
|
|
59
|
+
const norm = spec.endsWith("/") ? spec.slice(0, -1) : spec;
|
|
60
|
+
const base = resolve(root, "plugins");
|
|
61
|
+
const target = resolve(base, norm);
|
|
62
|
+
if (target === base || !target.startsWith(base + sep)) {
|
|
63
|
+
return `invalid plugin spec "${spec}" - must name something inside plugins/`;
|
|
64
|
+
}
|
|
65
|
+
const canonical = relative(base, target).split(sep).join("/");
|
|
66
|
+
if (norm !== canonical) {
|
|
67
|
+
return `invalid plugin spec "${spec}" - use the canonical form "${canonical}" (no ./ or ..)`;
|
|
68
|
+
}
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
function canonicalSpec(root, spec) {
|
|
72
|
+
const slash = spec.indexOf("/");
|
|
73
|
+
const dirName = slash === -1 ? spec : spec.slice(0, slash);
|
|
74
|
+
const rest = slash === -1 ? "" : spec.slice(slash);
|
|
75
|
+
const base = join2(root, "plugins");
|
|
76
|
+
let entries;
|
|
77
|
+
try {
|
|
78
|
+
entries = readdirSync2(base);
|
|
79
|
+
} catch {
|
|
80
|
+
return spec;
|
|
81
|
+
}
|
|
82
|
+
const target = dirName.toLowerCase();
|
|
83
|
+
const hit = entries.find((e) => e.toLowerCase() === target);
|
|
84
|
+
return hit ? hit + rest : spec;
|
|
85
|
+
}
|
|
86
|
+
function localPath(root) {
|
|
87
|
+
return join2(root, "CLAUDE.local.md");
|
|
88
|
+
}
|
|
89
|
+
function entryExists(root, entry) {
|
|
90
|
+
const p = join2(root, entry);
|
|
91
|
+
try {
|
|
92
|
+
return statSync(p).isFile();
|
|
93
|
+
} catch {
|
|
94
|
+
return false;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
function importLine(entry) {
|
|
98
|
+
return `@${entry}`;
|
|
99
|
+
}
|
|
100
|
+
function lineTarget(rawLine) {
|
|
101
|
+
const line = rawLine.trim();
|
|
102
|
+
if (!line.startsWith("@"))
|
|
103
|
+
return "";
|
|
104
|
+
const m = line.match(/^(@\S+)/);
|
|
105
|
+
return m ? m[1] : "";
|
|
106
|
+
}
|
|
107
|
+
function listPluginDirs(root) {
|
|
108
|
+
const dir = join2(root, "plugins");
|
|
109
|
+
if (!existsSync2(dir))
|
|
110
|
+
return [];
|
|
111
|
+
return readdirSync2(dir).filter((name) => !name.startsWith("_") && !name.startsWith(".")).filter((name) => {
|
|
112
|
+
try {
|
|
113
|
+
return statSync(join2(dir, name)).isDirectory();
|
|
114
|
+
} catch {
|
|
115
|
+
return false;
|
|
116
|
+
}
|
|
117
|
+
}).sort();
|
|
118
|
+
}
|
|
119
|
+
function readLocal(root) {
|
|
120
|
+
try {
|
|
121
|
+
return readFileSync2(localPath(root), "utf8");
|
|
122
|
+
} catch {
|
|
123
|
+
return "";
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
function lineEnding(content) {
|
|
127
|
+
const crlf = content.split(`\r
|
|
128
|
+
`).length - 1;
|
|
129
|
+
const lf = content.split(`
|
|
130
|
+
`).length - 1 - crlf;
|
|
131
|
+
return crlf > lf ? `\r
|
|
132
|
+
` : `
|
|
133
|
+
`;
|
|
134
|
+
}
|
|
135
|
+
function liveImportLines(root) {
|
|
136
|
+
const out = [];
|
|
137
|
+
let inFence = false;
|
|
138
|
+
for (const raw of readLocal(root).split(/\r?\n/)) {
|
|
139
|
+
const line = raw.trim();
|
|
140
|
+
if (line.startsWith("```") || line.startsWith("~~~")) {
|
|
141
|
+
inFence = !inFence;
|
|
142
|
+
continue;
|
|
143
|
+
}
|
|
144
|
+
if (!inFence && line.startsWith("@"))
|
|
145
|
+
out.push(line);
|
|
146
|
+
}
|
|
147
|
+
return out;
|
|
148
|
+
}
|
|
149
|
+
function importTargets(root) {
|
|
150
|
+
return liveImportLines(root).map((l) => l.slice(1));
|
|
151
|
+
}
|
|
152
|
+
function isEnabled(root, name) {
|
|
153
|
+
const prefix = `@plugins/${name}/`;
|
|
154
|
+
return liveImportLines(root).some((l) => l.startsWith(prefix));
|
|
155
|
+
}
|
|
156
|
+
function addPlugin(root, spec) {
|
|
157
|
+
const invalid = specError(root, spec);
|
|
158
|
+
if (invalid)
|
|
159
|
+
return { entry: entryFor(spec), added: false, error: invalid };
|
|
160
|
+
spec = canonicalSpec(root, spec);
|
|
161
|
+
const entry = entryFor(spec);
|
|
162
|
+
if (!entryExists(root, entry)) {
|
|
163
|
+
return {
|
|
164
|
+
entry,
|
|
165
|
+
added: false,
|
|
166
|
+
error: `no such plugin entry: ${entry}; expected an agent.md or a <name>/<file>.md`
|
|
167
|
+
};
|
|
168
|
+
}
|
|
169
|
+
const line = importLine(entry);
|
|
170
|
+
const p = localPath(root);
|
|
171
|
+
try {
|
|
172
|
+
let content = existsSync2(p) ? readFileSync2(p, "utf8") : HEADER;
|
|
173
|
+
const already = content.split(/\r?\n/).some((l) => lineTarget(l) === line);
|
|
174
|
+
if (already)
|
|
175
|
+
return { entry, added: false };
|
|
176
|
+
const eol = lineEnding(content);
|
|
177
|
+
if (!content.endsWith(`
|
|
178
|
+
`))
|
|
179
|
+
content += eol;
|
|
180
|
+
content += line + eol;
|
|
181
|
+
writeFileSync2(p, content);
|
|
182
|
+
return { entry, added: true };
|
|
183
|
+
} catch (e) {
|
|
184
|
+
return { entry, added: false, error: `cannot update ${p}: ${e instanceof Error ? e.message : String(e)}` };
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
function rmPlugin(root, spec) {
|
|
188
|
+
const p = localPath(root);
|
|
189
|
+
if (!existsSync2(p))
|
|
190
|
+
return 0;
|
|
191
|
+
if (spec.endsWith("/"))
|
|
192
|
+
spec = spec.slice(0, -1);
|
|
193
|
+
const target = spec.includes("/") ? importLine(entryFor(spec)) : "";
|
|
194
|
+
const gone = spec.includes("/") ? (l) => lineTarget(l) === target : (l) => l.trim().startsWith(`@plugins/${spec}/`);
|
|
195
|
+
const content = readFileSync2(p, "utf8");
|
|
196
|
+
const lines = content.split(/\r?\n/);
|
|
197
|
+
const kept = lines.filter((l) => !gone(l));
|
|
198
|
+
const removed = lines.length - kept.length;
|
|
199
|
+
if (removed)
|
|
200
|
+
writeFileSync2(p, kept.join(lineEnding(content)));
|
|
201
|
+
return removed;
|
|
202
|
+
}
|
|
203
|
+
var HEADER = `# Personal plugin toggles (this machine only)
|
|
204
|
+
|
|
205
|
+
> Gitignored. Claude Code auto-loads this right after CLAUDE.md, so whatever you @import here is
|
|
206
|
+
> wired into the agent every session. This is the on/off switch: add a line to enable a plugin,
|
|
207
|
+
> delete or comment it to disable. Managed by \`imprnt plugin add/rm\`, or hand-edit it.
|
|
208
|
+
|
|
209
|
+
`;
|
|
210
|
+
var init_plugins = () => {};
|
|
211
|
+
|
|
212
|
+
// scripts/lib/install.ts
|
|
213
|
+
import { existsSync as existsSync3, mkdtempSync, cpSync, rmSync, readFileSync as readFileSync3, statSync as statSync2 } from "node:fs";
|
|
214
|
+
import { tmpdir } from "node:os";
|
|
215
|
+
import { join as join3, basename, resolve as resolve2 } from "node:path";
|
|
216
|
+
import { spawnSync } from "node:child_process";
|
|
217
|
+
function coreChannel(pkgRoot) {
|
|
218
|
+
try {
|
|
219
|
+
const v = JSON.parse(readFileSync3(join3(pkgRoot, "package.json"), "utf8")).version;
|
|
220
|
+
return typeof v === "string" && v.includes("-edge.") ? "edge" : "latest";
|
|
221
|
+
} catch {
|
|
222
|
+
return "latest";
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
function npmPack(spec, tmp) {
|
|
226
|
+
const pack = spawnSync("npm", ["pack", spec, "--pack-destination", tmp], { encoding: "utf8" });
|
|
227
|
+
if (pack.status !== 0) {
|
|
228
|
+
const errLines = (pack.stderr || "").split(/\r?\n/).filter((l) => /^npm (error|ERR!)/.test(l) && !l.includes("complete log")).map((l) => l.replace(/^npm (error|ERR!) ?/, "")).filter(Boolean);
|
|
229
|
+
const why = errLines.join(`
|
|
230
|
+
`) || (pack.stderr || "").trim() || (pack.error ? String(pack.error.message) : `exit ${pack.status}`);
|
|
231
|
+
return { error: `npm pack failed for ${spec}: ${why}` };
|
|
232
|
+
}
|
|
233
|
+
const tgz = pack.stdout.trim().split(/\r?\n/).filter(Boolean).pop();
|
|
234
|
+
return tgz ? { tgz } : { error: `npm pack produced no tarball for ${spec}` };
|
|
235
|
+
}
|
|
236
|
+
function installPlugin(projectRoot, name, opts = {}) {
|
|
237
|
+
const invalid = specError(projectRoot, name);
|
|
238
|
+
if (invalid)
|
|
239
|
+
return { copied: false, dest: join3(projectRoot, "plugins"), error: invalid };
|
|
240
|
+
name = canonicalSpec(projectRoot, name);
|
|
241
|
+
const dest = join3(projectRoot, "plugins", name);
|
|
242
|
+
if (existsSync3(join3(dest, "agent.md")) && !opts.force)
|
|
243
|
+
return { copied: false, dest, skipped: true };
|
|
244
|
+
if (opts.from && !existsSync3(resolve2(opts.from))) {
|
|
245
|
+
return { copied: false, dest, error: `--from path not found: ${resolve2(opts.from)}` };
|
|
246
|
+
}
|
|
247
|
+
const base = `imprnt-plugin-${name}`;
|
|
248
|
+
const specs = opts.from ? [resolve2(opts.from)] : opts.channel === "edge" ? [`${base}@edge`, base] : [base];
|
|
249
|
+
const tmp = mkdtempSync(join3(tmpdir(), "imprnt-pkg-"));
|
|
250
|
+
try {
|
|
251
|
+
let tgz;
|
|
252
|
+
let lastErr;
|
|
253
|
+
for (const spec of specs) {
|
|
254
|
+
const r = npmPack(spec, tmp);
|
|
255
|
+
if (r.tgz) {
|
|
256
|
+
tgz = r.tgz;
|
|
257
|
+
break;
|
|
258
|
+
}
|
|
259
|
+
lastErr = r.error;
|
|
260
|
+
}
|
|
261
|
+
if (!tgz)
|
|
262
|
+
return { copied: false, dest, error: lastErr ?? `npm pack produced no tarball for ${base}` };
|
|
263
|
+
const ex = spawnSync("tar", ["-xzf", join3(tmp, tgz), "-C", tmp], { encoding: "utf8" });
|
|
264
|
+
if (ex.status !== 0)
|
|
265
|
+
return { copied: false, dest, error: `tar extract failed: ${(ex.stderr || "").trim()}` };
|
|
266
|
+
const src = join3(tmp, "package");
|
|
267
|
+
if (!existsSync3(join3(src, "agent.md"))) {
|
|
268
|
+
return { copied: false, dest, error: `${name} has no agent.md — not an imprnt plugin?` };
|
|
269
|
+
}
|
|
270
|
+
if (opts.force)
|
|
271
|
+
rmSync(dest, { recursive: true, force: true });
|
|
272
|
+
cpSync(src, dest, { recursive: true, force: true, filter: (s) => basename(s) !== "package.json" });
|
|
273
|
+
return { copied: true, dest };
|
|
274
|
+
} finally {
|
|
275
|
+
rmSync(tmp, { recursive: true, force: true });
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
function purgePlugin(projectRoot, name) {
|
|
279
|
+
if (specError(projectRoot, name))
|
|
280
|
+
return false;
|
|
281
|
+
if (name.includes("/"))
|
|
282
|
+
return false;
|
|
283
|
+
if (name.startsWith("_"))
|
|
284
|
+
return false;
|
|
285
|
+
const dir = join3(projectRoot, "plugins", name);
|
|
286
|
+
if (!existsSync3(dir))
|
|
287
|
+
return false;
|
|
288
|
+
if (!statSync2(dir).isDirectory())
|
|
289
|
+
return false;
|
|
290
|
+
rmSync(dir, { recursive: true, force: true });
|
|
291
|
+
return true;
|
|
292
|
+
}
|
|
293
|
+
var OFFICIAL;
|
|
294
|
+
var init_install = __esm(() => {
|
|
295
|
+
init_plugins();
|
|
296
|
+
OFFICIAL = ["anti-slop", "character", "whenful", "guard"];
|
|
297
|
+
});
|
|
298
|
+
|
|
299
|
+
// scripts/lib/roots.ts
|
|
300
|
+
import { existsSync as existsSync4 } from "node:fs";
|
|
301
|
+
import { dirname, join as join4 } from "node:path";
|
|
302
|
+
function projectRoot(start = process.cwd()) {
|
|
303
|
+
const override = process.env.IMPRNT_ROOT ?? process.env.IMPRINT_ROOT;
|
|
304
|
+
if (override)
|
|
305
|
+
return override;
|
|
306
|
+
let dir = start;
|
|
307
|
+
for (;; ) {
|
|
308
|
+
if (existsSync4(join4(dir, "vault")) || existsSync4(join4(dir, "CLAUDE.local.md")))
|
|
309
|
+
return dir;
|
|
310
|
+
const parent = dirname(dir);
|
|
311
|
+
if (parent === dir)
|
|
312
|
+
break;
|
|
313
|
+
dir = parent;
|
|
314
|
+
}
|
|
315
|
+
return start;
|
|
316
|
+
}
|
|
317
|
+
var init_roots = () => {};
|
|
318
|
+
|
|
319
|
+
// scripts/lib/moc.ts
|
|
320
|
+
import { lstatSync, readdirSync as readdirSync3, readFileSync as readFileSync4, writeFileSync as writeFileSync3 } from "node:fs";
|
|
321
|
+
import { join as join5, relative as relative2 } from "node:path";
|
|
322
|
+
function stripCode(raw) {
|
|
323
|
+
const blank = (s) => s.replace(/[^\n]/g, " ");
|
|
324
|
+
const lines = raw.split(`
|
|
325
|
+
`);
|
|
326
|
+
const out = [];
|
|
327
|
+
let fence = null;
|
|
328
|
+
for (const line of lines) {
|
|
329
|
+
if (fence) {
|
|
330
|
+
out.push(blank(line));
|
|
331
|
+
if (line.trim().startsWith(fence))
|
|
332
|
+
fence = null;
|
|
333
|
+
continue;
|
|
334
|
+
}
|
|
335
|
+
const m = line.match(FENCE);
|
|
336
|
+
if (m) {
|
|
337
|
+
fence = m[2];
|
|
338
|
+
out.push(blank(line));
|
|
339
|
+
continue;
|
|
340
|
+
}
|
|
341
|
+
out.push(line.replace(/`[^`\n]*`/g, blank));
|
|
342
|
+
}
|
|
343
|
+
return out.join(`
|
|
344
|
+
`);
|
|
345
|
+
}
|
|
346
|
+
function stripBom(raw) {
|
|
347
|
+
return raw.charCodeAt(0) === 65279 ? raw.slice(1) : raw;
|
|
348
|
+
}
|
|
349
|
+
function frontmatter(raw) {
|
|
350
|
+
return stripBom(raw).match(/^---\r?\n([\s\S]*?)\r?\n---/)?.[1] ?? "";
|
|
351
|
+
}
|
|
352
|
+
function stripQuotes(v) {
|
|
353
|
+
const m = v.match(/^(["'])([\s\S]*)\1$/);
|
|
354
|
+
return m ? m[2] : v;
|
|
355
|
+
}
|
|
356
|
+
function fmScalar(fm, key) {
|
|
357
|
+
const lines = fm.split(/\r?\n/);
|
|
358
|
+
const keyRe = new RegExp(`^${key}:\\s*(.*)$`, "i");
|
|
359
|
+
for (const [i, line] of lines.entries()) {
|
|
360
|
+
const m = line.match(keyRe);
|
|
361
|
+
if (!m)
|
|
362
|
+
continue;
|
|
363
|
+
const rest = m[1].trim();
|
|
364
|
+
if (rest === "|" || rest === ">") {
|
|
365
|
+
const block = [];
|
|
366
|
+
for (let j = i + 1;j < lines.length; j++) {
|
|
367
|
+
const cont = lines[j].match(/^(\s+)(.*\S)\s*$/);
|
|
368
|
+
if (!cont)
|
|
369
|
+
break;
|
|
370
|
+
block.push(cont[2]);
|
|
371
|
+
}
|
|
372
|
+
return block.join(" ");
|
|
373
|
+
}
|
|
374
|
+
return stripQuotes(rest);
|
|
375
|
+
}
|
|
376
|
+
return "";
|
|
377
|
+
}
|
|
378
|
+
function fmList(fm, key) {
|
|
379
|
+
const lines = fm.split(/\r?\n/);
|
|
380
|
+
const keyRe = new RegExp(`^${key}:\\s*(.*)$`, "i");
|
|
381
|
+
for (const [i, line] of lines.entries()) {
|
|
382
|
+
const m = line.match(keyRe);
|
|
383
|
+
if (!m)
|
|
384
|
+
continue;
|
|
385
|
+
const rest = m[1].trim();
|
|
386
|
+
if (rest.startsWith("[")) {
|
|
387
|
+
const inner = rest.match(/^\[(.*)\]/)?.[1] ?? "";
|
|
388
|
+
return inner.split(",").map((s) => stripQuotes(s.trim())).filter(Boolean);
|
|
389
|
+
}
|
|
390
|
+
if (rest !== "")
|
|
391
|
+
return [];
|
|
392
|
+
const items = [];
|
|
393
|
+
for (let j = i + 1;j < lines.length; j++) {
|
|
394
|
+
const item = lines[j].match(/^\s*-(?:\s+(.*\S))?\s*$/);
|
|
395
|
+
if (!item)
|
|
396
|
+
break;
|
|
397
|
+
const v = stripQuotes((item[1] ?? "").trim());
|
|
398
|
+
if (v)
|
|
399
|
+
items.push(v);
|
|
400
|
+
}
|
|
401
|
+
return items;
|
|
402
|
+
}
|
|
403
|
+
return [];
|
|
404
|
+
}
|
|
405
|
+
function walk(vault, dir) {
|
|
406
|
+
const out = [];
|
|
407
|
+
for (const entry of readdirSync3(dir)) {
|
|
408
|
+
if (entry.startsWith(".") || entry.startsWith("_"))
|
|
409
|
+
continue;
|
|
410
|
+
const p = join5(dir, entry);
|
|
411
|
+
let st;
|
|
412
|
+
try {
|
|
413
|
+
st = lstatSync(p);
|
|
414
|
+
} catch {
|
|
415
|
+
continue;
|
|
416
|
+
}
|
|
417
|
+
if (st.isSymbolicLink())
|
|
418
|
+
continue;
|
|
419
|
+
if (st.isDirectory())
|
|
420
|
+
out.push(...walk(vault, p));
|
|
421
|
+
else if (entry.endsWith(".md")) {
|
|
422
|
+
const rel = relative2(vault, p);
|
|
423
|
+
if (!rel.includes("/") && !rel.includes("\\") && CONTROL.has(rel))
|
|
424
|
+
continue;
|
|
425
|
+
out.push(p);
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
return out;
|
|
429
|
+
}
|
|
430
|
+
function collectNotes(vault) {
|
|
431
|
+
const notes = [];
|
|
432
|
+
for (const path of walk(vault, vault)) {
|
|
433
|
+
const raw = readFileSync4(path, "utf8");
|
|
434
|
+
const fm = frontmatter(raw);
|
|
435
|
+
const rel = relative2(vault, path).split("\\").join("/");
|
|
436
|
+
const folder = rel.includes("/") ? rel.slice(0, rel.indexOf("/")) : ".";
|
|
437
|
+
const slug = rel.replace(/\.md$/, "");
|
|
438
|
+
const title = stripCode(raw).match(/^#[ \t]+(\S.*)$/m)?.[1]?.trim() ?? slug;
|
|
439
|
+
const summary = fmScalar(fm, "summary") || title;
|
|
440
|
+
notes.push({ path, folder, slug, title, summary, type: fmScalar(fm, "type"), tags: fmList(fm, "tags") });
|
|
441
|
+
}
|
|
442
|
+
return notes;
|
|
443
|
+
}
|
|
444
|
+
function folderRank(f) {
|
|
445
|
+
const i = FOLDER_ORDER.indexOf(f);
|
|
446
|
+
return i < 0 ? FOLDER_ORDER.length : i;
|
|
447
|
+
}
|
|
448
|
+
function generateIndex(vault) {
|
|
449
|
+
const notes = collectNotes(vault);
|
|
450
|
+
const byFolder = new Map;
|
|
451
|
+
for (const n of notes) {
|
|
452
|
+
if (!byFolder.has(n.folder))
|
|
453
|
+
byFolder.set(n.folder, []);
|
|
454
|
+
byFolder.get(n.folder).push(n);
|
|
455
|
+
}
|
|
456
|
+
const folders = [...byFolder.keys()].sort((a, b) => folderRank(a) - folderRank(b) || a.localeCompare(b));
|
|
457
|
+
const lines = [
|
|
458
|
+
"---",
|
|
459
|
+
"type: index",
|
|
460
|
+
"---",
|
|
461
|
+
"",
|
|
462
|
+
"# Index",
|
|
463
|
+
"",
|
|
464
|
+
`> Generated by \`imprnt check\` — do not edit by hand. ${notes.length} notes across ${folders.length} folders.`,
|
|
465
|
+
""
|
|
466
|
+
];
|
|
467
|
+
for (const f of folders) {
|
|
468
|
+
const items = byFolder.get(f).sort((a, b) => a.slug.localeCompare(b.slug));
|
|
469
|
+
lines.push(`## ${f}/ (${items.length})`, "");
|
|
470
|
+
for (const n of items) {
|
|
471
|
+
const tags = n.tags.length ? ` \`${n.tags.join("` `")}\`` : "";
|
|
472
|
+
lines.push(`- [[${n.slug}]] — ${n.summary}${tags}`);
|
|
473
|
+
}
|
|
474
|
+
lines.push("");
|
|
475
|
+
}
|
|
476
|
+
writeFileSync3(join5(vault, "index.md"), lines.join(`
|
|
477
|
+
`) + `
|
|
478
|
+
`);
|
|
479
|
+
return { count: notes.length, folders: folders.length };
|
|
480
|
+
}
|
|
481
|
+
var CONTROL, FOLDER_ORDER, FENCE;
|
|
482
|
+
var init_moc = __esm(() => {
|
|
483
|
+
CONTROL = new Set(["index.md", "hot.md", "log.md", "_tags.md"]);
|
|
484
|
+
FOLDER_ORDER = [
|
|
485
|
+
"people",
|
|
486
|
+
"orgs",
|
|
487
|
+
"holdings",
|
|
488
|
+
"identity",
|
|
489
|
+
"health",
|
|
490
|
+
"finances",
|
|
491
|
+
"work",
|
|
492
|
+
"life",
|
|
493
|
+
"projects",
|
|
494
|
+
"events",
|
|
495
|
+
"mistakes"
|
|
496
|
+
];
|
|
497
|
+
FENCE = /^(\s*)(```+|~~~+)/;
|
|
498
|
+
});
|
|
499
|
+
|
|
500
|
+
// scripts/lib/registry.ts
|
|
501
|
+
import { existsSync as existsSync5, mkdirSync, readFileSync as readFileSync5, statSync as statSync3, writeFileSync as writeFileSync4 } from "node:fs";
|
|
502
|
+
import { homedir } from "node:os";
|
|
503
|
+
import { dirname as dirname2, join as join6, resolve as resolve3 } from "node:path";
|
|
504
|
+
function configPath() {
|
|
505
|
+
const base = process.env.XDG_CONFIG_HOME || join6(homedir(), ".config");
|
|
506
|
+
return join6(base, "imprnt", "config.json");
|
|
507
|
+
}
|
|
508
|
+
function readRegistry() {
|
|
509
|
+
try {
|
|
510
|
+
const raw = JSON.parse(readFileSync5(configPath(), "utf8"));
|
|
511
|
+
const vaults = {};
|
|
512
|
+
for (const [k, v] of Object.entries(raw?.vaults ?? {}))
|
|
513
|
+
if (typeof v === "string")
|
|
514
|
+
vaults[k] = v;
|
|
515
|
+
return { default: typeof raw?.default === "string" ? raw.default : undefined, vaults };
|
|
516
|
+
} catch {
|
|
517
|
+
return { vaults: {} };
|
|
518
|
+
}
|
|
519
|
+
}
|
|
520
|
+
function liveDefault(reg) {
|
|
521
|
+
const name = reg.default ?? Object.keys(reg.vaults)[0];
|
|
522
|
+
const path = name ? reg.vaults[name] : undefined;
|
|
523
|
+
return path && isVaultProject(path) ? path : undefined;
|
|
524
|
+
}
|
|
525
|
+
function registeredRoot() {
|
|
526
|
+
return liveDefault(readRegistry());
|
|
527
|
+
}
|
|
528
|
+
function registerVault(path, opts = {}) {
|
|
529
|
+
const reg = readRegistry();
|
|
530
|
+
const current = liveDefault(reg);
|
|
531
|
+
if (current === path)
|
|
532
|
+
return { status: "already", current: path };
|
|
533
|
+
if (current && !opts.force)
|
|
534
|
+
return { status: "kept", current };
|
|
535
|
+
const name = opts.name ?? "personal";
|
|
536
|
+
reg.vaults[name] = path;
|
|
537
|
+
reg.default = name;
|
|
538
|
+
const p = configPath();
|
|
539
|
+
try {
|
|
540
|
+
mkdirSync(dirname2(p), { recursive: true });
|
|
541
|
+
writeFileSync4(p, JSON.stringify(reg, null, 2) + `
|
|
542
|
+
`);
|
|
543
|
+
} catch (e) {
|
|
544
|
+
return { status: "error", current: path, error: e instanceof Error ? e.message : String(e) };
|
|
545
|
+
}
|
|
546
|
+
return { status: "registered", current: path };
|
|
547
|
+
}
|
|
548
|
+
function isVaultProject(dir) {
|
|
549
|
+
const vault = join6(dir, "vault");
|
|
550
|
+
try {
|
|
551
|
+
return statSync3(vault).isDirectory() && existsSync5(join6(vault, "index.md")) && existsSync5(join6(vault, "_tags.md"));
|
|
552
|
+
} catch {
|
|
553
|
+
return false;
|
|
554
|
+
}
|
|
555
|
+
}
|
|
556
|
+
function vaultProjectRoot(start = process.cwd()) {
|
|
557
|
+
const rootEnv = process.env.IMPRNT_ROOT ?? process.env.IMPRINT_ROOT;
|
|
558
|
+
if (rootEnv)
|
|
559
|
+
return resolve3(start, rootEnv);
|
|
560
|
+
const vaultEnv = process.env.IMPRNT_VAULT ?? process.env.IMPRINT_VAULT;
|
|
561
|
+
if (vaultEnv)
|
|
562
|
+
return dirname2(resolve3(start, vaultEnv));
|
|
563
|
+
let dir = start;
|
|
564
|
+
for (;; ) {
|
|
565
|
+
if (isVaultProject(dir))
|
|
566
|
+
return dir;
|
|
567
|
+
const parent = dirname2(dir);
|
|
568
|
+
if (parent === dir)
|
|
569
|
+
break;
|
|
570
|
+
dir = parent;
|
|
571
|
+
}
|
|
572
|
+
return registeredRoot();
|
|
573
|
+
}
|
|
574
|
+
var init_registry = () => {};
|
|
575
|
+
|
|
576
|
+
// scripts/lib/launch.ts
|
|
577
|
+
import { existsSync as existsSync6, readFileSync as readFileSync6, realpathSync, statSync as statSync4 } from "node:fs";
|
|
578
|
+
import { homedir as homedir2 } from "node:os";
|
|
579
|
+
import { isAbsolute, join as join7, resolve as resolve4, sep as sep2 } from "node:path";
|
|
580
|
+
import { spawnSync as spawnSync2 } from "node:child_process";
|
|
581
|
+
function importPath(root, target) {
|
|
582
|
+
if (target.startsWith("~/"))
|
|
583
|
+
return join7(homedir2(), target.slice(2));
|
|
584
|
+
if (isAbsolute(target))
|
|
585
|
+
return target;
|
|
586
|
+
return join7(root, target);
|
|
587
|
+
}
|
|
588
|
+
function castFragment(root) {
|
|
589
|
+
const parts = [];
|
|
590
|
+
for (const target of importTargets(root)) {
|
|
591
|
+
const p = importPath(root, target);
|
|
592
|
+
if (!existsSync6(p)) {
|
|
593
|
+
console.error(`imp: skipping missing import @${target}`);
|
|
594
|
+
continue;
|
|
595
|
+
}
|
|
596
|
+
try {
|
|
597
|
+
parts.push(readFileSync6(p, "utf8").trim());
|
|
598
|
+
} catch {
|
|
599
|
+
console.error(`imp: skipping unreadable import @${target}`);
|
|
600
|
+
}
|
|
601
|
+
}
|
|
602
|
+
return parts.join(`
|
|
603
|
+
|
|
604
|
+
`);
|
|
605
|
+
}
|
|
606
|
+
function pointerFragment(pkgRoot, vaultProject) {
|
|
607
|
+
const tpl = readFileSync6(join7(pkgRoot, "templates", "pointer.md"), "utf8");
|
|
608
|
+
return tpl.replaceAll("{{VAULT_PROJECT}}", () => vaultProject);
|
|
609
|
+
}
|
|
610
|
+
function realResolve(p) {
|
|
611
|
+
try {
|
|
612
|
+
return realpathSync(p);
|
|
613
|
+
} catch {
|
|
614
|
+
return resolve4(p);
|
|
615
|
+
}
|
|
616
|
+
}
|
|
617
|
+
function isInside(child, parent) {
|
|
618
|
+
const c = realResolve(child);
|
|
619
|
+
const p = realResolve(parent);
|
|
620
|
+
return c === p || c.startsWith(p + sep2);
|
|
621
|
+
}
|
|
622
|
+
function isDir(p) {
|
|
623
|
+
try {
|
|
624
|
+
return statSync4(p).isDirectory();
|
|
625
|
+
} catch {
|
|
626
|
+
return false;
|
|
627
|
+
}
|
|
628
|
+
}
|
|
629
|
+
function childEnv(vaultProject) {
|
|
630
|
+
return process.env.IMPRNT_VAULT || process.env.IMPRINT_VAULT ? process.env : { ...process.env, IMPRNT_VAULT: join7(vaultProject, "vault") };
|
|
631
|
+
}
|
|
632
|
+
function buildLaunch(opts) {
|
|
633
|
+
const pass = [...opts.passthrough ?? []];
|
|
634
|
+
if (!opts.vaultProject)
|
|
635
|
+
return { args: pass, env: process.env };
|
|
636
|
+
if (!isDir(opts.vaultProject)) {
|
|
637
|
+
console.error(`imp: vault project not found at ${opts.vaultProject} - launching plain claude (re-run \`imprnt init\` there, or fix IMPRNT_ROOT)`);
|
|
638
|
+
return { args: pass, env: process.env };
|
|
639
|
+
}
|
|
640
|
+
if (isInside(opts.cwd, opts.vaultProject))
|
|
641
|
+
return { args: pass, env: childEnv(opts.vaultProject) };
|
|
642
|
+
const fragment = [castFragment(opts.vaultProject), pointerFragment(opts.pkgRoot, opts.vaultProject)].filter(Boolean).join(`
|
|
643
|
+
|
|
644
|
+
`);
|
|
645
|
+
const args = [...pass];
|
|
646
|
+
const firstTerm = args.indexOf("--");
|
|
647
|
+
const scanEnd = firstTerm >= 0 ? firstTerm - 1 : args.length - 1;
|
|
648
|
+
let merged = false;
|
|
649
|
+
for (let i = scanEnd;i >= 0 && !merged; i--) {
|
|
650
|
+
const prev = args[i - 1];
|
|
651
|
+
const inValuePosition = prev === "-p" || prev === "--print" || prev === "--append-system-prompt" || prev === "--system-prompt" || prev === "--add-dir";
|
|
652
|
+
if (args[i] === "--append-system-prompt" && args[i + 1] !== undefined) {
|
|
653
|
+
args[i + 1] += `
|
|
654
|
+
|
|
655
|
+
` + fragment;
|
|
656
|
+
merged = true;
|
|
657
|
+
} else if (!inValuePosition && args[i].startsWith("--append-system-prompt=")) {
|
|
658
|
+
args[i] += `
|
|
659
|
+
|
|
660
|
+
` + fragment;
|
|
661
|
+
merged = true;
|
|
662
|
+
}
|
|
663
|
+
}
|
|
664
|
+
const term = args.indexOf("--");
|
|
665
|
+
const inject = merged ? ["--add-dir", opts.vaultProject] : ["--append-system-prompt", fragment, "--add-dir", opts.vaultProject];
|
|
666
|
+
if (term >= 0)
|
|
667
|
+
args.splice(term, 0, ...inject);
|
|
668
|
+
else
|
|
669
|
+
args.push(...inject);
|
|
670
|
+
return { args, env: childEnv(opts.vaultProject) };
|
|
671
|
+
}
|
|
672
|
+
function launchClaude(cwd, args, env = process.env) {
|
|
673
|
+
if (!isDir(cwd)) {
|
|
674
|
+
console.error(`imp: vault project not found at ${cwd} — re-run \`imprnt init\` in its new location (add --register to switch the default)`);
|
|
675
|
+
return 1;
|
|
676
|
+
}
|
|
677
|
+
const r = spawnSync2("claude", args, { cwd, stdio: "inherit", env });
|
|
678
|
+
if (r.error) {
|
|
679
|
+
const code = r.error.code;
|
|
680
|
+
console.error(code === "ENOENT" ? "imp: `claude` not found on PATH. Install Claude Code first: npm i -g @anthropic-ai/claude-code" : `imp: failed to launch claude: ${r.error.message}`);
|
|
681
|
+
return 1;
|
|
682
|
+
}
|
|
683
|
+
return r.status ?? 1;
|
|
684
|
+
}
|
|
685
|
+
var init_launch = __esm(() => {
|
|
686
|
+
init_plugins();
|
|
687
|
+
});
|
|
688
|
+
|
|
689
|
+
// scripts/lib/manifest.ts
|
|
690
|
+
import { readFileSync as readFileSync7, writeFileSync as writeFileSync5, renameSync, existsSync as existsSync7 } from "node:fs";
|
|
691
|
+
import { join as join8 } from "node:path";
|
|
692
|
+
function manifestPath(vault) {
|
|
693
|
+
return join8(vault, ".manifest.json");
|
|
694
|
+
}
|
|
695
|
+
function loadManifest(vault) {
|
|
696
|
+
const p = manifestPath(vault);
|
|
697
|
+
if (!existsSync7(p))
|
|
698
|
+
return {};
|
|
699
|
+
try {
|
|
700
|
+
return JSON.parse(readFileSync7(p, "utf8"));
|
|
701
|
+
} catch {
|
|
702
|
+
let n = 0;
|
|
703
|
+
let backup = `${p}.corrupt-${n}`;
|
|
704
|
+
while (existsSync7(backup))
|
|
705
|
+
backup = `${p}.corrupt-${++n}`;
|
|
706
|
+
renameSync(p, backup);
|
|
707
|
+
throw new Error(`manifest is corrupt and could not be parsed: ${p}
|
|
708
|
+
backed up to ${backup} — inspect it, then retry. provenance was not lost.`);
|
|
709
|
+
}
|
|
710
|
+
}
|
|
711
|
+
function saveManifest(vault, m) {
|
|
712
|
+
const p = manifestPath(vault);
|
|
713
|
+
const tmp = `${p}.tmp.${process.pid}.${Date.now()}`;
|
|
714
|
+
writeFileSync5(tmp, JSON.stringify(m, null, 2) + `
|
|
715
|
+
`);
|
|
716
|
+
renameSync(tmp, p);
|
|
717
|
+
}
|
|
718
|
+
var init_manifest = () => {};
|
|
719
|
+
|
|
720
|
+
// scripts/ingest.ts
|
|
721
|
+
var exports_ingest = {};
|
|
722
|
+
import { readFileSync as readFileSync8, writeFileSync as writeFileSync6, mkdirSync as mkdirSync2, existsSync as existsSync8, copyFileSync, readdirSync as readdirSync4, rmSync as rmSync2, statSync as statSync5 } from "node:fs";
|
|
723
|
+
import { createHash } from "node:crypto";
|
|
724
|
+
import { basename as basename2, extname, join as join9, relative as relative3 } from "node:path";
|
|
725
|
+
function slugify(s) {
|
|
726
|
+
return s.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "").slice(0, 60);
|
|
727
|
+
}
|
|
728
|
+
function deriveSlug(candidates) {
|
|
729
|
+
for (const c of candidates) {
|
|
730
|
+
const s = slugify(c);
|
|
731
|
+
if (!s)
|
|
732
|
+
continue;
|
|
733
|
+
if (/^[\d-]+$/.test(s) && /\p{L}/u.test(c))
|
|
734
|
+
continue;
|
|
735
|
+
return s;
|
|
736
|
+
}
|
|
737
|
+
return "";
|
|
738
|
+
}
|
|
739
|
+
function looksLikePath(arg) {
|
|
740
|
+
if (/\s/.test(arg))
|
|
741
|
+
return false;
|
|
742
|
+
if (/^[a-z][a-z0-9+.-]*:\/\//i.test(arg))
|
|
743
|
+
return false;
|
|
744
|
+
if (arg.includes("/") || arg.includes("\\"))
|
|
745
|
+
return true;
|
|
746
|
+
if (PATH_EXTS.has(extname(arg).toLowerCase()))
|
|
747
|
+
return true;
|
|
748
|
+
return false;
|
|
749
|
+
}
|
|
750
|
+
function looksLikeTranscript(speakers, turnCount, contentLines, recurringSpeaker) {
|
|
751
|
+
if (speakers.size < 2)
|
|
752
|
+
return false;
|
|
753
|
+
if (recurringSpeaker)
|
|
754
|
+
return true;
|
|
755
|
+
return speakers.size === 2 && turnCount === 2 && contentLines === 2;
|
|
756
|
+
}
|
|
757
|
+
function frontmatter2(raw) {
|
|
758
|
+
return raw.match(/^---\r?\n([\s\S]*?)\r?\n---/)?.[1] ?? "";
|
|
759
|
+
}
|
|
760
|
+
function fmScalar2(fm, key) {
|
|
761
|
+
return (fm.match(new RegExp(`^${key}:\\s*(.+)$`, "im"))?.[1] ?? "").trim().replace(/^["']|["']$/g, "");
|
|
762
|
+
}
|
|
763
|
+
function fmList2(fm, key) {
|
|
764
|
+
const line = fm.match(new RegExp(`^${key}:\\s*\\[(.*)\\]`, "im"))?.[1] ?? "";
|
|
765
|
+
return line.split(",").map((s) => s.trim().replace(/^["'\[]+|["'\]]+$/g, "")).filter(Boolean);
|
|
766
|
+
}
|
|
767
|
+
function linkSlug(s) {
|
|
768
|
+
return s.trim().replace(/^\[\[/, "").replace(/\]\]$/, "").replace(/#.*$/, "").replace(/\|.*$/, "").replace(/\.md$/, "").trim();
|
|
769
|
+
}
|
|
770
|
+
function injectSource(text, target) {
|
|
771
|
+
return text.replace(/^(---\r?\n[\s\S]*?)(\r?\n)(---)/, `$1$2source: "[[${target}]]"$2$3`);
|
|
772
|
+
}
|
|
773
|
+
function targetFolder(type, domain) {
|
|
774
|
+
if (Object.hasOwn(TYPE_FOLDER, type))
|
|
775
|
+
return TYPE_FOLDER[type];
|
|
776
|
+
if (type === "principle" || type === "note") {
|
|
777
|
+
return DOMAIN_FOLDERS.has(domain) ? domain : null;
|
|
778
|
+
}
|
|
779
|
+
return null;
|
|
780
|
+
}
|
|
781
|
+
function applyStaged(staged, vault) {
|
|
782
|
+
if (!existsSync8(staged)) {
|
|
783
|
+
console.error(`no such staged note: ${staged}`);
|
|
784
|
+
return "error";
|
|
785
|
+
}
|
|
786
|
+
if (statSync5(staged).isDirectory()) {
|
|
787
|
+
console.error(` ✗ ${staged}: is a directory - --apply takes a single staged note file`);
|
|
788
|
+
return "error";
|
|
789
|
+
}
|
|
790
|
+
const text = readFileSync8(staged, "utf8").replace(/^/, "");
|
|
791
|
+
const fm = frontmatter2(text);
|
|
792
|
+
const type = fmScalar2(fm, "type");
|
|
793
|
+
const domain = fmScalar2(fm, "domain");
|
|
794
|
+
if (!type) {
|
|
795
|
+
console.error(` ✗ ${staged}: no \`type:\` in frontmatter — can't file a note with no type`);
|
|
796
|
+
return "error";
|
|
797
|
+
}
|
|
798
|
+
const folder = targetFolder(type, domain);
|
|
799
|
+
if (!folder) {
|
|
800
|
+
console.error(` ✗ ${staged}: type \`${type}\`${domain ? ` / domain \`${domain}\`` : ""} maps to no vault folder`);
|
|
801
|
+
console.error(` entities: person|org|holding · forms: event|mistake · project · a principle/note needs a valid domain:`);
|
|
802
|
+
return "error";
|
|
803
|
+
}
|
|
804
|
+
const title = text.match(/^#\s+(.+)$/m)?.[1]?.trim() ?? "";
|
|
805
|
+
const slug = deriveSlug([fmScalar2(fm, "slug"), title, basename2(staged, ".md")]);
|
|
806
|
+
if (!slug) {
|
|
807
|
+
console.error(` ✗ ${staged}: can't derive a slug - slug:, the H1 title, and the filename all slugify to nothing`);
|
|
808
|
+
return "error";
|
|
809
|
+
}
|
|
810
|
+
const hash = createHash("sha256").update(text).digest("hex").slice(0, 16);
|
|
811
|
+
const noteRel = `${folder}/${slug}`;
|
|
812
|
+
const notePath = join9(vault, `${noteRel}.md`);
|
|
813
|
+
const stagedSource = fmScalar2(fm, "source");
|
|
814
|
+
const manifest = loadManifest(vault);
|
|
815
|
+
const priorSnapshot = Object.values(manifest).find((e) => e.hash === hash && e.raw && existsSync8(e.raw))?.raw;
|
|
816
|
+
const rawDir = join9(vault, "..", "raw", "proposed");
|
|
817
|
+
let rawPath = "";
|
|
818
|
+
let rawEntry;
|
|
819
|
+
let sourceTarget;
|
|
820
|
+
if (stagedSource) {
|
|
821
|
+
rawEntry = linkSlug(stagedSource);
|
|
822
|
+
sourceTarget = rawEntry;
|
|
823
|
+
} else if (priorSnapshot) {
|
|
824
|
+
rawPath = priorSnapshot;
|
|
825
|
+
rawEntry = rawPath;
|
|
826
|
+
sourceTarget = "raw/" + relative3(join9(vault, "..", "raw"), rawPath).split("\\").join("/").replace(/\.md$/, "");
|
|
827
|
+
} else {
|
|
828
|
+
rawPath = join9(rawDir, `${slug}-${hash}.md`);
|
|
829
|
+
rawEntry = rawPath;
|
|
830
|
+
sourceTarget = `raw/proposed/${slug}-${hash}`;
|
|
831
|
+
}
|
|
832
|
+
const finalText = stagedSource ? text : injectSource(text, sourceTarget);
|
|
833
|
+
const fileHash = createHash("sha256").update(finalText).digest("hex").slice(0, 16);
|
|
834
|
+
if (existsSync8(notePath)) {
|
|
835
|
+
const existingHash = createHash("sha256").update(readFileSync8(notePath, "utf8")).digest("hex").slice(0, 16);
|
|
836
|
+
if (existingHash === fileHash) {
|
|
837
|
+
console.log(` = ${noteRel} already filed, identical content (hash ${fileHash}) — no-op`);
|
|
838
|
+
rmSync2(staged);
|
|
839
|
+
return "noop";
|
|
840
|
+
}
|
|
841
|
+
console.error(` ! ${noteRel} exists with DIFFERENT content — not overwriting (contradiction discipline)`);
|
|
842
|
+
flagNeedsReview(vault, `- [ ] proposed note conflicts with existing [[${noteRel}]] — staged at \`${staged}\` (hash ${fileHash} vs ${existingHash}); reconcile or stamp \`> superseded by\``);
|
|
843
|
+
return "conflict";
|
|
844
|
+
}
|
|
845
|
+
const manifestKey = `apply:sha256:${hash}:${noteRel}`;
|
|
846
|
+
if (!stagedSource && !priorSnapshot) {
|
|
847
|
+
mkdirSync2(rawDir, { recursive: true });
|
|
848
|
+
if (!existsSync8(rawPath))
|
|
849
|
+
writeFileSync6(rawPath, text);
|
|
850
|
+
}
|
|
851
|
+
manifest[manifestKey] = { hash, note: notePath, ingested: new Date().toISOString(), raw: rawEntry };
|
|
852
|
+
saveManifest(vault, manifest);
|
|
853
|
+
mkdirSync2(join9(vault, folder), { recursive: true });
|
|
854
|
+
writeFileSync6(notePath, finalText);
|
|
855
|
+
const participantsList = fmList2(fm, "participants");
|
|
856
|
+
const participantsScalar = participantsList.length ? "" : fmScalar2(fm, "participants");
|
|
857
|
+
const participants = (participantsScalar ? [participantsScalar] : participantsList).map(linkSlug);
|
|
858
|
+
const owner = linkSlug(fmScalar2(fm, "owner"));
|
|
859
|
+
const peopleLinks = [...participants, ...owner ? [owner] : []].filter((l) => l.startsWith("people/")).map((l) => l.slice("people/".length));
|
|
860
|
+
let unresolved = 0;
|
|
861
|
+
for (const personSlug of new Set(peopleLinks)) {
|
|
862
|
+
if (!personResolved(vault, personSlug, personSlug.replace(/-/g, " "))) {
|
|
863
|
+
unresolved++;
|
|
864
|
+
flagNeedsReview(vault, `- [ ] unresolved person \`people/${personSlug}\` — from [[${noteRel}]] (applied from \`${staged}\`)`);
|
|
865
|
+
}
|
|
866
|
+
}
|
|
867
|
+
rmSync2(staged);
|
|
868
|
+
console.log(` ✓ filed ${noteRel} (type: ${type}${domain ? `, domain: ${domain}` : ""})`);
|
|
869
|
+
if (stagedSource)
|
|
870
|
+
console.log(` source -> ${rawEntry} (note's own source: kept - no redundant snapshot)`);
|
|
871
|
+
else
|
|
872
|
+
console.log(` snapshot -> ${rawPath}${priorSnapshot ? " (reused — identical bytes already snapshotted)" : ""}`);
|
|
873
|
+
console.log(` staged copy removed: ${staged}`);
|
|
874
|
+
if (unresolved)
|
|
875
|
+
console.log(` ⚠ ${unresolved} unresolved person link(s) -> needs-review`);
|
|
876
|
+
return "filed";
|
|
877
|
+
}
|
|
878
|
+
var PATH_EXTS, TYPE_FOLDER, DOMAIN_FOLDERS, args, vault, inlineText, useStdin = false, slugHint = "", positional, src, text, srcBytes, isFile = false, hash, manifestKey, manifest, prior, fname, dateMatch, date, lines, META_KEYS, META, EMAIL_HEADER_KEYS, EMAIL_HEADER, SPEAKER, speakers, turnsBySpeaker, subject = "", turnCount = 0, contentLines = 0, recurringSpeaker, isTranscript, slugBasis, subjectSlug, noteSlug, rawDir, priorSnapshot, rawPath, title, people, rawRel, fm, PENDING = "<!-- semantic-clean: pending — the agent fills this. The only paid LLM step. -->", body, note, dir, effectiveSlug, notePath, unresolved;
|
|
879
|
+
var init_ingest = __esm(() => {
|
|
880
|
+
init_manifest();
|
|
881
|
+
init_resolve();
|
|
882
|
+
init_roots();
|
|
883
|
+
PATH_EXTS = new Set([".txt", ".md", ".markdown", ".csv", ".json", ".log", ".pdf", ".rtf", ".html", ".htm", ".vtt", ".srt"]);
|
|
884
|
+
TYPE_FOLDER = {
|
|
885
|
+
person: "people",
|
|
886
|
+
org: "orgs",
|
|
887
|
+
holding: "holdings",
|
|
888
|
+
project: "projects",
|
|
889
|
+
event: "events",
|
|
890
|
+
mistake: "mistakes"
|
|
891
|
+
};
|
|
892
|
+
DOMAIN_FOLDERS = new Set(["identity", "health", "finances", "work", "life"]);
|
|
893
|
+
{
|
|
894
|
+
const a = process.argv.slice(2);
|
|
895
|
+
let applyVault = process.env.IMPRNT_VAULT ?? process.env.IMPRINT_VAULT ?? "./vault";
|
|
896
|
+
for (let i = 0;i < a.length; i++)
|
|
897
|
+
if (a[i] === "--vault") {
|
|
898
|
+
const v = a[++i];
|
|
899
|
+
if (v === undefined) {
|
|
900
|
+
console.error("--vault requires a directory argument");
|
|
901
|
+
process.exit(1);
|
|
902
|
+
}
|
|
903
|
+
applyVault = v;
|
|
904
|
+
}
|
|
905
|
+
if (a.includes("--apply-all")) {
|
|
906
|
+
if (!existsSync8(applyVault)) {
|
|
907
|
+
console.error(`no vault at ${applyVault} — run \`imprnt init\` first`);
|
|
908
|
+
process.exit(1);
|
|
909
|
+
}
|
|
910
|
+
try {
|
|
911
|
+
loadManifest(applyVault);
|
|
912
|
+
} catch (e) {
|
|
913
|
+
console.error(e instanceof Error ? e.message : String(e));
|
|
914
|
+
process.exit(1);
|
|
915
|
+
}
|
|
916
|
+
const pluginsDir = join9(projectRoot(), "plugins");
|
|
917
|
+
const staged = [];
|
|
918
|
+
if (existsSync8(pluginsDir)) {
|
|
919
|
+
for (const entry of readdirSync4(pluginsDir)) {
|
|
920
|
+
const proposed = join9(pluginsDir, entry, "proposed");
|
|
921
|
+
if (!existsSync8(proposed) || !statSync5(proposed).isDirectory())
|
|
922
|
+
continue;
|
|
923
|
+
for (const f of readdirSync4(proposed))
|
|
924
|
+
if (f.endsWith(".md"))
|
|
925
|
+
staged.push(join9(proposed, f));
|
|
926
|
+
}
|
|
927
|
+
}
|
|
928
|
+
staged.sort();
|
|
929
|
+
console.log(`ingest --apply-all — ${staged.length} staged note(s) across plugins/*/proposed/`);
|
|
930
|
+
let filed = 0, noop = 0, conflict = 0, error = 0;
|
|
931
|
+
for (const s of staged) {
|
|
932
|
+
let r;
|
|
933
|
+
try {
|
|
934
|
+
r = applyStaged(s, applyVault);
|
|
935
|
+
} catch (e) {
|
|
936
|
+
console.error(` ✗ ${s}: ${e instanceof Error ? e.message : String(e)}`);
|
|
937
|
+
r = "error";
|
|
938
|
+
}
|
|
939
|
+
if (r === "filed")
|
|
940
|
+
filed++;
|
|
941
|
+
else if (r === "noop")
|
|
942
|
+
noop++;
|
|
943
|
+
else if (r === "conflict")
|
|
944
|
+
conflict++;
|
|
945
|
+
else
|
|
946
|
+
error++;
|
|
947
|
+
}
|
|
948
|
+
console.log(`
|
|
949
|
+
${filed} filed, ${noop} no-op, ${conflict} conflict, ${error} error.`);
|
|
950
|
+
process.exit(conflict + error ? 1 : 0);
|
|
951
|
+
}
|
|
952
|
+
const applyIdx = a.indexOf("--apply");
|
|
953
|
+
if (applyIdx >= 0) {
|
|
954
|
+
const file = a[applyIdx + 1];
|
|
955
|
+
if (!file || file.startsWith("--")) {
|
|
956
|
+
console.error("usage: imprnt ingest --apply <file> [--vault DIR]");
|
|
957
|
+
process.exit(1);
|
|
958
|
+
}
|
|
959
|
+
if (!existsSync8(applyVault)) {
|
|
960
|
+
console.error(`no vault at ${applyVault} — run \`imprnt init\` first`);
|
|
961
|
+
process.exit(1);
|
|
962
|
+
}
|
|
963
|
+
console.log(`ingest --apply ${file}`);
|
|
964
|
+
const r = applyStaged(file, applyVault);
|
|
965
|
+
process.exit(r === "conflict" || r === "error" ? 1 : 0);
|
|
966
|
+
}
|
|
967
|
+
}
|
|
968
|
+
args = process.argv.slice(2);
|
|
969
|
+
vault = process.env.IMPRNT_VAULT ?? process.env.IMPRINT_VAULT ?? "./vault";
|
|
970
|
+
positional = [];
|
|
971
|
+
for (let i = 0;i < args.length; i++) {
|
|
972
|
+
if (args[i] === "--vault") {
|
|
973
|
+
const v = args[++i];
|
|
974
|
+
if (v === undefined) {
|
|
975
|
+
console.error("--vault requires a directory argument");
|
|
976
|
+
process.exit(1);
|
|
977
|
+
}
|
|
978
|
+
vault = v;
|
|
979
|
+
} else if (args[i] === "--text")
|
|
980
|
+
inlineText = args[++i];
|
|
981
|
+
else if (args[i] === "--stdin")
|
|
982
|
+
useStdin = true;
|
|
983
|
+
else if (args[i] === "--slug")
|
|
984
|
+
slugHint = args[++i];
|
|
985
|
+
else
|
|
986
|
+
positional.push(args[i]);
|
|
987
|
+
}
|
|
988
|
+
if (useStdin) {
|
|
989
|
+
srcBytes = readFileSync8(0);
|
|
990
|
+
text = srcBytes.toString("utf8");
|
|
991
|
+
src = "<stdin>";
|
|
992
|
+
} else if (inlineText !== undefined) {
|
|
993
|
+
text = inlineText;
|
|
994
|
+
srcBytes = Buffer.from(text, "utf8");
|
|
995
|
+
src = "<text>";
|
|
996
|
+
} else {
|
|
997
|
+
const arg = positional[0];
|
|
998
|
+
if (!arg) {
|
|
999
|
+
console.error('usage: imprnt ingest <file|text> [--text "<bytes>"] [--stdin] [--slug S] [--vault DIR]');
|
|
1000
|
+
process.exit(1);
|
|
1001
|
+
}
|
|
1002
|
+
if (existsSync8(arg)) {
|
|
1003
|
+
if (statSync5(arg).isDirectory()) {
|
|
1004
|
+
console.error(`${arg} is a directory - ingest takes a single file; mirror a tree with \`imprnt snapshot ${arg} --dest <name>\``);
|
|
1005
|
+
process.exit(1);
|
|
1006
|
+
}
|
|
1007
|
+
srcBytes = readFileSync8(arg);
|
|
1008
|
+
text = srcBytes.toString("utf8");
|
|
1009
|
+
src = arg;
|
|
1010
|
+
isFile = true;
|
|
1011
|
+
} else if (looksLikePath(arg)) {
|
|
1012
|
+
console.error(`no such file: ${arg}`);
|
|
1013
|
+
process.exit(1);
|
|
1014
|
+
} else {
|
|
1015
|
+
text = arg;
|
|
1016
|
+
srcBytes = Buffer.from(text, "utf8");
|
|
1017
|
+
src = "<text>";
|
|
1018
|
+
}
|
|
1019
|
+
}
|
|
1020
|
+
if (!text.trim()) {
|
|
1021
|
+
console.error("empty source — nothing to ingest");
|
|
1022
|
+
process.exit(1);
|
|
1023
|
+
}
|
|
1024
|
+
hash = createHash("sha256").update(srcBytes).digest("hex").slice(0, 16);
|
|
1025
|
+
manifestKey = isFile ? src : `bytes:sha256:${hash}`;
|
|
1026
|
+
manifest = loadManifest(vault);
|
|
1027
|
+
prior = manifest[manifestKey];
|
|
1028
|
+
if (prior?.hash === hash && (!prior.note || existsSync8(prior.note)) && (!prior.raw || existsSync8(prior.raw))) {
|
|
1029
|
+
console.log(`unchanged (hash ${hash}) — skipping ${src}. note: ${prior.note}`);
|
|
1030
|
+
process.exit(0);
|
|
1031
|
+
}
|
|
1032
|
+
fname = basename2(src);
|
|
1033
|
+
dateMatch = fname.match(/(\d{4}-\d{2}-\d{2})/) || text.match(/^\s*date:\s*(\d{4}-\d{2}-\d{2})/im);
|
|
1034
|
+
date = dateMatch ? dateMatch[1] : new Date().toISOString().slice(0, 10);
|
|
1035
|
+
lines = text.split(/\r?\n/);
|
|
1036
|
+
META_KEYS = new Set(["date", "subject", "topic", "note", "notes", "attendees", "participants"]);
|
|
1037
|
+
META = /^([A-Za-z][A-Za-z]*):\s*(.*)$/;
|
|
1038
|
+
EMAIL_HEADER_KEYS = new Set(["from", "to", "cc", "bcc", "reply-to", "sent"]);
|
|
1039
|
+
EMAIL_HEADER = /^([A-Za-z][A-Za-z-]*):\s/;
|
|
1040
|
+
SPEAKER = /^([A-Z][A-Za-z.'-]+(?: [A-Z][A-Za-z.'-]+){0,2}):\s+\S.*$/;
|
|
1041
|
+
speakers = new Set;
|
|
1042
|
+
turnsBySpeaker = new Map;
|
|
1043
|
+
for (const line of lines) {
|
|
1044
|
+
const meta = line.match(META);
|
|
1045
|
+
if (meta && META_KEYS.has(meta[1].trim().toLowerCase())) {
|
|
1046
|
+
const key = meta[1].trim().toLowerCase();
|
|
1047
|
+
if ((key === "subject" || key === "topic") && !subject)
|
|
1048
|
+
subject = meta[2].trim();
|
|
1049
|
+
continue;
|
|
1050
|
+
}
|
|
1051
|
+
const eh = line.match(EMAIL_HEADER);
|
|
1052
|
+
if (eh && EMAIL_HEADER_KEYS.has(eh[1].toLowerCase()))
|
|
1053
|
+
continue;
|
|
1054
|
+
if (line.trim())
|
|
1055
|
+
contentLines++;
|
|
1056
|
+
const m = line.match(SPEAKER);
|
|
1057
|
+
if (!m)
|
|
1058
|
+
continue;
|
|
1059
|
+
const who = m[1].trim();
|
|
1060
|
+
speakers.add(who);
|
|
1061
|
+
turnsBySpeaker.set(who, (turnsBySpeaker.get(who) ?? 0) + 1);
|
|
1062
|
+
turnCount++;
|
|
1063
|
+
}
|
|
1064
|
+
recurringSpeaker = [...turnsBySpeaker.values()].some((n) => n >= 2);
|
|
1065
|
+
isTranscript = isFile && looksLikeTranscript(speakers, turnCount, contentLines, recurringSpeaker);
|
|
1066
|
+
slugBasis = slugHint || subject || [...speakers].join("-") || (isFile ? basename2(src, extname(src)) : text.slice(0, 60));
|
|
1067
|
+
subjectSlug = slugify(slugBasis) || "source";
|
|
1068
|
+
noteSlug = `${date}-${subjectSlug}`;
|
|
1069
|
+
rawDir = join9(vault, "..", "raw", isTranscript ? "transcripts" : "adhoc");
|
|
1070
|
+
priorSnapshot = Object.values(manifest).find((e) => e.hash === hash && e.raw && existsSync8(e.raw))?.raw;
|
|
1071
|
+
if (priorSnapshot) {
|
|
1072
|
+
rawPath = priorSnapshot;
|
|
1073
|
+
} else {
|
|
1074
|
+
const ext = isFile ? extname(src) || ".txt" : ".md";
|
|
1075
|
+
const rawName = isFile ? `${date}-${subjectSlug}-${hash}${ext}` : `${subjectSlug}-${hash}${ext}`;
|
|
1076
|
+
rawPath = join9(rawDir, rawName);
|
|
1077
|
+
try {
|
|
1078
|
+
mkdirSync2(rawDir, { recursive: true });
|
|
1079
|
+
if (!existsSync8(rawPath)) {
|
|
1080
|
+
if (isFile)
|
|
1081
|
+
copyFileSync(src, rawPath);
|
|
1082
|
+
else
|
|
1083
|
+
writeFileSync6(rawPath, text);
|
|
1084
|
+
}
|
|
1085
|
+
} catch (e) {
|
|
1086
|
+
console.error(`cannot write snapshot ${rawPath}: ${e instanceof Error ? e.message : e}`);
|
|
1087
|
+
process.exit(1);
|
|
1088
|
+
}
|
|
1089
|
+
}
|
|
1090
|
+
if (!isTranscript) {
|
|
1091
|
+
manifest[manifestKey] = { hash, note: "", ingested: new Date().toISOString(), raw: rawPath };
|
|
1092
|
+
saveManifest(vault, manifest);
|
|
1093
|
+
flagNeedsReview(vault, `- [ ] unclassified source \`${rawPath}\` — snapshotted, needs TYPE + note`);
|
|
1094
|
+
console.log(`snapshotted ${src}`);
|
|
1095
|
+
console.log(` snapshot -> ${rawPath}${priorSnapshot ? " (reused — identical bytes already snapshotted)" : " (immutable)"}`);
|
|
1096
|
+
console.log(` no skeleton written — not a confident transcript. next (the one LLM step):`);
|
|
1097
|
+
console.log(` read ${rawPath}, then file it: an entity -> people/ orgs/ holdings/; a held position ->`);
|
|
1098
|
+
console.log(` identity/; else by domain (health/ finances/ work/ life/). Write type + summary + tags`);
|
|
1099
|
+
console.log(` (from vault/_tags.md) + kind, and link >=1 existing entity. Then \`imprnt check\`.`);
|
|
1100
|
+
process.exit(0);
|
|
1101
|
+
}
|
|
1102
|
+
title = subject || `1:1 — ${[...speakers].join(", ")}`;
|
|
1103
|
+
people = [...speakers].map((s) => `"[[people/${slugify(s)}]]"`);
|
|
1104
|
+
rawRel = "raw/" + relative3(join9(vault, "..", "raw"), rawPath).split("\\").join("/").replace(/\.md$/, "");
|
|
1105
|
+
fm = [
|
|
1106
|
+
"---",
|
|
1107
|
+
"type: event",
|
|
1108
|
+
`date: ${date}`,
|
|
1109
|
+
`participants: [${people.join(", ")}]`,
|
|
1110
|
+
"summary: # LLM writes one line — `imprnt check` reads it to build index.md",
|
|
1111
|
+
"tags: [] # LLM fills the best-fit tag (vault/_tags.md); coin a new one if none fits, check syncs it",
|
|
1112
|
+
"project: # LLM links the project this event touched",
|
|
1113
|
+
`source: "[[${rawRel}]]"`,
|
|
1114
|
+
`source_hash: ${hash}`,
|
|
1115
|
+
"status: draft-deterministic # -> 'enriched' after the LLM semantic pass",
|
|
1116
|
+
`ingested: ${new Date().toISOString()}`,
|
|
1117
|
+
"---"
|
|
1118
|
+
].join(`
|
|
1119
|
+
`);
|
|
1120
|
+
body = `# ${title}
|
|
1121
|
+
|
|
1122
|
+
> ${turnCount} turns · ${speakers.size} participants · parsed deterministically. No LLM was used to produce this skeleton.
|
|
1123
|
+
|
|
1124
|
+
## Summary
|
|
1125
|
+
${PENDING}
|
|
1126
|
+
|
|
1127
|
+
## Decisions
|
|
1128
|
+
${PENDING}
|
|
1129
|
+
|
|
1130
|
+
## Action items
|
|
1131
|
+
${PENDING}
|
|
1132
|
+
|
|
1133
|
+
## Open questions
|
|
1134
|
+
${PENDING}
|
|
1135
|
+
|
|
1136
|
+
## Participants
|
|
1137
|
+
${[...speakers].map((s) => `- [[people/${slugify(s)}]]`).join(`
|
|
1138
|
+
`) || "_none detected_"}
|
|
1139
|
+
|
|
1140
|
+
## Source
|
|
1141
|
+
Snapshot: \`${rawPath}\` (sha256:${hash}), copied verbatim from \`${src}\`. Immutable — do not edit; re-ingest instead.
|
|
1142
|
+
`;
|
|
1143
|
+
note = `${fm}
|
|
1144
|
+
|
|
1145
|
+
${body}`;
|
|
1146
|
+
dir = join9(vault, "events");
|
|
1147
|
+
mkdirSync2(dir, { recursive: true });
|
|
1148
|
+
effectiveSlug = noteSlug;
|
|
1149
|
+
notePath = join9(dir, `${noteSlug}.md`);
|
|
1150
|
+
if (existsSync8(notePath)) {
|
|
1151
|
+
const existingFm = frontmatter2(readFileSync8(notePath, "utf8"));
|
|
1152
|
+
const existingSourceHash = fmScalar2(existingFm, "source_hash");
|
|
1153
|
+
if (existingSourceHash && existingSourceHash === hash) {
|
|
1154
|
+
manifest[manifestKey] = { hash, note: notePath, ingested: new Date().toISOString(), raw: rawPath };
|
|
1155
|
+
saveManifest(vault, manifest);
|
|
1156
|
+
console.log(`= ${src}: event note already filed from identical bytes (hash ${hash}) — no-op`);
|
|
1157
|
+
console.log(` note kept -> ${notePath}`);
|
|
1158
|
+
process.exit(0);
|
|
1159
|
+
}
|
|
1160
|
+
effectiveSlug = `${noteSlug}-${hash.slice(0, 8)}`;
|
|
1161
|
+
notePath = join9(dir, `${effectiveSlug}.md`);
|
|
1162
|
+
flagNeedsReview(vault, `- [ ] slug collision: existing [[events/${noteSlug}]] vs new [[events/${effectiveSlug}]] (hash ${hash}); two different sources mapped to the same date+subject slug — reconcile`);
|
|
1163
|
+
console.log(`! ${src}: slug \`events/${noteSlug}\` already holds a different source -> filing new note under \`events/${effectiveSlug}\``);
|
|
1164
|
+
console.log(` existing note kept untouched -> ${join9(dir, `${noteSlug}.md`)}`);
|
|
1165
|
+
let counter = 1;
|
|
1166
|
+
while (existsSync8(notePath)) {
|
|
1167
|
+
const existingDisambigHash = fmScalar2(frontmatter2(readFileSync8(notePath, "utf8")), "source_hash");
|
|
1168
|
+
if (existingDisambigHash === hash) {
|
|
1169
|
+
manifest[manifestKey] = { hash, note: notePath, ingested: new Date().toISOString(), raw: rawPath };
|
|
1170
|
+
saveManifest(vault, manifest);
|
|
1171
|
+
console.log(` disambiguated note already exists from identical bytes -> ${notePath} (no-op)`);
|
|
1172
|
+
process.exit(0);
|
|
1173
|
+
}
|
|
1174
|
+
counter++;
|
|
1175
|
+
effectiveSlug = `${noteSlug}-${hash.slice(0, 8)}-${counter}`;
|
|
1176
|
+
notePath = join9(dir, `${effectiveSlug}.md`);
|
|
1177
|
+
console.log(` hash8 collision with a distinct source -> trying \`events/${effectiveSlug}\` instead`);
|
|
1178
|
+
}
|
|
1179
|
+
}
|
|
1180
|
+
writeFileSync6(notePath, note);
|
|
1181
|
+
manifest[manifestKey] = { hash, note: notePath, ingested: new Date().toISOString(), raw: rawPath };
|
|
1182
|
+
saveManifest(vault, manifest);
|
|
1183
|
+
unresolved = [...speakers].filter((name) => !personResolved(vault, slugify(name), name));
|
|
1184
|
+
for (const name of unresolved) {
|
|
1185
|
+
flagNeedsReview(vault, `- [ ] unresolved person \`${name}\` — from [[events/${effectiveSlug}]] (${date})`);
|
|
1186
|
+
}
|
|
1187
|
+
console.log(`ingested ${src}`);
|
|
1188
|
+
console.log(` snapshot -> ${rawPath}${priorSnapshot ? " (reused — identical bytes already snapshotted)" : " (immutable)"}`);
|
|
1189
|
+
console.log(` note -> ${notePath} (${speakers.size} participants, ${turnCount} turns)`);
|
|
1190
|
+
if (unresolved.length)
|
|
1191
|
+
console.log(` ⚠ ${unresolved.length} unresolved participant(s) -> needs-review: ${unresolved.join(", ")}`);
|
|
1192
|
+
console.log(` deterministic skeleton only. next (the one LLM step): the agent fills`);
|
|
1193
|
+
console.log(` summary + Summary/Decisions/Actions/Questions, assigns tags from vault/_tags.md,`);
|
|
1194
|
+
console.log(` and links people + projects (resolving the flagged participants). Then \`imprnt check\`.`);
|
|
1195
|
+
});
|
|
1196
|
+
|
|
1197
|
+
// scripts/lib/tags.ts
|
|
1198
|
+
import { readFileSync as readFileSync9, writeFileSync as writeFileSync7, existsSync as existsSync9 } from "node:fs";
|
|
1199
|
+
import { join as join10 } from "node:path";
|
|
1200
|
+
function section(text2, name) {
|
|
1201
|
+
return text2.match(new RegExp(`##\\s*${name}\\s*\\n([\\s\\S]*?)(?:\\n##\\s|\\s*$)`, "i"))?.[1] ?? "";
|
|
1202
|
+
}
|
|
1203
|
+
function tagTokens(line) {
|
|
1204
|
+
return line.split(",").map((s) => s.trim().normalize("NFC")).filter((s) => TAG_TOKEN.test(s));
|
|
1205
|
+
}
|
|
1206
|
+
function isTagLine(line) {
|
|
1207
|
+
const t = line.trim();
|
|
1208
|
+
if (t === "" || t.startsWith("##") || t.startsWith("<!--"))
|
|
1209
|
+
return false;
|
|
1210
|
+
const segs = t.split(",").map((s) => s.trim()).filter((s) => s !== "");
|
|
1211
|
+
if (segs.length === 0)
|
|
1212
|
+
return false;
|
|
1213
|
+
const valid = segs.filter((s) => TAG_TOKEN.test(s)).length;
|
|
1214
|
+
return valid * 2 > segs.length;
|
|
1215
|
+
}
|
|
1216
|
+
function kebab(tag) {
|
|
1217
|
+
const t = tag.trim().normalize("NFC").toLowerCase().replace(/[\s_]+/g, "-").replace(/-+/g, "-").replace(/^-|-$/g, "");
|
|
1218
|
+
return TAG_TOKEN.test(t) ? t : "";
|
|
1219
|
+
}
|
|
1220
|
+
function loadTags(vault2) {
|
|
1221
|
+
const approved = new Set;
|
|
1222
|
+
const synonyms = new Map;
|
|
1223
|
+
const p = join10(vault2, "_tags.md");
|
|
1224
|
+
if (!existsSync9(p))
|
|
1225
|
+
return { approved, synonyms };
|
|
1226
|
+
const text2 = readFileSync9(p, "utf8");
|
|
1227
|
+
for (const line of section(text2, "Tags").split(/\r?\n/)) {
|
|
1228
|
+
for (const t of tagTokens(line))
|
|
1229
|
+
approved.add(t.toLowerCase());
|
|
1230
|
+
}
|
|
1231
|
+
for (const line of section(text2, "Synonyms").split(/\r?\n/)) {
|
|
1232
|
+
const m = line.match(/^(.*?)\s*->\s*(.+?)\s*$/);
|
|
1233
|
+
if (!m)
|
|
1234
|
+
continue;
|
|
1235
|
+
const canon = kebab(m[2]);
|
|
1236
|
+
if (!canon)
|
|
1237
|
+
continue;
|
|
1238
|
+
for (const raw of m[1].split(",")) {
|
|
1239
|
+
const syn = kebab(raw);
|
|
1240
|
+
if (syn)
|
|
1241
|
+
synonyms.set(syn, canon);
|
|
1242
|
+
}
|
|
1243
|
+
}
|
|
1244
|
+
return { approved, synonyms };
|
|
1245
|
+
}
|
|
1246
|
+
function normalize(vocab, term) {
|
|
1247
|
+
let cur = kebab(term) || term.toLowerCase();
|
|
1248
|
+
const seen = new Set([cur]);
|
|
1249
|
+
for (;; ) {
|
|
1250
|
+
const next = vocab.synonyms.get(cur);
|
|
1251
|
+
if (next === undefined || seen.has(next))
|
|
1252
|
+
return cur;
|
|
1253
|
+
seen.add(next);
|
|
1254
|
+
cur = next;
|
|
1255
|
+
}
|
|
1256
|
+
}
|
|
1257
|
+
function appendTags(vault2, newTags) {
|
|
1258
|
+
const p = join10(vault2, "_tags.md");
|
|
1259
|
+
if (!existsSync9(p) || newTags.length === 0)
|
|
1260
|
+
return [];
|
|
1261
|
+
const { approved } = loadTags(vault2);
|
|
1262
|
+
const tags = [];
|
|
1263
|
+
for (const raw of newTags) {
|
|
1264
|
+
const t = kebab(raw);
|
|
1265
|
+
if (t && !approved.has(t) && !tags.includes(t))
|
|
1266
|
+
tags.push(t);
|
|
1267
|
+
}
|
|
1268
|
+
if (tags.length === 0)
|
|
1269
|
+
return [];
|
|
1270
|
+
const text2 = readFileSync9(p, "utf8");
|
|
1271
|
+
const lines2 = text2.split(`
|
|
1272
|
+
`);
|
|
1273
|
+
const h = lines2.findIndex((l) => /^##\s*Tags\s*$/i.test(l.trim()));
|
|
1274
|
+
if (h < 0) {
|
|
1275
|
+
const base = text2.replace(/\s+$/, "");
|
|
1276
|
+
const sec = `## Tags
|
|
1277
|
+
${tags.join(", ")}
|
|
1278
|
+
`;
|
|
1279
|
+
writeFileSync7(p, base === "" ? sec : `${base}
|
|
1280
|
+
|
|
1281
|
+
${sec}`);
|
|
1282
|
+
return tags;
|
|
1283
|
+
}
|
|
1284
|
+
let last = -1;
|
|
1285
|
+
for (let i = h + 1;i < lines2.length; i++) {
|
|
1286
|
+
if (lines2[i].trim().startsWith("##"))
|
|
1287
|
+
break;
|
|
1288
|
+
if (isTagLine(lines2[i]))
|
|
1289
|
+
last = i;
|
|
1290
|
+
}
|
|
1291
|
+
if (last < 0)
|
|
1292
|
+
lines2.splice(h + 1, 0, tags.join(", "));
|
|
1293
|
+
else
|
|
1294
|
+
lines2[last] = `${lines2[last].replace(/\s+$/, "")}, ${tags.join(", ")}`;
|
|
1295
|
+
writeFileSync7(p, lines2.join(`
|
|
1296
|
+
`));
|
|
1297
|
+
return tags;
|
|
1298
|
+
}
|
|
1299
|
+
var TAG_TOKEN;
|
|
1300
|
+
var init_tags = __esm(() => {
|
|
1301
|
+
TAG_TOKEN = /^[\p{L}\p{N}-]+$/u;
|
|
1302
|
+
});
|
|
1303
|
+
|
|
1304
|
+
// scripts/recall.ts
|
|
1305
|
+
var exports_recall = {};
|
|
1306
|
+
import { lstatSync as lstatSync2, readdirSync as readdirSync5, readFileSync as readFileSync10 } from "node:fs";
|
|
1307
|
+
import { basename as basename3, join as join11, relative as relative4 } from "node:path";
|
|
1308
|
+
function walk2(dir2) {
|
|
1309
|
+
const out = [];
|
|
1310
|
+
for (const entry of readdirSync5(dir2)) {
|
|
1311
|
+
if (entry.startsWith(".") || entry.startsWith("_"))
|
|
1312
|
+
continue;
|
|
1313
|
+
const p = join11(dir2, entry);
|
|
1314
|
+
const rel = relative4(vault2, p);
|
|
1315
|
+
if (!rel.includes("/") && !rel.includes("\\") && CONTROL2.has(rel))
|
|
1316
|
+
continue;
|
|
1317
|
+
try {
|
|
1318
|
+
const st = lstatSync2(p);
|
|
1319
|
+
if (st.isSymbolicLink())
|
|
1320
|
+
continue;
|
|
1321
|
+
if (st.isDirectory())
|
|
1322
|
+
out.push(...walk2(p));
|
|
1323
|
+
else if (entry.endsWith(".md"))
|
|
1324
|
+
out.push(p);
|
|
1325
|
+
} catch {
|
|
1326
|
+
continue;
|
|
1327
|
+
}
|
|
1328
|
+
}
|
|
1329
|
+
return out;
|
|
1330
|
+
}
|
|
1331
|
+
var args2, vault2, limit = 15, positional2, query, vocab, MAX_SYNONYM_NGRAM, STOPWORDS, tokenize = (text2) => text2.normalize("NFC").toLowerCase().replace(/['’]/gu, "").split(/[^\p{L}\p{N}]+/u).filter(Boolean), phraseSynonymTokens = (q) => {
|
|
1332
|
+
const out = [];
|
|
1333
|
+
const words = q.normalize("NFC").toLowerCase().split(/[\s-]+/).map((w) => w.replace(/^[^\p{L}\p{N}]+|[^\p{L}\p{N}]+$/gu, "")).filter(Boolean);
|
|
1334
|
+
for (let n = Math.min(words.length, MAX_SYNONYM_NGRAM);n >= 2; n--) {
|
|
1335
|
+
for (let i = 0;i + n <= words.length; i++) {
|
|
1336
|
+
const span = words.slice(i, i + n);
|
|
1337
|
+
for (const key of new Set([span.join(" "), span.join("-")])) {
|
|
1338
|
+
const canon = normalize(vocab, key);
|
|
1339
|
+
if (canon !== key)
|
|
1340
|
+
out.push(...tokenize(canon));
|
|
1341
|
+
}
|
|
1342
|
+
}
|
|
1343
|
+
}
|
|
1344
|
+
return out;
|
|
1345
|
+
}, rawTerms, contentTerms, baseTerms, queryTerms, CONTROL2, files, TITLE_BOOST = 3, TAG_BOOST = 2, BODY_BOOST = 1, docs, df, N, avgdl, K1 = 1.5, B = 0.75, idf = (term) => {
|
|
1346
|
+
const n = df.get(term) ?? 0;
|
|
1347
|
+
return Math.max(0, Math.log(1 + (N - n + 0.5) / (n + 0.5)));
|
|
1348
|
+
}, bm25Term = (tf, dl, termIdf) => {
|
|
1349
|
+
if (tf <= 0)
|
|
1350
|
+
return 0;
|
|
1351
|
+
return termIdf * (tf * (K1 + 1)) / (tf + K1 * (1 - B + B * (dl / avgdl)));
|
|
1352
|
+
}, variantIdf, hits, scoringGroups, shown, expanded;
|
|
1353
|
+
var init_recall = __esm(() => {
|
|
1354
|
+
init_tags();
|
|
1355
|
+
init_moc();
|
|
1356
|
+
args2 = process.argv.slice(2);
|
|
1357
|
+
vault2 = process.env.IMPRNT_VAULT ?? process.env.IMPRINT_VAULT ?? "./vault";
|
|
1358
|
+
positional2 = [];
|
|
1359
|
+
for (let i = 0;i < args2.length; i++) {
|
|
1360
|
+
if (args2[i] === "--vault") {
|
|
1361
|
+
const v = args2[++i];
|
|
1362
|
+
if (v === undefined) {
|
|
1363
|
+
console.error("--vault requires a directory argument");
|
|
1364
|
+
process.exit(1);
|
|
1365
|
+
}
|
|
1366
|
+
vault2 = v;
|
|
1367
|
+
} else if (args2[i] === "--limit") {
|
|
1368
|
+
const tok = args2[++i];
|
|
1369
|
+
if (tok === undefined || !/^[0-9]+$/.test(tok)) {
|
|
1370
|
+
console.error("--limit must be a positive integer");
|
|
1371
|
+
process.exit(1);
|
|
1372
|
+
}
|
|
1373
|
+
const n = parseInt(tok, 10);
|
|
1374
|
+
if (!Number.isFinite(n) || n <= 0) {
|
|
1375
|
+
console.error("--limit must be a positive integer");
|
|
1376
|
+
process.exit(1);
|
|
1377
|
+
}
|
|
1378
|
+
limit = n;
|
|
1379
|
+
} else
|
|
1380
|
+
positional2.push(args2[i]);
|
|
1381
|
+
}
|
|
1382
|
+
query = positional2.join(" ").trim();
|
|
1383
|
+
if (!query) {
|
|
1384
|
+
console.error('usage: imprnt recall "<query>" [--vault DIR] [--limit N]');
|
|
1385
|
+
process.exit(1);
|
|
1386
|
+
}
|
|
1387
|
+
vocab = loadTags(vault2);
|
|
1388
|
+
MAX_SYNONYM_NGRAM = Math.max(1, ...[...vocab.synonyms.keys()].map((k) => k.trim().split(/[\s-]+/).filter(Boolean).length));
|
|
1389
|
+
STOPWORDS = new Set([
|
|
1390
|
+
"what",
|
|
1391
|
+
"where",
|
|
1392
|
+
"how",
|
|
1393
|
+
"when",
|
|
1394
|
+
"which",
|
|
1395
|
+
"who",
|
|
1396
|
+
"whom",
|
|
1397
|
+
"whose",
|
|
1398
|
+
"why",
|
|
1399
|
+
"a",
|
|
1400
|
+
"an",
|
|
1401
|
+
"the",
|
|
1402
|
+
"this",
|
|
1403
|
+
"that",
|
|
1404
|
+
"these",
|
|
1405
|
+
"those",
|
|
1406
|
+
"i",
|
|
1407
|
+
"me",
|
|
1408
|
+
"my",
|
|
1409
|
+
"mine",
|
|
1410
|
+
"myself",
|
|
1411
|
+
"we",
|
|
1412
|
+
"us",
|
|
1413
|
+
"our",
|
|
1414
|
+
"ours",
|
|
1415
|
+
"you",
|
|
1416
|
+
"your",
|
|
1417
|
+
"yours",
|
|
1418
|
+
"he",
|
|
1419
|
+
"him",
|
|
1420
|
+
"his",
|
|
1421
|
+
"she",
|
|
1422
|
+
"her",
|
|
1423
|
+
"hers",
|
|
1424
|
+
"it",
|
|
1425
|
+
"its",
|
|
1426
|
+
"they",
|
|
1427
|
+
"them",
|
|
1428
|
+
"their",
|
|
1429
|
+
"theirs",
|
|
1430
|
+
"and",
|
|
1431
|
+
"or",
|
|
1432
|
+
"but",
|
|
1433
|
+
"nor",
|
|
1434
|
+
"so",
|
|
1435
|
+
"than",
|
|
1436
|
+
"if",
|
|
1437
|
+
"then",
|
|
1438
|
+
"of",
|
|
1439
|
+
"to",
|
|
1440
|
+
"in",
|
|
1441
|
+
"on",
|
|
1442
|
+
"for",
|
|
1443
|
+
"with",
|
|
1444
|
+
"from",
|
|
1445
|
+
"by",
|
|
1446
|
+
"at",
|
|
1447
|
+
"as",
|
|
1448
|
+
"into",
|
|
1449
|
+
"onto",
|
|
1450
|
+
"about",
|
|
1451
|
+
"over",
|
|
1452
|
+
"under",
|
|
1453
|
+
"between",
|
|
1454
|
+
"through",
|
|
1455
|
+
"be",
|
|
1456
|
+
"am",
|
|
1457
|
+
"is",
|
|
1458
|
+
"are",
|
|
1459
|
+
"was",
|
|
1460
|
+
"were",
|
|
1461
|
+
"been",
|
|
1462
|
+
"being",
|
|
1463
|
+
"have",
|
|
1464
|
+
"has",
|
|
1465
|
+
"had",
|
|
1466
|
+
"do",
|
|
1467
|
+
"does",
|
|
1468
|
+
"did",
|
|
1469
|
+
"done",
|
|
1470
|
+
"can",
|
|
1471
|
+
"could",
|
|
1472
|
+
"will",
|
|
1473
|
+
"would",
|
|
1474
|
+
"shall",
|
|
1475
|
+
"should",
|
|
1476
|
+
"may",
|
|
1477
|
+
"might",
|
|
1478
|
+
"must",
|
|
1479
|
+
"not",
|
|
1480
|
+
"no",
|
|
1481
|
+
"yes",
|
|
1482
|
+
"there",
|
|
1483
|
+
"here",
|
|
1484
|
+
"think",
|
|
1485
|
+
"know",
|
|
1486
|
+
"believe"
|
|
1487
|
+
]);
|
|
1488
|
+
rawTerms = tokenize(query);
|
|
1489
|
+
contentTerms = rawTerms.filter((w) => !STOPWORDS.has(w));
|
|
1490
|
+
baseTerms = contentTerms.length ? contentTerms : rawTerms;
|
|
1491
|
+
queryTerms = baseTerms.map((w) => [...new Set([w, ...tokenize(normalize(vocab, w))])]);
|
|
1492
|
+
for (const t of phraseSynonymTokens(query)) {
|
|
1493
|
+
if (!STOPWORDS.has(t))
|
|
1494
|
+
queryTerms.push([t]);
|
|
1495
|
+
}
|
|
1496
|
+
if (!queryTerms.length) {
|
|
1497
|
+
console.error("empty query after tokenizing");
|
|
1498
|
+
process.exit(1);
|
|
1499
|
+
}
|
|
1500
|
+
CONTROL2 = new Set(["index.md", "hot.md", "log.md", "_tags.md"]);
|
|
1501
|
+
files = [];
|
|
1502
|
+
try {
|
|
1503
|
+
files = walk2(vault2);
|
|
1504
|
+
} catch {
|
|
1505
|
+
console.error(`no vault at ${vault2} — run \`imprnt init\` first`);
|
|
1506
|
+
process.exit(1);
|
|
1507
|
+
}
|
|
1508
|
+
docs = [];
|
|
1509
|
+
df = new Map;
|
|
1510
|
+
for (const path of files) {
|
|
1511
|
+
const raw = stripBom(readFileSync10(path, "utf8"));
|
|
1512
|
+
const fmMatch = raw.match(/^---\r?\n([\s\S]*?)\r?\n---/);
|
|
1513
|
+
const fm2 = fmMatch?.[1] ?? "";
|
|
1514
|
+
const body2 = fmMatch ? raw.slice(fmMatch.index + fmMatch[0].length) : raw;
|
|
1515
|
+
const titleText = stripCode(body2).match(/^#[ \t]+(\S.*)$/m)?.[1] ?? "";
|
|
1516
|
+
const aliases = fmList(fm2, "aliases").join(" ");
|
|
1517
|
+
const tags = fmList(fm2, "tags").map((t) => normalize(vocab, t));
|
|
1518
|
+
const tf = new Map;
|
|
1519
|
+
const add = (tokens, weight) => {
|
|
1520
|
+
for (const t of tokens)
|
|
1521
|
+
tf.set(t, (tf.get(t) ?? 0) + weight);
|
|
1522
|
+
};
|
|
1523
|
+
add([...tokenize(titleText), ...tokenize(basename3(path, ".md"))], TITLE_BOOST);
|
|
1524
|
+
add(tokenize(aliases), TITLE_BOOST);
|
|
1525
|
+
add(tags.flatMap(tokenize), TAG_BOOST);
|
|
1526
|
+
add(tokenize(body2), BODY_BOOST);
|
|
1527
|
+
let len = 0;
|
|
1528
|
+
for (const c of tf.values())
|
|
1529
|
+
len += c;
|
|
1530
|
+
docs.push({ path, tf, len });
|
|
1531
|
+
for (const term of tf.keys())
|
|
1532
|
+
df.set(term, (df.get(term) ?? 0) + 1);
|
|
1533
|
+
}
|
|
1534
|
+
N = docs.length || 1;
|
|
1535
|
+
avgdl = docs.reduce((s, d) => s + d.len, 0) / N || 1;
|
|
1536
|
+
variantIdf = new Map;
|
|
1537
|
+
for (const group of queryTerms)
|
|
1538
|
+
for (const v of group)
|
|
1539
|
+
if (!variantIdf.has(v))
|
|
1540
|
+
variantIdf.set(v, idf(v));
|
|
1541
|
+
hits = [];
|
|
1542
|
+
scoringGroups = [...queryTerms].sort((a, b) => a.length - b.length);
|
|
1543
|
+
for (const d of docs) {
|
|
1544
|
+
let score = 0;
|
|
1545
|
+
const scored = new Set;
|
|
1546
|
+
for (const group of scoringGroups) {
|
|
1547
|
+
let best = 0;
|
|
1548
|
+
let bestVariant = "";
|
|
1549
|
+
for (const v of group) {
|
|
1550
|
+
if (scored.has(v))
|
|
1551
|
+
continue;
|
|
1552
|
+
const s = bm25Term(d.tf.get(v) ?? 0, d.len, variantIdf.get(v) ?? 0);
|
|
1553
|
+
if (s > best) {
|
|
1554
|
+
best = s;
|
|
1555
|
+
bestVariant = v;
|
|
1556
|
+
}
|
|
1557
|
+
}
|
|
1558
|
+
if (best > 0) {
|
|
1559
|
+
score += best;
|
|
1560
|
+
scored.add(bestVariant);
|
|
1561
|
+
}
|
|
1562
|
+
}
|
|
1563
|
+
if (score > 0)
|
|
1564
|
+
hits.push({ path: relative4(vault2, d.path), score: Math.round(score * 100) / 100 });
|
|
1565
|
+
}
|
|
1566
|
+
hits.sort((a, b) => b.score - a.score || a.path.localeCompare(b.path));
|
|
1567
|
+
if (!hits.length) {
|
|
1568
|
+
console.log(`no matches for "${query}" in ${vault2}`);
|
|
1569
|
+
process.exit(0);
|
|
1570
|
+
}
|
|
1571
|
+
shown = hits.slice(0, limit);
|
|
1572
|
+
expanded = queryTerms.map((g) => g.join("|")).join(" ");
|
|
1573
|
+
console.log(`recall "${query}" [${expanded}] — ${hits.length} match(es)${hits.length > shown.length ? `, showing top ${shown.length}` : ""}, BM25-ranked:
|
|
1574
|
+
`);
|
|
1575
|
+
for (const h of shown)
|
|
1576
|
+
console.log(` [${h.score.toFixed(2)}] ${h.path}`);
|
|
1577
|
+
if (hits.length > shown.length) {
|
|
1578
|
+
console.log(`
|
|
1579
|
+
… ${hits.length - shown.length} lower-ranked hit(s) hidden. Raise with --limit if needed; usually you don't.`);
|
|
1580
|
+
}
|
|
1581
|
+
});
|
|
1582
|
+
|
|
1583
|
+
// scripts/snapshot.ts
|
|
1584
|
+
var exports_snapshot = {};
|
|
1585
|
+
import { readdirSync as readdirSync6, readFileSync as readFileSync11, copyFileSync as copyFileSync2, mkdirSync as mkdirSync3, existsSync as existsSync10, statSync as statSync6, lstatSync as lstatSync3 } from "node:fs";
|
|
1586
|
+
import { createHash as createHash2 } from "node:crypto";
|
|
1587
|
+
import { join as join12, relative as relative5, basename as basename4, extname as extname2, resolve as resolve5, sep as sep3 } from "node:path";
|
|
1588
|
+
function destWithinRaw(rawRoot, dest) {
|
|
1589
|
+
const rootResolved = resolve5(rawRoot);
|
|
1590
|
+
const destResolved = resolve5(rootResolved, dest);
|
|
1591
|
+
if (destResolved !== rootResolved && !destResolved.startsWith(rootResolved + sep3))
|
|
1592
|
+
return null;
|
|
1593
|
+
return destResolved;
|
|
1594
|
+
}
|
|
1595
|
+
function collect(p, base) {
|
|
1596
|
+
let skippedLinks = 0;
|
|
1597
|
+
const st = statSync6(p);
|
|
1598
|
+
if (st.isFile())
|
|
1599
|
+
return { files: [{ abs: p, rel: basename4(p) }], skippedLinks };
|
|
1600
|
+
const out = [];
|
|
1601
|
+
const walk3 = (dir2) => {
|
|
1602
|
+
for (const entry of readdirSync6(dir2)) {
|
|
1603
|
+
if (SKIP.has(entry))
|
|
1604
|
+
continue;
|
|
1605
|
+
const f = join12(dir2, entry);
|
|
1606
|
+
const ls = lstatSync3(f);
|
|
1607
|
+
if (ls.isSymbolicLink()) {
|
|
1608
|
+
let target;
|
|
1609
|
+
try {
|
|
1610
|
+
target = statSync6(f);
|
|
1611
|
+
} catch {
|
|
1612
|
+
skippedLinks++;
|
|
1613
|
+
continue;
|
|
1614
|
+
}
|
|
1615
|
+
if (target.isFile())
|
|
1616
|
+
out.push({ abs: f, rel: relative5(base, f) });
|
|
1617
|
+
else
|
|
1618
|
+
skippedLinks++;
|
|
1619
|
+
continue;
|
|
1620
|
+
}
|
|
1621
|
+
if (ls.isDirectory())
|
|
1622
|
+
walk3(f);
|
|
1623
|
+
else if (ls.isFile())
|
|
1624
|
+
out.push({ abs: f, rel: relative5(base, f) });
|
|
1625
|
+
}
|
|
1626
|
+
};
|
|
1627
|
+
walk3(p);
|
|
1628
|
+
return { files: out, skippedLinks };
|
|
1629
|
+
}
|
|
1630
|
+
var args3, vault3, dest = "", positional3, src2, rawRoot, destRoot, manifest2, SKIP, files2, skippedLinks, copied = 0, unchanged = 0, disambiguated = 0;
|
|
1631
|
+
var init_snapshot = __esm(() => {
|
|
1632
|
+
init_manifest();
|
|
1633
|
+
args3 = process.argv.slice(2);
|
|
1634
|
+
vault3 = process.env.IMPRNT_VAULT ?? process.env.IMPRINT_VAULT ?? "./vault";
|
|
1635
|
+
positional3 = [];
|
|
1636
|
+
for (let i = 0;i < args3.length; i++) {
|
|
1637
|
+
if (args3[i] === "--vault") {
|
|
1638
|
+
const v = args3[++i];
|
|
1639
|
+
if (v === undefined) {
|
|
1640
|
+
console.error("--vault requires a directory argument");
|
|
1641
|
+
process.exit(1);
|
|
1642
|
+
}
|
|
1643
|
+
vault3 = v;
|
|
1644
|
+
} else if (args3[i] === "--dest") {
|
|
1645
|
+
const d = args3[++i];
|
|
1646
|
+
if (d === undefined) {
|
|
1647
|
+
console.error("--dest requires a path argument");
|
|
1648
|
+
process.exit(1);
|
|
1649
|
+
}
|
|
1650
|
+
dest = d;
|
|
1651
|
+
} else
|
|
1652
|
+
positional3.push(args3[i]);
|
|
1653
|
+
}
|
|
1654
|
+
src2 = positional3[0];
|
|
1655
|
+
if (!src2 || !dest) {
|
|
1656
|
+
console.error("usage: imprnt snapshot <src> --dest <relpath> [--vault DIR]");
|
|
1657
|
+
process.exit(1);
|
|
1658
|
+
}
|
|
1659
|
+
if (!existsSync10(src2)) {
|
|
1660
|
+
console.error(`no such source: ${src2}`);
|
|
1661
|
+
process.exit(1);
|
|
1662
|
+
}
|
|
1663
|
+
rawRoot = join12(vault3, "..", "raw");
|
|
1664
|
+
destRoot = destWithinRaw(rawRoot, dest);
|
|
1665
|
+
if (!destRoot) {
|
|
1666
|
+
console.error(`refusing --dest '${dest}': it escapes raw/ (resolves outside ${resolve5(rawRoot)}). raw/ is immutable — pick a dest inside it.`);
|
|
1667
|
+
process.exit(1);
|
|
1668
|
+
}
|
|
1669
|
+
manifest2 = loadManifest(vault3);
|
|
1670
|
+
SKIP = new Set([".git", ".DS_Store", "node_modules", ".manifest.json"]);
|
|
1671
|
+
({ files: files2, skippedLinks } = collect(src2, src2));
|
|
1672
|
+
for (const { abs, rel } of files2) {
|
|
1673
|
+
const srcBytes2 = readFileSync11(abs);
|
|
1674
|
+
const hash2 = createHash2("sha256").update(srcBytes2).digest("hex").slice(0, 16);
|
|
1675
|
+
let rawPath2 = join12(destRoot, rel);
|
|
1676
|
+
let key = join12("raw", dest, rel);
|
|
1677
|
+
if (manifest2[key]?.hash === hash2 && existsSync10(rawPath2)) {
|
|
1678
|
+
unchanged++;
|
|
1679
|
+
continue;
|
|
1680
|
+
}
|
|
1681
|
+
if (existsSync10(rawPath2)) {
|
|
1682
|
+
if (Buffer.compare(readFileSync11(rawPath2), srcBytes2) === 0) {
|
|
1683
|
+
manifest2[key] = { hash: hash2, note: manifest2[key]?.note ?? "", ingested: new Date().toISOString(), raw: key, src: abs };
|
|
1684
|
+
unchanged++;
|
|
1685
|
+
continue;
|
|
1686
|
+
}
|
|
1687
|
+
const ext = extname2(rel);
|
|
1688
|
+
const stem = rel.slice(0, rel.length - ext.length);
|
|
1689
|
+
let relD = `${stem}-${hash2.slice(0, 8)}${ext}`;
|
|
1690
|
+
let n = 2;
|
|
1691
|
+
while (existsSync10(join12(destRoot, relD)) && Buffer.compare(readFileSync11(join12(destRoot, relD)), srcBytes2) !== 0) {
|
|
1692
|
+
relD = `${stem}-${hash2.slice(0, 8)}-${n}${ext}`;
|
|
1693
|
+
n++;
|
|
1694
|
+
}
|
|
1695
|
+
rawPath2 = join12(destRoot, relD);
|
|
1696
|
+
key = join12("raw", dest, relD);
|
|
1697
|
+
if (existsSync10(rawPath2)) {
|
|
1698
|
+
manifest2[key] = { hash: hash2, note: manifest2[key]?.note ?? "", ingested: new Date().toISOString(), raw: key, src: abs };
|
|
1699
|
+
unchanged++;
|
|
1700
|
+
continue;
|
|
1701
|
+
}
|
|
1702
|
+
console.log(` ! raw/${join12(dest, rel)} already holds different bytes - immutable, writing raw/${join12(dest, relD)} instead`);
|
|
1703
|
+
disambiguated++;
|
|
1704
|
+
}
|
|
1705
|
+
mkdirSync3(join12(rawPath2, ".."), { recursive: true });
|
|
1706
|
+
copyFileSync2(abs, rawPath2);
|
|
1707
|
+
manifest2[key] = { hash: hash2, note: "", ingested: new Date().toISOString(), raw: key, src: abs };
|
|
1708
|
+
copied++;
|
|
1709
|
+
}
|
|
1710
|
+
saveManifest(vault3, manifest2);
|
|
1711
|
+
console.log(`snapshot ${src2} → raw/${dest}/`);
|
|
1712
|
+
console.log(` ${copied} copied, ${unchanged} unchanged (immutable). ${files2.length} file(s) total.`);
|
|
1713
|
+
if (disambiguated)
|
|
1714
|
+
console.log(` ${disambiguated} filed under a disambiguated name - an existing raw/ snapshot is never overwritten.`);
|
|
1715
|
+
if (skippedLinks)
|
|
1716
|
+
console.log(` ${skippedLinks} symlink(s) skipped (dangling or directory links).`);
|
|
1717
|
+
if (copied)
|
|
1718
|
+
console.log(` next: the LLM reads raw/${dest}/ and fans sources out into vault notes (source: raw/${dest}/...).`);
|
|
1719
|
+
});
|
|
1720
|
+
|
|
1721
|
+
// scripts/check.ts
|
|
1722
|
+
var exports_check = {};
|
|
1723
|
+
import { readdirSync as readdirSync7, readFileSync as readFileSync12, statSync as statSync7, existsSync as existsSync11, writeFileSync as writeFileSync8 } from "node:fs";
|
|
1724
|
+
import { spawnSync as spawnSync3 } from "node:child_process";
|
|
1725
|
+
import { join as join13, relative as relative6, dirname as dirname3 } from "node:path";
|
|
1726
|
+
function resolves(target) {
|
|
1727
|
+
const t = target.trim().replace(/^\.\//, "").replace(/\.md$/, "");
|
|
1728
|
+
if (!t)
|
|
1729
|
+
return false;
|
|
1730
|
+
if (t.includes("/"))
|
|
1731
|
+
return allSlugs.has(t);
|
|
1732
|
+
return byBasename.has(t);
|
|
1733
|
+
}
|
|
1734
|
+
function targetFolders(target) {
|
|
1735
|
+
const t = target.trim().replace(/^\.\//, "").replace(/\.md$/, "");
|
|
1736
|
+
if (!t)
|
|
1737
|
+
return [];
|
|
1738
|
+
if (t.includes("/")) {
|
|
1739
|
+
const f = folderOf.get(t);
|
|
1740
|
+
return f ? [f] : [];
|
|
1741
|
+
}
|
|
1742
|
+
return (byBasename.get(t) ?? []).map((s) => folderOf.get(s)).filter((f) => !!f);
|
|
1743
|
+
}
|
|
1744
|
+
function linksEntity(target) {
|
|
1745
|
+
return targetFolders(target).some((f) => ENTITY_FOLDERS.has(f));
|
|
1746
|
+
}
|
|
1747
|
+
function syncNeedsReview(lines2) {
|
|
1748
|
+
const p = join13(vault4, "_needs-review.md");
|
|
1749
|
+
const exists = existsSync11(p);
|
|
1750
|
+
if (!exists && lines2.length === 0)
|
|
1751
|
+
return "none";
|
|
1752
|
+
const prev = exists ? readFileSync12(p, "utf8") : `---
|
|
1753
|
+
type: needs-review
|
|
1754
|
+
---
|
|
1755
|
+
|
|
1756
|
+
# Needs review
|
|
1757
|
+
|
|
1758
|
+
`;
|
|
1759
|
+
const b = prev.indexOf(REVIEW_BEGIN);
|
|
1760
|
+
const e = b !== -1 ? prev.indexOf(REVIEW_END, b + REVIEW_BEGIN.length) : -1;
|
|
1761
|
+
const section2 = `${REVIEW_BEGIN}
|
|
1762
|
+
${lines2.join(`
|
|
1763
|
+
`)}
|
|
1764
|
+
${REVIEW_END}
|
|
1765
|
+
`;
|
|
1766
|
+
let next;
|
|
1767
|
+
if (b !== -1) {
|
|
1768
|
+
const before = prev.slice(0, b);
|
|
1769
|
+
let after;
|
|
1770
|
+
if (e !== -1) {
|
|
1771
|
+
after = prev.slice(e + REVIEW_END.length).replace(/^\n/, "");
|
|
1772
|
+
} else {
|
|
1773
|
+
after = prev.slice(b + REVIEW_BEGIN.length).replace(/^\n/, "");
|
|
1774
|
+
}
|
|
1775
|
+
next = before + (lines2.length ? section2 : "") + after;
|
|
1776
|
+
} else {
|
|
1777
|
+
if (lines2.length === 0)
|
|
1778
|
+
return "none";
|
|
1779
|
+
next = (prev.endsWith(`
|
|
1780
|
+
`) ? prev : prev + `
|
|
1781
|
+
`) + section2;
|
|
1782
|
+
}
|
|
1783
|
+
if (next !== prev || !exists)
|
|
1784
|
+
writeFileSync8(p, next);
|
|
1785
|
+
return lines2.length ? "written" : "cleared";
|
|
1786
|
+
}
|
|
1787
|
+
var args4, vault4, all = false, ENTITY_FOLDERS, DOMAIN_FOLDERS2, LINK, notes, allSlugs, folderOf, byBasename, orphans, disconnected, domainIssues, untagged, review, referencedRaw, tagVocab, hasRealTag = (tags) => tags.some((t) => /[\p{L}\p{N}]/u.test(normalize(tagVocab, t))), hasTagsFile, addedTags, dupPairs, manifest3, rawEntries, norm = (p) => p.replace(/^\.\//, "").replace(/^.*\/raw\//, "raw/").replace(/\.md$/, ""), refNorm, uncovered, REVIEW_BEGIN = "<!-- imprnt-check:begin (regenerated by `imprnt check` - do not edit between the markers) -->", REVIEW_END = "<!-- imprnt-check:end -->", cap = (xs, n = 25) => xs.slice(0, n).concat(xs.length > n ? [` … +${xs.length - n} more`] : []), count, folders, synced, issues;
|
|
1788
|
+
var init_check = __esm(() => {
|
|
1789
|
+
init_roots();
|
|
1790
|
+
init_moc();
|
|
1791
|
+
init_tags();
|
|
1792
|
+
init_manifest();
|
|
1793
|
+
args4 = process.argv.slice(2);
|
|
1794
|
+
vault4 = process.env.IMPRNT_VAULT ?? process.env.IMPRINT_VAULT ?? "./vault";
|
|
1795
|
+
for (let i = 0;i < args4.length; i++) {
|
|
1796
|
+
if (args4[i] === "--vault") {
|
|
1797
|
+
const v = args4[++i];
|
|
1798
|
+
if (v === undefined) {
|
|
1799
|
+
console.error("--vault requires a directory argument");
|
|
1800
|
+
process.exit(1);
|
|
1801
|
+
}
|
|
1802
|
+
vault4 = v;
|
|
1803
|
+
} else if (args4[i] === "--all")
|
|
1804
|
+
all = true;
|
|
1805
|
+
}
|
|
1806
|
+
if (!existsSync11(vault4)) {
|
|
1807
|
+
console.error(`no vault at ${vault4} — run \`imprnt init\` first`);
|
|
1808
|
+
process.exit(1);
|
|
1809
|
+
}
|
|
1810
|
+
ENTITY_FOLDERS = new Set(["people", "orgs", "holdings"]);
|
|
1811
|
+
DOMAIN_FOLDERS2 = new Set(["identity", "health", "finances", "work", "life"]);
|
|
1812
|
+
LINK = /\[\[([^\]|#]+)(?:#[^\]|]+)?(?:\|[^\]]+)?\]\]/g;
|
|
1813
|
+
notes = collectNotes(vault4);
|
|
1814
|
+
allSlugs = new Set(notes.map((n) => n.slug));
|
|
1815
|
+
folderOf = new Map(notes.map((n) => [n.slug, n.folder]));
|
|
1816
|
+
byBasename = new Map;
|
|
1817
|
+
for (const n of notes) {
|
|
1818
|
+
const base = n.slug.includes("/") ? n.slug.slice(n.slug.lastIndexOf("/") + 1) : n.slug;
|
|
1819
|
+
(byBasename.get(base) ?? byBasename.set(base, []).get(base)).push(n.slug);
|
|
1820
|
+
}
|
|
1821
|
+
orphans = [];
|
|
1822
|
+
disconnected = [];
|
|
1823
|
+
domainIssues = [];
|
|
1824
|
+
untagged = [];
|
|
1825
|
+
review = [];
|
|
1826
|
+
referencedRaw = new Set;
|
|
1827
|
+
tagVocab = loadTags(vault4);
|
|
1828
|
+
for (const n of notes) {
|
|
1829
|
+
const raw = readFileSync12(n.path, "utf8");
|
|
1830
|
+
const fm2 = frontmatter(raw);
|
|
1831
|
+
const links = [...stripCode(raw).matchAll(LINK)].map((m) => m[1].trim()).filter((l) => !l.startsWith("raw/"));
|
|
1832
|
+
for (const l of links)
|
|
1833
|
+
if (!resolves(l)) {
|
|
1834
|
+
orphans.push(` ${n.slug} → [[${l}]]`);
|
|
1835
|
+
review.push(`- [ ] orphan link [[${l}]] — from [[${n.slug}]], target note missing`);
|
|
1836
|
+
}
|
|
1837
|
+
if (!ENTITY_FOLDERS.has(n.folder) && !links.some(linksEntity)) {
|
|
1838
|
+
disconnected.push(` ${n.slug}`);
|
|
1839
|
+
review.push(`- [ ] disconnected note [[${n.slug}]] — links no entity`);
|
|
1840
|
+
}
|
|
1841
|
+
if (!hasRealTag(n.tags)) {
|
|
1842
|
+
untagged.push(` ${n.slug}`);
|
|
1843
|
+
review.push(`- [ ] untagged note [[${n.slug}]] — empty tags, findable by body/title only`);
|
|
1844
|
+
}
|
|
1845
|
+
const domainRaw = (fm2.match(/^domain:\s*(.+)$/m)?.[1] ?? "").trim();
|
|
1846
|
+
const domain = stripQuotes(/^["']/.test(domainRaw) ? domainRaw : domainRaw.replace(/\s+#.*$/, "").trim());
|
|
1847
|
+
if (DOMAIN_FOLDERS2.has(n.folder) && domain !== n.folder) {
|
|
1848
|
+
domainIssues.push(` ${n.slug} — in ${n.folder}/ but domain: ${domain || "(missing)"}`);
|
|
1849
|
+
review.push(`- [ ] domain mismatch [[${n.slug}]] — in ${n.folder}/ but domain: ${domain || "(missing)"}`);
|
|
1850
|
+
}
|
|
1851
|
+
const src3 = fm2.match(/^source:\s*["']?(.+?)["']?\s*$/im)?.[1]?.trim().replace(/^\[\[/, "").replace(/\]\]$/, "");
|
|
1852
|
+
if (src3)
|
|
1853
|
+
referencedRaw.add(src3.replace(/^\.\//, ""));
|
|
1854
|
+
for (const s of fmList(fm2, "sources"))
|
|
1855
|
+
referencedRaw.add(s.replace(/^\[\[/, "").replace(/\]\]$/, "").replace(/^\.\//, ""));
|
|
1856
|
+
}
|
|
1857
|
+
hasTagsFile = existsSync11(join13(vault4, "_tags.md"));
|
|
1858
|
+
addedTags = [];
|
|
1859
|
+
dupPairs = [];
|
|
1860
|
+
if (hasTagsFile) {
|
|
1861
|
+
const vocab2 = loadTags(vault4);
|
|
1862
|
+
const usedCanon = new Set;
|
|
1863
|
+
for (const n of notes)
|
|
1864
|
+
for (const t of n.tags) {
|
|
1865
|
+
const c = normalize(vocab2, t);
|
|
1866
|
+
if (c)
|
|
1867
|
+
usedCanon.add(c);
|
|
1868
|
+
}
|
|
1869
|
+
const newTags = [...usedCanon].filter((c) => !vocab2.approved.has(c)).sort();
|
|
1870
|
+
addedTags = appendTags(vault4, newTags);
|
|
1871
|
+
const lev = (a, b) => {
|
|
1872
|
+
const d = Array.from({ length: a.length + 1 }, (_, i) => [i, ...Array(b.length).fill(0)]);
|
|
1873
|
+
for (let j = 0;j <= b.length; j++)
|
|
1874
|
+
d[0][j] = j;
|
|
1875
|
+
for (let i = 1;i <= a.length; i++)
|
|
1876
|
+
for (let j = 1;j <= b.length; j++)
|
|
1877
|
+
d[i][j] = Math.min(d[i - 1][j] + 1, d[i][j - 1] + 1, d[i - 1][j - 1] + (a[i - 1] === b[j - 1] ? 0 : 1));
|
|
1878
|
+
return d[a.length][b.length];
|
|
1879
|
+
};
|
|
1880
|
+
const DIGIT = /[0-9]/;
|
|
1881
|
+
const digitOnlyEdit = (short, long) => {
|
|
1882
|
+
if (short.length === long.length) {
|
|
1883
|
+
const k2 = [...short].findIndex((c, i) => c !== long[i]);
|
|
1884
|
+
return k2 >= 0 && DIGIT.test(short[k2]) && DIGIT.test(long[k2]);
|
|
1885
|
+
}
|
|
1886
|
+
let k = 0;
|
|
1887
|
+
while (k < short.length && short[k] === long[k])
|
|
1888
|
+
k++;
|
|
1889
|
+
return DIGIT.test(long[k]);
|
|
1890
|
+
};
|
|
1891
|
+
const tagArr = [...new Set([...vocab2.approved, ...addedTags])].sort();
|
|
1892
|
+
const byLen = new Map;
|
|
1893
|
+
for (let i = 0;i < tagArr.length; i++)
|
|
1894
|
+
(byLen.get(tagArr[i].length) ?? byLen.set(tagArr[i].length, []).get(tagArr[i].length)).push(i);
|
|
1895
|
+
for (let i = 0;i < tagArr.length; i++) {
|
|
1896
|
+
const a = tagArr[i];
|
|
1897
|
+
const cand = [];
|
|
1898
|
+
for (let L = a.length - 3;L <= a.length + 3; L++) {
|
|
1899
|
+
const bucket = byLen.get(L);
|
|
1900
|
+
if (!bucket)
|
|
1901
|
+
continue;
|
|
1902
|
+
for (const j of bucket)
|
|
1903
|
+
if (j > i)
|
|
1904
|
+
cand.push(j);
|
|
1905
|
+
}
|
|
1906
|
+
cand.sort((x, y) => x - y);
|
|
1907
|
+
for (const j of cand) {
|
|
1908
|
+
const b = tagArr[j];
|
|
1909
|
+
if (normalize(vocab2, a) === normalize(vocab2, b))
|
|
1910
|
+
continue;
|
|
1911
|
+
const short = a.length <= b.length ? a : b, long = a.length <= b.length ? b : a;
|
|
1912
|
+
const prefixDup = short.length >= 4 && long.startsWith(short) && long.length - short.length <= 3;
|
|
1913
|
+
const near = short.length >= 4 && Math.abs(a.length - b.length) <= 1 && lev(a, b) <= 1 && !digitOnlyEdit(short, long);
|
|
1914
|
+
if (prefixDup || near)
|
|
1915
|
+
dupPairs.push(` ${a} ~ ${b}`);
|
|
1916
|
+
}
|
|
1917
|
+
}
|
|
1918
|
+
}
|
|
1919
|
+
manifest3 = loadManifest(vault4);
|
|
1920
|
+
rawEntries = Object.values(manifest3).map((e) => e.raw).filter(Boolean);
|
|
1921
|
+
refNorm = new Set([...referencedRaw].map(norm));
|
|
1922
|
+
uncovered = [...new Set(rawEntries.map(norm))].filter((r) => !refNorm.has(r)).sort();
|
|
1923
|
+
for (const r of uncovered)
|
|
1924
|
+
review.push(`- [ ] unclassified snapshot \`${r}\` — no vault note points back`);
|
|
1925
|
+
console.log(`imprnt check — ${notes.length} notes in ${vault4}
|
|
1926
|
+
`);
|
|
1927
|
+
if (orphans.length) {
|
|
1928
|
+
console.log(`⚠ orphan links (${orphans.length}) — target note missing:`);
|
|
1929
|
+
console.log(cap(orphans).join(`
|
|
1930
|
+
`), `
|
|
1931
|
+
`);
|
|
1932
|
+
} else
|
|
1933
|
+
console.log("✓ no orphan links");
|
|
1934
|
+
if (disconnected.length) {
|
|
1935
|
+
console.log(`⚠ disconnected notes (${disconnected.length}) — domain/form note links no entity:`);
|
|
1936
|
+
console.log(cap(disconnected).join(`
|
|
1937
|
+
`), `
|
|
1938
|
+
`);
|
|
1939
|
+
} else
|
|
1940
|
+
console.log("✓ every domain/form note links the graph");
|
|
1941
|
+
if (domainIssues.length) {
|
|
1942
|
+
console.log(`⚠ domain mismatches (${domainIssues.length}) — folder ≠ domain: field:`);
|
|
1943
|
+
console.log(cap(domainIssues).join(`
|
|
1944
|
+
`), `
|
|
1945
|
+
`);
|
|
1946
|
+
} else
|
|
1947
|
+
console.log("✓ every domain note's folder matches its domain: field");
|
|
1948
|
+
if (untagged.length) {
|
|
1949
|
+
console.log(`⚠ untagged notes (${untagged.length}) — no tags, findable by body/title only:`);
|
|
1950
|
+
console.log(cap(untagged).join(`
|
|
1951
|
+
`), `
|
|
1952
|
+
`);
|
|
1953
|
+
} else
|
|
1954
|
+
console.log("✓ every note carries at least one tag");
|
|
1955
|
+
if (hasTagsFile) {
|
|
1956
|
+
if (addedTags.length)
|
|
1957
|
+
console.log(`↑ synced ${addedTags.length} new tag(s) into _tags.md: ${addedTags.join(", ")}`);
|
|
1958
|
+
else
|
|
1959
|
+
console.log("✓ tag vocabulary in sync");
|
|
1960
|
+
if (dupPairs.length) {
|
|
1961
|
+
console.log(`⚠ candidate duplicate tags (${dupPairs.length}) — add a synonym in _tags.md to merge:`);
|
|
1962
|
+
console.log(cap(dupPairs).join(`
|
|
1963
|
+
`), `
|
|
1964
|
+
`);
|
|
1965
|
+
}
|
|
1966
|
+
}
|
|
1967
|
+
if (rawEntries.length) {
|
|
1968
|
+
if (uncovered.length) {
|
|
1969
|
+
console.log(`⚠ uncovered snapshots (${uncovered.length}/${new Set(rawEntries.map(norm)).size}) — raw source no note points back to:`);
|
|
1970
|
+
console.log(cap(uncovered).join(`
|
|
1971
|
+
`), `
|
|
1972
|
+
`);
|
|
1973
|
+
} else
|
|
1974
|
+
console.log("✓ every raw snapshot has a derived note");
|
|
1975
|
+
}
|
|
1976
|
+
({ count, folders } = generateIndex(vault4));
|
|
1977
|
+
console.log(`↻ regenerated index.md — ${count} notes across ${folders} folders`);
|
|
1978
|
+
synced = syncNeedsReview(review);
|
|
1979
|
+
if (synced === "written")
|
|
1980
|
+
console.log(`↻ ${review.length} finding(s) → _needs-review.md (run \`imprnt hot\` to see them)`);
|
|
1981
|
+
else if (synced === "cleared")
|
|
1982
|
+
console.log("↻ cleared resolved findings from _needs-review.md");
|
|
1983
|
+
issues = orphans.length + disconnected.length + domainIssues.length + untagged.length + uncovered.length + dupPairs.length;
|
|
1984
|
+
console.log(issues ? `
|
|
1985
|
+
${issues} thing(s) to look at above.` : `
|
|
1986
|
+
clean.`);
|
|
1987
|
+
if (all) {
|
|
1988
|
+
const pluginsDir = join13(projectRoot(), "plugins");
|
|
1989
|
+
const checks = [];
|
|
1990
|
+
if (existsSync11(pluginsDir)) {
|
|
1991
|
+
for (const entry of readdirSync7(pluginsDir)) {
|
|
1992
|
+
let isDir2 = false;
|
|
1993
|
+
try {
|
|
1994
|
+
isDir2 = statSync7(join13(pluginsDir, entry)).isDirectory();
|
|
1995
|
+
} catch {
|
|
1996
|
+
continue;
|
|
1997
|
+
}
|
|
1998
|
+
const p = join13(pluginsDir, entry, "check.js");
|
|
1999
|
+
if (isDir2 && existsSync11(p))
|
|
2000
|
+
checks.push(p);
|
|
2001
|
+
}
|
|
2002
|
+
}
|
|
2003
|
+
checks.sort();
|
|
2004
|
+
console.log(`
|
|
2005
|
+
— plugins (${checks.length}) —`);
|
|
2006
|
+
if (checks.length === 0)
|
|
2007
|
+
console.log(" (no plugins/*/check.js found)");
|
|
2008
|
+
let failed = 0;
|
|
2009
|
+
for (const checkPath of checks) {
|
|
2010
|
+
const name = relative6(pluginsDir, dirname3(checkPath)).split("\\").join("/");
|
|
2011
|
+
const proc = spawnSync3(process.execPath, [checkPath], { stdio: "inherit" });
|
|
2012
|
+
const code = proc.status ?? 1;
|
|
2013
|
+
const ok = code === 0;
|
|
2014
|
+
if (!ok)
|
|
2015
|
+
failed++;
|
|
2016
|
+
console.log(` ${ok ? "✓" : "✗"} plugins/${name}/check.js → exit ${code}`);
|
|
2017
|
+
}
|
|
2018
|
+
if (failed)
|
|
2019
|
+
console.log(`
|
|
2020
|
+
${failed} plugin check(s) failed.`);
|
|
2021
|
+
else if (checks.length)
|
|
2022
|
+
console.log(`
|
|
2023
|
+
all plugin checks passed.`);
|
|
2024
|
+
if (failed || issues)
|
|
2025
|
+
process.exit(1);
|
|
2026
|
+
} else if (issues) {
|
|
2027
|
+
process.exit(1);
|
|
2028
|
+
}
|
|
2029
|
+
});
|
|
2030
|
+
|
|
2031
|
+
// scripts/cli.ts
|
|
2032
|
+
var exports_cli = {};
|
|
2033
|
+
import { cpSync as cpSync2, mkdirSync as mkdirSync4, existsSync as existsSync12, readFileSync as readFileSync13 } from "node:fs";
|
|
2034
|
+
import { join as join14, dirname as dirname4 } from "node:path";
|
|
2035
|
+
import { fileURLToPath } from "node:url";
|
|
2036
|
+
function vaultArg() {
|
|
2037
|
+
const i = rest.indexOf("--vault");
|
|
2038
|
+
if (i >= 0) {
|
|
2039
|
+
const val = rest[i + 1];
|
|
2040
|
+
if (val === undefined) {
|
|
2041
|
+
console.error("usage: --vault <dir> (missing directory after --vault)");
|
|
2042
|
+
process.exit(1);
|
|
2043
|
+
}
|
|
2044
|
+
return val;
|
|
2045
|
+
}
|
|
2046
|
+
return process.env.IMPRNT_VAULT || process.env.IMPRINT_VAULT || "./vault";
|
|
2047
|
+
}
|
|
2048
|
+
function requireVaultHome() {
|
|
2049
|
+
const home = vaultProjectRoot();
|
|
2050
|
+
if (!home) {
|
|
2051
|
+
console.error("no vault project found — run `imprnt init` in your vault project first");
|
|
2052
|
+
process.exit(1);
|
|
2053
|
+
}
|
|
2054
|
+
return home;
|
|
2055
|
+
}
|
|
2056
|
+
var here, pkgRoot, cmd, rest, asImp;
|
|
2057
|
+
var init_cli = __esm(async () => {
|
|
2058
|
+
init_resolve();
|
|
2059
|
+
init_plugins();
|
|
2060
|
+
init_install();
|
|
2061
|
+
init_roots();
|
|
2062
|
+
init_moc();
|
|
2063
|
+
init_registry();
|
|
2064
|
+
init_launch();
|
|
2065
|
+
here = dirname4(fileURLToPath(import.meta.url));
|
|
2066
|
+
pkgRoot = join14(here, "..");
|
|
2067
|
+
[cmd, ...rest] = process.argv.slice(2);
|
|
2068
|
+
asImp = globalThis.__IMPRNT_IMP__ === true;
|
|
2069
|
+
if (["ingest", "recall", "snapshot", "check"].includes(cmd))
|
|
2070
|
+
process.argv.splice(2, 1);
|
|
2071
|
+
switch (cmd) {
|
|
2072
|
+
case "ingest":
|
|
2073
|
+
await Promise.resolve().then(() => (init_ingest(), exports_ingest));
|
|
2074
|
+
break;
|
|
2075
|
+
case "recall":
|
|
2076
|
+
await Promise.resolve().then(() => (init_recall(), exports_recall));
|
|
2077
|
+
break;
|
|
2078
|
+
case "snapshot":
|
|
2079
|
+
await Promise.resolve().then(() => (init_snapshot(), exports_snapshot));
|
|
2080
|
+
break;
|
|
2081
|
+
case "check":
|
|
2082
|
+
await Promise.resolve().then(() => (init_check(), exports_check));
|
|
2083
|
+
break;
|
|
2084
|
+
case "hot": {
|
|
2085
|
+
const vault5 = vaultArg();
|
|
2086
|
+
const review2 = openNeedsReview(vault5);
|
|
2087
|
+
if (review2.length) {
|
|
2088
|
+
console.log(`⚠ NEEDS REVIEW (${review2.length}) — clear these:`);
|
|
2089
|
+
for (const r of review2)
|
|
2090
|
+
console.log(r);
|
|
2091
|
+
console.log("");
|
|
2092
|
+
}
|
|
2093
|
+
const p = join14(vault5, "hot.md");
|
|
2094
|
+
if (!existsSync12(p)) {
|
|
2095
|
+
console.error(`no hot.md at ${p} — run \`imprnt init\``);
|
|
2096
|
+
process.exit(1);
|
|
2097
|
+
}
|
|
2098
|
+
console.log(readFileSync13(p, "utf8"));
|
|
2099
|
+
break;
|
|
2100
|
+
}
|
|
2101
|
+
case "lair": {
|
|
2102
|
+
const home = requireVaultHome();
|
|
2103
|
+
process.exit(launchClaude(home, rest, childEnv(home)));
|
|
2104
|
+
}
|
|
2105
|
+
case "context": {
|
|
2106
|
+
const home = requireVaultHome();
|
|
2107
|
+
const contract = join14(home, "CLAUDE.md");
|
|
2108
|
+
if (!existsSync12(contract)) {
|
|
2109
|
+
console.error(`no CLAUDE.md at ${home} — run \`imprnt init\` there to drop the contract`);
|
|
2110
|
+
process.exit(1);
|
|
2111
|
+
}
|
|
2112
|
+
process.stdout.write(readFileSync13(contract, "utf8"));
|
|
2113
|
+
break;
|
|
2114
|
+
}
|
|
2115
|
+
case "plugin": {
|
|
2116
|
+
const proj = projectRoot();
|
|
2117
|
+
const [sub, ...specs] = rest;
|
|
2118
|
+
if (sub === "list") {
|
|
2119
|
+
const dirs = listPluginDirs(proj);
|
|
2120
|
+
console.log("plugins:");
|
|
2121
|
+
if (!dirs.length)
|
|
2122
|
+
console.log(" (none installed under plugins/)");
|
|
2123
|
+
for (const name of dirs)
|
|
2124
|
+
console.log(` ${isEnabled(proj, name) ? "[on] " : "[off]"} ${name}`);
|
|
2125
|
+
const available = OFFICIAL.filter((o) => !dirs.includes(o));
|
|
2126
|
+
if (available.length)
|
|
2127
|
+
console.log(`
|
|
2128
|
+
available to add: ${available.join(", ")}`);
|
|
2129
|
+
console.log(`
|
|
2130
|
+
enable: imprnt plugin add <name> disable: imprnt plugin rm <name> [--purge]`);
|
|
2131
|
+
break;
|
|
2132
|
+
}
|
|
2133
|
+
if (sub === "add") {
|
|
2134
|
+
let from;
|
|
2135
|
+
let force = false;
|
|
2136
|
+
const names = [];
|
|
2137
|
+
for (let i = 0;i < specs.length; i++) {
|
|
2138
|
+
if (specs[i] === "--from") {
|
|
2139
|
+
from = specs[++i];
|
|
2140
|
+
if (from === undefined) {
|
|
2141
|
+
console.error("usage: --from <dir> (missing directory after --from)");
|
|
2142
|
+
process.exit(1);
|
|
2143
|
+
}
|
|
2144
|
+
} else if (specs[i] === "--force")
|
|
2145
|
+
force = true;
|
|
2146
|
+
else
|
|
2147
|
+
names.push(specs[i]);
|
|
2148
|
+
}
|
|
2149
|
+
if (!names.length) {
|
|
2150
|
+
console.error("usage: imprnt plugin add <name> [--from <dir>] [--force] | <name>/<file.md>");
|
|
2151
|
+
process.exit(1);
|
|
2152
|
+
}
|
|
2153
|
+
if (from !== undefined && names.length > 1) {
|
|
2154
|
+
console.error(`--from installs one local plugin - name exactly one (got: ${names.join(", ")})`);
|
|
2155
|
+
process.exit(1);
|
|
2156
|
+
}
|
|
2157
|
+
const channel = coreChannel(pkgRoot);
|
|
2158
|
+
let failed = false;
|
|
2159
|
+
for (const name of names) {
|
|
2160
|
+
try {
|
|
2161
|
+
if (name.includes("/")) {
|
|
2162
|
+
const { entry: entry2, added: added2, error: error2 } = addPlugin(proj, name);
|
|
2163
|
+
if (error2) {
|
|
2164
|
+
console.error(`${name}: ${error2}`);
|
|
2165
|
+
failed = true;
|
|
2166
|
+
continue;
|
|
2167
|
+
}
|
|
2168
|
+
console.log(added2 ? `wired @${entry2}` : `already wired @${entry2}`);
|
|
2169
|
+
continue;
|
|
2170
|
+
}
|
|
2171
|
+
const r = installPlugin(proj, name, { from, force, channel });
|
|
2172
|
+
if (r.error) {
|
|
2173
|
+
console.error(`${name}: ${r.error}`);
|
|
2174
|
+
failed = true;
|
|
2175
|
+
continue;
|
|
2176
|
+
}
|
|
2177
|
+
if (r.copied)
|
|
2178
|
+
console.log(`installed ${name} → plugins/${name}/`);
|
|
2179
|
+
else if (r.skipped)
|
|
2180
|
+
console.log(`plugins/${name}/ already present (use --force to refresh)`);
|
|
2181
|
+
const { entry, added, error } = addPlugin(proj, name);
|
|
2182
|
+
if (error) {
|
|
2183
|
+
console.error(`${name}: ${error}${r.copied ? ` (plugins/${name}/ is installed but not wired)` : ""}`);
|
|
2184
|
+
failed = true;
|
|
2185
|
+
continue;
|
|
2186
|
+
}
|
|
2187
|
+
console.log(added ? `wired @${entry}` : `already wired @${entry}`);
|
|
2188
|
+
} catch (e) {
|
|
2189
|
+
console.error(`${name}: ${e instanceof Error ? e.message : String(e)}`);
|
|
2190
|
+
failed = true;
|
|
2191
|
+
}
|
|
2192
|
+
}
|
|
2193
|
+
if (failed)
|
|
2194
|
+
process.exit(1);
|
|
2195
|
+
break;
|
|
2196
|
+
}
|
|
2197
|
+
if (sub === "rm") {
|
|
2198
|
+
let purge = false;
|
|
2199
|
+
const names = [];
|
|
2200
|
+
for (const s of specs) {
|
|
2201
|
+
if (s === "--purge")
|
|
2202
|
+
purge = true;
|
|
2203
|
+
else
|
|
2204
|
+
names.push(s);
|
|
2205
|
+
}
|
|
2206
|
+
if (!names.length) {
|
|
2207
|
+
console.error("usage: imprnt plugin rm <name> [--purge] [<name> ...]");
|
|
2208
|
+
process.exit(1);
|
|
2209
|
+
}
|
|
2210
|
+
let failed = false;
|
|
2211
|
+
for (const name of names) {
|
|
2212
|
+
try {
|
|
2213
|
+
const invalid = specError(proj, name);
|
|
2214
|
+
if (invalid) {
|
|
2215
|
+
console.error(`${name}: ${invalid}`);
|
|
2216
|
+
failed = true;
|
|
2217
|
+
continue;
|
|
2218
|
+
}
|
|
2219
|
+
const removed = rmPlugin(proj, name);
|
|
2220
|
+
let msg = removed ? `unwired ${name} (${removed} line${removed === 1 ? "" : "s"})` : `${name} was not wired`;
|
|
2221
|
+
if (purge)
|
|
2222
|
+
msg += purgePlugin(proj, name) ? `, purged plugins/${name}/` : `, nothing to purge`;
|
|
2223
|
+
console.log(msg);
|
|
2224
|
+
} catch (e) {
|
|
2225
|
+
console.error(`${name}: ${e instanceof Error ? e.message : String(e)}`);
|
|
2226
|
+
failed = true;
|
|
2227
|
+
}
|
|
2228
|
+
}
|
|
2229
|
+
if (failed)
|
|
2230
|
+
process.exit(1);
|
|
2231
|
+
break;
|
|
2232
|
+
}
|
|
2233
|
+
console.error("usage: imprnt plugin list | add <name> [--from <dir>] [--force] | rm <name> [--purge]");
|
|
2234
|
+
process.exit(1);
|
|
2235
|
+
}
|
|
2236
|
+
case "init": {
|
|
2237
|
+
const enclosing = projectRoot();
|
|
2238
|
+
if (enclosing !== process.cwd() && isVaultProject(enclosing)) {
|
|
2239
|
+
console.error(`refusing to init: this directory is inside the vault project at ${enclosing} - run \`imprnt init\` there instead`);
|
|
2240
|
+
process.exit(1);
|
|
2241
|
+
}
|
|
2242
|
+
const entities = ["people", "orgs", "holdings"];
|
|
2243
|
+
const domains = ["identity", "health", "finances", "work", "life", "projects"];
|
|
2244
|
+
const forms = ["events", "mistakes"];
|
|
2245
|
+
const vaultDirs = [...entities, ...domains, ...forms];
|
|
2246
|
+
const vaultPath = join14(process.cwd(), "vault");
|
|
2247
|
+
const vaultExisted = existsSync12(vaultPath);
|
|
2248
|
+
let createdDirs = 0;
|
|
2249
|
+
for (const d of ["vault", ...vaultDirs.map((t) => `vault/${t}`), "raw"]) {
|
|
2250
|
+
const abs = join14(process.cwd(), d);
|
|
2251
|
+
if (!existsSync12(abs))
|
|
2252
|
+
createdDirs++;
|
|
2253
|
+
try {
|
|
2254
|
+
mkdirSync4(abs, { recursive: true });
|
|
2255
|
+
} catch (e) {
|
|
2256
|
+
console.error(`cannot create ./${d}: ${e instanceof Error ? e.message : String(e)}`);
|
|
2257
|
+
process.exit(1);
|
|
2258
|
+
}
|
|
2259
|
+
}
|
|
2260
|
+
const added = [];
|
|
2261
|
+
for (const f of ["index.md", "hot.md", "log.md", "_tags.md"]) {
|
|
2262
|
+
const dst = join14(vaultPath, f);
|
|
2263
|
+
if (!existsSync12(dst)) {
|
|
2264
|
+
cpSync2(join14(pkgRoot, "templates", f), dst);
|
|
2265
|
+
added.push(`vault/${f}`);
|
|
2266
|
+
}
|
|
2267
|
+
}
|
|
2268
|
+
const claudeMd = join14(process.cwd(), "CLAUDE.md");
|
|
2269
|
+
if (!existsSync12(claudeMd) && existsSync12(join14(pkgRoot, "CLAUDE.md"))) {
|
|
2270
|
+
cpSync2(join14(pkgRoot, "CLAUDE.md"), claudeMd);
|
|
2271
|
+
added.push("CLAUDE.md");
|
|
2272
|
+
}
|
|
2273
|
+
const reg = registerVault(process.cwd(), { force: rest.includes("--register") });
|
|
2274
|
+
if (reg.status === "registered")
|
|
2275
|
+
console.log(`registered as imp's default vault project (${configPath()})`);
|
|
2276
|
+
else if (reg.status === "kept")
|
|
2277
|
+
console.log(`kept the existing default vault project (${reg.current}) — run \`imprnt init --register\` here to switch`);
|
|
2278
|
+
else if (reg.status === "error")
|
|
2279
|
+
console.error(`could not register as imp's default vault project (${configPath()}): ${reg.error} — the vault still works via ./vault or IMPRNT_VAULT`);
|
|
2280
|
+
if (!vaultExisted) {
|
|
2281
|
+
console.log("initialized vault at ./vault");
|
|
2282
|
+
console.log(` entities: ${entities.join(", ")}`);
|
|
2283
|
+
console.log(` domains: ${domains.join(", ")}`);
|
|
2284
|
+
console.log(` forms: ${forms.join(", ")}`);
|
|
2285
|
+
console.log(" + raw/ for immutable by-source snapshots");
|
|
2286
|
+
console.log("next: type `imp` to talk, or ingest a source (`imprnt ingest <file>`), then `imprnt check`.");
|
|
2287
|
+
} else {
|
|
2288
|
+
const noteCount = collectNotes(vaultPath).length;
|
|
2289
|
+
console.log(`found existing vault at ./vault — ${noteCount} note${noteCount === 1 ? "" : "s"}, left untouched`);
|
|
2290
|
+
if (added.length)
|
|
2291
|
+
console.log(` added missing control file${added.length === 1 ? "" : "s"}: ${added.join(", ")}`);
|
|
2292
|
+
else if (createdDirs)
|
|
2293
|
+
console.log(` added ${createdDirs} missing folder${createdDirs === 1 ? "" : "s"}`);
|
|
2294
|
+
else
|
|
2295
|
+
console.log(" already initialized — nothing to add");
|
|
2296
|
+
console.log("run `imprnt check` to validate the graph.");
|
|
2297
|
+
}
|
|
2298
|
+
break;
|
|
2299
|
+
}
|
|
2300
|
+
default: {
|
|
2301
|
+
const isHelp = cmd === "help" || cmd === "--help" || cmd === "-h";
|
|
2302
|
+
const bare = cmd === undefined;
|
|
2303
|
+
const wantsLaunch = asImp && !isHelp && (bare || cmd.startsWith("-"));
|
|
2304
|
+
if (wantsLaunch && (!bare || process.stdin.isTTY && process.stdout.isTTY)) {
|
|
2305
|
+
const home = vaultProjectRoot();
|
|
2306
|
+
if (!home) {
|
|
2307
|
+
console.error("imp: no vault registered yet — run `imprnt init` in your vault project to give your assistant a memory. Launching plain claude.");
|
|
2308
|
+
}
|
|
2309
|
+
const { args: args5, env } = buildLaunch({ cwd: process.cwd(), vaultProject: home, pkgRoot, passthrough: bare ? [] : [cmd, ...rest] });
|
|
2310
|
+
process.exit(launchClaude(process.cwd(), args5, env));
|
|
2311
|
+
}
|
|
2312
|
+
console.log(`imprnt — deterministic-first markdown knowledge vault
|
|
2313
|
+
|
|
2314
|
+
the front door (the \`imp\` bin):
|
|
2315
|
+
imp open your assistant HERE: claude + your cast + the vault pointer
|
|
2316
|
+
imp lair open it in your vault project — full contract, resumable history
|
|
2317
|
+
imp -c | --resume | <claude flags> flags pass through to claude
|
|
2318
|
+
|
|
2319
|
+
engine (same subcommands under \`imp\` or \`imprnt\`):
|
|
2320
|
+
imprnt init [--register] scaffold ./vault (entities/domains/forms) and ./raw, register as imp's default vault
|
|
2321
|
+
imprnt snapshot <src> --dest <relpath> mirror a file/dir into raw/<relpath> (immutable, hashed) — the migration's deterministic half
|
|
2322
|
+
imprnt ingest <file|text> [--vault D] snapshot a source -> raw/; a transcript file also gets an event skeleton (no LLM)
|
|
2323
|
+
imprnt recall "<query>" [--vault D] synonym-aware BM25 ranking over the vault
|
|
2324
|
+
imprnt context print the vault contract — agents run this before writing any note
|
|
2325
|
+
imprnt check [--all] [--vault D] integrity (orphan links, disconnected notes, uncovered snapshots) + regenerate index.md; --all also runs each plugins/*/check.js
|
|
2326
|
+
imprnt ingest --apply <file> [--vault D] file a pre-enriched staged note from a plugin into the vault (snapshot + resolve); --apply-all globs plugins/*/proposed/
|
|
2327
|
+
imprnt hot [--vault D] needs-review + the session primer
|
|
2328
|
+
imprnt plugin list show installed plugins (on/off) + official ones available to add
|
|
2329
|
+
imprnt plugin add <name> [--from D] fetch imprnt-plugin-<name>, copy into plugins/, wire it (idempotent; --force refreshes)
|
|
2330
|
+
imprnt plugin rm <name> [--purge] unwire a plugin; --purge also deletes plugins/<name>/
|
|
2331
|
+
|
|
2332
|
+
layout: entities (people · orgs · holdings) · domains (identity · health · finances · work · life · projects) · forms (events · mistakes)
|
|
2333
|
+
the vault is plain markdown. an agent greps it directly — no MCP, no DB.`);
|
|
2334
|
+
if (cmd && !isHelp)
|
|
2335
|
+
process.exit(1);
|
|
2336
|
+
}
|
|
2337
|
+
}
|
|
2338
|
+
});
|
|
2339
|
+
await init_cli();
|