@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/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,17 +65,18 @@ __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);
|
|
67
72
|
|
|
68
73
|
// src/collector.ts
|
|
69
|
-
var
|
|
70
|
-
var
|
|
74
|
+
var import_node_fs4 = __toESM(require("fs"), 1);
|
|
75
|
+
var import_node_path4 = __toESM(require("path"), 1);
|
|
71
76
|
|
|
72
|
-
// src/
|
|
73
|
-
var import_node_crypto = __toESM(require("crypto"), 1);
|
|
77
|
+
// src/assistants.ts
|
|
74
78
|
var import_node_fs2 = __toESM(require("fs"), 1);
|
|
79
|
+
var import_node_os2 = __toESM(require("os"), 1);
|
|
75
80
|
var import_node_path2 = __toESM(require("path"), 1);
|
|
76
81
|
|
|
77
82
|
// src/fsutil.ts
|
|
@@ -88,8 +93,9 @@ function cwdKey(absPath) {
|
|
|
88
93
|
function makeTemplate(absPath, ctx) {
|
|
89
94
|
const candidates = [
|
|
90
95
|
[ctx.userClaude, "{userClaude}"],
|
|
91
|
-
[ctx.project, "{project}"]
|
|
92
|
-
|
|
96
|
+
[ctx.project, "{project}"],
|
|
97
|
+
[ctx.home, "{home}"]
|
|
98
|
+
].filter(([root]) => Boolean(root)).sort((a, b) => b[0].length - a[0].length);
|
|
93
99
|
let out = absPath;
|
|
94
100
|
for (const [root, token] of candidates) {
|
|
95
101
|
if (absPath === root || absPath.startsWith(root + import_node_path.default.sep)) {
|
|
@@ -100,7 +106,7 @@ function makeTemplate(absPath, ctx) {
|
|
|
100
106
|
return ctx.cwdKey ? out.split(ctx.cwdKey).join("{cwdKey}") : out;
|
|
101
107
|
}
|
|
102
108
|
function resolveTemplate(template, ctx) {
|
|
103
|
-
const expanded = template.split("{userClaude}").join(ctx.userClaude).split("{project}").join(ctx.project).split("{cwdKey}").join(ctx.cwdKey);
|
|
109
|
+
const expanded = template.split("{userClaude}").join(ctx.userClaude).split("{project}").join(ctx.project).split("{home}").join(ctx.home).split("{cwdKey}").join(ctx.cwdKey);
|
|
104
110
|
return import_node_path.default.normalize(expanded);
|
|
105
111
|
}
|
|
106
112
|
function toPosix(p) {
|
|
@@ -222,7 +228,176 @@ function formatBytes(n) {
|
|
|
222
228
|
return `${(n / 1024 / 1024 / 1024).toFixed(2)} GB`;
|
|
223
229
|
}
|
|
224
230
|
|
|
231
|
+
// src/assistants.ts
|
|
232
|
+
function filesIn(dir, exclude, prefix) {
|
|
233
|
+
return walk(dir, { exclude }).map((abs) => ({
|
|
234
|
+
abs,
|
|
235
|
+
rel: `${prefix}/${toPosix(import_node_path2.default.relative(dir, abs))}`
|
|
236
|
+
}));
|
|
237
|
+
}
|
|
238
|
+
function fileIfExists(abs, rel) {
|
|
239
|
+
return import_node_fs2.default.existsSync(abs) ? [{ abs, rel }] : [];
|
|
240
|
+
}
|
|
241
|
+
function planStem(fileName) {
|
|
242
|
+
return fileName.replace(/\.md$/, "").replace(/-agent-[0-9a-f]+$/i, "");
|
|
243
|
+
}
|
|
244
|
+
function relatedByProject(dir, projectRoot, exclude) {
|
|
245
|
+
const all = walk(dir, { exclude });
|
|
246
|
+
if (all.length === 0) return [];
|
|
247
|
+
const projectName = import_node_path2.default.basename(projectRoot);
|
|
248
|
+
const related = /* @__PURE__ */ new Set();
|
|
249
|
+
const stems = /* @__PURE__ */ new Set();
|
|
250
|
+
for (const abs of all) {
|
|
251
|
+
let text = "";
|
|
252
|
+
try {
|
|
253
|
+
text = import_node_fs2.default.readFileSync(abs, "utf8");
|
|
254
|
+
} catch {
|
|
255
|
+
continue;
|
|
256
|
+
}
|
|
257
|
+
if (text.includes(projectRoot) || text.includes(projectName)) {
|
|
258
|
+
related.add(abs);
|
|
259
|
+
stems.add(planStem(import_node_path2.default.basename(abs)));
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
for (const abs of all) {
|
|
263
|
+
if (stems.has(planStem(import_node_path2.default.basename(abs)))) related.add(abs);
|
|
264
|
+
}
|
|
265
|
+
return [...related];
|
|
266
|
+
}
|
|
267
|
+
var ADAPTERS = [
|
|
268
|
+
{
|
|
269
|
+
id: "claude",
|
|
270
|
+
name: "Claude Code",
|
|
271
|
+
projectGlobs: [
|
|
272
|
+
"CLAUDE.md",
|
|
273
|
+
"CLAUDE.local.md",
|
|
274
|
+
"**/CLAUDE.md",
|
|
275
|
+
".claude/settings.json",
|
|
276
|
+
".claude/settings.local.json",
|
|
277
|
+
".claude/memory/",
|
|
278
|
+
".claude/plans/",
|
|
279
|
+
".claude/commands/",
|
|
280
|
+
".claude/agents/",
|
|
281
|
+
".claude/skills/"
|
|
282
|
+
],
|
|
283
|
+
userDir: () => userClaudeDir(),
|
|
284
|
+
collectUser: (projectRoot, _home, exclude) => {
|
|
285
|
+
const root = userClaudeDir();
|
|
286
|
+
const out = [];
|
|
287
|
+
const key = cwdKey(projectRoot);
|
|
288
|
+
const projectsDir = import_node_path2.default.join(root, "projects");
|
|
289
|
+
let keys = [];
|
|
290
|
+
try {
|
|
291
|
+
keys = import_node_fs2.default.readdirSync(projectsDir, { withFileTypes: true }).filter((d) => d.isDirectory() && (d.name === key || d.name.startsWith(key + "-"))).map((d) => d.name);
|
|
292
|
+
} catch {
|
|
293
|
+
keys = [];
|
|
294
|
+
}
|
|
295
|
+
for (const k of keys) {
|
|
296
|
+
const memDir = import_node_path2.default.join(projectsDir, k, "memory");
|
|
297
|
+
const prefix = k === key ? "memory" : `memory-sub/${k.slice(key.length + 1)}`;
|
|
298
|
+
out.push(...filesIn(memDir, exclude, prefix));
|
|
299
|
+
}
|
|
300
|
+
out.push(...filesIn(import_node_path2.default.join(root, "skills"), exclude, "skills"));
|
|
301
|
+
out.push(...filesIn(import_node_path2.default.join(root, "agents"), exclude, "agents"));
|
|
302
|
+
for (const abs of relatedByProject(import_node_path2.default.join(root, "plans"), projectRoot, exclude)) {
|
|
303
|
+
out.push({ abs, rel: `plans/${import_node_path2.default.basename(abs)}` });
|
|
304
|
+
}
|
|
305
|
+
out.push(...fileIfExists(import_node_path2.default.join(root, "CLAUDE.md"), "user/CLAUDE.md"));
|
|
306
|
+
out.push(...fileIfExists(import_node_path2.default.join(root, "settings.json"), "user/settings.json"));
|
|
307
|
+
return out;
|
|
308
|
+
}
|
|
309
|
+
},
|
|
310
|
+
{
|
|
311
|
+
id: "codex",
|
|
312
|
+
name: "OpenAI Codex",
|
|
313
|
+
projectGlobs: ["AGENTS.md", "**/AGENTS.md", ".codex/"],
|
|
314
|
+
userDir: (home) => import_node_path2.default.join(home, ".codex"),
|
|
315
|
+
collectUser: (projectRoot, home, exclude) => {
|
|
316
|
+
const root = import_node_path2.default.join(home, ".codex");
|
|
317
|
+
const out = [];
|
|
318
|
+
out.push(...fileIfExists(import_node_path2.default.join(root, "AGENTS.md"), "user/AGENTS.md"));
|
|
319
|
+
out.push(...fileIfExists(import_node_path2.default.join(root, "config.toml"), "user/config.toml"));
|
|
320
|
+
out.push(...filesIn(import_node_path2.default.join(root, "prompts"), exclude, "prompts"));
|
|
321
|
+
return out;
|
|
322
|
+
}
|
|
323
|
+
},
|
|
324
|
+
{
|
|
325
|
+
id: "cursor",
|
|
326
|
+
name: "Cursor",
|
|
327
|
+
// `.cursorrules` is the legacy single-file form; `.cursor/rules/*.mdc` is current.
|
|
328
|
+
projectGlobs: [".cursorrules", ".cursor/rules/", ".cursor/"],
|
|
329
|
+
userDir: (home) => import_node_path2.default.join(home, ".cursor"),
|
|
330
|
+
collectUser: (_projectRoot, home, exclude) => filesIn(import_node_path2.default.join(home, ".cursor", "rules"), exclude, "rules")
|
|
331
|
+
},
|
|
332
|
+
{
|
|
333
|
+
id: "copilot",
|
|
334
|
+
name: "GitHub Copilot",
|
|
335
|
+
projectGlobs: [
|
|
336
|
+
".github/copilot-instructions.md",
|
|
337
|
+
".github/instructions/",
|
|
338
|
+
".github/prompts/"
|
|
339
|
+
]
|
|
340
|
+
},
|
|
341
|
+
{
|
|
342
|
+
id: "windsurf",
|
|
343
|
+
name: "Windsurf",
|
|
344
|
+
projectGlobs: [".windsurfrules", ".windsurf/rules/", ".windsurf/"],
|
|
345
|
+
userDir: (home) => import_node_path2.default.join(home, ".windsurf")
|
|
346
|
+
},
|
|
347
|
+
{
|
|
348
|
+
id: "gemini",
|
|
349
|
+
name: "Gemini CLI",
|
|
350
|
+
projectGlobs: ["GEMINI.md", "**/GEMINI.md", ".gemini/"],
|
|
351
|
+
userDir: (home) => import_node_path2.default.join(home, ".gemini"),
|
|
352
|
+
collectUser: (_projectRoot, home, exclude) => {
|
|
353
|
+
const root = import_node_path2.default.join(home, ".gemini");
|
|
354
|
+
const out = [];
|
|
355
|
+
out.push(...fileIfExists(import_node_path2.default.join(root, "GEMINI.md"), "user/GEMINI.md"));
|
|
356
|
+
out.push(...fileIfExists(import_node_path2.default.join(root, "settings.json"), "user/settings.json"));
|
|
357
|
+
out.push(...filesIn(import_node_path2.default.join(root, "commands"), exclude, "commands"));
|
|
358
|
+
return out;
|
|
359
|
+
}
|
|
360
|
+
},
|
|
361
|
+
{
|
|
362
|
+
id: "cline",
|
|
363
|
+
name: "Cline",
|
|
364
|
+
projectGlobs: [".clinerules", ".clinerules/"]
|
|
365
|
+
},
|
|
366
|
+
{
|
|
367
|
+
id: "aider",
|
|
368
|
+
name: "Aider",
|
|
369
|
+
projectGlobs: ["CONVENTIONS.md", ".aider.conf.yml", ".aider.conf.yaml"]
|
|
370
|
+
},
|
|
371
|
+
{
|
|
372
|
+
id: "continue",
|
|
373
|
+
name: "Continue",
|
|
374
|
+
projectGlobs: [".continue/", ".continuerules"],
|
|
375
|
+
userDir: (home) => import_node_path2.default.join(home, ".continue")
|
|
376
|
+
}
|
|
377
|
+
];
|
|
378
|
+
function adapterById(id) {
|
|
379
|
+
return ADAPTERS.find((a) => a.id === id);
|
|
380
|
+
}
|
|
381
|
+
function homeDir() {
|
|
382
|
+
return import_node_os2.default.homedir();
|
|
383
|
+
}
|
|
384
|
+
function detectAssistants(projectRoot, projectFiles) {
|
|
385
|
+
const home = homeDir();
|
|
386
|
+
return ADAPTERS.filter((a) => {
|
|
387
|
+
if (a.userDir) {
|
|
388
|
+
try {
|
|
389
|
+
if (import_node_fs2.default.existsSync(a.userDir(home))) return true;
|
|
390
|
+
} catch {
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
return projectFiles.some((rel) => matchesAny(rel, a.projectGlobs));
|
|
394
|
+
});
|
|
395
|
+
}
|
|
396
|
+
|
|
225
397
|
// src/config.ts
|
|
398
|
+
var import_node_crypto = __toESM(require("crypto"), 1);
|
|
399
|
+
var import_node_fs3 = __toESM(require("fs"), 1);
|
|
400
|
+
var import_node_path3 = __toESM(require("path"), 1);
|
|
226
401
|
var STORE_DIR = ".contextsync";
|
|
227
402
|
var CONFIG_FILE = "config.json";
|
|
228
403
|
var HANDOFF_FILE = "handoff.json";
|
|
@@ -251,29 +426,30 @@ var DEFAULT_EXCLUDE = [
|
|
|
251
426
|
function defaultConfig(projectRoot) {
|
|
252
427
|
return {
|
|
253
428
|
projectId: import_node_crypto.default.randomUUID(),
|
|
254
|
-
name:
|
|
429
|
+
name: import_node_path3.default.basename(projectRoot),
|
|
255
430
|
rootHint: projectRoot,
|
|
256
431
|
tiers: ["core", "handoff"],
|
|
432
|
+
assistants: ["auto"],
|
|
257
433
|
artifactPaths: ["graphify-out"],
|
|
258
434
|
exclude: [...DEFAULT_EXCLUDE],
|
|
259
435
|
remotes: {}
|
|
260
436
|
};
|
|
261
437
|
}
|
|
262
438
|
function findProjectRoot(start = process.cwd()) {
|
|
263
|
-
let dir =
|
|
439
|
+
let dir = import_node_path3.default.resolve(start);
|
|
264
440
|
for (; ; ) {
|
|
265
|
-
if (
|
|
266
|
-
if (
|
|
267
|
-
const parent =
|
|
441
|
+
if (import_node_fs3.default.existsSync(import_node_path3.default.join(dir, STORE_DIR, CONFIG_FILE))) return dir;
|
|
442
|
+
if (import_node_fs3.default.existsSync(import_node_path3.default.join(dir, ".git"))) return dir;
|
|
443
|
+
const parent = import_node_path3.default.dirname(dir);
|
|
268
444
|
if (parent === dir) return null;
|
|
269
445
|
dir = parent;
|
|
270
446
|
}
|
|
271
447
|
}
|
|
272
448
|
function storeDir(projectRoot) {
|
|
273
|
-
return
|
|
449
|
+
return import_node_path3.default.join(projectRoot, STORE_DIR);
|
|
274
450
|
}
|
|
275
451
|
function configPath(projectRoot) {
|
|
276
|
-
return
|
|
452
|
+
return import_node_path3.default.join(storeDir(projectRoot), CONFIG_FILE);
|
|
277
453
|
}
|
|
278
454
|
function loadConfig(projectRoot) {
|
|
279
455
|
const cfg = readJson(configPath(projectRoot));
|
|
@@ -282,7 +458,9 @@ function loadConfig(projectRoot) {
|
|
|
282
458
|
...defaultConfig(projectRoot),
|
|
283
459
|
...cfg,
|
|
284
460
|
remotes: cfg.remotes ?? {},
|
|
285
|
-
tiers: cfg.tiers ?? ["core", "handoff"]
|
|
461
|
+
tiers: cfg.tiers ?? ["core", "handoff"],
|
|
462
|
+
// Stores written before multi-assistant support have no `assistants` key.
|
|
463
|
+
assistants: cfg.assistants ?? ["auto"]
|
|
286
464
|
};
|
|
287
465
|
}
|
|
288
466
|
function saveConfig(projectRoot, cfg) {
|
|
@@ -290,54 +468,12 @@ function saveConfig(projectRoot, cfg) {
|
|
|
290
468
|
}
|
|
291
469
|
|
|
292
470
|
// src/collector.ts
|
|
293
|
-
var PROJECT_CONTEXT_GLOBS = [
|
|
294
|
-
"CLAUDE.md",
|
|
295
|
-
"CLAUDE.local.md",
|
|
296
|
-
"AGENTS.md",
|
|
297
|
-
"**/CLAUDE.md",
|
|
298
|
-
"**/AGENTS.md",
|
|
299
|
-
".cursorrules",
|
|
300
|
-
".github/copilot-instructions.md",
|
|
301
|
-
".claude/settings.json",
|
|
302
|
-
// `.local.json` variants are gitignored by default, so nothing else carries
|
|
303
|
-
// them — which makes them exactly the kind of file this tool exists for.
|
|
304
|
-
".claude/settings.local.json",
|
|
305
|
-
".claude/memory/",
|
|
306
|
-
".claude/plans/",
|
|
307
|
-
".claude/commands/",
|
|
308
|
-
".claude/agents/",
|
|
309
|
-
".claude/skills/"
|
|
310
|
-
];
|
|
311
|
-
function planStem(fileName) {
|
|
312
|
-
return fileName.replace(/\.md$/, "").replace(/-agent-[0-9a-f]+$/i, "");
|
|
313
|
-
}
|
|
314
|
-
function collectPlans(plansDir, projectRoot, exclude) {
|
|
315
|
-
const all = walk(plansDir, { exclude });
|
|
316
|
-
if (all.length === 0) return [];
|
|
317
|
-
const projectName = import_node_path3.default.basename(projectRoot);
|
|
318
|
-
const related = /* @__PURE__ */ new Set();
|
|
319
|
-
const stems = /* @__PURE__ */ new Set();
|
|
320
|
-
for (const abs of all) {
|
|
321
|
-
let text = "";
|
|
322
|
-
try {
|
|
323
|
-
text = import_node_fs3.default.readFileSync(abs, "utf8");
|
|
324
|
-
} catch {
|
|
325
|
-
continue;
|
|
326
|
-
}
|
|
327
|
-
if (text.includes(projectRoot) || text.includes(projectName)) {
|
|
328
|
-
related.add(abs);
|
|
329
|
-
stems.add(planStem(import_node_path3.default.basename(abs)));
|
|
330
|
-
}
|
|
331
|
-
}
|
|
332
|
-
for (const abs of all) {
|
|
333
|
-
if (stems.has(planStem(import_node_path3.default.basename(abs)))) related.add(abs);
|
|
334
|
-
}
|
|
335
|
-
return [...related];
|
|
336
|
-
}
|
|
337
471
|
function push(out, sourcePath, storePath, tier, sourceRoot, ctx) {
|
|
338
472
|
let size = 0;
|
|
339
473
|
try {
|
|
340
|
-
|
|
474
|
+
const st = import_node_fs4.default.statSync(sourcePath);
|
|
475
|
+
if (!st.isFile()) return;
|
|
476
|
+
size = st.size;
|
|
341
477
|
} catch {
|
|
342
478
|
return;
|
|
343
479
|
}
|
|
@@ -350,76 +486,106 @@ function push(out, sourcePath, storePath, tier, sourceRoot, ctx) {
|
|
|
350
486
|
restoreTemplate: makeTemplate(sourcePath, ctx)
|
|
351
487
|
});
|
|
352
488
|
}
|
|
489
|
+
function templateContextFor(projectRoot) {
|
|
490
|
+
return {
|
|
491
|
+
userClaude: userClaudeDir(),
|
|
492
|
+
project: projectRoot,
|
|
493
|
+
cwdKey: cwdKey(projectRoot),
|
|
494
|
+
home: homeDir()
|
|
495
|
+
};
|
|
496
|
+
}
|
|
353
497
|
function collect(projectRoot, cfg, tiers) {
|
|
354
|
-
const
|
|
355
|
-
const
|
|
356
|
-
const ctx = { userClaude, project: projectRoot, cwdKey: key };
|
|
498
|
+
const ctx = templateContextFor(projectRoot);
|
|
499
|
+
const home = ctx.home;
|
|
357
500
|
const files = [];
|
|
358
501
|
const skippedTracked = [];
|
|
359
502
|
const exclude = [...HARD_DENY, ...cfg.exclude];
|
|
360
503
|
const tracked = gitTrackedSet(projectRoot);
|
|
504
|
+
const projectRel = walk(projectRoot, { exclude }).map((abs) => ({
|
|
505
|
+
abs,
|
|
506
|
+
rel: toPosix(import_node_path4.default.relative(projectRoot, abs))
|
|
507
|
+
}));
|
|
508
|
+
const configured = cfg.assistants && cfg.assistants.length > 0 && !cfg.assistants.includes("auto");
|
|
509
|
+
const assistants = configured ? cfg.assistants.map(adapterById).filter(Boolean) : detectAssistants(projectRoot, projectRel.map((p) => p.rel));
|
|
361
510
|
if (tiers.includes("core")) {
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
511
|
+
const claimed = /* @__PURE__ */ new Set();
|
|
512
|
+
for (const adapter of assistants) {
|
|
513
|
+
for (const { abs, rel } of projectRel) {
|
|
514
|
+
if (claimed.has(rel)) continue;
|
|
515
|
+
if (!matchesAny(rel, adapter.projectGlobs)) continue;
|
|
516
|
+
claimed.add(rel);
|
|
517
|
+
if (tracked.has(abs)) {
|
|
518
|
+
skippedTracked.push(rel);
|
|
519
|
+
continue;
|
|
520
|
+
}
|
|
521
|
+
push(files, abs, `project/${rel}`, "core", "project", ctx);
|
|
368
522
|
}
|
|
369
|
-
push(files, abs, `project/${rel}`, "core", "project", ctx);
|
|
370
|
-
}
|
|
371
|
-
const projectsDir = import_node_path3.default.join(userClaude, "projects");
|
|
372
|
-
let projectKeys = [];
|
|
373
|
-
try {
|
|
374
|
-
projectKeys = import_node_fs3.default.readdirSync(projectsDir, { withFileTypes: true }).filter((d) => d.isDirectory() && (d.name === key || d.name.startsWith(key + "-"))).map((d) => d.name);
|
|
375
|
-
} catch {
|
|
376
|
-
projectKeys = [];
|
|
377
523
|
}
|
|
378
|
-
for (const
|
|
379
|
-
|
|
380
|
-
for (const abs of
|
|
381
|
-
|
|
382
|
-
const storePath = pk === key ? `memory/${rel}` : `memory-sub/${pk.slice(key.length + 1)}/${rel}`;
|
|
383
|
-
push(files, abs, storePath, "core", "user", ctx);
|
|
524
|
+
for (const adapter of assistants) {
|
|
525
|
+
if (!adapter.collectUser) continue;
|
|
526
|
+
for (const { abs, rel } of adapter.collectUser(projectRoot, home, exclude)) {
|
|
527
|
+
push(files, abs, `assistants/${adapter.id}/${rel}`, "core", "user", ctx);
|
|
384
528
|
}
|
|
385
529
|
}
|
|
386
|
-
for (const abs of collectPlans(import_node_path3.default.join(userClaude, "plans"), projectRoot, exclude)) {
|
|
387
|
-
const rel = toPosix(import_node_path3.default.relative(import_node_path3.default.join(userClaude, "plans"), abs));
|
|
388
|
-
push(files, abs, `plans/${rel}`, "core", "user", ctx);
|
|
389
|
-
}
|
|
390
|
-
const skillsDir = import_node_path3.default.join(userClaude, "skills");
|
|
391
|
-
for (const abs of walk(skillsDir, { exclude })) {
|
|
392
|
-
const rel = toPosix(import_node_path3.default.relative(skillsDir, abs));
|
|
393
|
-
push(files, abs, `skills/${rel}`, "core", "user", ctx);
|
|
394
|
-
}
|
|
395
|
-
const agentsDir = import_node_path3.default.join(userClaude, "agents");
|
|
396
|
-
for (const abs of walk(agentsDir, { exclude })) {
|
|
397
|
-
const rel = toPosix(import_node_path3.default.relative(agentsDir, abs));
|
|
398
|
-
push(files, abs, `agents/${rel}`, "core", "user", ctx);
|
|
399
|
-
}
|
|
400
|
-
for (const name of ["CLAUDE.md", "settings.json"]) {
|
|
401
|
-
const abs = import_node_path3.default.join(userClaude, name);
|
|
402
|
-
if (import_node_fs3.default.existsSync(abs)) push(files, abs, `user/${name}`, "core", "user", ctx);
|
|
403
|
-
}
|
|
404
530
|
}
|
|
405
531
|
if (tiers.includes("artifacts")) {
|
|
406
532
|
for (const relDir of cfg.artifactPaths) {
|
|
407
|
-
const absDir =
|
|
533
|
+
const absDir = import_node_path4.default.join(projectRoot, relDir);
|
|
408
534
|
for (const abs of walk(absDir, { exclude })) {
|
|
409
|
-
const rel = toPosix(
|
|
535
|
+
const rel = toPosix(import_node_path4.default.relative(absDir, abs));
|
|
410
536
|
push(files, abs, `artifacts/${relDir}/${rel}`, "artifacts", "project", ctx);
|
|
411
537
|
}
|
|
412
538
|
}
|
|
413
539
|
}
|
|
414
540
|
if (tiers.includes("transcripts")) {
|
|
415
|
-
const projDir =
|
|
541
|
+
const projDir = import_node_path4.default.join(userClaudeDir(), "projects", ctx.cwdKey);
|
|
416
542
|
for (const abs of walk(projDir, { exclude })) {
|
|
417
|
-
const rel = toPosix(
|
|
543
|
+
const rel = toPosix(import_node_path4.default.relative(projDir, abs));
|
|
418
544
|
if (rel.startsWith("memory/")) continue;
|
|
419
|
-
push(files, abs, `transcripts/${rel}`, "transcripts", "user", ctx);
|
|
545
|
+
push(files, abs, `transcripts/claude/${rel}`, "transcripts", "user", ctx);
|
|
546
|
+
}
|
|
547
|
+
}
|
|
548
|
+
return { files, skippedTracked, assistants, ctx };
|
|
549
|
+
}
|
|
550
|
+
function describeExcluded(projectRoot, tiers) {
|
|
551
|
+
const userClaude = userClaudeDir();
|
|
552
|
+
const key = cwdKey(projectRoot);
|
|
553
|
+
const out = [];
|
|
554
|
+
const measure = (dir, filter) => {
|
|
555
|
+
let files = 0;
|
|
556
|
+
let bytes = 0;
|
|
557
|
+
for (const abs of walk(dir, { exclude: [] })) {
|
|
558
|
+
if (filter && !filter(abs)) continue;
|
|
559
|
+
files++;
|
|
560
|
+
try {
|
|
561
|
+
bytes += import_node_fs4.default.statSync(abs).size;
|
|
562
|
+
} catch {
|
|
563
|
+
}
|
|
564
|
+
}
|
|
565
|
+
return { files, bytes };
|
|
566
|
+
};
|
|
567
|
+
if (!tiers.includes("transcripts")) {
|
|
568
|
+
const m = measure(
|
|
569
|
+
import_node_path4.default.join(userClaude, "projects", key),
|
|
570
|
+
(p) => !p.includes(`${import_node_path4.default.sep}memory${import_node_path4.default.sep}`)
|
|
571
|
+
);
|
|
572
|
+
if (m.files > 0) {
|
|
573
|
+
out.push({
|
|
574
|
+
label: "session transcripts",
|
|
575
|
+
...m,
|
|
576
|
+
reason: "cloud-only \u2014 append-only logs this large would permanently bloat the repo"
|
|
577
|
+
});
|
|
420
578
|
}
|
|
421
579
|
}
|
|
422
|
-
|
|
580
|
+
for (const [dir, label, reason] of [
|
|
581
|
+
["uploads", "pasted files/images", "session-scoped binaries; regenerate rather than sync"],
|
|
582
|
+
["file-history", "edit-undo snapshots", "transient local undo state, not portable context"],
|
|
583
|
+
["tasks", "task outputs", "session-scoped tool output"]
|
|
584
|
+
]) {
|
|
585
|
+
const m = measure(import_node_path4.default.join(userClaude, dir));
|
|
586
|
+
if (m.files > 0) out.push({ label, ...m, reason });
|
|
587
|
+
}
|
|
588
|
+
return out.filter((g) => g.bytes > 0);
|
|
423
589
|
}
|
|
424
590
|
function summarize(files) {
|
|
425
591
|
const empty = { count: 0, bytes: 0 };
|
|
@@ -436,9 +602,89 @@ function summarize(files) {
|
|
|
436
602
|
return out;
|
|
437
603
|
}
|
|
438
604
|
|
|
605
|
+
// src/handoff.ts
|
|
606
|
+
function asStringArray(value) {
|
|
607
|
+
if (value === void 0 || value === null) return [];
|
|
608
|
+
if (!Array.isArray(value)) return null;
|
|
609
|
+
if (!value.every((v) => typeof v === "string")) return null;
|
|
610
|
+
return value;
|
|
611
|
+
}
|
|
612
|
+
function validateHandoff(raw) {
|
|
613
|
+
const errors = [];
|
|
614
|
+
if (typeof raw !== "object" || raw === null || Array.isArray(raw)) {
|
|
615
|
+
return { ok: false, errors: ["handoff must be a JSON object"] };
|
|
616
|
+
}
|
|
617
|
+
const o = raw;
|
|
618
|
+
const goal = typeof o.goal === "string" ? o.goal.trim() : "";
|
|
619
|
+
const nextStep = typeof o.nextStep === "string" ? o.nextStep.trim() : "";
|
|
620
|
+
if (!goal) errors.push("`goal` is required (what the session set out to do)");
|
|
621
|
+
if (!nextStep) errors.push("`nextStep` is required (the single next action)");
|
|
622
|
+
const decisions = asStringArray(o.decisions);
|
|
623
|
+
const openThreads = asStringArray(o.openThreads);
|
|
624
|
+
const filesTouched = asStringArray(o.filesTouched);
|
|
625
|
+
if (decisions === null) errors.push("`decisions` must be an array of strings");
|
|
626
|
+
if (openThreads === null) errors.push("`openThreads` must be an array of strings");
|
|
627
|
+
if (filesTouched === null) errors.push("`filesTouched` must be an array of strings");
|
|
628
|
+
let updatedAt = typeof o.updatedAt === "string" ? o.updatedAt : "";
|
|
629
|
+
if (!updatedAt || Number.isNaN(Date.parse(updatedAt))) updatedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
630
|
+
if (errors.length > 0) return { ok: false, errors };
|
|
631
|
+
return {
|
|
632
|
+
ok: true,
|
|
633
|
+
errors: [],
|
|
634
|
+
handoff: {
|
|
635
|
+
updatedAt,
|
|
636
|
+
goal,
|
|
637
|
+
decisions: decisions ?? [],
|
|
638
|
+
openThreads: openThreads ?? [],
|
|
639
|
+
filesTouched: filesTouched ?? [],
|
|
640
|
+
nextStep,
|
|
641
|
+
...typeof o.notes === "string" && o.notes.trim() ? { notes: o.notes.trim() } : {}
|
|
642
|
+
}
|
|
643
|
+
};
|
|
644
|
+
}
|
|
645
|
+
function ago(iso) {
|
|
646
|
+
const ms = Date.now() - Date.parse(iso);
|
|
647
|
+
if (Number.isNaN(ms)) return "unknown";
|
|
648
|
+
const mins = Math.floor(ms / 6e4);
|
|
649
|
+
if (mins < 1) return "just now";
|
|
650
|
+
if (mins < 60) return `${mins}m ago`;
|
|
651
|
+
const hours = Math.floor(mins / 60);
|
|
652
|
+
if (hours < 24) return `${hours}h ago`;
|
|
653
|
+
return `${Math.floor(hours / 24)}d ago`;
|
|
654
|
+
}
|
|
655
|
+
function formatHandoff(h) {
|
|
656
|
+
const lines = [`Where you left off (${ago(h.updatedAt)})`, "", ` Goal ${h.goal}`, ` Next step ${h.nextStep}`];
|
|
657
|
+
if (h.decisions.length > 0) {
|
|
658
|
+
lines.push("", " Decided:");
|
|
659
|
+
for (const d of h.decisions) lines.push(` \xB7 ${d}`);
|
|
660
|
+
}
|
|
661
|
+
if (h.openThreads.length > 0) {
|
|
662
|
+
lines.push("", " Still open:");
|
|
663
|
+
for (const t of h.openThreads) lines.push(` \xB7 ${t}`);
|
|
664
|
+
}
|
|
665
|
+
if (h.filesTouched.length > 0) {
|
|
666
|
+
const shown = h.filesTouched.slice(0, 12);
|
|
667
|
+
lines.push("", " Files touched:");
|
|
668
|
+
for (const f of shown) lines.push(` ${f}`);
|
|
669
|
+
if (h.filesTouched.length > shown.length) {
|
|
670
|
+
lines.push(` \u2026 and ${h.filesTouched.length - shown.length} more`);
|
|
671
|
+
}
|
|
672
|
+
}
|
|
673
|
+
if (h.notes) lines.push("", ` Notes: ${h.notes}`);
|
|
674
|
+
return lines;
|
|
675
|
+
}
|
|
676
|
+
function handoffAge(h) {
|
|
677
|
+
return ago(h.updatedAt);
|
|
678
|
+
}
|
|
679
|
+
function isStale(h, newestContentMs) {
|
|
680
|
+
const t = Date.parse(h.updatedAt);
|
|
681
|
+
if (Number.isNaN(t)) return true;
|
|
682
|
+
return newestContentMs - t > 60 * 60 * 1e3;
|
|
683
|
+
}
|
|
684
|
+
|
|
439
685
|
// src/scaffold.ts
|
|
440
|
-
var
|
|
441
|
-
var
|
|
686
|
+
var import_node_fs5 = __toESM(require("fs"), 1);
|
|
687
|
+
var import_node_path5 = __toESM(require("path"), 1);
|
|
442
688
|
var SLASH_COMMAND_PATH = ".claude/commands/context.md";
|
|
443
689
|
var SLASH_COMMAND_BODY = `---
|
|
444
690
|
description: Sync this project's LLM context (memory, skills, instructions, handoff)
|
|
@@ -488,26 +734,26 @@ Run the context-sync action requested in: $ARGUMENTS
|
|
|
488
734
|
Run: \`npx @tricknowtech/context status\` and summarize the result.
|
|
489
735
|
`;
|
|
490
736
|
function installSlashCommand(projectRoot) {
|
|
491
|
-
const dest =
|
|
492
|
-
if (
|
|
493
|
-
ensureDir(
|
|
494
|
-
|
|
737
|
+
const dest = import_node_path5.default.join(projectRoot, SLASH_COMMAND_PATH);
|
|
738
|
+
if (import_node_fs5.default.existsSync(dest)) return { path: SLASH_COMMAND_PATH, written: false };
|
|
739
|
+
ensureDir(import_node_path5.default.dirname(dest));
|
|
740
|
+
import_node_fs5.default.writeFileSync(dest, SLASH_COMMAND_BODY, "utf8");
|
|
495
741
|
return { path: SLASH_COMMAND_PATH, written: true };
|
|
496
742
|
}
|
|
497
743
|
function ensureGitignoreEntries(projectRoot, artifactsEnabled) {
|
|
498
|
-
const gitignore =
|
|
744
|
+
const gitignore = import_node_path5.default.join(projectRoot, ".gitignore");
|
|
499
745
|
const wanted = [".contextsync/transcripts/"];
|
|
500
746
|
if (!artifactsEnabled) wanted.push(".contextsync/artifacts/");
|
|
501
747
|
let existing = "";
|
|
502
748
|
try {
|
|
503
|
-
existing =
|
|
749
|
+
existing = import_node_fs5.default.readFileSync(gitignore, "utf8");
|
|
504
750
|
} catch {
|
|
505
751
|
}
|
|
506
752
|
const lines = new Set(existing.split("\n").map((l) => l.trim()));
|
|
507
753
|
const missing = wanted.filter((w) => !lines.has(w));
|
|
508
754
|
if (missing.length === 0) return [];
|
|
509
755
|
const prefix = existing.length > 0 && !existing.endsWith("\n") ? "\n" : "";
|
|
510
|
-
|
|
756
|
+
import_node_fs5.default.appendFileSync(
|
|
511
757
|
gitignore,
|
|
512
758
|
`${prefix}
|
|
513
759
|
# tricknowtech context-sync \u2014 never commit these tiers
|
|
@@ -519,7 +765,7 @@ ${missing.join("\n")}
|
|
|
519
765
|
}
|
|
520
766
|
|
|
521
767
|
// src/secrets.ts
|
|
522
|
-
var
|
|
768
|
+
var import_node_fs6 = __toESM(require("fs"), 1);
|
|
523
769
|
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;
|
|
524
770
|
function looksLikeCredential(value) {
|
|
525
771
|
if (/^[A-Za-z_$][\w$]*(?:\.[A-Za-z_$][\w$]*)+$/.test(value)) return false;
|
|
@@ -573,7 +819,7 @@ function scanFiles(files) {
|
|
|
573
819
|
for (const file of files) {
|
|
574
820
|
let buf;
|
|
575
821
|
try {
|
|
576
|
-
buf =
|
|
822
|
+
buf = import_node_fs6.default.readFileSync(file.sourcePath);
|
|
577
823
|
} catch {
|
|
578
824
|
continue;
|
|
579
825
|
}
|
|
@@ -611,8 +857,8 @@ function formatHits(hits) {
|
|
|
611
857
|
}
|
|
612
858
|
|
|
613
859
|
// src/store.ts
|
|
614
|
-
var
|
|
615
|
-
var
|
|
860
|
+
var import_node_fs7 = __toESM(require("fs"), 1);
|
|
861
|
+
var import_node_path6 = __toESM(require("path"), 1);
|
|
616
862
|
var MANIFEST_FILE = "manifest.json";
|
|
617
863
|
var LocalStore = class {
|
|
618
864
|
constructor(projectRoot) {
|
|
@@ -623,13 +869,13 @@ var LocalStore = class {
|
|
|
623
869
|
return storeDir(this.projectRoot);
|
|
624
870
|
}
|
|
625
871
|
write(files, projectRoot) {
|
|
626
|
-
for (const name of
|
|
872
|
+
for (const name of import_node_fs7.default.existsSync(this.dir) ? import_node_fs7.default.readdirSync(this.dir) : []) {
|
|
627
873
|
if (name === "config.json" || name === HANDOFF_FILE) continue;
|
|
628
|
-
|
|
874
|
+
import_node_fs7.default.rmSync(import_node_path6.default.join(this.dir, name), { recursive: true, force: true });
|
|
629
875
|
}
|
|
630
876
|
const entries = [];
|
|
631
877
|
for (const file of files) {
|
|
632
|
-
const dest =
|
|
878
|
+
const dest = import_node_path6.default.join(this.dir, file.storePath);
|
|
633
879
|
try {
|
|
634
880
|
copyFile(file.sourcePath, dest);
|
|
635
881
|
} catch {
|
|
@@ -650,11 +896,11 @@ var LocalStore = class {
|
|
|
650
896
|
writtenFrom: projectRoot,
|
|
651
897
|
entries
|
|
652
898
|
};
|
|
653
|
-
writeJson(
|
|
899
|
+
writeJson(import_node_path6.default.join(this.dir, MANIFEST_FILE), manifest);
|
|
654
900
|
return manifest;
|
|
655
901
|
}
|
|
656
902
|
readManifest() {
|
|
657
|
-
return readJson(
|
|
903
|
+
return readJson(import_node_path6.default.join(this.dir, MANIFEST_FILE));
|
|
658
904
|
}
|
|
659
905
|
restore(ctx, opts = {}) {
|
|
660
906
|
const manifest = this.readManifest();
|
|
@@ -662,12 +908,12 @@ var LocalStore = class {
|
|
|
662
908
|
const skipped = [];
|
|
663
909
|
if (!manifest) return { restored, skipped };
|
|
664
910
|
for (const entry of manifest.entries) {
|
|
665
|
-
const src =
|
|
666
|
-
if (!
|
|
911
|
+
const src = import_node_path6.default.join(this.dir, entry.storePath);
|
|
912
|
+
if (!import_node_fs7.default.existsSync(src)) continue;
|
|
667
913
|
const dest = resolveTemplate(entry.restoreTemplate, ctx);
|
|
668
|
-
if (!opts.force &&
|
|
914
|
+
if (!opts.force && import_node_fs7.default.existsSync(dest)) {
|
|
669
915
|
try {
|
|
670
|
-
if (!
|
|
916
|
+
if (!import_node_fs7.default.readFileSync(dest).equals(import_node_fs7.default.readFileSync(src))) {
|
|
671
917
|
skipped.push(entry.storePath);
|
|
672
918
|
continue;
|
|
673
919
|
}
|
|
@@ -677,8 +923,8 @@ var LocalStore = class {
|
|
|
677
923
|
}
|
|
678
924
|
}
|
|
679
925
|
try {
|
|
680
|
-
ensureDir(
|
|
681
|
-
|
|
926
|
+
ensureDir(import_node_path6.default.dirname(dest));
|
|
927
|
+
import_node_fs7.default.copyFileSync(src, dest);
|
|
682
928
|
restored.push(entry.storePath);
|
|
683
929
|
} catch {
|
|
684
930
|
skipped.push(entry.storePath);
|
|
@@ -687,10 +933,10 @@ var LocalStore = class {
|
|
|
687
933
|
return { restored, skipped };
|
|
688
934
|
}
|
|
689
935
|
readHandoff() {
|
|
690
|
-
return readJson(
|
|
936
|
+
return readJson(import_node_path6.default.join(this.dir, HANDOFF_FILE));
|
|
691
937
|
}
|
|
692
938
|
writeHandoff(handoff) {
|
|
693
|
-
writeJson(
|
|
939
|
+
writeJson(import_node_path6.default.join(this.dir, HANDOFF_FILE), handoff);
|
|
694
940
|
}
|
|
695
941
|
};
|
|
696
942
|
|
|
@@ -714,13 +960,17 @@ var LOCAL_TIERS = ["core", "handoff", "artifacts"];
|
|
|
714
960
|
configPath,
|
|
715
961
|
cwdKey,
|
|
716
962
|
defaultConfig,
|
|
963
|
+
describeExcluded,
|
|
717
964
|
ensureGitignoreEntries,
|
|
718
965
|
findProjectRoot,
|
|
719
966
|
formatBytes,
|
|
967
|
+
formatHandoff,
|
|
720
968
|
formatHits,
|
|
721
969
|
gitTrackedSet,
|
|
970
|
+
handoffAge,
|
|
722
971
|
installSlashCommand,
|
|
723
972
|
isGitRepo,
|
|
973
|
+
isStale,
|
|
724
974
|
loadConfig,
|
|
725
975
|
makeTemplate,
|
|
726
976
|
matchesAny,
|
|
@@ -730,5 +980,6 @@ var LOCAL_TIERS = ["core", "handoff", "artifacts"];
|
|
|
730
980
|
storeDir,
|
|
731
981
|
summarize,
|
|
732
982
|
userClaudeDir,
|
|
983
|
+
validateHandoff,
|
|
733
984
|
walk
|
|
734
985
|
});
|