@cspeach/cli 1.1.18 → 1.1.20
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 +3 -4
- package/dist/agent/anthropic-provider.js +30 -10
- package/dist/agent/cache-keepalive.js +162 -0
- package/dist/agent/cold-prune.js +116 -0
- package/dist/agent/loop.js +804 -159
- package/dist/agent/provider-shape.js +263 -0
- package/dist/agent/providers/ai-hub-provider.js +17 -2
- package/dist/agent/providers/byok-provider.js +33 -3
- package/dist/agent/providers/local-provider.js +12 -2
- package/dist/agent/repair-partial.js +125 -7
- package/dist/agent/summarise-via-provider.js +6 -1
- package/dist/agent/system-prompt.js +38 -0
- package/dist/agent/tool-dispatch.js +66 -21
- package/dist/agent/tool-loading-pin.js +100 -0
- package/dist/agent/turn-error-ux.js +77 -26
- package/dist/agent/turn-stream.js +25 -3
- package/dist/approvals/adt-type.js +27 -0
- package/dist/approvals/advisory-prompt.js +48 -0
- package/dist/approvals/advisory-render.js +25 -11
- package/dist/approvals/approval-prompt.js +74 -24
- package/dist/approvals/jwt.js +2 -1
- package/dist/approvals/render.js +64 -36
- package/dist/approvals/risk-floor.js +53 -2
- package/dist/auth/api-key.js +30 -16
- package/dist/auth/keychain.js +0 -0
- package/dist/auth/me.js +25 -7
- package/dist/cli.js +36 -6
- package/dist/commands/auto-compact.js +33 -16
- package/dist/commands/compact.js +49 -11
- package/dist/commands/config-set.js +16 -3
- package/dist/commands/config-show.js +18 -5
- package/dist/commands/cost.js +54 -35
- package/dist/commands/help.js +88 -50
- package/dist/commands/login.js +28 -19
- package/dist/commands/logout.js +7 -3
- package/dist/commands/plan-audit.js +1 -0
- package/dist/commands/plan-chain.js +71 -77
- package/dist/commands/plan-continue.js +1 -0
- package/dist/commands/plan-resume.js +20 -12
- package/dist/commands/whoami.js +23 -9
- package/dist/config/loader.js +70 -5
- package/dist/cost/cost-log.js +62 -2
- package/dist/cost/pricing.js +11 -6
- package/dist/doctor/checks/forge-rules.js +13 -3
- package/dist/doctor/checks/keychain.js +6 -4
- package/dist/doctor/run.js +101 -20
- package/dist/index.js +5 -2
- package/dist/lib/graceful-exit.js +56 -0
- package/dist/lib/piped-prompt.js +65 -0
- package/dist/lib/spill-labels.js +13 -0
- package/dist/lock-contention.js +2 -2
- package/dist/models/resolve.js +93 -2
- package/dist/models/server-config.js +158 -3
- package/dist/one-shot.js +150 -45
- package/dist/projects/answer-blockers.js +6 -1
- package/dist/projects/handover-md.js +15 -14
- package/dist/projects/image-attachments.js +15 -2
- package/dist/projects/plan-run.js +97 -54
- package/dist/projects/promote-command.js +11 -2
- package/dist/projects/save-command.js +3 -2
- package/dist/projects/status.js +2 -1
- package/dist/projects/yes-no.js +12 -0
- package/dist/renderer/abap-inline.js +18 -14
- package/dist/renderer/answer-trim.js +69 -0
- package/dist/renderer/banners.js +8 -8
- package/dist/renderer/brand-settled.js +19 -0
- package/dist/renderer/change-card.js +156 -0
- package/dist/renderer/checklist-format.js +130 -0
- package/dist/renderer/color-mode.js +45 -0
- package/dist/renderer/error-detail.js +104 -0
- package/dist/renderer/fence-state.js +46 -0
- package/dist/renderer/footer-line.js +100 -0
- package/dist/renderer/glyphs.js +25 -0
- package/dist/renderer/goodbye.js +38 -0
- package/dist/renderer/legacy-palette.js +41 -0
- package/dist/renderer/look.js +30 -0
- package/dist/renderer/markdown.js +299 -92
- package/dist/renderer/notice-log.js +72 -0
- package/dist/renderer/notice-shape.js +48 -0
- package/dist/renderer/notices.js +40 -8
- package/dist/renderer/panel-rows.js +16 -0
- package/dist/renderer/pipeline.js +66 -0
- package/dist/renderer/progress-chatter.js +23 -17
- package/dist/renderer/rendering-mode.js +34 -0
- package/dist/renderer/routing-line.js +14 -0
- package/dist/renderer/sanitize.js +12 -0
- package/dist/renderer/severity.js +2 -7
- package/dist/renderer/startup-lines.js +154 -0
- package/dist/renderer/status-footer.js +52 -34
- package/dist/renderer/steering-echo.js +41 -0
- package/dist/renderer/syntax.js +31 -12
- package/dist/renderer/tables.js +5 -1
- package/dist/renderer/theme.js +44 -0
- package/dist/renderer/thinking-heartbeat.js +33 -2
- package/dist/renderer/todo-block.js +10 -49
- package/dist/renderer/tool-labels.js +269 -0
- package/dist/renderer/tool-widget.js +245 -51
- package/dist/renderer/trace.js +42 -0
- package/dist/renderer/transcript-flow.js +132 -0
- package/dist/renderer/tty.js +34 -1
- package/dist/renderer/ui-width.js +55 -0
- package/dist/renderer/verify-chain.js +4 -2
- package/dist/renderer/widget-fallback.js +58 -65
- package/dist/repl/bracketed-paste.js +7 -1
- package/dist/repl/builtin-commands.js +19 -8
- package/dist/repl/credential-handover-gate.js +28 -0
- package/dist/repl/current-transport.js +13 -0
- package/dist/repl/early-line-buffer.js +5 -2
- package/dist/repl/file-picker.js +54 -12
- package/dist/repl/ink-stdin-guard.js +66 -0
- package/dist/repl/inquirer-guard.js +59 -16
- package/dist/repl/inquirer-theme.js +27 -33
- package/dist/repl/paste-marker.js +19 -0
- package/dist/repl/plan-turn-end.js +20 -0
- package/dist/repl/post-turn-status.js +43 -11
- package/dist/repl/reroute-turn.js +21 -0
- package/dist/repl/reset-tty-stdin.js +44 -0
- package/dist/repl/restore-guard.js +22 -0
- package/dist/repl/resume-standalone.js +63 -0
- package/dist/repl/rule8-detector.js +11 -3
- package/dist/repl/safety-confirm.js +161 -93
- package/dist/repl/session-spend-line.js +8 -4
- package/dist/repl/themed-prompts.js +19 -0
- package/dist/repl/ui-look-command.js +75 -0
- package/dist/repl/update-method-preview-hook.js +48 -5
- package/dist/repl.js +863 -339
- package/dist/rewind/cli.js +3 -2
- package/dist/rewind/format.js +14 -9
- package/dist/rewind/restore.js +11 -0
- package/dist/router/classifier.js +47 -4
- package/dist/router/intent-extractor.js +24 -8
- package/dist/router/piped-routing.js +23 -0
- package/dist/router/resume-routing.js +16 -0
- package/dist/router/routing-failure.js +57 -0
- package/dist/sap/first-run-choice.js +1 -1
- package/dist/sap/onboarding.js +3 -2
- package/dist/sap/standalone-onboarding.js +1 -1
- package/dist/sap/system-info.js +4 -2
- package/dist/sap/unreachable.js +28 -0
- package/dist/sap-errors/clean-error-text.js +182 -0
- package/dist/session/interrupt-reason.js +39 -0
- package/dist/session/recap.js +36 -24
- package/dist/session/repin-model.js +18 -0
- package/dist/session/resume.js +104 -36
- package/dist/session/store.js +84 -28
- package/dist/session/user-prompt.js +21 -0
- package/dist/skill-catalog.js +7 -0
- package/dist/skills/bundled-skills.js +76 -69
- package/dist/skills/preamble.js +75 -0
- package/dist/skills/source-manifest.js +11 -1
- package/dist/standards/standards-init.js +1 -1
- package/dist/test-helpers/answer-any.js +46 -0
- package/dist/tools/approval.js +27 -11
- package/dist/tools/ask-question.js +47 -5
- package/dist/tools/filesystem/file-read.js +11 -1
- package/dist/tools/filesystem/file-write.js +19 -2
- package/dist/tools/fiori/fe-extend.js +16 -1
- package/dist/tools/fiori/fe-scaffold.js +105 -3
- package/dist/tools/fiori/html-escapes.js +28 -0
- package/dist/tools/fiori/metadata/audit.js +605 -0
- package/dist/tools/fiori/metadata/references.js +104 -0
- package/dist/tools/fiori/metadata/types.js +8 -0
- package/dist/tools/fiori/preview/app-guard.js +215 -0
- package/dist/tools/fiori/preview/env.js +193 -0
- package/dist/tools/fiori/preview/readiness.js +127 -0
- package/dist/tools/fiori/preview/registry.js +412 -0
- package/dist/tools/fiori/preview/start.js +469 -0
- package/dist/tools/fiori/samples/loader.js +20 -5
- package/dist/tools/fiori/scaffold.js +18 -6
- package/dist/tools/fiori/smoke/app-driver-page.js +372 -0
- package/dist/tools/fiori/smoke/app-driver.js +100 -0
- package/dist/tools/fiori/smoke/assertions.js +161 -24
- package/dist/tools/fiori/smoke/audit-columns.js +21 -0
- package/dist/tools/fiori/smoke/batch.js +273 -0
- package/dist/tools/fiori/smoke/draft-safety.js +241 -0
- package/dist/tools/fiori/smoke/draft-smoke.js +473 -0
- package/dist/tools/fiori/smoke/driver.js +171 -4
- package/dist/tools/fiori/smoke/evidence.js +35 -0
- package/dist/tools/fiori/smoke/labels-codes.js +207 -0
- package/dist/tools/fiori/smoke/nav-error.js +26 -0
- package/dist/tools/fiori/smoke/run-smoke.js +155 -12
- package/dist/tools/fiori/tools.js +651 -7
- package/dist/tools/fiori/ui5-version.js +16 -0
- package/dist/tools/index.js +8 -0
- package/dist/tools/local-build.js +6 -0
- package/dist/tools/result-spill.js +238 -0
- package/dist/tools/sap-read.js +74 -15
- package/dist/tools/sap-write.js +122 -10
- package/dist/tools/shell/shell_exec.js +39 -12
- package/dist/tools/subagent/adt-serial.js +33 -0
- package/dist/tools/subagent/agent_run.js +2 -0
- package/dist/tools/subagent/read_agent.js +178 -0
- package/dist/tools/subagent/reader-prompt.js +48 -0
- package/dist/tools/syntax-state.js +67 -0
- package/dist/tools/todo.js +26 -19
- package/dist/tools/tool-loading.js +255 -0
- package/dist/tools/tool-output-read.js +117 -0
- package/dist/tools/transport-fit.js +462 -0
- package/dist/tools/transport-resolution.js +3 -1
- package/dist/tools/transport.js +39 -8
- package/dist/tools/verify.js +184 -8
- package/dist/ui/app.js +284 -146
- package/dist/ui/approval-modal.js +41 -15
- package/dist/ui/ask-answer-text.js +17 -0
- package/dist/ui/body.js +25 -6
- package/dist/ui/card-slot.js +98 -0
- package/dist/ui/checklist.js +9 -0
- package/dist/ui/coaching-picker-classic.js +8 -5
- package/dist/ui/command-palette.js +6 -4
- package/dist/ui/confirm-request.js +18 -0
- package/dist/ui/context-grid.js +2 -1
- package/dist/ui/file-palette.js +6 -4
- package/dist/ui/files-card-emitter.js +19 -0
- package/dist/ui/footer-line.js +16 -0
- package/dist/ui/footer.js +105 -104
- package/dist/ui/input-wrap.js +122 -0
- package/dist/ui/interrupt.js +103 -0
- package/dist/ui/key-burst.js +117 -0
- package/dist/ui/line-resolution.js +4 -3
- package/dist/ui/live-area.js +71 -0
- package/dist/ui/login-banner.js +54 -43
- package/dist/ui/rewind-panel.js +76 -24
- package/dist/ui/role-props.js +47 -0
- package/dist/ui/sap-state-store.js +2 -1
- package/dist/ui/session-timeline.js +49 -3
- package/dist/ui/skill-picker.js +5 -47
- package/dist/ui/text-input.js +128 -24
- package/dist/ui/todo-emitter.js +19 -0
- package/dist/ui/turn-status-emitter.js +65 -2
- package/dist/ui/turn-status.js +16 -19
- package/dist/ui/use-card-request.js +44 -0
- package/dist/ui/widgets/ask-card.js +521 -0
- package/dist/ui/widgets/bar-chart.js +11 -5
- package/dist/ui/widgets/coaching-picker.js +50 -55
- package/dist/ui/widgets/confirm-card.js +193 -0
- package/dist/ui/widgets/dep-graph.js +3 -2
- package/dist/ui/widgets/diff-viewer.js +6 -5
- package/dist/ui/widgets/files-card.js +97 -0
- package/dist/ui/widgets/question-card.js +2 -1
- package/dist/ui/widgets/reclaim-raw-stdin.js +24 -0
- package/dist/ui/widgets/shortcuts-sheet.js +25 -0
- package/dist/ui/widgets/stack-frames.js +2 -1
- package/dist/upgrade-check.js +39 -8
- package/package.json +12 -2
- package/test-harness/fe-draft/app.ts +188 -0
- package/test-harness/fe-draft/fe-draft.e2e.test.ts +461 -0
- package/test-harness/fe-draft/manifest-enhancer.unit.test.ts +51 -0
- package/test-harness/fe-draft/routes.ts +224 -0
- package/test-harness/fe-draft/routes.unit.test.ts +45 -0
- package/test-harness/fe-draft/server.ts +225 -0
- package/test-harness/fe-draft/variants.ts +196 -0
- package/vitest.smoke-e2e.config.ts +20 -0
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Auto-compact orchestration — Phase 2 #9 (2026-05-16).
|
|
3
3
|
*
|
|
4
|
-
* Calls `runCompact` end-of-turn when the
|
|
5
|
-
*
|
|
6
|
-
* have passed since the last
|
|
4
|
+
* Calls `runCompact` end-of-turn when the last model call's full context
|
|
5
|
+
* (input + cache read + cache write — Task 19, ruling F12) has reached
|
|
6
|
+
* `compact.auto_threshold_tokens` AND enough turns have passed since the last
|
|
7
|
+
* auto-compact run. The caller passes the RESOLVED threshold (file > served
|
|
8
|
+
* `auto_compact_tokens` > 150 000; see models/resolve.ts).
|
|
7
9
|
*
|
|
8
10
|
* Why end-of-turn (not start-of-turn): firing at the end means the cost
|
|
9
11
|
* (one Haiku call to summarise) is amortised over the user's natural
|
|
@@ -21,7 +23,11 @@
|
|
|
21
23
|
*/
|
|
22
24
|
import chalk from 'chalk';
|
|
23
25
|
import { saveSession } from '../session/store.js';
|
|
24
|
-
import { runCompact,
|
|
26
|
+
import { runCompact, planCompaction, canCompact } from './compact.js';
|
|
27
|
+
/** The number the gate compares: the context size, else the legacy counter. */
|
|
28
|
+
function gateTokens(input) {
|
|
29
|
+
return input.lastContextTokens ?? input.sessionInputTokens ?? 0;
|
|
30
|
+
}
|
|
25
31
|
/**
|
|
26
32
|
* Pure decision function — no IO. Decides whether auto-compact should
|
|
27
33
|
* fire given the current session state and config. Extracted so the
|
|
@@ -29,11 +35,11 @@ import { runCompact, formatCompactResult, } from './compact.js';
|
|
|
29
35
|
* Summariser.
|
|
30
36
|
*/
|
|
31
37
|
export function shouldAutoCompact(input) {
|
|
32
|
-
const {
|
|
38
|
+
const { turnNumber, lastAutoCompactTurn, config } = input;
|
|
33
39
|
if (!config.auto_enabled) {
|
|
34
40
|
return { fire: false, reason: 'disabled' };
|
|
35
41
|
}
|
|
36
|
-
if (
|
|
42
|
+
if (gateTokens(input) < config.auto_threshold_tokens) {
|
|
37
43
|
return { fire: false, reason: 'below-threshold' };
|
|
38
44
|
}
|
|
39
45
|
// Throttle: don't bounce. If we just ran auto-compact a few turns ago
|
|
@@ -43,7 +49,10 @@ export function shouldAutoCompact(input) {
|
|
|
43
49
|
const turnsSince = lastAutoCompactTurn === undefined
|
|
44
50
|
? Number.POSITIVE_INFINITY
|
|
45
51
|
: turnNumber - lastAutoCompactTurn;
|
|
46
|
-
|
|
52
|
+
// Final review I4 — a marker ahead of the turn number (a legacy marker from
|
|
53
|
+
// the old user-message count, which shrinks after a compaction) never
|
|
54
|
+
// throttles; the loop now passes a monotonic per-session counter.
|
|
55
|
+
if (turnsSince >= 0 && turnsSince < config.min_turns_between_auto) {
|
|
47
56
|
return { fire: false, reason: 'throttled' };
|
|
48
57
|
}
|
|
49
58
|
return { fire: true };
|
|
@@ -58,19 +67,28 @@ export function shouldAutoCompact(input) {
|
|
|
58
67
|
export async function maybeAutoCompact(params) {
|
|
59
68
|
const { session, config, turnNumber, summarise } = params;
|
|
60
69
|
const emit = params.emit ?? ((line) => console.log(line));
|
|
61
|
-
const
|
|
70
|
+
const gate = {
|
|
71
|
+
lastContextTokens: session.last_context_tokens,
|
|
62
72
|
sessionInputTokens: session.usage?.input_tokens ?? 0,
|
|
73
|
+
};
|
|
74
|
+
const decision = shouldAutoCompact({
|
|
75
|
+
...gate,
|
|
63
76
|
turnNumber,
|
|
64
77
|
lastAutoCompactTurn: session.lastAutoCompactTurn,
|
|
65
78
|
config,
|
|
66
79
|
});
|
|
67
80
|
if (!decision.fire)
|
|
68
81
|
return;
|
|
69
|
-
//
|
|
70
|
-
//
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
82
|
+
// Task 19 — at 150k a session can cross the gate with too few user turns
|
|
83
|
+
// for runCompact to cut anything (it would return "nothing to do"). Stay
|
|
84
|
+
// silent then, instead of printing the line on every turn (canCompact is
|
|
85
|
+
// the predicate runCompact's own bail-outs use).
|
|
86
|
+
if (!canCompact(planCompaction(session.messages, config.keep_recent_turns), config.keep_recent_turns))
|
|
87
|
+
return;
|
|
88
|
+
// Task 19 — ONE short line, printed when the work is done so it can carry
|
|
89
|
+
// the backup path (the one-step rollback). A failure prints its own line.
|
|
90
|
+
const head = `⚙ auto-compact: context ${gateTokens(gate).toLocaleString('en-US')} tokens ≥ `
|
|
91
|
+
+ `${config.auto_threshold_tokens.toLocaleString('en-US')}`;
|
|
74
92
|
try {
|
|
75
93
|
const result = await runCompact({
|
|
76
94
|
session,
|
|
@@ -82,13 +100,12 @@ export async function maybeAutoCompact(params) {
|
|
|
82
100
|
// runCompact already saved the session inside its happy path, but we
|
|
83
101
|
// mutate one more field (the throttle marker) so re-save to capture it.
|
|
84
102
|
await saveSession(session);
|
|
103
|
+
emit(chalk.yellow(`${head} — older turns summarised · backup: ${result.backupPath}`));
|
|
85
104
|
}
|
|
86
|
-
emit(formatCompactResult(result, config.keep_recent_turns));
|
|
87
105
|
}
|
|
88
106
|
catch (err) {
|
|
89
107
|
// Defensive: never let a summariser blow take down the turn.
|
|
90
108
|
const msg = err instanceof Error ? err.message : String(err);
|
|
91
|
-
emit(chalk.yellow(`⚙ auto-compact bailed: ${msg}
|
|
92
|
-
emit(chalk.dim(' session left intact; /compact still available manually.'));
|
|
109
|
+
emit(chalk.yellow(`⚙ auto-compact bailed: ${msg} — session left intact; /compact still works.`));
|
|
93
110
|
}
|
|
94
111
|
}
|
package/dist/commands/compact.js
CHANGED
|
@@ -26,8 +26,11 @@
|
|
|
26
26
|
*/
|
|
27
27
|
import fs from 'node:fs/promises';
|
|
28
28
|
import path from 'node:path';
|
|
29
|
-
import chalk from 'chalk';
|
|
30
29
|
import { sessionsDir } from '../config/paths.js';
|
|
30
|
+
import { panelHeading, panelRow } from '../renderer/panel-rows.js';
|
|
31
|
+
import { formatInfo } from '../renderer/notice-shape.js';
|
|
32
|
+
import { paint } from '../renderer/theme.js';
|
|
33
|
+
import { getGlyphs } from '../renderer/glyphs.js';
|
|
31
34
|
import { saveSession } from '../session/store.js';
|
|
32
35
|
import { isUserPrompt } from '../session/user-prompt.js';
|
|
33
36
|
/** Keep this many recent user-prompt anchored turns verbatim. */
|
|
@@ -213,6 +216,32 @@ export function buildSummaryMessage(summary, droppedTurns, todos) {
|
|
|
213
216
|
].join('\n'),
|
|
214
217
|
};
|
|
215
218
|
}
|
|
219
|
+
const THINKING_BLOCK_TYPES = new Set(['thinking', 'redacted_thinking']);
|
|
220
|
+
/** Text left in an assistant message that held only thinking (an interrupted turn): the API rejects empty content. */
|
|
221
|
+
export const THINKING_ONLY_PLACEHOLDER = '(thinking removed by /compact)';
|
|
222
|
+
/**
|
|
223
|
+
* Task 7 — drop `thinking` / `redacted_thinking` blocks from an assistant
|
|
224
|
+
* message. User messages and string content are returned unchanged (the same
|
|
225
|
+
* object); every other block is kept as-is and in order. Never mutates the
|
|
226
|
+
* input. Idempotent.
|
|
227
|
+
*/
|
|
228
|
+
export function stripThinking(m) {
|
|
229
|
+
if (m.role !== 'assistant' || !Array.isArray(m.content))
|
|
230
|
+
return m;
|
|
231
|
+
const blocks = m.content;
|
|
232
|
+
if (!blocks.some((b) => THINKING_BLOCK_TYPES.has(b?.type ?? '')))
|
|
233
|
+
return m;
|
|
234
|
+
const rest = blocks.filter((b) => !THINKING_BLOCK_TYPES.has(b?.type ?? ''));
|
|
235
|
+
return { ...m, content: rest.length > 0 ? rest : [{ type: 'text', text: THINKING_ONLY_PLACEHOLDER }] };
|
|
236
|
+
}
|
|
237
|
+
/**
|
|
238
|
+
* Task 19 — true when `plan` leaves enough older history for runCompact to
|
|
239
|
+
* summarise. The one predicate runCompact's bail-outs and the auto-compact
|
|
240
|
+
* silent check share.
|
|
241
|
+
*/
|
|
242
|
+
export function canCompact(plan, keepRecentTurns) {
|
|
243
|
+
return plan.userPromptCount >= keepRecentTurns && plan.toSummarise.length >= MIN_COMPACT_THRESHOLD;
|
|
244
|
+
}
|
|
216
245
|
/**
|
|
217
246
|
* Top-level compact orchestration. Pure async function — caller wires it
|
|
218
247
|
* into the REPL slash dispatcher. Throws only on catastrophic IO failure
|
|
@@ -226,7 +255,8 @@ export async function runCompact(opts) {
|
|
|
226
255
|
const { session, summarise } = opts;
|
|
227
256
|
const keep = opts.keepRecentTurns ?? KEEP_RECENT_TURNS;
|
|
228
257
|
const plan = planCompaction(session.messages, keep);
|
|
229
|
-
// Bail if there's not enough older history to be worth compacting
|
|
258
|
+
// Bail if there's not enough older history to be worth compacting
|
|
259
|
+
// (the two checks canCompact combines; kept apart for their messages).
|
|
230
260
|
if (plan.userPromptCount < keep) {
|
|
231
261
|
return {
|
|
232
262
|
performed: false,
|
|
@@ -259,7 +289,15 @@ export async function runCompact(opts) {
|
|
|
259
289
|
// we accept losing the original first prompt because the summary should
|
|
260
290
|
// mention what the goal was.)
|
|
261
291
|
const summaryMsg = buildSummaryMessage(summary, plan.toSummarise.length, session.todos);
|
|
262
|
-
|
|
292
|
+
// Task 7 — the API's conversation check rejects a replayed thinking block
|
|
293
|
+
// whose earlier turns changed (Opus 5.5 binds each block to the exact prefix
|
|
294
|
+
// that produced it). The summary changed them, so the kept tail goes out
|
|
295
|
+
// without thinking; text and tool calls stay. The backup above keeps all.
|
|
296
|
+
const kept = plan.toKeep.map(stripThinking);
|
|
297
|
+
session.messages = [summaryMsg, ...kept];
|
|
298
|
+
// Task 19 — the recorded context size described the old history; the next
|
|
299
|
+
// call records the new one. Clearing it keeps the auto-compact gate honest.
|
|
300
|
+
delete session.last_context_tokens;
|
|
263
301
|
await saveSession(session);
|
|
264
302
|
return {
|
|
265
303
|
performed: true,
|
|
@@ -277,18 +315,18 @@ export function formatCompactResult(result,
|
|
|
277
315
|
/** Optional — falls back to the legacy constant for back-compat. */
|
|
278
316
|
keepRecentTurns = KEEP_RECENT_TURNS) {
|
|
279
317
|
if (!result.performed) {
|
|
280
|
-
return
|
|
318
|
+
return formatInfo(`/compact: ${result.reason ?? 'nothing to do'}`).trimEnd();
|
|
281
319
|
}
|
|
282
320
|
const lines = [
|
|
283
321
|
'',
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
322
|
+
panelHeading('Compacted', 'older turns summarised'),
|
|
323
|
+
panelRow('Dropped', `${result.droppedTurns} messages`),
|
|
324
|
+
panelRow('Kept', `${result.keptTurns} messages (most recent ${keepRecentTurns} user turns intact)`),
|
|
325
|
+
panelRow('Summary', `~${result.summaryTokens?.toLocaleString()} tokens`),
|
|
326
|
+
panelRow('Backup', `${result.backupPath}`),
|
|
289
327
|
'',
|
|
290
|
-
|
|
291
|
-
|
|
328
|
+
`${paint('ok', getGlyphs().done)} The next turn's context is roughly summary-size instead of the full history.`,
|
|
329
|
+
formatInfo('The very next turn rebuilds the prompt cache once; turns after that are cheap again.').trimEnd(),
|
|
292
330
|
'',
|
|
293
331
|
];
|
|
294
332
|
return lines.join('\n');
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import chalk from 'chalk';
|
|
2
|
+
import { formatAttention } from '../renderer/notice-shape.js';
|
|
2
3
|
import { loadConfig, saveConfig, isValidWriteMode } from '../config/loader.js';
|
|
3
4
|
import { STANDALONE_PLATFORMS, STANDALONE_DATABASES, STANDALONE_DEPLOYMENTS, STANDALONE_IDES, isStandalonePlatform, deriveDatabase, deriveDeployment, defaultAbapCloudOnly, defaultRelease, } from '../sap/standalone-profile.js';
|
|
4
5
|
/**
|
|
@@ -95,7 +96,7 @@ function errUnset(msg) {
|
|
|
95
96
|
* Internal, loader-derived flags that are never serialised (stripped by
|
|
96
97
|
* saveConfig). They must never be targets of `config set`/`config unset`.
|
|
97
98
|
*/
|
|
98
|
-
const INTERNAL_KEYS = new Set(['default_model_set', 'file_keys']);
|
|
99
|
+
const INTERNAL_KEYS = new Set(['default_model_set', 'effort_set', 'file_keys']);
|
|
99
100
|
/**
|
|
100
101
|
* Virtual keys whose values live in the OS keychain, not config.toml. `config
|
|
101
102
|
* unset` cannot remove them — that is a keychain operation — so they are
|
|
@@ -265,6 +266,10 @@ export async function runConfigUnset(args) {
|
|
|
265
266
|
}
|
|
266
267
|
const cfg = await loadConfig();
|
|
267
268
|
const removed = unsetConfigKey(cfg, key);
|
|
269
|
+
// Task 4 — `effort` and its explicit marker go together: removing the value
|
|
270
|
+
// without the marker would leave a stale marker for a later bare effort.
|
|
271
|
+
if (key === 'effort')
|
|
272
|
+
unsetConfigKey(cfg, 'effort_explicit');
|
|
268
273
|
if (!removed) {
|
|
269
274
|
console.log(chalk.dim(`${key} was not set — nothing to unset.`));
|
|
270
275
|
return;
|
|
@@ -350,8 +355,11 @@ export async function runConfigSet(args) {
|
|
|
350
355
|
}
|
|
351
356
|
cfg.sap[alias].insecure_skip_tls_verify = b;
|
|
352
357
|
if (b) {
|
|
353
|
-
console.log(
|
|
354
|
-
|
|
358
|
+
console.log(formatAttention({
|
|
359
|
+
text: `TLS verification is off for ${alias}`,
|
|
360
|
+
fact: 'dev only, never in production',
|
|
361
|
+
next: `cspeach config set sap.${alias}.ca_cert_path <path-to-corporate-CA>`,
|
|
362
|
+
}).trimEnd());
|
|
355
363
|
}
|
|
356
364
|
await saveConfig(cfg);
|
|
357
365
|
console.log(chalk.green(`Set ${key} = ${b}`));
|
|
@@ -455,6 +463,11 @@ export async function runConfigSet(args) {
|
|
|
455
463
|
err(`Invalid effort value "${value}". Allowed: ${EFFORT_VALUES.join(', ')}`);
|
|
456
464
|
}
|
|
457
465
|
cfg.effort = value;
|
|
466
|
+
// Task 4 (ruling F2) — the marker makes this an explicit act: only then
|
|
467
|
+
// does resolveEffort send output_config.effort. Kept on save even when
|
|
468
|
+
// the value equals the built-in default (xhigh).
|
|
469
|
+
cfg.effort_explicit = true;
|
|
470
|
+
markExplicit(cfg, 'effort_explicit');
|
|
458
471
|
break;
|
|
459
472
|
case 'telemetry':
|
|
460
473
|
if (!TELEMETRY_VALUES.includes(value)) {
|
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
import chalk from 'chalk';
|
|
2
2
|
import { loadConfig } from '../config/loader.js';
|
|
3
|
+
import { paint } from '../renderer/theme.js';
|
|
4
|
+
import { formatAttention } from '../renderer/notice-shape.js';
|
|
5
|
+
import { getCustomerModelConfig } from '../models/server-config.js';
|
|
3
6
|
/**
|
|
4
7
|
* Dotted paths of keys that must never appear in plaintext config output.
|
|
5
8
|
* SAP passwords live in the keychain and should never land in config.toml,
|
|
@@ -23,6 +26,10 @@ const REDACTED_KEYS = new Set([
|
|
|
23
26
|
const INTERNAL_KEYS = new Set([
|
|
24
27
|
'default_model_set',
|
|
25
28
|
'file_keys',
|
|
29
|
+
// Task 4 — effort_set is loader-derived; effort_explicit is the stored
|
|
30
|
+
// marker behind it (plumbing, not a user-facing setting).
|
|
31
|
+
'effort_set',
|
|
32
|
+
'effort_explicit',
|
|
26
33
|
]);
|
|
27
34
|
function formatTomlValue(value) {
|
|
28
35
|
if (typeof value === 'string')
|
|
@@ -57,7 +64,7 @@ function printLlmMissingHints(llm) {
|
|
|
57
64
|
for (const key of required) {
|
|
58
65
|
const value = llm[key];
|
|
59
66
|
if (value === undefined || value === null || value === '') {
|
|
60
|
-
console.log(`${key} = ${
|
|
67
|
+
console.log(`${key} = ${paint('bad', '<not set>')}`);
|
|
61
68
|
}
|
|
62
69
|
}
|
|
63
70
|
}
|
|
@@ -83,14 +90,14 @@ function printConfigAsToml(cfg, prefix = '') {
|
|
|
83
90
|
// the implication (writes proposed, never executed) without having to
|
|
84
91
|
// read the docs.
|
|
85
92
|
if (fullKey === 'write_mode' && v === 'advisory-only') {
|
|
86
|
-
console.log(
|
|
93
|
+
console.log(' ' + formatAttention({ text: 'Advisory mode', fact: 'writes are proposed, never executed' }).trimEnd());
|
|
87
94
|
}
|
|
88
95
|
}
|
|
89
96
|
}
|
|
90
97
|
for (const [k, v] of tables) {
|
|
91
98
|
const fullKey = prefix ? `${prefix}.${k}` : k;
|
|
92
99
|
console.log('');
|
|
93
|
-
console.log(chalk.
|
|
100
|
+
console.log(chalk.bold(`[${fullKey}]`));
|
|
94
101
|
printConfigAsToml(v, fullKey);
|
|
95
102
|
// Mode-aware extras for the `[llm]` section — print required keys that
|
|
96
103
|
// are empty as `<not set>` so a user who ran `cspeach config set llm.mode
|
|
@@ -136,6 +143,12 @@ export async function runConfigShow(args) {
|
|
|
136
143
|
console.log(chalk.gray('# ~/.cspeach/config.toml'));
|
|
137
144
|
printConfigAsToml(cfg);
|
|
138
145
|
console.log('');
|
|
146
|
+
// Task 12 — one trailing line when the account admin's model is enforced
|
|
147
|
+
// by the proxy (/v1/me/model-config). No snapshot ⇒ output unchanged.
|
|
148
|
+
const customer = getCustomerModelConfig();
|
|
149
|
+
if (customer?.model && customer.enforced) {
|
|
150
|
+
console.log(chalk.gray(`# model set by your account admin: ${customer.model} (enforced)`));
|
|
151
|
+
}
|
|
139
152
|
return;
|
|
140
153
|
}
|
|
141
154
|
const keyPath = args[0];
|
|
@@ -158,7 +171,7 @@ export async function runConfigShow(args) {
|
|
|
158
171
|
if (typeof resolved === 'object' && resolved !== null) {
|
|
159
172
|
// Sub-object (e.g. sap.S4H-DEV) — print as TOML section
|
|
160
173
|
console.log('');
|
|
161
|
-
console.log(chalk.
|
|
174
|
+
console.log(chalk.bold(`[${keyPath}]`));
|
|
162
175
|
for (const [k, v] of Object.entries(resolved)) {
|
|
163
176
|
const subKey = `${keyPath}.${k}`;
|
|
164
177
|
if (REDACTED_KEYS.has(subKey)) {
|
|
@@ -173,7 +186,7 @@ export async function runConfigShow(args) {
|
|
|
173
186
|
else {
|
|
174
187
|
console.log(String(resolved));
|
|
175
188
|
if (keyPath === 'write_mode' && resolved === 'advisory-only') {
|
|
176
|
-
console.log(
|
|
189
|
+
console.log(' ' + formatAttention({ text: 'Advisory mode', fact: 'writes are proposed, never executed' }).trimEnd());
|
|
177
190
|
}
|
|
178
191
|
}
|
|
179
192
|
}
|
package/dist/commands/cost.js
CHANGED
|
@@ -12,6 +12,10 @@ import chalk from 'chalk';
|
|
|
12
12
|
import { readCostLog, summarise } from '../cost/cost-log.js';
|
|
13
13
|
import { formatCost } from '../cost/pricing.js';
|
|
14
14
|
import { creditsActive, formatCredits, globalCredits, CREDITS_PENDING, } from '../cost/credits.js';
|
|
15
|
+
import { panelHeading, panelRow } from '../renderer/panel-rows.js';
|
|
16
|
+
import { formatInfo } from '../renderer/notice-shape.js';
|
|
17
|
+
import { countLabel } from '../renderer/checklist-format.js';
|
|
18
|
+
import { sanitizeControlChars } from '../renderer/sanitize.js';
|
|
15
19
|
/**
|
|
16
20
|
* Sum the credits the proxy charged across the logged turns. Entries written
|
|
17
21
|
* before credits existed (or on turns whose balance fetch failed) carry no
|
|
@@ -21,6 +25,9 @@ function summariseCredits(entries) {
|
|
|
21
25
|
let total = 0;
|
|
22
26
|
let unknown = 0;
|
|
23
27
|
for (const e of entries) {
|
|
28
|
+
// Final review I6 — a zero-token reader rollup line is not a turn.
|
|
29
|
+
if (e.kind === 'reader_rollup')
|
|
30
|
+
continue;
|
|
24
31
|
if (typeof e.credits_charged === 'number')
|
|
25
32
|
total += e.credits_charged;
|
|
26
33
|
else
|
|
@@ -28,12 +35,18 @@ function summariseCredits(entries) {
|
|
|
28
35
|
}
|
|
29
36
|
return { total, unknown };
|
|
30
37
|
}
|
|
31
|
-
|
|
38
|
+
/** Model ids can come from server-side model governance: one line, no control chars. */
|
|
39
|
+
const modelText = (m) => sanitizeControlChars(m).replace(/\s*\r?\n\s*/g, ' ').trim();
|
|
40
|
+
/**
|
|
41
|
+
* `emit` (piece 2) — Ink passes the chunkEmitter so the panel lands in the
|
|
42
|
+
* transcript, not above the React frame. Default: console.log (classic).
|
|
43
|
+
*/
|
|
44
|
+
export async function runCostCommand(sessionId, credits = globalCredits.snapshot(), emit = (l) => console.log(l)) {
|
|
32
45
|
const entries = await readCostLog(sessionId);
|
|
33
46
|
if (entries.length === 0) {
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
47
|
+
emit('');
|
|
48
|
+
emit(formatInfo('No cost data yet for this session. Run a turn first.').trimEnd());
|
|
49
|
+
emit('');
|
|
37
50
|
return;
|
|
38
51
|
}
|
|
39
52
|
const s = summarise(entries);
|
|
@@ -43,47 +56,53 @@ export async function runCostCommand(sessionId, credits = globalCredits.snapshot
|
|
|
43
56
|
// stay: they are a usage fact, not a COGS leak.
|
|
44
57
|
const inCredits = creditsActive();
|
|
45
58
|
const perTurn = summariseCredits(entries);
|
|
46
|
-
|
|
47
|
-
|
|
59
|
+
const turnsNote = `${s.turns} turn${s.turns === 1 ? '' : 's'}`;
|
|
60
|
+
emit('');
|
|
48
61
|
if (inCredits) {
|
|
49
62
|
const unit = credits?.unitLabel ?? 'credits';
|
|
50
63
|
// Prefer the live session tracker (balance deltas observed this run);
|
|
51
64
|
// fall back to the logged per-turn charges when it has nothing yet.
|
|
52
65
|
const total = credits && credits.sessionTotal > 0 ? credits.sessionTotal : perTurn.total;
|
|
53
|
-
|
|
54
|
-
if (credits?.balance != null)
|
|
55
|
-
|
|
56
|
-
}
|
|
66
|
+
emit(panelHeading(`Session usage so far: ${formatCredits(total, unit)}`, turnsNote));
|
|
67
|
+
if (credits?.balance != null)
|
|
68
|
+
emit(panelRow('Balance', formatCredits(credits.balance, unit)));
|
|
57
69
|
}
|
|
58
70
|
else {
|
|
59
|
-
|
|
71
|
+
emit(panelHeading(`Session cost so far: ${formatCost(s.totalCost)}`, turnsNote));
|
|
72
|
+
}
|
|
73
|
+
emit('');
|
|
74
|
+
emit(chalk.bold('Tokens'));
|
|
75
|
+
emit(panelRow('input', s.totalTokens.input.toLocaleString().padStart(12)));
|
|
76
|
+
emit(panelRow('output', s.totalTokens.output.toLocaleString().padStart(12)));
|
|
77
|
+
emit(panelRow('cache read', s.totalTokens.cacheRead.toLocaleString().padStart(12)));
|
|
78
|
+
emit(panelRow('cache create', s.totalTokens.cacheCreate.toLocaleString().padStart(12)));
|
|
79
|
+
// Task 18 fix M3 — reader subagent spend (already inside the total above).
|
|
80
|
+
// USD only: in credits mode the balance delta already carries it.
|
|
81
|
+
if (s.readerCalls > 0 && !inCredits) {
|
|
82
|
+
emit('');
|
|
83
|
+
emit(panelRow('Readers', `${s.readerCalls} run${s.readerCalls === 1 ? '' : 's'} ${formatCost(s.readerCost)}`));
|
|
60
84
|
}
|
|
61
|
-
// Token breakdown
|
|
62
|
-
console.log('');
|
|
63
|
-
console.log(chalk.dim('Tokens:'));
|
|
64
|
-
console.log(` input ${s.totalTokens.input.toLocaleString().padStart(12)}`);
|
|
65
|
-
console.log(` output ${s.totalTokens.output.toLocaleString().padStart(12)}`);
|
|
66
|
-
console.log(` cache read ${s.totalTokens.cacheRead.toLocaleString().padStart(12)}`);
|
|
67
|
-
console.log(` cache create ${s.totalTokens.cacheCreate.toLocaleString().padStart(12)}`);
|
|
68
|
-
// Per-model split (Phase 2 will make this interesting)
|
|
69
85
|
if (s.byModel.length > 1) {
|
|
70
|
-
|
|
71
|
-
|
|
86
|
+
emit('');
|
|
87
|
+
emit(chalk.bold('By model:'));
|
|
72
88
|
for (const m of s.byModel) {
|
|
73
89
|
// Credits mode: turn counts only. A per-model dollar split is a direct
|
|
74
90
|
// read on our per-model COGS.
|
|
75
91
|
const tail = inCredits ? '' : ` ${formatCost(m.cost)}`;
|
|
76
|
-
|
|
92
|
+
emit(panelRow(modelText(m.model), `${m.turns} turn${m.turns === 1 ? '' : 's'}${tail}`, 24));
|
|
77
93
|
}
|
|
78
94
|
}
|
|
79
95
|
else if (s.byModel[0]) {
|
|
80
|
-
|
|
81
|
-
|
|
96
|
+
emit('');
|
|
97
|
+
emit(panelRow('Model', modelText(s.byModel[0].model)));
|
|
82
98
|
}
|
|
83
|
-
// Recent turns — last 5
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
99
|
+
// Recent turns — last 5, most recent first. Piece 2: one count format, "N of M" (P1's countLabel).
|
|
100
|
+
// Final review I6 — reader rollup lines (interrupted turns) are not turns:
|
|
101
|
+
// they stay out of the list and the count (summarise() already skips them).
|
|
102
|
+
const turnEntries = entries.filter((e) => e.kind !== 'reader_rollup');
|
|
103
|
+
const recent = turnEntries.slice(-5).reverse();
|
|
104
|
+
emit('');
|
|
105
|
+
emit(chalk.bold(`Recent turns · ${countLabel(recent.length, turnEntries.length)}`));
|
|
87
106
|
for (const e of recent) {
|
|
88
107
|
const tok = (e.tokens.input + e.tokens.output).toLocaleString();
|
|
89
108
|
// Credits mode: the per-turn figure is what the LEDGER charged (a balance
|
|
@@ -95,14 +114,14 @@ export async function runCostCommand(sessionId, credits = globalCredits.snapshot
|
|
|
95
114
|
? formatCredits(e.credits_charged, credits?.unitLabel ?? 'credits')
|
|
96
115
|
: CREDITS_PENDING)
|
|
97
116
|
: formatCost(e.cost);
|
|
98
|
-
|
|
117
|
+
emit(panelRow(`turn ${String(e.turn).padStart(3)}`, `${tok.padStart(10)} tok ${tail}`, 8));
|
|
99
118
|
}
|
|
100
119
|
if (inCredits && perTurn.unknown > 0) {
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
+ `their usage is included in a later turn's figure.`));
|
|
120
|
+
emit('');
|
|
121
|
+
emit(formatInfo(`${perTurn.unknown} turn${perTurn.unknown === 1 ? '' : 's'} had no balance reading — `
|
|
122
|
+
+ `their usage is included in a later turn's figure.`).trimEnd());
|
|
104
123
|
}
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
124
|
+
emit('');
|
|
125
|
+
emit(formatInfo(`Detailed JSONL: ~/.cspeach/sessions/${sessionId}-cost.jsonl`).trimEnd());
|
|
126
|
+
emit('');
|
|
108
127
|
}
|