@x6txy/ctxscope 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/CHANGELOG.md ADDED
@@ -0,0 +1,16 @@
1
+ # Changelog
2
+
3
+ ## 0.1.0 - 2026-06-21
4
+
5
+ Published as `@x6txy/ctxscope` on npm. The installed binary is still `ctxscope`.
6
+
7
+ ### Added
8
+
9
+ - Added `ctxscope scan [path]` CLI command.
10
+ - Added agent filters for `all`, `codex`, `opencode`, `claude`, and `generic`.
11
+ - Added context file discovery for common coding-agent instruction surfaces.
12
+ - Added token estimates for discovered context files.
13
+ - Added warning codes `CTX001` through `CTX006` for safe context hygiene checks.
14
+ - Added human-readable terminal output with aligned columns and color support.
15
+ - Added stable JSON output via `--json`.
16
+ - Added README, MIT license, and npm package metadata.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 x6txy
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,129 @@
1
+ # ctxscope
2
+
3
+ Inspect and lint coding-agent context files.
4
+
5
+ `ctxscope` helps you see the instructions your coding agents may read before they start working: `AGENTS.md`, `CLAUDE.md`, `SKILL.md`, OpenCode skills, Cursor rules, and GitHub Copilot instructions.
6
+
7
+ It answers the first questions every agent-heavy repo eventually has:
8
+
9
+ - What context files exist?
10
+ - How much context do they add?
11
+ - Which agent is each file probably for?
12
+ - Which files are suspiciously large, empty, duplicated, or stale?
13
+
14
+ ## Install
15
+
16
+ Run without installing:
17
+
18
+ ```bash
19
+ npx @x6txy/ctxscope scan
20
+ ```
21
+
22
+ Or install globally:
23
+
24
+ ```bash
25
+ npm install -g @x6txy/ctxscope
26
+ ctxscope scan
27
+ ```
28
+
29
+ ## Usage
30
+
31
+ ```bash
32
+ ctxscope --help
33
+ ctxscope --version
34
+ ctxscope scan [path]
35
+ ctxscope scan --agent <all|codex|opencode|claude|generic>
36
+ ctxscope scan --json
37
+ ```
38
+
39
+ Examples:
40
+
41
+ ```bash
42
+ ctxscope scan
43
+ ctxscope scan apps/web
44
+ ctxscope scan --agent codex
45
+ ctxscope scan --agent opencode --json
46
+ ```
47
+
48
+ ## Output
49
+
50
+ ```text
51
+ ctxscope scan
52
+
53
+ Agent all
54
+ Target /repo
55
+
56
+ Files (3)
57
+ Path Tokens Agents
58
+ .opencode/skills/backend/SKILL.md ~6 opencode, generic
59
+ AGENTS.md ~13 codex, opencode, claude, generic
60
+ src/AGENTS.md ~3 codex, opencode, claude, generic
61
+
62
+ Summary
63
+ 3 files, ~22 tokens, 4 warnings
64
+
65
+ Warnings (4)
66
+ WARN CTX002 AGENTS.md
67
+ heading "testing" appears in 2 context files
68
+ ```
69
+
70
+ ## Supported Agents
71
+
72
+ v0.1 uses pattern-based discovery. It does not claim perfect runtime tracing for every agent.
73
+
74
+ | Agent | Files detected |
75
+ | --- | --- |
76
+ | Codex | `AGENTS.md`, `**/AGENTS.md` |
77
+ | OpenCode | `AGENTS.md`, `**/AGENTS.md`, `.opencode/**/*.md`, `.opencode/skills/**/SKILL.md` |
78
+ | Claude Code | `CLAUDE.md`, `**/CLAUDE.md`, `AGENTS.md` |
79
+ | Generic | `AGENTS.md`, `CLAUDE.md`, `SKILL.md`, `**/SKILL.md`, `.cursor/rules/**`, `.github/copilot-instructions.md` |
80
+
81
+ Ignored directories:
82
+
83
+ - `.git`
84
+ - `node_modules`
85
+ - `dist`
86
+
87
+ ## Warning Codes
88
+
89
+ `ctxscope scan` reports objective hygiene warnings only.
90
+
91
+ | Code | Meaning |
92
+ | --- | --- |
93
+ | `CTX001` | Oversized context file |
94
+ | `CTX002` | Duplicate heading across context files |
95
+ | `CTX003` | Stale relative markdown link |
96
+ | `CTX004` | Empty context file |
97
+ | `CTX005` | TODO, FIXME, or obsolete marker |
98
+ | `CTX006` | Repeated paragraph |
99
+
100
+ ## JSON Output
101
+
102
+ Use `--json` for automation:
103
+
104
+ ```bash
105
+ ctxscope scan --json
106
+ ```
107
+
108
+ Shape:
109
+
110
+ ```json
111
+ {
112
+ "agent": "all",
113
+ "target": ".",
114
+ "files": [],
115
+ "totalTokens": 0,
116
+ "warnings": []
117
+ }
118
+ ```
119
+
120
+ ## Limitations
121
+
122
+ - Token counts are estimates: `ceil(character_count / 4)`.
123
+ - v0.1 is discovery-based, not real session tracing.
124
+ - Semantic conflicts are not detected yet. For example, `npm` vs `pnpm` policy conflicts are planned for a future `doctor` command.
125
+ - `diff` and `trace` are future commands.
126
+
127
+ ## License
128
+
129
+ MIT
package/dist/cli.js ADDED
@@ -0,0 +1,491 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/cli.ts
4
+ import { readFileSync as readFileSync3 } from "fs";
5
+
6
+ // src/output.ts
7
+ var colorEnabled = process.env.NO_COLOR === void 0 && (process.stdout.isTTY || process.env.FORCE_COLOR !== void 0);
8
+ var colors = {
9
+ bold: (value) => color("\x1B[1m", value),
10
+ cyan: (value) => color("\x1B[36m", value),
11
+ dim: (value) => color("\x1B[2m", value),
12
+ green: (value) => color("\x1B[32m", value),
13
+ yellow: (value) => color("\x1B[33m", value)
14
+ };
15
+ function formatHumanScanResult(result) {
16
+ const sections = [
17
+ colors.bold(colors.cyan("ctxscope scan")),
18
+ formatMeta(result),
19
+ formatFiles(result),
20
+ formatSummary(result),
21
+ formatWarnings(result)
22
+ ];
23
+ return sections.filter(Boolean).join("\n\n");
24
+ }
25
+ function formatJsonScanResult(result) {
26
+ return JSON.stringify({
27
+ agent: result.agent,
28
+ target: result.target,
29
+ files: result.files,
30
+ totalTokens: result.totalTokens,
31
+ warnings: result.warnings
32
+ }, null, 2);
33
+ }
34
+ function formatWarning(warning) {
35
+ return `${colors.yellow(warning.severity.toUpperCase())} ${colors.yellow(warning.code)} ${warning.path}
36
+ ${colors.dim(warning.message)}`;
37
+ }
38
+ function formatMeta(result) {
39
+ return [
40
+ `${colors.dim("Agent")} ${result.agent}`,
41
+ `${colors.dim("Target")} ${result.target}`
42
+ ].join("\n");
43
+ }
44
+ function formatFiles(result) {
45
+ if (result.files.length === 0) {
46
+ return `${colors.bold("Files")} ${colors.dim("(0)")}
47
+ No context files found.`;
48
+ }
49
+ const pathWidth = Math.max("Path".length, ...result.files.map((file) => file.path.length));
50
+ const tokenWidth = Math.max("Tokens".length, ...result.files.map((file) => formatTokenCell(file.tokens, file.skippedBinary).length));
51
+ const header = [
52
+ colors.dim("Path".padEnd(pathWidth)),
53
+ colors.dim("Tokens".padStart(tokenWidth)),
54
+ colors.dim("Agents")
55
+ ].join(" ");
56
+ const rows = result.files.map((file) => [
57
+ file.path.padEnd(pathWidth),
58
+ formatTokenCell(file.tokens, file.skippedBinary).padStart(tokenWidth),
59
+ file.agents.join(", ")
60
+ ].join(" "));
61
+ return `${colors.bold("Files")} ${colors.dim(`(${result.files.length})`)}
62
+ ${header}
63
+ ${rows.join("\n")}`;
64
+ }
65
+ function formatSummary(result) {
66
+ const warningLabel = result.warnings.length === 0 ? colors.green("0 warnings") : colors.yellow(`${result.warnings.length} warnings`);
67
+ return `${colors.bold("Summary")}
68
+ ${formatNumber(result.files.length)} files, ~${formatNumber(result.totalTokens)} tokens, ${warningLabel}`;
69
+ }
70
+ function formatWarnings(result) {
71
+ if (result.warnings.length === 0) {
72
+ return "";
73
+ }
74
+ return `${colors.bold("Warnings")} ${colors.dim(`(${result.warnings.length})`)}
75
+ ${result.warnings.map(formatWarning).join("\n")}`;
76
+ }
77
+ function formatTokenCell(tokens, skippedBinary) {
78
+ return skippedBinary ? "binary" : `~${formatNumber(tokens)}`;
79
+ }
80
+ function formatNumber(value) {
81
+ return String(value).replace(/\B(?=(\d{3})+(?!\d))/g, ",");
82
+ }
83
+ function color(code, value) {
84
+ return colorEnabled ? `${code}${value}\x1B[0m` : value;
85
+ }
86
+
87
+ // src/scan.ts
88
+ import { statSync as statSync2 } from "fs";
89
+ import { dirname as dirname2, relative as relative2, resolve as resolve3 } from "path";
90
+
91
+ // src/agents.ts
92
+ var AGENT_ORDER = ["codex", "opencode", "claude", "generic"];
93
+ function agentsForPath(relativePath) {
94
+ const normalized = normalizePath(relativePath);
95
+ const fileName = basename(normalized);
96
+ const agents = /* @__PURE__ */ new Set();
97
+ if (normalized.startsWith(".opencode/")) {
98
+ agents.add("opencode");
99
+ if (fileName === "SKILL.md") {
100
+ agents.add("generic");
101
+ }
102
+ return AGENT_ORDER.filter((agent) => agents.has(agent));
103
+ }
104
+ if (fileName === "AGENTS.md") {
105
+ agents.add("codex");
106
+ agents.add("opencode");
107
+ agents.add("claude");
108
+ agents.add("generic");
109
+ }
110
+ if (fileName === "CLAUDE.md") {
111
+ agents.add("claude");
112
+ agents.add("generic");
113
+ }
114
+ if (fileName === "SKILL.md") {
115
+ agents.add("generic");
116
+ if (normalized.startsWith(".opencode/skills/")) {
117
+ agents.add("opencode");
118
+ }
119
+ }
120
+ if (normalized.startsWith(".cursor/rules/")) {
121
+ agents.add("generic");
122
+ }
123
+ if (normalized === ".github/copilot-instructions.md") {
124
+ agents.add("generic");
125
+ }
126
+ return AGENT_ORDER.filter((agent) => agents.has(agent));
127
+ }
128
+ function matchesAgentFilter(agents, filter) {
129
+ return filter === "all" || agents.includes(filter);
130
+ }
131
+ function normalizePath(path) {
132
+ return path.replaceAll("\\", "/").replace(/^\.\//, "");
133
+ }
134
+ function basename(path) {
135
+ const parts = path.split("/");
136
+ return parts[parts.length - 1] ?? path;
137
+ }
138
+
139
+ // src/files.ts
140
+ import { readdirSync, statSync } from "fs";
141
+ import { relative, resolve } from "path";
142
+ var IGNORED_DIRECTORIES = /* @__PURE__ */ new Set([".git", "dist", "node_modules"]);
143
+ function listFiles(targetPath) {
144
+ const absoluteTarget = resolve(targetPath);
145
+ const stat = statSync(absoluteTarget);
146
+ if (stat.isFile()) {
147
+ return [absoluteTarget];
148
+ }
149
+ if (!stat.isDirectory()) {
150
+ return [];
151
+ }
152
+ const files = [];
153
+ walk(absoluteTarget, files);
154
+ return files.sort((a, b) => relative(process.cwd(), a).localeCompare(relative(process.cwd(), b)));
155
+ }
156
+ function walk(directory, files) {
157
+ const entries = readdirSync(directory, { withFileTypes: true });
158
+ for (const entry of entries) {
159
+ const absolutePath = resolve(directory, entry.name);
160
+ if (entry.isDirectory()) {
161
+ if (!IGNORED_DIRECTORIES.has(entry.name)) {
162
+ walk(absolutePath, files);
163
+ }
164
+ continue;
165
+ }
166
+ if (entry.isFile()) {
167
+ files.push(absolutePath);
168
+ }
169
+ }
170
+ }
171
+
172
+ // src/token.ts
173
+ import { readFileSync } from "fs";
174
+ function estimateFileTokens(path) {
175
+ const buffer = readFileSync(path);
176
+ if (looksBinary(buffer)) {
177
+ return { tokens: 0, skipped: true };
178
+ }
179
+ const content = buffer.toString("utf8");
180
+ return {
181
+ tokens: estimateTextTokens(content),
182
+ skipped: false
183
+ };
184
+ }
185
+ function estimateTextTokens(content) {
186
+ return Math.ceil(content.length / 4);
187
+ }
188
+ function looksBinary(buffer) {
189
+ if (buffer.includes(0)) {
190
+ return true;
191
+ }
192
+ const sampleSize = Math.min(buffer.length, 1024);
193
+ if (sampleSize === 0) {
194
+ return false;
195
+ }
196
+ let suspiciousBytes = 0;
197
+ for (let index = 0; index < sampleSize; index += 1) {
198
+ const byte = buffer[index];
199
+ const isCommonTextByte = byte === 9 || byte === 10 || byte === 13 || byte >= 32 && byte <= 126 || byte >= 128;
200
+ if (!isCommonTextByte) {
201
+ suspiciousBytes += 1;
202
+ }
203
+ }
204
+ return suspiciousBytes / sampleSize > 0.3;
205
+ }
206
+
207
+ // src/warnings.ts
208
+ import { existsSync, readFileSync as readFileSync2 } from "fs";
209
+ import { dirname, resolve as resolve2 } from "path";
210
+ var OVERSIZED_TOKEN_LIMIT = 2500;
211
+ var MARKER_PATTERN = /\b(TODO|FIXME|OBSOLETE)\b/i;
212
+ var MARKDOWN_LINK_PATTERN = /(?<!!)\[[^\]\n]+\]\(([^)]+)\)/g;
213
+ function collectWarnings(files) {
214
+ const warnings = [];
215
+ const headings = /* @__PURE__ */ new Map();
216
+ for (const input of files) {
217
+ warnings.push(...collectFileWarnings(input));
218
+ collectHeadings(input, headings);
219
+ }
220
+ warnings.push(...collectDuplicateHeadingWarnings(headings));
221
+ return warnings.sort((a, b) => {
222
+ const pathComparison = a.path.localeCompare(b.path);
223
+ return pathComparison === 0 ? a.code.localeCompare(b.code) : pathComparison;
224
+ });
225
+ }
226
+ function collectFileWarnings(input) {
227
+ const warnings = [];
228
+ const { file } = input;
229
+ if (file.skippedBinary) {
230
+ return warnings;
231
+ }
232
+ const content = readFileSync2(input.absolutePath, "utf8");
233
+ if (file.tokens > OVERSIZED_TOKEN_LIMIT) {
234
+ warnings.push({
235
+ code: "CTX001",
236
+ severity: "warn",
237
+ path: file.path,
238
+ message: `larger than ${OVERSIZED_TOKEN_LIMIT} estimated tokens`
239
+ });
240
+ }
241
+ if (content.trim().length === 0) {
242
+ warnings.push({
243
+ code: "CTX004",
244
+ severity: "warn",
245
+ path: file.path,
246
+ message: "empty context file"
247
+ });
248
+ }
249
+ if (MARKER_PATTERN.test(content)) {
250
+ warnings.push({
251
+ code: "CTX005",
252
+ severity: "warn",
253
+ path: file.path,
254
+ message: "contains TODO, FIXME, or obsolete markers"
255
+ });
256
+ }
257
+ warnings.push(...collectStaleLinkWarnings(input.absolutePath, file.path, content));
258
+ warnings.push(...collectRepeatedParagraphWarnings(file.path, content));
259
+ return warnings;
260
+ }
261
+ function collectHeadings(input, headings) {
262
+ var _a;
263
+ if (input.file.skippedBinary) {
264
+ return;
265
+ }
266
+ const content = readFileSync2(input.absolutePath, "utf8");
267
+ for (const line of content.split(/\r?\n/)) {
268
+ const match = /^(#{1,6})\s+(.+?)\s*$/.exec(line);
269
+ if (!match) {
270
+ continue;
271
+ }
272
+ const heading = (_a = match[2]) == null ? void 0 : _a.trim().toLowerCase();
273
+ if (!heading) {
274
+ continue;
275
+ }
276
+ const paths = headings.get(heading) ?? [];
277
+ paths.push(input.file.path);
278
+ headings.set(heading, paths);
279
+ }
280
+ }
281
+ function collectDuplicateHeadingWarnings(headings) {
282
+ const warnings = [];
283
+ for (const [heading, paths] of headings.entries()) {
284
+ const uniquePaths = [...new Set(paths)];
285
+ if (uniquePaths.length < 2) {
286
+ continue;
287
+ }
288
+ for (const path of uniquePaths) {
289
+ warnings.push({
290
+ code: "CTX002",
291
+ severity: "warn",
292
+ path,
293
+ message: `heading "${heading}" appears in ${uniquePaths.length} context files`
294
+ });
295
+ }
296
+ }
297
+ return warnings;
298
+ }
299
+ function collectStaleLinkWarnings(absolutePath, displayPath, content) {
300
+ var _a, _b;
301
+ const warnings = [];
302
+ const directory = dirname(absolutePath);
303
+ for (const match of content.matchAll(MARKDOWN_LINK_PATTERN)) {
304
+ const href = (_a = match[1]) == null ? void 0 : _a.trim();
305
+ if (!href || shouldSkipLink(href)) {
306
+ continue;
307
+ }
308
+ const pathOnly = ((_b = href.split("#")[0]) == null ? void 0 : _b.split("?")[0]) ?? "";
309
+ if (!pathOnly || !existsSync(resolve2(directory, pathOnly))) {
310
+ warnings.push({
311
+ code: "CTX003",
312
+ severity: "warn",
313
+ path: displayPath,
314
+ message: `links to missing file: ${href}`
315
+ });
316
+ }
317
+ }
318
+ return warnings;
319
+ }
320
+ function collectRepeatedParagraphWarnings(displayPath, content) {
321
+ const paragraphs = content.split(/\n\s*\n/).map((paragraph) => paragraph.trim().replace(/\s+/g, " ")).filter((paragraph) => paragraph.length >= 40);
322
+ const seen = /* @__PURE__ */ new Set();
323
+ for (const paragraph of paragraphs) {
324
+ if (seen.has(paragraph)) {
325
+ return [{
326
+ code: "CTX006",
327
+ severity: "warn",
328
+ path: displayPath,
329
+ message: "contains a repeated paragraph"
330
+ }];
331
+ }
332
+ seen.add(paragraph);
333
+ }
334
+ return [];
335
+ }
336
+ function shouldSkipLink(href) {
337
+ return href.startsWith("http://") || href.startsWith("https://") || href.startsWith("mailto:") || href.startsWith("#");
338
+ }
339
+
340
+ // src/scan.ts
341
+ function scanContext(target, agent) {
342
+ const root = getScanRoot(target);
343
+ const absoluteFiles = listFiles(target);
344
+ const discoveredFiles = absoluteFiles.map((absolutePath) => {
345
+ const path = normalizeRelativePath(relative2(root, absolutePath));
346
+ const agents = agentsForPath(path);
347
+ if (agents.length === 0 || !matchesAgentFilter(agents, agent)) {
348
+ return null;
349
+ }
350
+ const estimate = estimateFileTokens(absolutePath);
351
+ return {
352
+ path,
353
+ agents,
354
+ tokens: estimate.tokens,
355
+ skippedBinary: estimate.skipped
356
+ };
357
+ }).filter((file) => file !== null).sort((a, b) => a.path.localeCompare(b.path));
358
+ const absoluteByPath = new Map(absoluteFiles.map((absolutePath) => [
359
+ normalizeRelativePath(relative2(root, absolutePath)),
360
+ absolutePath
361
+ ]));
362
+ const warningInputs = discoveredFiles.map((file) => ({ file, absolutePath: absoluteByPath.get(file.path) })).filter((input) => input.absolutePath !== void 0);
363
+ return {
364
+ agent,
365
+ target,
366
+ files: discoveredFiles,
367
+ totalTokens: discoveredFiles.reduce((total, file) => total + file.tokens, 0),
368
+ warnings: collectWarnings(warningInputs)
369
+ };
370
+ }
371
+ function getScanRoot(target) {
372
+ const absoluteTarget = resolve3(target);
373
+ const stat = statSync2(absoluteTarget);
374
+ return stat.isDirectory() ? absoluteTarget : dirname2(absoluteTarget);
375
+ }
376
+ function normalizeRelativePath(path) {
377
+ const normalized = path.replaceAll("\\", "/");
378
+ return normalized === "" ? "." : normalized;
379
+ }
380
+
381
+ // src/types.ts
382
+ var SUPPORTED_AGENTS = ["all", "codex", "opencode", "claude", "generic"];
383
+
384
+ // src/cli.ts
385
+ function getVersion() {
386
+ try {
387
+ const packageJson = JSON.parse(
388
+ readFileSync3(new URL("../package.json", import.meta.url), "utf8")
389
+ );
390
+ return packageJson.version ?? "0.0.0";
391
+ } catch {
392
+ return "0.0.0";
393
+ }
394
+ }
395
+ function printHelp() {
396
+ console.log(`ctxscope ${getVersion()}
397
+
398
+ Inspect and lint coding-agent context files.
399
+
400
+ Usage:
401
+ ctxscope --help
402
+ ctxscope --version
403
+ ctxscope scan [path] [--agent <agent>] [--json]
404
+
405
+ Commands:
406
+ scan Discover coding-agent context files for a path.
407
+
408
+ Options:
409
+ --agent <agent> Agent profile: all, codex, opencode, claude, generic.
410
+ Default: all.
411
+ --json Print machine-readable JSON.
412
+ -h, --help Show this help message.
413
+ -v, --version Show the package version.
414
+ `);
415
+ }
416
+ function fail(message) {
417
+ console.error(`ctxscope: ${message}`);
418
+ console.error("Run `ctxscope --help` for usage.");
419
+ process.exit(1);
420
+ }
421
+ function parseAgent(value) {
422
+ if (!value) {
423
+ fail("missing value for --agent");
424
+ }
425
+ if (!SUPPORTED_AGENTS.includes(value)) {
426
+ fail(`unsupported agent '${value}'. Expected one of: ${SUPPORTED_AGENTS.join(", ")}`);
427
+ }
428
+ return value;
429
+ }
430
+ function parseScanOptions(args) {
431
+ const options = {
432
+ agent: "all",
433
+ json: false,
434
+ target: "."
435
+ };
436
+ let targetSet = false;
437
+ for (let index = 0; index < args.length; index += 1) {
438
+ const arg = args[index];
439
+ if (arg === "--help" || arg === "-h") {
440
+ printHelp();
441
+ process.exit(0);
442
+ }
443
+ if (arg === "--json") {
444
+ options.json = true;
445
+ continue;
446
+ }
447
+ if (arg === "--agent") {
448
+ options.agent = parseAgent(args[index + 1]);
449
+ index += 1;
450
+ continue;
451
+ }
452
+ if (arg.startsWith("--agent=")) {
453
+ options.agent = parseAgent(arg.slice("--agent=".length));
454
+ continue;
455
+ }
456
+ if (arg.startsWith("-")) {
457
+ fail(`unknown option '${arg}'`);
458
+ }
459
+ if (targetSet) {
460
+ fail(`unexpected extra path '${arg}'`);
461
+ }
462
+ options.target = arg;
463
+ targetSet = true;
464
+ }
465
+ return options;
466
+ }
467
+ function runScan(options) {
468
+ const result = scanContext(options.target, options.agent);
469
+ if (options.json) {
470
+ console.log(formatJsonScanResult(result));
471
+ return;
472
+ }
473
+ console.log(formatHumanScanResult(result));
474
+ }
475
+ function main(argv) {
476
+ const [command, ...args] = argv;
477
+ if (!command || command === "--help" || command === "-h") {
478
+ printHelp();
479
+ return;
480
+ }
481
+ if (command === "--version" || command === "-v") {
482
+ console.log(getVersion());
483
+ return;
484
+ }
485
+ if (command === "scan") {
486
+ runScan(parseScanOptions(args));
487
+ return;
488
+ }
489
+ fail(`unknown command '${command}'`);
490
+ }
491
+ main(process.argv.slice(2));
package/package.json ADDED
@@ -0,0 +1,41 @@
1
+ {
2
+ "name": "@x6txy/ctxscope",
3
+ "version": "0.1.0",
4
+ "description": "Inspect and lint coding-agent context files",
5
+ "type": "module",
6
+ "keywords": [
7
+ "agents",
8
+ "ai",
9
+ "codex",
10
+ "claude",
11
+ "opencode",
12
+ "cli",
13
+ "context",
14
+ "lint"
15
+ ],
16
+ "repository": {
17
+ "type": "git",
18
+ "url": "git+https://github.com/x6txy/ctxscope.git"
19
+ },
20
+ "bugs": {
21
+ "url": "https://github.com/x6txy/ctxscope/issues"
22
+ },
23
+ "homepage": "https://github.com/x6txy/ctxscope#readme",
24
+ "bin": {
25
+ "ctxscope": "dist/cli.js"
26
+ },
27
+ "files": [
28
+ "dist",
29
+ "README.md",
30
+ "LICENSE",
31
+ "CHANGELOG.md"
32
+ ],
33
+ "scripts": {
34
+ "build": "tsup src/cli.ts --format esm --clean",
35
+ "prepack": "pnpm build"
36
+ },
37
+ "engines": {
38
+ "node": ">=20"
39
+ },
40
+ "license": "MIT"
41
+ }