@chemx/starter-kit 26.10.26-386 → 26.10.29-360

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 CHANGED
@@ -9,7 +9,7 @@
9
9
  | Standard Reflex | `chemx` Equivalent | Token & Architecture Advantage |
10
10
  | :--- | :--- | :--- |
11
11
  | `grep -rn "pattern" .` | `chemx q -g "pattern" --bare` or `chemx q -g "pattern" -C 3 --bare` | Fixed-string search matching rg token-for-token (57 chars / 14 tokens per hit); add `-C <N>` to inspect surrounding code in a single turn without a second read call. `a\|b` is literal text: add `--regex` for alternation (RE2 subset). `-l` prints the matching files once, `-o --unique` prints the distinct matched text (a `--gs-[a-z-]+` token list in one call), paths after the pattern scope it (`--glob <path>` is `--dir`; `--max <N>` is `-n`), dot-directories such as `.github` and `.claude` are searched (only git-ignored paths are left out; `--hidden` adds them), and `chemx q <text>` with no symbol hit runs this search for you. See [docs/search.md](docs/search.md). |
12
- | `sed -n 'N,Mp' <file>` | `chemx read <file>:N-M --bare` | Verbatim slice matching sed byte-for-byte; 0 header, box or line number tokens. |
12
+ | `sed -n 'N,Mp' <file>` | `chemx read <file>:N-M --bare` (or `--lines a-b`, `--lines=a-b`, `--start=N --end=M`; `--full` for whole file) | Verbatim slice matching sed byte-for-byte; 0 header, box or line number tokens. `--bare` never substitutes an outline on long files. `--full` returns the whole file without truncation. |
13
13
  | `git diff` | `chemx d` | Zero-context (`-U0`), auto-collapses to `--stat` if > 80 lines, micro-syncs index. |
14
14
  | Editing `docs/CHANGELOG.md` for your change | write `docs/changelog.d/<ticket>.md` (first line `### Fixed`, `### Added` or a title with the ticket id, then bullets); `chemx changelog --check` validates, `chemx changelog` folds the fragments into Unreleased and deletes them | One file per change, so parallel tickets never edit the same lines. Never edit `docs/CHANGELOG.md` directly. See [docs/changelog.d/README.md](docs/changelog.d/README.md). |
15
15
  | `git log` | `chemx log -n 5` | One-line commit history (5 commits measured about 140 tokens on 2026-10-09, roughly 28 per commit). |
@@ -198,7 +198,8 @@ export const saveAuditSnapshot = (report, cwd = process.cwd(), scopeInfo = {}) =
198
198
  const history = getAuditHistory(cwd);
199
199
 
200
200
  // Prevent duplicate snapshots if the same scope is saved again within 5 seconds with same metrics
201
- const last = history.findLast((s) => scopeKey(s) === scopeKey(snapshot));
201
+ const targetScope = scopeKey(snapshot);
202
+ const last = [...history].reverse().find((s) => scopeKey(s) === targetScope);
202
203
  if (last) {
203
204
  const timeDiff = Math.abs(Date.now() - new Date(last.timestamp).getTime());
204
205
  const isSameMetrics = hasMatchingAuditMetrics(last, snapshot);
@@ -14,8 +14,9 @@ const COMPONENT_EXTENSIONS = new Set(['.vue', '.tsx', '.jsx']);
14
14
  const TEMPLATE_EXTENSIONS = new Set(['.vue', '.html', '.svelte']);
15
15
  const COMMENT_PREFIXES = ['//', '*', '/*'];
16
16
  const CONSOLE_LOG_METHODS = ['log', 'info', 'warn'];
17
- const NON_INTERACTIVE_FLAGS = ['--non-interactive', '--no-interactive', '--ci', '--headless', '--yes', '-y'];
18
- const FORMAT_FLAGS = ['--markdown', '--md', '--json'];
17
+ const NON_INTERACTIVE_FLAGS = ['--non-interactive', '--no-interactive', '--ci', '--headless', '--yes', '-y', '--strict'];
18
+ const FORMAT_FLAGS = ['--markdown', '--md', '--json', '--unroll', '--all'];
19
+ const AGENT_ENV_KEYS = ['CI', 'GIT_DIR', 'ANTIGRAVITY_CSRF_TOKEN', 'ANTIGRAVITY_AGENT', 'CLAUDECODE', 'CLAUDE_CODE', 'CURSOR_TRACE_ID', 'CODEX_SANDBOX', 'AIDER_MODEL'];
19
20
  const GRADE_RANKS = { 'A+': 5, 'A': 4, 'B': 3, 'C': 2, 'D': 1, 'F': 0 };
20
21
 
21
22
  export const isComponentPath = (path) => {
@@ -157,7 +158,7 @@ export const isNonInteractiveSession = (rawArgs, env = process.env) => {
157
158
  const hasCliFlag = NON_INTERACTIVE_FLAGS.some((flag) => rawArgs.includes(flag));
158
159
  const hasOutputFlag = rawArgs.some((arg) => arg.startsWith('--output=') || arg.startsWith('-o='));
159
160
  const hasFormatFlag = FORMAT_FLAGS.some((flag) => rawArgs.includes(flag));
160
- const hasCiEnv = Boolean(env.CI || env.GIT_DIR);
161
+ const hasCiEnv = AGENT_ENV_KEYS.some((key) => Boolean(env[key]));
161
162
  const isNotTTY = !isStdoutTty();
162
163
  return [hasCliFlag, hasOutputFlag, hasFormatFlag, hasCiEnv, isNotTTY].some(Boolean);
163
164
  };
@@ -207,8 +207,9 @@ export const findUnknownFlag = (command, rawArgs) => {
207
207
  if (isPattern) i++;
208
208
  continue;
209
209
  }
210
+ const rangeNote = entry.name === 'read' ? ' Range forms: <file>:N-M, --lines a-b, or --start=N --end=M.' : '';
210
211
  const isBadShort = shorts !== null && /^-[A-Za-z](=.*)?$/.test(arg) && !shorts.has(arg.slice(0, 2));
211
- if (isBadShort) return `chemx ${entry.name}: unknown flag ${arg.slice(0, 2)}. Run \`chemx ${entry.name} --help\`.`;
212
+ if (isBadShort) return `chemx ${entry.name}: unknown flag ${arg.slice(0, 2)}.${rangeNote} Run \`chemx ${entry.name} --help\`.`;
212
213
  const isLongFlag = arg.startsWith('--') && arg.length > 2;
213
214
  if (!isLongFlag) continue;
214
215
  const hasInlineValue = arg.includes('=');
@@ -223,8 +224,13 @@ export const findUnknownFlag = (command, rawArgs) => {
223
224
  const closest = suggestFlag(name, candidates);
224
225
  const isPassedOn = isTypoOnly && !closest;
225
226
  if (isPassedOn) continue;
226
- const hint = closest ? `; did you mean ${closest}?` : '';
227
- return `chemx ${entry.name}: unknown flag ${name}${hint} Run \`chemx ${entry.name} --help\`.`;
227
+ let hint = '';
228
+ if (closest) {
229
+ hint = `; did you mean ${closest}?`;
230
+ } else if (rangeNote) {
231
+ hint = '.';
232
+ }
233
+ return `chemx ${entry.name}: unknown flag ${name}${hint}${rangeNote} Run \`chemx ${entry.name} --help\`.`;
228
234
  }
229
235
  return null;
230
236
  };
@@ -64,7 +64,7 @@ export const SEARCH_COMMANDS = [
64
64
  brief: 'Read a file by outline, symbol or lines',
65
65
  usage: 'chemx read <file>|<rev>:<file>[:N-M] [options]',
66
66
  summary: 'Token-minified file reader with AST outline and logic extraction.',
67
- description: 'Extracts structural outlines, logic skeletons, line ranges, or targeted symbol declarations, from the working tree or (rev:file) at a git revision. A file over the 100-line window reads as an outline; one with nothing to outline (yaml, json, txt, shell) reads as its first 100 lines of text with the next range named.',
67
+ description: 'Extracts structural outlines, logic skeletons, line ranges, or targeted symbol declarations, from the working tree or (rev:file) at a git revision. A file over the 100-line window reads as an outline; --full returns the whole file; --bare reads text without substituting an outline; range forms are <file>:N-M, --lines a-b, or --start=N --end=M.',
68
68
  flags: [
69
69
  { flag: '--outline, -o', desc: 'Signatures only' },
70
70
  { flag: '--logic, -l', desc: 'AST logic skeleton: control flow, guards and mutations' },
@@ -76,6 +76,8 @@ export const SEARCH_COMMANDS = [
76
76
  { flag: '--connections', desc: 'Include caller graph and dependent references alongside symbol' },
77
77
  { flag: '--strip-comments', desc: 'Remove all code comments' },
78
78
  { flag: '--compact', desc: 'Remove blank lines and indentation' },
79
+ { flag: '--lines=<range>', desc: 'Line range a-b (alias of :a-b)' },
80
+ { flag: '--full', desc: 'Read the whole file without truncation' },
79
81
  { flag: '--start=<N>', desc: 'Starting line number (1-indexed)' },
80
82
  { flag: '--end=<N>', desc: 'Ending line number (1-indexed)' },
81
83
  { flag: '--bare, -b', desc: 'Raw content without headers or line numbers' },
@@ -83,6 +85,8 @@ export const SEARCH_COMMANDS = [
83
85
  ],
84
86
  examples: [
85
87
  'chemx read src/store.ts --outline',
88
+ 'chemx read src/store.ts --lines 10-50',
89
+ 'chemx read src/store.ts --full',
86
90
  'chemx read HEAD~1:src/store.ts --symbol=save',
87
91
  'chemx read src/controller.ts --logic',
88
92
  'chemx read api.ts --symbol=login',
@@ -17,6 +17,20 @@ export const handleChemxRead = (args = {}, cwd = process.cwd()) => {
17
17
  let startLine = args.startLine;
18
18
  let endLine = args.endLine;
19
19
 
20
+ const hasArgsLines = Boolean(args.lines);
21
+ const isStartLineMissing = startLine === undefined;
22
+ const isEndLineMissing = endLine === undefined;
23
+ const shouldParseLinesArg = hasArgsLines && (isStartLineMissing || isEndLineMissing);
24
+ if (shouldParseLinesArg) {
25
+ const linesMatch = String(args.lines).match(/^(\d+)(?:[-:](\d+))?$/);
26
+ if (linesMatch) {
27
+ if (isStartLineMissing) startLine = parseInt(linesMatch[1], 10);
28
+ const hasEndInMatch = Boolean(linesMatch[2]);
29
+ const shouldSetEnd = isEndLineMissing && hasEndInMatch;
30
+ if (shouldSetEnd) endLine = parseInt(linesMatch[2], 10);
31
+ }
32
+ }
33
+
20
34
  const colonMatch = typeof rawPath === 'string' && rawPath.match(/^([^:]+):(\d+)(?:[-:](\d+))?$/);
21
35
  const hasColonRange = Boolean(colonMatch);
22
36
  if (hasColonRange) {
@@ -39,7 +53,10 @@ export const handleChemxRead = (args = {}, cwd = process.cwd()) => {
39
53
  compact: args.compact === true,
40
54
  startLine,
41
55
  endLine,
42
- rev: args.rev
56
+ full: args.full === true,
57
+ bare: Boolean(args.bare),
58
+ rev: args.rev,
59
+ hintSyntax: 'mcp'
43
60
  });
44
61
  const relFile = path.relative(targetCwd, targetPath) || res.file;
45
62
  res.file = args.rev ? `${args.rev}:${relFile}` : relFile;
@@ -54,7 +71,7 @@ export const handleChemxRead = (args = {}, cwd = process.cwd()) => {
54
71
 
55
72
  const isBare = Boolean(args.bare);
56
73
  if (isBare) {
57
- return res.content;
74
+ return res.trailer ? `${res.content}\n${res.trailer}` : res.content;
58
75
  }
59
76
 
60
77
  const ext = path.extname(targetPath).toLowerCase();
package/cli/reader-cli.js CHANGED
@@ -32,15 +32,20 @@ export const runReaderCli = (args, isCli = false) => {
32
32
  ` --backtrace=<name> Reverse caller chain card (uses the .chemx index)`,
33
33
  ` --connections References and blast radius card for the file or --symbol`,
34
34
  ` --symbol=<name> Declaration by AST range (also Class.method, store.action)`,
35
+ ` --lines=<range> Line range a-b (alias of :a-b)`,
36
+ ` --full Read the whole file without truncation`,
35
37
  ` --start=<N> Start line number (1-indexed)`,
36
38
  ` --end=<N> End line number (1-indexed)`,
37
39
  ` --strip-comments Blank out comments (token based; line numbers stay true)`,
38
40
  ` --compact Drop repeated blank lines (original numbers still shown)`,
41
+ ` --bare, -b Raw content without headers or line numbers`,
39
42
  ` --json Output result as minified JSON`,
40
43
  ` -h, --help Show this help message`,
41
44
  '',
42
45
  `${ANSI.BOLD}EXAMPLES${ANSI.RESET}`,
43
46
  ` chemx read src/store.ts --outline`,
47
+ ` chemx read src/store.ts --lines 10-50`,
48
+ ` chemx read src/store.ts --full`,
44
49
  ` chemx read src/controller.ts --outline --enrich`,
45
50
  ` chemx read src/card.vue --outline --enrich --trace=handleSubmit`,
46
51
  ` chemx read src/controller.ts --logic`,
@@ -52,7 +57,20 @@ export const runReaderCli = (args, isCli = false) => {
52
57
  return { help: true, success: true };
53
58
  }
54
59
 
55
- const nonFlagArgs = args.filter((a) => !a.startsWith('-'));
60
+ const skipIndices = new Set();
61
+ for (let i = 0; i < args.length; i++) {
62
+ const a = args[i];
63
+ const isValFlag = a === '-s' || a === '--symbol' || a === '--lines';
64
+ const nextArg = args[i + 1];
65
+ const hasNextNonFlag = Boolean(nextArg) && !nextArg.startsWith('-');
66
+ const shouldSkipNext = isValFlag && hasNextNonFlag;
67
+ if (shouldSkipNext) {
68
+ skipIndices.add(i + 1);
69
+ }
70
+ }
71
+
72
+ const isNonFlagCandidate = (a, idx) => !a.startsWith('-') && !skipIndices.has(idx);
73
+ const nonFlagArgs = args.filter(isNonFlagCandidate);
56
74
  let filePath = nonFlagArgs[0];
57
75
 
58
76
  if (!filePath) {
@@ -64,16 +82,46 @@ export const runReaderCli = (args, isCli = false) => {
64
82
  let startLine;
65
83
  let endLine;
66
84
 
85
+ let linesRange = null;
86
+ const linesEq = args.find((a) => a.startsWith('--lines='));
87
+ if (linesEq) {
88
+ linesRange = linesEq.slice('--lines='.length);
89
+ } else {
90
+ const linesIdx = args.findIndex((a) => a === '--lines');
91
+ const nextToken = linesIdx === -1 ? undefined : args[linesIdx + 1];
92
+ const hasSpacedLines = Boolean(nextToken) && !nextToken.startsWith('-');
93
+ if (hasSpacedLines) {
94
+ linesRange = nextToken;
95
+ }
96
+ }
97
+
98
+ const hasLinesRange = Boolean(linesRange);
99
+ if (hasLinesRange) {
100
+ const rangeMatch = linesRange.match(/^(\d+)(?:[-:](\d+))?$/);
101
+ if (rangeMatch) {
102
+ const isStartLineMissing = startLine === undefined;
103
+ if (isStartLineMissing) startLine = parseInt(rangeMatch[1], 10);
104
+ const hasEndInMatch = Boolean(rangeMatch[2]);
105
+ const isEndLineMissing = endLine === undefined;
106
+ const shouldSetEnd = isEndLineMissing && hasEndInMatch;
107
+ if (shouldSetEnd) endLine = parseInt(rangeMatch[2], 10);
108
+ }
109
+ }
110
+
67
111
  const colonMatch = filePath.match(/^([^:]+):(\d+)(?:[-:](\d+))?$/);
68
112
  if (colonMatch) {
69
113
  filePath = colonMatch[1];
70
- startLine = parseInt(colonMatch[2], 10);
114
+ const isStartLineMissing = startLine === undefined;
115
+ if (isStartLineMissing) startLine = parseInt(colonMatch[2], 10);
71
116
  const hasEndLineSuffix = Boolean(colonMatch[3]);
72
- if (hasEndLineSuffix) endLine = parseInt(colonMatch[3], 10);
117
+ const isEndLineMissing = endLine === undefined;
118
+ const shouldSetEnd = hasEndLineSuffix && isEndLineMissing;
119
+ if (shouldSetEnd) endLine = parseInt(colonMatch[3], 10);
73
120
  }
74
121
 
75
122
  const isJson = args.includes('--json') || args.includes('-j');
76
123
  const isBare = args.includes('--bare') || args.includes('-b');
124
+ const isFull = args.includes('--full');
77
125
  const isOutline = args.includes('--outline') || args.includes('-o');
78
126
  const isLogic = args.includes('--logic') || args.includes('-l');
79
127
  const isTemplate = args.includes('--template') || args.includes('-t');
@@ -137,6 +185,8 @@ export const runReaderCli = (args, isCli = false) => {
137
185
  symbol,
138
186
  startLine,
139
187
  endLine,
188
+ full: isFull,
189
+ bare: isBare,
140
190
  hintSyntax: 'cli',
141
191
  });
142
192
 
@@ -152,7 +202,8 @@ export const runReaderCli = (args, isCli = false) => {
152
202
  if (isJson) {
153
203
  process.stdout.write(JSON.stringify(res, null, 2) + '\n');
154
204
  } else if (isBare) {
155
- process.stdout.write((res.content || '') + '\n');
205
+ const output = res.trailer ? `${res.content || ''}\n${res.trailer}` : (res.content || '');
206
+ process.stdout.write(output + '\n');
156
207
  } else {
157
208
  const hasConflict = Boolean(res.conflict);
158
209
  if (hasConflict) process.stdout.write(`${ANSI.GOLD}⚠ ${res.conflict}${ANSI.RESET}\n`);
@@ -3,7 +3,7 @@
3
3
  * content, and a header that states where the slice sits in the file.
4
4
  */
5
5
 
6
- const VERBATIM_MODES = new Set(['range', 'symbol', 'template']);
6
+ const VERBATIM_MODES = new Set(['range', 'symbol', 'template', 'full']);
7
7
 
8
8
  export const isVerbatimMode = (mode) => VERBATIM_MODES.has(mode);
9
9
 
package/cli/reader.js CHANGED
@@ -222,6 +222,21 @@ export const readTokenOptimized = (targetPath, options = {}) => {
222
222
  let startLine = options.startLine;
223
223
  let endLine = options.endLine;
224
224
 
225
+ const hasOptionsLines = Boolean(options.lines);
226
+ const isStartLineMissing = startLine === undefined;
227
+ const isEndLineMissing = endLine === undefined;
228
+ const shouldParseOptionsLines = hasOptionsLines && (isStartLineMissing || isEndLineMissing);
229
+ if (shouldParseOptionsLines) {
230
+ const linesMatch = String(options.lines).match(/^(\d+)(?:[-:](\d+))?$/);
231
+ if (linesMatch) {
232
+ const needsStart = startLine === undefined;
233
+ if (needsStart) startLine = parseInt(linesMatch[1], 10);
234
+ const hasEndMatch = Boolean(linesMatch[2]);
235
+ const needsEnd = endLine === undefined && hasEndMatch;
236
+ if (needsEnd) endLine = parseInt(linesMatch[2], 10);
237
+ }
238
+ }
239
+
225
240
  const colonMatch = typeof targetPath === 'string' && targetPath.match(/^([^:]+):(\d+)(?:[-:](\d+))?$/);
226
241
  if (colonMatch) {
227
242
  rawPath = colonMatch[1];
@@ -326,12 +341,15 @@ export const readTokenOptimized = (targetPath, options = {}) => {
326
341
 
327
342
  const hasLineRange = typeof startLine === 'number' || typeof endLine === 'number';
328
343
  const autoThreshold = typeof options.autoOutlineThreshold === 'number' ? options.autoOutlineThreshold : 100;
344
+ const isFull = Boolean(options.full);
345
+ const isBare = Boolean(options.bare);
329
346
 
330
347
  // Read window: files longer than autoThreshold read without a symbol or slice return an AST
331
348
  // outline to protect context. A tool budget, not an architecture rule (AGENTS.md owns those).
332
349
  // A file with no outline to give (yaml, json, txt) falls through to the first window of text.
350
+ // --full and --bare (without explicit --outline) never substitute an outline.
333
351
  const isOverWindow = !hasLineRange && totalLines > autoThreshold;
334
- const canAutoOutline = isOverWindow && !conflictNote && hasOutlineExtractor(rawPath);
352
+ const canAutoOutline = isOverWindow && !conflictNote && !isFull && (!isBare || wantsOutline) && hasOutlineExtractor(rawPath);
335
353
  const autoOutline = canAutoOutline ? generateAstOutline(rawContent, rawPath) : '';
336
354
  const shouldAutoOutline = !isEmptyOutline(autoOutline);
337
355
  if (shouldAutoOutline) {
@@ -374,7 +392,7 @@ export const readTokenOptimized = (targetPath, options = {}) => {
374
392
 
375
393
  // Cap each slice to the read window (a tool budget, not an architecture rule)
376
394
  const maxLines = autoThreshold;
377
- const isCapped = (endIdx - startIdx) > maxLines;
395
+ const isCapped = !isFull && (endIdx - startIdx) > maxLines;
378
396
  if (isCapped) {
379
397
  endIdx = startIdx + maxLines;
380
398
  }
@@ -382,19 +400,21 @@ export const readTokenOptimized = (targetPath, options = {}) => {
382
400
  const slice = sliceWithTransforms(rawContent, resolvedPath, startIdx, endIdx, options);
383
401
  const isCliHint = options.hintSyntax === 'cli';
384
402
  const nextEnd = Math.min(totalLines, endIdx + maxLines);
403
+ const remaining = totalLines - endIdx;
385
404
  const formatTruncationHint = () => {
386
405
  if (!isCapped) return null;
406
+ const remainMsg = `${remaining} ${remaining === 1 ? 'line remains' : 'lines remain'}`;
387
407
  if (isCliHint) {
388
- return `// [Truncated at the ${maxLines}-line chemx read window. Use chemx read ${rawPath}:${endIdx + 1}-${nextEnd} to inspect subsequent lines]`;
408
+ return `// [Truncated at the ${maxLines}-line chemx read window. ${remainMsg}; use chemx read ${rawPath}:${endIdx + 1}-${nextEnd} to inspect subsequent lines]`;
389
409
  }
390
- return `// [Truncated at the ${maxLines}-line chemx read window. Use startLine=${endIdx + 1} to inspect subsequent lines]`;
410
+ return `// [Truncated at the ${maxLines}-line chemx read window. ${remainMsg}; use startLine=${endIdx + 1} to inspect subsequent lines]`;
391
411
  };
392
412
  const trailer = formatTruncationHint();
393
413
 
394
414
  return {
395
415
  file: rawPath,
396
416
  totalLines,
397
- mode: 'range',
417
+ mode: isFull ? 'full' : 'range',
398
418
  startLine: startIdx + 1,
399
419
  endLine: endIdx,
400
420
  lineCount: slice.lines.length,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@chemx/starter-kit",
3
- "version": "26.10.26-386",
3
+ "version": "26.10.29-360",
4
4
  "description": "Chemical X Protocol: Private drop-in architecture starter kit and capsule generator",
5
5
  "type": "module",
6
6
  "bin": {