@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
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
|
|
40
|
-
var
|
|
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
|
|
44
|
-
var
|
|
44
|
+
var import_node_fs4 = __toESM(require("fs"), 1);
|
|
45
|
+
var import_node_path4 = __toESM(require("path"), 1);
|
|
45
46
|
|
|
46
|
-
// src/
|
|
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
|
-
|
|
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:
|
|
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 =
|
|
409
|
+
let dir = import_node_path3.default.resolve(start);
|
|
238
410
|
for (; ; ) {
|
|
239
|
-
if (
|
|
240
|
-
if (
|
|
241
|
-
const parent =
|
|
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
|
|
419
|
+
return import_node_path3.default.join(projectRoot, STORE_DIR);
|
|
248
420
|
}
|
|
249
421
|
function configPath(projectRoot) {
|
|
250
|
-
return
|
|
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
|
-
|
|
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
|
|
329
|
-
const
|
|
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
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
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
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
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 =
|
|
503
|
+
const absDir = import_node_path4.default.join(projectRoot, relDir);
|
|
382
504
|
for (const abs of walk(absDir, { exclude })) {
|
|
383
|
-
const rel = toPosix(
|
|
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 =
|
|
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(
|
|
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 +=
|
|
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
|
|
417
|
-
|
|
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(
|
|
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
|
|
453
|
-
var
|
|
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 =
|
|
504
|
-
if (
|
|
505
|
-
ensureDir(
|
|
506
|
-
|
|
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 =
|
|
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 =
|
|
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
|
-
|
|
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
|
|
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 =
|
|
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
|
|
627
|
-
var
|
|
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
|
|
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
|
-
|
|
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 =
|
|
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(
|
|
869
|
+
writeJson(import_node_path6.default.join(this.dir, MANIFEST_FILE), manifest);
|
|
666
870
|
return manifest;
|
|
667
871
|
}
|
|
668
872
|
readManifest() {
|
|
669
|
-
return readJson(
|
|
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 =
|
|
678
|
-
if (!
|
|
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 &&
|
|
884
|
+
if (!opts.force && import_node_fs7.default.existsSync(dest)) {
|
|
681
885
|
try {
|
|
682
|
-
if (!
|
|
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(
|
|
693
|
-
|
|
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(
|
|
906
|
+
return readJson(import_node_path6.default.join(this.dir, HANDOFF_FILE));
|
|
703
907
|
}
|
|
704
908
|
writeHandoff(handoff) {
|
|
705
|
-
writeJson(
|
|
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 ${
|
|
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
|
|
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 ${
|
|
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 ${
|
|
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 ${
|
|
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
|
|
857
|
-
if (
|
|
858
|
-
lines.push(
|
|
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 ${
|
|
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 !
|
|
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
|
|
921
|
-
ctx status
|
|
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 (
|
|
937
|
-
|
|
938
|
-
|
|
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: [
|
|
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();
|