@cspeach/cli 1.1.17 → 1.1.19
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/loop.js +143 -25
- package/dist/agent/providers/local-provider.js +4 -1
- package/dist/agent/repair-partial.js +76 -19
- package/dist/agent/tool-dispatch.js +80 -22
- 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 +25 -6
- package/dist/commands/compact.js +12 -9
- package/dist/commands/config-set.js +6 -2
- package/dist/commands/config-show.js +7 -5
- package/dist/commands/cost.js +41 -34
- 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-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 +15 -3
- package/dist/cost/pricing.js +6 -5
- 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/lock-contention.js +2 -2
- package/dist/one-shot.js +135 -40
- package/dist/projects/answer-blockers.js +6 -1
- package/dist/projects/handover-md.js +15 -14
- 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 +96 -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 +152 -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 +238 -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 +42 -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/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 +36 -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 +795 -332
- 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/resume.js +104 -36
- package/dist/session/store.js +69 -27
- 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/signing-public-key.js +1 -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-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/sap-read.js +16 -1
- package/dist/tools/sap-write.js +122 -10
- package/dist/tools/shell/shell_exec.js +30 -12
- package/dist/tools/syntax-state.js +67 -0
- package/dist/tools/todo.js +23 -18
- package/dist/tools/transport-fit.js +462 -0
- package/dist/tools/transport-resolution.js +3 -1
- package/dist/tools/transport.js +33 -7
- 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 +1 -0
- 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 +28 -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 +11 -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
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
|
|
@@ -28,12 +32,18 @@ function summariseCredits(entries) {
|
|
|
28
32
|
}
|
|
29
33
|
return { total, unknown };
|
|
30
34
|
}
|
|
31
|
-
|
|
35
|
+
/** Model ids can come from server-side model governance: one line, no control chars. */
|
|
36
|
+
const modelText = (m) => sanitizeControlChars(m).replace(/\s*\r?\n\s*/g, ' ').trim();
|
|
37
|
+
/**
|
|
38
|
+
* `emit` (piece 2) — Ink passes the chunkEmitter so the panel lands in the
|
|
39
|
+
* transcript, not above the React frame. Default: console.log (classic).
|
|
40
|
+
*/
|
|
41
|
+
export async function runCostCommand(sessionId, credits = globalCredits.snapshot(), emit = (l) => console.log(l)) {
|
|
32
42
|
const entries = await readCostLog(sessionId);
|
|
33
43
|
if (entries.length === 0) {
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
44
|
+
emit('');
|
|
45
|
+
emit(formatInfo('No cost data yet for this session. Run a turn first.').trimEnd());
|
|
46
|
+
emit('');
|
|
37
47
|
return;
|
|
38
48
|
}
|
|
39
49
|
const s = summarise(entries);
|
|
@@ -43,47 +53,44 @@ export async function runCostCommand(sessionId, credits = globalCredits.snapshot
|
|
|
43
53
|
// stay: they are a usage fact, not a COGS leak.
|
|
44
54
|
const inCredits = creditsActive();
|
|
45
55
|
const perTurn = summariseCredits(entries);
|
|
46
|
-
|
|
47
|
-
|
|
56
|
+
const turnsNote = `${s.turns} turn${s.turns === 1 ? '' : 's'}`;
|
|
57
|
+
emit('');
|
|
48
58
|
if (inCredits) {
|
|
49
59
|
const unit = credits?.unitLabel ?? 'credits';
|
|
50
60
|
// Prefer the live session tracker (balance deltas observed this run);
|
|
51
61
|
// fall back to the logged per-turn charges when it has nothing yet.
|
|
52
62
|
const total = credits && credits.sessionTotal > 0 ? credits.sessionTotal : perTurn.total;
|
|
53
|
-
|
|
54
|
-
if (credits?.balance != null)
|
|
55
|
-
|
|
56
|
-
}
|
|
63
|
+
emit(panelHeading(`Session usage so far: ${formatCredits(total, unit)}`, turnsNote));
|
|
64
|
+
if (credits?.balance != null)
|
|
65
|
+
emit(panelRow('Balance', formatCredits(credits.balance, unit)));
|
|
57
66
|
}
|
|
58
67
|
else {
|
|
59
|
-
|
|
68
|
+
emit(panelHeading(`Session cost so far: ${formatCost(s.totalCost)}`, turnsNote));
|
|
60
69
|
}
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
console.log(` cache create ${s.totalTokens.cacheCreate.toLocaleString().padStart(12)}`);
|
|
68
|
-
// Per-model split (Phase 2 will make this interesting)
|
|
70
|
+
emit('');
|
|
71
|
+
emit(chalk.bold('Tokens'));
|
|
72
|
+
emit(panelRow('input', s.totalTokens.input.toLocaleString().padStart(12)));
|
|
73
|
+
emit(panelRow('output', s.totalTokens.output.toLocaleString().padStart(12)));
|
|
74
|
+
emit(panelRow('cache read', s.totalTokens.cacheRead.toLocaleString().padStart(12)));
|
|
75
|
+
emit(panelRow('cache create', s.totalTokens.cacheCreate.toLocaleString().padStart(12)));
|
|
69
76
|
if (s.byModel.length > 1) {
|
|
70
|
-
|
|
71
|
-
|
|
77
|
+
emit('');
|
|
78
|
+
emit(chalk.bold('By model:'));
|
|
72
79
|
for (const m of s.byModel) {
|
|
73
80
|
// Credits mode: turn counts only. A per-model dollar split is a direct
|
|
74
81
|
// read on our per-model COGS.
|
|
75
82
|
const tail = inCredits ? '' : ` ${formatCost(m.cost)}`;
|
|
76
|
-
|
|
83
|
+
emit(panelRow(modelText(m.model), `${m.turns} turn${m.turns === 1 ? '' : 's'}${tail}`, 24));
|
|
77
84
|
}
|
|
78
85
|
}
|
|
79
86
|
else if (s.byModel[0]) {
|
|
80
|
-
|
|
81
|
-
|
|
87
|
+
emit('');
|
|
88
|
+
emit(panelRow('Model', modelText(s.byModel[0].model)));
|
|
82
89
|
}
|
|
83
|
-
// Recent turns — last 5
|
|
90
|
+
// Recent turns — last 5, most recent first. Piece 2: one count format, "N of M" (P1's countLabel).
|
|
84
91
|
const recent = entries.slice(-5).reverse();
|
|
85
|
-
|
|
86
|
-
|
|
92
|
+
emit('');
|
|
93
|
+
emit(chalk.bold(`Recent turns · ${countLabel(recent.length, entries.length)}`));
|
|
87
94
|
for (const e of recent) {
|
|
88
95
|
const tok = (e.tokens.input + e.tokens.output).toLocaleString();
|
|
89
96
|
// Credits mode: the per-turn figure is what the LEDGER charged (a balance
|
|
@@ -95,14 +102,14 @@ export async function runCostCommand(sessionId, credits = globalCredits.snapshot
|
|
|
95
102
|
? formatCredits(e.credits_charged, credits?.unitLabel ?? 'credits')
|
|
96
103
|
: CREDITS_PENDING)
|
|
97
104
|
: formatCost(e.cost);
|
|
98
|
-
|
|
105
|
+
emit(panelRow(`turn ${String(e.turn).padStart(3)}`, `${tok.padStart(10)} tok ${tail}`, 8));
|
|
99
106
|
}
|
|
100
107
|
if (inCredits && perTurn.unknown > 0) {
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
+ `their usage is included in a later turn's figure.`));
|
|
108
|
+
emit('');
|
|
109
|
+
emit(formatInfo(`${perTurn.unknown} turn${perTurn.unknown === 1 ? '' : 's'} had no balance reading — `
|
|
110
|
+
+ `their usage is included in a later turn's figure.`).trimEnd());
|
|
104
111
|
}
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
112
|
+
emit('');
|
|
113
|
+
emit(formatInfo(`Detailed JSONL: ~/.cspeach/sessions/${sessionId}-cost.jsonl`).trimEnd());
|
|
114
|
+
emit('');
|
|
108
115
|
}
|
package/dist/commands/help.js
CHANGED
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
import chalk from 'chalk';
|
|
2
2
|
import { SKILL_CATALOG } from '../skill-catalog.js';
|
|
3
|
+
import { costCommandDescription } from '../repl/builtin-commands.js';
|
|
4
|
+
import { panelHeading } from '../renderer/panel-rows.js';
|
|
5
|
+
import { paint } from '../renderer/theme.js';
|
|
6
|
+
import { uiWidth, truncateToWidth } from '../renderer/ui-width.js';
|
|
7
|
+
// I2's shortcuts table and note — one source for the `?` sheet and /help.
|
|
8
|
+
import { SHORTCUT_ROWS, SHORTCUTS_NOTE } from '../ui/widgets/shortcuts-sheet.js';
|
|
3
9
|
// Workflow order: plan → understand → modify → build → upgrade → ship.
|
|
4
10
|
const CATEGORY_ORDER = [
|
|
5
11
|
'Pre-Coding',
|
|
@@ -9,13 +15,45 @@ const CATEGORY_ORDER = [
|
|
|
9
15
|
'Upgrade / Migrate',
|
|
10
16
|
'Ship / Release',
|
|
11
17
|
];
|
|
12
|
-
const NAME_COL_PAD = 28; // padded width of
|
|
13
|
-
const
|
|
14
|
-
const DESC_GUTTER = 1; // 1 space between name col and description
|
|
15
|
-
const NAME_COL_WIDTH = NAME_GUTTER + NAME_COL_PAD + DESC_GUTTER; // 31
|
|
18
|
+
const NAME_COL_PAD = 28; // padded width of the command/skill column
|
|
19
|
+
const NAME_COL_WIDTH = 2 + NAME_COL_PAD + 1; // indent + column + gap = 31
|
|
16
20
|
const MAX_DESC_LINES = 2;
|
|
17
|
-
const MIN_DESC_WIDTH =
|
|
18
|
-
const
|
|
21
|
+
const MIN_DESC_WIDTH = 20;
|
|
22
|
+
const fixed = (s) => () => s;
|
|
23
|
+
/**
|
|
24
|
+
* Piece 2 §10 — /help grouped by what you want to do. Every palette command
|
|
25
|
+
* is listed (a test enforces it), plus /files and /ui look. The /cost text
|
|
26
|
+
* follows the cost display mode (credits for managed customers).
|
|
27
|
+
*/
|
|
28
|
+
export const HELP_GROUPS = [
|
|
29
|
+
{ title: 'Work on SAP', rows: [
|
|
30
|
+
{ name: '/transport [TR]', description: fixed('View or set the transport for this session') },
|
|
31
|
+
{ name: '/mode [advisory|gated|auto]', description: fixed('Set the write mode (Shift+Tab also cycles)') },
|
|
32
|
+
{ name: '/safety-mode on|off|status', description: fixed('Turn the Forge Rule confirmation cards on or off') },
|
|
33
|
+
{ name: '/rewind', description: fixed('Undo a write from this session (Esc Esc)') },
|
|
34
|
+
{ name: '/reroute <skill>', description: fixed('Re-run your last prompt with a different skill') },
|
|
35
|
+
] },
|
|
36
|
+
{ title: 'Local files', rows: [
|
|
37
|
+
{ name: '/files', description: fixed('Pick a saved project file and what to do with it') },
|
|
38
|
+
{ name: '@<file>', description: fixed('Attach a file from this folder to your prompt') },
|
|
39
|
+
{ name: '/export audit [file]', description: fixed('Write the signable session audit record to a file') },
|
|
40
|
+
] },
|
|
41
|
+
{ title: 'Session', rows: [
|
|
42
|
+
{ name: '/new', description: fixed('Next prompt picks a skill afresh (also /reset)') },
|
|
43
|
+
{ name: '/recap', description: fixed('Show the recap (last session, open tasks, plan)') },
|
|
44
|
+
{ name: '/cost', description: costCommandDescription },
|
|
45
|
+
{ name: '/context', description: fixed("Show what fills the model's context window") },
|
|
46
|
+
{ name: '/compact', description: fixed('Shrink a long session and keep the facts') },
|
|
47
|
+
{ name: '/help', description: fixed('This screen (also /skills)') },
|
|
48
|
+
{ name: '/exit', description: fixed('Quit CSPeach (also /quit)') },
|
|
49
|
+
] },
|
|
50
|
+
{ title: 'Look and settings', rows: [
|
|
51
|
+
{ name: '/ui [auto|ink]', description: fixed('Change the UI mode (applies after a relaunch)') },
|
|
52
|
+
{ name: '/ui look [panels|legacy]', description: fixed('Switch between the panels and legacy looks') },
|
|
53
|
+
] },
|
|
54
|
+
];
|
|
55
|
+
/** /help lists the `?` key itself first, then I2's sheet rows in their order. */
|
|
56
|
+
const SHORTCUTS = [['?', 'shortcuts'], ...SHORTCUT_ROWS];
|
|
19
57
|
/**
|
|
20
58
|
* Word-wrap a description into at most `maxLines` lines of `width` chars each.
|
|
21
59
|
* If content overflows, the last line is suffixed with `…`.
|
|
@@ -28,7 +66,6 @@ function wrapDescription(desc, width, maxLines) {
|
|
|
28
66
|
while (i < words.length && lines.length < maxLines) {
|
|
29
67
|
const word = words[i];
|
|
30
68
|
if (current.length === 0) {
|
|
31
|
-
// Word longer than width — hard-truncate
|
|
32
69
|
current = word.length <= width ? word : word.slice(0, width);
|
|
33
70
|
i++;
|
|
34
71
|
}
|
|
@@ -41,65 +78,66 @@ function wrapDescription(desc, width, maxLines) {
|
|
|
41
78
|
current = '';
|
|
42
79
|
}
|
|
43
80
|
}
|
|
44
|
-
if (current.length > 0 && lines.length < maxLines)
|
|
81
|
+
if (current.length > 0 && lines.length < maxLines)
|
|
45
82
|
lines.push(current);
|
|
46
|
-
}
|
|
47
|
-
// Overflow → suffix last line with " …"
|
|
48
83
|
if (i < words.length && lines.length > 0) {
|
|
49
84
|
const last = lines[lines.length - 1];
|
|
50
|
-
|
|
51
|
-
lines[lines.length - 1] = last + ' …';
|
|
52
|
-
}
|
|
53
|
-
else {
|
|
54
|
-
lines[lines.length - 1] = last.slice(0, Math.max(0, width - 2)) + ' …';
|
|
55
|
-
}
|
|
85
|
+
lines[lines.length - 1] = last.length + 2 <= width ? last + ' …' : last.slice(0, Math.max(0, width - 2)) + ' …';
|
|
56
86
|
}
|
|
57
87
|
return lines;
|
|
58
88
|
}
|
|
59
|
-
|
|
60
|
-
* Prints the flat-list /help screen grouped by category.
|
|
61
|
-
* Cyan category headings, bold skill names, default-color descriptions.
|
|
62
|
-
* Descriptions wrap to MAX_DESC_LINES lines max, aligned to the name column.
|
|
63
|
-
*/
|
|
89
|
+
const groupTitle = (t) => paint('sap', chalk.bold(t));
|
|
64
90
|
/**
|
|
65
91
|
* Phase D5 (2026-05-17) — accepts an optional emit callback so Ink mode
|
|
66
92
|
* can route output through chunkEmitter instead of console.log (which
|
|
67
93
|
* corrupts the React frame). Default keeps the classic-mode behaviour.
|
|
68
94
|
*/
|
|
69
95
|
export function printHelp(emit = (s) => console.log(s)) {
|
|
70
|
-
const
|
|
71
|
-
const descWidth = Math.max(MIN_DESC_WIDTH,
|
|
96
|
+
const width = uiWidth();
|
|
97
|
+
const descWidth = Math.max(MIN_DESC_WIDTH, width - NAME_COL_WIDTH);
|
|
72
98
|
const continuationIndent = ' '.repeat(NAME_COL_WIDTH);
|
|
99
|
+
const row = (name, desc) => ` ${chalk.bold(truncateToWidth(name, NAME_COL_PAD).padEnd(NAME_COL_PAD))} ${paint('dim', truncateToWidth(desc, descWidth))}`;
|
|
73
100
|
emit('');
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
const
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
101
|
+
emit(panelHeading('Commands', 'type / to search'));
|
|
102
|
+
for (const group of HELP_GROUPS) {
|
|
103
|
+
emit('');
|
|
104
|
+
emit(groupTitle(group.title));
|
|
105
|
+
for (const r of group.rows)
|
|
106
|
+
emit(row(r.name, r.description()));
|
|
107
|
+
if (group.title === 'Work on SAP') {
|
|
108
|
+
// Skills live under "Work on SAP", one dim sub-heading per category.
|
|
109
|
+
for (const category of CATEGORY_ORDER) {
|
|
110
|
+
const skills = SKILL_CATALOG.filter((s) => s.category === category);
|
|
111
|
+
if (skills.length === 0)
|
|
112
|
+
continue;
|
|
113
|
+
emit(` ${paint('dim', category)}`);
|
|
114
|
+
for (const skill of skills) {
|
|
115
|
+
const descLines = wrapDescription(skill.description, descWidth, MAX_DESC_LINES);
|
|
116
|
+
emit(` ${chalk.bold(('/' + skill.name).padEnd(NAME_COL_PAD))} ${descLines[0] ?? ''}`);
|
|
117
|
+
for (let j = 1; j < descLines.length; j++)
|
|
118
|
+
emit(`${continuationIndent}${descLines[j]}`);
|
|
119
|
+
}
|
|
86
120
|
}
|
|
87
121
|
}
|
|
88
|
-
emit('');
|
|
89
122
|
}
|
|
90
|
-
emit(
|
|
91
|
-
emit(
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
123
|
+
emit('');
|
|
124
|
+
emit(groupTitle('Shortcuts'));
|
|
125
|
+
// Pack "key action" pairs into lines no wider than the UI. The plain
|
|
126
|
+
// length is tracked alongside the painted line (no ANSI stripping).
|
|
127
|
+
let line = ' ';
|
|
128
|
+
let lineLen = 1;
|
|
129
|
+
for (const [key, action] of SHORTCUTS) {
|
|
130
|
+
const cellLen = 2 + key.length + 2 + action.length;
|
|
131
|
+
if (lineLen + cellLen > width && lineLen > 1) {
|
|
132
|
+
emit(line);
|
|
133
|
+
line = ' ';
|
|
134
|
+
lineLen = 1;
|
|
135
|
+
}
|
|
136
|
+
line += ` ${chalk.bold(key)} ${paint('dim', action)}`;
|
|
137
|
+
lineLen += cellLen;
|
|
138
|
+
}
|
|
139
|
+
if (lineLen > 1)
|
|
140
|
+
emit(line);
|
|
141
|
+
emit(` ${paint('dim', truncateToWidth(SHORTCUTS_NOTE, width - 2))}`);
|
|
104
142
|
emit('');
|
|
105
143
|
}
|
package/dist/commands/login.js
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
* 3) Print user_code prominently so customer can read it
|
|
8
8
|
* 4) Poll /v1/auth/device/poll every interval until verified or maxAttempts
|
|
9
9
|
* 5) Save api_key + customer_id to ~/.cspeach/auth.json
|
|
10
|
-
* 6) Call GET /v1/me — if trial_mode === 'byok', prompt for
|
|
10
|
+
* 6) Call GET /v1/me — if trial_mode === 'byok', prompt for the user's own API key
|
|
11
11
|
* 7) Re-save auth file with byokAnthropicKey if provided
|
|
12
12
|
*
|
|
13
13
|
* Dependencies (open, print, prompt) are injected so the command is
|
|
@@ -16,8 +16,16 @@
|
|
|
16
16
|
import keychain from '../auth/keychain.js';
|
|
17
17
|
import { writeAuthFile } from '../auth/auth-file.js';
|
|
18
18
|
import { fetchOrgAnthropicKey } from '../auth/org-anthropic-key.js';
|
|
19
|
+
import { formatFailure, formatAttention, formatInfo } from '../renderer/notice-shape.js';
|
|
20
|
+
import { panelRow } from '../renderer/panel-rows.js';
|
|
21
|
+
import { paint } from '../renderer/theme.js';
|
|
22
|
+
import { getGlyphs } from '../renderer/glyphs.js';
|
|
23
|
+
import { sanitizeControlChars } from '../renderer/sanitize.js';
|
|
19
24
|
const ANTHROPIC_KEYCHAIN_SERVICE = 'cspeach.anthropic';
|
|
20
25
|
const ANTHROPIC_KEYCHAIN_ACCOUNT = 'api-key';
|
|
26
|
+
/** Server-supplied text (user code, URL, email, plan) is one line with no control characters. */
|
|
27
|
+
const serverText = (v) => sanitizeControlChars(String(v ?? '')).replace(/\s*\r?\n\s*/g, ' ').trim();
|
|
28
|
+
const done = (text) => `${paint('ok', getGlyphs().done)} ${text}`;
|
|
21
29
|
export async function runLoginCommand(deps) {
|
|
22
30
|
const proxy = deps.proxyUrl.replace(/\/$/, '');
|
|
23
31
|
const pollInterval = deps.pollIntervalMs ?? 5000;
|
|
@@ -27,26 +35,26 @@ export async function runLoginCommand(deps) {
|
|
|
27
35
|
try {
|
|
28
36
|
const r = await fetch(`${proxy}/v1/auth/device/start`, { method: 'POST' });
|
|
29
37
|
if (!r.ok) {
|
|
30
|
-
deps.print(
|
|
38
|
+
deps.print(formatFailure({ what: 'Could not start sign-in', detail: [`HTTP ${r.status}`], next: 'check your network, then run cspeach login again' }).trimEnd());
|
|
31
39
|
return 2;
|
|
32
40
|
}
|
|
33
41
|
start = await r.json();
|
|
34
42
|
}
|
|
35
43
|
catch (err) {
|
|
36
|
-
deps.print(
|
|
44
|
+
deps.print(formatFailure({ what: 'Could not start sign-in', detail: [err instanceof Error ? err.message : String(err)], next: 'check your network, then run cspeach login again' }).trimEnd());
|
|
37
45
|
return 2;
|
|
38
46
|
}
|
|
39
47
|
// 2 + 3) Browser + user_code. The next line auto-opens the browser, so
|
|
40
48
|
// there's nothing for the user to do — just print honest "opening" wording.
|
|
41
49
|
deps.print('');
|
|
42
|
-
deps.print(
|
|
43
|
-
deps.print(
|
|
50
|
+
deps.print(panelRow('One-time code', serverText(start.user_code), 14));
|
|
51
|
+
deps.print(panelRow('Opening', serverText(start.verification_url_complete), 14));
|
|
44
52
|
deps.print('');
|
|
45
53
|
try {
|
|
46
54
|
await deps.open(start.verification_url_complete);
|
|
47
55
|
}
|
|
48
56
|
catch {
|
|
49
|
-
deps.print(
|
|
57
|
+
deps.print(formatAttention({ text: "The browser didn't open", next: `open ${start.verification_url_complete} yourself` }).trimEnd());
|
|
50
58
|
}
|
|
51
59
|
// 4) Poll.
|
|
52
60
|
let polled = null;
|
|
@@ -67,19 +75,19 @@ export async function runLoginCommand(deps) {
|
|
|
67
75
|
if (errBody.error === 'authorization_pending')
|
|
68
76
|
continue;
|
|
69
77
|
if (errBody.error === 'expired_token') {
|
|
70
|
-
deps.print(
|
|
78
|
+
deps.print(formatFailure({ what: 'The sign-in code expired', next: 'cspeach login' }).trimEnd());
|
|
71
79
|
return 3;
|
|
72
80
|
}
|
|
73
|
-
deps.print(
|
|
81
|
+
deps.print(formatFailure({ what: 'Sign-in failed', detail: [`HTTP ${r.status} · ${errBody.error ?? 'unknown'}`], next: 'cspeach login' }).trimEnd());
|
|
74
82
|
return 4;
|
|
75
83
|
}
|
|
76
84
|
catch (err) {
|
|
77
|
-
deps.print(`
|
|
85
|
+
deps.print(formatInfo(`Network blip, retrying (${err instanceof Error ? err.message : String(err)})`).trimEnd());
|
|
78
86
|
continue;
|
|
79
87
|
}
|
|
80
88
|
}
|
|
81
89
|
if (!polled) {
|
|
82
|
-
deps.print(
|
|
90
|
+
deps.print(formatFailure({ what: 'Timed out waiting for the browser sign-in', next: 'cspeach login' }).trimEnd());
|
|
83
91
|
return 5;
|
|
84
92
|
}
|
|
85
93
|
// 5) Save (non-BYOK part).
|
|
@@ -88,13 +96,13 @@ export async function runLoginCommand(deps) {
|
|
|
88
96
|
customerId: polled.customer_id,
|
|
89
97
|
byokAnthropicKey: null,
|
|
90
98
|
});
|
|
91
|
-
deps.print(
|
|
99
|
+
deps.print(done('Saved your CSPeach key to ~/.cspeach/auth.json'));
|
|
92
100
|
// 6) /v1/me — figure out if BYOK is required.
|
|
93
101
|
try {
|
|
94
102
|
const r = await fetch(`${proxy}/v1/me`, { headers: { Authorization: `Bearer ${polled.api_key}` } });
|
|
95
103
|
if (r.ok) {
|
|
96
104
|
const me = await r.json();
|
|
97
|
-
deps.print(
|
|
105
|
+
deps.print(done(`Signed in as ${serverText(me.email)} · ${serverText(me.plan)}`));
|
|
98
106
|
if (me.trial_mode === 'byok' || me.plan === 'byo-key') {
|
|
99
107
|
// Try to fetch the org's Anthropic key first — non-fatal if unavailable.
|
|
100
108
|
const orgKey = await fetchOrgAnthropicKey(proxy, polled.api_key, {
|
|
@@ -104,14 +112,14 @@ export async function runLoginCommand(deps) {
|
|
|
104
112
|
((svc, acct, k) => keychain.setPassword(svc, acct, k));
|
|
105
113
|
if (orgKey !== null) {
|
|
106
114
|
await setKey(ANTHROPIC_KEYCHAIN_SERVICE, ANTHROPIC_KEYCHAIN_ACCOUNT, orgKey);
|
|
107
|
-
deps.print(
|
|
115
|
+
deps.print(done("Using your organization's API key"));
|
|
108
116
|
}
|
|
109
117
|
else {
|
|
110
118
|
// Fall back to the manual paste prompt — behaviour unchanged.
|
|
111
119
|
deps.print('');
|
|
112
|
-
deps.print('This account is BYOK
|
|
113
|
-
|
|
114
|
-
const ak = (await deps.prompt('Paste your Anthropic API key (sk-ant
|
|
120
|
+
deps.print(formatInfo('This account is BYOK: you provide your own API key').trimEnd());
|
|
121
|
+
// The key-entry row — the one place the vendor and its key page are named.
|
|
122
|
+
const ak = (await deps.prompt('Paste your Anthropic API key (sk-ant-..., from https://console.anthropic.com/settings/keys): ')).trim();
|
|
115
123
|
if (ak.startsWith('sk-ant-') && ak.length > 20) {
|
|
116
124
|
// The non-BYOK save above just persisted these exact values,
|
|
117
125
|
// so reading them back from disk would be redundant.
|
|
@@ -120,18 +128,19 @@ export async function runLoginCommand(deps) {
|
|
|
120
128
|
customerId: polled.customer_id,
|
|
121
129
|
byokAnthropicKey: ak,
|
|
122
130
|
});
|
|
123
|
-
deps.print('
|
|
131
|
+
deps.print(done('API key saved'));
|
|
124
132
|
}
|
|
125
133
|
else {
|
|
126
|
-
deps.print(
|
|
134
|
+
deps.print(formatFailure({ what: 'That does not look like your own API key', detail: ['it must start with sk-ant-', 'skipped'], next: 'run cspeach login again later' }).trimEnd());
|
|
127
135
|
}
|
|
128
136
|
}
|
|
129
137
|
}
|
|
130
138
|
}
|
|
131
139
|
}
|
|
132
140
|
catch (err) {
|
|
133
|
-
deps.print(`
|
|
141
|
+
deps.print(formatInfo(`Could not read your account details (${err instanceof Error ? err.message : String(err)}) · sign-in still worked`).trimEnd());
|
|
134
142
|
}
|
|
143
|
+
deps.print(` ${paint('dim', 'Next:')} run cspeach to start`);
|
|
135
144
|
return 0;
|
|
136
145
|
}
|
|
137
146
|
function sleep(ms) {
|
package/dist/commands/logout.js
CHANGED
|
@@ -3,15 +3,19 @@
|
|
|
3
3
|
* will fall back to the keytar-stored API key if one is present.
|
|
4
4
|
*/
|
|
5
5
|
import { deleteAuthFile, readAuthFile } from '../auth/auth-file.js';
|
|
6
|
+
import { formatInfo } from '../renderer/notice-shape.js';
|
|
7
|
+
import { paint } from '../renderer/theme.js';
|
|
8
|
+
import { getGlyphs } from '../renderer/glyphs.js';
|
|
6
9
|
export async function runLogoutCommand(deps) {
|
|
7
10
|
const before = await readAuthFile();
|
|
8
11
|
await deleteAuthFile();
|
|
9
12
|
if (before) {
|
|
10
|
-
deps.print('
|
|
11
|
-
deps.print(
|
|
13
|
+
deps.print(`${paint('ok', getGlyphs().done)} Signed out · removed ~/.cspeach/auth.json`);
|
|
14
|
+
deps.print(` ${paint('dim', 'The REPL uses the keychain-stored key if you have one.')}`);
|
|
15
|
+
deps.print(` ${paint('dim', 'Next:')} cspeach login (to sign in again)`);
|
|
12
16
|
}
|
|
13
17
|
else {
|
|
14
|
-
deps.print('Already signed out
|
|
18
|
+
deps.print(formatInfo('Already signed out · no auth.json present').trimEnd());
|
|
15
19
|
}
|
|
16
20
|
return 0;
|
|
17
21
|
}
|