@x6txy/ctxscope 0.2.0 → 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/CHANGELOG.md +17 -0
- package/README.md +84 -27
- package/dist/cli.js +453 -31
- package/package.json +2 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,22 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## Unreleased
|
|
4
|
+
|
|
5
|
+
### Added
|
|
6
|
+
|
|
7
|
+
- Added Agent Context Score to `ctxscope doctor`.
|
|
8
|
+
- Added diagnostic line and column metadata where rules can locate the issue.
|
|
9
|
+
- Added recommendations and safe fix metadata to diagnostics.
|
|
10
|
+
- Added `ctxscope fix` with `--dry-run`, `--json`, and `--agent` support.
|
|
11
|
+
- Added deterministic package manager normalization for context files when exactly one lockfile identifies the package manager.
|
|
12
|
+
- Added exact duplicate paragraph removal.
|
|
13
|
+
- Added `node:test` coverage for doctor metadata, scoring, and fix behavior.
|
|
14
|
+
|
|
15
|
+
### Changed
|
|
16
|
+
|
|
17
|
+
- Repositioned README around repairing stale agent instructions.
|
|
18
|
+
- `CTX102` missing script references are recommendation-only and are not autofixed by default.
|
|
19
|
+
|
|
3
20
|
## 0.2.0 - 2026-06-21
|
|
4
21
|
|
|
5
22
|
### Added
|
package/README.md
CHANGED
|
@@ -1,30 +1,31 @@
|
|
|
1
1
|
# ctxscope
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Make AI coding agents follow the right repository instructions.
|
|
4
4
|
|
|
5
|
-
`ctxscope`
|
|
5
|
+
`ctxscope` finds and fixes stale, conflicting, duplicated, and wasteful instructions across `AGENTS.md`, `CLAUDE.md`, `SKILL.md`, OpenCode skills, Cursor rules, and GitHub Copilot instructions.
|
|
6
6
|
|
|
7
|
-
It answers the
|
|
7
|
+
It answers the questions every agent-heavy repo eventually has:
|
|
8
8
|
|
|
9
9
|
- What context files exist?
|
|
10
10
|
- How much context do they add?
|
|
11
11
|
- Which agent is each file probably for?
|
|
12
|
-
- Which files are suspiciously large,
|
|
12
|
+
- Which files are suspiciously large, stale, duplicated, or wrong?
|
|
13
13
|
- Which context problems should fail CI?
|
|
14
|
+
- Which problems can be fixed safely right now?
|
|
14
15
|
|
|
15
16
|
## Install
|
|
16
17
|
|
|
17
18
|
Run without installing:
|
|
18
19
|
|
|
19
20
|
```bash
|
|
20
|
-
npx @x6txy/ctxscope
|
|
21
|
+
npx @x6txy/ctxscope doctor
|
|
21
22
|
```
|
|
22
23
|
|
|
23
24
|
Or install globally:
|
|
24
25
|
|
|
25
26
|
```bash
|
|
26
27
|
npm install -g @x6txy/ctxscope
|
|
27
|
-
ctxscope
|
|
28
|
+
ctxscope doctor
|
|
28
29
|
```
|
|
29
30
|
|
|
30
31
|
## Usage
|
|
@@ -32,13 +33,16 @@ ctxscope scan
|
|
|
32
33
|
```bash
|
|
33
34
|
ctxscope --help
|
|
34
35
|
ctxscope --version
|
|
35
|
-
ctxscope init
|
|
36
|
-
ctxscope scan [path]
|
|
37
|
-
ctxscope scan --agent <all|codex|opencode|claude|generic>
|
|
38
|
-
ctxscope scan --json
|
|
39
|
-
ctxscope doctor [path]
|
|
40
|
-
ctxscope doctor --ci
|
|
41
|
-
ctxscope doctor --json
|
|
36
|
+
ctxscope init
|
|
37
|
+
ctxscope scan [path]
|
|
38
|
+
ctxscope scan --agent <all|codex|opencode|claude|generic>
|
|
39
|
+
ctxscope scan --json
|
|
40
|
+
ctxscope doctor [path]
|
|
41
|
+
ctxscope doctor --ci
|
|
42
|
+
ctxscope doctor --json
|
|
43
|
+
ctxscope fix [path]
|
|
44
|
+
ctxscope fix --dry-run
|
|
45
|
+
ctxscope fix --json
|
|
42
46
|
```
|
|
43
47
|
|
|
44
48
|
Examples:
|
|
@@ -50,28 +54,57 @@ ctxscope scan --agent codex
|
|
|
50
54
|
ctxscope scan --agent opencode --json
|
|
51
55
|
ctxscope init
|
|
52
56
|
ctxscope doctor --ci
|
|
57
|
+
ctxscope fix --dry-run
|
|
58
|
+
ctxscope fix
|
|
53
59
|
```
|
|
54
60
|
|
|
55
61
|
## Output
|
|
56
62
|
|
|
57
63
|
```text
|
|
58
|
-
ctxscope
|
|
64
|
+
ctxscope doctor
|
|
59
65
|
|
|
60
66
|
Agent all
|
|
61
67
|
Target /repo
|
|
68
|
+
Status fail
|
|
62
69
|
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
70
|
+
Agent Context Score 64/100
|
|
71
|
+
Correctness 56
|
|
72
|
+
Freshness 90
|
|
73
|
+
Efficiency 76
|
|
74
|
+
Consistency 78
|
|
75
|
+
Coverage 100
|
|
68
76
|
|
|
69
77
|
Summary
|
|
70
|
-
|
|
78
|
+
4 files, ~9,200 tokens, 2 errors, 3 warnings
|
|
79
|
+
Run ctxscope fix to apply 1 safe fix.
|
|
80
|
+
|
|
81
|
+
Errors (2)
|
|
82
|
+
ERROR CTX101 AGENTS.md:18
|
|
83
|
+
conflicting package managers: npm, pnpm
|
|
84
|
+
Fix: Normalize package manager commands
|
|
71
85
|
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
86
|
+
ERROR CTX102 AGENTS.md:24
|
|
87
|
+
references missing package script: test:e2e
|
|
88
|
+
Recommendation: remove the command or replace it with an existing package.json script
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
`ctxscope fix` applies only deterministic safe fixes:
|
|
92
|
+
|
|
93
|
+
```text
|
|
94
|
+
ctxscope fix
|
|
95
|
+
|
|
96
|
+
Target /repo
|
|
97
|
+
Mode write
|
|
98
|
+
|
|
99
|
+
Summary
|
|
100
|
+
Agent Context Score 64 -> 82
|
|
101
|
+
Applied 1 safe fix
|
|
102
|
+
Skipped 1 fix
|
|
103
|
+
Saved ~420 tokens per session
|
|
104
|
+
|
|
105
|
+
Applied (1)
|
|
106
|
+
CTX101 AGENTS.md
|
|
107
|
+
Normalize package manager commands to pnpm
|
|
75
108
|
```
|
|
76
109
|
|
|
77
110
|
## Supported Agents
|
|
@@ -119,6 +152,8 @@ ctxscope doctor --json
|
|
|
119
152
|
|
|
120
153
|
`--ci` exits with code `1` when any diagnostic has severity `error`.
|
|
121
154
|
|
|
155
|
+
`doctor` also reports Agent Context Score, line numbers, safe fix hints, and recommendations.
|
|
156
|
+
|
|
122
157
|
Example output:
|
|
123
158
|
|
|
124
159
|
```text
|
|
@@ -130,15 +165,37 @@ Status fail
|
|
|
130
165
|
|
|
131
166
|
Summary
|
|
132
167
|
4 files, ~9,200 tokens, 2 errors, 1 warnings
|
|
168
|
+
Run ctxscope fix to apply 1 safe fix.
|
|
133
169
|
|
|
134
170
|
Errors (2)
|
|
135
|
-
ERROR CTX101 AGENTS.md
|
|
171
|
+
ERROR CTX101 AGENTS.md:18
|
|
136
172
|
conflicting package managers: npm, pnpm
|
|
173
|
+
Fix: Normalize package manager commands
|
|
137
174
|
|
|
138
|
-
ERROR CTX102 AGENTS.md
|
|
175
|
+
ERROR CTX102 AGENTS.md:24
|
|
139
176
|
references missing package script: test:e2e
|
|
177
|
+
Recommendation: remove the command or replace it with an existing package.json script
|
|
140
178
|
```
|
|
141
179
|
|
|
180
|
+
## Fix
|
|
181
|
+
|
|
182
|
+
Use `fix` to apply deterministic repairs:
|
|
183
|
+
|
|
184
|
+
```bash
|
|
185
|
+
ctxscope fix --dry-run
|
|
186
|
+
ctxscope fix
|
|
187
|
+
ctxscope fix --json
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
Safe v0.3 autofixes:
|
|
191
|
+
|
|
192
|
+
- Normalize package manager command prefixes when exactly one lockfile identifies the repo package manager.
|
|
193
|
+
- Remove exact duplicate paragraphs after the first occurrence.
|
|
194
|
+
|
|
195
|
+
Recommendation-only in v0.3:
|
|
196
|
+
|
|
197
|
+
- `CTX102` missing package script references are not autofixed because replacing commands can break workflows.
|
|
198
|
+
|
|
142
199
|
## Config
|
|
143
200
|
|
|
144
201
|
Create a default config:
|
|
@@ -207,9 +264,9 @@ Shape:
|
|
|
207
264
|
## Limitations
|
|
208
265
|
|
|
209
266
|
- Token counts are estimates: `ceil(character_count / 4)`.
|
|
210
|
-
-
|
|
267
|
+
- ctxscope is discovery-based, not real session tracing.
|
|
211
268
|
- Semantic checks are intentionally conservative. They inspect explicit commands and context text, not model behavior.
|
|
212
|
-
- `
|
|
269
|
+
- `trace`, GitHub Actions, AI fixes, and dashboards are future work.
|
|
213
270
|
|
|
214
271
|
## License
|
|
215
272
|
|
package/dist/cli.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
// src/cli.ts
|
|
4
|
-
import { readFileSync as
|
|
4
|
+
import { readFileSync as readFileSync7 } from "fs";
|
|
5
5
|
|
|
6
6
|
// src/config.ts
|
|
7
7
|
import { existsSync, readFileSync } from "fs";
|
|
@@ -235,7 +235,11 @@ function createDiagnostic(input, config) {
|
|
|
235
235
|
code: input.code,
|
|
236
236
|
severity,
|
|
237
237
|
path: input.path,
|
|
238
|
-
message: input.message
|
|
238
|
+
message: input.message,
|
|
239
|
+
line: input.line,
|
|
240
|
+
column: input.column,
|
|
241
|
+
recommendation: input.recommendation,
|
|
242
|
+
fix: input.fix
|
|
239
243
|
};
|
|
240
244
|
}
|
|
241
245
|
function sortDiagnostics(diagnostics) {
|
|
@@ -245,6 +249,29 @@ function sortDiagnostics(diagnostics) {
|
|
|
245
249
|
});
|
|
246
250
|
}
|
|
247
251
|
|
|
252
|
+
// src/locations.ts
|
|
253
|
+
function locationFromOffset(content, offset) {
|
|
254
|
+
const safeOffset = Math.max(0, Math.min(offset, content.length));
|
|
255
|
+
let line = 1;
|
|
256
|
+
let column = 1;
|
|
257
|
+
for (let index = 0; index < safeOffset; index += 1) {
|
|
258
|
+
if (content[index] === "\n") {
|
|
259
|
+
line += 1;
|
|
260
|
+
column = 1;
|
|
261
|
+
} else {
|
|
262
|
+
column += 1;
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
return { line, column };
|
|
266
|
+
}
|
|
267
|
+
function firstRegexLocation(content, pattern) {
|
|
268
|
+
const match = pattern.exec(content);
|
|
269
|
+
if (!match || match.index === void 0) {
|
|
270
|
+
return void 0;
|
|
271
|
+
}
|
|
272
|
+
return locationFromOffset(content, match.index);
|
|
273
|
+
}
|
|
274
|
+
|
|
248
275
|
// src/warnings.ts
|
|
249
276
|
var MARKER_PATTERN = /\b(TODO|FIXME|OBSOLETE)\b/i;
|
|
250
277
|
var MARKDOWN_LINK_PATTERN = /(?<!!)\[[^\]\n]+\]\(([^)]+)\)/g;
|
|
@@ -282,11 +309,15 @@ function collectFileDiagnostics(input, config) {
|
|
|
282
309
|
}, config));
|
|
283
310
|
}
|
|
284
311
|
if (MARKER_PATTERN.test(content)) {
|
|
312
|
+
const location = firstRegexLocation(content, new RegExp(MARKER_PATTERN.source, MARKER_PATTERN.flags));
|
|
285
313
|
pushDiagnostic(diagnostics, createDiagnostic({
|
|
286
314
|
code: "CTX005",
|
|
287
315
|
defaultSeverity: "warn",
|
|
288
316
|
path: file.path,
|
|
289
|
-
message: "contains TODO, FIXME, or obsolete markers"
|
|
317
|
+
message: "contains TODO, FIXME, or obsolete markers",
|
|
318
|
+
line: location == null ? void 0 : location.line,
|
|
319
|
+
column: location == null ? void 0 : location.column,
|
|
320
|
+
recommendation: "remove stale TODO, FIXME, or obsolete instructions from agent context"
|
|
290
321
|
}, config));
|
|
291
322
|
}
|
|
292
323
|
diagnostics.push(...collectStaleLinkDiagnostics(input.absolutePath, file.path, content, config));
|
|
@@ -346,29 +377,55 @@ function collectStaleLinkDiagnostics(absolutePath, displayPath, content, config)
|
|
|
346
377
|
code: "CTX003",
|
|
347
378
|
defaultSeverity: "warn",
|
|
348
379
|
path: displayPath,
|
|
349
|
-
message: `links to missing file: ${href}
|
|
380
|
+
message: `links to missing file: ${href}`,
|
|
381
|
+
line: match.index === void 0 ? void 0 : locationFromOffset(content, match.index).line,
|
|
382
|
+
column: match.index === void 0 ? void 0 : locationFromOffset(content, match.index).column,
|
|
383
|
+
recommendation: "remove the stale link or update it to an existing file"
|
|
350
384
|
}, config));
|
|
351
385
|
}
|
|
352
386
|
}
|
|
353
387
|
return diagnostics;
|
|
354
388
|
}
|
|
355
389
|
function collectRepeatedParagraphDiagnostics(displayPath, content, config) {
|
|
356
|
-
const paragraphs = content
|
|
390
|
+
const paragraphs = collectParagraphs(content);
|
|
357
391
|
const seen = /* @__PURE__ */ new Set();
|
|
358
392
|
for (const paragraph of paragraphs) {
|
|
359
|
-
if (seen.has(paragraph)) {
|
|
393
|
+
if (seen.has(paragraph.normalized)) {
|
|
394
|
+
const location = locationFromOffset(content, paragraph.offset);
|
|
360
395
|
const diagnostic = createDiagnostic({
|
|
361
396
|
code: "CTX006",
|
|
362
397
|
defaultSeverity: "warn",
|
|
363
398
|
path: displayPath,
|
|
364
|
-
message: "contains a repeated paragraph"
|
|
399
|
+
message: "contains a repeated paragraph",
|
|
400
|
+
line: location.line,
|
|
401
|
+
column: location.column,
|
|
402
|
+
fix: {
|
|
403
|
+
title: "Remove repeated paragraph",
|
|
404
|
+
kind: "delete",
|
|
405
|
+
safe: true
|
|
406
|
+
}
|
|
365
407
|
}, config);
|
|
366
408
|
return diagnostic ? [diagnostic] : [];
|
|
367
409
|
}
|
|
368
|
-
seen.add(paragraph);
|
|
410
|
+
seen.add(paragraph.normalized);
|
|
369
411
|
}
|
|
370
412
|
return [];
|
|
371
413
|
}
|
|
414
|
+
function collectParagraphs(content) {
|
|
415
|
+
var _a;
|
|
416
|
+
const paragraphs = [];
|
|
417
|
+
const pattern = /(^|\n)([^\S\r\n]*\S[\s\S]*?)(?=\n\s*\n|$)/g;
|
|
418
|
+
for (const match of content.matchAll(pattern)) {
|
|
419
|
+
const raw = match[2] ?? "";
|
|
420
|
+
const trimmedStart = raw.search(/\S/);
|
|
421
|
+
const offset = (match.index ?? 0) + (((_a = match[1]) == null ? void 0 : _a.length) ?? 0) + Math.max(trimmedStart, 0);
|
|
422
|
+
const normalized = raw.trim().replace(/\s+/g, " ");
|
|
423
|
+
if (normalized.length >= 40) {
|
|
424
|
+
paragraphs.push({ normalized, offset });
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
return paragraphs;
|
|
428
|
+
}
|
|
372
429
|
function shouldSkipLink(href) {
|
|
373
430
|
return href.startsWith("http://") || href.startsWith("https://") || href.startsWith("mailto:") || href.startsWith("#");
|
|
374
431
|
}
|
|
@@ -428,13 +485,14 @@ function collectBudgetDiagnostics(scan, config) {
|
|
|
428
485
|
code: "CTX105",
|
|
429
486
|
defaultSeverity: "error",
|
|
430
487
|
path: scan.target,
|
|
431
|
-
message: `total context is ~${scan.totalTokens} tokens, budget is ${config.maxTokens}
|
|
488
|
+
message: `total context is ~${scan.totalTokens} tokens, budget is ${config.maxTokens}`,
|
|
489
|
+
recommendation: "remove duplicated or low-value instructions, or raise maxTokens in ctxscope.config.json"
|
|
432
490
|
}, config);
|
|
433
491
|
return diagnostic ? [diagnostic] : [];
|
|
434
492
|
}
|
|
435
493
|
|
|
436
494
|
// src/rules/package-manager.ts
|
|
437
|
-
import { readFileSync as readFileSync4, statSync as statSync3 } from "fs";
|
|
495
|
+
import { existsSync as existsSync3, readFileSync as readFileSync4, statSync as statSync3 } from "fs";
|
|
438
496
|
import { dirname as dirname3, resolve as resolve5 } from "path";
|
|
439
497
|
var PACKAGE_MANAGER_PATTERNS = [
|
|
440
498
|
["npm", /\bnpm\s+(?:run\s+)?[a-z0-9:_-]+\b/i],
|
|
@@ -442,22 +500,46 @@ var PACKAGE_MANAGER_PATTERNS = [
|
|
|
442
500
|
["yarn", /\byarn\s+(?:run\s+)?[a-z0-9:_-]+\b/i],
|
|
443
501
|
["bun", /\bbun\s+(?:run\s+)?[a-z0-9:_-]+\b/i]
|
|
444
502
|
];
|
|
503
|
+
var LOCKFILES = [
|
|
504
|
+
["pnpm", "pnpm-lock.yaml"],
|
|
505
|
+
["npm", "package-lock.json"],
|
|
506
|
+
["yarn", "yarn.lock"],
|
|
507
|
+
["bun", "bun.lock"],
|
|
508
|
+
["bun", "bun.lockb"]
|
|
509
|
+
];
|
|
445
510
|
function collectPackageManagerDiagnostics(target, files, config) {
|
|
446
511
|
const root = getScanRoot2(target);
|
|
447
512
|
const mentions = files.flatMap((file) => collectPackageManagerMentions(root, file));
|
|
448
513
|
const managers = [...new Set(mentions.map((mention) => mention.manager))].sort();
|
|
514
|
+
const lockfileManager = detectPackageManagerFromLockfiles(root);
|
|
449
515
|
if (managers.length < 2) {
|
|
450
516
|
return [];
|
|
451
517
|
}
|
|
452
518
|
const involvedPaths = [...new Set(mentions.map((mention) => mention.path))].sort();
|
|
453
|
-
const diagnostics = involvedPaths.map((path) =>
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
519
|
+
const diagnostics = involvedPaths.map((path) => {
|
|
520
|
+
const mention = mentions.find((candidate) => candidate.path === path);
|
|
521
|
+
return createDiagnostic({
|
|
522
|
+
code: "CTX101",
|
|
523
|
+
defaultSeverity: "error",
|
|
524
|
+
path,
|
|
525
|
+
message: `conflicting package managers: ${managers.join(", ")}`,
|
|
526
|
+
line: mention == null ? void 0 : mention.line,
|
|
527
|
+
column: mention == null ? void 0 : mention.column,
|
|
528
|
+
recommendation: "use one package manager consistently in agent instructions",
|
|
529
|
+
fix: lockfileManager ? {
|
|
530
|
+
title: "Normalize package manager commands",
|
|
531
|
+
kind: "replace",
|
|
532
|
+
safe: true
|
|
533
|
+
} : void 0
|
|
534
|
+
}, config);
|
|
535
|
+
}).filter((diagnostic) => diagnostic !== null);
|
|
459
536
|
return sortDiagnostics(diagnostics);
|
|
460
537
|
}
|
|
538
|
+
function detectPackageManagerFromLockfiles(root) {
|
|
539
|
+
const detected = LOCKFILES.filter(([, lockfile]) => existsSync3(resolve5(root, lockfile))).map(([manager]) => manager);
|
|
540
|
+
const unique = [...new Set(detected)];
|
|
541
|
+
return unique.length === 1 ? unique[0] ?? null : null;
|
|
542
|
+
}
|
|
461
543
|
function collectPackageManagerMentions(root, file) {
|
|
462
544
|
if (file.skippedBinary) {
|
|
463
545
|
return [];
|
|
@@ -466,8 +548,9 @@ function collectPackageManagerMentions(root, file) {
|
|
|
466
548
|
const content = readFileSync4(absolutePath, "utf8");
|
|
467
549
|
const mentions = [];
|
|
468
550
|
for (const [manager, pattern] of PACKAGE_MANAGER_PATTERNS) {
|
|
469
|
-
|
|
470
|
-
|
|
551
|
+
const location = firstRegexLocation(content, new RegExp(pattern.source, pattern.flags));
|
|
552
|
+
if (location) {
|
|
553
|
+
mentions.push({ manager, path: file.path, line: location.line, column: location.column });
|
|
471
554
|
}
|
|
472
555
|
}
|
|
473
556
|
return mentions;
|
|
@@ -479,7 +562,7 @@ function getScanRoot2(target) {
|
|
|
479
562
|
}
|
|
480
563
|
|
|
481
564
|
// src/rules/package-scripts.ts
|
|
482
|
-
import { existsSync as
|
|
565
|
+
import { existsSync as existsSync4, readFileSync as readFileSync5, statSync as statSync4 } from "fs";
|
|
483
566
|
import { dirname as dirname4, resolve as resolve6 } from "path";
|
|
484
567
|
var SCRIPT_PATTERNS = [
|
|
485
568
|
/\bnpm\s+run\s+([a-z0-9:_-]+)\b/gi,
|
|
@@ -509,7 +592,10 @@ function collectPackageScriptDiagnostics(target, files, config) {
|
|
|
509
592
|
code: "CTX102",
|
|
510
593
|
defaultSeverity: "error",
|
|
511
594
|
path: mention.path,
|
|
512
|
-
message: `references missing package script: ${mention.script}
|
|
595
|
+
message: `references missing package script: ${mention.script}`,
|
|
596
|
+
line: mention.line,
|
|
597
|
+
column: mention.column,
|
|
598
|
+
recommendation: "remove the command or replace it with an existing package.json script"
|
|
513
599
|
}, config);
|
|
514
600
|
if (diagnostic) {
|
|
515
601
|
diagnostics.push(diagnostic);
|
|
@@ -529,7 +615,8 @@ function collectScriptMentions(root, file) {
|
|
|
529
615
|
for (const match of content.matchAll(pattern)) {
|
|
530
616
|
const script = match[1];
|
|
531
617
|
if (script) {
|
|
532
|
-
|
|
618
|
+
const location = locationFromOffset(content, match.index ?? 0);
|
|
619
|
+
mentions.push({ path: file.path, script, line: location.line, column: location.column });
|
|
533
620
|
}
|
|
534
621
|
}
|
|
535
622
|
}
|
|
@@ -537,7 +624,7 @@ function collectScriptMentions(root, file) {
|
|
|
537
624
|
}
|
|
538
625
|
function readPackageScripts(root) {
|
|
539
626
|
const packageJsonPath = resolve6(root, "package.json");
|
|
540
|
-
if (!
|
|
627
|
+
if (!existsSync4(packageJsonPath)) {
|
|
541
628
|
return null;
|
|
542
629
|
}
|
|
543
630
|
const parsed = JSON.parse(readFileSync5(packageJsonPath, "utf8"));
|
|
@@ -549,6 +636,39 @@ function getScanRoot3(target) {
|
|
|
549
636
|
return stat.isDirectory() ? absoluteTarget : dirname4(absoluteTarget);
|
|
550
637
|
}
|
|
551
638
|
|
|
639
|
+
// src/score.ts
|
|
640
|
+
var CATEGORY_RULES = {
|
|
641
|
+
correctness: ["CTX101", "CTX102"],
|
|
642
|
+
freshness: ["CTX003", "CTX005"],
|
|
643
|
+
efficiency: ["CTX001", "CTX006", "CTX105"],
|
|
644
|
+
consistency: ["CTX002", "CTX101"],
|
|
645
|
+
coverage: []
|
|
646
|
+
};
|
|
647
|
+
function calculateContextScore(scan, diagnostics, config) {
|
|
648
|
+
const errors = diagnostics.filter((diagnostic) => diagnostic.severity === "error").length;
|
|
649
|
+
const warnings = diagnostics.filter((diagnostic) => diagnostic.severity === "warn").length;
|
|
650
|
+
const budgetPenalty = scan.totalTokens > config.maxTokens ? Math.min(Math.ceil((scan.totalTokens - config.maxTokens) / config.maxTokens * 20), 20) : 0;
|
|
651
|
+
const duplicationPenalty = Math.min(diagnostics.filter((diagnostic) => diagnostic.code === "CTX006").length * 8, 16);
|
|
652
|
+
const overall = clampScore(100 - Math.min(errors * 18, 54) - Math.min(warnings * 6, 30) - budgetPenalty - duplicationPenalty);
|
|
653
|
+
return {
|
|
654
|
+
overall,
|
|
655
|
+
correctness: categoryScore(diagnostics, CATEGORY_RULES.correctness),
|
|
656
|
+
freshness: categoryScore(diagnostics, CATEGORY_RULES.freshness),
|
|
657
|
+
efficiency: clampScore(categoryScore(diagnostics, CATEGORY_RULES.efficiency) - budgetPenalty - duplicationPenalty),
|
|
658
|
+
consistency: categoryScore(diagnostics, CATEGORY_RULES.consistency),
|
|
659
|
+
coverage: scan.files.length === 0 ? 70 : 100
|
|
660
|
+
};
|
|
661
|
+
}
|
|
662
|
+
function categoryScore(diagnostics, codes) {
|
|
663
|
+
const relevant = diagnostics.filter((diagnostic) => codes.includes(diagnostic.code));
|
|
664
|
+
const errors = relevant.filter((diagnostic) => diagnostic.severity === "error").length;
|
|
665
|
+
const warnings = relevant.filter((diagnostic) => diagnostic.severity === "warn").length;
|
|
666
|
+
return clampScore(100 - Math.min(errors * 22, 66) - Math.min(warnings * 10, 40));
|
|
667
|
+
}
|
|
668
|
+
function clampScore(score) {
|
|
669
|
+
return Math.max(0, Math.min(100, Math.round(score)));
|
|
670
|
+
}
|
|
671
|
+
|
|
552
672
|
// src/doctor.ts
|
|
553
673
|
function runDoctor(target, agent, config) {
|
|
554
674
|
const scan = scanContext(target, agent, config);
|
|
@@ -570,14 +690,159 @@ function runDoctor(target, agent, config) {
|
|
|
570
690
|
warnings,
|
|
571
691
|
errors
|
|
572
692
|
},
|
|
693
|
+
score: calculateContextScore(scan, diagnostics, config),
|
|
573
694
|
files: scan.files,
|
|
574
695
|
diagnostics
|
|
575
696
|
};
|
|
576
697
|
}
|
|
577
698
|
|
|
699
|
+
// src/fix.ts
|
|
700
|
+
import { readFileSync as readFileSync6, statSync as statSync5, writeFileSync } from "fs";
|
|
701
|
+
import { dirname as dirname5, resolve as resolve7 } from "path";
|
|
702
|
+
function runFix(options, config) {
|
|
703
|
+
const root = getScanRoot4(options.target);
|
|
704
|
+
const before = runDoctor(options.target, options.agent, config);
|
|
705
|
+
const skipped = collectSkippedFixes(before);
|
|
706
|
+
const edits = collectSafeEdits(root, before.files);
|
|
707
|
+
const applied = [];
|
|
708
|
+
for (const edit of edits) {
|
|
709
|
+
applied.push({
|
|
710
|
+
code: edit.code,
|
|
711
|
+
path: edit.path,
|
|
712
|
+
title: edit.title,
|
|
713
|
+
dryRun: options.dryRun
|
|
714
|
+
});
|
|
715
|
+
if (!options.dryRun) {
|
|
716
|
+
writeFileSync(resolve7(root, edit.path), edit.content);
|
|
717
|
+
}
|
|
718
|
+
}
|
|
719
|
+
const after = options.dryRun ? before : runDoctor(options.target, options.agent, config);
|
|
720
|
+
return {
|
|
721
|
+
target: options.target,
|
|
722
|
+
applied,
|
|
723
|
+
skipped,
|
|
724
|
+
before,
|
|
725
|
+
after
|
|
726
|
+
};
|
|
727
|
+
}
|
|
728
|
+
function collectSafeEdits(root, files) {
|
|
729
|
+
const edits = /* @__PURE__ */ new Map();
|
|
730
|
+
for (const edit of collectPackageManagerEdits(root, files)) {
|
|
731
|
+
edits.set(edit.path, edit);
|
|
732
|
+
}
|
|
733
|
+
for (const edit of collectDuplicateParagraphEdits(root, files)) {
|
|
734
|
+
const current = edits.get(edit.path);
|
|
735
|
+
edits.set(edit.path, current ? { ...edit, content: removeDuplicateParagraphs(current.content) } : edit);
|
|
736
|
+
}
|
|
737
|
+
return [...edits.values()];
|
|
738
|
+
}
|
|
739
|
+
function collectPackageManagerEdits(root, files) {
|
|
740
|
+
const packageManager = detectPackageManagerFromLockfiles(root);
|
|
741
|
+
if (!packageManager) {
|
|
742
|
+
return [];
|
|
743
|
+
}
|
|
744
|
+
return files.flatMap((file) => {
|
|
745
|
+
if (file.skippedBinary) {
|
|
746
|
+
return [];
|
|
747
|
+
}
|
|
748
|
+
const absolutePath = resolve7(root, file.path);
|
|
749
|
+
const content = readFileSync6(absolutePath, "utf8");
|
|
750
|
+
const next = normalizePackageManagerCommands(content, packageManager);
|
|
751
|
+
return next === content ? [] : [{
|
|
752
|
+
code: "CTX101",
|
|
753
|
+
path: file.path,
|
|
754
|
+
title: `Normalize package manager commands to ${packageManager}`,
|
|
755
|
+
content: next
|
|
756
|
+
}];
|
|
757
|
+
});
|
|
758
|
+
}
|
|
759
|
+
function collectDuplicateParagraphEdits(root, files) {
|
|
760
|
+
return files.flatMap((file) => {
|
|
761
|
+
if (file.skippedBinary) {
|
|
762
|
+
return [];
|
|
763
|
+
}
|
|
764
|
+
const absolutePath = resolve7(root, file.path);
|
|
765
|
+
const content = readFileSync6(absolutePath, "utf8");
|
|
766
|
+
const next = removeDuplicateParagraphs(content);
|
|
767
|
+
return next === content ? [] : [{
|
|
768
|
+
code: "CTX006",
|
|
769
|
+
path: file.path,
|
|
770
|
+
title: "Remove repeated paragraph",
|
|
771
|
+
content: next
|
|
772
|
+
}];
|
|
773
|
+
});
|
|
774
|
+
}
|
|
775
|
+
function collectSkippedFixes(before) {
|
|
776
|
+
return before.diagnostics.filter((diagnostic) => diagnostic.code === "CTX102").map((diagnostic) => ({
|
|
777
|
+
code: diagnostic.code,
|
|
778
|
+
path: diagnostic.path,
|
|
779
|
+
title: "Replace missing package script reference",
|
|
780
|
+
reason: "missing package script replacements are recommendations only in v0.3"
|
|
781
|
+
}));
|
|
782
|
+
}
|
|
783
|
+
function normalizePackageManagerCommands(content, packageManager) {
|
|
784
|
+
return content.replace(/\b(npm|pnpm|yarn|bun)\s+(run\s+)?([a-z0-9:_-]+)\b/gi, (match, current, runPrefix, command) => {
|
|
785
|
+
const lowerCommand = command.toLowerCase();
|
|
786
|
+
if (current.toLowerCase() === packageManager && commandFormMatches(packageManager, runPrefix, lowerCommand)) {
|
|
787
|
+
return match;
|
|
788
|
+
}
|
|
789
|
+
if (["install", "add", "remove"].includes(lowerCommand)) {
|
|
790
|
+
return `${packageManager} ${command}`;
|
|
791
|
+
}
|
|
792
|
+
if (["exec", "dlx", "create", "init"].includes(lowerCommand)) {
|
|
793
|
+
return match;
|
|
794
|
+
}
|
|
795
|
+
if (packageManager === "npm") {
|
|
796
|
+
return `npm run ${command}`;
|
|
797
|
+
}
|
|
798
|
+
if (packageManager === "bun") {
|
|
799
|
+
return `bun run ${command}`;
|
|
800
|
+
}
|
|
801
|
+
return `${packageManager} ${command}`;
|
|
802
|
+
});
|
|
803
|
+
}
|
|
804
|
+
function removeDuplicateParagraphs(content) {
|
|
805
|
+
const parts = content.split(/(\n\s*\n)/);
|
|
806
|
+
const seen = /* @__PURE__ */ new Set();
|
|
807
|
+
const kept = [];
|
|
808
|
+
for (let index = 0; index < parts.length; index += 1) {
|
|
809
|
+
const part = parts[index] ?? "";
|
|
810
|
+
if (index % 2 === 1) {
|
|
811
|
+
kept.push(part);
|
|
812
|
+
continue;
|
|
813
|
+
}
|
|
814
|
+
const normalized = part.trim().replace(/\s+/g, " ");
|
|
815
|
+
if (normalized.length >= 40 && seen.has(normalized)) {
|
|
816
|
+
if (kept.length > 0 && /^\n\s*\n$/.test(kept[kept.length - 1] ?? "")) {
|
|
817
|
+
kept.pop();
|
|
818
|
+
}
|
|
819
|
+
continue;
|
|
820
|
+
}
|
|
821
|
+
if (normalized.length >= 40) {
|
|
822
|
+
seen.add(normalized);
|
|
823
|
+
}
|
|
824
|
+
kept.push(part);
|
|
825
|
+
}
|
|
826
|
+
return kept.join("");
|
|
827
|
+
}
|
|
828
|
+
function commandFormMatches(packageManager, runPrefix, command) {
|
|
829
|
+
if (["install", "add", "remove", "exec", "dlx", "create", "init"].includes(command)) {
|
|
830
|
+
return !runPrefix;
|
|
831
|
+
}
|
|
832
|
+
if (packageManager === "npm" || packageManager === "bun") {
|
|
833
|
+
return Boolean(runPrefix);
|
|
834
|
+
}
|
|
835
|
+
return !runPrefix;
|
|
836
|
+
}
|
|
837
|
+
function getScanRoot4(target) {
|
|
838
|
+
const absoluteTarget = resolve7(target);
|
|
839
|
+
const stat = statSync5(absoluteTarget);
|
|
840
|
+
return stat.isDirectory() ? absoluteTarget : dirname5(absoluteTarget);
|
|
841
|
+
}
|
|
842
|
+
|
|
578
843
|
// src/init.ts
|
|
579
|
-
import { existsSync as
|
|
580
|
-
import { resolve as
|
|
844
|
+
import { existsSync as existsSync5, writeFileSync as writeFileSync2 } from "fs";
|
|
845
|
+
import { resolve as resolve8 } from "path";
|
|
581
846
|
var InitError = class extends Error {
|
|
582
847
|
constructor(message) {
|
|
583
848
|
super(message);
|
|
@@ -585,11 +850,11 @@ var InitError = class extends Error {
|
|
|
585
850
|
}
|
|
586
851
|
};
|
|
587
852
|
function initConfig(cwd = process.cwd()) {
|
|
588
|
-
const path =
|
|
589
|
-
if (
|
|
853
|
+
const path = resolve8(cwd, CONFIG_FILE_NAME);
|
|
854
|
+
if (existsSync5(path)) {
|
|
590
855
|
throw new InitError(`${CONFIG_FILE_NAME} already exists`);
|
|
591
856
|
}
|
|
592
|
-
|
|
857
|
+
writeFileSync2(path, `${JSON.stringify(DEFAULT_CONFIG, null, 2)}
|
|
593
858
|
`, "utf8");
|
|
594
859
|
return { path };
|
|
595
860
|
}
|
|
@@ -627,6 +892,7 @@ function formatHumanDoctorResult(result) {
|
|
|
627
892
|
const sections = [
|
|
628
893
|
colors.bold(colors.cyan("ctxscope doctor")),
|
|
629
894
|
formatDoctorMeta(result),
|
|
895
|
+
formatDoctorScore(result),
|
|
630
896
|
formatDoctorSummary(result),
|
|
631
897
|
formatDoctorDiagnostics(result)
|
|
632
898
|
];
|
|
@@ -638,15 +904,43 @@ function formatJsonDoctorResult(result) {
|
|
|
638
904
|
target: result.target,
|
|
639
905
|
status: result.status,
|
|
640
906
|
summary: result.summary,
|
|
907
|
+
score: result.score,
|
|
641
908
|
files: result.files,
|
|
642
909
|
diagnostics: result.diagnostics
|
|
643
910
|
}, null, 2);
|
|
644
911
|
}
|
|
912
|
+
function formatHumanFixResult(result) {
|
|
913
|
+
const sections = [
|
|
914
|
+
colors.bold(colors.cyan("ctxscope fix")),
|
|
915
|
+
formatFixMeta(result),
|
|
916
|
+
formatFixSummary(result),
|
|
917
|
+
formatAppliedFixes(result),
|
|
918
|
+
formatSkippedFixes(result)
|
|
919
|
+
];
|
|
920
|
+
return sections.filter(Boolean).join("\n\n");
|
|
921
|
+
}
|
|
922
|
+
function formatJsonFixResult(result) {
|
|
923
|
+
return JSON.stringify({
|
|
924
|
+
target: result.target,
|
|
925
|
+
applied: result.applied,
|
|
926
|
+
skipped: result.skipped,
|
|
927
|
+
before: result.before,
|
|
928
|
+
after: result.after
|
|
929
|
+
}, null, 2);
|
|
930
|
+
}
|
|
645
931
|
function formatWarning(warning) {
|
|
932
|
+
var _a;
|
|
646
933
|
const severity = colorSeverity(warning);
|
|
647
934
|
const code = warning.severity === "error" ? colors.red(warning.code) : colors.yellow(warning.code);
|
|
648
|
-
|
|
649
|
-
|
|
935
|
+
const location = warning.line === void 0 ? warning.path : `${warning.path}:${warning.line}`;
|
|
936
|
+
const details = [colors.dim(warning.message)];
|
|
937
|
+
if ((_a = warning.fix) == null ? void 0 : _a.safe) {
|
|
938
|
+
details.push(`${colors.dim("Fix:")} ${warning.fix.title}`);
|
|
939
|
+
} else if (warning.recommendation) {
|
|
940
|
+
details.push(`${colors.dim("Recommendation:")} ${warning.recommendation}`);
|
|
941
|
+
}
|
|
942
|
+
return `${severity} ${code} ${location}
|
|
943
|
+
${details.join("\n ")}`;
|
|
650
944
|
}
|
|
651
945
|
function colorSeverity(diagnostic) {
|
|
652
946
|
return diagnostic.severity === "error" ? colors.red(diagnostic.severity.toUpperCase()) : colors.yellow(diagnostic.severity.toUpperCase());
|
|
@@ -707,8 +1001,33 @@ function formatDoctorSummary(result) {
|
|
|
707
1001
|
result.summary.errors > 0 ? colors.red(`${result.summary.errors} errors`) : null,
|
|
708
1002
|
result.summary.warnings > 0 ? colors.yellow(`${result.summary.warnings} warnings`) : null
|
|
709
1003
|
].filter(Boolean).join(", ");
|
|
1004
|
+
const safeFixes = result.diagnostics.filter((diagnostic) => {
|
|
1005
|
+
var _a;
|
|
1006
|
+
return (_a = diagnostic.fix) == null ? void 0 : _a.safe;
|
|
1007
|
+
}).length;
|
|
1008
|
+
const fixLine = safeFixes > 0 ? `
|
|
1009
|
+
Run ctxscope fix to apply ${safeFixes} safe ${safeFixes === 1 ? "fix" : "fixes"}.` : "";
|
|
710
1010
|
return `${colors.bold("Summary")}
|
|
711
|
-
${formatNumber(result.summary.files)} files, ~${formatNumber(result.summary.totalTokens)} tokens, ${diagnosticLabel}`;
|
|
1011
|
+
${formatNumber(result.summary.files)} files, ~${formatNumber(result.summary.totalTokens)} tokens, ${diagnosticLabel}${fixLine}`;
|
|
1012
|
+
}
|
|
1013
|
+
function formatDoctorScore(result) {
|
|
1014
|
+
return [
|
|
1015
|
+
`${colors.bold("Agent Context Score")} ${scoreColor(result.score.overall)(`${result.score.overall}/100`)}`,
|
|
1016
|
+
` Correctness ${scoreColor(result.score.correctness)(String(result.score.correctness))}`,
|
|
1017
|
+
` Freshness ${scoreColor(result.score.freshness)(String(result.score.freshness))}`,
|
|
1018
|
+
` Efficiency ${scoreColor(result.score.efficiency)(String(result.score.efficiency))}`,
|
|
1019
|
+
` Consistency ${scoreColor(result.score.consistency)(String(result.score.consistency))}`,
|
|
1020
|
+
` Coverage ${scoreColor(result.score.coverage)(String(result.score.coverage))}`
|
|
1021
|
+
].join("\n");
|
|
1022
|
+
}
|
|
1023
|
+
function scoreColor(score) {
|
|
1024
|
+
if (score >= 80) {
|
|
1025
|
+
return colors.green;
|
|
1026
|
+
}
|
|
1027
|
+
if (score >= 60) {
|
|
1028
|
+
return colors.yellow;
|
|
1029
|
+
}
|
|
1030
|
+
return colors.red;
|
|
712
1031
|
}
|
|
713
1032
|
function formatDoctorDiagnostics(result) {
|
|
714
1033
|
if (result.diagnostics.length === 0) {
|
|
@@ -727,6 +1046,44 @@ ${warnings.map(formatWarning).join("\n")}`);
|
|
|
727
1046
|
}
|
|
728
1047
|
return sections.join("\n\n");
|
|
729
1048
|
}
|
|
1049
|
+
function formatFixMeta(result) {
|
|
1050
|
+
const dryRun = result.applied.some((fix) => fix.dryRun);
|
|
1051
|
+
return [
|
|
1052
|
+
`${colors.dim("Target")} ${result.target}`,
|
|
1053
|
+
`${colors.dim("Mode")} ${dryRun ? "dry-run" : "write"}`
|
|
1054
|
+
].join("\n");
|
|
1055
|
+
}
|
|
1056
|
+
function formatFixSummary(result) {
|
|
1057
|
+
const beforeScore = result.before.score.overall;
|
|
1058
|
+
const afterScore = result.after.score.overall;
|
|
1059
|
+
const beforeTokens = result.before.summary.totalTokens;
|
|
1060
|
+
const afterTokens = result.after.summary.totalTokens;
|
|
1061
|
+
const savedTokens = Math.max(0, beforeTokens - afterTokens);
|
|
1062
|
+
const action = result.applied.some((fix) => fix.dryRun) ? "Would apply" : "Applied";
|
|
1063
|
+
return [
|
|
1064
|
+
`${colors.bold("Summary")}`,
|
|
1065
|
+
` Agent Context Score ${scoreColor(afterScore)(`${beforeScore} -> ${afterScore}`)}`,
|
|
1066
|
+
` ${action} ${result.applied.length} safe ${result.applied.length === 1 ? "fix" : "fixes"}`,
|
|
1067
|
+
` Skipped ${result.skipped.length} ${result.skipped.length === 1 ? "fix" : "fixes"}`,
|
|
1068
|
+
` Saved ~${formatNumber(savedTokens)} tokens per session`
|
|
1069
|
+
].join("\n");
|
|
1070
|
+
}
|
|
1071
|
+
function formatAppliedFixes(result) {
|
|
1072
|
+
if (result.applied.length === 0) {
|
|
1073
|
+
return "";
|
|
1074
|
+
}
|
|
1075
|
+
return `${colors.bold(result.applied.some((fix) => fix.dryRun) ? "Would Apply" : "Applied")} ${colors.dim(`(${result.applied.length})`)}
|
|
1076
|
+
${result.applied.map((fix) => `${fix.code} ${fix.path}
|
|
1077
|
+
${colors.dim(fix.title)}`).join("\n")}`;
|
|
1078
|
+
}
|
|
1079
|
+
function formatSkippedFixes(result) {
|
|
1080
|
+
if (result.skipped.length === 0) {
|
|
1081
|
+
return "";
|
|
1082
|
+
}
|
|
1083
|
+
return `${colors.bold("Skipped")} ${colors.dim(`(${result.skipped.length})`)}
|
|
1084
|
+
${result.skipped.map((fix) => `${fix.code} ${fix.path}
|
|
1085
|
+
${colors.dim(`${fix.title}: ${fix.reason}`)}`).join("\n")}`;
|
|
1086
|
+
}
|
|
730
1087
|
function formatTokenCell(tokens, skippedBinary) {
|
|
731
1088
|
return skippedBinary ? "binary" : `~${formatNumber(tokens)}`;
|
|
732
1089
|
}
|
|
@@ -744,7 +1101,7 @@ var SUPPORTED_AGENTS = ["all", "codex", "opencode", "claude", "generic"];
|
|
|
744
1101
|
function getVersion() {
|
|
745
1102
|
try {
|
|
746
1103
|
const packageJson = JSON.parse(
|
|
747
|
-
|
|
1104
|
+
readFileSync7(new URL("../package.json", import.meta.url), "utf8")
|
|
748
1105
|
);
|
|
749
1106
|
return packageJson.version ?? "0.0.0";
|
|
750
1107
|
} catch {
|
|
@@ -762,17 +1119,20 @@ Usage:
|
|
|
762
1119
|
ctxscope init
|
|
763
1120
|
ctxscope scan [path] [--agent <agent>] [--json]
|
|
764
1121
|
ctxscope doctor [path] [--agent <agent>] [--json] [--ci]
|
|
1122
|
+
ctxscope fix [path] [--agent <agent>] [--dry-run] [--json]
|
|
765
1123
|
|
|
766
1124
|
Commands:
|
|
767
1125
|
init Create ctxscope.config.json.
|
|
768
1126
|
scan Discover coding-agent context files for a path.
|
|
769
1127
|
doctor Lint coding-agent context files.
|
|
1128
|
+
fix Apply safe deterministic context fixes.
|
|
770
1129
|
|
|
771
1130
|
Options:
|
|
772
1131
|
--agent <agent> Agent profile: all, codex, opencode, claude, generic.
|
|
773
1132
|
Default: all.
|
|
774
1133
|
--json Print machine-readable JSON.
|
|
775
1134
|
--ci Exit 1 when doctor finds errors.
|
|
1135
|
+
--dry-run Show fixes without writing files.
|
|
776
1136
|
-h, --help Show this help message.
|
|
777
1137
|
-v, --version Show the package version.
|
|
778
1138
|
`);
|
|
@@ -870,6 +1230,48 @@ function parseDoctorOptions(args) {
|
|
|
870
1230
|
}
|
|
871
1231
|
return options;
|
|
872
1232
|
}
|
|
1233
|
+
function parseFixOptions(args) {
|
|
1234
|
+
const options = {
|
|
1235
|
+
agent: "all",
|
|
1236
|
+
dryRun: false,
|
|
1237
|
+
json: false,
|
|
1238
|
+
target: "."
|
|
1239
|
+
};
|
|
1240
|
+
let targetSet = false;
|
|
1241
|
+
for (let index = 0; index < args.length; index += 1) {
|
|
1242
|
+
const arg = args[index];
|
|
1243
|
+
if (arg === "--help" || arg === "-h") {
|
|
1244
|
+
printHelp();
|
|
1245
|
+
process.exit(0);
|
|
1246
|
+
}
|
|
1247
|
+
if (arg === "--dry-run") {
|
|
1248
|
+
options.dryRun = true;
|
|
1249
|
+
continue;
|
|
1250
|
+
}
|
|
1251
|
+
if (arg === "--json") {
|
|
1252
|
+
options.json = true;
|
|
1253
|
+
continue;
|
|
1254
|
+
}
|
|
1255
|
+
if (arg === "--agent") {
|
|
1256
|
+
options.agent = parseAgent(args[index + 1]);
|
|
1257
|
+
index += 1;
|
|
1258
|
+
continue;
|
|
1259
|
+
}
|
|
1260
|
+
if (arg.startsWith("--agent=")) {
|
|
1261
|
+
options.agent = parseAgent(arg.slice("--agent=".length));
|
|
1262
|
+
continue;
|
|
1263
|
+
}
|
|
1264
|
+
if (arg.startsWith("-")) {
|
|
1265
|
+
fail(`unknown option '${arg}'`);
|
|
1266
|
+
}
|
|
1267
|
+
if (targetSet) {
|
|
1268
|
+
fail(`unexpected extra path '${arg}'`);
|
|
1269
|
+
}
|
|
1270
|
+
options.target = arg;
|
|
1271
|
+
targetSet = true;
|
|
1272
|
+
}
|
|
1273
|
+
return options;
|
|
1274
|
+
}
|
|
873
1275
|
function runScan(options) {
|
|
874
1276
|
const config = loadConfig();
|
|
875
1277
|
const result = scanContext(options.target, options.agent, config);
|
|
@@ -891,6 +1293,15 @@ function runDoctorCommand(options) {
|
|
|
891
1293
|
process.exit(1);
|
|
892
1294
|
}
|
|
893
1295
|
}
|
|
1296
|
+
function runFixCommand(options) {
|
|
1297
|
+
const config = loadConfig();
|
|
1298
|
+
const result = runFix(options, config);
|
|
1299
|
+
if (options.json) {
|
|
1300
|
+
console.log(formatJsonFixResult(result));
|
|
1301
|
+
return;
|
|
1302
|
+
}
|
|
1303
|
+
console.log(formatHumanFixResult(result));
|
|
1304
|
+
}
|
|
894
1305
|
function runInitCommand() {
|
|
895
1306
|
const result = initConfig();
|
|
896
1307
|
console.log(`Created ${result.path}`);
|
|
@@ -941,6 +1352,17 @@ function main(argv) {
|
|
|
941
1352
|
}
|
|
942
1353
|
return;
|
|
943
1354
|
}
|
|
1355
|
+
if (command === "fix") {
|
|
1356
|
+
try {
|
|
1357
|
+
runFixCommand(parseFixOptions(args));
|
|
1358
|
+
} catch (error) {
|
|
1359
|
+
if (error instanceof ConfigError) {
|
|
1360
|
+
fail(error.message);
|
|
1361
|
+
}
|
|
1362
|
+
throw error;
|
|
1363
|
+
}
|
|
1364
|
+
return;
|
|
1365
|
+
}
|
|
944
1366
|
fail(`unknown command '${command}'`);
|
|
945
1367
|
}
|
|
946
1368
|
main(process.argv.slice(2));
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@x6txy/ctxscope",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"description": "Inspect and lint coding-agent context files",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"keywords": [
|
|
@@ -32,6 +32,7 @@
|
|
|
32
32
|
],
|
|
33
33
|
"scripts": {
|
|
34
34
|
"build": "tsup src/cli.ts --format esm --clean",
|
|
35
|
+
"test": "pnpm build && tsup test/*.test.ts --format esm --platform node --out-dir dist-test --clean && node --test dist-test/*.test.js",
|
|
35
36
|
"prepack": "pnpm build"
|
|
36
37
|
},
|
|
37
38
|
"engines": {
|