@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.js
CHANGED
|
@@ -3,6 +3,7 @@ import {
|
|
|
3
3
|
ALL_TIERS,
|
|
4
4
|
LOCAL_TIERS,
|
|
5
5
|
LocalStore,
|
|
6
|
+
SLASH_COMMAND_PATH,
|
|
6
7
|
collect,
|
|
7
8
|
configPath,
|
|
8
9
|
cwdKey,
|
|
@@ -11,18 +12,25 @@ import {
|
|
|
11
12
|
ensureGitignoreEntries,
|
|
12
13
|
findProjectRoot,
|
|
13
14
|
formatBytes,
|
|
15
|
+
formatHandoff,
|
|
14
16
|
formatHits,
|
|
17
|
+
handoffAge,
|
|
15
18
|
installSlashCommand,
|
|
16
19
|
isGitRepo,
|
|
20
|
+
isStale,
|
|
17
21
|
loadConfig,
|
|
22
|
+
resolveTemplate,
|
|
18
23
|
saveConfig,
|
|
19
24
|
scanFiles,
|
|
20
25
|
storeDir,
|
|
21
26
|
summarize,
|
|
22
|
-
|
|
23
|
-
|
|
27
|
+
templateContextFor,
|
|
28
|
+
userClaudeDir,
|
|
29
|
+
validateHandoff
|
|
30
|
+
} from "./chunk-BLEGQVXA.js";
|
|
24
31
|
|
|
25
32
|
// src/commands.ts
|
|
33
|
+
import { execFileSync } from "child_process";
|
|
26
34
|
import fs from "fs";
|
|
27
35
|
import path from "path";
|
|
28
36
|
function ok(lines) {
|
|
@@ -46,7 +54,7 @@ function requireProject() {
|
|
|
46
54
|
return { root, cfg };
|
|
47
55
|
}
|
|
48
56
|
function templateContext(root) {
|
|
49
|
-
return
|
|
57
|
+
return templateContextFor(root);
|
|
50
58
|
}
|
|
51
59
|
function effectiveTiers(cfg) {
|
|
52
60
|
const tiers = cfg.tiers.filter((t) => LOCAL_TIERS.includes(t));
|
|
@@ -82,7 +90,7 @@ function cmdPush(opts = {}) {
|
|
|
82
90
|
if ("code" in found) return found;
|
|
83
91
|
const { root, cfg } = found;
|
|
84
92
|
const { tiers, refused } = effectiveTiers(cfg);
|
|
85
|
-
const { files, skippedTracked } = collect(root, cfg, tiers);
|
|
93
|
+
const { files, skippedTracked, assistants } = collect(root, cfg, tiers);
|
|
86
94
|
const lines = [];
|
|
87
95
|
if (refused.length > 0) {
|
|
88
96
|
lines.push(
|
|
@@ -127,6 +135,9 @@ function cmdPush(opts = {}) {
|
|
|
127
135
|
const t = totals[tier];
|
|
128
136
|
if (t.count > 0) lines.push(` ${tier.padEnd(11)} ${String(t.count).padStart(4)} files ${formatBytes(t.bytes)}`);
|
|
129
137
|
}
|
|
138
|
+
if (assistants.length > 0) {
|
|
139
|
+
lines.push("", `Assistants: ${assistants.map((a) => a.name).join(", ")}`);
|
|
140
|
+
}
|
|
130
141
|
if (skippedTracked.length > 0) {
|
|
131
142
|
lines.push("", `${skippedTracked.length} project files skipped \u2014 git already tracks them.`);
|
|
132
143
|
}
|
|
@@ -138,6 +149,20 @@ function cmdPush(opts = {}) {
|
|
|
138
149
|
}
|
|
139
150
|
}
|
|
140
151
|
if (!opts.dryRun) {
|
|
152
|
+
const store = new LocalStore(root);
|
|
153
|
+
const raw = store.readHandoff();
|
|
154
|
+
const v = raw ? validateHandoff(raw) : null;
|
|
155
|
+
if (!raw) {
|
|
156
|
+
lines.push(
|
|
157
|
+
"",
|
|
158
|
+
"No handoff written \u2014 the other machine will get your project knowledge but",
|
|
159
|
+
"not where you left off. Use `/context push` in Claude Code to include one."
|
|
160
|
+
);
|
|
161
|
+
} else if (v && !v.ok) {
|
|
162
|
+
lines.push("", "Handoff present but malformed (it will be ignored):", ...v.errors.map((e) => ` \xB7 ${e}`));
|
|
163
|
+
} else if (v?.ok && isStale(v.handoff, Date.now())) {
|
|
164
|
+
lines.push("", `Handoff is ${handoffAge(v.handoff)} \u2014 re-run \`/context push\` to refresh it.`);
|
|
165
|
+
}
|
|
141
166
|
lines.push("", "Commit .contextsync/ to carry this context with the repo.");
|
|
142
167
|
}
|
|
143
168
|
return ok(lines);
|
|
@@ -167,12 +192,114 @@ function cmdPull(opts = {}) {
|
|
|
167
192
|
"Re-run with --force to overwrite them."
|
|
168
193
|
);
|
|
169
194
|
}
|
|
170
|
-
const
|
|
171
|
-
if (
|
|
172
|
-
lines.push(
|
|
195
|
+
const raw = store.readHandoff();
|
|
196
|
+
if (!raw) {
|
|
197
|
+
lines.push(
|
|
198
|
+
"",
|
|
199
|
+
"No handoff in this store \u2014 you have the project knowledge, but not where the",
|
|
200
|
+
"last session stopped. Run `/context push` (not bare `ctx push`) on the other",
|
|
201
|
+
"machine: only the model can write the handoff, since only it has the conversation."
|
|
202
|
+
);
|
|
203
|
+
return ok(lines);
|
|
204
|
+
}
|
|
205
|
+
const check = validateHandoff(raw);
|
|
206
|
+
if (!check.ok) {
|
|
207
|
+
lines.push("", "A handoff exists but is malformed and was ignored:", ...check.errors.map((e) => ` \xB7 ${e}`));
|
|
208
|
+
return ok(lines);
|
|
173
209
|
}
|
|
210
|
+
lines.push("", ...formatHandoff(check.handoff));
|
|
174
211
|
return ok(lines);
|
|
175
212
|
}
|
|
213
|
+
function cmdHandoff(opts = {}) {
|
|
214
|
+
const found = requireProject();
|
|
215
|
+
if ("code" in found) return found;
|
|
216
|
+
const { root } = found;
|
|
217
|
+
const store = new LocalStore(root);
|
|
218
|
+
if (opts.set !== void 0) {
|
|
219
|
+
let parsed;
|
|
220
|
+
try {
|
|
221
|
+
parsed = JSON.parse(opts.set);
|
|
222
|
+
} catch (e) {
|
|
223
|
+
return fail([`Could not parse handoff JSON: ${e.message}`]);
|
|
224
|
+
}
|
|
225
|
+
const check2 = validateHandoff(parsed);
|
|
226
|
+
if (!check2.ok) return fail(["Handoff is not valid:", ...check2.errors.map((e) => ` \xB7 ${e}`)]);
|
|
227
|
+
store.writeHandoff(check2.handoff);
|
|
228
|
+
return ok([`Handoff saved (${path.relative(root, storeDir(root))}/handoff.json).`, "", ...formatHandoff(check2.handoff)]);
|
|
229
|
+
}
|
|
230
|
+
const raw = store.readHandoff();
|
|
231
|
+
if (!raw) {
|
|
232
|
+
return ok([
|
|
233
|
+
"No handoff yet.",
|
|
234
|
+
"",
|
|
235
|
+
"Write one with `/context push` in Claude Code, or pipe JSON:",
|
|
236
|
+
` ctx handoff --set '{"goal":"\u2026","nextStep":"\u2026"}'`
|
|
237
|
+
]);
|
|
238
|
+
}
|
|
239
|
+
const check = validateHandoff(raw);
|
|
240
|
+
if (!check.ok) return fail(["Handoff is malformed:", ...check.errors.map((e) => ` \xB7 ${e}`)]);
|
|
241
|
+
return ok(formatHandoff(check.handoff));
|
|
242
|
+
}
|
|
243
|
+
function cmdDoctor() {
|
|
244
|
+
const root = findProjectRoot();
|
|
245
|
+
if (!root) return fail(["Not inside a project. Run `ctx init` first."]);
|
|
246
|
+
const lines = [];
|
|
247
|
+
let problems = 0;
|
|
248
|
+
const check = (okFlag, label, detail) => {
|
|
249
|
+
if (!okFlag) problems++;
|
|
250
|
+
lines.push(` ${okFlag ? "\u2713" : "\u2717"} ${label.padEnd(28)} ${detail}`);
|
|
251
|
+
};
|
|
252
|
+
const cfg = loadConfig(root);
|
|
253
|
+
check(Boolean(cfg), "store initialised", cfg ? `${path.relative(root, storeDir(root))}/` : "missing \u2014 run `ctx init`");
|
|
254
|
+
if (!cfg) return { code: 1, lines: ["Setup check", "", ...lines] };
|
|
255
|
+
const store = new LocalStore(root);
|
|
256
|
+
const manifest = store.readManifest();
|
|
257
|
+
check(Boolean(manifest), "pushed at least once", manifest ? `${manifest.entries.length} files` : "never \u2014 run `ctx push`");
|
|
258
|
+
const detected = collect(root, cfg, ["core"]).assistants;
|
|
259
|
+
check(
|
|
260
|
+
detected.length > 0,
|
|
261
|
+
"assistants detected",
|
|
262
|
+
detected.length > 0 ? detected.map((a) => a.name).join(", ") : "none \u2014 no known assistant config found"
|
|
263
|
+
);
|
|
264
|
+
check(
|
|
265
|
+
fs.existsSync(path.join(root, SLASH_COMMAND_PATH)),
|
|
266
|
+
"/context slash command",
|
|
267
|
+
fs.existsSync(path.join(root, SLASH_COMMAND_PATH)) ? SLASH_COMMAND_PATH : `missing \u2014 run \`ctx init --force\``
|
|
268
|
+
);
|
|
269
|
+
const inGit = isGitRepo(root);
|
|
270
|
+
check(inGit, "git repository", inGit ? "yes \u2014 store travels with the repo" : "no \u2014 the store will not sync anywhere");
|
|
271
|
+
let storeIgnored = false;
|
|
272
|
+
if (inGit) {
|
|
273
|
+
try {
|
|
274
|
+
execFileSync("git", ["-C", root, "check-ignore", "-q", ".contextsync/config.json"], { stdio: "ignore" });
|
|
275
|
+
storeIgnored = true;
|
|
276
|
+
} catch {
|
|
277
|
+
storeIgnored = false;
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
check(!storeIgnored, "store is committable", storeIgnored ? "IGNORED by git \u2014 it will never reach another machine" : "not gitignored");
|
|
281
|
+
const ctx = templateContext(root);
|
|
282
|
+
const memEntry = manifest?.entries.find((e) => e.storePath.startsWith("memory/"));
|
|
283
|
+
if (memEntry) {
|
|
284
|
+
const dest = resolveTemplate(memEntry.restoreTemplate, ctx);
|
|
285
|
+
const expectedDir = path.join(userClaudeDir(), "projects", cwdKey(root), "memory");
|
|
286
|
+
check(dest.startsWith(expectedDir), "memory restore path", dest.startsWith(expectedDir) ? expectedDir : `WRONG \u2192 ${dest}`);
|
|
287
|
+
} else {
|
|
288
|
+
check(false, "memory captured", "none found \u2014 is ~/.claude/projects/<key>/memory populated?");
|
|
289
|
+
}
|
|
290
|
+
const raw = store.readHandoff();
|
|
291
|
+
if (!raw) {
|
|
292
|
+
check(false, "handoff", "absent \u2014 `/context push` writes it; resume will have nothing to say");
|
|
293
|
+
} else {
|
|
294
|
+
const v = validateHandoff(raw);
|
|
295
|
+
check(v.ok, "handoff", v.ok ? `valid, ${handoffAge(v.handoff)}` : v.errors[0]);
|
|
296
|
+
if (v.ok && manifest && isStale(v.handoff, Date.parse(manifest.updatedAt))) {
|
|
297
|
+
lines.push(" ! handoff is much older than the last push \u2014 re-run `/context push`");
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
const header = problems === 0 ? "Setup check \u2014 all good" : `Setup check \u2014 ${problems} problem${problems === 1 ? "" : "s"}`;
|
|
301
|
+
return { code: problems === 0 ? 0 : 1, lines: [header, "", ...lines] };
|
|
302
|
+
}
|
|
176
303
|
function cmdStatus() {
|
|
177
304
|
const found = requireProject();
|
|
178
305
|
if ("code" in found) return found;
|
|
@@ -180,11 +307,16 @@ function cmdStatus() {
|
|
|
180
307
|
const store = new LocalStore(root);
|
|
181
308
|
const manifest = store.readManifest();
|
|
182
309
|
const { tiers } = effectiveTiers(cfg);
|
|
183
|
-
const { files, skippedTracked } = collect(root, cfg, tiers);
|
|
310
|
+
const { files, skippedTracked, assistants } = collect(root, cfg, tiers);
|
|
311
|
+
const rawHandoff = store.readHandoff();
|
|
312
|
+
const hv = rawHandoff ? validateHandoff(rawHandoff) : null;
|
|
313
|
+
const handoffLabel = !rawHandoff ? "none \u2014 run `/context push` to record where you left off" : hv?.ok ? `${handoffAge(hv.handoff)}` : "malformed (will be ignored)";
|
|
184
314
|
const lines = [
|
|
185
315
|
`Project ${cfg.name}`,
|
|
186
316
|
`Store ${path.relative(root, storeDir(root))}/`,
|
|
187
317
|
`Tiers ${cfg.tiers.join(", ")}`,
|
|
318
|
+
`Handoff ${handoffLabel}`,
|
|
319
|
+
`Tools ${assistants.length > 0 ? assistants.map((a) => a.name).join(", ") : "none detected"}`,
|
|
188
320
|
`Remotes ${Object.keys(cfg.remotes).length > 0 ? Object.keys(cfg.remotes).join(", ") : "none (local only)"}`,
|
|
189
321
|
""
|
|
190
322
|
];
|
|
@@ -226,13 +358,16 @@ function cmdStatus() {
|
|
|
226
358
|
}
|
|
227
359
|
|
|
228
360
|
// src/cli.ts
|
|
361
|
+
var VERSION = "0.3.0";
|
|
229
362
|
var USAGE = `tricknowtech context-sync \u2014 carry a project's LLM context between machines
|
|
230
363
|
|
|
231
364
|
Usage
|
|
232
365
|
ctx init [--artifacts] [--force] Create the store and install /context
|
|
233
366
|
ctx push [--dry-run] Collect context into the store
|
|
234
|
-
ctx pull [--force] Restore context
|
|
235
|
-
ctx status
|
|
367
|
+
ctx pull [--force] Restore context, and show where you left off
|
|
368
|
+
ctx status What has changed since the last push
|
|
369
|
+
ctx handoff [--set '<json>'] Show or write the session handoff
|
|
370
|
+
ctx doctor Check the setup is actually wired correctly
|
|
236
371
|
|
|
237
372
|
Options
|
|
238
373
|
--artifacts Include derived indexes (graphify-out/, etc.)
|
|
@@ -243,20 +378,38 @@ Options
|
|
|
243
378
|
-v, --version Show version
|
|
244
379
|
|
|
245
380
|
The store lives in .contextsync/ and is meant to be committed, so context
|
|
246
|
-
travels with the code. Session transcripts are excluded from local mode
|
|
381
|
+
travels with the code. Session transcripts are excluded from local mode.
|
|
382
|
+
|
|
383
|
+
Prefer \`/context push\` inside Claude Code over bare \`ctx push\`: only the
|
|
384
|
+
model can write the handoff that lets the next machine resume the work.`;
|
|
247
385
|
function parseArgs(argv) {
|
|
248
386
|
const flags = /* @__PURE__ */ new Set();
|
|
387
|
+
const values = /* @__PURE__ */ new Map();
|
|
249
388
|
let command = "";
|
|
250
|
-
for (
|
|
251
|
-
|
|
252
|
-
|
|
389
|
+
for (let i = 0; i < argv.length; i++) {
|
|
390
|
+
const arg = argv[i];
|
|
391
|
+
if (arg.startsWith("-")) {
|
|
392
|
+
const name = arg.replace(/^-+/, "");
|
|
393
|
+
const eq = name.indexOf("=");
|
|
394
|
+
if (eq !== -1) {
|
|
395
|
+
values.set(name.slice(0, eq), name.slice(eq + 1));
|
|
396
|
+
continue;
|
|
397
|
+
}
|
|
398
|
+
if (name === "set" && i + 1 < argv.length && !argv[i + 1].startsWith("-")) {
|
|
399
|
+
values.set(name, argv[++i]);
|
|
400
|
+
continue;
|
|
401
|
+
}
|
|
402
|
+
flags.add(name);
|
|
403
|
+
} else if (!command) {
|
|
404
|
+
command = arg;
|
|
405
|
+
}
|
|
253
406
|
}
|
|
254
|
-
return { command, flags };
|
|
407
|
+
return { command, flags, values };
|
|
255
408
|
}
|
|
256
409
|
function run(argv) {
|
|
257
|
-
const { command, flags } = parseArgs(argv);
|
|
410
|
+
const { command, flags, values } = parseArgs(argv);
|
|
258
411
|
if (flags.has("h") || flags.has("help")) return { code: 0, lines: [USAGE] };
|
|
259
|
-
if (flags.has("v") || flags.has("version")) return { code: 0, lines: [
|
|
412
|
+
if (flags.has("v") || flags.has("version")) return { code: 0, lines: [VERSION] };
|
|
260
413
|
switch (command) {
|
|
261
414
|
case "init":
|
|
262
415
|
return cmdInit({ artifacts: flags.has("artifacts"), force: flags.has("force") });
|
|
@@ -264,6 +417,10 @@ function run(argv) {
|
|
|
264
417
|
return cmdPush({ allowSecrets: flags.has("allow-secrets"), dryRun: flags.has("dry-run") });
|
|
265
418
|
case "pull":
|
|
266
419
|
return cmdPull({ force: flags.has("force") });
|
|
420
|
+
case "handoff":
|
|
421
|
+
return cmdHandoff({ set: values.get("set") });
|
|
422
|
+
case "doctor":
|
|
423
|
+
return cmdDoctor();
|
|
267
424
|
case "status":
|
|
268
425
|
case "":
|
|
269
426
|
return cmdStatus();
|