@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/dist/cli.cjs CHANGED
@@ -36,16 +36,17 @@ __export(cli_exports, {
36
36
  module.exports = __toCommonJS(cli_exports);
37
37
 
38
38
  // src/commands.ts
39
- var import_node_fs7 = __toESM(require("fs"), 1);
40
- var import_node_path6 = __toESM(require("path"), 1);
39
+ var import_node_child_process2 = require("child_process");
40
+ var import_node_fs8 = __toESM(require("fs"), 1);
41
+ var import_node_path7 = __toESM(require("path"), 1);
41
42
 
42
43
  // src/collector.ts
43
- var import_node_fs3 = __toESM(require("fs"), 1);
44
- var import_node_path3 = __toESM(require("path"), 1);
44
+ var import_node_fs4 = __toESM(require("fs"), 1);
45
+ var import_node_path4 = __toESM(require("path"), 1);
45
46
 
46
- // src/config.ts
47
- var import_node_crypto = __toESM(require("crypto"), 1);
47
+ // src/assistants.ts
48
48
  var import_node_fs2 = __toESM(require("fs"), 1);
49
+ var import_node_os2 = __toESM(require("os"), 1);
49
50
  var import_node_path2 = __toESM(require("path"), 1);
50
51
 
51
52
  // src/fsutil.ts
@@ -62,8 +63,9 @@ function cwdKey(absPath) {
62
63
  function makeTemplate(absPath, ctx) {
63
64
  const candidates = [
64
65
  [ctx.userClaude, "{userClaude}"],
65
- [ctx.project, "{project}"]
66
- ].sort((a, b) => b[0].length - a[0].length);
66
+ [ctx.project, "{project}"],
67
+ [ctx.home, "{home}"]
68
+ ].filter(([root]) => Boolean(root)).sort((a, b) => b[0].length - a[0].length);
67
69
  let out2 = absPath;
68
70
  for (const [root, token] of candidates) {
69
71
  if (absPath === root || absPath.startsWith(root + import_node_path.default.sep)) {
@@ -74,7 +76,7 @@ function makeTemplate(absPath, ctx) {
74
76
  return ctx.cwdKey ? out2.split(ctx.cwdKey).join("{cwdKey}") : out2;
75
77
  }
76
78
  function resolveTemplate(template, ctx) {
77
- const expanded = template.split("{userClaude}").join(ctx.userClaude).split("{project}").join(ctx.project).split("{cwdKey}").join(ctx.cwdKey);
79
+ const expanded = template.split("{userClaude}").join(ctx.userClaude).split("{project}").join(ctx.project).split("{home}").join(ctx.home).split("{cwdKey}").join(ctx.cwdKey);
78
80
  return import_node_path.default.normalize(expanded);
79
81
  }
80
82
  function toPosix(p) {
@@ -196,7 +198,176 @@ function formatBytes(n) {
196
198
  return `${(n / 1024 / 1024 / 1024).toFixed(2)} GB`;
197
199
  }
198
200
 
201
+ // src/assistants.ts
202
+ function filesIn(dir, exclude, prefix) {
203
+ return walk(dir, { exclude }).map((abs) => ({
204
+ abs,
205
+ rel: `${prefix}/${toPosix(import_node_path2.default.relative(dir, abs))}`
206
+ }));
207
+ }
208
+ function fileIfExists(abs, rel) {
209
+ return import_node_fs2.default.existsSync(abs) ? [{ abs, rel }] : [];
210
+ }
211
+ function planStem(fileName) {
212
+ return fileName.replace(/\.md$/, "").replace(/-agent-[0-9a-f]+$/i, "");
213
+ }
214
+ function relatedByProject(dir, projectRoot, exclude) {
215
+ const all = walk(dir, { exclude });
216
+ if (all.length === 0) return [];
217
+ const projectName = import_node_path2.default.basename(projectRoot);
218
+ const related = /* @__PURE__ */ new Set();
219
+ const stems = /* @__PURE__ */ new Set();
220
+ for (const abs of all) {
221
+ let text = "";
222
+ try {
223
+ text = import_node_fs2.default.readFileSync(abs, "utf8");
224
+ } catch {
225
+ continue;
226
+ }
227
+ if (text.includes(projectRoot) || text.includes(projectName)) {
228
+ related.add(abs);
229
+ stems.add(planStem(import_node_path2.default.basename(abs)));
230
+ }
231
+ }
232
+ for (const abs of all) {
233
+ if (stems.has(planStem(import_node_path2.default.basename(abs)))) related.add(abs);
234
+ }
235
+ return [...related];
236
+ }
237
+ var ADAPTERS = [
238
+ {
239
+ id: "claude",
240
+ name: "Claude Code",
241
+ projectGlobs: [
242
+ "CLAUDE.md",
243
+ "CLAUDE.local.md",
244
+ "**/CLAUDE.md",
245
+ ".claude/settings.json",
246
+ ".claude/settings.local.json",
247
+ ".claude/memory/",
248
+ ".claude/plans/",
249
+ ".claude/commands/",
250
+ ".claude/agents/",
251
+ ".claude/skills/"
252
+ ],
253
+ userDir: () => userClaudeDir(),
254
+ collectUser: (projectRoot, _home, exclude) => {
255
+ const root = userClaudeDir();
256
+ const out2 = [];
257
+ const key = cwdKey(projectRoot);
258
+ const projectsDir = import_node_path2.default.join(root, "projects");
259
+ let keys = [];
260
+ try {
261
+ keys = import_node_fs2.default.readdirSync(projectsDir, { withFileTypes: true }).filter((d) => d.isDirectory() && (d.name === key || d.name.startsWith(key + "-"))).map((d) => d.name);
262
+ } catch {
263
+ keys = [];
264
+ }
265
+ for (const k of keys) {
266
+ const memDir = import_node_path2.default.join(projectsDir, k, "memory");
267
+ const prefix = k === key ? "memory" : `memory-sub/${k.slice(key.length + 1)}`;
268
+ out2.push(...filesIn(memDir, exclude, prefix));
269
+ }
270
+ out2.push(...filesIn(import_node_path2.default.join(root, "skills"), exclude, "skills"));
271
+ out2.push(...filesIn(import_node_path2.default.join(root, "agents"), exclude, "agents"));
272
+ for (const abs of relatedByProject(import_node_path2.default.join(root, "plans"), projectRoot, exclude)) {
273
+ out2.push({ abs, rel: `plans/${import_node_path2.default.basename(abs)}` });
274
+ }
275
+ out2.push(...fileIfExists(import_node_path2.default.join(root, "CLAUDE.md"), "user/CLAUDE.md"));
276
+ out2.push(...fileIfExists(import_node_path2.default.join(root, "settings.json"), "user/settings.json"));
277
+ return out2;
278
+ }
279
+ },
280
+ {
281
+ id: "codex",
282
+ name: "OpenAI Codex",
283
+ projectGlobs: ["AGENTS.md", "**/AGENTS.md", ".codex/"],
284
+ userDir: (home) => import_node_path2.default.join(home, ".codex"),
285
+ collectUser: (projectRoot, home, exclude) => {
286
+ const root = import_node_path2.default.join(home, ".codex");
287
+ const out2 = [];
288
+ out2.push(...fileIfExists(import_node_path2.default.join(root, "AGENTS.md"), "user/AGENTS.md"));
289
+ out2.push(...fileIfExists(import_node_path2.default.join(root, "config.toml"), "user/config.toml"));
290
+ out2.push(...filesIn(import_node_path2.default.join(root, "prompts"), exclude, "prompts"));
291
+ return out2;
292
+ }
293
+ },
294
+ {
295
+ id: "cursor",
296
+ name: "Cursor",
297
+ // `.cursorrules` is the legacy single-file form; `.cursor/rules/*.mdc` is current.
298
+ projectGlobs: [".cursorrules", ".cursor/rules/", ".cursor/"],
299
+ userDir: (home) => import_node_path2.default.join(home, ".cursor"),
300
+ collectUser: (_projectRoot, home, exclude) => filesIn(import_node_path2.default.join(home, ".cursor", "rules"), exclude, "rules")
301
+ },
302
+ {
303
+ id: "copilot",
304
+ name: "GitHub Copilot",
305
+ projectGlobs: [
306
+ ".github/copilot-instructions.md",
307
+ ".github/instructions/",
308
+ ".github/prompts/"
309
+ ]
310
+ },
311
+ {
312
+ id: "windsurf",
313
+ name: "Windsurf",
314
+ projectGlobs: [".windsurfrules", ".windsurf/rules/", ".windsurf/"],
315
+ userDir: (home) => import_node_path2.default.join(home, ".windsurf")
316
+ },
317
+ {
318
+ id: "gemini",
319
+ name: "Gemini CLI",
320
+ projectGlobs: ["GEMINI.md", "**/GEMINI.md", ".gemini/"],
321
+ userDir: (home) => import_node_path2.default.join(home, ".gemini"),
322
+ collectUser: (_projectRoot, home, exclude) => {
323
+ const root = import_node_path2.default.join(home, ".gemini");
324
+ const out2 = [];
325
+ out2.push(...fileIfExists(import_node_path2.default.join(root, "GEMINI.md"), "user/GEMINI.md"));
326
+ out2.push(...fileIfExists(import_node_path2.default.join(root, "settings.json"), "user/settings.json"));
327
+ out2.push(...filesIn(import_node_path2.default.join(root, "commands"), exclude, "commands"));
328
+ return out2;
329
+ }
330
+ },
331
+ {
332
+ id: "cline",
333
+ name: "Cline",
334
+ projectGlobs: [".clinerules", ".clinerules/"]
335
+ },
336
+ {
337
+ id: "aider",
338
+ name: "Aider",
339
+ projectGlobs: ["CONVENTIONS.md", ".aider.conf.yml", ".aider.conf.yaml"]
340
+ },
341
+ {
342
+ id: "continue",
343
+ name: "Continue",
344
+ projectGlobs: [".continue/", ".continuerules"],
345
+ userDir: (home) => import_node_path2.default.join(home, ".continue")
346
+ }
347
+ ];
348
+ function adapterById(id) {
349
+ return ADAPTERS.find((a) => a.id === id);
350
+ }
351
+ function homeDir() {
352
+ return import_node_os2.default.homedir();
353
+ }
354
+ function detectAssistants(projectRoot, projectFiles) {
355
+ const home = homeDir();
356
+ return ADAPTERS.filter((a) => {
357
+ if (a.userDir) {
358
+ try {
359
+ if (import_node_fs2.default.existsSync(a.userDir(home))) return true;
360
+ } catch {
361
+ }
362
+ }
363
+ return projectFiles.some((rel) => matchesAny(rel, a.projectGlobs));
364
+ });
365
+ }
366
+
199
367
  // src/config.ts
368
+ var import_node_crypto = __toESM(require("crypto"), 1);
369
+ var import_node_fs3 = __toESM(require("fs"), 1);
370
+ var import_node_path3 = __toESM(require("path"), 1);
200
371
  var STORE_DIR = ".contextsync";
201
372
  var CONFIG_FILE = "config.json";
202
373
  var HANDOFF_FILE = "handoff.json";
@@ -225,29 +396,30 @@ var DEFAULT_EXCLUDE = [
225
396
  function defaultConfig(projectRoot) {
226
397
  return {
227
398
  projectId: import_node_crypto.default.randomUUID(),
228
- name: import_node_path2.default.basename(projectRoot),
399
+ name: import_node_path3.default.basename(projectRoot),
229
400
  rootHint: projectRoot,
230
401
  tiers: ["core", "handoff"],
402
+ assistants: ["auto"],
231
403
  artifactPaths: ["graphify-out"],
232
404
  exclude: [...DEFAULT_EXCLUDE],
233
405
  remotes: {}
234
406
  };
235
407
  }
236
408
  function findProjectRoot(start = process.cwd()) {
237
- let dir = import_node_path2.default.resolve(start);
409
+ let dir = import_node_path3.default.resolve(start);
238
410
  for (; ; ) {
239
- if (import_node_fs2.default.existsSync(import_node_path2.default.join(dir, STORE_DIR, CONFIG_FILE))) return dir;
240
- if (import_node_fs2.default.existsSync(import_node_path2.default.join(dir, ".git"))) return dir;
241
- const parent = import_node_path2.default.dirname(dir);
411
+ if (import_node_fs3.default.existsSync(import_node_path3.default.join(dir, STORE_DIR, CONFIG_FILE))) return dir;
412
+ if (import_node_fs3.default.existsSync(import_node_path3.default.join(dir, ".git"))) return dir;
413
+ const parent = import_node_path3.default.dirname(dir);
242
414
  if (parent === dir) return null;
243
415
  dir = parent;
244
416
  }
245
417
  }
246
418
  function storeDir(projectRoot) {
247
- return import_node_path2.default.join(projectRoot, STORE_DIR);
419
+ return import_node_path3.default.join(projectRoot, STORE_DIR);
248
420
  }
249
421
  function configPath(projectRoot) {
250
- return import_node_path2.default.join(storeDir(projectRoot), CONFIG_FILE);
422
+ return import_node_path3.default.join(storeDir(projectRoot), CONFIG_FILE);
251
423
  }
252
424
  function loadConfig(projectRoot) {
253
425
  const cfg = readJson(configPath(projectRoot));
@@ -256,7 +428,9 @@ function loadConfig(projectRoot) {
256
428
  ...defaultConfig(projectRoot),
257
429
  ...cfg,
258
430
  remotes: cfg.remotes ?? {},
259
- tiers: cfg.tiers ?? ["core", "handoff"]
431
+ tiers: cfg.tiers ?? ["core", "handoff"],
432
+ // Stores written before multi-assistant support have no `assistants` key.
433
+ assistants: cfg.assistants ?? ["auto"]
260
434
  };
261
435
  }
262
436
  function saveConfig(projectRoot, cfg) {
@@ -264,54 +438,12 @@ function saveConfig(projectRoot, cfg) {
264
438
  }
265
439
 
266
440
  // src/collector.ts
267
- var PROJECT_CONTEXT_GLOBS = [
268
- "CLAUDE.md",
269
- "CLAUDE.local.md",
270
- "AGENTS.md",
271
- "**/CLAUDE.md",
272
- "**/AGENTS.md",
273
- ".cursorrules",
274
- ".github/copilot-instructions.md",
275
- ".claude/settings.json",
276
- // `.local.json` variants are gitignored by default, so nothing else carries
277
- // them — which makes them exactly the kind of file this tool exists for.
278
- ".claude/settings.local.json",
279
- ".claude/memory/",
280
- ".claude/plans/",
281
- ".claude/commands/",
282
- ".claude/agents/",
283
- ".claude/skills/"
284
- ];
285
- function planStem(fileName) {
286
- return fileName.replace(/\.md$/, "").replace(/-agent-[0-9a-f]+$/i, "");
287
- }
288
- function collectPlans(plansDir, projectRoot, exclude) {
289
- const all = walk(plansDir, { exclude });
290
- if (all.length === 0) return [];
291
- const projectName = import_node_path3.default.basename(projectRoot);
292
- const related = /* @__PURE__ */ new Set();
293
- const stems = /* @__PURE__ */ new Set();
294
- for (const abs of all) {
295
- let text = "";
296
- try {
297
- text = import_node_fs3.default.readFileSync(abs, "utf8");
298
- } catch {
299
- continue;
300
- }
301
- if (text.includes(projectRoot) || text.includes(projectName)) {
302
- related.add(abs);
303
- stems.add(planStem(import_node_path3.default.basename(abs)));
304
- }
305
- }
306
- for (const abs of all) {
307
- if (stems.has(planStem(import_node_path3.default.basename(abs)))) related.add(abs);
308
- }
309
- return [...related];
310
- }
311
441
  function push(out2, sourcePath, storePath, tier, sourceRoot, ctx) {
312
442
  let size = 0;
313
443
  try {
314
- size = import_node_fs3.default.statSync(sourcePath).size;
444
+ const st = import_node_fs4.default.statSync(sourcePath);
445
+ if (!st.isFile()) return;
446
+ size = st.size;
315
447
  } catch {
316
448
  return;
317
449
  }
@@ -324,76 +456,66 @@ function push(out2, sourcePath, storePath, tier, sourceRoot, ctx) {
324
456
  restoreTemplate: makeTemplate(sourcePath, ctx)
325
457
  });
326
458
  }
459
+ function templateContextFor(projectRoot) {
460
+ return {
461
+ userClaude: userClaudeDir(),
462
+ project: projectRoot,
463
+ cwdKey: cwdKey(projectRoot),
464
+ home: homeDir()
465
+ };
466
+ }
327
467
  function collect(projectRoot, cfg, tiers) {
328
- const userClaude = userClaudeDir();
329
- const key = cwdKey(projectRoot);
330
- const ctx = { userClaude, project: projectRoot, cwdKey: key };
468
+ const ctx = templateContextFor(projectRoot);
469
+ const home = ctx.home;
331
470
  const files = [];
332
471
  const skippedTracked = [];
333
472
  const exclude = [...HARD_DENY, ...cfg.exclude];
334
473
  const tracked = gitTrackedSet(projectRoot);
474
+ const projectRel = walk(projectRoot, { exclude }).map((abs) => ({
475
+ abs,
476
+ rel: toPosix(import_node_path4.default.relative(projectRoot, abs))
477
+ }));
478
+ const configured = cfg.assistants && cfg.assistants.length > 0 && !cfg.assistants.includes("auto");
479
+ const assistants = configured ? cfg.assistants.map(adapterById).filter(Boolean) : detectAssistants(projectRoot, projectRel.map((p) => p.rel));
335
480
  if (tiers.includes("core")) {
336
- for (const abs of walk(projectRoot, { exclude })) {
337
- const rel = toPosix(import_node_path3.default.relative(projectRoot, abs));
338
- if (!matchesAny(rel, PROJECT_CONTEXT_GLOBS)) continue;
339
- if (tracked.has(abs)) {
340
- skippedTracked.push(rel);
341
- continue;
481
+ const claimed = /* @__PURE__ */ new Set();
482
+ for (const adapter of assistants) {
483
+ for (const { abs, rel } of projectRel) {
484
+ if (claimed.has(rel)) continue;
485
+ if (!matchesAny(rel, adapter.projectGlobs)) continue;
486
+ claimed.add(rel);
487
+ if (tracked.has(abs)) {
488
+ skippedTracked.push(rel);
489
+ continue;
490
+ }
491
+ push(files, abs, `project/${rel}`, "core", "project", ctx);
342
492
  }
343
- push(files, abs, `project/${rel}`, "core", "project", ctx);
344
493
  }
345
- const projectsDir = import_node_path3.default.join(userClaude, "projects");
346
- let projectKeys = [];
347
- try {
348
- projectKeys = import_node_fs3.default.readdirSync(projectsDir, { withFileTypes: true }).filter((d) => d.isDirectory() && (d.name === key || d.name.startsWith(key + "-"))).map((d) => d.name);
349
- } catch {
350
- projectKeys = [];
351
- }
352
- for (const pk of projectKeys) {
353
- const memoryDir = import_node_path3.default.join(projectsDir, pk, "memory");
354
- for (const abs of walk(memoryDir, { exclude })) {
355
- const rel = toPosix(import_node_path3.default.relative(memoryDir, abs));
356
- const storePath = pk === key ? `memory/${rel}` : `memory-sub/${pk.slice(key.length + 1)}/${rel}`;
357
- push(files, abs, storePath, "core", "user", ctx);
494
+ for (const adapter of assistants) {
495
+ if (!adapter.collectUser) continue;
496
+ for (const { abs, rel } of adapter.collectUser(projectRoot, home, exclude)) {
497
+ push(files, abs, `assistants/${adapter.id}/${rel}`, "core", "user", ctx);
358
498
  }
359
499
  }
360
- for (const abs of collectPlans(import_node_path3.default.join(userClaude, "plans"), projectRoot, exclude)) {
361
- const rel = toPosix(import_node_path3.default.relative(import_node_path3.default.join(userClaude, "plans"), abs));
362
- push(files, abs, `plans/${rel}`, "core", "user", ctx);
363
- }
364
- const skillsDir = import_node_path3.default.join(userClaude, "skills");
365
- for (const abs of walk(skillsDir, { exclude })) {
366
- const rel = toPosix(import_node_path3.default.relative(skillsDir, abs));
367
- push(files, abs, `skills/${rel}`, "core", "user", ctx);
368
- }
369
- const agentsDir = import_node_path3.default.join(userClaude, "agents");
370
- for (const abs of walk(agentsDir, { exclude })) {
371
- const rel = toPosix(import_node_path3.default.relative(agentsDir, abs));
372
- push(files, abs, `agents/${rel}`, "core", "user", ctx);
373
- }
374
- for (const name of ["CLAUDE.md", "settings.json"]) {
375
- const abs = import_node_path3.default.join(userClaude, name);
376
- if (import_node_fs3.default.existsSync(abs)) push(files, abs, `user/${name}`, "core", "user", ctx);
377
- }
378
500
  }
379
501
  if (tiers.includes("artifacts")) {
380
502
  for (const relDir of cfg.artifactPaths) {
381
- const absDir = import_node_path3.default.join(projectRoot, relDir);
503
+ const absDir = import_node_path4.default.join(projectRoot, relDir);
382
504
  for (const abs of walk(absDir, { exclude })) {
383
- const rel = toPosix(import_node_path3.default.relative(absDir, abs));
505
+ const rel = toPosix(import_node_path4.default.relative(absDir, abs));
384
506
  push(files, abs, `artifacts/${relDir}/${rel}`, "artifacts", "project", ctx);
385
507
  }
386
508
  }
387
509
  }
388
510
  if (tiers.includes("transcripts")) {
389
- const projDir = import_node_path3.default.join(userClaude, "projects", key);
511
+ const projDir = import_node_path4.default.join(userClaudeDir(), "projects", ctx.cwdKey);
390
512
  for (const abs of walk(projDir, { exclude })) {
391
- const rel = toPosix(import_node_path3.default.relative(projDir, abs));
513
+ const rel = toPosix(import_node_path4.default.relative(projDir, abs));
392
514
  if (rel.startsWith("memory/")) continue;
393
- push(files, abs, `transcripts/${rel}`, "transcripts", "user", ctx);
515
+ push(files, abs, `transcripts/claude/${rel}`, "transcripts", "user", ctx);
394
516
  }
395
517
  }
396
- return { files, skippedTracked, ctx };
518
+ return { files, skippedTracked, assistants, ctx };
397
519
  }
398
520
  function describeExcluded(projectRoot, tiers) {
399
521
  const userClaude = userClaudeDir();
@@ -406,15 +528,17 @@ function describeExcluded(projectRoot, tiers) {
406
528
  if (filter && !filter(abs)) continue;
407
529
  files++;
408
530
  try {
409
- bytes += import_node_fs3.default.statSync(abs).size;
531
+ bytes += import_node_fs4.default.statSync(abs).size;
410
532
  } catch {
411
533
  }
412
534
  }
413
535
  return { files, bytes };
414
536
  };
415
537
  if (!tiers.includes("transcripts")) {
416
- const projDir = import_node_path3.default.join(userClaude, "projects", key);
417
- const m = measure(projDir, (p) => !p.includes(`${import_node_path3.default.sep}memory${import_node_path3.default.sep}`));
538
+ const m = measure(
539
+ import_node_path4.default.join(userClaude, "projects", key),
540
+ (p) => !p.includes(`${import_node_path4.default.sep}memory${import_node_path4.default.sep}`)
541
+ );
418
542
  if (m.files > 0) {
419
543
  out2.push({
420
544
  label: "session transcripts",
@@ -428,7 +552,7 @@ function describeExcluded(projectRoot, tiers) {
428
552
  ["file-history", "edit-undo snapshots", "transient local undo state, not portable context"],
429
553
  ["tasks", "task outputs", "session-scoped tool output"]
430
554
  ]) {
431
- const m = measure(import_node_path3.default.join(userClaude, dir));
555
+ const m = measure(import_node_path4.default.join(userClaude, dir));
432
556
  if (m.files > 0) out2.push({ label, ...m, reason });
433
557
  }
434
558
  return out2.filter((g) => g.bytes > 0);
@@ -448,9 +572,89 @@ function summarize(files) {
448
572
  return out2;
449
573
  }
450
574
 
575
+ // src/handoff.ts
576
+ function asStringArray(value) {
577
+ if (value === void 0 || value === null) return [];
578
+ if (!Array.isArray(value)) return null;
579
+ if (!value.every((v) => typeof v === "string")) return null;
580
+ return value;
581
+ }
582
+ function validateHandoff(raw) {
583
+ const errors = [];
584
+ if (typeof raw !== "object" || raw === null || Array.isArray(raw)) {
585
+ return { ok: false, errors: ["handoff must be a JSON object"] };
586
+ }
587
+ const o = raw;
588
+ const goal = typeof o.goal === "string" ? o.goal.trim() : "";
589
+ const nextStep = typeof o.nextStep === "string" ? o.nextStep.trim() : "";
590
+ if (!goal) errors.push("`goal` is required (what the session set out to do)");
591
+ if (!nextStep) errors.push("`nextStep` is required (the single next action)");
592
+ const decisions = asStringArray(o.decisions);
593
+ const openThreads = asStringArray(o.openThreads);
594
+ const filesTouched = asStringArray(o.filesTouched);
595
+ if (decisions === null) errors.push("`decisions` must be an array of strings");
596
+ if (openThreads === null) errors.push("`openThreads` must be an array of strings");
597
+ if (filesTouched === null) errors.push("`filesTouched` must be an array of strings");
598
+ let updatedAt = typeof o.updatedAt === "string" ? o.updatedAt : "";
599
+ if (!updatedAt || Number.isNaN(Date.parse(updatedAt))) updatedAt = (/* @__PURE__ */ new Date()).toISOString();
600
+ if (errors.length > 0) return { ok: false, errors };
601
+ return {
602
+ ok: true,
603
+ errors: [],
604
+ handoff: {
605
+ updatedAt,
606
+ goal,
607
+ decisions: decisions ?? [],
608
+ openThreads: openThreads ?? [],
609
+ filesTouched: filesTouched ?? [],
610
+ nextStep,
611
+ ...typeof o.notes === "string" && o.notes.trim() ? { notes: o.notes.trim() } : {}
612
+ }
613
+ };
614
+ }
615
+ function ago(iso) {
616
+ const ms = Date.now() - Date.parse(iso);
617
+ if (Number.isNaN(ms)) return "unknown";
618
+ const mins = Math.floor(ms / 6e4);
619
+ if (mins < 1) return "just now";
620
+ if (mins < 60) return `${mins}m ago`;
621
+ const hours = Math.floor(mins / 60);
622
+ if (hours < 24) return `${hours}h ago`;
623
+ return `${Math.floor(hours / 24)}d ago`;
624
+ }
625
+ function formatHandoff(h) {
626
+ const lines = [`Where you left off (${ago(h.updatedAt)})`, "", ` Goal ${h.goal}`, ` Next step ${h.nextStep}`];
627
+ if (h.decisions.length > 0) {
628
+ lines.push("", " Decided:");
629
+ for (const d of h.decisions) lines.push(` \xB7 ${d}`);
630
+ }
631
+ if (h.openThreads.length > 0) {
632
+ lines.push("", " Still open:");
633
+ for (const t of h.openThreads) lines.push(` \xB7 ${t}`);
634
+ }
635
+ if (h.filesTouched.length > 0) {
636
+ const shown = h.filesTouched.slice(0, 12);
637
+ lines.push("", " Files touched:");
638
+ for (const f of shown) lines.push(` ${f}`);
639
+ if (h.filesTouched.length > shown.length) {
640
+ lines.push(` \u2026 and ${h.filesTouched.length - shown.length} more`);
641
+ }
642
+ }
643
+ if (h.notes) lines.push("", ` Notes: ${h.notes}`);
644
+ return lines;
645
+ }
646
+ function handoffAge(h) {
647
+ return ago(h.updatedAt);
648
+ }
649
+ function isStale(h, newestContentMs) {
650
+ const t = Date.parse(h.updatedAt);
651
+ if (Number.isNaN(t)) return true;
652
+ return newestContentMs - t > 60 * 60 * 1e3;
653
+ }
654
+
451
655
  // src/scaffold.ts
452
- var import_node_fs4 = __toESM(require("fs"), 1);
453
- var import_node_path4 = __toESM(require("path"), 1);
656
+ var import_node_fs5 = __toESM(require("fs"), 1);
657
+ var import_node_path5 = __toESM(require("path"), 1);
454
658
  var SLASH_COMMAND_PATH = ".claude/commands/context.md";
455
659
  var SLASH_COMMAND_BODY = `---
456
660
  description: Sync this project's LLM context (memory, skills, instructions, handoff)
@@ -500,26 +704,26 @@ Run the context-sync action requested in: $ARGUMENTS
500
704
  Run: \`npx @tricknowtech/context status\` and summarize the result.
501
705
  `;
502
706
  function installSlashCommand(projectRoot) {
503
- const dest = import_node_path4.default.join(projectRoot, SLASH_COMMAND_PATH);
504
- if (import_node_fs4.default.existsSync(dest)) return { path: SLASH_COMMAND_PATH, written: false };
505
- ensureDir(import_node_path4.default.dirname(dest));
506
- import_node_fs4.default.writeFileSync(dest, SLASH_COMMAND_BODY, "utf8");
707
+ const dest = import_node_path5.default.join(projectRoot, SLASH_COMMAND_PATH);
708
+ if (import_node_fs5.default.existsSync(dest)) return { path: SLASH_COMMAND_PATH, written: false };
709
+ ensureDir(import_node_path5.default.dirname(dest));
710
+ import_node_fs5.default.writeFileSync(dest, SLASH_COMMAND_BODY, "utf8");
507
711
  return { path: SLASH_COMMAND_PATH, written: true };
508
712
  }
509
713
  function ensureGitignoreEntries(projectRoot, artifactsEnabled) {
510
- const gitignore = import_node_path4.default.join(projectRoot, ".gitignore");
714
+ const gitignore = import_node_path5.default.join(projectRoot, ".gitignore");
511
715
  const wanted = [".contextsync/transcripts/"];
512
716
  if (!artifactsEnabled) wanted.push(".contextsync/artifacts/");
513
717
  let existing = "";
514
718
  try {
515
- existing = import_node_fs4.default.readFileSync(gitignore, "utf8");
719
+ existing = import_node_fs5.default.readFileSync(gitignore, "utf8");
516
720
  } catch {
517
721
  }
518
722
  const lines = new Set(existing.split("\n").map((l) => l.trim()));
519
723
  const missing = wanted.filter((w) => !lines.has(w));
520
724
  if (missing.length === 0) return [];
521
725
  const prefix = existing.length > 0 && !existing.endsWith("\n") ? "\n" : "";
522
- import_node_fs4.default.appendFileSync(
726
+ import_node_fs5.default.appendFileSync(
523
727
  gitignore,
524
728
  `${prefix}
525
729
  # tricknowtech context-sync \u2014 never commit these tiers
@@ -531,7 +735,7 @@ ${missing.join("\n")}
531
735
  }
532
736
 
533
737
  // src/secrets.ts
534
- var import_node_fs5 = __toESM(require("fs"), 1);
738
+ var import_node_fs6 = __toESM(require("fs"), 1);
535
739
  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;
536
740
  function looksLikeCredential(value) {
537
741
  if (/^[A-Za-z_$][\w$]*(?:\.[A-Za-z_$][\w$]*)+$/.test(value)) return false;
@@ -585,7 +789,7 @@ function scanFiles(files) {
585
789
  for (const file of files) {
586
790
  let buf;
587
791
  try {
588
- buf = import_node_fs5.default.readFileSync(file.sourcePath);
792
+ buf = import_node_fs6.default.readFileSync(file.sourcePath);
589
793
  } catch {
590
794
  continue;
591
795
  }
@@ -623,8 +827,8 @@ function formatHits(hits) {
623
827
  }
624
828
 
625
829
  // src/store.ts
626
- var import_node_fs6 = __toESM(require("fs"), 1);
627
- var import_node_path5 = __toESM(require("path"), 1);
830
+ var import_node_fs7 = __toESM(require("fs"), 1);
831
+ var import_node_path6 = __toESM(require("path"), 1);
628
832
  var MANIFEST_FILE = "manifest.json";
629
833
  var LocalStore = class {
630
834
  constructor(projectRoot) {
@@ -635,13 +839,13 @@ var LocalStore = class {
635
839
  return storeDir(this.projectRoot);
636
840
  }
637
841
  write(files, projectRoot) {
638
- for (const name of import_node_fs6.default.existsSync(this.dir) ? import_node_fs6.default.readdirSync(this.dir) : []) {
842
+ for (const name of import_node_fs7.default.existsSync(this.dir) ? import_node_fs7.default.readdirSync(this.dir) : []) {
639
843
  if (name === "config.json" || name === HANDOFF_FILE) continue;
640
- import_node_fs6.default.rmSync(import_node_path5.default.join(this.dir, name), { recursive: true, force: true });
844
+ import_node_fs7.default.rmSync(import_node_path6.default.join(this.dir, name), { recursive: true, force: true });
641
845
  }
642
846
  const entries = [];
643
847
  for (const file of files) {
644
- const dest = import_node_path5.default.join(this.dir, file.storePath);
848
+ const dest = import_node_path6.default.join(this.dir, file.storePath);
645
849
  try {
646
850
  copyFile(file.sourcePath, dest);
647
851
  } catch {
@@ -662,11 +866,11 @@ var LocalStore = class {
662
866
  writtenFrom: projectRoot,
663
867
  entries
664
868
  };
665
- writeJson(import_node_path5.default.join(this.dir, MANIFEST_FILE), manifest);
869
+ writeJson(import_node_path6.default.join(this.dir, MANIFEST_FILE), manifest);
666
870
  return manifest;
667
871
  }
668
872
  readManifest() {
669
- return readJson(import_node_path5.default.join(this.dir, MANIFEST_FILE));
873
+ return readJson(import_node_path6.default.join(this.dir, MANIFEST_FILE));
670
874
  }
671
875
  restore(ctx, opts = {}) {
672
876
  const manifest = this.readManifest();
@@ -674,12 +878,12 @@ var LocalStore = class {
674
878
  const skipped = [];
675
879
  if (!manifest) return { restored, skipped };
676
880
  for (const entry of manifest.entries) {
677
- const src = import_node_path5.default.join(this.dir, entry.storePath);
678
- if (!import_node_fs6.default.existsSync(src)) continue;
881
+ const src = import_node_path6.default.join(this.dir, entry.storePath);
882
+ if (!import_node_fs7.default.existsSync(src)) continue;
679
883
  const dest = resolveTemplate(entry.restoreTemplate, ctx);
680
- if (!opts.force && import_node_fs6.default.existsSync(dest)) {
884
+ if (!opts.force && import_node_fs7.default.existsSync(dest)) {
681
885
  try {
682
- if (!import_node_fs6.default.readFileSync(dest).equals(import_node_fs6.default.readFileSync(src))) {
886
+ if (!import_node_fs7.default.readFileSync(dest).equals(import_node_fs7.default.readFileSync(src))) {
683
887
  skipped.push(entry.storePath);
684
888
  continue;
685
889
  }
@@ -689,8 +893,8 @@ var LocalStore = class {
689
893
  }
690
894
  }
691
895
  try {
692
- ensureDir(import_node_path5.default.dirname(dest));
693
- import_node_fs6.default.copyFileSync(src, dest);
896
+ ensureDir(import_node_path6.default.dirname(dest));
897
+ import_node_fs7.default.copyFileSync(src, dest);
694
898
  restored.push(entry.storePath);
695
899
  } catch {
696
900
  skipped.push(entry.storePath);
@@ -699,10 +903,10 @@ var LocalStore = class {
699
903
  return { restored, skipped };
700
904
  }
701
905
  readHandoff() {
702
- return readJson(import_node_path5.default.join(this.dir, HANDOFF_FILE));
906
+ return readJson(import_node_path6.default.join(this.dir, HANDOFF_FILE));
703
907
  }
704
908
  writeHandoff(handoff) {
705
- writeJson(import_node_path5.default.join(this.dir, HANDOFF_FILE), handoff);
909
+ writeJson(import_node_path6.default.join(this.dir, HANDOFF_FILE), handoff);
706
910
  }
707
911
  };
708
912
 
@@ -727,12 +931,12 @@ function requireProject() {
727
931
  }
728
932
  const cfg = loadConfig(root);
729
933
  if (!cfg) {
730
- return fail([`No context store found at ${import_node_path6.default.join(root, ".contextsync")}.`, "Run `ctx init` first."]);
934
+ return fail([`No context store found at ${import_node_path7.default.join(root, ".contextsync")}.`, "Run `ctx init` first."]);
731
935
  }
732
936
  return { root, cfg };
733
937
  }
734
938
  function templateContext(root) {
735
- return { userClaude: userClaudeDir(), project: root, cwdKey: cwdKey(root) };
939
+ return templateContextFor(root);
736
940
  }
737
941
  function effectiveTiers(cfg) {
738
942
  const tiers = cfg.tiers.filter((t) => LOCAL_TIERS.includes(t));
@@ -750,7 +954,7 @@ function cmdInit(opts = {}) {
750
954
  saveConfig(root, cfg);
751
955
  const lines = [
752
956
  `Initialised context store for "${cfg.name}"`,
753
- ` store ${import_node_path6.default.relative(root, storeDir(root))}/`,
957
+ ` store ${import_node_path7.default.relative(root, storeDir(root))}/`,
754
958
  ` tiers ${cfg.tiers.join(", ")}`
755
959
  ];
756
960
  const slash = installSlashCommand(root);
@@ -768,7 +972,7 @@ function cmdPush(opts = {}) {
768
972
  if ("code" in found) return found;
769
973
  const { root, cfg } = found;
770
974
  const { tiers, refused } = effectiveTiers(cfg);
771
- const { files, skippedTracked } = collect(root, cfg, tiers);
975
+ const { files, skippedTracked, assistants } = collect(root, cfg, tiers);
772
976
  const lines = [];
773
977
  if (refused.length > 0) {
774
978
  lines.push(
@@ -807,12 +1011,15 @@ function cmdPush(opts = {}) {
807
1011
  } else {
808
1012
  const store = new LocalStore(root);
809
1013
  store.write(files, root);
810
- lines.push(`Synced ${files.length} files (${formatBytes(totalBytes)}) to ${import_node_path6.default.relative(root, storeDir(root))}/`);
1014
+ lines.push(`Synced ${files.length} files (${formatBytes(totalBytes)}) to ${import_node_path7.default.relative(root, storeDir(root))}/`);
811
1015
  }
812
1016
  for (const tier of ALL_TIERS) {
813
1017
  const t = totals[tier];
814
1018
  if (t.count > 0) lines.push(` ${tier.padEnd(11)} ${String(t.count).padStart(4)} files ${formatBytes(t.bytes)}`);
815
1019
  }
1020
+ if (assistants.length > 0) {
1021
+ lines.push("", `Assistants: ${assistants.map((a) => a.name).join(", ")}`);
1022
+ }
816
1023
  if (skippedTracked.length > 0) {
817
1024
  lines.push("", `${skippedTracked.length} project files skipped \u2014 git already tracks them.`);
818
1025
  }
@@ -824,6 +1031,20 @@ function cmdPush(opts = {}) {
824
1031
  }
825
1032
  }
826
1033
  if (!opts.dryRun) {
1034
+ const store = new LocalStore(root);
1035
+ const raw = store.readHandoff();
1036
+ const v = raw ? validateHandoff(raw) : null;
1037
+ if (!raw) {
1038
+ lines.push(
1039
+ "",
1040
+ "No handoff written \u2014 the other machine will get your project knowledge but",
1041
+ "not where you left off. Use `/context push` in Claude Code to include one."
1042
+ );
1043
+ } else if (v && !v.ok) {
1044
+ lines.push("", "Handoff present but malformed (it will be ignored):", ...v.errors.map((e) => ` \xB7 ${e}`));
1045
+ } else if (v?.ok && isStale(v.handoff, Date.now())) {
1046
+ lines.push("", `Handoff is ${handoffAge(v.handoff)} \u2014 re-run \`/context push\` to refresh it.`);
1047
+ }
827
1048
  lines.push("", "Commit .contextsync/ to carry this context with the repo.");
828
1049
  }
829
1050
  return ok(lines);
@@ -835,7 +1056,7 @@ function cmdPull(opts = {}) {
835
1056
  const store = new LocalStore(root);
836
1057
  const manifest = store.readManifest();
837
1058
  if (!manifest) {
838
- return fail([`No manifest in ${import_node_path6.default.relative(root, storeDir(root))}/.`, "Run `ctx push` on the source machine first."]);
1059
+ return fail([`No manifest in ${import_node_path7.default.relative(root, storeDir(root))}/.`, "Run `ctx push` on the source machine first."]);
839
1060
  }
840
1061
  const ctx = templateContext(root);
841
1062
  const { restored, skipped } = store.restore(ctx, { force: opts.force });
@@ -853,12 +1074,114 @@ function cmdPull(opts = {}) {
853
1074
  "Re-run with --force to overwrite them."
854
1075
  );
855
1076
  }
856
- const handoff = store.readHandoff();
857
- if (handoff) {
858
- lines.push("", `Handoff (${handoff.updatedAt}):`, ` goal: ${handoff.goal}`, ` next: ${handoff.nextStep}`);
1077
+ const raw = store.readHandoff();
1078
+ if (!raw) {
1079
+ lines.push(
1080
+ "",
1081
+ "No handoff in this store \u2014 you have the project knowledge, but not where the",
1082
+ "last session stopped. Run `/context push` (not bare `ctx push`) on the other",
1083
+ "machine: only the model can write the handoff, since only it has the conversation."
1084
+ );
1085
+ return ok(lines);
859
1086
  }
1087
+ const check = validateHandoff(raw);
1088
+ if (!check.ok) {
1089
+ lines.push("", "A handoff exists but is malformed and was ignored:", ...check.errors.map((e) => ` \xB7 ${e}`));
1090
+ return ok(lines);
1091
+ }
1092
+ lines.push("", ...formatHandoff(check.handoff));
860
1093
  return ok(lines);
861
1094
  }
1095
+ function cmdHandoff(opts = {}) {
1096
+ const found = requireProject();
1097
+ if ("code" in found) return found;
1098
+ const { root } = found;
1099
+ const store = new LocalStore(root);
1100
+ if (opts.set !== void 0) {
1101
+ let parsed;
1102
+ try {
1103
+ parsed = JSON.parse(opts.set);
1104
+ } catch (e) {
1105
+ return fail([`Could not parse handoff JSON: ${e.message}`]);
1106
+ }
1107
+ const check2 = validateHandoff(parsed);
1108
+ if (!check2.ok) return fail(["Handoff is not valid:", ...check2.errors.map((e) => ` \xB7 ${e}`)]);
1109
+ store.writeHandoff(check2.handoff);
1110
+ return ok([`Handoff saved (${import_node_path7.default.relative(root, storeDir(root))}/handoff.json).`, "", ...formatHandoff(check2.handoff)]);
1111
+ }
1112
+ const raw = store.readHandoff();
1113
+ if (!raw) {
1114
+ return ok([
1115
+ "No handoff yet.",
1116
+ "",
1117
+ "Write one with `/context push` in Claude Code, or pipe JSON:",
1118
+ ` ctx handoff --set '{"goal":"\u2026","nextStep":"\u2026"}'`
1119
+ ]);
1120
+ }
1121
+ const check = validateHandoff(raw);
1122
+ if (!check.ok) return fail(["Handoff is malformed:", ...check.errors.map((e) => ` \xB7 ${e}`)]);
1123
+ return ok(formatHandoff(check.handoff));
1124
+ }
1125
+ function cmdDoctor() {
1126
+ const root = findProjectRoot();
1127
+ if (!root) return fail(["Not inside a project. Run `ctx init` first."]);
1128
+ const lines = [];
1129
+ let problems = 0;
1130
+ const check = (okFlag, label, detail) => {
1131
+ if (!okFlag) problems++;
1132
+ lines.push(` ${okFlag ? "\u2713" : "\u2717"} ${label.padEnd(28)} ${detail}`);
1133
+ };
1134
+ const cfg = loadConfig(root);
1135
+ check(Boolean(cfg), "store initialised", cfg ? `${import_node_path7.default.relative(root, storeDir(root))}/` : "missing \u2014 run `ctx init`");
1136
+ if (!cfg) return { code: 1, lines: ["Setup check", "", ...lines] };
1137
+ const store = new LocalStore(root);
1138
+ const manifest = store.readManifest();
1139
+ check(Boolean(manifest), "pushed at least once", manifest ? `${manifest.entries.length} files` : "never \u2014 run `ctx push`");
1140
+ const detected = collect(root, cfg, ["core"]).assistants;
1141
+ check(
1142
+ detected.length > 0,
1143
+ "assistants detected",
1144
+ detected.length > 0 ? detected.map((a) => a.name).join(", ") : "none \u2014 no known assistant config found"
1145
+ );
1146
+ check(
1147
+ import_node_fs8.default.existsSync(import_node_path7.default.join(root, SLASH_COMMAND_PATH)),
1148
+ "/context slash command",
1149
+ import_node_fs8.default.existsSync(import_node_path7.default.join(root, SLASH_COMMAND_PATH)) ? SLASH_COMMAND_PATH : `missing \u2014 run \`ctx init --force\``
1150
+ );
1151
+ const inGit = isGitRepo(root);
1152
+ check(inGit, "git repository", inGit ? "yes \u2014 store travels with the repo" : "no \u2014 the store will not sync anywhere");
1153
+ let storeIgnored = false;
1154
+ if (inGit) {
1155
+ try {
1156
+ (0, import_node_child_process2.execFileSync)("git", ["-C", root, "check-ignore", "-q", ".contextsync/config.json"], { stdio: "ignore" });
1157
+ storeIgnored = true;
1158
+ } catch {
1159
+ storeIgnored = false;
1160
+ }
1161
+ }
1162
+ check(!storeIgnored, "store is committable", storeIgnored ? "IGNORED by git \u2014 it will never reach another machine" : "not gitignored");
1163
+ const ctx = templateContext(root);
1164
+ const memEntry = manifest?.entries.find((e) => e.storePath.startsWith("memory/"));
1165
+ if (memEntry) {
1166
+ const dest = resolveTemplate(memEntry.restoreTemplate, ctx);
1167
+ const expectedDir = import_node_path7.default.join(userClaudeDir(), "projects", cwdKey(root), "memory");
1168
+ check(dest.startsWith(expectedDir), "memory restore path", dest.startsWith(expectedDir) ? expectedDir : `WRONG \u2192 ${dest}`);
1169
+ } else {
1170
+ check(false, "memory captured", "none found \u2014 is ~/.claude/projects/<key>/memory populated?");
1171
+ }
1172
+ const raw = store.readHandoff();
1173
+ if (!raw) {
1174
+ check(false, "handoff", "absent \u2014 `/context push` writes it; resume will have nothing to say");
1175
+ } else {
1176
+ const v = validateHandoff(raw);
1177
+ check(v.ok, "handoff", v.ok ? `valid, ${handoffAge(v.handoff)}` : v.errors[0]);
1178
+ if (v.ok && manifest && isStale(v.handoff, Date.parse(manifest.updatedAt))) {
1179
+ lines.push(" ! handoff is much older than the last push \u2014 re-run `/context push`");
1180
+ }
1181
+ }
1182
+ const header = problems === 0 ? "Setup check \u2014 all good" : `Setup check \u2014 ${problems} problem${problems === 1 ? "" : "s"}`;
1183
+ return { code: problems === 0 ? 0 : 1, lines: [header, "", ...lines] };
1184
+ }
862
1185
  function cmdStatus() {
863
1186
  const found = requireProject();
864
1187
  if ("code" in found) return found;
@@ -866,11 +1189,16 @@ function cmdStatus() {
866
1189
  const store = new LocalStore(root);
867
1190
  const manifest = store.readManifest();
868
1191
  const { tiers } = effectiveTiers(cfg);
869
- const { files, skippedTracked } = collect(root, cfg, tiers);
1192
+ const { files, skippedTracked, assistants } = collect(root, cfg, tiers);
1193
+ const rawHandoff = store.readHandoff();
1194
+ const hv = rawHandoff ? validateHandoff(rawHandoff) : null;
1195
+ const handoffLabel = !rawHandoff ? "none \u2014 run `/context push` to record where you left off" : hv?.ok ? `${handoffAge(hv.handoff)}` : "malformed (will be ignored)";
870
1196
  const lines = [
871
1197
  `Project ${cfg.name}`,
872
- `Store ${import_node_path6.default.relative(root, storeDir(root))}/`,
1198
+ `Store ${import_node_path7.default.relative(root, storeDir(root))}/`,
873
1199
  `Tiers ${cfg.tiers.join(", ")}`,
1200
+ `Handoff ${handoffLabel}`,
1201
+ `Tools ${assistants.length > 0 ? assistants.map((a) => a.name).join(", ") : "none detected"}`,
874
1202
  `Remotes ${Object.keys(cfg.remotes).length > 0 ? Object.keys(cfg.remotes).join(", ") : "none (local only)"}`,
875
1203
  ""
876
1204
  ];
@@ -887,7 +1215,7 @@ function cmdStatus() {
887
1215
  if (!e) return false;
888
1216
  if (e.size !== f.size) return true;
889
1217
  try {
890
- return !import_node_fs7.default.readFileSync(import_node_path6.default.join(storeDir(root), k)).equals(import_node_fs7.default.readFileSync(f.sourcePath));
1218
+ return !import_node_fs8.default.readFileSync(import_node_path7.default.join(storeDir(root), k)).equals(import_node_fs8.default.readFileSync(f.sourcePath));
891
1219
  } catch {
892
1220
  return true;
893
1221
  }
@@ -912,13 +1240,16 @@ function cmdStatus() {
912
1240
  }
913
1241
 
914
1242
  // src/cli.ts
1243
+ var VERSION = "0.3.0";
915
1244
  var USAGE = `tricknowtech context-sync \u2014 carry a project's LLM context between machines
916
1245
 
917
1246
  Usage
918
1247
  ctx init [--artifacts] [--force] Create the store and install /context
919
1248
  ctx push [--dry-run] Collect context into the store
920
- ctx pull [--force] Restore context from the store
921
- ctx status Show what has changed since the last push
1249
+ ctx pull [--force] Restore context, and show where you left off
1250
+ ctx status What has changed since the last push
1251
+ ctx handoff [--set '<json>'] Show or write the session handoff
1252
+ ctx doctor Check the setup is actually wired correctly
922
1253
 
923
1254
  Options
924
1255
  --artifacts Include derived indexes (graphify-out/, etc.)
@@ -929,20 +1260,38 @@ Options
929
1260
  -v, --version Show version
930
1261
 
931
1262
  The store lives in .contextsync/ and is meant to be committed, so context
932
- travels with the code. Session transcripts are excluded from local mode.`;
1263
+ travels with the code. Session transcripts are excluded from local mode.
1264
+
1265
+ Prefer \`/context push\` inside Claude Code over bare \`ctx push\`: only the
1266
+ model can write the handoff that lets the next machine resume the work.`;
933
1267
  function parseArgs(argv) {
934
1268
  const flags = /* @__PURE__ */ new Set();
1269
+ const values = /* @__PURE__ */ new Map();
935
1270
  let command = "";
936
- for (const arg of argv) {
937
- if (arg.startsWith("-")) flags.add(arg.replace(/^-+/, ""));
938
- else if (!command) command = arg;
1271
+ for (let i = 0; i < argv.length; i++) {
1272
+ const arg = argv[i];
1273
+ if (arg.startsWith("-")) {
1274
+ const name = arg.replace(/^-+/, "");
1275
+ const eq = name.indexOf("=");
1276
+ if (eq !== -1) {
1277
+ values.set(name.slice(0, eq), name.slice(eq + 1));
1278
+ continue;
1279
+ }
1280
+ if (name === "set" && i + 1 < argv.length && !argv[i + 1].startsWith("-")) {
1281
+ values.set(name, argv[++i]);
1282
+ continue;
1283
+ }
1284
+ flags.add(name);
1285
+ } else if (!command) {
1286
+ command = arg;
1287
+ }
939
1288
  }
940
- return { command, flags };
1289
+ return { command, flags, values };
941
1290
  }
942
1291
  function run(argv) {
943
- const { command, flags } = parseArgs(argv);
1292
+ const { command, flags, values } = parseArgs(argv);
944
1293
  if (flags.has("h") || flags.has("help")) return { code: 0, lines: [USAGE] };
945
- if (flags.has("v") || flags.has("version")) return { code: 0, lines: ["0.1.1"] };
1294
+ if (flags.has("v") || flags.has("version")) return { code: 0, lines: [VERSION] };
946
1295
  switch (command) {
947
1296
  case "init":
948
1297
  return cmdInit({ artifacts: flags.has("artifacts"), force: flags.has("force") });
@@ -950,6 +1299,10 @@ function run(argv) {
950
1299
  return cmdPush({ allowSecrets: flags.has("allow-secrets"), dryRun: flags.has("dry-run") });
951
1300
  case "pull":
952
1301
  return cmdPull({ force: flags.has("force") });
1302
+ case "handoff":
1303
+ return cmdHandoff({ set: values.get("set") });
1304
+ case "doctor":
1305
+ return cmdDoctor();
953
1306
  case "status":
954
1307
  case "":
955
1308
  return cmdStatus();