@tricknowtech/context 0.1.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/dist/cli.cjs ADDED
@@ -0,0 +1,880 @@
1
+ #!/usr/bin/env node
2
+ "use strict";
3
+ var __create = Object.create;
4
+ var __defProp = Object.defineProperty;
5
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
6
+ var __getOwnPropNames = Object.getOwnPropertyNames;
7
+ var __getProtoOf = Object.getPrototypeOf;
8
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
9
+ var __export = (target, all) => {
10
+ for (var name in all)
11
+ __defProp(target, name, { get: all[name], enumerable: true });
12
+ };
13
+ var __copyProps = (to, from, except, desc) => {
14
+ if (from && typeof from === "object" || typeof from === "function") {
15
+ for (let key of __getOwnPropNames(from))
16
+ if (!__hasOwnProp.call(to, key) && key !== except)
17
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
18
+ }
19
+ return to;
20
+ };
21
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
22
+ // If the importer is in node compatibility mode or this is not an ESM
23
+ // file that has been converted to a CommonJS file using a Babel-
24
+ // compatible transform (i.e. "__esModule" has not been set), then set
25
+ // "default" to the CommonJS "module.exports" for node compatibility.
26
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
27
+ mod
28
+ ));
29
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
30
+
31
+ // src/cli.ts
32
+ var cli_exports = {};
33
+ __export(cli_exports, {
34
+ run: () => run
35
+ });
36
+ module.exports = __toCommonJS(cli_exports);
37
+
38
+ // src/commands.ts
39
+ var import_node_fs7 = __toESM(require("fs"), 1);
40
+ var import_node_path6 = __toESM(require("path"), 1);
41
+
42
+ // src/collector.ts
43
+ var import_node_fs3 = __toESM(require("fs"), 1);
44
+ var import_node_path3 = __toESM(require("path"), 1);
45
+
46
+ // src/config.ts
47
+ var import_node_crypto = __toESM(require("crypto"), 1);
48
+ var import_node_fs2 = __toESM(require("fs"), 1);
49
+ var import_node_path2 = __toESM(require("path"), 1);
50
+
51
+ // src/fsutil.ts
52
+ var import_node_child_process = require("child_process");
53
+ var import_node_fs = __toESM(require("fs"), 1);
54
+ var import_node_os = __toESM(require("os"), 1);
55
+ var import_node_path = __toESM(require("path"), 1);
56
+ function userClaudeDir() {
57
+ return process.env.CLAUDE_CONFIG_DIR ?? import_node_path.default.join(import_node_os.default.homedir(), ".claude");
58
+ }
59
+ function cwdKey(absPath) {
60
+ return absPath.replace(/[^a-zA-Z0-9]/g, "-");
61
+ }
62
+ function makeTemplate(absPath, ctx) {
63
+ const candidates = [
64
+ [ctx.userClaude, "{userClaude}"],
65
+ [ctx.project, "{project}"]
66
+ ].sort((a, b) => b[0].length - a[0].length);
67
+ let out2 = absPath;
68
+ for (const [root, token] of candidates) {
69
+ if (absPath === root || absPath.startsWith(root + import_node_path.default.sep)) {
70
+ out2 = token + "/" + toPosix(import_node_path.default.relative(root, absPath));
71
+ break;
72
+ }
73
+ }
74
+ return ctx.cwdKey ? out2.split(ctx.cwdKey).join("{cwdKey}") : out2;
75
+ }
76
+ function resolveTemplate(template, ctx) {
77
+ const expanded = template.split("{userClaude}").join(ctx.userClaude).split("{project}").join(ctx.project).split("{cwdKey}").join(ctx.cwdKey);
78
+ return import_node_path.default.normalize(expanded);
79
+ }
80
+ function toPosix(p) {
81
+ return p.split(import_node_path.default.sep).join("/");
82
+ }
83
+ function gitTrackedSet(root) {
84
+ try {
85
+ const out2 = (0, import_node_child_process.execFileSync)("git", ["-C", root, "ls-files", "-z"], {
86
+ encoding: "utf8",
87
+ maxBuffer: 64 * 1024 * 1024,
88
+ stdio: ["ignore", "pipe", "ignore"]
89
+ });
90
+ const set = /* @__PURE__ */ new Set();
91
+ for (const rel of out2.split("\0")) {
92
+ if (rel) set.add(import_node_path.default.resolve(root, rel));
93
+ }
94
+ return set;
95
+ } catch {
96
+ return /* @__PURE__ */ new Set();
97
+ }
98
+ }
99
+ function isGitRepo(root) {
100
+ try {
101
+ (0, import_node_child_process.execFileSync)("git", ["-C", root, "rev-parse", "--is-inside-work-tree"], {
102
+ stdio: "ignore"
103
+ });
104
+ return true;
105
+ } catch {
106
+ return false;
107
+ }
108
+ }
109
+ function globToRegExp(pattern) {
110
+ let re = "";
111
+ for (let i = 0; i < pattern.length; i++) {
112
+ const c = pattern[i];
113
+ if (c === "*") {
114
+ if (pattern[i + 1] === "*") {
115
+ if (pattern[i + 2] === "/") {
116
+ re += "(?:.*/)?";
117
+ i += 2;
118
+ } else {
119
+ re += ".*";
120
+ i += 1;
121
+ }
122
+ } else {
123
+ re += "[^/]*";
124
+ }
125
+ } else if (c === "?") {
126
+ re += "[^/]";
127
+ } else if ("\\^$.|+()[]{}".includes(c)) {
128
+ re += "\\" + c;
129
+ } else {
130
+ re += c;
131
+ }
132
+ }
133
+ if (pattern.endsWith("/")) re += ".*";
134
+ return new RegExp("^" + re + "$");
135
+ }
136
+ function matchesAny(relPath, patterns) {
137
+ const p = toPosix(relPath);
138
+ return patterns.some((pattern) => {
139
+ const re = globToRegExp(pattern);
140
+ if (re.test(p)) return true;
141
+ if (!pattern.includes("/")) {
142
+ return p.split("/").some((seg) => globToRegExp(pattern).test(seg));
143
+ }
144
+ return false;
145
+ });
146
+ }
147
+ function walk(root, opts = {}) {
148
+ const { exclude = [], maxFiles = 5e4 } = opts;
149
+ const out2 = [];
150
+ if (!import_node_fs.default.existsSync(root)) return out2;
151
+ const stack = [root];
152
+ while (stack.length > 0) {
153
+ const dir = stack.pop();
154
+ let entries;
155
+ try {
156
+ entries = import_node_fs.default.readdirSync(dir, { withFileTypes: true });
157
+ } catch {
158
+ continue;
159
+ }
160
+ for (const entry of entries) {
161
+ const abs = import_node_path.default.join(dir, entry.name);
162
+ const rel = toPosix(import_node_path.default.relative(root, abs));
163
+ if (matchesAny(rel, exclude)) continue;
164
+ if (entry.isDirectory()) {
165
+ stack.push(abs);
166
+ } else if (entry.isFile()) {
167
+ if (out2.length >= maxFiles) return out2;
168
+ out2.push(abs);
169
+ }
170
+ }
171
+ }
172
+ return out2;
173
+ }
174
+ function ensureDir(dir) {
175
+ import_node_fs.default.mkdirSync(dir, { recursive: true });
176
+ }
177
+ function readJson(file) {
178
+ try {
179
+ return JSON.parse(import_node_fs.default.readFileSync(file, "utf8"));
180
+ } catch {
181
+ return null;
182
+ }
183
+ }
184
+ function writeJson(file, value) {
185
+ ensureDir(import_node_path.default.dirname(file));
186
+ import_node_fs.default.writeFileSync(file, JSON.stringify(value, null, 2) + "\n", "utf8");
187
+ }
188
+ function copyFile(from, to) {
189
+ ensureDir(import_node_path.default.dirname(to));
190
+ import_node_fs.default.copyFileSync(from, to);
191
+ }
192
+ function formatBytes(n) {
193
+ if (n < 1024) return `${n} B`;
194
+ if (n < 1024 * 1024) return `${(n / 1024).toFixed(1)} KB`;
195
+ if (n < 1024 * 1024 * 1024) return `${(n / 1024 / 1024).toFixed(1)} MB`;
196
+ return `${(n / 1024 / 1024 / 1024).toFixed(2)} GB`;
197
+ }
198
+
199
+ // src/config.ts
200
+ var STORE_DIR = ".contextsync";
201
+ var CONFIG_FILE = "config.json";
202
+ var HANDOFF_FILE = "handoff.json";
203
+ var HARD_DENY = [
204
+ "**/.env",
205
+ "**/.env.*",
206
+ "**/.credentials.json",
207
+ "**/.claude.json",
208
+ "**/*.pem",
209
+ "**/*.key",
210
+ "**/id_rsa*",
211
+ "**/id_ed25519*",
212
+ "**/node_modules/",
213
+ "**/.git/",
214
+ "**/vendor/",
215
+ `**/${STORE_DIR}/`
216
+ ];
217
+ var DEFAULT_EXCLUDE = [
218
+ "**/*.log",
219
+ "**/.DS_Store",
220
+ "**/dist/",
221
+ "**/build/",
222
+ "**/.next/",
223
+ "**/__pycache__/"
224
+ ];
225
+ function defaultConfig(projectRoot) {
226
+ return {
227
+ projectId: import_node_crypto.default.randomUUID(),
228
+ name: import_node_path2.default.basename(projectRoot),
229
+ rootHint: projectRoot,
230
+ tiers: ["core", "handoff"],
231
+ artifactPaths: ["graphify-out"],
232
+ exclude: [...DEFAULT_EXCLUDE],
233
+ remotes: {}
234
+ };
235
+ }
236
+ function findProjectRoot(start = process.cwd()) {
237
+ let dir = import_node_path2.default.resolve(start);
238
+ for (; ; ) {
239
+ if (import_node_fs2.default.existsSync(import_node_path2.default.join(dir, STORE_DIR, CONFIG_FILE))) return dir;
240
+ if (import_node_fs2.default.existsSync(import_node_path2.default.join(dir, ".git"))) return dir;
241
+ const parent = import_node_path2.default.dirname(dir);
242
+ if (parent === dir) return null;
243
+ dir = parent;
244
+ }
245
+ }
246
+ function storeDir(projectRoot) {
247
+ return import_node_path2.default.join(projectRoot, STORE_DIR);
248
+ }
249
+ function configPath(projectRoot) {
250
+ return import_node_path2.default.join(storeDir(projectRoot), CONFIG_FILE);
251
+ }
252
+ function loadConfig(projectRoot) {
253
+ const cfg = readJson(configPath(projectRoot));
254
+ if (!cfg) return null;
255
+ return {
256
+ ...defaultConfig(projectRoot),
257
+ ...cfg,
258
+ remotes: cfg.remotes ?? {},
259
+ tiers: cfg.tiers ?? ["core", "handoff"]
260
+ };
261
+ }
262
+ function saveConfig(projectRoot, cfg) {
263
+ writeJson(configPath(projectRoot), cfg);
264
+ }
265
+
266
+ // src/collector.ts
267
+ var PROJECT_CONTEXT_GLOBS = [
268
+ "CLAUDE.md",
269
+ "CLAUDE.local.md",
270
+ "AGENTS.md",
271
+ "**/CLAUDE.md",
272
+ "**/AGENTS.md",
273
+ ".cursorrules",
274
+ ".github/copilot-instructions.md",
275
+ ".claude/settings.json",
276
+ ".claude/memory/",
277
+ ".claude/plans/",
278
+ ".claude/commands/",
279
+ ".claude/agents/",
280
+ ".claude/skills/"
281
+ ];
282
+ function push(out2, sourcePath, storePath, tier, sourceRoot, ctx) {
283
+ let size = 0;
284
+ try {
285
+ size = import_node_fs3.default.statSync(sourcePath).size;
286
+ } catch {
287
+ return;
288
+ }
289
+ out2.push({
290
+ storePath,
291
+ sourcePath,
292
+ sourceRoot,
293
+ tier,
294
+ size,
295
+ restoreTemplate: makeTemplate(sourcePath, ctx)
296
+ });
297
+ }
298
+ function collect(projectRoot, cfg, tiers) {
299
+ const userClaude = userClaudeDir();
300
+ const key = cwdKey(projectRoot);
301
+ const ctx = { userClaude, project: projectRoot, cwdKey: key };
302
+ const files = [];
303
+ const skippedTracked = [];
304
+ const exclude = [...HARD_DENY, ...cfg.exclude];
305
+ const tracked = gitTrackedSet(projectRoot);
306
+ if (tiers.includes("core")) {
307
+ for (const abs of walk(projectRoot, { exclude })) {
308
+ const rel = toPosix(import_node_path3.default.relative(projectRoot, abs));
309
+ if (!matchesAny(rel, PROJECT_CONTEXT_GLOBS)) continue;
310
+ if (tracked.has(abs)) {
311
+ skippedTracked.push(rel);
312
+ continue;
313
+ }
314
+ push(files, abs, `project/${rel}`, "core", "project", ctx);
315
+ }
316
+ const memoryDir = import_node_path3.default.join(userClaude, "projects", key, "memory");
317
+ for (const abs of walk(memoryDir, { exclude })) {
318
+ const rel = toPosix(import_node_path3.default.relative(memoryDir, abs));
319
+ push(files, abs, `memory/${rel}`, "core", "user", ctx);
320
+ }
321
+ const skillsDir = import_node_path3.default.join(userClaude, "skills");
322
+ for (const abs of walk(skillsDir, { exclude })) {
323
+ const rel = toPosix(import_node_path3.default.relative(skillsDir, abs));
324
+ push(files, abs, `skills/${rel}`, "core", "user", ctx);
325
+ }
326
+ const agentsDir = import_node_path3.default.join(userClaude, "agents");
327
+ for (const abs of walk(agentsDir, { exclude })) {
328
+ const rel = toPosix(import_node_path3.default.relative(agentsDir, abs));
329
+ push(files, abs, `agents/${rel}`, "core", "user", ctx);
330
+ }
331
+ for (const name of ["CLAUDE.md", "settings.json"]) {
332
+ const abs = import_node_path3.default.join(userClaude, name);
333
+ if (import_node_fs3.default.existsSync(abs)) push(files, abs, `user/${name}`, "core", "user", ctx);
334
+ }
335
+ }
336
+ if (tiers.includes("artifacts")) {
337
+ for (const relDir of cfg.artifactPaths) {
338
+ const absDir = import_node_path3.default.join(projectRoot, relDir);
339
+ for (const abs of walk(absDir, { exclude })) {
340
+ const rel = toPosix(import_node_path3.default.relative(absDir, abs));
341
+ push(files, abs, `artifacts/${relDir}/${rel}`, "artifacts", "project", ctx);
342
+ }
343
+ }
344
+ }
345
+ if (tiers.includes("transcripts")) {
346
+ const projDir = import_node_path3.default.join(userClaude, "projects", key);
347
+ for (const abs of walk(projDir, { exclude })) {
348
+ const rel = toPosix(import_node_path3.default.relative(projDir, abs));
349
+ if (rel.startsWith("memory/")) continue;
350
+ push(files, abs, `transcripts/${rel}`, "transcripts", "user", ctx);
351
+ }
352
+ }
353
+ return { files, skippedTracked, ctx };
354
+ }
355
+ function summarize(files) {
356
+ const empty = { count: 0, bytes: 0 };
357
+ const out2 = {
358
+ core: { ...empty },
359
+ handoff: { ...empty },
360
+ artifacts: { ...empty },
361
+ transcripts: { ...empty }
362
+ };
363
+ for (const f of files) {
364
+ out2[f.tier].count++;
365
+ out2[f.tier].bytes += f.size;
366
+ }
367
+ return out2;
368
+ }
369
+
370
+ // src/scaffold.ts
371
+ var import_node_fs4 = __toESM(require("fs"), 1);
372
+ var import_node_path4 = __toESM(require("path"), 1);
373
+ var SLASH_COMMAND_PATH = ".claude/commands/context.md";
374
+ var SLASH_COMMAND_BODY = `---
375
+ description: Sync this project's LLM context (memory, skills, instructions, handoff)
376
+ argument-hint: "push | pull | status"
377
+ allowed-tools: Bash(npx @tricknowtech/context:*), Read, Write
378
+ ---
379
+
380
+ Run the context-sync action requested in: $ARGUMENTS
381
+ (If no argument was given, treat it as \`status\`.)
382
+
383
+ ## push
384
+
385
+ 1. Write a handoff summary of THIS conversation to \`.contextsync/handoff.json\`.
386
+ Use exactly this shape \u2014 it is read back verbatim on the other device:
387
+
388
+ \`\`\`json
389
+ {
390
+ "updatedAt": "<ISO 8601 timestamp>",
391
+ "goal": "<what this session set out to achieve, one or two sentences>",
392
+ "decisions": ["<decisions already made, so they aren't re-litigated>"],
393
+ "openThreads": ["<work explicitly left unfinished>"],
394
+ "filesTouched": ["<repo-relative paths changed this session>"],
395
+ "nextStep": "<the single next action someone should take>",
396
+ "notes": "<anything else worth carrying over; optional>"
397
+ }
398
+ \`\`\`
399
+
400
+ Be specific and factual. Write what was actually decided and actually left
401
+ open \u2014 a vague handoff is worse than none, because it reads as progress.
402
+ Never put credentials, tokens, or .env values in any field.
403
+
404
+ 2. Then run: \`npx @tricknowtech/context push\`
405
+
406
+ 3. Report what was synced. If the push was refused for possible secrets, show
407
+ the findings and STOP \u2014 do not re-run with \`--allow-secrets\` unless the
408
+ user explicitly tells you the hits are false positives.
409
+
410
+ ## pull
411
+
412
+ 1. Run: \`npx @tricknowtech/context pull\`
413
+ 2. Read \`.contextsync/handoff.json\` and summarize for the user, out loud:
414
+ the goal, what was decided, what is still open, and the next step.
415
+ 3. If any files were skipped as conflicting, list them and ask before forcing.
416
+
417
+ ## status
418
+
419
+ Run: \`npx @tricknowtech/context status\` and summarize the result.
420
+ `;
421
+ function installSlashCommand(projectRoot) {
422
+ const dest = import_node_path4.default.join(projectRoot, SLASH_COMMAND_PATH);
423
+ if (import_node_fs4.default.existsSync(dest)) return { path: SLASH_COMMAND_PATH, written: false };
424
+ ensureDir(import_node_path4.default.dirname(dest));
425
+ import_node_fs4.default.writeFileSync(dest, SLASH_COMMAND_BODY, "utf8");
426
+ return { path: SLASH_COMMAND_PATH, written: true };
427
+ }
428
+ function ensureGitignoreEntries(projectRoot, artifactsEnabled) {
429
+ const gitignore = import_node_path4.default.join(projectRoot, ".gitignore");
430
+ const wanted = [".contextsync/transcripts/"];
431
+ if (!artifactsEnabled) wanted.push(".contextsync/artifacts/");
432
+ let existing = "";
433
+ try {
434
+ existing = import_node_fs4.default.readFileSync(gitignore, "utf8");
435
+ } catch {
436
+ }
437
+ const lines = new Set(existing.split("\n").map((l) => l.trim()));
438
+ const missing = wanted.filter((w) => !lines.has(w));
439
+ if (missing.length === 0) return [];
440
+ const prefix = existing.length > 0 && !existing.endsWith("\n") ? "\n" : "";
441
+ import_node_fs4.default.appendFileSync(
442
+ gitignore,
443
+ `${prefix}
444
+ # tricknowtech context-sync \u2014 never commit these tiers
445
+ ${missing.join("\n")}
446
+ `,
447
+ "utf8"
448
+ );
449
+ return missing;
450
+ }
451
+
452
+ // src/secrets.ts
453
+ var import_node_fs5 = __toESM(require("fs"), 1);
454
+ 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;
455
+ function looksLikeCredential(value) {
456
+ if (/^[A-Za-z_$][\w$]*(?:\.[A-Za-z_$][\w$]*)+$/.test(value)) return false;
457
+ if (/^[A-Za-z][A-Za-z_]*$/.test(value) && value.length < 24) return false;
458
+ const classes = [/[a-z]/, /[A-Z]/, /[0-9]/, /[_\-+/=]/].filter((re) => re.test(value)).length;
459
+ return classes >= 2;
460
+ }
461
+ var RULES = [
462
+ { name: "private-key-block", re: /-----BEGIN (?:RSA |EC |DSA |OPENSSH |PGP )?PRIVATE KEY-----/ },
463
+ { name: "aws-access-key-id", re: /\b(?:AKIA|ASIA)[0-9A-Z]{16}\b/ },
464
+ { name: "github-token", re: /\bgh[pousr]_[A-Za-z0-9]{36,}\b/ },
465
+ { name: "slack-token", re: /\bxox[baprs]-[A-Za-z0-9-]{10,}\b/ },
466
+ { name: "stripe-live-key", re: /\b[sr]k_live_[A-Za-z0-9]{16,}\b/ },
467
+ { name: "razorpay-live-key", re: /\brzp_live_[A-Za-z0-9]{10,}\b/ },
468
+ { name: "google-api-key", re: /\bAIza[0-9A-Za-z_-]{35}\b/ },
469
+ { name: "jwt", re: /\beyJ[A-Za-z0-9_-]{10,}\.eyJ[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}\b/ },
470
+ // Tricknowtech's own public-API key formats — esk_/wak_/fak_ are live
471
+ // credentials for a customer's account, and csk_ will be ours.
472
+ { name: "tricknowtech-api-key", re: /\b(?:esk|wak|fak|csk)_[a-f0-9]{40,}\b/ },
473
+ // Connection strings carrying an inline password.
474
+ { name: "db-url-with-password", re: /\b[a-z][a-z0-9+.-]*:\/\/[^\s:/@]+:[^\s:/@]{4,}@[^\s/]+/ },
475
+ // Generic `SOMETHING_SECRET=value` / `"apiKey": "value"` assignments.
476
+ // The value charset deliberately excludes brackets and whitespace so that
477
+ // `tokens: data.get(...)` truncates to a short identifier and falls out.
478
+ {
479
+ name: "assigned-secret",
480
+ re: /\b([A-Za-z0-9_]*(?:SECRET|PASSWORD|PASSWD|TOKEN|API[_-]?KEY|APIKEY|ACCESS[_-]?KEY|PRIVATE[_-]?KEY|CREDENTIAL|AUTH)[A-Za-z0-9_]*)["']?\s*[:=]\s*["']?([A-Za-z0-9_\-+/=.]{8,})/i,
481
+ accept: (m) => !BENIGN_KEY.test(m[1]) && looksLikeCredential(m[2])
482
+ }
483
+ ];
484
+ var PLACEHOLDER = /^(?:x{3,}|\.{3,}|-+|_+|\$\{.*\}|<.*>|\{\{.*\}\}|change[_-]?me|your[_-].*|placeholder|example|dummy|sample|redacted|null|true|false|undefined|test|password|secret|token)$/i;
485
+ function isPlaceholder(value) {
486
+ if (PLACEHOLDER.test(value)) return true;
487
+ return new Set(value).size <= 2;
488
+ }
489
+ function mask(value) {
490
+ const trimmed = value.trim();
491
+ if (trimmed.length <= 8) return "*".repeat(trimmed.length);
492
+ return `${trimmed.slice(0, 4)}${"*".repeat(Math.min(12, trimmed.length - 8))}${trimmed.slice(-4)}`;
493
+ }
494
+ function looksBinary(buf) {
495
+ const n = Math.min(buf.length, 1024);
496
+ for (let i = 0; i < n; i++) {
497
+ if (buf[i] === 0) return true;
498
+ }
499
+ return false;
500
+ }
501
+ var MAX_SCAN_BYTES = 5 * 1024 * 1024;
502
+ function scanFiles(files) {
503
+ const hits = [];
504
+ for (const file of files) {
505
+ let buf;
506
+ try {
507
+ buf = import_node_fs5.default.readFileSync(file.sourcePath);
508
+ } catch {
509
+ continue;
510
+ }
511
+ if (looksBinary(buf)) continue;
512
+ const text = buf.subarray(0, MAX_SCAN_BYTES).toString("utf8");
513
+ const lines = text.split("\n");
514
+ for (let i = 0; i < lines.length; i++) {
515
+ const line = lines[i];
516
+ if (line.length > 4e3) continue;
517
+ for (const rule of RULES) {
518
+ const m = rule.re.exec(line);
519
+ if (!m) continue;
520
+ if (rule.accept && !rule.accept(m)) continue;
521
+ const captured = m[m.length - 1] ?? m[0];
522
+ if (isPlaceholder(captured)) continue;
523
+ hits.push({
524
+ storePath: file.storePath,
525
+ line: i + 1,
526
+ rule: rule.name,
527
+ preview: mask(captured)
528
+ });
529
+ break;
530
+ }
531
+ }
532
+ }
533
+ return hits;
534
+ }
535
+ function formatHits(hits) {
536
+ const shown = hits.slice(0, 20);
537
+ const lines = shown.map((h) => ` ${h.storePath}:${h.line} [${h.rule}] ${h.preview}`);
538
+ if (hits.length > shown.length) {
539
+ lines.push(` \u2026 and ${hits.length - shown.length} more`);
540
+ }
541
+ return lines.join("\n");
542
+ }
543
+
544
+ // src/store.ts
545
+ var import_node_fs6 = __toESM(require("fs"), 1);
546
+ var import_node_path5 = __toESM(require("path"), 1);
547
+ var MANIFEST_FILE = "manifest.json";
548
+ var LocalStore = class {
549
+ constructor(projectRoot) {
550
+ this.projectRoot = projectRoot;
551
+ }
552
+ projectRoot;
553
+ get dir() {
554
+ return storeDir(this.projectRoot);
555
+ }
556
+ write(files, projectRoot) {
557
+ for (const name of import_node_fs6.default.existsSync(this.dir) ? import_node_fs6.default.readdirSync(this.dir) : []) {
558
+ if (name === "config.json" || name === HANDOFF_FILE) continue;
559
+ import_node_fs6.default.rmSync(import_node_path5.default.join(this.dir, name), { recursive: true, force: true });
560
+ }
561
+ const entries = [];
562
+ for (const file of files) {
563
+ const dest = import_node_path5.default.join(this.dir, file.storePath);
564
+ try {
565
+ copyFile(file.sourcePath, dest);
566
+ } catch {
567
+ continue;
568
+ }
569
+ entries.push({
570
+ storePath: file.storePath,
571
+ restoreTemplate: file.restoreTemplate,
572
+ tier: file.tier,
573
+ sourceRoot: file.sourceRoot,
574
+ size: file.size
575
+ });
576
+ }
577
+ entries.sort((a, b) => a.storePath.localeCompare(b.storePath));
578
+ const manifest = {
579
+ version: 1,
580
+ updatedAt: (/* @__PURE__ */ new Date()).toISOString(),
581
+ writtenFrom: projectRoot,
582
+ entries
583
+ };
584
+ writeJson(import_node_path5.default.join(this.dir, MANIFEST_FILE), manifest);
585
+ return manifest;
586
+ }
587
+ readManifest() {
588
+ return readJson(import_node_path5.default.join(this.dir, MANIFEST_FILE));
589
+ }
590
+ restore(ctx, opts = {}) {
591
+ const manifest = this.readManifest();
592
+ const restored = [];
593
+ const skipped = [];
594
+ if (!manifest) return { restored, skipped };
595
+ for (const entry of manifest.entries) {
596
+ const src = import_node_path5.default.join(this.dir, entry.storePath);
597
+ if (!import_node_fs6.default.existsSync(src)) continue;
598
+ const dest = resolveTemplate(entry.restoreTemplate, ctx);
599
+ if (!opts.force && import_node_fs6.default.existsSync(dest)) {
600
+ try {
601
+ if (!import_node_fs6.default.readFileSync(dest).equals(import_node_fs6.default.readFileSync(src))) {
602
+ skipped.push(entry.storePath);
603
+ continue;
604
+ }
605
+ } catch {
606
+ skipped.push(entry.storePath);
607
+ continue;
608
+ }
609
+ }
610
+ try {
611
+ ensureDir(import_node_path5.default.dirname(dest));
612
+ import_node_fs6.default.copyFileSync(src, dest);
613
+ restored.push(entry.storePath);
614
+ } catch {
615
+ skipped.push(entry.storePath);
616
+ }
617
+ }
618
+ return { restored, skipped };
619
+ }
620
+ readHandoff() {
621
+ return readJson(import_node_path5.default.join(this.dir, HANDOFF_FILE));
622
+ }
623
+ writeHandoff(handoff) {
624
+ writeJson(import_node_path5.default.join(this.dir, HANDOFF_FILE), handoff);
625
+ }
626
+ };
627
+
628
+ // src/types.ts
629
+ var ALL_TIERS = ["core", "handoff", "artifacts", "transcripts"];
630
+ var LOCAL_TIERS = ["core", "handoff", "artifacts"];
631
+
632
+ // src/commands.ts
633
+ function ok(lines) {
634
+ return { code: 0, lines };
635
+ }
636
+ function fail(lines) {
637
+ return { code: 1, lines };
638
+ }
639
+ function requireProject() {
640
+ const root = findProjectRoot();
641
+ if (!root) {
642
+ return fail([
643
+ "Not inside a project.",
644
+ "Run this from a git repository, or run `ctx init` to create a store here."
645
+ ]);
646
+ }
647
+ const cfg = loadConfig(root);
648
+ if (!cfg) {
649
+ return fail([`No context store found at ${import_node_path6.default.join(root, ".contextsync")}.`, "Run `ctx init` first."]);
650
+ }
651
+ return { root, cfg };
652
+ }
653
+ function templateContext(root) {
654
+ return { userClaude: userClaudeDir(), project: root, cwdKey: cwdKey(root) };
655
+ }
656
+ function effectiveTiers(cfg) {
657
+ const tiers = cfg.tiers.filter((t) => LOCAL_TIERS.includes(t));
658
+ const refused = cfg.tiers.filter((t) => !LOCAL_TIERS.includes(t));
659
+ return { tiers, refused };
660
+ }
661
+ function cmdInit(opts = {}) {
662
+ const root = findProjectRoot() ?? process.cwd();
663
+ const existing = loadConfig(root);
664
+ if (existing && !opts.force) {
665
+ return fail([`Already initialised at ${configPath(root)}.`, "Pass --force to overwrite the config."]);
666
+ }
667
+ const cfg = defaultConfig(root);
668
+ if (opts.artifacts) cfg.tiers = [...cfg.tiers, "artifacts"];
669
+ saveConfig(root, cfg);
670
+ const lines = [
671
+ `Initialised context store for "${cfg.name}"`,
672
+ ` store ${import_node_path6.default.relative(root, storeDir(root))}/`,
673
+ ` tiers ${cfg.tiers.join(", ")}`
674
+ ];
675
+ const slash = installSlashCommand(root);
676
+ lines.push(` command ${slash.path}${slash.written ? "" : " (already present, left alone)"}`);
677
+ const added = ensureGitignoreEntries(root, Boolean(opts.artifacts));
678
+ if (added.length > 0) lines.push(` ignored ${added.join(", ")}`);
679
+ if (!isGitRepo(root)) {
680
+ lines.push("", "Note: this is not a git repository, so the store will not travel with the code.");
681
+ }
682
+ lines.push("", "Next: run `/context push` in Claude Code, or `ctx push` directly.");
683
+ return ok(lines);
684
+ }
685
+ function cmdPush(opts = {}) {
686
+ const found = requireProject();
687
+ if ("code" in found) return found;
688
+ const { root, cfg } = found;
689
+ const { tiers, refused } = effectiveTiers(cfg);
690
+ const { files, skippedTracked } = collect(root, cfg, tiers);
691
+ const lines = [];
692
+ if (refused.length > 0) {
693
+ lines.push(
694
+ `Skipping ${refused.join(", ")} \u2014 not supported by a local store.`,
695
+ " Transcripts run to hundreds of MB and would permanently bloat the repo.",
696
+ " Add a cloud remote to sync them.",
697
+ ""
698
+ );
699
+ }
700
+ if (files.length === 0) {
701
+ lines.push("Nothing to sync.");
702
+ if (skippedTracked.length > 0) {
703
+ lines.push(`(${skippedTracked.length} project files skipped \u2014 git already tracks them.)`);
704
+ }
705
+ return ok(lines);
706
+ }
707
+ const hits = scanFiles(files);
708
+ if (hits.length > 0 && !opts.allowSecrets) {
709
+ return fail([
710
+ ...lines,
711
+ `Refusing to push \u2014 ${hits.length} possible secret${hits.length === 1 ? "" : "s"} found:`,
712
+ "",
713
+ formatHits(hits),
714
+ "",
715
+ "These would be committed to the repository and be very hard to remove.",
716
+ "Fix the source, add an exclude pattern, or re-run with --allow-secrets if these are false positives."
717
+ ]);
718
+ }
719
+ if (hits.length > 0) {
720
+ lines.push(`Warning: pushing ${hits.length} possible secret(s) because --allow-secrets was set.`, "");
721
+ }
722
+ const totals = summarize(files);
723
+ const totalBytes = files.reduce((n, f) => n + f.size, 0);
724
+ if (opts.dryRun) {
725
+ lines.push(`Would sync ${files.length} files (${formatBytes(totalBytes)}):`);
726
+ } else {
727
+ const store = new LocalStore(root);
728
+ store.write(files, root);
729
+ lines.push(`Synced ${files.length} files (${formatBytes(totalBytes)}) to ${import_node_path6.default.relative(root, storeDir(root))}/`);
730
+ }
731
+ for (const tier of ALL_TIERS) {
732
+ const t = totals[tier];
733
+ if (t.count > 0) lines.push(` ${tier.padEnd(11)} ${String(t.count).padStart(4)} files ${formatBytes(t.bytes)}`);
734
+ }
735
+ if (skippedTracked.length > 0) {
736
+ lines.push("", `${skippedTracked.length} project files skipped \u2014 git already tracks them.`);
737
+ }
738
+ if (!opts.dryRun) {
739
+ lines.push("", "Commit .contextsync/ to carry this context with the repo.");
740
+ }
741
+ return ok(lines);
742
+ }
743
+ function cmdPull(opts = {}) {
744
+ const found = requireProject();
745
+ if ("code" in found) return found;
746
+ const { root } = found;
747
+ const store = new LocalStore(root);
748
+ const manifest = store.readManifest();
749
+ if (!manifest) {
750
+ return fail([`No manifest in ${import_node_path6.default.relative(root, storeDir(root))}/.`, "Run `ctx push` on the source machine first."]);
751
+ }
752
+ const ctx = templateContext(root);
753
+ const { restored, skipped } = store.restore(ctx, { force: opts.force });
754
+ const lines = [`Restored ${restored.length} of ${manifest.entries.length} files.`];
755
+ if (manifest.writtenFrom && manifest.writtenFrom !== root) {
756
+ lines.push(` Rewrote paths from ${manifest.writtenFrom} \u2192 ${root}`);
757
+ }
758
+ if (skipped.length > 0) {
759
+ lines.push(
760
+ "",
761
+ `${skipped.length} file(s) left alone because the local copy differs:`,
762
+ ...skipped.slice(0, 15).map((s) => ` ${s}`),
763
+ ...skipped.length > 15 ? [` \u2026 and ${skipped.length - 15} more`] : [],
764
+ "",
765
+ "Re-run with --force to overwrite them."
766
+ );
767
+ }
768
+ const handoff = store.readHandoff();
769
+ if (handoff) {
770
+ lines.push("", `Handoff (${handoff.updatedAt}):`, ` goal: ${handoff.goal}`, ` next: ${handoff.nextStep}`);
771
+ }
772
+ return ok(lines);
773
+ }
774
+ function cmdStatus() {
775
+ const found = requireProject();
776
+ if ("code" in found) return found;
777
+ const { root, cfg } = found;
778
+ const store = new LocalStore(root);
779
+ const manifest = store.readManifest();
780
+ const { tiers } = effectiveTiers(cfg);
781
+ const { files, skippedTracked } = collect(root, cfg, tiers);
782
+ const lines = [
783
+ `Project ${cfg.name}`,
784
+ `Store ${import_node_path6.default.relative(root, storeDir(root))}/`,
785
+ `Tiers ${cfg.tiers.join(", ")}`,
786
+ `Remotes ${Object.keys(cfg.remotes).length > 0 ? Object.keys(cfg.remotes).join(", ") : "none (local only)"}`,
787
+ ""
788
+ ];
789
+ if (!manifest) {
790
+ lines.push(`Never pushed. ${files.length} files ready to sync.`);
791
+ return ok(lines);
792
+ }
793
+ const stored = new Map(manifest.entries.map((e) => [e.storePath, e]));
794
+ const current = new Map(files.map((f) => [f.storePath, f]));
795
+ const added = [...current.keys()].filter((k) => !stored.has(k));
796
+ const removed = [...stored.keys()].filter((k) => !current.has(k));
797
+ const changed = [...current.entries()].filter(([k, f]) => {
798
+ const e = stored.get(k);
799
+ if (!e) return false;
800
+ if (e.size !== f.size) return true;
801
+ try {
802
+ return !import_node_fs7.default.readFileSync(import_node_path6.default.join(storeDir(root), k)).equals(import_node_fs7.default.readFileSync(f.sourcePath));
803
+ } catch {
804
+ return true;
805
+ }
806
+ }).map(([k]) => k);
807
+ lines.push(`Last push ${manifest.updatedAt}`);
808
+ if (added.length + removed.length + changed.length === 0) {
809
+ lines.push("", "Up to date.");
810
+ } else {
811
+ lines.push("");
812
+ for (const k of changed.slice(0, 20)) lines.push(` modified ${k}`);
813
+ for (const k of added.slice(0, 20)) lines.push(` new ${k}`);
814
+ for (const k of removed.slice(0, 20)) lines.push(` removed ${k}`);
815
+ const shown = Math.min(changed.length, 20) + Math.min(added.length, 20) + Math.min(removed.length, 20);
816
+ const total = changed.length + added.length + removed.length;
817
+ if (total > shown) lines.push(` \u2026 and ${total - shown} more`);
818
+ lines.push("", "Run `ctx push` to sync.");
819
+ }
820
+ if (skippedTracked.length > 0) {
821
+ lines.push("", `${skippedTracked.length} project files carried by git directly.`);
822
+ }
823
+ return ok(lines);
824
+ }
825
+
826
+ // src/cli.ts
827
+ var USAGE = `tricknowtech context-sync \u2014 carry a project's LLM context between machines
828
+
829
+ Usage
830
+ ctx init [--artifacts] [--force] Create the store and install /context
831
+ ctx push [--dry-run] Collect context into the store
832
+ ctx pull [--force] Restore context from the store
833
+ ctx status Show what has changed since the last push
834
+
835
+ Options
836
+ --artifacts Include derived indexes (graphify-out/, etc.)
837
+ --allow-secrets Push even if the secret scan finds hits (think first)
838
+ --dry-run Show what would be synced without writing
839
+ --force init: overwrite config \xB7 pull: overwrite differing files
840
+ -h, --help Show this help
841
+ -v, --version Show version
842
+
843
+ 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.`;
845
+ function parseArgs(argv) {
846
+ const flags = /* @__PURE__ */ new Set();
847
+ let command = "";
848
+ for (const arg of argv) {
849
+ if (arg.startsWith("-")) flags.add(arg.replace(/^-+/, ""));
850
+ else if (!command) command = arg;
851
+ }
852
+ return { command, flags };
853
+ }
854
+ function run(argv) {
855
+ const { command, flags } = parseArgs(argv);
856
+ if (flags.has("h") || flags.has("help")) return { code: 0, lines: [USAGE] };
857
+ if (flags.has("v") || flags.has("version")) return { code: 0, lines: ["0.1.0"] };
858
+ switch (command) {
859
+ case "init":
860
+ return cmdInit({ artifacts: flags.has("artifacts"), force: flags.has("force") });
861
+ case "push":
862
+ return cmdPush({ allowSecrets: flags.has("allow-secrets"), dryRun: flags.has("dry-run") });
863
+ case "pull":
864
+ return cmdPull({ force: flags.has("force") });
865
+ case "status":
866
+ case "":
867
+ return cmdStatus();
868
+ default:
869
+ return { code: 1, lines: [`Unknown command: ${command}`, "", USAGE] };
870
+ }
871
+ }
872
+ var result = run(process.argv.slice(2));
873
+ var out = result.lines.join("\n");
874
+ if (result.code === 0) console.log(out);
875
+ else console.error(out);
876
+ process.exit(result.code);
877
+ // Annotate the CommonJS export names for ESM import in node:
878
+ 0 && (module.exports = {
879
+ run
880
+ });