@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.js
CHANGED
|
@@ -3,25 +3,33 @@ 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,
|
|
9
10
|
defaultConfig,
|
|
11
|
+
describeExcluded,
|
|
10
12
|
ensureGitignoreEntries,
|
|
11
13
|
findProjectRoot,
|
|
12
14
|
formatBytes,
|
|
15
|
+
formatHandoff,
|
|
13
16
|
formatHits,
|
|
17
|
+
handoffAge,
|
|
14
18
|
installSlashCommand,
|
|
15
19
|
isGitRepo,
|
|
20
|
+
isStale,
|
|
16
21
|
loadConfig,
|
|
22
|
+
resolveTemplate,
|
|
17
23
|
saveConfig,
|
|
18
24
|
scanFiles,
|
|
19
25
|
storeDir,
|
|
20
26
|
summarize,
|
|
21
|
-
userClaudeDir
|
|
22
|
-
|
|
27
|
+
userClaudeDir,
|
|
28
|
+
validateHandoff
|
|
29
|
+
} from "./chunk-6GB2SN2L.js";
|
|
23
30
|
|
|
24
31
|
// src/commands.ts
|
|
32
|
+
import { execFileSync } from "child_process";
|
|
25
33
|
import fs from "fs";
|
|
26
34
|
import path from "path";
|
|
27
35
|
function ok(lines) {
|
|
@@ -129,7 +137,28 @@ function cmdPush(opts = {}) {
|
|
|
129
137
|
if (skippedTracked.length > 0) {
|
|
130
138
|
lines.push("", `${skippedTracked.length} project files skipped \u2014 git already tracks them.`);
|
|
131
139
|
}
|
|
140
|
+
const excluded = describeExcluded(root, tiers);
|
|
141
|
+
if (excluded.length > 0) {
|
|
142
|
+
lines.push("", "Not included (by design):");
|
|
143
|
+
for (const g of excluded) {
|
|
144
|
+
lines.push(` ${g.label.padEnd(22)} ${formatBytes(g.bytes).padStart(9)} ${g.reason}`);
|
|
145
|
+
}
|
|
146
|
+
}
|
|
132
147
|
if (!opts.dryRun) {
|
|
148
|
+
const store = new LocalStore(root);
|
|
149
|
+
const raw = store.readHandoff();
|
|
150
|
+
const v = raw ? validateHandoff(raw) : null;
|
|
151
|
+
if (!raw) {
|
|
152
|
+
lines.push(
|
|
153
|
+
"",
|
|
154
|
+
"No handoff written \u2014 the other machine will get your project knowledge but",
|
|
155
|
+
"not where you left off. Use `/context push` in Claude Code to include one."
|
|
156
|
+
);
|
|
157
|
+
} else if (v && !v.ok) {
|
|
158
|
+
lines.push("", "Handoff present but malformed (it will be ignored):", ...v.errors.map((e) => ` \xB7 ${e}`));
|
|
159
|
+
} else if (v?.ok && isStale(v.handoff, Date.now())) {
|
|
160
|
+
lines.push("", `Handoff is ${handoffAge(v.handoff)} \u2014 re-run \`/context push\` to refresh it.`);
|
|
161
|
+
}
|
|
133
162
|
lines.push("", "Commit .contextsync/ to carry this context with the repo.");
|
|
134
163
|
}
|
|
135
164
|
return ok(lines);
|
|
@@ -159,12 +188,108 @@ function cmdPull(opts = {}) {
|
|
|
159
188
|
"Re-run with --force to overwrite them."
|
|
160
189
|
);
|
|
161
190
|
}
|
|
162
|
-
const
|
|
163
|
-
if (
|
|
164
|
-
lines.push(
|
|
191
|
+
const raw = store.readHandoff();
|
|
192
|
+
if (!raw) {
|
|
193
|
+
lines.push(
|
|
194
|
+
"",
|
|
195
|
+
"No handoff in this store \u2014 you have the project knowledge, but not where the",
|
|
196
|
+
"last session stopped. Run `/context push` (not bare `ctx push`) on the other",
|
|
197
|
+
"machine: only the model can write the handoff, since only it has the conversation."
|
|
198
|
+
);
|
|
199
|
+
return ok(lines);
|
|
200
|
+
}
|
|
201
|
+
const check = validateHandoff(raw);
|
|
202
|
+
if (!check.ok) {
|
|
203
|
+
lines.push("", "A handoff exists but is malformed and was ignored:", ...check.errors.map((e) => ` \xB7 ${e}`));
|
|
204
|
+
return ok(lines);
|
|
165
205
|
}
|
|
206
|
+
lines.push("", ...formatHandoff(check.handoff));
|
|
166
207
|
return ok(lines);
|
|
167
208
|
}
|
|
209
|
+
function cmdHandoff(opts = {}) {
|
|
210
|
+
const found = requireProject();
|
|
211
|
+
if ("code" in found) return found;
|
|
212
|
+
const { root } = found;
|
|
213
|
+
const store = new LocalStore(root);
|
|
214
|
+
if (opts.set !== void 0) {
|
|
215
|
+
let parsed;
|
|
216
|
+
try {
|
|
217
|
+
parsed = JSON.parse(opts.set);
|
|
218
|
+
} catch (e) {
|
|
219
|
+
return fail([`Could not parse handoff JSON: ${e.message}`]);
|
|
220
|
+
}
|
|
221
|
+
const check2 = validateHandoff(parsed);
|
|
222
|
+
if (!check2.ok) return fail(["Handoff is not valid:", ...check2.errors.map((e) => ` \xB7 ${e}`)]);
|
|
223
|
+
store.writeHandoff(check2.handoff);
|
|
224
|
+
return ok([`Handoff saved (${path.relative(root, storeDir(root))}/handoff.json).`, "", ...formatHandoff(check2.handoff)]);
|
|
225
|
+
}
|
|
226
|
+
const raw = store.readHandoff();
|
|
227
|
+
if (!raw) {
|
|
228
|
+
return ok([
|
|
229
|
+
"No handoff yet.",
|
|
230
|
+
"",
|
|
231
|
+
"Write one with `/context push` in Claude Code, or pipe JSON:",
|
|
232
|
+
` ctx handoff --set '{"goal":"\u2026","nextStep":"\u2026"}'`
|
|
233
|
+
]);
|
|
234
|
+
}
|
|
235
|
+
const check = validateHandoff(raw);
|
|
236
|
+
if (!check.ok) return fail(["Handoff is malformed:", ...check.errors.map((e) => ` \xB7 ${e}`)]);
|
|
237
|
+
return ok(formatHandoff(check.handoff));
|
|
238
|
+
}
|
|
239
|
+
function cmdDoctor() {
|
|
240
|
+
const root = findProjectRoot();
|
|
241
|
+
if (!root) return fail(["Not inside a project. Run `ctx init` first."]);
|
|
242
|
+
const lines = [];
|
|
243
|
+
let problems = 0;
|
|
244
|
+
const check = (okFlag, label, detail) => {
|
|
245
|
+
if (!okFlag) problems++;
|
|
246
|
+
lines.push(` ${okFlag ? "\u2713" : "\u2717"} ${label.padEnd(28)} ${detail}`);
|
|
247
|
+
};
|
|
248
|
+
const cfg = loadConfig(root);
|
|
249
|
+
check(Boolean(cfg), "store initialised", cfg ? `${path.relative(root, storeDir(root))}/` : "missing \u2014 run `ctx init`");
|
|
250
|
+
if (!cfg) return { code: 1, lines: ["Setup check", "", ...lines] };
|
|
251
|
+
const store = new LocalStore(root);
|
|
252
|
+
const manifest = store.readManifest();
|
|
253
|
+
check(Boolean(manifest), "pushed at least once", manifest ? `${manifest.entries.length} files` : "never \u2014 run `ctx push`");
|
|
254
|
+
check(
|
|
255
|
+
fs.existsSync(path.join(root, SLASH_COMMAND_PATH)),
|
|
256
|
+
"/context slash command",
|
|
257
|
+
fs.existsSync(path.join(root, SLASH_COMMAND_PATH)) ? SLASH_COMMAND_PATH : `missing \u2014 run \`ctx init --force\``
|
|
258
|
+
);
|
|
259
|
+
const inGit = isGitRepo(root);
|
|
260
|
+
check(inGit, "git repository", inGit ? "yes \u2014 store travels with the repo" : "no \u2014 the store will not sync anywhere");
|
|
261
|
+
let storeIgnored = false;
|
|
262
|
+
if (inGit) {
|
|
263
|
+
try {
|
|
264
|
+
execFileSync("git", ["-C", root, "check-ignore", "-q", ".contextsync/config.json"], { stdio: "ignore" });
|
|
265
|
+
storeIgnored = true;
|
|
266
|
+
} catch {
|
|
267
|
+
storeIgnored = false;
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
check(!storeIgnored, "store is committable", storeIgnored ? "IGNORED by git \u2014 it will never reach another machine" : "not gitignored");
|
|
271
|
+
const ctx = templateContext(root);
|
|
272
|
+
const memEntry = manifest?.entries.find((e) => e.storePath.startsWith("memory/"));
|
|
273
|
+
if (memEntry) {
|
|
274
|
+
const dest = resolveTemplate(memEntry.restoreTemplate, ctx);
|
|
275
|
+
const expectedDir = path.join(userClaudeDir(), "projects", cwdKey(root), "memory");
|
|
276
|
+
check(dest.startsWith(expectedDir), "memory restore path", dest.startsWith(expectedDir) ? expectedDir : `WRONG \u2192 ${dest}`);
|
|
277
|
+
} else {
|
|
278
|
+
check(false, "memory captured", "none found \u2014 is ~/.claude/projects/<key>/memory populated?");
|
|
279
|
+
}
|
|
280
|
+
const raw = store.readHandoff();
|
|
281
|
+
if (!raw) {
|
|
282
|
+
check(false, "handoff", "absent \u2014 `/context push` writes it; resume will have nothing to say");
|
|
283
|
+
} else {
|
|
284
|
+
const v = validateHandoff(raw);
|
|
285
|
+
check(v.ok, "handoff", v.ok ? `valid, ${handoffAge(v.handoff)}` : v.errors[0]);
|
|
286
|
+
if (v.ok && manifest && isStale(v.handoff, Date.parse(manifest.updatedAt))) {
|
|
287
|
+
lines.push(" ! handoff is much older than the last push \u2014 re-run `/context push`");
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
const header = problems === 0 ? "Setup check \u2014 all good" : `Setup check \u2014 ${problems} problem${problems === 1 ? "" : "s"}`;
|
|
291
|
+
return { code: problems === 0 ? 0 : 1, lines: [header, "", ...lines] };
|
|
292
|
+
}
|
|
168
293
|
function cmdStatus() {
|
|
169
294
|
const found = requireProject();
|
|
170
295
|
if ("code" in found) return found;
|
|
@@ -173,10 +298,14 @@ function cmdStatus() {
|
|
|
173
298
|
const manifest = store.readManifest();
|
|
174
299
|
const { tiers } = effectiveTiers(cfg);
|
|
175
300
|
const { files, skippedTracked } = collect(root, cfg, tiers);
|
|
301
|
+
const rawHandoff = store.readHandoff();
|
|
302
|
+
const hv = rawHandoff ? validateHandoff(rawHandoff) : null;
|
|
303
|
+
const handoffLabel = !rawHandoff ? "none \u2014 run `/context push` to record where you left off" : hv?.ok ? `${handoffAge(hv.handoff)}` : "malformed (will be ignored)";
|
|
176
304
|
const lines = [
|
|
177
305
|
`Project ${cfg.name}`,
|
|
178
306
|
`Store ${path.relative(root, storeDir(root))}/`,
|
|
179
307
|
`Tiers ${cfg.tiers.join(", ")}`,
|
|
308
|
+
`Handoff ${handoffLabel}`,
|
|
180
309
|
`Remotes ${Object.keys(cfg.remotes).length > 0 ? Object.keys(cfg.remotes).join(", ") : "none (local only)"}`,
|
|
181
310
|
""
|
|
182
311
|
];
|
|
@@ -218,13 +347,16 @@ function cmdStatus() {
|
|
|
218
347
|
}
|
|
219
348
|
|
|
220
349
|
// src/cli.ts
|
|
350
|
+
var VERSION = "0.2.0";
|
|
221
351
|
var USAGE = `tricknowtech context-sync \u2014 carry a project's LLM context between machines
|
|
222
352
|
|
|
223
353
|
Usage
|
|
224
354
|
ctx init [--artifacts] [--force] Create the store and install /context
|
|
225
355
|
ctx push [--dry-run] Collect context into the store
|
|
226
|
-
ctx pull [--force] Restore context
|
|
227
|
-
ctx status
|
|
356
|
+
ctx pull [--force] Restore context, and show where you left off
|
|
357
|
+
ctx status What has changed since the last push
|
|
358
|
+
ctx handoff [--set '<json>'] Show or write the session handoff
|
|
359
|
+
ctx doctor Check the setup is actually wired correctly
|
|
228
360
|
|
|
229
361
|
Options
|
|
230
362
|
--artifacts Include derived indexes (graphify-out/, etc.)
|
|
@@ -235,20 +367,38 @@ Options
|
|
|
235
367
|
-v, --version Show version
|
|
236
368
|
|
|
237
369
|
The store lives in .contextsync/ and is meant to be committed, so context
|
|
238
|
-
travels with the code. Session transcripts are excluded from local mode
|
|
370
|
+
travels with the code. Session transcripts are excluded from local mode.
|
|
371
|
+
|
|
372
|
+
Prefer \`/context push\` inside Claude Code over bare \`ctx push\`: only the
|
|
373
|
+
model can write the handoff that lets the next machine resume the work.`;
|
|
239
374
|
function parseArgs(argv) {
|
|
240
375
|
const flags = /* @__PURE__ */ new Set();
|
|
376
|
+
const values = /* @__PURE__ */ new Map();
|
|
241
377
|
let command = "";
|
|
242
|
-
for (
|
|
243
|
-
|
|
244
|
-
|
|
378
|
+
for (let i = 0; i < argv.length; i++) {
|
|
379
|
+
const arg = argv[i];
|
|
380
|
+
if (arg.startsWith("-")) {
|
|
381
|
+
const name = arg.replace(/^-+/, "");
|
|
382
|
+
const eq = name.indexOf("=");
|
|
383
|
+
if (eq !== -1) {
|
|
384
|
+
values.set(name.slice(0, eq), name.slice(eq + 1));
|
|
385
|
+
continue;
|
|
386
|
+
}
|
|
387
|
+
if (name === "set" && i + 1 < argv.length && !argv[i + 1].startsWith("-")) {
|
|
388
|
+
values.set(name, argv[++i]);
|
|
389
|
+
continue;
|
|
390
|
+
}
|
|
391
|
+
flags.add(name);
|
|
392
|
+
} else if (!command) {
|
|
393
|
+
command = arg;
|
|
394
|
+
}
|
|
245
395
|
}
|
|
246
|
-
return { command, flags };
|
|
396
|
+
return { command, flags, values };
|
|
247
397
|
}
|
|
248
398
|
function run(argv) {
|
|
249
|
-
const { command, flags } = parseArgs(argv);
|
|
399
|
+
const { command, flags, values } = parseArgs(argv);
|
|
250
400
|
if (flags.has("h") || flags.has("help")) return { code: 0, lines: [USAGE] };
|
|
251
|
-
if (flags.has("v") || flags.has("version")) return { code: 0, lines: [
|
|
401
|
+
if (flags.has("v") || flags.has("version")) return { code: 0, lines: [VERSION] };
|
|
252
402
|
switch (command) {
|
|
253
403
|
case "init":
|
|
254
404
|
return cmdInit({ artifacts: flags.has("artifacts"), force: flags.has("force") });
|
|
@@ -256,6 +406,10 @@ function run(argv) {
|
|
|
256
406
|
return cmdPush({ allowSecrets: flags.has("allow-secrets"), dryRun: flags.has("dry-run") });
|
|
257
407
|
case "pull":
|
|
258
408
|
return cmdPull({ force: flags.has("force") });
|
|
409
|
+
case "handoff":
|
|
410
|
+
return cmdHandoff({ set: values.get("set") });
|
|
411
|
+
case "doctor":
|
|
412
|
+
return cmdDoctor();
|
|
259
413
|
case "status":
|
|
260
414
|
case "":
|
|
261
415
|
return cmdStatus();
|
package/dist/index.cjs
CHANGED
|
@@ -45,13 +45,17 @@ __export(index_exports, {
|
|
|
45
45
|
configPath: () => configPath,
|
|
46
46
|
cwdKey: () => cwdKey,
|
|
47
47
|
defaultConfig: () => defaultConfig,
|
|
48
|
+
describeExcluded: () => describeExcluded,
|
|
48
49
|
ensureGitignoreEntries: () => ensureGitignoreEntries,
|
|
49
50
|
findProjectRoot: () => findProjectRoot,
|
|
50
51
|
formatBytes: () => formatBytes,
|
|
52
|
+
formatHandoff: () => formatHandoff,
|
|
51
53
|
formatHits: () => formatHits,
|
|
52
54
|
gitTrackedSet: () => gitTrackedSet,
|
|
55
|
+
handoffAge: () => handoffAge,
|
|
53
56
|
installSlashCommand: () => installSlashCommand,
|
|
54
57
|
isGitRepo: () => isGitRepo,
|
|
58
|
+
isStale: () => isStale,
|
|
55
59
|
loadConfig: () => loadConfig,
|
|
56
60
|
makeTemplate: () => makeTemplate,
|
|
57
61
|
matchesAny: () => matchesAny,
|
|
@@ -61,6 +65,7 @@ __export(index_exports, {
|
|
|
61
65
|
storeDir: () => storeDir,
|
|
62
66
|
summarize: () => summarize,
|
|
63
67
|
userClaudeDir: () => userClaudeDir,
|
|
68
|
+
validateHandoff: () => validateHandoff,
|
|
64
69
|
walk: () => walk
|
|
65
70
|
});
|
|
66
71
|
module.exports = __toCommonJS(index_exports);
|
|
@@ -299,12 +304,41 @@ var PROJECT_CONTEXT_GLOBS = [
|
|
|
299
304
|
".cursorrules",
|
|
300
305
|
".github/copilot-instructions.md",
|
|
301
306
|
".claude/settings.json",
|
|
307
|
+
// `.local.json` variants are gitignored by default, so nothing else carries
|
|
308
|
+
// them — which makes them exactly the kind of file this tool exists for.
|
|
309
|
+
".claude/settings.local.json",
|
|
302
310
|
".claude/memory/",
|
|
303
311
|
".claude/plans/",
|
|
304
312
|
".claude/commands/",
|
|
305
313
|
".claude/agents/",
|
|
306
314
|
".claude/skills/"
|
|
307
315
|
];
|
|
316
|
+
function planStem(fileName) {
|
|
317
|
+
return fileName.replace(/\.md$/, "").replace(/-agent-[0-9a-f]+$/i, "");
|
|
318
|
+
}
|
|
319
|
+
function collectPlans(plansDir, projectRoot, exclude) {
|
|
320
|
+
const all = walk(plansDir, { exclude });
|
|
321
|
+
if (all.length === 0) return [];
|
|
322
|
+
const projectName = import_node_path3.default.basename(projectRoot);
|
|
323
|
+
const related = /* @__PURE__ */ new Set();
|
|
324
|
+
const stems = /* @__PURE__ */ new Set();
|
|
325
|
+
for (const abs of all) {
|
|
326
|
+
let text = "";
|
|
327
|
+
try {
|
|
328
|
+
text = import_node_fs3.default.readFileSync(abs, "utf8");
|
|
329
|
+
} catch {
|
|
330
|
+
continue;
|
|
331
|
+
}
|
|
332
|
+
if (text.includes(projectRoot) || text.includes(projectName)) {
|
|
333
|
+
related.add(abs);
|
|
334
|
+
stems.add(planStem(import_node_path3.default.basename(abs)));
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
for (const abs of all) {
|
|
338
|
+
if (stems.has(planStem(import_node_path3.default.basename(abs)))) related.add(abs);
|
|
339
|
+
}
|
|
340
|
+
return [...related];
|
|
341
|
+
}
|
|
308
342
|
function push(out, sourcePath, storePath, tier, sourceRoot, ctx) {
|
|
309
343
|
let size = 0;
|
|
310
344
|
try {
|
|
@@ -339,10 +373,24 @@ function collect(projectRoot, cfg, tiers) {
|
|
|
339
373
|
}
|
|
340
374
|
push(files, abs, `project/${rel}`, "core", "project", ctx);
|
|
341
375
|
}
|
|
342
|
-
const
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
376
|
+
const projectsDir = import_node_path3.default.join(userClaude, "projects");
|
|
377
|
+
let projectKeys = [];
|
|
378
|
+
try {
|
|
379
|
+
projectKeys = import_node_fs3.default.readdirSync(projectsDir, { withFileTypes: true }).filter((d) => d.isDirectory() && (d.name === key || d.name.startsWith(key + "-"))).map((d) => d.name);
|
|
380
|
+
} catch {
|
|
381
|
+
projectKeys = [];
|
|
382
|
+
}
|
|
383
|
+
for (const pk of projectKeys) {
|
|
384
|
+
const memoryDir = import_node_path3.default.join(projectsDir, pk, "memory");
|
|
385
|
+
for (const abs of walk(memoryDir, { exclude })) {
|
|
386
|
+
const rel = toPosix(import_node_path3.default.relative(memoryDir, abs));
|
|
387
|
+
const storePath = pk === key ? `memory/${rel}` : `memory-sub/${pk.slice(key.length + 1)}/${rel}`;
|
|
388
|
+
push(files, abs, storePath, "core", "user", ctx);
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
for (const abs of collectPlans(import_node_path3.default.join(userClaude, "plans"), projectRoot, exclude)) {
|
|
392
|
+
const rel = toPosix(import_node_path3.default.relative(import_node_path3.default.join(userClaude, "plans"), abs));
|
|
393
|
+
push(files, abs, `plans/${rel}`, "core", "user", ctx);
|
|
346
394
|
}
|
|
347
395
|
const skillsDir = import_node_path3.default.join(userClaude, "skills");
|
|
348
396
|
for (const abs of walk(skillsDir, { exclude })) {
|
|
@@ -378,6 +426,44 @@ function collect(projectRoot, cfg, tiers) {
|
|
|
378
426
|
}
|
|
379
427
|
return { files, skippedTracked, ctx };
|
|
380
428
|
}
|
|
429
|
+
function describeExcluded(projectRoot, tiers) {
|
|
430
|
+
const userClaude = userClaudeDir();
|
|
431
|
+
const key = cwdKey(projectRoot);
|
|
432
|
+
const out = [];
|
|
433
|
+
const measure = (dir, filter) => {
|
|
434
|
+
let files = 0;
|
|
435
|
+
let bytes = 0;
|
|
436
|
+
for (const abs of walk(dir, { exclude: [] })) {
|
|
437
|
+
if (filter && !filter(abs)) continue;
|
|
438
|
+
files++;
|
|
439
|
+
try {
|
|
440
|
+
bytes += import_node_fs3.default.statSync(abs).size;
|
|
441
|
+
} catch {
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
return { files, bytes };
|
|
445
|
+
};
|
|
446
|
+
if (!tiers.includes("transcripts")) {
|
|
447
|
+
const projDir = import_node_path3.default.join(userClaude, "projects", key);
|
|
448
|
+
const m = measure(projDir, (p) => !p.includes(`${import_node_path3.default.sep}memory${import_node_path3.default.sep}`));
|
|
449
|
+
if (m.files > 0) {
|
|
450
|
+
out.push({
|
|
451
|
+
label: "session transcripts",
|
|
452
|
+
...m,
|
|
453
|
+
reason: "cloud-only \u2014 append-only logs this large would permanently bloat the repo"
|
|
454
|
+
});
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
for (const [dir, label, reason] of [
|
|
458
|
+
["uploads", "pasted files/images", "session-scoped binaries; regenerate rather than sync"],
|
|
459
|
+
["file-history", "edit-undo snapshots", "transient local undo state, not portable context"],
|
|
460
|
+
["tasks", "task outputs", "session-scoped tool output"]
|
|
461
|
+
]) {
|
|
462
|
+
const m = measure(import_node_path3.default.join(userClaude, dir));
|
|
463
|
+
if (m.files > 0) out.push({ label, ...m, reason });
|
|
464
|
+
}
|
|
465
|
+
return out.filter((g) => g.bytes > 0);
|
|
466
|
+
}
|
|
381
467
|
function summarize(files) {
|
|
382
468
|
const empty = { count: 0, bytes: 0 };
|
|
383
469
|
const out = {
|
|
@@ -393,6 +479,86 @@ function summarize(files) {
|
|
|
393
479
|
return out;
|
|
394
480
|
}
|
|
395
481
|
|
|
482
|
+
// src/handoff.ts
|
|
483
|
+
function asStringArray(value) {
|
|
484
|
+
if (value === void 0 || value === null) return [];
|
|
485
|
+
if (!Array.isArray(value)) return null;
|
|
486
|
+
if (!value.every((v) => typeof v === "string")) return null;
|
|
487
|
+
return value;
|
|
488
|
+
}
|
|
489
|
+
function validateHandoff(raw) {
|
|
490
|
+
const errors = [];
|
|
491
|
+
if (typeof raw !== "object" || raw === null || Array.isArray(raw)) {
|
|
492
|
+
return { ok: false, errors: ["handoff must be a JSON object"] };
|
|
493
|
+
}
|
|
494
|
+
const o = raw;
|
|
495
|
+
const goal = typeof o.goal === "string" ? o.goal.trim() : "";
|
|
496
|
+
const nextStep = typeof o.nextStep === "string" ? o.nextStep.trim() : "";
|
|
497
|
+
if (!goal) errors.push("`goal` is required (what the session set out to do)");
|
|
498
|
+
if (!nextStep) errors.push("`nextStep` is required (the single next action)");
|
|
499
|
+
const decisions = asStringArray(o.decisions);
|
|
500
|
+
const openThreads = asStringArray(o.openThreads);
|
|
501
|
+
const filesTouched = asStringArray(o.filesTouched);
|
|
502
|
+
if (decisions === null) errors.push("`decisions` must be an array of strings");
|
|
503
|
+
if (openThreads === null) errors.push("`openThreads` must be an array of strings");
|
|
504
|
+
if (filesTouched === null) errors.push("`filesTouched` must be an array of strings");
|
|
505
|
+
let updatedAt = typeof o.updatedAt === "string" ? o.updatedAt : "";
|
|
506
|
+
if (!updatedAt || Number.isNaN(Date.parse(updatedAt))) updatedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
507
|
+
if (errors.length > 0) return { ok: false, errors };
|
|
508
|
+
return {
|
|
509
|
+
ok: true,
|
|
510
|
+
errors: [],
|
|
511
|
+
handoff: {
|
|
512
|
+
updatedAt,
|
|
513
|
+
goal,
|
|
514
|
+
decisions: decisions ?? [],
|
|
515
|
+
openThreads: openThreads ?? [],
|
|
516
|
+
filesTouched: filesTouched ?? [],
|
|
517
|
+
nextStep,
|
|
518
|
+
...typeof o.notes === "string" && o.notes.trim() ? { notes: o.notes.trim() } : {}
|
|
519
|
+
}
|
|
520
|
+
};
|
|
521
|
+
}
|
|
522
|
+
function ago(iso) {
|
|
523
|
+
const ms = Date.now() - Date.parse(iso);
|
|
524
|
+
if (Number.isNaN(ms)) return "unknown";
|
|
525
|
+
const mins = Math.floor(ms / 6e4);
|
|
526
|
+
if (mins < 1) return "just now";
|
|
527
|
+
if (mins < 60) return `${mins}m ago`;
|
|
528
|
+
const hours = Math.floor(mins / 60);
|
|
529
|
+
if (hours < 24) return `${hours}h ago`;
|
|
530
|
+
return `${Math.floor(hours / 24)}d ago`;
|
|
531
|
+
}
|
|
532
|
+
function formatHandoff(h) {
|
|
533
|
+
const lines = [`Where you left off (${ago(h.updatedAt)})`, "", ` Goal ${h.goal}`, ` Next step ${h.nextStep}`];
|
|
534
|
+
if (h.decisions.length > 0) {
|
|
535
|
+
lines.push("", " Decided:");
|
|
536
|
+
for (const d of h.decisions) lines.push(` \xB7 ${d}`);
|
|
537
|
+
}
|
|
538
|
+
if (h.openThreads.length > 0) {
|
|
539
|
+
lines.push("", " Still open:");
|
|
540
|
+
for (const t of h.openThreads) lines.push(` \xB7 ${t}`);
|
|
541
|
+
}
|
|
542
|
+
if (h.filesTouched.length > 0) {
|
|
543
|
+
const shown = h.filesTouched.slice(0, 12);
|
|
544
|
+
lines.push("", " Files touched:");
|
|
545
|
+
for (const f of shown) lines.push(` ${f}`);
|
|
546
|
+
if (h.filesTouched.length > shown.length) {
|
|
547
|
+
lines.push(` \u2026 and ${h.filesTouched.length - shown.length} more`);
|
|
548
|
+
}
|
|
549
|
+
}
|
|
550
|
+
if (h.notes) lines.push("", ` Notes: ${h.notes}`);
|
|
551
|
+
return lines;
|
|
552
|
+
}
|
|
553
|
+
function handoffAge(h) {
|
|
554
|
+
return ago(h.updatedAt);
|
|
555
|
+
}
|
|
556
|
+
function isStale(h, newestContentMs) {
|
|
557
|
+
const t = Date.parse(h.updatedAt);
|
|
558
|
+
if (Number.isNaN(t)) return true;
|
|
559
|
+
return newestContentMs - t > 60 * 60 * 1e3;
|
|
560
|
+
}
|
|
561
|
+
|
|
396
562
|
// src/scaffold.ts
|
|
397
563
|
var import_node_fs4 = __toESM(require("fs"), 1);
|
|
398
564
|
var import_node_path4 = __toESM(require("path"), 1);
|
|
@@ -671,13 +837,17 @@ var LOCAL_TIERS = ["core", "handoff", "artifacts"];
|
|
|
671
837
|
configPath,
|
|
672
838
|
cwdKey,
|
|
673
839
|
defaultConfig,
|
|
840
|
+
describeExcluded,
|
|
674
841
|
ensureGitignoreEntries,
|
|
675
842
|
findProjectRoot,
|
|
676
843
|
formatBytes,
|
|
844
|
+
formatHandoff,
|
|
677
845
|
formatHits,
|
|
678
846
|
gitTrackedSet,
|
|
847
|
+
handoffAge,
|
|
679
848
|
installSlashCommand,
|
|
680
849
|
isGitRepo,
|
|
850
|
+
isStale,
|
|
681
851
|
loadConfig,
|
|
682
852
|
makeTemplate,
|
|
683
853
|
matchesAny,
|
|
@@ -687,5 +857,6 @@ var LOCAL_TIERS = ["core", "handoff", "artifacts"];
|
|
|
687
857
|
storeDir,
|
|
688
858
|
summarize,
|
|
689
859
|
userClaudeDir,
|
|
860
|
+
validateHandoff,
|
|
690
861
|
walk
|
|
691
862
|
});
|
package/dist/index.d.cts
CHANGED
|
@@ -155,11 +155,47 @@ interface CollectResult {
|
|
|
155
155
|
* repo and are exactly what never makes it to a second machine today.
|
|
156
156
|
*/
|
|
157
157
|
declare function collect(projectRoot: string, cfg: ProjectConfig, tiers: Tier[]): CollectResult;
|
|
158
|
+
interface ExcludedGroup {
|
|
159
|
+
label: string;
|
|
160
|
+
files: number;
|
|
161
|
+
bytes: number;
|
|
162
|
+
reason: string;
|
|
163
|
+
}
|
|
164
|
+
/**
|
|
165
|
+
* What was deliberately left behind, and why.
|
|
166
|
+
*
|
|
167
|
+
* Without this a user sees "13 files synced" against a ~400 MB context
|
|
168
|
+
* directory and reasonably concludes the tool is broken. Naming the omissions
|
|
169
|
+
* — with sizes — is the difference between a considered exclusion and a
|
|
170
|
+
* silent one.
|
|
171
|
+
*/
|
|
172
|
+
declare function describeExcluded(projectRoot: string, tiers: Tier[]): ExcludedGroup[];
|
|
158
173
|
declare function summarize(files: CollectedFile[]): Record<Tier, {
|
|
159
174
|
count: number;
|
|
160
175
|
bytes: number;
|
|
161
176
|
}>;
|
|
162
177
|
|
|
178
|
+
/**
|
|
179
|
+
* The handoff is the whole point of the tool — files alone tell you what a
|
|
180
|
+
* project knows, but not where you stopped. It is written by the model (only
|
|
181
|
+
* it has the conversation) and read back on the other machine.
|
|
182
|
+
*
|
|
183
|
+
* Because a model writes it, it needs real validation: a silently malformed
|
|
184
|
+
* handoff is worse than none, since `pull` would look like it worked while
|
|
185
|
+
* handing back nothing usable.
|
|
186
|
+
*/
|
|
187
|
+
interface ValidationResult {
|
|
188
|
+
ok: boolean;
|
|
189
|
+
errors: string[];
|
|
190
|
+
handoff?: Handoff;
|
|
191
|
+
}
|
|
192
|
+
declare function validateHandoff(raw: unknown): ValidationResult;
|
|
193
|
+
/** Render a handoff for a human picking the work back up. */
|
|
194
|
+
declare function formatHandoff(h: Handoff): string[];
|
|
195
|
+
declare function handoffAge(h: Handoff): string;
|
|
196
|
+
/** True when the handoff predates the newest synced content by a wide margin. */
|
|
197
|
+
declare function isStale(h: Handoff, newestContentMs: number): boolean;
|
|
198
|
+
|
|
163
199
|
declare const STORE_DIR = ".contextsync";
|
|
164
200
|
declare const CONFIG_FILE = "config.json";
|
|
165
201
|
declare const HANDOFF_FILE = "handoff.json";
|
|
@@ -266,4 +302,4 @@ declare class LocalStore implements Store {
|
|
|
266
302
|
writeHandoff(handoff: Handoff): void;
|
|
267
303
|
}
|
|
268
304
|
|
|
269
|
-
export { ALL_TIERS, CONFIG_FILE, type CollectResult, type CollectedFile, DEFAULT_EXCLUDE, HANDOFF_FILE, HARD_DENY, type Handoff, LOCAL_TIERS, LocalStore, MANIFEST_FILE, type Manifest, type ManifestEntry, type ProjectConfig, type RemoteConfig, type RestoreResult, SLASH_COMMAND_BODY, SLASH_COMMAND_PATH, STORE_DIR, type SecretHit, type SourceRoot, type Store, type TemplateContext, type Tier, collect, configPath, cwdKey, defaultConfig, ensureGitignoreEntries, findProjectRoot, formatBytes, formatHits, gitTrackedSet, installSlashCommand, isGitRepo, loadConfig, makeTemplate, matchesAny, resolveTemplate, saveConfig, scanFiles, storeDir, summarize, userClaudeDir, walk };
|
|
305
|
+
export { ALL_TIERS, CONFIG_FILE, type CollectResult, type CollectedFile, DEFAULT_EXCLUDE, type ExcludedGroup, HANDOFF_FILE, HARD_DENY, type Handoff, LOCAL_TIERS, LocalStore, MANIFEST_FILE, type Manifest, type ManifestEntry, type ProjectConfig, type RemoteConfig, type RestoreResult, SLASH_COMMAND_BODY, SLASH_COMMAND_PATH, STORE_DIR, type SecretHit, type SourceRoot, type Store, type TemplateContext, type Tier, type ValidationResult, collect, configPath, cwdKey, defaultConfig, describeExcluded, ensureGitignoreEntries, findProjectRoot, formatBytes, formatHandoff, formatHits, gitTrackedSet, handoffAge, installSlashCommand, isGitRepo, isStale, loadConfig, makeTemplate, matchesAny, resolveTemplate, saveConfig, scanFiles, storeDir, summarize, userClaudeDir, validateHandoff, walk };
|
package/dist/index.d.ts
CHANGED
|
@@ -155,11 +155,47 @@ interface CollectResult {
|
|
|
155
155
|
* repo and are exactly what never makes it to a second machine today.
|
|
156
156
|
*/
|
|
157
157
|
declare function collect(projectRoot: string, cfg: ProjectConfig, tiers: Tier[]): CollectResult;
|
|
158
|
+
interface ExcludedGroup {
|
|
159
|
+
label: string;
|
|
160
|
+
files: number;
|
|
161
|
+
bytes: number;
|
|
162
|
+
reason: string;
|
|
163
|
+
}
|
|
164
|
+
/**
|
|
165
|
+
* What was deliberately left behind, and why.
|
|
166
|
+
*
|
|
167
|
+
* Without this a user sees "13 files synced" against a ~400 MB context
|
|
168
|
+
* directory and reasonably concludes the tool is broken. Naming the omissions
|
|
169
|
+
* — with sizes — is the difference between a considered exclusion and a
|
|
170
|
+
* silent one.
|
|
171
|
+
*/
|
|
172
|
+
declare function describeExcluded(projectRoot: string, tiers: Tier[]): ExcludedGroup[];
|
|
158
173
|
declare function summarize(files: CollectedFile[]): Record<Tier, {
|
|
159
174
|
count: number;
|
|
160
175
|
bytes: number;
|
|
161
176
|
}>;
|
|
162
177
|
|
|
178
|
+
/**
|
|
179
|
+
* The handoff is the whole point of the tool — files alone tell you what a
|
|
180
|
+
* project knows, but not where you stopped. It is written by the model (only
|
|
181
|
+
* it has the conversation) and read back on the other machine.
|
|
182
|
+
*
|
|
183
|
+
* Because a model writes it, it needs real validation: a silently malformed
|
|
184
|
+
* handoff is worse than none, since `pull` would look like it worked while
|
|
185
|
+
* handing back nothing usable.
|
|
186
|
+
*/
|
|
187
|
+
interface ValidationResult {
|
|
188
|
+
ok: boolean;
|
|
189
|
+
errors: string[];
|
|
190
|
+
handoff?: Handoff;
|
|
191
|
+
}
|
|
192
|
+
declare function validateHandoff(raw: unknown): ValidationResult;
|
|
193
|
+
/** Render a handoff for a human picking the work back up. */
|
|
194
|
+
declare function formatHandoff(h: Handoff): string[];
|
|
195
|
+
declare function handoffAge(h: Handoff): string;
|
|
196
|
+
/** True when the handoff predates the newest synced content by a wide margin. */
|
|
197
|
+
declare function isStale(h: Handoff, newestContentMs: number): boolean;
|
|
198
|
+
|
|
163
199
|
declare const STORE_DIR = ".contextsync";
|
|
164
200
|
declare const CONFIG_FILE = "config.json";
|
|
165
201
|
declare const HANDOFF_FILE = "handoff.json";
|
|
@@ -266,4 +302,4 @@ declare class LocalStore implements Store {
|
|
|
266
302
|
writeHandoff(handoff: Handoff): void;
|
|
267
303
|
}
|
|
268
304
|
|
|
269
|
-
export { ALL_TIERS, CONFIG_FILE, type CollectResult, type CollectedFile, DEFAULT_EXCLUDE, HANDOFF_FILE, HARD_DENY, type Handoff, LOCAL_TIERS, LocalStore, MANIFEST_FILE, type Manifest, type ManifestEntry, type ProjectConfig, type RemoteConfig, type RestoreResult, SLASH_COMMAND_BODY, SLASH_COMMAND_PATH, STORE_DIR, type SecretHit, type SourceRoot, type Store, type TemplateContext, type Tier, collect, configPath, cwdKey, defaultConfig, ensureGitignoreEntries, findProjectRoot, formatBytes, formatHits, gitTrackedSet, installSlashCommand, isGitRepo, loadConfig, makeTemplate, matchesAny, resolveTemplate, saveConfig, scanFiles, storeDir, summarize, userClaudeDir, walk };
|
|
305
|
+
export { ALL_TIERS, CONFIG_FILE, type CollectResult, type CollectedFile, DEFAULT_EXCLUDE, type ExcludedGroup, HANDOFF_FILE, HARD_DENY, type Handoff, LOCAL_TIERS, LocalStore, MANIFEST_FILE, type Manifest, type ManifestEntry, type ProjectConfig, type RemoteConfig, type RestoreResult, SLASH_COMMAND_BODY, SLASH_COMMAND_PATH, STORE_DIR, type SecretHit, type SourceRoot, type Store, type TemplateContext, type Tier, type ValidationResult, collect, configPath, cwdKey, defaultConfig, describeExcluded, ensureGitignoreEntries, findProjectRoot, formatBytes, formatHandoff, formatHits, gitTrackedSet, handoffAge, installSlashCommand, isGitRepo, isStale, loadConfig, makeTemplate, matchesAny, resolveTemplate, saveConfig, scanFiles, storeDir, summarize, userClaudeDir, validateHandoff, walk };
|
package/dist/index.js
CHANGED
|
@@ -14,13 +14,17 @@ import {
|
|
|
14
14
|
configPath,
|
|
15
15
|
cwdKey,
|
|
16
16
|
defaultConfig,
|
|
17
|
+
describeExcluded,
|
|
17
18
|
ensureGitignoreEntries,
|
|
18
19
|
findProjectRoot,
|
|
19
20
|
formatBytes,
|
|
21
|
+
formatHandoff,
|
|
20
22
|
formatHits,
|
|
21
23
|
gitTrackedSet,
|
|
24
|
+
handoffAge,
|
|
22
25
|
installSlashCommand,
|
|
23
26
|
isGitRepo,
|
|
27
|
+
isStale,
|
|
24
28
|
loadConfig,
|
|
25
29
|
makeTemplate,
|
|
26
30
|
matchesAny,
|
|
@@ -30,8 +34,9 @@ import {
|
|
|
30
34
|
storeDir,
|
|
31
35
|
summarize,
|
|
32
36
|
userClaudeDir,
|
|
37
|
+
validateHandoff,
|
|
33
38
|
walk
|
|
34
|
-
} from "./chunk-
|
|
39
|
+
} from "./chunk-6GB2SN2L.js";
|
|
35
40
|
export {
|
|
36
41
|
ALL_TIERS,
|
|
37
42
|
CONFIG_FILE,
|
|
@@ -48,13 +53,17 @@ export {
|
|
|
48
53
|
configPath,
|
|
49
54
|
cwdKey,
|
|
50
55
|
defaultConfig,
|
|
56
|
+
describeExcluded,
|
|
51
57
|
ensureGitignoreEntries,
|
|
52
58
|
findProjectRoot,
|
|
53
59
|
formatBytes,
|
|
60
|
+
formatHandoff,
|
|
54
61
|
formatHits,
|
|
55
62
|
gitTrackedSet,
|
|
63
|
+
handoffAge,
|
|
56
64
|
installSlashCommand,
|
|
57
65
|
isGitRepo,
|
|
66
|
+
isStale,
|
|
58
67
|
loadConfig,
|
|
59
68
|
makeTemplate,
|
|
60
69
|
matchesAny,
|
|
@@ -64,5 +73,6 @@ export {
|
|
|
64
73
|
storeDir,
|
|
65
74
|
summarize,
|
|
66
75
|
userClaudeDir,
|
|
76
|
+
validateHandoff,
|
|
67
77
|
walk
|
|
68
78
|
};
|