@tricknowtech/context 0.1.1 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +52 -3
- package/dist/{chunk-BWATZKYM.js → chunk-BLEGQVXA.js} +312 -102
- package/dist/cli.cjs +513 -160
- package/dist/cli.js +174 -17
- package/dist/index.cjs +384 -133
- package/dist/index.d.cts +85 -8
- package/dist/index.d.ts +85 -8
- package/dist/index.js +11 -1
- package/package.json +11 -3
|
@@ -12,8 +12,9 @@ function cwdKey(absPath) {
|
|
|
12
12
|
function makeTemplate(absPath, ctx) {
|
|
13
13
|
const candidates = [
|
|
14
14
|
[ctx.userClaude, "{userClaude}"],
|
|
15
|
-
[ctx.project, "{project}"]
|
|
16
|
-
|
|
15
|
+
[ctx.project, "{project}"],
|
|
16
|
+
[ctx.home, "{home}"]
|
|
17
|
+
].filter(([root]) => Boolean(root)).sort((a, b) => b[0].length - a[0].length);
|
|
17
18
|
let out = absPath;
|
|
18
19
|
for (const [root, token] of candidates) {
|
|
19
20
|
if (absPath === root || absPath.startsWith(root + path.sep)) {
|
|
@@ -24,7 +25,7 @@ function makeTemplate(absPath, ctx) {
|
|
|
24
25
|
return ctx.cwdKey ? out.split(ctx.cwdKey).join("{cwdKey}") : out;
|
|
25
26
|
}
|
|
26
27
|
function resolveTemplate(template, ctx) {
|
|
27
|
-
const expanded = template.split("{userClaude}").join(ctx.userClaude).split("{project}").join(ctx.project).split("{cwdKey}").join(ctx.cwdKey);
|
|
28
|
+
const expanded = template.split("{userClaude}").join(ctx.userClaude).split("{project}").join(ctx.project).split("{home}").join(ctx.home).split("{cwdKey}").join(ctx.cwdKey);
|
|
28
29
|
return path.normalize(expanded);
|
|
29
30
|
}
|
|
30
31
|
function toPosix(p) {
|
|
@@ -181,6 +182,7 @@ function defaultConfig(projectRoot) {
|
|
|
181
182
|
name: path2.basename(projectRoot),
|
|
182
183
|
rootHint: projectRoot,
|
|
183
184
|
tiers: ["core", "handoff"],
|
|
185
|
+
assistants: ["auto"],
|
|
184
186
|
artifactPaths: ["graphify-out"],
|
|
185
187
|
exclude: [...DEFAULT_EXCLUDE],
|
|
186
188
|
remotes: {}
|
|
@@ -209,7 +211,9 @@ function loadConfig(projectRoot) {
|
|
|
209
211
|
...defaultConfig(projectRoot),
|
|
210
212
|
...cfg,
|
|
211
213
|
remotes: cfg.remotes ?? {},
|
|
212
|
-
tiers: cfg.tiers ?? ["core", "handoff"]
|
|
214
|
+
tiers: cfg.tiers ?? ["core", "handoff"],
|
|
215
|
+
// Stores written before multi-assistant support have no `assistants` key.
|
|
216
|
+
assistants: cfg.assistants ?? ["auto"]
|
|
213
217
|
};
|
|
214
218
|
}
|
|
215
219
|
function saveConfig(projectRoot, cfg) {
|
|
@@ -217,31 +221,27 @@ function saveConfig(projectRoot, cfg) {
|
|
|
217
221
|
}
|
|
218
222
|
|
|
219
223
|
// src/collector.ts
|
|
224
|
+
import fs4 from "fs";
|
|
225
|
+
import path4 from "path";
|
|
226
|
+
|
|
227
|
+
// src/assistants.ts
|
|
220
228
|
import fs3 from "fs";
|
|
229
|
+
import os2 from "os";
|
|
221
230
|
import path3 from "path";
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
// `.local.json` variants are gitignored by default, so nothing else carries
|
|
232
|
-
// them — which makes them exactly the kind of file this tool exists for.
|
|
233
|
-
".claude/settings.local.json",
|
|
234
|
-
".claude/memory/",
|
|
235
|
-
".claude/plans/",
|
|
236
|
-
".claude/commands/",
|
|
237
|
-
".claude/agents/",
|
|
238
|
-
".claude/skills/"
|
|
239
|
-
];
|
|
231
|
+
function filesIn(dir, exclude, prefix) {
|
|
232
|
+
return walk(dir, { exclude }).map((abs) => ({
|
|
233
|
+
abs,
|
|
234
|
+
rel: `${prefix}/${toPosix(path3.relative(dir, abs))}`
|
|
235
|
+
}));
|
|
236
|
+
}
|
|
237
|
+
function fileIfExists(abs, rel) {
|
|
238
|
+
return fs3.existsSync(abs) ? [{ abs, rel }] : [];
|
|
239
|
+
}
|
|
240
240
|
function planStem(fileName) {
|
|
241
241
|
return fileName.replace(/\.md$/, "").replace(/-agent-[0-9a-f]+$/i, "");
|
|
242
242
|
}
|
|
243
|
-
function
|
|
244
|
-
const all = walk(
|
|
243
|
+
function relatedByProject(dir, projectRoot, exclude) {
|
|
244
|
+
const all = walk(dir, { exclude });
|
|
245
245
|
if (all.length === 0) return [];
|
|
246
246
|
const projectName = path3.basename(projectRoot);
|
|
247
247
|
const related = /* @__PURE__ */ new Set();
|
|
@@ -263,10 +263,143 @@ function collectPlans(plansDir, projectRoot, exclude) {
|
|
|
263
263
|
}
|
|
264
264
|
return [...related];
|
|
265
265
|
}
|
|
266
|
+
var ADAPTERS = [
|
|
267
|
+
{
|
|
268
|
+
id: "claude",
|
|
269
|
+
name: "Claude Code",
|
|
270
|
+
projectGlobs: [
|
|
271
|
+
"CLAUDE.md",
|
|
272
|
+
"CLAUDE.local.md",
|
|
273
|
+
"**/CLAUDE.md",
|
|
274
|
+
".claude/settings.json",
|
|
275
|
+
".claude/settings.local.json",
|
|
276
|
+
".claude/memory/",
|
|
277
|
+
".claude/plans/",
|
|
278
|
+
".claude/commands/",
|
|
279
|
+
".claude/agents/",
|
|
280
|
+
".claude/skills/"
|
|
281
|
+
],
|
|
282
|
+
userDir: () => userClaudeDir(),
|
|
283
|
+
collectUser: (projectRoot, _home, exclude) => {
|
|
284
|
+
const root = userClaudeDir();
|
|
285
|
+
const out = [];
|
|
286
|
+
const key = cwdKey(projectRoot);
|
|
287
|
+
const projectsDir = path3.join(root, "projects");
|
|
288
|
+
let keys = [];
|
|
289
|
+
try {
|
|
290
|
+
keys = fs3.readdirSync(projectsDir, { withFileTypes: true }).filter((d) => d.isDirectory() && (d.name === key || d.name.startsWith(key + "-"))).map((d) => d.name);
|
|
291
|
+
} catch {
|
|
292
|
+
keys = [];
|
|
293
|
+
}
|
|
294
|
+
for (const k of keys) {
|
|
295
|
+
const memDir = path3.join(projectsDir, k, "memory");
|
|
296
|
+
const prefix = k === key ? "memory" : `memory-sub/${k.slice(key.length + 1)}`;
|
|
297
|
+
out.push(...filesIn(memDir, exclude, prefix));
|
|
298
|
+
}
|
|
299
|
+
out.push(...filesIn(path3.join(root, "skills"), exclude, "skills"));
|
|
300
|
+
out.push(...filesIn(path3.join(root, "agents"), exclude, "agents"));
|
|
301
|
+
for (const abs of relatedByProject(path3.join(root, "plans"), projectRoot, exclude)) {
|
|
302
|
+
out.push({ abs, rel: `plans/${path3.basename(abs)}` });
|
|
303
|
+
}
|
|
304
|
+
out.push(...fileIfExists(path3.join(root, "CLAUDE.md"), "user/CLAUDE.md"));
|
|
305
|
+
out.push(...fileIfExists(path3.join(root, "settings.json"), "user/settings.json"));
|
|
306
|
+
return out;
|
|
307
|
+
}
|
|
308
|
+
},
|
|
309
|
+
{
|
|
310
|
+
id: "codex",
|
|
311
|
+
name: "OpenAI Codex",
|
|
312
|
+
projectGlobs: ["AGENTS.md", "**/AGENTS.md", ".codex/"],
|
|
313
|
+
userDir: (home) => path3.join(home, ".codex"),
|
|
314
|
+
collectUser: (projectRoot, home, exclude) => {
|
|
315
|
+
const root = path3.join(home, ".codex");
|
|
316
|
+
const out = [];
|
|
317
|
+
out.push(...fileIfExists(path3.join(root, "AGENTS.md"), "user/AGENTS.md"));
|
|
318
|
+
out.push(...fileIfExists(path3.join(root, "config.toml"), "user/config.toml"));
|
|
319
|
+
out.push(...filesIn(path3.join(root, "prompts"), exclude, "prompts"));
|
|
320
|
+
return out;
|
|
321
|
+
}
|
|
322
|
+
},
|
|
323
|
+
{
|
|
324
|
+
id: "cursor",
|
|
325
|
+
name: "Cursor",
|
|
326
|
+
// `.cursorrules` is the legacy single-file form; `.cursor/rules/*.mdc` is current.
|
|
327
|
+
projectGlobs: [".cursorrules", ".cursor/rules/", ".cursor/"],
|
|
328
|
+
userDir: (home) => path3.join(home, ".cursor"),
|
|
329
|
+
collectUser: (_projectRoot, home, exclude) => filesIn(path3.join(home, ".cursor", "rules"), exclude, "rules")
|
|
330
|
+
},
|
|
331
|
+
{
|
|
332
|
+
id: "copilot",
|
|
333
|
+
name: "GitHub Copilot",
|
|
334
|
+
projectGlobs: [
|
|
335
|
+
".github/copilot-instructions.md",
|
|
336
|
+
".github/instructions/",
|
|
337
|
+
".github/prompts/"
|
|
338
|
+
]
|
|
339
|
+
},
|
|
340
|
+
{
|
|
341
|
+
id: "windsurf",
|
|
342
|
+
name: "Windsurf",
|
|
343
|
+
projectGlobs: [".windsurfrules", ".windsurf/rules/", ".windsurf/"],
|
|
344
|
+
userDir: (home) => path3.join(home, ".windsurf")
|
|
345
|
+
},
|
|
346
|
+
{
|
|
347
|
+
id: "gemini",
|
|
348
|
+
name: "Gemini CLI",
|
|
349
|
+
projectGlobs: ["GEMINI.md", "**/GEMINI.md", ".gemini/"],
|
|
350
|
+
userDir: (home) => path3.join(home, ".gemini"),
|
|
351
|
+
collectUser: (_projectRoot, home, exclude) => {
|
|
352
|
+
const root = path3.join(home, ".gemini");
|
|
353
|
+
const out = [];
|
|
354
|
+
out.push(...fileIfExists(path3.join(root, "GEMINI.md"), "user/GEMINI.md"));
|
|
355
|
+
out.push(...fileIfExists(path3.join(root, "settings.json"), "user/settings.json"));
|
|
356
|
+
out.push(...filesIn(path3.join(root, "commands"), exclude, "commands"));
|
|
357
|
+
return out;
|
|
358
|
+
}
|
|
359
|
+
},
|
|
360
|
+
{
|
|
361
|
+
id: "cline",
|
|
362
|
+
name: "Cline",
|
|
363
|
+
projectGlobs: [".clinerules", ".clinerules/"]
|
|
364
|
+
},
|
|
365
|
+
{
|
|
366
|
+
id: "aider",
|
|
367
|
+
name: "Aider",
|
|
368
|
+
projectGlobs: ["CONVENTIONS.md", ".aider.conf.yml", ".aider.conf.yaml"]
|
|
369
|
+
},
|
|
370
|
+
{
|
|
371
|
+
id: "continue",
|
|
372
|
+
name: "Continue",
|
|
373
|
+
projectGlobs: [".continue/", ".continuerules"],
|
|
374
|
+
userDir: (home) => path3.join(home, ".continue")
|
|
375
|
+
}
|
|
376
|
+
];
|
|
377
|
+
function adapterById(id) {
|
|
378
|
+
return ADAPTERS.find((a) => a.id === id);
|
|
379
|
+
}
|
|
380
|
+
function homeDir() {
|
|
381
|
+
return os2.homedir();
|
|
382
|
+
}
|
|
383
|
+
function detectAssistants(projectRoot, projectFiles) {
|
|
384
|
+
const home = homeDir();
|
|
385
|
+
return ADAPTERS.filter((a) => {
|
|
386
|
+
if (a.userDir) {
|
|
387
|
+
try {
|
|
388
|
+
if (fs3.existsSync(a.userDir(home))) return true;
|
|
389
|
+
} catch {
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
return projectFiles.some((rel) => matchesAny(rel, a.projectGlobs));
|
|
393
|
+
});
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
// src/collector.ts
|
|
266
397
|
function push(out, sourcePath, storePath, tier, sourceRoot, ctx) {
|
|
267
398
|
let size = 0;
|
|
268
399
|
try {
|
|
269
|
-
|
|
400
|
+
const st = fs4.statSync(sourcePath);
|
|
401
|
+
if (!st.isFile()) return;
|
|
402
|
+
size = st.size;
|
|
270
403
|
} catch {
|
|
271
404
|
return;
|
|
272
405
|
}
|
|
@@ -279,76 +412,66 @@ function push(out, sourcePath, storePath, tier, sourceRoot, ctx) {
|
|
|
279
412
|
restoreTemplate: makeTemplate(sourcePath, ctx)
|
|
280
413
|
});
|
|
281
414
|
}
|
|
415
|
+
function templateContextFor(projectRoot) {
|
|
416
|
+
return {
|
|
417
|
+
userClaude: userClaudeDir(),
|
|
418
|
+
project: projectRoot,
|
|
419
|
+
cwdKey: cwdKey(projectRoot),
|
|
420
|
+
home: homeDir()
|
|
421
|
+
};
|
|
422
|
+
}
|
|
282
423
|
function collect(projectRoot, cfg, tiers) {
|
|
283
|
-
const
|
|
284
|
-
const
|
|
285
|
-
const ctx = { userClaude, project: projectRoot, cwdKey: key };
|
|
424
|
+
const ctx = templateContextFor(projectRoot);
|
|
425
|
+
const home = ctx.home;
|
|
286
426
|
const files = [];
|
|
287
427
|
const skippedTracked = [];
|
|
288
428
|
const exclude = [...HARD_DENY, ...cfg.exclude];
|
|
289
429
|
const tracked = gitTrackedSet(projectRoot);
|
|
430
|
+
const projectRel = walk(projectRoot, { exclude }).map((abs) => ({
|
|
431
|
+
abs,
|
|
432
|
+
rel: toPosix(path4.relative(projectRoot, abs))
|
|
433
|
+
}));
|
|
434
|
+
const configured = cfg.assistants && cfg.assistants.length > 0 && !cfg.assistants.includes("auto");
|
|
435
|
+
const assistants = configured ? cfg.assistants.map(adapterById).filter(Boolean) : detectAssistants(projectRoot, projectRel.map((p) => p.rel));
|
|
290
436
|
if (tiers.includes("core")) {
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
437
|
+
const claimed = /* @__PURE__ */ new Set();
|
|
438
|
+
for (const adapter of assistants) {
|
|
439
|
+
for (const { abs, rel } of projectRel) {
|
|
440
|
+
if (claimed.has(rel)) continue;
|
|
441
|
+
if (!matchesAny(rel, adapter.projectGlobs)) continue;
|
|
442
|
+
claimed.add(rel);
|
|
443
|
+
if (tracked.has(abs)) {
|
|
444
|
+
skippedTracked.push(rel);
|
|
445
|
+
continue;
|
|
446
|
+
}
|
|
447
|
+
push(files, abs, `project/${rel}`, "core", "project", ctx);
|
|
297
448
|
}
|
|
298
|
-
push(files, abs, `project/${rel}`, "core", "project", ctx);
|
|
299
|
-
}
|
|
300
|
-
const projectsDir = path3.join(userClaude, "projects");
|
|
301
|
-
let projectKeys = [];
|
|
302
|
-
try {
|
|
303
|
-
projectKeys = fs3.readdirSync(projectsDir, { withFileTypes: true }).filter((d) => d.isDirectory() && (d.name === key || d.name.startsWith(key + "-"))).map((d) => d.name);
|
|
304
|
-
} catch {
|
|
305
|
-
projectKeys = [];
|
|
306
449
|
}
|
|
307
|
-
for (const
|
|
308
|
-
|
|
309
|
-
for (const abs of
|
|
310
|
-
|
|
311
|
-
const storePath = pk === key ? `memory/${rel}` : `memory-sub/${pk.slice(key.length + 1)}/${rel}`;
|
|
312
|
-
push(files, abs, storePath, "core", "user", ctx);
|
|
450
|
+
for (const adapter of assistants) {
|
|
451
|
+
if (!adapter.collectUser) continue;
|
|
452
|
+
for (const { abs, rel } of adapter.collectUser(projectRoot, home, exclude)) {
|
|
453
|
+
push(files, abs, `assistants/${adapter.id}/${rel}`, "core", "user", ctx);
|
|
313
454
|
}
|
|
314
455
|
}
|
|
315
|
-
for (const abs of collectPlans(path3.join(userClaude, "plans"), projectRoot, exclude)) {
|
|
316
|
-
const rel = toPosix(path3.relative(path3.join(userClaude, "plans"), abs));
|
|
317
|
-
push(files, abs, `plans/${rel}`, "core", "user", ctx);
|
|
318
|
-
}
|
|
319
|
-
const skillsDir = path3.join(userClaude, "skills");
|
|
320
|
-
for (const abs of walk(skillsDir, { exclude })) {
|
|
321
|
-
const rel = toPosix(path3.relative(skillsDir, abs));
|
|
322
|
-
push(files, abs, `skills/${rel}`, "core", "user", ctx);
|
|
323
|
-
}
|
|
324
|
-
const agentsDir = path3.join(userClaude, "agents");
|
|
325
|
-
for (const abs of walk(agentsDir, { exclude })) {
|
|
326
|
-
const rel = toPosix(path3.relative(agentsDir, abs));
|
|
327
|
-
push(files, abs, `agents/${rel}`, "core", "user", ctx);
|
|
328
|
-
}
|
|
329
|
-
for (const name of ["CLAUDE.md", "settings.json"]) {
|
|
330
|
-
const abs = path3.join(userClaude, name);
|
|
331
|
-
if (fs3.existsSync(abs)) push(files, abs, `user/${name}`, "core", "user", ctx);
|
|
332
|
-
}
|
|
333
456
|
}
|
|
334
457
|
if (tiers.includes("artifacts")) {
|
|
335
458
|
for (const relDir of cfg.artifactPaths) {
|
|
336
|
-
const absDir =
|
|
459
|
+
const absDir = path4.join(projectRoot, relDir);
|
|
337
460
|
for (const abs of walk(absDir, { exclude })) {
|
|
338
|
-
const rel = toPosix(
|
|
461
|
+
const rel = toPosix(path4.relative(absDir, abs));
|
|
339
462
|
push(files, abs, `artifacts/${relDir}/${rel}`, "artifacts", "project", ctx);
|
|
340
463
|
}
|
|
341
464
|
}
|
|
342
465
|
}
|
|
343
466
|
if (tiers.includes("transcripts")) {
|
|
344
|
-
const projDir =
|
|
467
|
+
const projDir = path4.join(userClaudeDir(), "projects", ctx.cwdKey);
|
|
345
468
|
for (const abs of walk(projDir, { exclude })) {
|
|
346
|
-
const rel = toPosix(
|
|
469
|
+
const rel = toPosix(path4.relative(projDir, abs));
|
|
347
470
|
if (rel.startsWith("memory/")) continue;
|
|
348
|
-
push(files, abs, `transcripts/${rel}`, "transcripts", "user", ctx);
|
|
471
|
+
push(files, abs, `transcripts/claude/${rel}`, "transcripts", "user", ctx);
|
|
349
472
|
}
|
|
350
473
|
}
|
|
351
|
-
return { files, skippedTracked, ctx };
|
|
474
|
+
return { files, skippedTracked, assistants, ctx };
|
|
352
475
|
}
|
|
353
476
|
function describeExcluded(projectRoot, tiers) {
|
|
354
477
|
const userClaude = userClaudeDir();
|
|
@@ -361,15 +484,17 @@ function describeExcluded(projectRoot, tiers) {
|
|
|
361
484
|
if (filter && !filter(abs)) continue;
|
|
362
485
|
files++;
|
|
363
486
|
try {
|
|
364
|
-
bytes +=
|
|
487
|
+
bytes += fs4.statSync(abs).size;
|
|
365
488
|
} catch {
|
|
366
489
|
}
|
|
367
490
|
}
|
|
368
491
|
return { files, bytes };
|
|
369
492
|
};
|
|
370
493
|
if (!tiers.includes("transcripts")) {
|
|
371
|
-
const
|
|
372
|
-
|
|
494
|
+
const m = measure(
|
|
495
|
+
path4.join(userClaude, "projects", key),
|
|
496
|
+
(p) => !p.includes(`${path4.sep}memory${path4.sep}`)
|
|
497
|
+
);
|
|
373
498
|
if (m.files > 0) {
|
|
374
499
|
out.push({
|
|
375
500
|
label: "session transcripts",
|
|
@@ -383,7 +508,7 @@ function describeExcluded(projectRoot, tiers) {
|
|
|
383
508
|
["file-history", "edit-undo snapshots", "transient local undo state, not portable context"],
|
|
384
509
|
["tasks", "task outputs", "session-scoped tool output"]
|
|
385
510
|
]) {
|
|
386
|
-
const m = measure(
|
|
511
|
+
const m = measure(path4.join(userClaude, dir));
|
|
387
512
|
if (m.files > 0) out.push({ label, ...m, reason });
|
|
388
513
|
}
|
|
389
514
|
return out.filter((g) => g.bytes > 0);
|
|
@@ -403,9 +528,89 @@ function summarize(files) {
|
|
|
403
528
|
return out;
|
|
404
529
|
}
|
|
405
530
|
|
|
531
|
+
// src/handoff.ts
|
|
532
|
+
function asStringArray(value) {
|
|
533
|
+
if (value === void 0 || value === null) return [];
|
|
534
|
+
if (!Array.isArray(value)) return null;
|
|
535
|
+
if (!value.every((v) => typeof v === "string")) return null;
|
|
536
|
+
return value;
|
|
537
|
+
}
|
|
538
|
+
function validateHandoff(raw) {
|
|
539
|
+
const errors = [];
|
|
540
|
+
if (typeof raw !== "object" || raw === null || Array.isArray(raw)) {
|
|
541
|
+
return { ok: false, errors: ["handoff must be a JSON object"] };
|
|
542
|
+
}
|
|
543
|
+
const o = raw;
|
|
544
|
+
const goal = typeof o.goal === "string" ? o.goal.trim() : "";
|
|
545
|
+
const nextStep = typeof o.nextStep === "string" ? o.nextStep.trim() : "";
|
|
546
|
+
if (!goal) errors.push("`goal` is required (what the session set out to do)");
|
|
547
|
+
if (!nextStep) errors.push("`nextStep` is required (the single next action)");
|
|
548
|
+
const decisions = asStringArray(o.decisions);
|
|
549
|
+
const openThreads = asStringArray(o.openThreads);
|
|
550
|
+
const filesTouched = asStringArray(o.filesTouched);
|
|
551
|
+
if (decisions === null) errors.push("`decisions` must be an array of strings");
|
|
552
|
+
if (openThreads === null) errors.push("`openThreads` must be an array of strings");
|
|
553
|
+
if (filesTouched === null) errors.push("`filesTouched` must be an array of strings");
|
|
554
|
+
let updatedAt = typeof o.updatedAt === "string" ? o.updatedAt : "";
|
|
555
|
+
if (!updatedAt || Number.isNaN(Date.parse(updatedAt))) updatedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
556
|
+
if (errors.length > 0) return { ok: false, errors };
|
|
557
|
+
return {
|
|
558
|
+
ok: true,
|
|
559
|
+
errors: [],
|
|
560
|
+
handoff: {
|
|
561
|
+
updatedAt,
|
|
562
|
+
goal,
|
|
563
|
+
decisions: decisions ?? [],
|
|
564
|
+
openThreads: openThreads ?? [],
|
|
565
|
+
filesTouched: filesTouched ?? [],
|
|
566
|
+
nextStep,
|
|
567
|
+
...typeof o.notes === "string" && o.notes.trim() ? { notes: o.notes.trim() } : {}
|
|
568
|
+
}
|
|
569
|
+
};
|
|
570
|
+
}
|
|
571
|
+
function ago(iso) {
|
|
572
|
+
const ms = Date.now() - Date.parse(iso);
|
|
573
|
+
if (Number.isNaN(ms)) return "unknown";
|
|
574
|
+
const mins = Math.floor(ms / 6e4);
|
|
575
|
+
if (mins < 1) return "just now";
|
|
576
|
+
if (mins < 60) return `${mins}m ago`;
|
|
577
|
+
const hours = Math.floor(mins / 60);
|
|
578
|
+
if (hours < 24) return `${hours}h ago`;
|
|
579
|
+
return `${Math.floor(hours / 24)}d ago`;
|
|
580
|
+
}
|
|
581
|
+
function formatHandoff(h) {
|
|
582
|
+
const lines = [`Where you left off (${ago(h.updatedAt)})`, "", ` Goal ${h.goal}`, ` Next step ${h.nextStep}`];
|
|
583
|
+
if (h.decisions.length > 0) {
|
|
584
|
+
lines.push("", " Decided:");
|
|
585
|
+
for (const d of h.decisions) lines.push(` \xB7 ${d}`);
|
|
586
|
+
}
|
|
587
|
+
if (h.openThreads.length > 0) {
|
|
588
|
+
lines.push("", " Still open:");
|
|
589
|
+
for (const t of h.openThreads) lines.push(` \xB7 ${t}`);
|
|
590
|
+
}
|
|
591
|
+
if (h.filesTouched.length > 0) {
|
|
592
|
+
const shown = h.filesTouched.slice(0, 12);
|
|
593
|
+
lines.push("", " Files touched:");
|
|
594
|
+
for (const f of shown) lines.push(` ${f}`);
|
|
595
|
+
if (h.filesTouched.length > shown.length) {
|
|
596
|
+
lines.push(` \u2026 and ${h.filesTouched.length - shown.length} more`);
|
|
597
|
+
}
|
|
598
|
+
}
|
|
599
|
+
if (h.notes) lines.push("", ` Notes: ${h.notes}`);
|
|
600
|
+
return lines;
|
|
601
|
+
}
|
|
602
|
+
function handoffAge(h) {
|
|
603
|
+
return ago(h.updatedAt);
|
|
604
|
+
}
|
|
605
|
+
function isStale(h, newestContentMs) {
|
|
606
|
+
const t = Date.parse(h.updatedAt);
|
|
607
|
+
if (Number.isNaN(t)) return true;
|
|
608
|
+
return newestContentMs - t > 60 * 60 * 1e3;
|
|
609
|
+
}
|
|
610
|
+
|
|
406
611
|
// src/scaffold.ts
|
|
407
|
-
import
|
|
408
|
-
import
|
|
612
|
+
import fs5 from "fs";
|
|
613
|
+
import path5 from "path";
|
|
409
614
|
var SLASH_COMMAND_PATH = ".claude/commands/context.md";
|
|
410
615
|
var SLASH_COMMAND_BODY = `---
|
|
411
616
|
description: Sync this project's LLM context (memory, skills, instructions, handoff)
|
|
@@ -455,26 +660,26 @@ Run the context-sync action requested in: $ARGUMENTS
|
|
|
455
660
|
Run: \`npx @tricknowtech/context status\` and summarize the result.
|
|
456
661
|
`;
|
|
457
662
|
function installSlashCommand(projectRoot) {
|
|
458
|
-
const dest =
|
|
459
|
-
if (
|
|
460
|
-
ensureDir(
|
|
461
|
-
|
|
663
|
+
const dest = path5.join(projectRoot, SLASH_COMMAND_PATH);
|
|
664
|
+
if (fs5.existsSync(dest)) return { path: SLASH_COMMAND_PATH, written: false };
|
|
665
|
+
ensureDir(path5.dirname(dest));
|
|
666
|
+
fs5.writeFileSync(dest, SLASH_COMMAND_BODY, "utf8");
|
|
462
667
|
return { path: SLASH_COMMAND_PATH, written: true };
|
|
463
668
|
}
|
|
464
669
|
function ensureGitignoreEntries(projectRoot, artifactsEnabled) {
|
|
465
|
-
const gitignore =
|
|
670
|
+
const gitignore = path5.join(projectRoot, ".gitignore");
|
|
466
671
|
const wanted = [".contextsync/transcripts/"];
|
|
467
672
|
if (!artifactsEnabled) wanted.push(".contextsync/artifacts/");
|
|
468
673
|
let existing = "";
|
|
469
674
|
try {
|
|
470
|
-
existing =
|
|
675
|
+
existing = fs5.readFileSync(gitignore, "utf8");
|
|
471
676
|
} catch {
|
|
472
677
|
}
|
|
473
678
|
const lines = new Set(existing.split("\n").map((l) => l.trim()));
|
|
474
679
|
const missing = wanted.filter((w) => !lines.has(w));
|
|
475
680
|
if (missing.length === 0) return [];
|
|
476
681
|
const prefix = existing.length > 0 && !existing.endsWith("\n") ? "\n" : "";
|
|
477
|
-
|
|
682
|
+
fs5.appendFileSync(
|
|
478
683
|
gitignore,
|
|
479
684
|
`${prefix}
|
|
480
685
|
# tricknowtech context-sync \u2014 never commit these tiers
|
|
@@ -486,7 +691,7 @@ ${missing.join("\n")}
|
|
|
486
691
|
}
|
|
487
692
|
|
|
488
693
|
// src/secrets.ts
|
|
489
|
-
import
|
|
694
|
+
import fs6 from "fs";
|
|
490
695
|
var BENIGN_KEY = /(?:^|[_-])(?:input|output|prompt|completion|total|max|min|num|new|cache|cached|remaining|used|count|context|window|budget|estimated?)[_-]?tokens?$|tokens?[_-]?(?:count|used|limit|remaining|in|out|usage|per|budget)$/i;
|
|
491
696
|
function looksLikeCredential(value) {
|
|
492
697
|
if (/^[A-Za-z_$][\w$]*(?:\.[A-Za-z_$][\w$]*)+$/.test(value)) return false;
|
|
@@ -540,7 +745,7 @@ function scanFiles(files) {
|
|
|
540
745
|
for (const file of files) {
|
|
541
746
|
let buf;
|
|
542
747
|
try {
|
|
543
|
-
buf =
|
|
748
|
+
buf = fs6.readFileSync(file.sourcePath);
|
|
544
749
|
} catch {
|
|
545
750
|
continue;
|
|
546
751
|
}
|
|
@@ -578,8 +783,8 @@ function formatHits(hits) {
|
|
|
578
783
|
}
|
|
579
784
|
|
|
580
785
|
// src/store.ts
|
|
581
|
-
import
|
|
582
|
-
import
|
|
786
|
+
import fs7 from "fs";
|
|
787
|
+
import path6 from "path";
|
|
583
788
|
var MANIFEST_FILE = "manifest.json";
|
|
584
789
|
var LocalStore = class {
|
|
585
790
|
constructor(projectRoot) {
|
|
@@ -590,13 +795,13 @@ var LocalStore = class {
|
|
|
590
795
|
return storeDir(this.projectRoot);
|
|
591
796
|
}
|
|
592
797
|
write(files, projectRoot) {
|
|
593
|
-
for (const name of
|
|
798
|
+
for (const name of fs7.existsSync(this.dir) ? fs7.readdirSync(this.dir) : []) {
|
|
594
799
|
if (name === "config.json" || name === HANDOFF_FILE) continue;
|
|
595
|
-
|
|
800
|
+
fs7.rmSync(path6.join(this.dir, name), { recursive: true, force: true });
|
|
596
801
|
}
|
|
597
802
|
const entries = [];
|
|
598
803
|
for (const file of files) {
|
|
599
|
-
const dest =
|
|
804
|
+
const dest = path6.join(this.dir, file.storePath);
|
|
600
805
|
try {
|
|
601
806
|
copyFile(file.sourcePath, dest);
|
|
602
807
|
} catch {
|
|
@@ -617,11 +822,11 @@ var LocalStore = class {
|
|
|
617
822
|
writtenFrom: projectRoot,
|
|
618
823
|
entries
|
|
619
824
|
};
|
|
620
|
-
writeJson(
|
|
825
|
+
writeJson(path6.join(this.dir, MANIFEST_FILE), manifest);
|
|
621
826
|
return manifest;
|
|
622
827
|
}
|
|
623
828
|
readManifest() {
|
|
624
|
-
return readJson(
|
|
829
|
+
return readJson(path6.join(this.dir, MANIFEST_FILE));
|
|
625
830
|
}
|
|
626
831
|
restore(ctx, opts = {}) {
|
|
627
832
|
const manifest = this.readManifest();
|
|
@@ -629,12 +834,12 @@ var LocalStore = class {
|
|
|
629
834
|
const skipped = [];
|
|
630
835
|
if (!manifest) return { restored, skipped };
|
|
631
836
|
for (const entry of manifest.entries) {
|
|
632
|
-
const src =
|
|
633
|
-
if (!
|
|
837
|
+
const src = path6.join(this.dir, entry.storePath);
|
|
838
|
+
if (!fs7.existsSync(src)) continue;
|
|
634
839
|
const dest = resolveTemplate(entry.restoreTemplate, ctx);
|
|
635
|
-
if (!opts.force &&
|
|
840
|
+
if (!opts.force && fs7.existsSync(dest)) {
|
|
636
841
|
try {
|
|
637
|
-
if (!
|
|
842
|
+
if (!fs7.readFileSync(dest).equals(fs7.readFileSync(src))) {
|
|
638
843
|
skipped.push(entry.storePath);
|
|
639
844
|
continue;
|
|
640
845
|
}
|
|
@@ -644,8 +849,8 @@ var LocalStore = class {
|
|
|
644
849
|
}
|
|
645
850
|
}
|
|
646
851
|
try {
|
|
647
|
-
ensureDir(
|
|
648
|
-
|
|
852
|
+
ensureDir(path6.dirname(dest));
|
|
853
|
+
fs7.copyFileSync(src, dest);
|
|
649
854
|
restored.push(entry.storePath);
|
|
650
855
|
} catch {
|
|
651
856
|
skipped.push(entry.storePath);
|
|
@@ -654,10 +859,10 @@ var LocalStore = class {
|
|
|
654
859
|
return { restored, skipped };
|
|
655
860
|
}
|
|
656
861
|
readHandoff() {
|
|
657
|
-
return readJson(
|
|
862
|
+
return readJson(path6.join(this.dir, HANDOFF_FILE));
|
|
658
863
|
}
|
|
659
864
|
writeHandoff(handoff) {
|
|
660
|
-
writeJson(
|
|
865
|
+
writeJson(path6.join(this.dir, HANDOFF_FILE), handoff);
|
|
661
866
|
}
|
|
662
867
|
};
|
|
663
868
|
|
|
@@ -686,9 +891,14 @@ export {
|
|
|
686
891
|
configPath,
|
|
687
892
|
loadConfig,
|
|
688
893
|
saveConfig,
|
|
894
|
+
templateContextFor,
|
|
689
895
|
collect,
|
|
690
896
|
describeExcluded,
|
|
691
897
|
summarize,
|
|
898
|
+
validateHandoff,
|
|
899
|
+
formatHandoff,
|
|
900
|
+
handoffAge,
|
|
901
|
+
isStale,
|
|
692
902
|
SLASH_COMMAND_PATH,
|
|
693
903
|
SLASH_COMMAND_BODY,
|
|
694
904
|
installSlashCommand,
|