aiseerr 1.0.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/AGENTS.md ADDED
@@ -0,0 +1,14 @@
1
+ # AI Agent Rules
2
+
3
+ <!-- ai-xray:start -->
4
+ ## Project Context Protocol (powered by ai-xray)
5
+
6
+ Before modifying this project, gather context by running:
7
+ npx ai-xray scout --budget=2000
8
+
9
+ When debugging a failing test:
10
+ npx ai-xray read <file> --lines=<error-range>
11
+
12
+ Before committing changes:
13
+ npx ai-xray diff --full
14
+ <!-- ai-xray:end -->
package/CODE-REVIEW.md ADDED
@@ -0,0 +1,333 @@
1
+ # Code Review: ai-xray v1.0.0
2
+
3
+ > **Reviewer:** Claude Opus 4 (via Ana)
4
+ > **Date:** 2026-03-06
5
+ > **Scope:** Full source review — all 8 source files, 2 test files, build config, dependencies
6
+ > **Verdict:** Solid v1 — ships clean, zero runtime deps, 10.6KB bundle. A handful of real bugs and several "Gemini-isms" to trim. No blockers.
7
+
8
+ ---
9
+
10
+ ## Summary
11
+
12
+ ai-xray is a well-scoped, well-executed CLI tool that does exactly what it promises: structured JSON context gathering for AI agents. The zero-dependency design is genuine (not just marketing), the codebase is small (~630 LOC across 8 files), and the build output is a single 10.6KB file. The code is straightforward and readable. However, there are **2 security issues** (command injection in `diff.ts`/`env.ts`), **1 correctness bug** (budget system doesn't actually truncate), and several patterns where Gemini over-designed flexibility that will never be used. Test coverage is minimal (2 files, 5 tests) and misses the most important commands entirely.
13
+
14
+ ---
15
+
16
+ ## 🔴 Critical Issues (Security / Bugs)
17
+
18
+ ### C1. Command Injection via `--file` in `diff.ts` (SECURITY)
19
+
20
+ **File:** `src/commands/diff.ts:113-114`
21
+
22
+ ```typescript
23
+ const diffCmd = targetFile ? `git diff ${targetFile}` : `git diff`;
24
+ const diffCmdStaged = targetFile ? `git diff --cached ${targetFile}` : `git diff --cached`;
25
+ ```
26
+
27
+ `targetFile` comes directly from user input (`--file=<value>`) and is interpolated into a shell command passed to `execSync()`. An attacker (or a confused AI agent) could pass:
28
+
29
+ ```bash
30
+ ai-xray diff --file="; rm -rf /"
31
+ ```
32
+
33
+ This would execute arbitrary shell commands. Since this tool is specifically designed to be called by AI agents (which can be manipulated via prompt injection in code comments), this is a real attack surface.
34
+
35
+ **Fix:** Use `execSync`'s array form or `execFileSync`, or at minimum sanitize `targetFile` to reject characters outside `[a-zA-Z0-9._/\\-]`.
36
+
37
+ ### C2. Command Injection via `execSafe` pattern (SECURITY)
38
+
39
+ **Files:** `src/commands/env.ts:9-15`, `src/commands/diff.ts:5-11`
40
+
41
+ Both files define identical `execSafe()` functions that pass strings to `execSync()` with shell interpretation. While the currently hardcoded commands are safe, this pattern is fragile — any future extension that interpolates user input into these calls inherits the injection risk. The `targetFile` issue above (C1) is a concrete example.
42
+
43
+ **Fix:** Switch to `execFileSync('git', ['diff', targetFile])` pattern. This avoids shell interpretation entirely.
44
+
45
+ ### C3. Budget System Doesn't Actually Truncate — It Just Labels (BUG)
46
+
47
+ **File:** `src/utils/output.ts:12-53`
48
+
49
+ The `applyBudget()` function checks if the serialized JSON exceeds the token budget, but when it does, it only attaches a `_meta.truncated: true` flag and a "hint" string. **It does not actually remove or reduce any data.** In fact, attaching the `_meta` object makes the output *larger*.
50
+
51
+ ```typescript
52
+ if (estimated > budget) {
53
+ // ... only attaches _meta, doesn't prune anything
54
+ return {
55
+ ...data, // ← entire original data preserved
56
+ _meta: { ... } // ← adds MORE data
57
+ };
58
+ }
59
+ ```
60
+
61
+ The `read.ts` command does its own character-level truncation (correctly), but `tree.ts` and `scout.ts` rely on the global budget system which is effectively a no-op. An AI agent trusting `--budget=500` could still receive 5000+ tokens of tree output.
62
+
63
+ **Fix:** `applyBudget()` needs to actually prune the JSON — e.g., recursively truncate string values, remove deep keys, or serialize-then-slice with a closing marker.
64
+
65
+ ---
66
+
67
+ ## 🟡 Major Issues (Design / Performance / Correctness)
68
+
69
+ ### M1. Duplicate `execSafe` Function (DRY Violation)
70
+
71
+ **Files:** `src/commands/env.ts:9-15` and `src/commands/diff.ts:5-11`
72
+
73
+ Identical function defined in two files. Should be in `src/utils/exec.ts`.
74
+
75
+ ### M2. `tree.ts` Calls `statSync` Twice Per Entry
76
+
77
+ **File:** `src/commands/tree.ts:73-82`
78
+
79
+ ```typescript
80
+ if (isIgnored(entryPath) || entry.startsWith('.DS_Store')) {
81
+ const stat = statSync(entryPath, { throwIfNoEntry: false }); // stat #1
82
+ if (stat?.isDirectory()) {
83
+ stats.ignoredDirs.add(entry);
84
+ }
85
+ continue;
86
+ }
87
+
88
+ const stat = statSync(entryPath, { throwIfNoEntry: false }); // stat #2
89
+ ```
90
+
91
+ For non-ignored entries this is fine (only the second stat runs), but for ignored entries we stat just to record them in `ignoredDirs` — this is wasteful for large directories with many ignored files. More importantly, for very large repos (monorepos with thousands of files), the synchronous recursive traversal could block the event loop for seconds.
92
+
93
+ **Fix:** Either check name-only patterns without statting, or use a single stat per entry.
94
+
95
+ ### M3. `.gitignore` Parsing Is Extremely Naive
96
+
97
+ **File:** `src/commands/tree.ts:4-28`
98
+
99
+ The `buildIgnoreMatcher()` only handles:
100
+ - Exact name matches (after stripping trailing `/`)
101
+ - Leading `*` glob (e.g., `*.log`)
102
+
103
+ It does **not** handle:
104
+ - Negation patterns (`!important.log`)
105
+ - Directory-only patterns (`build/`)
106
+ - Nested path patterns (`src/**/test`)
107
+ - Mid-string globs (`foo*.js`)
108
+ - Comments with inline content
109
+
110
+ For a v1 labeled "sufficient for 95% of use cases" this is acceptable, but the comment should be more honest — it's closer to 70% coverage. Real-world `.gitignore` files routinely use `**` and negation.
111
+
112
+ ### M4. `env.ts` Only Supports Node.js Projects
113
+
114
+ **File:** `src/commands/env.ts:21-23`
115
+
116
+ ```typescript
117
+ if (!existsSync(pkgPath)) {
118
+ throw new Error('Not a Node.js project: package.json not found');
119
+ }
120
+ ```
121
+
122
+ The README doesn't scope this to Node.js only, and AI agents calling `ai-xray env` on a Python or Rust project will get an unhelpful error. The `tree`, `read`, and `diff` commands work for any project, but `env` and `scout` (which calls `env`) will crash.
123
+
124
+ **Fix:** Return a degraded result `{ packageManager: null, note: "No package.json found" }` instead of throwing. Let `scout` handle the null gracefully.
125
+
126
+ ### M5. No Timeout on `execSync` Calls
127
+
128
+ **Files:** `src/commands/env.ts`, `src/commands/diff.ts`
129
+
130
+ `execSync` calls to `git` and package managers have no timeout. In edge cases (large repos, slow git operations, hanging git hooks), these will block indefinitely.
131
+
132
+ **Fix:** Add `{ timeout: 10000 }` to `execSync` options.
133
+
134
+ ### M6. Test Coverage Is Minimal
135
+
136
+ **Files:** `tests/env.test.ts` (1 test), `tests/output.test.ts` (4 tests)
137
+
138
+ Only 2 of 8 source files have tests. The most complex commands (`tree`, `read`, `diff`, `scout`, `init`) have **zero tests**. The `read.ts` budget truncation logic (which actually works correctly) is untested. The `tree.ts` recursive traversal with its compaction logic is untested.
139
+
140
+ ---
141
+
142
+ ## 🟢 Minor Issues (Style / Naming / Polish)
143
+
144
+ ### m1. Pervasive `any` Types
145
+
146
+ Every command handler returns `any` and most internal variables are typed as `any`. Example:
147
+
148
+ ```typescript
149
+ export function handleScoutCommand(budget?: number): any {
150
+ const result: any = {};
151
+ ```
152
+
153
+ For a TypeScript project with `"strict": true`, this defeats much of the type system's value. At minimum, define return interfaces for each command.
154
+
155
+ ### m2. `--pretty` Detection Via Global `process.argv` Inspection
156
+
157
+ **File:** `src/utils/output.ts:57`
158
+
159
+ ```typescript
160
+ process.argv.includes('--pretty')
161
+ ```
162
+
163
+ This reaches into global state instead of receiving `pretty` as a parameter. It works, but it's a hidden coupling. The `cli.ts` even has a comment acknowledging this: `// handled globally in output.ts by inspecting process.argv`.
164
+
165
+ ### m3. `.DS_Store` Hardcoded Check in `tree.ts`
166
+
167
+ **File:** `src/commands/tree.ts:73`
168
+
169
+ ```typescript
170
+ if (isIgnored(entryPath) || entry.startsWith('.DS_Store')) {
171
+ ```
172
+
173
+ `.DS_Store` doesn't "start with" `.DS_Store` — it **equals** `.DS_Store`. The `startsWith` call is technically correct but misleading; it would also match a hypothetical `.DS_Store_backup`. Use `===` for exact match. Also, this should be in the default ignore list, not a special case.
174
+
175
+ ### m4. Inconsistent Error Handling in `scout.ts`
176
+
177
+ **File:** `src/commands/scout.ts:12-16`
178
+
179
+ ```typescript
180
+ try {
181
+ result.env = handleEnvCommand();
182
+ } catch (e: any) {
183
+ result.env = { error: e.message };
184
+ }
185
+ ```
186
+
187
+ Scout wraps each sub-command in try/catch, which is good. But `handleEnvCommand()` throws on non-Node.js projects (M4 above), meaning `scout` will produce `{ env: { error: "Not a Node.js project..." } }` — a degraded but functional result. This is actually decent error handling, but it papers over the underlying issue.
188
+
189
+ ### m5. `process.exit(0)` in `outputSuccess`
190
+
191
+ **File:** `src/utils/output.ts:58`
192
+
193
+ Calling `process.exit(0)` after writing to stdout means the process terminates before the write buffer is guaranteed to flush. On most systems this works because stdout is synchronous for TTY, but when piped (which is the primary use case — AI agents piping JSON), the buffer may not flush.
194
+
195
+ **Fix:** Use `process.stdout.write(data, () => process.exit(0))` or just let the process exit naturally after `main()` completes (remove the `process.exit(0)` call).
196
+
197
+ ### m6. `help` is Both a Positional Command and a Flag
198
+
199
+ **File:** `src/cli.ts:27`
200
+
201
+ ```typescript
202
+ if (args.length === 0 || args.includes('--help') || args.includes('-h') || args.includes('help')) {
203
+ ```
204
+
205
+ `args.includes('help')` will match `help` anywhere in the argument list, including `ai-xray read help.md`. This would show help instead of reading the file `help.md`.
206
+
207
+ ---
208
+
209
+ ## 🎭 Gemini-isms (Over-Engineering Patterns)
210
+
211
+ ### G1. The Budget System Architecture Is Over-Designed for What It Does
212
+
213
+ The codebase has a two-layer budget architecture:
214
+ 1. Per-command truncation (only `read.ts` implements this)
215
+ 2. Global `applyBudget()` wrapper (does nothing except add metadata)
216
+
217
+ This is classic Gemini: build the *framework* for a feature (metadata, hints, token estimation) without implementing the *substance* (actual truncation). The comments in `output.ts` even acknowledge this:
218
+
219
+ > "Specific commands should ideally handle pruning *before* this step... but this serves as a final check or metadata attach point."
220
+
221
+ Translation: "I built the scaffold but didn't finish the building."
222
+
223
+ ### G2. Unnecessarily Verbose JSON Output Structure
224
+
225
+ The help command output includes deeply nested objects with `usage` and `desc` for each command. This is well-structured, but consider that AI agents already know how to read a flat string. A simpler format would save tokens:
226
+
227
+ ```json
228
+ {"commands": ["scout [--budget=N]", "env", "tree [dir] [--depth=N]", ...]}
229
+ ```
230
+
231
+ The current verbose format costs ~3x more tokens than necessary for the same information.
232
+
233
+ ### G3. `testFramework.parseHint` and `lintFramework.parseHint`
234
+
235
+ **File:** `src/commands/env.ts:67-77`
236
+
237
+ ```typescript
238
+ testFramework = {
239
+ name: 'vitest',
240
+ runCommand: `npx vitest run --reporter=json`,
241
+ parseHint: `Look for 'testResults[].assertionResults[].failureMessages'`
242
+ };
243
+ ```
244
+
245
+ Including natural-language "hints" for AI agents on how to parse JSON output is a charming but unnecessary abstraction. Any competent LLM can figure out vitest's JSON output format. This is Gemini being helpful to a fault — spending tokens to save hypothetical future LLM confusion.
246
+
247
+ ### G4. `_moreReplacedFiles` Key in Tree Output
248
+
249
+ **File:** `src/commands/tree.ts:114`
250
+
251
+ ```typescript
252
+ result['_moreReplacedFiles'] = hiddenCount;
253
+ ```
254
+
255
+ This key name is confusing — "replaced" implies substitution, but it means "additional files not shown." The `_files` array uses `_more` for the same concept. Inconsistent naming within the same function.
256
+
257
+ ### G5. Workspaces Duplication in Scout
258
+
259
+ **File:** `src/commands/scout.ts:85-90`
260
+
261
+ ```typescript
262
+ if (result.env && result.env.workspaces) {
263
+ result.workspaces = result.env.workspaces;
264
+ } else {
265
+ result.workspaces = null;
266
+ }
267
+ ```
268
+
269
+ This copies `env.workspaces` to a top-level `workspaces` key for "convenience." Now the same data exists in two places in the JSON output, wasting tokens — the exact thing the tool is designed to minimize.
270
+
271
+ ---
272
+
273
+ ## ✅ Positive Aspects
274
+
275
+ 1. **Zero runtime dependencies is genuine.** `package.json` has only devDependencies. The 10.6KB bundled output is impressively small. This is a real competitive advantage.
276
+
277
+ 2. **Machine-first protocol is well-executed.** Structured JSON on stdout, structured errors on stderr, no ANSI codes, no interactivity. The uncaughtException/unhandledRejection handlers guarantee JSON output even on crashes. This shows real product thinking.
278
+
279
+ 3. **The `read` command is well-designed.** Multi-file support, line ranges, JSON key extraction, per-file budget tracking with character-level truncation — this is the most polished command and clearly saw the most iteration.
280
+
281
+ 4. **Smart array compaction in `tree`**. The `_files` array for leaf directories is a genuinely clever optimization that saves significant tokens vs. the naive `{ "file.ts": {} }` per-file format.
282
+
283
+ 5. **`init` has idempotent injection.** Checks for `<!-- ai-xray:start -->` before re-injecting. Simple and correct.
284
+
285
+ 6. **The overall architecture is clean.** One file per command, a shared output utility, a simple router in `cli.ts`. No framework, no DI container, no abstract base classes. For once, Gemini showed restraint.
286
+
287
+ 7. **Build config is minimal and correct.** `tsup` with `noExternal: [/(.*)/]` to bundle everything. `target: node18`. Clean.
288
+
289
+ ---
290
+
291
+ ## Recommendations
292
+
293
+ ### Priority 1 (Fix Before Publishing)
294
+ 1. **Fix command injection** in `diff.ts` — use `execFileSync` or sanitize `targetFile`
295
+ 2. **Fix `applyBudget()` to actually truncate** — currently it's a no-op that misleads consumers
296
+ 3. **Fix `help` command collision** — `ai-xray read help.md` should read the file, not show help
297
+ 4. **Fix `process.exit(0)` in stdout pipe scenarios** — use callback or natural exit
298
+
299
+ ### Priority 2 (Before v1.1)
300
+ 5. **Add tests for `tree`, `read`, `diff`, `scout`, `init`** — current coverage is ~25%
301
+ 6. **Add `execSync` timeout** (10s) to all git/pm calls
302
+ 7. **Degrade gracefully on non-Node.js projects** instead of throwing in `env`
303
+ 8. **Extract shared `execSafe`** to `utils/exec.ts`
304
+ 9. **Define TypeScript return types** for all command handlers
305
+
306
+ ### Priority 3 (Nice to Have)
307
+ 10. **Improve `.gitignore` parsing** or document its limitations explicitly
308
+ 11. **Remove workspace duplication** in scout output
309
+ 12. **Rename `_moreReplacedFiles`** to `_hiddenFiles` or `_more` for consistency
310
+ 13. **Consider removing `parseHint` fields** — they cost tokens for minimal value
311
+
312
+ ---
313
+
314
+ ## Stats
315
+
316
+ | Metric | Value |
317
+ |---|---|
318
+ | Source files | 8 |
319
+ | Total LOC (source) | ~630 |
320
+ | Test files | 2 |
321
+ | Test cases | 5 |
322
+ | Runtime dependencies | 0 |
323
+ | Bundle size | 10.6 KB |
324
+ | Build time | ~15ms |
325
+ | Test time | ~454ms |
326
+ | Critical issues | 3 (2 security, 1 bug) |
327
+ | Major issues | 6 |
328
+ | Minor issues | 6 |
329
+ | Gemini-isms | 5 |
330
+
331
+ ---
332
+
333
+ *Review complete. Overall: a clean, focused v1 with real product value. Fix the security issues and the budget bug, add tests, and it's ready to ship.*