@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 +1 -1
- package/cli/audit/history.js +2 -1
- package/cli/audit/rules-predicates.js +4 -3
- package/cli/commands/unknown-flags.js +9 -3
- package/cli/commands-schema-search.js +5 -1
- package/cli/mcp/tools-read.js +19 -2
- package/cli/reader-cli.js +55 -4
- package/cli/reader-format.js +1 -1
- package/cli/reader.js +25 -5
- package/package.json +1 -1
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). |
|
package/cli/audit/history.js
CHANGED
|
@@ -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
|
|
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 =
|
|
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)}
|
|
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
|
-
|
|
227
|
-
|
|
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;
|
|
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',
|
package/cli/mcp/tools-read.js
CHANGED
|
@@ -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
|
-
|
|
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
|
|
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
|
-
|
|
114
|
+
const isStartLineMissing = startLine === undefined;
|
|
115
|
+
if (isStartLineMissing) startLine = parseInt(colonMatch[2], 10);
|
|
71
116
|
const hasEndLineSuffix = Boolean(colonMatch[3]);
|
|
72
|
-
|
|
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
|
-
|
|
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`);
|
package/cli/reader-format.js
CHANGED
|
@@ -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.
|
|
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.
|
|
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,
|