claudectx 1.1.0 → 1.1.1
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 +132 -1
- package/dist/index.js +130 -30
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +130 -30
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -28,6 +28,10 @@ A typical Claude Code session costs **$2–$15 in tokens**. Most of it is wasted
|
|
|
28
28
|
| Full file reads for small questions | 70% overhead from unnecessary lines | `claudectx mcp` |
|
|
29
29
|
| No prompt caching configured | Paying 10× for static context | `claudectx optimize --cache` |
|
|
30
30
|
| No cross-session memory | Repeating the same context every session | `claudectx compress` |
|
|
31
|
+
| Dead `@refs` and stale sections in CLAUDE.md | Silent token waste on every request | `claudectx drift` |
|
|
32
|
+
| Unknown cost before running a big task | Surprise bills after the fact | `claudectx budget` |
|
|
33
|
+
| Cache cold at session start | First request always a full miss | `claudectx warmup` |
|
|
34
|
+
| No visibility into team-wide spend | Can't attribute cost across devs | `claudectx teams` |
|
|
31
35
|
|
|
32
36
|
### Where your tokens actually go
|
|
33
37
|
|
|
@@ -59,6 +63,23 @@ claudectx compress
|
|
|
59
63
|
|
|
60
64
|
# Review your token spend for the last 7 days
|
|
61
65
|
claudectx report
|
|
66
|
+
|
|
67
|
+
# --- v1.1.0 additions ---
|
|
68
|
+
|
|
69
|
+
# Know the cost before you start a task
|
|
70
|
+
claudectx budget "src/**/*.ts" "tests/**/*.ts"
|
|
71
|
+
|
|
72
|
+
# Pre-warm your prompt cache so the first request is free
|
|
73
|
+
claudectx warmup --api-key $ANTHROPIC_API_KEY
|
|
74
|
+
|
|
75
|
+
# Find dead references and stale sections in CLAUDE.md
|
|
76
|
+
claudectx drift
|
|
77
|
+
|
|
78
|
+
# Convert CLAUDE.md to Cursor / Copilot / Windsurf format
|
|
79
|
+
claudectx convert --to cursor
|
|
80
|
+
|
|
81
|
+
# Export your usage data for team-wide cost attribution
|
|
82
|
+
claudectx teams export
|
|
62
83
|
```
|
|
63
84
|
|
|
64
85
|
## Installation
|
|
@@ -229,6 +250,116 @@ DAILY USAGE
|
|
|
229
250
|
|
|
230
251
|
---
|
|
231
252
|
|
|
253
|
+
### `claudectx budget` — Know the cost before you start
|
|
254
|
+
|
|
255
|
+
Before running a big task, see exactly which files will be read, how many tokens they'll consume, and what it'll cost.
|
|
256
|
+
|
|
257
|
+
```bash
|
|
258
|
+
claudectx budget "src/**/*.ts" # Estimate all TypeScript files
|
|
259
|
+
claudectx budget "**/*.py" --threshold 20000 # Warn if total exceeds 20K tokens
|
|
260
|
+
claudectx budget "src/**" --model opus --json # JSON output for scripting
|
|
261
|
+
```
|
|
262
|
+
|
|
263
|
+
Output shows per-file token counts, cache hit likelihood (based on your recent reads), total cost estimate, and `.claudeignore` recommendations for files that are large but rarely useful.
|
|
264
|
+
|
|
265
|
+
---
|
|
266
|
+
|
|
267
|
+
### `claudectx warmup` — Pre-warm the prompt cache
|
|
268
|
+
|
|
269
|
+
Start each session with a cache hit instead of a full miss. Sends a silent priming request to Anthropic so your CLAUDE.md is cached before Claude Code touches it.
|
|
270
|
+
|
|
271
|
+
```bash
|
|
272
|
+
claudectx warmup --api-key $ANTHROPIC_API_KEY # 5-min cache TTL (default)
|
|
273
|
+
claudectx warmup --ttl 60 --api-key $ANTHROPIC_API_KEY # 60-min extended TTL
|
|
274
|
+
claudectx warmup --cron "0 9 * * 1-5" --api-key ... # Install as daily cron job
|
|
275
|
+
claudectx warmup --json # Output result as JSON
|
|
276
|
+
```
|
|
277
|
+
|
|
278
|
+
Reports tokens warmed, write cost, savings per cache hit, and break-even request count.
|
|
279
|
+
|
|
280
|
+
---
|
|
281
|
+
|
|
282
|
+
### `claudectx drift` — CLAUDE.md stays fresh, not stale
|
|
283
|
+
|
|
284
|
+
Over time CLAUDE.md accumulates dead references and sections nobody reads. `drift` finds them.
|
|
285
|
+
|
|
286
|
+
```bash
|
|
287
|
+
claudectx drift # Scan current project
|
|
288
|
+
claudectx drift --days 14 # Use 14-day read window (default: 30)
|
|
289
|
+
claudectx drift --fix # Interactively remove flagged lines
|
|
290
|
+
claudectx drift --json # JSON output
|
|
291
|
+
```
|
|
292
|
+
|
|
293
|
+
Detects 4 types of drift:
|
|
294
|
+
|
|
295
|
+
| Type | Example |
|
|
296
|
+
|---|---|
|
|
297
|
+
| **Dead `@ref`** | `@src/old-service.ts` — file deleted |
|
|
298
|
+
| **Git-deleted mention** | `legacy-auth.py` appears in prose but was removed in git |
|
|
299
|
+
| **Stale section** | `## Android Setup` — zero reads in 30 days |
|
|
300
|
+
| **Dead inline path** | `src/utils/helper.py` mentioned in text, no longer exists |
|
|
301
|
+
|
|
302
|
+
---
|
|
303
|
+
|
|
304
|
+
### `claudectx hooks` — Hook marketplace
|
|
305
|
+
|
|
306
|
+
Install named, pre-configured hooks beyond the basic read logger.
|
|
307
|
+
|
|
308
|
+
```bash
|
|
309
|
+
claudectx hooks list # Show all available hooks
|
|
310
|
+
claudectx hooks add auto-compress --config apiKey=sk-... # Install a hook
|
|
311
|
+
claudectx hooks remove slack-digest # Remove an installed hook
|
|
312
|
+
claudectx hooks status # Show what's installed
|
|
313
|
+
```
|
|
314
|
+
|
|
315
|
+
**Built-in hooks:**
|
|
316
|
+
|
|
317
|
+
| Hook | Trigger | What it does |
|
|
318
|
+
|---|---|---|
|
|
319
|
+
| `auto-compress` | PostToolUse (Read) | Runs `claudectx compress` after each session |
|
|
320
|
+
| `daily-budget` | PreToolUse | Checks token budget before each tool call |
|
|
321
|
+
| `slack-digest` | Stop | Posts session report to a Slack webhook |
|
|
322
|
+
| `session-warmup` | PostToolUse (Read) | Re-warms the cache after each read |
|
|
323
|
+
|
|
324
|
+
---
|
|
325
|
+
|
|
326
|
+
### `claudectx convert` — Use your CLAUDE.md everywhere
|
|
327
|
+
|
|
328
|
+
You wrote great instructions for Claude. Use them with Cursor, Copilot, or Windsurf too.
|
|
329
|
+
|
|
330
|
+
```bash
|
|
331
|
+
claudectx convert --to cursor # → .cursor/rules/<section>.mdc (one per ## section)
|
|
332
|
+
claudectx convert --to copilot # → .github/copilot-instructions.md
|
|
333
|
+
claudectx convert --to windsurf # → .windsurfrules
|
|
334
|
+
claudectx convert --to cursor --dry-run # Preview without writing
|
|
335
|
+
```
|
|
336
|
+
|
|
337
|
+
Each `##` section in CLAUDE.md becomes a separate Cursor `.mdc` file with `alwaysApply: true` frontmatter. `@file` references are stripped for assistants that don't support them.
|
|
338
|
+
|
|
339
|
+
---
|
|
340
|
+
|
|
341
|
+
### `claudectx teams` — Multi-developer cost attribution
|
|
342
|
+
|
|
343
|
+
See where the money goes across your whole team — without sharing session content.
|
|
344
|
+
|
|
345
|
+
```bash
|
|
346
|
+
# Step 1: each developer runs this on their own machine
|
|
347
|
+
claudectx teams export
|
|
348
|
+
|
|
349
|
+
# Step 2: collect the JSON files in a shared directory, then aggregate
|
|
350
|
+
claudectx teams aggregate --dir ./reports/
|
|
351
|
+
|
|
352
|
+
# Optional: copy your export to a shared location
|
|
353
|
+
claudectx teams share --to /shared/reports/
|
|
354
|
+
|
|
355
|
+
# Anonymize for review
|
|
356
|
+
claudectx teams aggregate --dir ./reports/ --anonymize
|
|
357
|
+
```
|
|
358
|
+
|
|
359
|
+
Output shows per-developer spend, cache hit rate, avg request size, and top shared files. Exports are lightweight JSON — no session content, no prompts, just aggregated token counts.
|
|
360
|
+
|
|
361
|
+
---
|
|
362
|
+
|
|
232
363
|
## How it all fits together
|
|
233
364
|
|
|
234
365
|
```
|
|
@@ -261,7 +392,7 @@ git clone https://github.com/Horilla/claudectx.git
|
|
|
261
392
|
cd claudectx
|
|
262
393
|
npm install
|
|
263
394
|
npm run build
|
|
264
|
-
npm test #
|
|
395
|
+
npm test # 278 tests, should all pass
|
|
265
396
|
npm run lint # 0 errors expected
|
|
266
397
|
```
|
|
267
398
|
|
package/dist/index.js
CHANGED
|
@@ -2243,12 +2243,12 @@ async function handleLogStdin() {
|
|
|
2243
2243
|
}
|
|
2244
2244
|
}
|
|
2245
2245
|
function readStdin() {
|
|
2246
|
-
return new Promise((
|
|
2246
|
+
return new Promise((resolve12) => {
|
|
2247
2247
|
let data = "";
|
|
2248
2248
|
process.stdin.setEncoding("utf-8");
|
|
2249
2249
|
process.stdin.on("data", (chunk) => data += chunk);
|
|
2250
|
-
process.stdin.on("end", () =>
|
|
2251
|
-
setTimeout(() =>
|
|
2250
|
+
process.stdin.on("end", () => resolve12(data));
|
|
2251
|
+
setTimeout(() => resolve12(data), 500);
|
|
2252
2252
|
});
|
|
2253
2253
|
}
|
|
2254
2254
|
|
|
@@ -3241,29 +3241,65 @@ async function executeWarmup(claudeMdContent, model, ttl, client) {
|
|
|
3241
3241
|
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
3242
3242
|
};
|
|
3243
3243
|
}
|
|
3244
|
-
|
|
3244
|
+
function isValidCronExpr(expr) {
|
|
3245
|
+
const parts = expr.trim().split(/\s+/);
|
|
3246
|
+
if (parts.length < 5 || parts.length > 6) return false;
|
|
3247
|
+
return parts.every((p) => /^[0-9*,/\-]+$/.test(p));
|
|
3248
|
+
}
|
|
3249
|
+
async function installCron(cronExpr) {
|
|
3250
|
+
if (!isValidCronExpr(cronExpr)) {
|
|
3251
|
+
process.stderr.write(
|
|
3252
|
+
`Error: invalid cron expression "${cronExpr}".
|
|
3253
|
+
Example: "0 9 * * 1-5" (weekdays at 9am)
|
|
3254
|
+
`
|
|
3255
|
+
);
|
|
3256
|
+
process.exit(1);
|
|
3257
|
+
}
|
|
3245
3258
|
const { execSync: execSync3 } = await import("child_process");
|
|
3246
|
-
const command = `claudectx warmup
|
|
3259
|
+
const command = `claudectx warmup`;
|
|
3247
3260
|
const cronLine = `${cronExpr} ${command}`;
|
|
3261
|
+
const marker = "# claudectx warmup";
|
|
3248
3262
|
try {
|
|
3249
3263
|
let existing = "";
|
|
3250
3264
|
try {
|
|
3251
|
-
existing = execSync3("crontab -l
|
|
3265
|
+
existing = execSync3("crontab -l", {
|
|
3266
|
+
encoding: "utf-8",
|
|
3267
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
3268
|
+
});
|
|
3252
3269
|
} catch {
|
|
3253
3270
|
existing = "";
|
|
3254
3271
|
}
|
|
3255
|
-
if (existing.includes(
|
|
3256
|
-
process.stdout.write("Cron job already installed for claudectx warmup.\n");
|
|
3272
|
+
if (existing.includes(marker)) {
|
|
3273
|
+
process.stdout.write(" Cron job already installed for claudectx warmup.\n");
|
|
3257
3274
|
return;
|
|
3258
3275
|
}
|
|
3259
|
-
const newCrontab = existing.trimEnd() +
|
|
3260
|
-
|
|
3261
|
-
|
|
3276
|
+
const newCrontab = existing.trimEnd() + `
|
|
3277
|
+
${marker}
|
|
3278
|
+
${cronLine}
|
|
3279
|
+
`;
|
|
3280
|
+
const { writeFileSync: writeFileSync11, unlinkSync: unlinkSync2 } = await import("fs");
|
|
3281
|
+
const { tmpdir } = await import("os");
|
|
3282
|
+
const { join: join17 } = await import("path");
|
|
3283
|
+
const tmpFile = join17(tmpdir(), `claudectx-cron-${Date.now()}.txt`);
|
|
3284
|
+
try {
|
|
3285
|
+
writeFileSync11(tmpFile, newCrontab, "utf-8");
|
|
3286
|
+
execSync3(`crontab ${tmpFile}`, { stdio: ["pipe", "pipe", "pipe"] });
|
|
3287
|
+
} finally {
|
|
3288
|
+
try {
|
|
3289
|
+
unlinkSync2(tmpFile);
|
|
3290
|
+
} catch {
|
|
3291
|
+
}
|
|
3292
|
+
}
|
|
3293
|
+
process.stdout.write(` \u2713 Cron job installed: ${cronLine}
|
|
3262
3294
|
`);
|
|
3295
|
+
process.stdout.write(" Note: Set ANTHROPIC_API_KEY in your cron environment (e.g. via ~/.profile).\n");
|
|
3263
3296
|
} catch {
|
|
3264
|
-
process.stdout.write(
|
|
3297
|
+
process.stdout.write(
|
|
3298
|
+
` Could not install cron automatically. Add this line manually with "crontab -e":
|
|
3299
|
+
ANTHROPIC_API_KEY=<your-key>
|
|
3265
3300
|
${cronLine}
|
|
3266
|
-
`
|
|
3301
|
+
`
|
|
3302
|
+
);
|
|
3267
3303
|
}
|
|
3268
3304
|
}
|
|
3269
3305
|
async function warmupCommand(options) {
|
|
@@ -3333,7 +3369,7 @@ async function warmupCommand(options) {
|
|
|
3333
3369
|
}
|
|
3334
3370
|
process.stdout.write("\n");
|
|
3335
3371
|
if (options.cron) {
|
|
3336
|
-
await installCron(options.cron
|
|
3372
|
+
await installCron(options.cron);
|
|
3337
3373
|
}
|
|
3338
3374
|
}
|
|
3339
3375
|
|
|
@@ -3392,15 +3428,15 @@ async function findGitDeletedMentions(content, projectRoot) {
|
|
|
3392
3428
|
for (let i = 0; i < lines.length; i++) {
|
|
3393
3429
|
const line = lines[i];
|
|
3394
3430
|
for (const deleted of deletedFiles) {
|
|
3395
|
-
const
|
|
3396
|
-
if (line.includes(
|
|
3431
|
+
const basename8 = path22.basename(deleted);
|
|
3432
|
+
if (line.includes(basename8) || line.includes(deleted)) {
|
|
3397
3433
|
issues.push({
|
|
3398
3434
|
type: "git-deleted",
|
|
3399
3435
|
line: i + 1,
|
|
3400
3436
|
text: line.trim(),
|
|
3401
3437
|
severity: "warning",
|
|
3402
3438
|
estimatedTokenWaste: countTokens(line),
|
|
3403
|
-
suggestion: `References "${
|
|
3439
|
+
suggestion: `References "${basename8}" which was deleted from git. Consider removing this mention.`
|
|
3404
3440
|
});
|
|
3405
3441
|
break;
|
|
3406
3442
|
}
|
|
@@ -3607,11 +3643,34 @@ async function applyFix(claudeMdPath, issues) {
|
|
|
3607
3643
|
const lines = content.split("\n");
|
|
3608
3644
|
const lineSet = new Set(selectedLines.map((l) => l - 1));
|
|
3609
3645
|
const newLines = lines.filter((_, i) => !lineSet.has(i));
|
|
3610
|
-
|
|
3646
|
+
const newContent = newLines.join("\n");
|
|
3647
|
+
const backupPath = `${claudeMdPath}.bak`;
|
|
3648
|
+
fs20.writeFileSync(backupPath, content, "utf-8");
|
|
3649
|
+
const os5 = await import("os");
|
|
3650
|
+
const tmpPath = `${claudeMdPath}.tmp-${Date.now()}`;
|
|
3651
|
+
try {
|
|
3652
|
+
fs20.writeFileSync(tmpPath, newContent, "utf-8");
|
|
3653
|
+
fs20.renameSync(tmpPath, claudeMdPath);
|
|
3654
|
+
} catch (err) {
|
|
3655
|
+
try {
|
|
3656
|
+
fs20.copyFileSync(backupPath, claudeMdPath);
|
|
3657
|
+
} catch {
|
|
3658
|
+
}
|
|
3659
|
+
try {
|
|
3660
|
+
fs20.unlinkSync(tmpPath);
|
|
3661
|
+
} catch {
|
|
3662
|
+
}
|
|
3663
|
+
process.stderr.write(`Error writing CLAUDE.md: ${err instanceof Error ? err.message : String(err)}
|
|
3664
|
+
`);
|
|
3665
|
+
process.exit(1);
|
|
3666
|
+
}
|
|
3611
3667
|
process.stdout.write(`
|
|
3612
|
-
\u2713 Removed ${selectedLines.length} line(s) from ${claudeMdPath}
|
|
3668
|
+
\u2713 Removed ${selectedLines.length} line(s) from ${path23.basename(claudeMdPath)}
|
|
3669
|
+
`);
|
|
3670
|
+
process.stdout.write(` \u2713 Backup saved to ${backupPath}
|
|
3613
3671
|
|
|
3614
3672
|
`);
|
|
3673
|
+
void os5;
|
|
3615
3674
|
}
|
|
3616
3675
|
|
|
3617
3676
|
// src/commands/hooks.ts
|
|
@@ -3627,10 +3686,10 @@ var HOOK_REGISTRY = [
|
|
|
3627
3686
|
description: "Compress session when token count exceeds threshold",
|
|
3628
3687
|
triggerEvent: "PostToolUse",
|
|
3629
3688
|
matcher: "Read",
|
|
3630
|
-
|
|
3689
|
+
// API key is read from ANTHROPIC_API_KEY env var — never stored in settings.json
|
|
3690
|
+
commandTemplate: "claudectx compress --auto",
|
|
3631
3691
|
configSchema: {
|
|
3632
|
-
threshold: { type: "number", default: 5e4, description: "Token threshold to trigger compression" }
|
|
3633
|
-
apiKey: { type: "string", description: "Anthropic API key for AI summarization", required: true }
|
|
3692
|
+
threshold: { type: "number", default: 5e4, description: "Token threshold to trigger compression" }
|
|
3634
3693
|
},
|
|
3635
3694
|
category: "compression"
|
|
3636
3695
|
},
|
|
@@ -3663,10 +3722,9 @@ var HOOK_REGISTRY = [
|
|
|
3663
3722
|
description: "Pre-warm the Anthropic prompt cache on each session start",
|
|
3664
3723
|
triggerEvent: "PostToolUse",
|
|
3665
3724
|
matcher: "Read",
|
|
3666
|
-
|
|
3667
|
-
|
|
3668
|
-
|
|
3669
|
-
},
|
|
3725
|
+
// API key is read from ANTHROPIC_API_KEY env var — never stored in settings.json
|
|
3726
|
+
commandTemplate: "claudectx warmup",
|
|
3727
|
+
configSchema: {},
|
|
3670
3728
|
category: "warmup"
|
|
3671
3729
|
}
|
|
3672
3730
|
];
|
|
@@ -3695,9 +3753,18 @@ function buildHookEntry(def, config) {
|
|
|
3695
3753
|
// src/commands/hooks.ts
|
|
3696
3754
|
function readInstalledHooks(projectRoot) {
|
|
3697
3755
|
const settingsPath = path24.join(projectRoot, ".claude", "settings.local.json");
|
|
3756
|
+
if (!fs21.existsSync(settingsPath)) return {};
|
|
3698
3757
|
try {
|
|
3699
3758
|
return JSON.parse(fs21.readFileSync(settingsPath, "utf-8"));
|
|
3700
3759
|
} catch {
|
|
3760
|
+
process.stderr.write(
|
|
3761
|
+
`Warning: ${settingsPath} exists but contains invalid JSON. Existing settings will be preserved as a backup.
|
|
3762
|
+
`
|
|
3763
|
+
);
|
|
3764
|
+
try {
|
|
3765
|
+
fs21.copyFileSync(settingsPath, `${settingsPath}.bak`);
|
|
3766
|
+
} catch {
|
|
3767
|
+
}
|
|
3701
3768
|
return {};
|
|
3702
3769
|
}
|
|
3703
3770
|
}
|
|
@@ -3784,6 +3851,16 @@ async function hooksAdd(name, projectRoot, configPairs) {
|
|
|
3784
3851
|
`);
|
|
3785
3852
|
process.exit(1);
|
|
3786
3853
|
}
|
|
3854
|
+
const sensitiveKeys = Object.keys(config).filter(
|
|
3855
|
+
(k) => /key|token|secret|password|webhook/i.test(k)
|
|
3856
|
+
);
|
|
3857
|
+
if (sensitiveKeys.length > 0) {
|
|
3858
|
+
process.stderr.write(
|
|
3859
|
+
`Warning: config field(s) [${sensitiveKeys.join(", ")}] will be stored in plain text in
|
|
3860
|
+
.claude/settings.local.json \u2014 ensure this file is in your .gitignore.
|
|
3861
|
+
`
|
|
3862
|
+
);
|
|
3863
|
+
}
|
|
3787
3864
|
const entry = { ...buildHookEntry(def, config), name };
|
|
3788
3865
|
const settings = readInstalledHooks(projectRoot);
|
|
3789
3866
|
const hooksObj = settings.hooks ?? {};
|
|
@@ -3948,7 +4025,9 @@ Converting CLAUDE.md \u2192 ${files.length} Cursor rule file(s)
|
|
|
3948
4025
|
`);
|
|
3949
4026
|
for (const file of files) {
|
|
3950
4027
|
const filePath = path25.join(targetDir, file.filename);
|
|
3951
|
-
|
|
4028
|
+
const exists = fs22.existsSync(filePath);
|
|
4029
|
+
const prefix = options.dryRun ? "[dry-run] " : exists ? "[overwrite] " : "";
|
|
4030
|
+
process.stdout.write(` ${prefix}\u2192 .cursor/rules/${file.filename}
|
|
3952
4031
|
`);
|
|
3953
4032
|
if (!options.dryRun) {
|
|
3954
4033
|
fs22.mkdirSync(targetDir, { recursive: true });
|
|
@@ -3959,8 +4038,9 @@ Converting CLAUDE.md \u2192 ${files.length} Cursor rule file(s)
|
|
|
3959
4038
|
} else if (to === "copilot") {
|
|
3960
4039
|
const converted = claudeMdToCopilot(content);
|
|
3961
4040
|
const targetPath = path25.join(projectRoot, ".github", "copilot-instructions.md");
|
|
4041
|
+
const exists = fs22.existsSync(targetPath);
|
|
3962
4042
|
process.stdout.write(`
|
|
3963
|
-
Converting CLAUDE.md \u2192 .github/copilot-instructions.md
|
|
4043
|
+
Converting CLAUDE.md \u2192 .github/copilot-instructions.md${exists ? " [overwrite]" : ""}
|
|
3964
4044
|
`);
|
|
3965
4045
|
if (!options.dryRun) {
|
|
3966
4046
|
fs22.mkdirSync(path25.dirname(targetPath), { recursive: true });
|
|
@@ -3976,8 +4056,9 @@ Converting CLAUDE.md \u2192 .github/copilot-instructions.md
|
|
|
3976
4056
|
} else if (to === "windsurf") {
|
|
3977
4057
|
const converted = claudeMdToWindsurf(content);
|
|
3978
4058
|
const targetPath = path25.join(projectRoot, ".windsurfrules");
|
|
4059
|
+
const exists = fs22.existsSync(targetPath);
|
|
3979
4060
|
process.stdout.write(`
|
|
3980
|
-
Converting CLAUDE.md \u2192 .windsurfrules
|
|
4061
|
+
Converting CLAUDE.md \u2192 .windsurfrules${exists ? " [overwrite]" : ""}
|
|
3981
4062
|
`);
|
|
3982
4063
|
if (!options.dryRun) {
|
|
3983
4064
|
fs22.writeFileSync(targetPath, converted, "utf-8");
|
|
@@ -4251,7 +4332,26 @@ async function teamsShare(options) {
|
|
|
4251
4332
|
}
|
|
4252
4333
|
const latest = exportFiles[0];
|
|
4253
4334
|
const src = path27.join(storeDir, latest);
|
|
4254
|
-
|
|
4335
|
+
let destPath;
|
|
4336
|
+
try {
|
|
4337
|
+
const stat = fs24.statSync(dest);
|
|
4338
|
+
destPath = stat.isDirectory() ? path27.join(dest, latest) : dest;
|
|
4339
|
+
} catch {
|
|
4340
|
+
destPath = dest;
|
|
4341
|
+
}
|
|
4342
|
+
const destDir = path27.dirname(path27.resolve(destPath));
|
|
4343
|
+
let resolvedDir;
|
|
4344
|
+
try {
|
|
4345
|
+
resolvedDir = fs24.realpathSync(destDir);
|
|
4346
|
+
} catch {
|
|
4347
|
+
resolvedDir = destDir;
|
|
4348
|
+
}
|
|
4349
|
+
const systemDirs = ["/etc", "/bin", "/sbin", "/usr", "/var", "/proc", "/sys"];
|
|
4350
|
+
if (systemDirs.some((d) => resolvedDir === d || resolvedDir.startsWith(d + "/"))) {
|
|
4351
|
+
process.stderr.write(`Error: destination path resolves to a system directory (${resolvedDir}). Aborting.
|
|
4352
|
+
`);
|
|
4353
|
+
process.exit(1);
|
|
4354
|
+
}
|
|
4255
4355
|
fs24.copyFileSync(src, destPath);
|
|
4256
4356
|
process.stdout.write(` \u2713 Copied ${latest} \u2192 ${destPath}
|
|
4257
4357
|
|