@cspeach/cli 1.1.14 → 1.1.16
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/README.md +63 -0
- package/dist/agent/loop.js +32 -7
- package/dist/agent/tool-dispatch.js +13 -9
- package/dist/cli.js +81 -27
- package/dist/commands/compact.js +6 -2
- package/dist/commands/project-context-impact.js +2 -2
- package/dist/commands/project.js +301 -0
- package/dist/commands/team.js +1166 -0
- package/dist/one-shot.js +2 -0
- package/dist/projects/image-attachments.js +155 -0
- package/dist/projects/save-command.js +20 -0
- package/dist/register/atc-file.js +802 -0
- package/dist/register/baseline.js +106 -0
- package/dist/register/measure.js +380 -0
- package/dist/register/records.js +159 -0
- package/dist/register/store.js +227 -0
- package/dist/repl/post-turn-status.js +2 -1
- package/dist/repl.js +16 -2
- package/dist/rewind/candidates.js +14 -1
- package/dist/rewind/restore.js +53 -19
- package/dist/router/classifier.js +24 -4
- package/dist/session/recap.js +8 -5
- package/dist/session/resume.js +37 -17
- package/dist/session/store.js +5 -3
- package/dist/session/user-prompt.js +23 -0
- package/dist/skill-catalog.js +33 -0
- package/dist/skills/bundled-skills.js +1 -1
- package/dist/tools/filesystem/extract-document.js +6 -0
- package/dist/tools/filesystem/extract-xlsx.js +139 -0
- package/dist/tools/filesystem/file-write.js +20 -0
- package/dist/tools/filesystem/read-document.js +15 -6
- package/dist/tools/include-snapshot.js +24 -0
- package/dist/tools/sap-write.js +19 -9
- package/dist/tools/snapshot.js +27 -0
- package/dist/ui/clipboard-image.js +125 -0
- package/dist/ui/footer.js +11 -1
- package/dist/ui/text-input.js +30 -2
- package/package.json +7 -4
package/README.md
CHANGED
|
@@ -128,6 +128,69 @@ If `cspeach doctor` fails:
|
|
|
128
128
|
|
|
129
129
|
If the REPL hangs on first prompt, check `https://api.cspeach.dev/health`.
|
|
130
130
|
|
|
131
|
+
## Working on team projects (contributors only)
|
|
132
|
+
|
|
133
|
+
`cspeach team …` and the `/team/` page are built on two sibling packages in this
|
|
134
|
+
repo:
|
|
135
|
+
|
|
136
|
+
- `register-core/` — the merge: records in, one picture out
|
|
137
|
+
- `sap-client/` — the SAP calls, including `atcRunSet`
|
|
138
|
+
|
|
139
|
+
The CLI depends on them as **`file:` links**, and pnpm **copies their `dist/`**
|
|
140
|
+
into `node_modules` at install time. Neither `dist/` is in git. So a fresh
|
|
141
|
+
checkout, or any checkout where somebody changed one of them and did not
|
|
142
|
+
rebuild, runs the CLI against whatever was copied last — usually a
|
|
143
|
+
`TypeError: … is not a function` in the middle of a `cspeach team refresh`.
|
|
144
|
+
|
|
145
|
+
`src/register/__tests__/linked-packages.test.ts` is the tripwire. When it fails
|
|
146
|
+
with *"The linked packages are out of date"*, run the recipe below.
|
|
147
|
+
|
|
148
|
+
### The recipe
|
|
149
|
+
|
|
150
|
+
Git Bash:
|
|
151
|
+
|
|
152
|
+
```bash
|
|
153
|
+
cd sap-client && npm run build
|
|
154
|
+
cd ../register-core && ../cspeach-cli/node_modules/.bin/tsc
|
|
155
|
+
cd ../cspeach-cli && pnpm install
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
Windows (PowerShell or cmd):
|
|
159
|
+
|
|
160
|
+
```text
|
|
161
|
+
cd sap-client
|
|
162
|
+
npm run build
|
|
163
|
+
cd ..\register-core
|
|
164
|
+
..\cspeach-cli\node_modules\.bin\tsc.CMD
|
|
165
|
+
cd ..\cspeach-cli
|
|
166
|
+
pnpm install
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
Then check the link really took:
|
|
170
|
+
|
|
171
|
+
```bash
|
|
172
|
+
grep -c ruleIdFromAtc cspeach-cli/node_modules/@cspeach/register-core/dist/rules.js # ≥ 1
|
|
173
|
+
grep -c atcRunSet cspeach-cli/node_modules/@cspeach/sap-client/dist/adt-client.js # ≥ 1
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
If either prints `0`, run `pnpm install --force` in `cspeach-cli/`.
|
|
177
|
+
|
|
178
|
+
Notes:
|
|
179
|
+
|
|
180
|
+
- **`npm run build` does not work in `register-core/`** in this checkout — its
|
|
181
|
+
devDependencies are not installed. Build it with the CLI's own compiler, as
|
|
182
|
+
above. `sap-client/` builds normally.
|
|
183
|
+
- **`dist/index.js` is only a re-export barrel**, so grep a real module
|
|
184
|
+
(`dist/rules.js`, `dist/register.js`) when you check a new name landed.
|
|
185
|
+
- **`pnpm install` rewrites one path separator in `cspeach-cli/pnpm-lock.yaml`.**
|
|
186
|
+
That is noise. Restore it before you commit:
|
|
187
|
+
`git checkout -- cspeach-cli/pnpm-lock.yaml`.
|
|
188
|
+
- The **viewer** reads `register-core` from SOURCE (a Vite alias) and needs none
|
|
189
|
+
of this.
|
|
190
|
+
- Both packages are **unpublished**. `prepublishOnly` blocks `npm publish` of
|
|
191
|
+
the CLI while either dependency is still a `file:` link; they have to be
|
|
192
|
+
published and the versions pinned before a CLI release.
|
|
193
|
+
|
|
131
194
|
## Links
|
|
132
195
|
|
|
133
196
|
- **Landing**: https://cspeach.dev
|
package/dist/agent/loop.js
CHANGED
|
@@ -30,6 +30,8 @@ import { parseFromFlag, coerceDocFromFlagToAttachment } from '../skills/promotio
|
|
|
30
30
|
import { input } from '@inquirer/prompts';
|
|
31
31
|
import { withInquirer } from '../repl/inquirer-guard.js';
|
|
32
32
|
import { expandTextFileAttachments } from '../projects/workspace.js';
|
|
33
|
+
import { expandImageAttachments, buildUserContent, trimHistoryImages } from '../projects/image-attachments.js';
|
|
34
|
+
import { expandImageChips } from '../ui/clipboard-image.js';
|
|
33
35
|
import { getSapSystemInfo, renderSapSystemBlock } from '../sap/system-info.js';
|
|
34
36
|
import { renderStandaloneContextBlock } from '../sap/standalone-profile.js';
|
|
35
37
|
import { renderStandardsBlock } from '../standards/standards-file.js';
|
|
@@ -45,6 +47,7 @@ import { buildSapConnectionContext } from './sap-connection-adapter.js';
|
|
|
45
47
|
import { getCurrentTransport } from '../repl/current-transport.js';
|
|
46
48
|
import { collectTurnAssistantText } from './turn-assistant-text.js';
|
|
47
49
|
import { checkAndMark, newGuardState } from './parallel-write-guard.js';
|
|
50
|
+
import { userPromptText } from '../session/user-prompt.js';
|
|
48
51
|
/**
|
|
49
52
|
* Author identity for project-file metadata. Reads CSPEACH_AUTHOR_NAME first,
|
|
50
53
|
* then platform USER/USERNAME, then a generic fallback. Role is fixed to
|
|
@@ -103,17 +106,15 @@ const SAVE_HOOK_SKILLS = new Set([
|
|
|
103
106
|
* per-process counter would re-inject; the content scan correctly
|
|
104
107
|
* short-circuits.
|
|
105
108
|
*
|
|
106
|
-
* - tool_result entries push `role: 'user'` with array-shaped content
|
|
107
|
-
*
|
|
108
|
-
*
|
|
109
|
+
* - tool_result entries push `role: 'user'` with array-shaped content;
|
|
110
|
+
* userPromptText skips those. An image-attachment prompt is also an
|
|
111
|
+
* array ([image…, text]) and IS checked (2026-09-23).
|
|
109
112
|
*
|
|
110
113
|
* Exported for unit testing only — the call site is one floor up in runTurn.
|
|
111
114
|
*/
|
|
112
115
|
export function hasProjectContextBlock(messages) {
|
|
113
116
|
for (const m of messages) {
|
|
114
|
-
if (m
|
|
115
|
-
continue;
|
|
116
|
-
if (typeof m.content === 'string' && m.content.includes('<project_context')) {
|
|
117
|
+
if (userPromptText(m)?.includes('<project_context')) {
|
|
117
118
|
return true;
|
|
118
119
|
}
|
|
119
120
|
}
|
|
@@ -332,6 +333,20 @@ export async function runTurn(params) {
|
|
|
332
333
|
userMessageForSave = rest;
|
|
333
334
|
}
|
|
334
335
|
}
|
|
336
|
+
// Tier 1 screenshots (2026-09-23) — image paths in the prompt (typed,
|
|
337
|
+
// @-referenced or dragged in) become image content blocks and the path
|
|
338
|
+
// becomes `[attached <name>]`. Runs BEFORE the text-file expansion so a
|
|
339
|
+
// path written inside an attached document is never pulled in. Too large
|
|
340
|
+
// or too many images → the turn is not sent (same early exit as --from).
|
|
341
|
+
// Pasted clipboard images arrive as [Image #N] chips — swap each for its
|
|
342
|
+
// saved file path first, so the step below attaches it like a typed path.
|
|
343
|
+
const imageExpansion = await expandImageAttachments(expandImageChips(userMessageForLLM), params.ctx.cwd, (line) => emit(chalk.dim(line)));
|
|
344
|
+
if (imageExpansion.error) {
|
|
345
|
+
emit(chalk.yellow(`
|
|
346
|
+
${imageExpansion.error}`));
|
|
347
|
+
return;
|
|
348
|
+
}
|
|
349
|
+
userMessageForLLM = imageExpansion.text;
|
|
335
350
|
// Phase D — `@<text-file>` attachment expansion.
|
|
336
351
|
// After --from has consumed its own @<token>, scan what's left for
|
|
337
352
|
// `@<filename>` references that resolve to .txt / .md files in the
|
|
@@ -416,7 +431,17 @@ export async function runTurn(params) {
|
|
|
416
431
|
const finalUserContent = projectContextBlock
|
|
417
432
|
? `${projectContextBlock}\n\n${enriched}`
|
|
418
433
|
: enriched;
|
|
419
|
-
|
|
434
|
+
// Images (if any) ride as blocks ahead of the text; with none this is the
|
|
435
|
+
// same plain string as before.
|
|
436
|
+
session.messages.push({ role: 'user', content: buildUserContent(finalUserContent, imageExpansion.images) });
|
|
437
|
+
if (imageExpansion.images.length > 0 && params.provider.name === 'local-openai-compat') {
|
|
438
|
+
// The local (OpenAI-compatible) provider keeps text blocks only.
|
|
439
|
+
emit(chalk.dim('(local model: the image is not sent — the model gets the text only)'));
|
|
440
|
+
}
|
|
441
|
+
const droppedImages = trimHistoryImages(session.messages);
|
|
442
|
+
if (droppedImages > 0) {
|
|
443
|
+
emit(chalk.dim(`(${droppedImages} older image(s) removed from the conversation to keep it under the size limit)`));
|
|
444
|
+
}
|
|
420
445
|
session.skill = params.skill;
|
|
421
446
|
/**
|
|
422
447
|
* key: `${tool_name}:${object}` — consecutive failures per (tool, target).
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
1
2
|
import { getTool, isStandalone } from '../tools/index.js';
|
|
2
3
|
import { NOT_CONNECTED_MESSAGE } from '../sap/offline-adt-client.js';
|
|
3
4
|
import { presentSafetyConfirmation } from '../repl/safety-confirm.js';
|
|
@@ -30,7 +31,7 @@ export async function dispatchTool(name, args, ctx) {
|
|
|
30
31
|
// GUIDE.md) — local scratch, not SAP objects. Rule 8 protects SAP batches;
|
|
31
32
|
// applying it here made the 2nd+ file (GUIDE.md) hit the batch card and,
|
|
32
33
|
// headless, get refused. Exempt from the gate AND from the batch count.
|
|
33
|
-
const scratchWrite = isScratchOutputWrite(tool, args);
|
|
34
|
+
const scratchWrite = isScratchOutputWrite(tool, args, ctx.cwd);
|
|
34
35
|
if (tool.isMutating && !scratchWrite && shouldGateRule8()) {
|
|
35
36
|
const r = await presentSafetyConfirmation({
|
|
36
37
|
rule: 'RULE_8_BATCH_PLAN',
|
|
@@ -83,21 +84,24 @@ export async function dispatchTool(name, args, ctx) {
|
|
|
83
84
|
}
|
|
84
85
|
}
|
|
85
86
|
/**
|
|
86
|
-
* True for a mutating filesystem tool whose target sits under
|
|
87
|
-
*
|
|
88
|
-
* Windows backslash form
|
|
89
|
-
*
|
|
87
|
+
* True for a mutating filesystem tool whose target sits under the project's
|
|
88
|
+
* own ./cspeach-out/ (cwd). Accepts `cspeach-out/…`, `./cspeach-out/…`, the
|
|
89
|
+
* Windows backslash form, and an ABSOLUTE path that resolves inside
|
|
90
|
+
* <cwd>/cspeach-out (2026-09-23: models write full paths, and a project deep
|
|
91
|
+
* in a synced folder hit the batch card on every file after the first).
|
|
92
|
+
* Rejects anything with a `..` segment and any cspeach-out outside cwd.
|
|
93
|
+
* Deliberately narrow: only this one folder is exempt.
|
|
90
94
|
*/
|
|
91
|
-
export function isScratchOutputWrite(tool, args) {
|
|
95
|
+
export function isScratchOutputWrite(tool, args, cwd = process.cwd()) {
|
|
92
96
|
if (!tool.isMutating || tool.category !== 'filesystem')
|
|
93
97
|
return false;
|
|
94
98
|
const raw = args?.path;
|
|
95
99
|
if (typeof raw !== 'string' || raw.length === 0)
|
|
96
100
|
return false;
|
|
97
101
|
const p = raw.replace(/\\/g, '/').replace(/^\.\//, '');
|
|
98
|
-
if (p.startsWith('/') || /^[A-Za-z]:\//.test(p))
|
|
99
|
-
return false;
|
|
100
102
|
if (p.split('/').some((seg) => seg === '..'))
|
|
101
103
|
return false;
|
|
102
|
-
|
|
104
|
+
const scratchRoot = path.resolve(cwd, 'cspeach-out');
|
|
105
|
+
const rel = path.relative(scratchRoot, path.resolve(cwd, p));
|
|
106
|
+
return rel === '' || (!rel.startsWith('..') && !path.isAbsolute(rel));
|
|
103
107
|
}
|
package/dist/cli.js
CHANGED
|
@@ -4,6 +4,11 @@ import { runAddSapWizard } from './sap/onboarding.js';
|
|
|
4
4
|
import { runGcOnStartup } from './session/gc.js';
|
|
5
5
|
import { checkForUpgrade } from './upgrade-check.js';
|
|
6
6
|
import { extractSapAliasFlag, extractNoSapFlag } from './cli-args.js';
|
|
7
|
+
/**
|
|
8
|
+
* The verbs that make `cspeach project …` a mistyped team command rather than
|
|
9
|
+
* a prompt somebody meant to send. See the check in `main`.
|
|
10
|
+
*/
|
|
11
|
+
const TEAM_SUBCOMMANDS = new Set(['init', 'join', 'lane', 'refresh', 'status', 'outbox', 'rule', 'leave']);
|
|
7
12
|
export async function main(argv) {
|
|
8
13
|
runGcOnStartup().catch(() => { });
|
|
9
14
|
checkForUpgrade().catch(() => { });
|
|
@@ -144,6 +149,47 @@ export async function main(argv) {
|
|
|
144
149
|
const cfg = await loadConfig();
|
|
145
150
|
return runWhoamiCommand({ proxyUrl: cfg.proxy_url, print: (s) => console.log(s) });
|
|
146
151
|
}
|
|
152
|
+
// A team project — many laptops, one picture (spec 2026-09-18).
|
|
153
|
+
if (args[0] === 'team') {
|
|
154
|
+
const { runTeamCommand } = await import('./commands/team.js');
|
|
155
|
+
const { getAuthorIdentity } = await import('./agent/loop.js');
|
|
156
|
+
return runTeamCommand(args.slice(1), {
|
|
157
|
+
print: (s) => console.log(s),
|
|
158
|
+
identity: () => getAuthorIdentity(),
|
|
159
|
+
noSap,
|
|
160
|
+
// Standalone: no connection is offered at all, so nothing in `team` can reach for one.
|
|
161
|
+
connect: noSap ? undefined : async (alias) => {
|
|
162
|
+
const { loadConfig } = await import('./config/loader.js');
|
|
163
|
+
const { getConnection } = await import('./sap/connection-manager.js');
|
|
164
|
+
const { AdtClient } = await import('@cspeach/sap-client');
|
|
165
|
+
const cfg = await loadConfig();
|
|
166
|
+
const name = alias ?? sapAlias ?? Object.keys(cfg.sap)[0];
|
|
167
|
+
if (!name || !cfg.sap[name])
|
|
168
|
+
throw new Error('No SAP system is configured. Run: cspeach config add-sap');
|
|
169
|
+
const adt = new AdtClient(await getConnection(name, cfg.sap[name]));
|
|
170
|
+
return {
|
|
171
|
+
sql: (q, max) => adt.sqlQuery(q, max),
|
|
172
|
+
system: { sid: name },
|
|
173
|
+
atc: { runSet: (refs, variant, opts) => adt.atcRunSet(refs, variant, opts) },
|
|
174
|
+
};
|
|
175
|
+
},
|
|
176
|
+
ask: async (question) => {
|
|
177
|
+
const { isHeadless } = await import('./renderer/tty.js');
|
|
178
|
+
if (isHeadless())
|
|
179
|
+
return '';
|
|
180
|
+
const { input } = await import('@inquirer/prompts');
|
|
181
|
+
return input({ message: question });
|
|
182
|
+
},
|
|
183
|
+
});
|
|
184
|
+
}
|
|
185
|
+
// Typed from yesterday's notes. Without this the words fall through to the
|
|
186
|
+
// one-shot path at the bottom of this function and are sent to the model as
|
|
187
|
+
// a PAID prompt on a machine that is configured and logged in. A hint, not
|
|
188
|
+
// an alias: the old family does not run, and nothing is renamed back.
|
|
189
|
+
if (args[0] === 'project' && TEAM_SUBCOMMANDS.has(args[1] ?? '')) {
|
|
190
|
+
console.log('Unknown command: cspeach project. Team projects use: cspeach team init|join|lane|refresh|status|outbox|rule|leave');
|
|
191
|
+
return 1;
|
|
192
|
+
}
|
|
147
193
|
if (args[0] === 'analyze' && args[1] === 'project-context') {
|
|
148
194
|
const { runProjectContextImpactCommand } = await import('./commands/project-context-impact.js');
|
|
149
195
|
return runProjectContextImpactCommand(args.slice(2));
|
|
@@ -162,32 +208,40 @@ export async function main(argv) {
|
|
|
162
208
|
return runRepl({ sapAlias, noSap });
|
|
163
209
|
}
|
|
164
210
|
function printHelp() {
|
|
165
|
-
console.log(`
|
|
166
|
-
CSPeach — AI-assisted ABAP development CLI
|
|
167
|
-
|
|
168
|
-
Usage:
|
|
169
|
-
cspeach Start interactive REPL
|
|
170
|
-
cspeach "<prompt>" One-shot — runs one turn and exits
|
|
171
|
-
cspeach --one-shot "<prompt>" Same as above, explicit flag
|
|
172
|
-
cspeach --sap-alias <name> Use a specific SAP alias (overrides default)
|
|
173
|
-
cspeach --no-sap Start without a SAP system (paste-mode skills only)
|
|
174
|
-
cspeach --resume [id] Resume a previous session (list if no id)
|
|
175
|
-
cspeach config add-sap Add a SAP system interactively
|
|
176
|
-
cspeach config standalone Set the standalone SAP profile (no system)
|
|
177
|
-
cspeach standards init Capture your team's ABAP development standards
|
|
178
|
-
cspeach configure Alias for \`cspeach config add-sap\`
|
|
179
|
-
cspeach config set <key> <value> Update a config key
|
|
180
|
-
cspeach config unset <key> Remove a config key (revert to default)
|
|
181
|
-
cspeach config show [key] Show config (full or single key)
|
|
182
|
-
cspeach doctor Diagnose install + connectivity
|
|
183
|
-
cspeach login Sign in via device-code flow
|
|
184
|
-
cspeach logout Sign out (keep keychain key, if any)
|
|
185
|
-
cspeach whoami Print current account info
|
|
186
|
-
cspeach
|
|
187
|
-
cspeach
|
|
188
|
-
cspeach
|
|
189
|
-
cspeach --
|
|
190
|
-
|
|
191
|
-
|
|
211
|
+
console.log(`
|
|
212
|
+
CSPeach — AI-assisted ABAP development CLI
|
|
213
|
+
|
|
214
|
+
Usage:
|
|
215
|
+
cspeach Start interactive REPL
|
|
216
|
+
cspeach "<prompt>" One-shot — runs one turn and exits
|
|
217
|
+
cspeach --one-shot "<prompt>" Same as above, explicit flag
|
|
218
|
+
cspeach --sap-alias <name> Use a specific SAP alias (overrides default)
|
|
219
|
+
cspeach --no-sap Start without a SAP system (paste-mode skills only)
|
|
220
|
+
cspeach --resume [id] Resume a previous session (list if no id)
|
|
221
|
+
cspeach config add-sap Add a SAP system interactively
|
|
222
|
+
cspeach config standalone Set the standalone SAP profile (no system)
|
|
223
|
+
cspeach standards init Capture your team's ABAP development standards
|
|
224
|
+
cspeach configure Alias for \`cspeach config add-sap\`
|
|
225
|
+
cspeach config set <key> <value> Update a config key
|
|
226
|
+
cspeach config unset <key> Remove a config key (revert to default)
|
|
227
|
+
cspeach config show [key] Show config (full or single key)
|
|
228
|
+
cspeach doctor Diagnose install + connectivity
|
|
229
|
+
cspeach login Sign in via device-code flow
|
|
230
|
+
cspeach logout Sign out (keep keychain key, if any)
|
|
231
|
+
cspeach whoami Print current account info
|
|
232
|
+
cspeach team init <folder> Lead: pull the object list from SAP, start a team project
|
|
233
|
+
cspeach team join <folder> Record your results against a team project (on this laptop only)
|
|
234
|
+
cspeach team lane add|list Lead: who owns which packages
|
|
235
|
+
cspeach team refresh --lane <id> Measure with ATC: what is verified, what did not hold
|
|
236
|
+
cspeach team status [folder] Where every object stands, who owns it, what is proven
|
|
237
|
+
cspeach team outbox What this laptop has recorded, and where
|
|
238
|
+
cspeach team rule T N <d> Lead: settle a conflicting decision
|
|
239
|
+
cspeach team leave Stop recording
|
|
240
|
+
cspeach analyze project-context Compare flag-on vs flag-off session metrics
|
|
241
|
+
cspeach --upgrade Upgrade to latest via npm
|
|
242
|
+
cspeach --version Print version
|
|
243
|
+
cspeach --help Show this help
|
|
244
|
+
|
|
245
|
+
Docs: https://cspeach.dev
|
|
192
246
|
`);
|
|
193
247
|
}
|
package/dist/commands/compact.js
CHANGED
|
@@ -29,6 +29,7 @@ import path from 'node:path';
|
|
|
29
29
|
import chalk from 'chalk';
|
|
30
30
|
import { sessionsDir } from '../config/paths.js';
|
|
31
31
|
import { saveSession } from '../session/store.js';
|
|
32
|
+
import { isUserPrompt } from '../session/user-prompt.js';
|
|
32
33
|
/** Keep this many recent user-prompt anchored turns verbatim. */
|
|
33
34
|
export const KEEP_RECENT_TURNS = 5;
|
|
34
35
|
/** Don't bother compacting if there's only a handful of older turns. */
|
|
@@ -58,8 +59,9 @@ keepRecentTurns = KEEP_RECENT_TURNS) {
|
|
|
58
59
|
let cutAt = 0;
|
|
59
60
|
for (let i = messages.length - 1; i >= 0; i--) {
|
|
60
61
|
const msg = messages[i];
|
|
61
|
-
// A user PROMPT (not a tool_result
|
|
62
|
-
|
|
62
|
+
// A user PROMPT (not a tool_result round) — string content, or an
|
|
63
|
+
// image-attachment prompt ([image…, text]).
|
|
64
|
+
if (isUserPrompt(msg)) {
|
|
63
65
|
userPromptsSeen++;
|
|
64
66
|
if (userPromptsSeen === keepRecentTurns) {
|
|
65
67
|
cutAt = i;
|
|
@@ -134,6 +136,8 @@ export function serialiseForSummariser(messages) {
|
|
|
134
136
|
return '';
|
|
135
137
|
if (b.type === 'text')
|
|
136
138
|
return b.text ?? '';
|
|
139
|
+
if (b.type === 'image')
|
|
140
|
+
return '[image]';
|
|
137
141
|
if (b.type === 'thinking')
|
|
138
142
|
return `[thinking: ${(b.thinking ?? '').slice(0, 200)}]`;
|
|
139
143
|
if (b.type === 'tool_use')
|
|
@@ -24,6 +24,7 @@ import fs from 'node:fs/promises';
|
|
|
24
24
|
import path from 'node:path';
|
|
25
25
|
import chalk from 'chalk';
|
|
26
26
|
import { sessionsDir } from '../config/paths.js';
|
|
27
|
+
import { userPromptText } from '../session/user-prompt.js';
|
|
27
28
|
/**
|
|
28
29
|
* Read all session JSONs in `dir` (defaults to sessionsDir()), classify
|
|
29
30
|
* each by whether the flag was on, and produce a comparison report.
|
|
@@ -134,8 +135,7 @@ async function readAllSessions(dir) {
|
|
|
134
135
|
function toMetrics(s) {
|
|
135
136
|
const firstUser = s.messages.find((m) => m.role === 'user');
|
|
136
137
|
const flagOn = firstUser !== undefined &&
|
|
137
|
-
|
|
138
|
-
firstUser.content.includes('<project_context');
|
|
138
|
+
(userPromptText(firstUser)?.includes('<project_context') ?? false);
|
|
139
139
|
// Count "turns" as user messages — each user message kicks off a model turn.
|
|
140
140
|
const turnCount = s.messages.filter((m) => m.role === 'user').length;
|
|
141
141
|
const usage = s.usage ?? { input_tokens: 0, output_tokens: 0, cache_read_input_tokens: 0 };
|