@tricknowtech/context 0.1.0 → 0.2.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 +27 -2
- package/dist/{chunk-DEH5MUFT.js → chunk-6GB2SN2L.js} +170 -4
- package/dist/cli.cjs +324 -16
- package/dist/cli.js +168 -14
- package/dist/index.cjs +175 -4
- package/dist/index.d.cts +37 -1
- package/dist/index.d.ts +37 -1
- package/dist/index.js +11 -1
- package/package.json +3 -3
package/dist/cli.cjs
CHANGED
|
@@ -36,6 +36,7 @@ __export(cli_exports, {
|
|
|
36
36
|
module.exports = __toCommonJS(cli_exports);
|
|
37
37
|
|
|
38
38
|
// src/commands.ts
|
|
39
|
+
var import_node_child_process2 = require("child_process");
|
|
39
40
|
var import_node_fs7 = __toESM(require("fs"), 1);
|
|
40
41
|
var import_node_path6 = __toESM(require("path"), 1);
|
|
41
42
|
|
|
@@ -273,12 +274,41 @@ var PROJECT_CONTEXT_GLOBS = [
|
|
|
273
274
|
".cursorrules",
|
|
274
275
|
".github/copilot-instructions.md",
|
|
275
276
|
".claude/settings.json",
|
|
277
|
+
// `.local.json` variants are gitignored by default, so nothing else carries
|
|
278
|
+
// them — which makes them exactly the kind of file this tool exists for.
|
|
279
|
+
".claude/settings.local.json",
|
|
276
280
|
".claude/memory/",
|
|
277
281
|
".claude/plans/",
|
|
278
282
|
".claude/commands/",
|
|
279
283
|
".claude/agents/",
|
|
280
284
|
".claude/skills/"
|
|
281
285
|
];
|
|
286
|
+
function planStem(fileName) {
|
|
287
|
+
return fileName.replace(/\.md$/, "").replace(/-agent-[0-9a-f]+$/i, "");
|
|
288
|
+
}
|
|
289
|
+
function collectPlans(plansDir, projectRoot, exclude) {
|
|
290
|
+
const all = walk(plansDir, { exclude });
|
|
291
|
+
if (all.length === 0) return [];
|
|
292
|
+
const projectName = import_node_path3.default.basename(projectRoot);
|
|
293
|
+
const related = /* @__PURE__ */ new Set();
|
|
294
|
+
const stems = /* @__PURE__ */ new Set();
|
|
295
|
+
for (const abs of all) {
|
|
296
|
+
let text = "";
|
|
297
|
+
try {
|
|
298
|
+
text = import_node_fs3.default.readFileSync(abs, "utf8");
|
|
299
|
+
} catch {
|
|
300
|
+
continue;
|
|
301
|
+
}
|
|
302
|
+
if (text.includes(projectRoot) || text.includes(projectName)) {
|
|
303
|
+
related.add(abs);
|
|
304
|
+
stems.add(planStem(import_node_path3.default.basename(abs)));
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
for (const abs of all) {
|
|
308
|
+
if (stems.has(planStem(import_node_path3.default.basename(abs)))) related.add(abs);
|
|
309
|
+
}
|
|
310
|
+
return [...related];
|
|
311
|
+
}
|
|
282
312
|
function push(out2, sourcePath, storePath, tier, sourceRoot, ctx) {
|
|
283
313
|
let size = 0;
|
|
284
314
|
try {
|
|
@@ -313,10 +343,24 @@ function collect(projectRoot, cfg, tiers) {
|
|
|
313
343
|
}
|
|
314
344
|
push(files, abs, `project/${rel}`, "core", "project", ctx);
|
|
315
345
|
}
|
|
316
|
-
const
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
346
|
+
const projectsDir = import_node_path3.default.join(userClaude, "projects");
|
|
347
|
+
let projectKeys = [];
|
|
348
|
+
try {
|
|
349
|
+
projectKeys = import_node_fs3.default.readdirSync(projectsDir, { withFileTypes: true }).filter((d) => d.isDirectory() && (d.name === key || d.name.startsWith(key + "-"))).map((d) => d.name);
|
|
350
|
+
} catch {
|
|
351
|
+
projectKeys = [];
|
|
352
|
+
}
|
|
353
|
+
for (const pk of projectKeys) {
|
|
354
|
+
const memoryDir = import_node_path3.default.join(projectsDir, pk, "memory");
|
|
355
|
+
for (const abs of walk(memoryDir, { exclude })) {
|
|
356
|
+
const rel = toPosix(import_node_path3.default.relative(memoryDir, abs));
|
|
357
|
+
const storePath = pk === key ? `memory/${rel}` : `memory-sub/${pk.slice(key.length + 1)}/${rel}`;
|
|
358
|
+
push(files, abs, storePath, "core", "user", ctx);
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
for (const abs of collectPlans(import_node_path3.default.join(userClaude, "plans"), projectRoot, exclude)) {
|
|
362
|
+
const rel = toPosix(import_node_path3.default.relative(import_node_path3.default.join(userClaude, "plans"), abs));
|
|
363
|
+
push(files, abs, `plans/${rel}`, "core", "user", ctx);
|
|
320
364
|
}
|
|
321
365
|
const skillsDir = import_node_path3.default.join(userClaude, "skills");
|
|
322
366
|
for (const abs of walk(skillsDir, { exclude })) {
|
|
@@ -352,6 +396,44 @@ function collect(projectRoot, cfg, tiers) {
|
|
|
352
396
|
}
|
|
353
397
|
return { files, skippedTracked, ctx };
|
|
354
398
|
}
|
|
399
|
+
function describeExcluded(projectRoot, tiers) {
|
|
400
|
+
const userClaude = userClaudeDir();
|
|
401
|
+
const key = cwdKey(projectRoot);
|
|
402
|
+
const out2 = [];
|
|
403
|
+
const measure = (dir, filter) => {
|
|
404
|
+
let files = 0;
|
|
405
|
+
let bytes = 0;
|
|
406
|
+
for (const abs of walk(dir, { exclude: [] })) {
|
|
407
|
+
if (filter && !filter(abs)) continue;
|
|
408
|
+
files++;
|
|
409
|
+
try {
|
|
410
|
+
bytes += import_node_fs3.default.statSync(abs).size;
|
|
411
|
+
} catch {
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
return { files, bytes };
|
|
415
|
+
};
|
|
416
|
+
if (!tiers.includes("transcripts")) {
|
|
417
|
+
const projDir = import_node_path3.default.join(userClaude, "projects", key);
|
|
418
|
+
const m = measure(projDir, (p) => !p.includes(`${import_node_path3.default.sep}memory${import_node_path3.default.sep}`));
|
|
419
|
+
if (m.files > 0) {
|
|
420
|
+
out2.push({
|
|
421
|
+
label: "session transcripts",
|
|
422
|
+
...m,
|
|
423
|
+
reason: "cloud-only \u2014 append-only logs this large would permanently bloat the repo"
|
|
424
|
+
});
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
for (const [dir, label, reason] of [
|
|
428
|
+
["uploads", "pasted files/images", "session-scoped binaries; regenerate rather than sync"],
|
|
429
|
+
["file-history", "edit-undo snapshots", "transient local undo state, not portable context"],
|
|
430
|
+
["tasks", "task outputs", "session-scoped tool output"]
|
|
431
|
+
]) {
|
|
432
|
+
const m = measure(import_node_path3.default.join(userClaude, dir));
|
|
433
|
+
if (m.files > 0) out2.push({ label, ...m, reason });
|
|
434
|
+
}
|
|
435
|
+
return out2.filter((g) => g.bytes > 0);
|
|
436
|
+
}
|
|
355
437
|
function summarize(files) {
|
|
356
438
|
const empty = { count: 0, bytes: 0 };
|
|
357
439
|
const out2 = {
|
|
@@ -367,6 +449,86 @@ function summarize(files) {
|
|
|
367
449
|
return out2;
|
|
368
450
|
}
|
|
369
451
|
|
|
452
|
+
// src/handoff.ts
|
|
453
|
+
function asStringArray(value) {
|
|
454
|
+
if (value === void 0 || value === null) return [];
|
|
455
|
+
if (!Array.isArray(value)) return null;
|
|
456
|
+
if (!value.every((v) => typeof v === "string")) return null;
|
|
457
|
+
return value;
|
|
458
|
+
}
|
|
459
|
+
function validateHandoff(raw) {
|
|
460
|
+
const errors = [];
|
|
461
|
+
if (typeof raw !== "object" || raw === null || Array.isArray(raw)) {
|
|
462
|
+
return { ok: false, errors: ["handoff must be a JSON object"] };
|
|
463
|
+
}
|
|
464
|
+
const o = raw;
|
|
465
|
+
const goal = typeof o.goal === "string" ? o.goal.trim() : "";
|
|
466
|
+
const nextStep = typeof o.nextStep === "string" ? o.nextStep.trim() : "";
|
|
467
|
+
if (!goal) errors.push("`goal` is required (what the session set out to do)");
|
|
468
|
+
if (!nextStep) errors.push("`nextStep` is required (the single next action)");
|
|
469
|
+
const decisions = asStringArray(o.decisions);
|
|
470
|
+
const openThreads = asStringArray(o.openThreads);
|
|
471
|
+
const filesTouched = asStringArray(o.filesTouched);
|
|
472
|
+
if (decisions === null) errors.push("`decisions` must be an array of strings");
|
|
473
|
+
if (openThreads === null) errors.push("`openThreads` must be an array of strings");
|
|
474
|
+
if (filesTouched === null) errors.push("`filesTouched` must be an array of strings");
|
|
475
|
+
let updatedAt = typeof o.updatedAt === "string" ? o.updatedAt : "";
|
|
476
|
+
if (!updatedAt || Number.isNaN(Date.parse(updatedAt))) updatedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
477
|
+
if (errors.length > 0) return { ok: false, errors };
|
|
478
|
+
return {
|
|
479
|
+
ok: true,
|
|
480
|
+
errors: [],
|
|
481
|
+
handoff: {
|
|
482
|
+
updatedAt,
|
|
483
|
+
goal,
|
|
484
|
+
decisions: decisions ?? [],
|
|
485
|
+
openThreads: openThreads ?? [],
|
|
486
|
+
filesTouched: filesTouched ?? [],
|
|
487
|
+
nextStep,
|
|
488
|
+
...typeof o.notes === "string" && o.notes.trim() ? { notes: o.notes.trim() } : {}
|
|
489
|
+
}
|
|
490
|
+
};
|
|
491
|
+
}
|
|
492
|
+
function ago(iso) {
|
|
493
|
+
const ms = Date.now() - Date.parse(iso);
|
|
494
|
+
if (Number.isNaN(ms)) return "unknown";
|
|
495
|
+
const mins = Math.floor(ms / 6e4);
|
|
496
|
+
if (mins < 1) return "just now";
|
|
497
|
+
if (mins < 60) return `${mins}m ago`;
|
|
498
|
+
const hours = Math.floor(mins / 60);
|
|
499
|
+
if (hours < 24) return `${hours}h ago`;
|
|
500
|
+
return `${Math.floor(hours / 24)}d ago`;
|
|
501
|
+
}
|
|
502
|
+
function formatHandoff(h) {
|
|
503
|
+
const lines = [`Where you left off (${ago(h.updatedAt)})`, "", ` Goal ${h.goal}`, ` Next step ${h.nextStep}`];
|
|
504
|
+
if (h.decisions.length > 0) {
|
|
505
|
+
lines.push("", " Decided:");
|
|
506
|
+
for (const d of h.decisions) lines.push(` \xB7 ${d}`);
|
|
507
|
+
}
|
|
508
|
+
if (h.openThreads.length > 0) {
|
|
509
|
+
lines.push("", " Still open:");
|
|
510
|
+
for (const t of h.openThreads) lines.push(` \xB7 ${t}`);
|
|
511
|
+
}
|
|
512
|
+
if (h.filesTouched.length > 0) {
|
|
513
|
+
const shown = h.filesTouched.slice(0, 12);
|
|
514
|
+
lines.push("", " Files touched:");
|
|
515
|
+
for (const f of shown) lines.push(` ${f}`);
|
|
516
|
+
if (h.filesTouched.length > shown.length) {
|
|
517
|
+
lines.push(` \u2026 and ${h.filesTouched.length - shown.length} more`);
|
|
518
|
+
}
|
|
519
|
+
}
|
|
520
|
+
if (h.notes) lines.push("", ` Notes: ${h.notes}`);
|
|
521
|
+
return lines;
|
|
522
|
+
}
|
|
523
|
+
function handoffAge(h) {
|
|
524
|
+
return ago(h.updatedAt);
|
|
525
|
+
}
|
|
526
|
+
function isStale(h, newestContentMs) {
|
|
527
|
+
const t = Date.parse(h.updatedAt);
|
|
528
|
+
if (Number.isNaN(t)) return true;
|
|
529
|
+
return newestContentMs - t > 60 * 60 * 1e3;
|
|
530
|
+
}
|
|
531
|
+
|
|
370
532
|
// src/scaffold.ts
|
|
371
533
|
var import_node_fs4 = __toESM(require("fs"), 1);
|
|
372
534
|
var import_node_path4 = __toESM(require("path"), 1);
|
|
@@ -735,7 +897,28 @@ function cmdPush(opts = {}) {
|
|
|
735
897
|
if (skippedTracked.length > 0) {
|
|
736
898
|
lines.push("", `${skippedTracked.length} project files skipped \u2014 git already tracks them.`);
|
|
737
899
|
}
|
|
900
|
+
const excluded = describeExcluded(root, tiers);
|
|
901
|
+
if (excluded.length > 0) {
|
|
902
|
+
lines.push("", "Not included (by design):");
|
|
903
|
+
for (const g of excluded) {
|
|
904
|
+
lines.push(` ${g.label.padEnd(22)} ${formatBytes(g.bytes).padStart(9)} ${g.reason}`);
|
|
905
|
+
}
|
|
906
|
+
}
|
|
738
907
|
if (!opts.dryRun) {
|
|
908
|
+
const store = new LocalStore(root);
|
|
909
|
+
const raw = store.readHandoff();
|
|
910
|
+
const v = raw ? validateHandoff(raw) : null;
|
|
911
|
+
if (!raw) {
|
|
912
|
+
lines.push(
|
|
913
|
+
"",
|
|
914
|
+
"No handoff written \u2014 the other machine will get your project knowledge but",
|
|
915
|
+
"not where you left off. Use `/context push` in Claude Code to include one."
|
|
916
|
+
);
|
|
917
|
+
} else if (v && !v.ok) {
|
|
918
|
+
lines.push("", "Handoff present but malformed (it will be ignored):", ...v.errors.map((e) => ` \xB7 ${e}`));
|
|
919
|
+
} else if (v?.ok && isStale(v.handoff, Date.now())) {
|
|
920
|
+
lines.push("", `Handoff is ${handoffAge(v.handoff)} \u2014 re-run \`/context push\` to refresh it.`);
|
|
921
|
+
}
|
|
739
922
|
lines.push("", "Commit .contextsync/ to carry this context with the repo.");
|
|
740
923
|
}
|
|
741
924
|
return ok(lines);
|
|
@@ -765,12 +948,108 @@ function cmdPull(opts = {}) {
|
|
|
765
948
|
"Re-run with --force to overwrite them."
|
|
766
949
|
);
|
|
767
950
|
}
|
|
768
|
-
const
|
|
769
|
-
if (
|
|
770
|
-
lines.push(
|
|
951
|
+
const raw = store.readHandoff();
|
|
952
|
+
if (!raw) {
|
|
953
|
+
lines.push(
|
|
954
|
+
"",
|
|
955
|
+
"No handoff in this store \u2014 you have the project knowledge, but not where the",
|
|
956
|
+
"last session stopped. Run `/context push` (not bare `ctx push`) on the other",
|
|
957
|
+
"machine: only the model can write the handoff, since only it has the conversation."
|
|
958
|
+
);
|
|
959
|
+
return ok(lines);
|
|
960
|
+
}
|
|
961
|
+
const check = validateHandoff(raw);
|
|
962
|
+
if (!check.ok) {
|
|
963
|
+
lines.push("", "A handoff exists but is malformed and was ignored:", ...check.errors.map((e) => ` \xB7 ${e}`));
|
|
964
|
+
return ok(lines);
|
|
771
965
|
}
|
|
966
|
+
lines.push("", ...formatHandoff(check.handoff));
|
|
772
967
|
return ok(lines);
|
|
773
968
|
}
|
|
969
|
+
function cmdHandoff(opts = {}) {
|
|
970
|
+
const found = requireProject();
|
|
971
|
+
if ("code" in found) return found;
|
|
972
|
+
const { root } = found;
|
|
973
|
+
const store = new LocalStore(root);
|
|
974
|
+
if (opts.set !== void 0) {
|
|
975
|
+
let parsed;
|
|
976
|
+
try {
|
|
977
|
+
parsed = JSON.parse(opts.set);
|
|
978
|
+
} catch (e) {
|
|
979
|
+
return fail([`Could not parse handoff JSON: ${e.message}`]);
|
|
980
|
+
}
|
|
981
|
+
const check2 = validateHandoff(parsed);
|
|
982
|
+
if (!check2.ok) return fail(["Handoff is not valid:", ...check2.errors.map((e) => ` \xB7 ${e}`)]);
|
|
983
|
+
store.writeHandoff(check2.handoff);
|
|
984
|
+
return ok([`Handoff saved (${import_node_path6.default.relative(root, storeDir(root))}/handoff.json).`, "", ...formatHandoff(check2.handoff)]);
|
|
985
|
+
}
|
|
986
|
+
const raw = store.readHandoff();
|
|
987
|
+
if (!raw) {
|
|
988
|
+
return ok([
|
|
989
|
+
"No handoff yet.",
|
|
990
|
+
"",
|
|
991
|
+
"Write one with `/context push` in Claude Code, or pipe JSON:",
|
|
992
|
+
` ctx handoff --set '{"goal":"\u2026","nextStep":"\u2026"}'`
|
|
993
|
+
]);
|
|
994
|
+
}
|
|
995
|
+
const check = validateHandoff(raw);
|
|
996
|
+
if (!check.ok) return fail(["Handoff is malformed:", ...check.errors.map((e) => ` \xB7 ${e}`)]);
|
|
997
|
+
return ok(formatHandoff(check.handoff));
|
|
998
|
+
}
|
|
999
|
+
function cmdDoctor() {
|
|
1000
|
+
const root = findProjectRoot();
|
|
1001
|
+
if (!root) return fail(["Not inside a project. Run `ctx init` first."]);
|
|
1002
|
+
const lines = [];
|
|
1003
|
+
let problems = 0;
|
|
1004
|
+
const check = (okFlag, label, detail) => {
|
|
1005
|
+
if (!okFlag) problems++;
|
|
1006
|
+
lines.push(` ${okFlag ? "\u2713" : "\u2717"} ${label.padEnd(28)} ${detail}`);
|
|
1007
|
+
};
|
|
1008
|
+
const cfg = loadConfig(root);
|
|
1009
|
+
check(Boolean(cfg), "store initialised", cfg ? `${import_node_path6.default.relative(root, storeDir(root))}/` : "missing \u2014 run `ctx init`");
|
|
1010
|
+
if (!cfg) return { code: 1, lines: ["Setup check", "", ...lines] };
|
|
1011
|
+
const store = new LocalStore(root);
|
|
1012
|
+
const manifest = store.readManifest();
|
|
1013
|
+
check(Boolean(manifest), "pushed at least once", manifest ? `${manifest.entries.length} files` : "never \u2014 run `ctx push`");
|
|
1014
|
+
check(
|
|
1015
|
+
import_node_fs7.default.existsSync(import_node_path6.default.join(root, SLASH_COMMAND_PATH)),
|
|
1016
|
+
"/context slash command",
|
|
1017
|
+
import_node_fs7.default.existsSync(import_node_path6.default.join(root, SLASH_COMMAND_PATH)) ? SLASH_COMMAND_PATH : `missing \u2014 run \`ctx init --force\``
|
|
1018
|
+
);
|
|
1019
|
+
const inGit = isGitRepo(root);
|
|
1020
|
+
check(inGit, "git repository", inGit ? "yes \u2014 store travels with the repo" : "no \u2014 the store will not sync anywhere");
|
|
1021
|
+
let storeIgnored = false;
|
|
1022
|
+
if (inGit) {
|
|
1023
|
+
try {
|
|
1024
|
+
(0, import_node_child_process2.execFileSync)("git", ["-C", root, "check-ignore", "-q", ".contextsync/config.json"], { stdio: "ignore" });
|
|
1025
|
+
storeIgnored = true;
|
|
1026
|
+
} catch {
|
|
1027
|
+
storeIgnored = false;
|
|
1028
|
+
}
|
|
1029
|
+
}
|
|
1030
|
+
check(!storeIgnored, "store is committable", storeIgnored ? "IGNORED by git \u2014 it will never reach another machine" : "not gitignored");
|
|
1031
|
+
const ctx = templateContext(root);
|
|
1032
|
+
const memEntry = manifest?.entries.find((e) => e.storePath.startsWith("memory/"));
|
|
1033
|
+
if (memEntry) {
|
|
1034
|
+
const dest = resolveTemplate(memEntry.restoreTemplate, ctx);
|
|
1035
|
+
const expectedDir = import_node_path6.default.join(userClaudeDir(), "projects", cwdKey(root), "memory");
|
|
1036
|
+
check(dest.startsWith(expectedDir), "memory restore path", dest.startsWith(expectedDir) ? expectedDir : `WRONG \u2192 ${dest}`);
|
|
1037
|
+
} else {
|
|
1038
|
+
check(false, "memory captured", "none found \u2014 is ~/.claude/projects/<key>/memory populated?");
|
|
1039
|
+
}
|
|
1040
|
+
const raw = store.readHandoff();
|
|
1041
|
+
if (!raw) {
|
|
1042
|
+
check(false, "handoff", "absent \u2014 `/context push` writes it; resume will have nothing to say");
|
|
1043
|
+
} else {
|
|
1044
|
+
const v = validateHandoff(raw);
|
|
1045
|
+
check(v.ok, "handoff", v.ok ? `valid, ${handoffAge(v.handoff)}` : v.errors[0]);
|
|
1046
|
+
if (v.ok && manifest && isStale(v.handoff, Date.parse(manifest.updatedAt))) {
|
|
1047
|
+
lines.push(" ! handoff is much older than the last push \u2014 re-run `/context push`");
|
|
1048
|
+
}
|
|
1049
|
+
}
|
|
1050
|
+
const header = problems === 0 ? "Setup check \u2014 all good" : `Setup check \u2014 ${problems} problem${problems === 1 ? "" : "s"}`;
|
|
1051
|
+
return { code: problems === 0 ? 0 : 1, lines: [header, "", ...lines] };
|
|
1052
|
+
}
|
|
774
1053
|
function cmdStatus() {
|
|
775
1054
|
const found = requireProject();
|
|
776
1055
|
if ("code" in found) return found;
|
|
@@ -779,10 +1058,14 @@ function cmdStatus() {
|
|
|
779
1058
|
const manifest = store.readManifest();
|
|
780
1059
|
const { tiers } = effectiveTiers(cfg);
|
|
781
1060
|
const { files, skippedTracked } = collect(root, cfg, tiers);
|
|
1061
|
+
const rawHandoff = store.readHandoff();
|
|
1062
|
+
const hv = rawHandoff ? validateHandoff(rawHandoff) : null;
|
|
1063
|
+
const handoffLabel = !rawHandoff ? "none \u2014 run `/context push` to record where you left off" : hv?.ok ? `${handoffAge(hv.handoff)}` : "malformed (will be ignored)";
|
|
782
1064
|
const lines = [
|
|
783
1065
|
`Project ${cfg.name}`,
|
|
784
1066
|
`Store ${import_node_path6.default.relative(root, storeDir(root))}/`,
|
|
785
1067
|
`Tiers ${cfg.tiers.join(", ")}`,
|
|
1068
|
+
`Handoff ${handoffLabel}`,
|
|
786
1069
|
`Remotes ${Object.keys(cfg.remotes).length > 0 ? Object.keys(cfg.remotes).join(", ") : "none (local only)"}`,
|
|
787
1070
|
""
|
|
788
1071
|
];
|
|
@@ -824,13 +1107,16 @@ function cmdStatus() {
|
|
|
824
1107
|
}
|
|
825
1108
|
|
|
826
1109
|
// src/cli.ts
|
|
1110
|
+
var VERSION = "0.2.0";
|
|
827
1111
|
var USAGE = `tricknowtech context-sync \u2014 carry a project's LLM context between machines
|
|
828
1112
|
|
|
829
1113
|
Usage
|
|
830
1114
|
ctx init [--artifacts] [--force] Create the store and install /context
|
|
831
1115
|
ctx push [--dry-run] Collect context into the store
|
|
832
|
-
ctx pull [--force] Restore context
|
|
833
|
-
ctx status
|
|
1116
|
+
ctx pull [--force] Restore context, and show where you left off
|
|
1117
|
+
ctx status What has changed since the last push
|
|
1118
|
+
ctx handoff [--set '<json>'] Show or write the session handoff
|
|
1119
|
+
ctx doctor Check the setup is actually wired correctly
|
|
834
1120
|
|
|
835
1121
|
Options
|
|
836
1122
|
--artifacts Include derived indexes (graphify-out/, etc.)
|
|
@@ -841,20 +1127,38 @@ Options
|
|
|
841
1127
|
-v, --version Show version
|
|
842
1128
|
|
|
843
1129
|
The store lives in .contextsync/ and is meant to be committed, so context
|
|
844
|
-
travels with the code. Session transcripts are excluded from local mode
|
|
1130
|
+
travels with the code. Session transcripts are excluded from local mode.
|
|
1131
|
+
|
|
1132
|
+
Prefer \`/context push\` inside Claude Code over bare \`ctx push\`: only the
|
|
1133
|
+
model can write the handoff that lets the next machine resume the work.`;
|
|
845
1134
|
function parseArgs(argv) {
|
|
846
1135
|
const flags = /* @__PURE__ */ new Set();
|
|
1136
|
+
const values = /* @__PURE__ */ new Map();
|
|
847
1137
|
let command = "";
|
|
848
|
-
for (
|
|
849
|
-
|
|
850
|
-
|
|
1138
|
+
for (let i = 0; i < argv.length; i++) {
|
|
1139
|
+
const arg = argv[i];
|
|
1140
|
+
if (arg.startsWith("-")) {
|
|
1141
|
+
const name = arg.replace(/^-+/, "");
|
|
1142
|
+
const eq = name.indexOf("=");
|
|
1143
|
+
if (eq !== -1) {
|
|
1144
|
+
values.set(name.slice(0, eq), name.slice(eq + 1));
|
|
1145
|
+
continue;
|
|
1146
|
+
}
|
|
1147
|
+
if (name === "set" && i + 1 < argv.length && !argv[i + 1].startsWith("-")) {
|
|
1148
|
+
values.set(name, argv[++i]);
|
|
1149
|
+
continue;
|
|
1150
|
+
}
|
|
1151
|
+
flags.add(name);
|
|
1152
|
+
} else if (!command) {
|
|
1153
|
+
command = arg;
|
|
1154
|
+
}
|
|
851
1155
|
}
|
|
852
|
-
return { command, flags };
|
|
1156
|
+
return { command, flags, values };
|
|
853
1157
|
}
|
|
854
1158
|
function run(argv) {
|
|
855
|
-
const { command, flags } = parseArgs(argv);
|
|
1159
|
+
const { command, flags, values } = parseArgs(argv);
|
|
856
1160
|
if (flags.has("h") || flags.has("help")) return { code: 0, lines: [USAGE] };
|
|
857
|
-
if (flags.has("v") || flags.has("version")) return { code: 0, lines: [
|
|
1161
|
+
if (flags.has("v") || flags.has("version")) return { code: 0, lines: [VERSION] };
|
|
858
1162
|
switch (command) {
|
|
859
1163
|
case "init":
|
|
860
1164
|
return cmdInit({ artifacts: flags.has("artifacts"), force: flags.has("force") });
|
|
@@ -862,6 +1166,10 @@ function run(argv) {
|
|
|
862
1166
|
return cmdPush({ allowSecrets: flags.has("allow-secrets"), dryRun: flags.has("dry-run") });
|
|
863
1167
|
case "pull":
|
|
864
1168
|
return cmdPull({ force: flags.has("force") });
|
|
1169
|
+
case "handoff":
|
|
1170
|
+
return cmdHandoff({ set: values.get("set") });
|
|
1171
|
+
case "doctor":
|
|
1172
|
+
return cmdDoctor();
|
|
865
1173
|
case "status":
|
|
866
1174
|
case "":
|
|
867
1175
|
return cmdStatus();
|