@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.
@@ -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
- ].sort((a, b) => b[0].length - a[0].length);
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
- var PROJECT_CONTEXT_GLOBS = [
223
- "CLAUDE.md",
224
- "CLAUDE.local.md",
225
- "AGENTS.md",
226
- "**/CLAUDE.md",
227
- "**/AGENTS.md",
228
- ".cursorrules",
229
- ".github/copilot-instructions.md",
230
- ".claude/settings.json",
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 collectPlans(plansDir, projectRoot, exclude) {
244
- const all = walk(plansDir, { exclude });
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
- size = fs3.statSync(sourcePath).size;
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 userClaude = userClaudeDir();
284
- const key = cwdKey(projectRoot);
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
- for (const abs of walk(projectRoot, { exclude })) {
292
- const rel = toPosix(path3.relative(projectRoot, abs));
293
- if (!matchesAny(rel, PROJECT_CONTEXT_GLOBS)) continue;
294
- if (tracked.has(abs)) {
295
- skippedTracked.push(rel);
296
- continue;
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 pk of projectKeys) {
308
- const memoryDir = path3.join(projectsDir, pk, "memory");
309
- for (const abs of walk(memoryDir, { exclude })) {
310
- const rel = toPosix(path3.relative(memoryDir, abs));
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 = path3.join(projectRoot, relDir);
459
+ const absDir = path4.join(projectRoot, relDir);
337
460
  for (const abs of walk(absDir, { exclude })) {
338
- const rel = toPosix(path3.relative(absDir, abs));
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 = path3.join(userClaude, "projects", key);
467
+ const projDir = path4.join(userClaudeDir(), "projects", ctx.cwdKey);
345
468
  for (const abs of walk(projDir, { exclude })) {
346
- const rel = toPosix(path3.relative(projDir, abs));
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 += fs3.statSync(abs).size;
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 projDir = path3.join(userClaude, "projects", key);
372
- const m = measure(projDir, (p) => !p.includes(`${path3.sep}memory${path3.sep}`));
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(path3.join(userClaude, dir));
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 fs4 from "fs";
408
- import path4 from "path";
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 = path4.join(projectRoot, SLASH_COMMAND_PATH);
459
- if (fs4.existsSync(dest)) return { path: SLASH_COMMAND_PATH, written: false };
460
- ensureDir(path4.dirname(dest));
461
- fs4.writeFileSync(dest, SLASH_COMMAND_BODY, "utf8");
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 = path4.join(projectRoot, ".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 = fs4.readFileSync(gitignore, "utf8");
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
- fs4.appendFileSync(
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 fs5 from "fs";
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 = fs5.readFileSync(file.sourcePath);
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 fs6 from "fs";
582
- import path5 from "path";
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 fs6.existsSync(this.dir) ? fs6.readdirSync(this.dir) : []) {
798
+ for (const name of fs7.existsSync(this.dir) ? fs7.readdirSync(this.dir) : []) {
594
799
  if (name === "config.json" || name === HANDOFF_FILE) continue;
595
- fs6.rmSync(path5.join(this.dir, name), { recursive: true, force: true });
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 = path5.join(this.dir, file.storePath);
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(path5.join(this.dir, MANIFEST_FILE), manifest);
825
+ writeJson(path6.join(this.dir, MANIFEST_FILE), manifest);
621
826
  return manifest;
622
827
  }
623
828
  readManifest() {
624
- return readJson(path5.join(this.dir, MANIFEST_FILE));
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 = path5.join(this.dir, entry.storePath);
633
- if (!fs6.existsSync(src)) continue;
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 && fs6.existsSync(dest)) {
840
+ if (!opts.force && fs7.existsSync(dest)) {
636
841
  try {
637
- if (!fs6.readFileSync(dest).equals(fs6.readFileSync(src))) {
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(path5.dirname(dest));
648
- fs6.copyFileSync(src, dest);
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(path5.join(this.dir, HANDOFF_FILE));
862
+ return readJson(path6.join(this.dir, HANDOFF_FILE));
658
863
  }
659
864
  writeHandoff(handoff) {
660
- writeJson(path5.join(this.dir, HANDOFF_FILE), handoff);
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,