@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.
@@ -0,0 +1,620 @@
1
+ // src/fsutil.ts
2
+ import { execFileSync } from "child_process";
3
+ import fs from "fs";
4
+ import os from "os";
5
+ import path from "path";
6
+ function userClaudeDir() {
7
+ return process.env.CLAUDE_CONFIG_DIR ?? path.join(os.homedir(), ".claude");
8
+ }
9
+ function cwdKey(absPath) {
10
+ return absPath.replace(/[^a-zA-Z0-9]/g, "-");
11
+ }
12
+ function makeTemplate(absPath, ctx) {
13
+ const candidates = [
14
+ [ctx.userClaude, "{userClaude}"],
15
+ [ctx.project, "{project}"]
16
+ ].sort((a, b) => b[0].length - a[0].length);
17
+ let out = absPath;
18
+ for (const [root, token] of candidates) {
19
+ if (absPath === root || absPath.startsWith(root + path.sep)) {
20
+ out = token + "/" + toPosix(path.relative(root, absPath));
21
+ break;
22
+ }
23
+ }
24
+ return ctx.cwdKey ? out.split(ctx.cwdKey).join("{cwdKey}") : out;
25
+ }
26
+ function resolveTemplate(template, ctx) {
27
+ const expanded = template.split("{userClaude}").join(ctx.userClaude).split("{project}").join(ctx.project).split("{cwdKey}").join(ctx.cwdKey);
28
+ return path.normalize(expanded);
29
+ }
30
+ function toPosix(p) {
31
+ return p.split(path.sep).join("/");
32
+ }
33
+ function gitTrackedSet(root) {
34
+ try {
35
+ const out = execFileSync("git", ["-C", root, "ls-files", "-z"], {
36
+ encoding: "utf8",
37
+ maxBuffer: 64 * 1024 * 1024,
38
+ stdio: ["ignore", "pipe", "ignore"]
39
+ });
40
+ const set = /* @__PURE__ */ new Set();
41
+ for (const rel of out.split("\0")) {
42
+ if (rel) set.add(path.resolve(root, rel));
43
+ }
44
+ return set;
45
+ } catch {
46
+ return /* @__PURE__ */ new Set();
47
+ }
48
+ }
49
+ function isGitRepo(root) {
50
+ try {
51
+ execFileSync("git", ["-C", root, "rev-parse", "--is-inside-work-tree"], {
52
+ stdio: "ignore"
53
+ });
54
+ return true;
55
+ } catch {
56
+ return false;
57
+ }
58
+ }
59
+ function globToRegExp(pattern) {
60
+ let re = "";
61
+ for (let i = 0; i < pattern.length; i++) {
62
+ const c = pattern[i];
63
+ if (c === "*") {
64
+ if (pattern[i + 1] === "*") {
65
+ if (pattern[i + 2] === "/") {
66
+ re += "(?:.*/)?";
67
+ i += 2;
68
+ } else {
69
+ re += ".*";
70
+ i += 1;
71
+ }
72
+ } else {
73
+ re += "[^/]*";
74
+ }
75
+ } else if (c === "?") {
76
+ re += "[^/]";
77
+ } else if ("\\^$.|+()[]{}".includes(c)) {
78
+ re += "\\" + c;
79
+ } else {
80
+ re += c;
81
+ }
82
+ }
83
+ if (pattern.endsWith("/")) re += ".*";
84
+ return new RegExp("^" + re + "$");
85
+ }
86
+ function matchesAny(relPath, patterns) {
87
+ const p = toPosix(relPath);
88
+ return patterns.some((pattern) => {
89
+ const re = globToRegExp(pattern);
90
+ if (re.test(p)) return true;
91
+ if (!pattern.includes("/")) {
92
+ return p.split("/").some((seg) => globToRegExp(pattern).test(seg));
93
+ }
94
+ return false;
95
+ });
96
+ }
97
+ function walk(root, opts = {}) {
98
+ const { exclude = [], maxFiles = 5e4 } = opts;
99
+ const out = [];
100
+ if (!fs.existsSync(root)) return out;
101
+ const stack = [root];
102
+ while (stack.length > 0) {
103
+ const dir = stack.pop();
104
+ let entries;
105
+ try {
106
+ entries = fs.readdirSync(dir, { withFileTypes: true });
107
+ } catch {
108
+ continue;
109
+ }
110
+ for (const entry of entries) {
111
+ const abs = path.join(dir, entry.name);
112
+ const rel = toPosix(path.relative(root, abs));
113
+ if (matchesAny(rel, exclude)) continue;
114
+ if (entry.isDirectory()) {
115
+ stack.push(abs);
116
+ } else if (entry.isFile()) {
117
+ if (out.length >= maxFiles) return out;
118
+ out.push(abs);
119
+ }
120
+ }
121
+ }
122
+ return out;
123
+ }
124
+ function ensureDir(dir) {
125
+ fs.mkdirSync(dir, { recursive: true });
126
+ }
127
+ function readJson(file) {
128
+ try {
129
+ return JSON.parse(fs.readFileSync(file, "utf8"));
130
+ } catch {
131
+ return null;
132
+ }
133
+ }
134
+ function writeJson(file, value) {
135
+ ensureDir(path.dirname(file));
136
+ fs.writeFileSync(file, JSON.stringify(value, null, 2) + "\n", "utf8");
137
+ }
138
+ function copyFile(from, to) {
139
+ ensureDir(path.dirname(to));
140
+ fs.copyFileSync(from, to);
141
+ }
142
+ function formatBytes(n) {
143
+ if (n < 1024) return `${n} B`;
144
+ if (n < 1024 * 1024) return `${(n / 1024).toFixed(1)} KB`;
145
+ if (n < 1024 * 1024 * 1024) return `${(n / 1024 / 1024).toFixed(1)} MB`;
146
+ return `${(n / 1024 / 1024 / 1024).toFixed(2)} GB`;
147
+ }
148
+
149
+ // src/config.ts
150
+ import crypto from "crypto";
151
+ import fs2 from "fs";
152
+ import path2 from "path";
153
+ var STORE_DIR = ".contextsync";
154
+ var CONFIG_FILE = "config.json";
155
+ var HANDOFF_FILE = "handoff.json";
156
+ var HARD_DENY = [
157
+ "**/.env",
158
+ "**/.env.*",
159
+ "**/.credentials.json",
160
+ "**/.claude.json",
161
+ "**/*.pem",
162
+ "**/*.key",
163
+ "**/id_rsa*",
164
+ "**/id_ed25519*",
165
+ "**/node_modules/",
166
+ "**/.git/",
167
+ "**/vendor/",
168
+ `**/${STORE_DIR}/`
169
+ ];
170
+ var DEFAULT_EXCLUDE = [
171
+ "**/*.log",
172
+ "**/.DS_Store",
173
+ "**/dist/",
174
+ "**/build/",
175
+ "**/.next/",
176
+ "**/__pycache__/"
177
+ ];
178
+ function defaultConfig(projectRoot) {
179
+ return {
180
+ projectId: crypto.randomUUID(),
181
+ name: path2.basename(projectRoot),
182
+ rootHint: projectRoot,
183
+ tiers: ["core", "handoff"],
184
+ artifactPaths: ["graphify-out"],
185
+ exclude: [...DEFAULT_EXCLUDE],
186
+ remotes: {}
187
+ };
188
+ }
189
+ function findProjectRoot(start = process.cwd()) {
190
+ let dir = path2.resolve(start);
191
+ for (; ; ) {
192
+ if (fs2.existsSync(path2.join(dir, STORE_DIR, CONFIG_FILE))) return dir;
193
+ if (fs2.existsSync(path2.join(dir, ".git"))) return dir;
194
+ const parent = path2.dirname(dir);
195
+ if (parent === dir) return null;
196
+ dir = parent;
197
+ }
198
+ }
199
+ function storeDir(projectRoot) {
200
+ return path2.join(projectRoot, STORE_DIR);
201
+ }
202
+ function configPath(projectRoot) {
203
+ return path2.join(storeDir(projectRoot), CONFIG_FILE);
204
+ }
205
+ function loadConfig(projectRoot) {
206
+ const cfg = readJson(configPath(projectRoot));
207
+ if (!cfg) return null;
208
+ return {
209
+ ...defaultConfig(projectRoot),
210
+ ...cfg,
211
+ remotes: cfg.remotes ?? {},
212
+ tiers: cfg.tiers ?? ["core", "handoff"]
213
+ };
214
+ }
215
+ function saveConfig(projectRoot, cfg) {
216
+ writeJson(configPath(projectRoot), cfg);
217
+ }
218
+
219
+ // src/collector.ts
220
+ import fs3 from "fs";
221
+ import path3 from "path";
222
+ var PROJECT_CONTEXT_GLOBS = [
223
+ "CLAUDE.md",
224
+ "CLAUDE.local.md",
225
+ "AGENTS.md",
226
+ "**/CLAUDE.md",
227
+ "**/AGENTS.md",
228
+ ".cursorrules",
229
+ ".github/copilot-instructions.md",
230
+ ".claude/settings.json",
231
+ ".claude/memory/",
232
+ ".claude/plans/",
233
+ ".claude/commands/",
234
+ ".claude/agents/",
235
+ ".claude/skills/"
236
+ ];
237
+ function push(out, sourcePath, storePath, tier, sourceRoot, ctx) {
238
+ let size = 0;
239
+ try {
240
+ size = fs3.statSync(sourcePath).size;
241
+ } catch {
242
+ return;
243
+ }
244
+ out.push({
245
+ storePath,
246
+ sourcePath,
247
+ sourceRoot,
248
+ tier,
249
+ size,
250
+ restoreTemplate: makeTemplate(sourcePath, ctx)
251
+ });
252
+ }
253
+ function collect(projectRoot, cfg, tiers) {
254
+ const userClaude = userClaudeDir();
255
+ const key = cwdKey(projectRoot);
256
+ const ctx = { userClaude, project: projectRoot, cwdKey: key };
257
+ const files = [];
258
+ const skippedTracked = [];
259
+ const exclude = [...HARD_DENY, ...cfg.exclude];
260
+ const tracked = gitTrackedSet(projectRoot);
261
+ if (tiers.includes("core")) {
262
+ for (const abs of walk(projectRoot, { exclude })) {
263
+ const rel = toPosix(path3.relative(projectRoot, abs));
264
+ if (!matchesAny(rel, PROJECT_CONTEXT_GLOBS)) continue;
265
+ if (tracked.has(abs)) {
266
+ skippedTracked.push(rel);
267
+ continue;
268
+ }
269
+ push(files, abs, `project/${rel}`, "core", "project", ctx);
270
+ }
271
+ const memoryDir = path3.join(userClaude, "projects", key, "memory");
272
+ for (const abs of walk(memoryDir, { exclude })) {
273
+ const rel = toPosix(path3.relative(memoryDir, abs));
274
+ push(files, abs, `memory/${rel}`, "core", "user", ctx);
275
+ }
276
+ const skillsDir = path3.join(userClaude, "skills");
277
+ for (const abs of walk(skillsDir, { exclude })) {
278
+ const rel = toPosix(path3.relative(skillsDir, abs));
279
+ push(files, abs, `skills/${rel}`, "core", "user", ctx);
280
+ }
281
+ const agentsDir = path3.join(userClaude, "agents");
282
+ for (const abs of walk(agentsDir, { exclude })) {
283
+ const rel = toPosix(path3.relative(agentsDir, abs));
284
+ push(files, abs, `agents/${rel}`, "core", "user", ctx);
285
+ }
286
+ for (const name of ["CLAUDE.md", "settings.json"]) {
287
+ const abs = path3.join(userClaude, name);
288
+ if (fs3.existsSync(abs)) push(files, abs, `user/${name}`, "core", "user", ctx);
289
+ }
290
+ }
291
+ if (tiers.includes("artifacts")) {
292
+ for (const relDir of cfg.artifactPaths) {
293
+ const absDir = path3.join(projectRoot, relDir);
294
+ for (const abs of walk(absDir, { exclude })) {
295
+ const rel = toPosix(path3.relative(absDir, abs));
296
+ push(files, abs, `artifacts/${relDir}/${rel}`, "artifacts", "project", ctx);
297
+ }
298
+ }
299
+ }
300
+ if (tiers.includes("transcripts")) {
301
+ const projDir = path3.join(userClaude, "projects", key);
302
+ for (const abs of walk(projDir, { exclude })) {
303
+ const rel = toPosix(path3.relative(projDir, abs));
304
+ if (rel.startsWith("memory/")) continue;
305
+ push(files, abs, `transcripts/${rel}`, "transcripts", "user", ctx);
306
+ }
307
+ }
308
+ return { files, skippedTracked, ctx };
309
+ }
310
+ function summarize(files) {
311
+ const empty = { count: 0, bytes: 0 };
312
+ const out = {
313
+ core: { ...empty },
314
+ handoff: { ...empty },
315
+ artifacts: { ...empty },
316
+ transcripts: { ...empty }
317
+ };
318
+ for (const f of files) {
319
+ out[f.tier].count++;
320
+ out[f.tier].bytes += f.size;
321
+ }
322
+ return out;
323
+ }
324
+
325
+ // src/scaffold.ts
326
+ import fs4 from "fs";
327
+ import path4 from "path";
328
+ var SLASH_COMMAND_PATH = ".claude/commands/context.md";
329
+ var SLASH_COMMAND_BODY = `---
330
+ description: Sync this project's LLM context (memory, skills, instructions, handoff)
331
+ argument-hint: "push | pull | status"
332
+ allowed-tools: Bash(npx @tricknowtech/context:*), Read, Write
333
+ ---
334
+
335
+ Run the context-sync action requested in: $ARGUMENTS
336
+ (If no argument was given, treat it as \`status\`.)
337
+
338
+ ## push
339
+
340
+ 1. Write a handoff summary of THIS conversation to \`.contextsync/handoff.json\`.
341
+ Use exactly this shape \u2014 it is read back verbatim on the other device:
342
+
343
+ \`\`\`json
344
+ {
345
+ "updatedAt": "<ISO 8601 timestamp>",
346
+ "goal": "<what this session set out to achieve, one or two sentences>",
347
+ "decisions": ["<decisions already made, so they aren't re-litigated>"],
348
+ "openThreads": ["<work explicitly left unfinished>"],
349
+ "filesTouched": ["<repo-relative paths changed this session>"],
350
+ "nextStep": "<the single next action someone should take>",
351
+ "notes": "<anything else worth carrying over; optional>"
352
+ }
353
+ \`\`\`
354
+
355
+ Be specific and factual. Write what was actually decided and actually left
356
+ open \u2014 a vague handoff is worse than none, because it reads as progress.
357
+ Never put credentials, tokens, or .env values in any field.
358
+
359
+ 2. Then run: \`npx @tricknowtech/context push\`
360
+
361
+ 3. Report what was synced. If the push was refused for possible secrets, show
362
+ the findings and STOP \u2014 do not re-run with \`--allow-secrets\` unless the
363
+ user explicitly tells you the hits are false positives.
364
+
365
+ ## pull
366
+
367
+ 1. Run: \`npx @tricknowtech/context pull\`
368
+ 2. Read \`.contextsync/handoff.json\` and summarize for the user, out loud:
369
+ the goal, what was decided, what is still open, and the next step.
370
+ 3. If any files were skipped as conflicting, list them and ask before forcing.
371
+
372
+ ## status
373
+
374
+ Run: \`npx @tricknowtech/context status\` and summarize the result.
375
+ `;
376
+ function installSlashCommand(projectRoot) {
377
+ const dest = path4.join(projectRoot, SLASH_COMMAND_PATH);
378
+ if (fs4.existsSync(dest)) return { path: SLASH_COMMAND_PATH, written: false };
379
+ ensureDir(path4.dirname(dest));
380
+ fs4.writeFileSync(dest, SLASH_COMMAND_BODY, "utf8");
381
+ return { path: SLASH_COMMAND_PATH, written: true };
382
+ }
383
+ function ensureGitignoreEntries(projectRoot, artifactsEnabled) {
384
+ const gitignore = path4.join(projectRoot, ".gitignore");
385
+ const wanted = [".contextsync/transcripts/"];
386
+ if (!artifactsEnabled) wanted.push(".contextsync/artifacts/");
387
+ let existing = "";
388
+ try {
389
+ existing = fs4.readFileSync(gitignore, "utf8");
390
+ } catch {
391
+ }
392
+ const lines = new Set(existing.split("\n").map((l) => l.trim()));
393
+ const missing = wanted.filter((w) => !lines.has(w));
394
+ if (missing.length === 0) return [];
395
+ const prefix = existing.length > 0 && !existing.endsWith("\n") ? "\n" : "";
396
+ fs4.appendFileSync(
397
+ gitignore,
398
+ `${prefix}
399
+ # tricknowtech context-sync \u2014 never commit these tiers
400
+ ${missing.join("\n")}
401
+ `,
402
+ "utf8"
403
+ );
404
+ return missing;
405
+ }
406
+
407
+ // src/secrets.ts
408
+ import fs5 from "fs";
409
+ 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;
410
+ function looksLikeCredential(value) {
411
+ if (/^[A-Za-z_$][\w$]*(?:\.[A-Za-z_$][\w$]*)+$/.test(value)) return false;
412
+ if (/^[A-Za-z][A-Za-z_]*$/.test(value) && value.length < 24) return false;
413
+ const classes = [/[a-z]/, /[A-Z]/, /[0-9]/, /[_\-+/=]/].filter((re) => re.test(value)).length;
414
+ return classes >= 2;
415
+ }
416
+ var RULES = [
417
+ { name: "private-key-block", re: /-----BEGIN (?:RSA |EC |DSA |OPENSSH |PGP )?PRIVATE KEY-----/ },
418
+ { name: "aws-access-key-id", re: /\b(?:AKIA|ASIA)[0-9A-Z]{16}\b/ },
419
+ { name: "github-token", re: /\bgh[pousr]_[A-Za-z0-9]{36,}\b/ },
420
+ { name: "slack-token", re: /\bxox[baprs]-[A-Za-z0-9-]{10,}\b/ },
421
+ { name: "stripe-live-key", re: /\b[sr]k_live_[A-Za-z0-9]{16,}\b/ },
422
+ { name: "razorpay-live-key", re: /\brzp_live_[A-Za-z0-9]{10,}\b/ },
423
+ { name: "google-api-key", re: /\bAIza[0-9A-Za-z_-]{35}\b/ },
424
+ { name: "jwt", re: /\beyJ[A-Za-z0-9_-]{10,}\.eyJ[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}\b/ },
425
+ // Tricknowtech's own public-API key formats — esk_/wak_/fak_ are live
426
+ // credentials for a customer's account, and csk_ will be ours.
427
+ { name: "tricknowtech-api-key", re: /\b(?:esk|wak|fak|csk)_[a-f0-9]{40,}\b/ },
428
+ // Connection strings carrying an inline password.
429
+ { name: "db-url-with-password", re: /\b[a-z][a-z0-9+.-]*:\/\/[^\s:/@]+:[^\s:/@]{4,}@[^\s/]+/ },
430
+ // Generic `SOMETHING_SECRET=value` / `"apiKey": "value"` assignments.
431
+ // The value charset deliberately excludes brackets and whitespace so that
432
+ // `tokens: data.get(...)` truncates to a short identifier and falls out.
433
+ {
434
+ name: "assigned-secret",
435
+ 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,
436
+ accept: (m) => !BENIGN_KEY.test(m[1]) && looksLikeCredential(m[2])
437
+ }
438
+ ];
439
+ var PLACEHOLDER = /^(?:x{3,}|\.{3,}|-+|_+|\$\{.*\}|<.*>|\{\{.*\}\}|change[_-]?me|your[_-].*|placeholder|example|dummy|sample|redacted|null|true|false|undefined|test|password|secret|token)$/i;
440
+ function isPlaceholder(value) {
441
+ if (PLACEHOLDER.test(value)) return true;
442
+ return new Set(value).size <= 2;
443
+ }
444
+ function mask(value) {
445
+ const trimmed = value.trim();
446
+ if (trimmed.length <= 8) return "*".repeat(trimmed.length);
447
+ return `${trimmed.slice(0, 4)}${"*".repeat(Math.min(12, trimmed.length - 8))}${trimmed.slice(-4)}`;
448
+ }
449
+ function looksBinary(buf) {
450
+ const n = Math.min(buf.length, 1024);
451
+ for (let i = 0; i < n; i++) {
452
+ if (buf[i] === 0) return true;
453
+ }
454
+ return false;
455
+ }
456
+ var MAX_SCAN_BYTES = 5 * 1024 * 1024;
457
+ function scanFiles(files) {
458
+ const hits = [];
459
+ for (const file of files) {
460
+ let buf;
461
+ try {
462
+ buf = fs5.readFileSync(file.sourcePath);
463
+ } catch {
464
+ continue;
465
+ }
466
+ if (looksBinary(buf)) continue;
467
+ const text = buf.subarray(0, MAX_SCAN_BYTES).toString("utf8");
468
+ const lines = text.split("\n");
469
+ for (let i = 0; i < lines.length; i++) {
470
+ const line = lines[i];
471
+ if (line.length > 4e3) continue;
472
+ for (const rule of RULES) {
473
+ const m = rule.re.exec(line);
474
+ if (!m) continue;
475
+ if (rule.accept && !rule.accept(m)) continue;
476
+ const captured = m[m.length - 1] ?? m[0];
477
+ if (isPlaceholder(captured)) continue;
478
+ hits.push({
479
+ storePath: file.storePath,
480
+ line: i + 1,
481
+ rule: rule.name,
482
+ preview: mask(captured)
483
+ });
484
+ break;
485
+ }
486
+ }
487
+ }
488
+ return hits;
489
+ }
490
+ function formatHits(hits) {
491
+ const shown = hits.slice(0, 20);
492
+ const lines = shown.map((h) => ` ${h.storePath}:${h.line} [${h.rule}] ${h.preview}`);
493
+ if (hits.length > shown.length) {
494
+ lines.push(` \u2026 and ${hits.length - shown.length} more`);
495
+ }
496
+ return lines.join("\n");
497
+ }
498
+
499
+ // src/store.ts
500
+ import fs6 from "fs";
501
+ import path5 from "path";
502
+ var MANIFEST_FILE = "manifest.json";
503
+ var LocalStore = class {
504
+ constructor(projectRoot) {
505
+ this.projectRoot = projectRoot;
506
+ }
507
+ projectRoot;
508
+ get dir() {
509
+ return storeDir(this.projectRoot);
510
+ }
511
+ write(files, projectRoot) {
512
+ for (const name of fs6.existsSync(this.dir) ? fs6.readdirSync(this.dir) : []) {
513
+ if (name === "config.json" || name === HANDOFF_FILE) continue;
514
+ fs6.rmSync(path5.join(this.dir, name), { recursive: true, force: true });
515
+ }
516
+ const entries = [];
517
+ for (const file of files) {
518
+ const dest = path5.join(this.dir, file.storePath);
519
+ try {
520
+ copyFile(file.sourcePath, dest);
521
+ } catch {
522
+ continue;
523
+ }
524
+ entries.push({
525
+ storePath: file.storePath,
526
+ restoreTemplate: file.restoreTemplate,
527
+ tier: file.tier,
528
+ sourceRoot: file.sourceRoot,
529
+ size: file.size
530
+ });
531
+ }
532
+ entries.sort((a, b) => a.storePath.localeCompare(b.storePath));
533
+ const manifest = {
534
+ version: 1,
535
+ updatedAt: (/* @__PURE__ */ new Date()).toISOString(),
536
+ writtenFrom: projectRoot,
537
+ entries
538
+ };
539
+ writeJson(path5.join(this.dir, MANIFEST_FILE), manifest);
540
+ return manifest;
541
+ }
542
+ readManifest() {
543
+ return readJson(path5.join(this.dir, MANIFEST_FILE));
544
+ }
545
+ restore(ctx, opts = {}) {
546
+ const manifest = this.readManifest();
547
+ const restored = [];
548
+ const skipped = [];
549
+ if (!manifest) return { restored, skipped };
550
+ for (const entry of manifest.entries) {
551
+ const src = path5.join(this.dir, entry.storePath);
552
+ if (!fs6.existsSync(src)) continue;
553
+ const dest = resolveTemplate(entry.restoreTemplate, ctx);
554
+ if (!opts.force && fs6.existsSync(dest)) {
555
+ try {
556
+ if (!fs6.readFileSync(dest).equals(fs6.readFileSync(src))) {
557
+ skipped.push(entry.storePath);
558
+ continue;
559
+ }
560
+ } catch {
561
+ skipped.push(entry.storePath);
562
+ continue;
563
+ }
564
+ }
565
+ try {
566
+ ensureDir(path5.dirname(dest));
567
+ fs6.copyFileSync(src, dest);
568
+ restored.push(entry.storePath);
569
+ } catch {
570
+ skipped.push(entry.storePath);
571
+ }
572
+ }
573
+ return { restored, skipped };
574
+ }
575
+ readHandoff() {
576
+ return readJson(path5.join(this.dir, HANDOFF_FILE));
577
+ }
578
+ writeHandoff(handoff) {
579
+ writeJson(path5.join(this.dir, HANDOFF_FILE), handoff);
580
+ }
581
+ };
582
+
583
+ // src/types.ts
584
+ var ALL_TIERS = ["core", "handoff", "artifacts", "transcripts"];
585
+ var LOCAL_TIERS = ["core", "handoff", "artifacts"];
586
+
587
+ export {
588
+ userClaudeDir,
589
+ cwdKey,
590
+ makeTemplate,
591
+ resolveTemplate,
592
+ gitTrackedSet,
593
+ isGitRepo,
594
+ matchesAny,
595
+ walk,
596
+ formatBytes,
597
+ STORE_DIR,
598
+ CONFIG_FILE,
599
+ HANDOFF_FILE,
600
+ HARD_DENY,
601
+ DEFAULT_EXCLUDE,
602
+ defaultConfig,
603
+ findProjectRoot,
604
+ storeDir,
605
+ configPath,
606
+ loadConfig,
607
+ saveConfig,
608
+ collect,
609
+ summarize,
610
+ SLASH_COMMAND_PATH,
611
+ SLASH_COMMAND_BODY,
612
+ installSlashCommand,
613
+ ensureGitignoreEntries,
614
+ scanFiles,
615
+ formatHits,
616
+ MANIFEST_FILE,
617
+ LocalStore,
618
+ ALL_TIERS,
619
+ LOCAL_TIERS
620
+ };