@cspeach/cli 1.1.44 → 1.1.46
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/dist/agent/chat-prompt.js +47 -0
- package/dist/agent/chat-unavailable.js +9 -0
- package/dist/agent/level-state.js +11 -0
- package/dist/agent/level.js +60 -0
- package/dist/agent/loop.js +110 -2
- package/dist/agent/parallel-write-guard.js +2 -0
- package/dist/agent/provider-shape.js +6 -2
- package/dist/agent/providers/ai-hub-provider.js +3 -1
- package/dist/agent/providers/azure-provider.js +3 -1
- package/dist/agent/providers/byok-provider.js +3 -1
- package/dist/agent/providers/local-provider.js +3 -1
- package/dist/agent/style-state.js +11 -0
- package/dist/agent/style.js +59 -0
- package/dist/agent/system-prompt.js +24 -3
- package/dist/agent/tool-dispatch.js +55 -0
- package/dist/approvals/jwt.js +5 -2
- package/dist/approvals/op-labels.js +39 -4
- package/dist/approvals/render.js +36 -9
- package/dist/approvals/risk-floor.js +14 -3
- package/dist/cli.js +8 -0
- package/dist/commands/config-set.js +59 -1
- package/dist/commands/config-show.js +1 -0
- package/dist/commands/export-docx.js +66 -0
- package/dist/commands/help.js +28 -2
- package/dist/config/loader.js +51 -1
- package/dist/docx/doc-meta.js +47 -0
- package/dist/docx/markdown-to-docx.js +250 -0
- package/dist/docx/write-docx.js +54 -0
- package/dist/one-shot.js +11 -0
- package/dist/projects/document-save.js +62 -0
- package/dist/projects/document-skills.js +9 -0
- package/dist/renderer/footer-line.js +6 -1
- package/dist/renderer/progress-chatter.js +9 -2
- package/dist/renderer/startup-lines.js +6 -2
- package/dist/renderer/tool-labels.js +5 -2
- package/dist/renderer/tool-widget.js +5 -1
- package/dist/repl/builtin-commands.js +4 -1
- package/dist/repl/chat-command.js +64 -0
- package/dist/repl/chat-lane.js +135 -0
- package/dist/repl/level-command.js +79 -0
- package/dist/repl/style-command.js +67 -0
- package/dist/repl.js +187 -105
- package/dist/sap/profile-features.js +5 -0
- package/dist/session/audit-export.js +50 -2
- package/dist/session/queued-step.js +5 -1
- package/dist/session/schema.js +1 -0
- package/dist/session/store.js +4 -0
- package/dist/tools/approval.js +59 -1
- package/dist/tools/hand-off.js +86 -0
- package/dist/tools/release-texts.js +9 -0
- package/dist/tools/sap-read.js +6 -2
- package/dist/tools/sap-run.js +163 -0
- package/dist/tools/tool-loading.js +11 -7
- package/dist/tools/transport.js +248 -22
- package/dist/tools/write-mode.js +2 -0
- package/dist/ui/approval-modal.js +11 -3
- package/dist/ui/sap-state-store.js +1 -0
- package/dist/ui/session-timeline.js +1 -1
- package/dist/ui/widgets/ask-card.js +2 -1
- package/package.json +3 -3
package/dist/repl.js
CHANGED
|
@@ -16,6 +16,10 @@ import { formatPanel } from './renderer/panel-rows.js';
|
|
|
16
16
|
import { sanitizeControlChars } from './renderer/sanitize.js';
|
|
17
17
|
import { formatRoutingLine } from './renderer/routing-line.js';
|
|
18
18
|
import { handleUiLook, handleUiRendering } from './repl/ui-look-command.js';
|
|
19
|
+
import { handleStyleCommand, pickStyleClassic, pickStyleInk } from './repl/style-command.js';
|
|
20
|
+
import { handleLevelCommand, pickLevelClassic, pickLevelInk } from './repl/level-command.js';
|
|
21
|
+
import { handleChatCommand } from './repl/chat-command.js';
|
|
22
|
+
import { ChatUnavailableError } from './agent/chat-unavailable.js';
|
|
19
23
|
import { CLI_VERSION } from './lib/version.js';
|
|
20
24
|
import { createOfflineAdtClient } from './sap/offline-adt-client.js';
|
|
21
25
|
import { STANDALONE_ALIAS } from './tools/index.js';
|
|
@@ -23,7 +27,11 @@ import { resumeStandaloneDecision, applySystemSwitch } from './repl/resume-stand
|
|
|
23
27
|
import { clearAppFlow } from './lane/app-flow.js';
|
|
24
28
|
import { clearJourneyFlow } from './journey/flow.js';
|
|
25
29
|
import { createDispatchSlot, queuedStepView } from './session/queued-step.js';
|
|
26
|
-
import { applyResumedSkill
|
|
30
|
+
import { applyResumedSkill } from './router/resume-routing.js';
|
|
31
|
+
import { ACK_PHRASES, wantsContinuation, isConversationalAck, decideLane, turnStatusLabel, applyLaneTools, chatFallbackOrigin } from './repl/chat-lane.js';
|
|
32
|
+
import { CHAT_SKILL } from './agent/chat-prompt.js';
|
|
33
|
+
import { setAssistantStyle } from './agent/style-state.js';
|
|
34
|
+
import { setAssistantLevel } from './agent/level-state.js';
|
|
27
35
|
import { readableInterruptReason, isUserStopReason } from './session/interrupt-reason.js';
|
|
28
36
|
import { getStore } from './auth/api-key.js';
|
|
29
37
|
import { readAuthFile } from './auth/auth-file.js';
|
|
@@ -152,10 +160,12 @@ import { applyLocalBuildConfig, applyStandaloneFileTools } from './tools/local-b
|
|
|
152
160
|
import './tools/sap-read.js';
|
|
153
161
|
import './tools/sap-write.js';
|
|
154
162
|
import './tools/transport.js';
|
|
163
|
+
import './tools/sap-run.js';
|
|
155
164
|
import './tools/approval.js';
|
|
156
165
|
import './tools/snapshot.js';
|
|
157
166
|
import './tools/ask-question.js';
|
|
158
167
|
import './tools/dispatch-skill.js';
|
|
168
|
+
import './tools/hand-off.js';
|
|
159
169
|
import './tools/todo.js';
|
|
160
170
|
import './tools/filesystem/file-read.js';
|
|
161
171
|
import './tools/tool-output-read.js';
|
|
@@ -210,68 +220,8 @@ function loadHistory() {
|
|
|
210
220
|
// and were unreachable.
|
|
211
221
|
.reverse());
|
|
212
222
|
}
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
* in session.skill, an ack-only follow-up should route there instead of
|
|
216
|
-
* re-classifying as a fresh prompt (which produces nonsense like "yes"
|
|
217
|
-
* → /abap-explain at 35% confidence). */
|
|
218
|
-
const ACK_PHRASES = new Set([
|
|
219
|
-
'yes', 'y', 'yeah', 'yep', 'yup', 'sure',
|
|
220
|
-
'ok', 'okay', 'k', 'kk',
|
|
221
|
-
'proceed', 'continue', 'go', 'go ahead', 'do it', 'do that', 'go on',
|
|
222
|
-
'no', 'n', 'nope', 'nah',
|
|
223
|
-
'stop', 'cancel', 'abort', 'never mind', 'nm',
|
|
224
|
-
'approve', 'approved', 'accept', 'accepted',
|
|
225
|
-
'decline', 'denied', 'reject', 'rejected',
|
|
226
|
-
'skip', 'next',
|
|
227
|
-
]);
|
|
228
|
-
/** Conversational acknowledgements that mean "I'm done with the previous
|
|
229
|
-
* turn, no further work needed." Distinct from ACK_PHRASES (which
|
|
230
|
-
* largely mean "yes/no to a question"). For these we don't classify,
|
|
231
|
-
* don't call any model — just print a friendly ack and return. The user
|
|
232
|
-
* isn't asking us to do more work; they're closing the prior thread.
|
|
233
|
-
*
|
|
234
|
-
* 2026-05-17 — Laeeq hit "thank you" being classified and run through
|
|
235
|
-
* /abap-explain, producing another huge code explanation. THIS is the
|
|
236
|
-
* fix for that. */
|
|
237
|
-
const CONVERSATIONAL_ACKS = new Set([
|
|
238
|
-
'thanks', 'thank you', 'thank u', 'thx', 'tx', 'ty', 'ta',
|
|
239
|
-
'cool', 'nice', 'great', 'awesome', 'perfect',
|
|
240
|
-
'got it', 'gotit', 'understood',
|
|
241
|
-
'cheers',
|
|
242
|
-
// Common ackowledgements that follow a long answer.
|
|
243
|
-
'makes sense',
|
|
244
|
-
]);
|
|
245
|
-
/**
|
|
246
|
-
* True when `trimmed` is one of CONVERSATIONAL_ACKS. We treat these as
|
|
247
|
-
* "user closes the previous thread, no further work" — print a friendly
|
|
248
|
-
* acknowledgement and return without classifying or calling any model.
|
|
249
|
-
*/
|
|
250
|
-
function isConversationalAck(trimmed) {
|
|
251
|
-
const normalized = trimmed.toLowerCase().trim().replace(/[.!?,]+$/, '');
|
|
252
|
-
return CONVERSATIONAL_ACKS.has(normalized);
|
|
253
|
-
}
|
|
254
|
-
/**
|
|
255
|
-
* Returns true when `trimmed` is a short acknowledgement-style follow-up
|
|
256
|
-
* AND there is a previous skill to continue. Treats both bare acks and
|
|
257
|
-
* short ack-prefixed phrases ("yes please", "no thanks") the same way.
|
|
258
|
-
* Strips trailing punctuation so "yes." and "yes!" still register.
|
|
259
|
-
*/
|
|
260
|
-
function isAckContinuation(trimmed, sessionSkill) {
|
|
261
|
-
if (!sessionSkill)
|
|
262
|
-
return false;
|
|
263
|
-
const normalized = trimmed.toLowerCase().trim().replace(/[.!?,]+$/, '');
|
|
264
|
-
if (ACK_PHRASES.has(normalized))
|
|
265
|
-
return true;
|
|
266
|
-
// "yes please", "go ahead and do it", "no thanks" — first word is an ack
|
|
267
|
-
// and the whole phrase is short enough to be a follow-up not a new task.
|
|
268
|
-
if (normalized.length <= 30) {
|
|
269
|
-
const firstWord = normalized.split(/\s+/)[0] ?? '';
|
|
270
|
-
if (ACK_PHRASES.has(firstWord))
|
|
271
|
-
return true;
|
|
272
|
-
}
|
|
273
|
-
return false;
|
|
274
|
-
}
|
|
223
|
+
// CONVERSATIONAL_ACKS / isConversationalAck live in repl/chat-lane.ts (moved
|
|
224
|
+
// unchanged — the chat-lane decision reads the same set).
|
|
275
225
|
/**
|
|
276
226
|
* Returns true when `trimmed` is a bare ack ("y", "n", "ok", "cancel", ...)
|
|
277
227
|
* with no surrounding context — regardless of whether a session.skill exists.
|
|
@@ -537,6 +487,15 @@ export async function runRepl(opts) {
|
|
|
537
487
|
}
|
|
538
488
|
let cfg = await loadConfig();
|
|
539
489
|
setLook(cfg.ui.look);
|
|
490
|
+
// Chat lane (spec 2026-10-17 D1/D2/D5/D9) — the process lane and talking
|
|
491
|
+
// style. `laneSetting` is a `let`: Task 14's `/chat on|off` and the D11
|
|
492
|
+
// fallback (Task 15) reassign it, and decideLane reads it on every message.
|
|
493
|
+
// applyLaneTools is the one place the hand_off flag is enabled (interactive
|
|
494
|
+
// chat lane only — piped stdin keeps today's tools array and router).
|
|
495
|
+
let laneSetting = cfg.assistant.lane;
|
|
496
|
+
applyLaneTools(laneSetting, !!process.stdin.isTTY);
|
|
497
|
+
setAssistantStyle(cfg.assistant.style);
|
|
498
|
+
setAssistantLevel(cfg.assistant.level);
|
|
540
499
|
// Apply the rendering mode BEFORE any shouldUseInk() call. This is the
|
|
541
500
|
// interactive entry point, so it is never one-shot whatever argv holds. A
|
|
542
501
|
// stored `classic` is retiring: read as auto, rewritten once, one dim notice
|
|
@@ -819,6 +778,8 @@ export async function runRepl(opts) {
|
|
|
819
778
|
user: standalone ? '' : cfg.sap[selectedAlias].username,
|
|
820
779
|
writeMode: startupWriteMode,
|
|
821
780
|
standalone,
|
|
781
|
+
// Chat lane is interactive-only (applyLaneTools); piped stdin never chats.
|
|
782
|
+
chatLane: laneSetting === 'chat' && !!process.stdin.isTTY,
|
|
822
783
|
});
|
|
823
784
|
// v0.6 — when resuming, adopt the on-disk SessionState verbatim. This
|
|
824
785
|
// preserves messages[], usage, last_skill, awaitingSkillAnswer, and the
|
|
@@ -930,6 +891,7 @@ export async function runRepl(opts) {
|
|
|
930
891
|
tools: localTools,
|
|
931
892
|
writeMode: startupWriteMode,
|
|
932
893
|
cwd: process.cwd(),
|
|
894
|
+
chat: laneSetting === 'chat' && !!process.stdin.isTTY,
|
|
933
895
|
};
|
|
934
896
|
if (opts?.resumedSession) {
|
|
935
897
|
const turnCount = session.messages.filter((m) => m.role === 'user' && typeof m.content === 'string').length;
|
|
@@ -978,7 +940,7 @@ export async function runRepl(opts) {
|
|
|
978
940
|
}
|
|
979
941
|
}
|
|
980
942
|
else if (getLook() === 'panels') {
|
|
981
|
-
process.stdout.write(panelsStartTail({ status: panelsStatus, third: panelsHintLine(localTools), notices: startupNotices }));
|
|
943
|
+
process.stdout.write(panelsStartTail({ status: panelsStatus, third: panelsHintLine(localTools, laneSetting), notices: startupNotices }));
|
|
982
944
|
}
|
|
983
945
|
else {
|
|
984
946
|
console.log(chalk.gray(`\nSession ${session.id} on ${selectedAlias}. Type /exit to quit.\n`));
|
|
@@ -1165,7 +1127,7 @@ export async function runRepl(opts) {
|
|
|
1165
1127
|
if (trimmed === '/help' || trimmed === '/skills') {
|
|
1166
1128
|
// Phase D5 — route through chunkEmitter so help lands in Body,
|
|
1167
1129
|
// not above the Ink frame (which would scroll off + corrupt).
|
|
1168
|
-
printHelp((line) => chunkEmitter.emit('chunk', line + '\n'));
|
|
1130
|
+
printHelp((line) => chunkEmitter.emit('chunk', line + '\n'), { lane: laneSetting });
|
|
1169
1131
|
return;
|
|
1170
1132
|
}
|
|
1171
1133
|
// /usage (2026-10-09) — this month in credits, from the proxy's ledger.
|
|
@@ -1189,8 +1151,14 @@ export async function runRepl(opts) {
|
|
|
1189
1151
|
if (trimmed === '/export' || trimmed.startsWith('/export ')) {
|
|
1190
1152
|
const { parseExportArgs, runExportAuditCommand } = await import('./commands/export-audit.js');
|
|
1191
1153
|
const { sub, filename } = parseExportArgs(trimmed.slice('/export'.length));
|
|
1154
|
+
// Word export (2026-10-19): /export docx <file>.md, same body as `cspeach export`.
|
|
1155
|
+
if (sub === 'docx') {
|
|
1156
|
+
const { docxFileFromReplArgs, runExportDocxCommand } = await import('./commands/export-docx.js');
|
|
1157
|
+
await runExportDocxCommand({ file: docxFileFromReplArgs(trimmed.slice('/export'.length)), docx: true, system: session.sap_system, emit: (l) => chunkEmitter.emit('chunk', l + '\n') });
|
|
1158
|
+
return;
|
|
1159
|
+
}
|
|
1192
1160
|
if (sub !== 'audit') {
|
|
1193
|
-
chunkEmitter.emit('chunk', chalk.dim('\nUsage: /export audit [filename]\n'));
|
|
1161
|
+
chunkEmitter.emit('chunk', chalk.dim('\nUsage: /export audit [filename] · /export docx <file>.md\n'));
|
|
1194
1162
|
return;
|
|
1195
1163
|
}
|
|
1196
1164
|
await runExportAuditCommand({
|
|
@@ -1317,6 +1285,24 @@ export async function runRepl(opts) {
|
|
|
1317
1285
|
await handleUiRendering(arg, cfg, saveConfig, emit, shouldUseInk() ? 'ink' : 'classic');
|
|
1318
1286
|
return;
|
|
1319
1287
|
}
|
|
1288
|
+
// /style [value] — talking style (spec 2026-10-17 D9); saved like /ui look.
|
|
1289
|
+
if (trimmed === '/style' || trimmed.startsWith('/style ')) {
|
|
1290
|
+
if (!cfg.assistant)
|
|
1291
|
+
cfg.assistant = { ...DEFAULT_CONFIG.assistant };
|
|
1292
|
+
const emit = (s) => chunkEmitter.emit('chunk', s + '\n');
|
|
1293
|
+
await handleStyleCommand(trimmed.slice('/style'.length), cfg, saveConfig, emit, pickStyleInk);
|
|
1294
|
+
return;
|
|
1295
|
+
}
|
|
1296
|
+
// /level [value] — who CSPeach is talking to (spec 2026-10-17 §10 D13); saved like /style.
|
|
1297
|
+
// The D15 tip reads the write mode in force now (a /mode change this session counts).
|
|
1298
|
+
if (trimmed === '/level' || trimmed.startsWith('/level ')) {
|
|
1299
|
+
if (!cfg.assistant)
|
|
1300
|
+
cfg.assistant = { ...DEFAULT_CONFIG.assistant };
|
|
1301
|
+
const emit = (s) => chunkEmitter.emit('chunk', s + '\n');
|
|
1302
|
+
const writeModeNow = getEffectiveWriteMode(cfg.write_mode, cfg.sap[selectedAlias]?.role);
|
|
1303
|
+
await handleLevelCommand(trimmed.slice('/level'.length), cfg, saveConfig, emit, pickLevelInk, writeModeNow);
|
|
1304
|
+
return;
|
|
1305
|
+
}
|
|
1320
1306
|
// /reroute — re-dispatch session.lastUserPrompt to a different skill
|
|
1321
1307
|
if (trimmed === '/reroute' || trimmed.startsWith('/reroute ')) {
|
|
1322
1308
|
session.awaitingSkillAnswer = false;
|
|
@@ -1384,7 +1370,19 @@ export async function runRepl(opts) {
|
|
|
1384
1370
|
todoEmitter.emit('update', []);
|
|
1385
1371
|
clearAppFlow(session); // Task 7 — /new ends an /abap-app flow
|
|
1386
1372
|
clearJourneyFlow(session); // Task 3 — and a clean-core journey
|
|
1387
|
-
chunkEmitter.emit('chunk', '\n' + formatInfo('Fresh start · the next prompt picks a skill again'));
|
|
1373
|
+
chunkEmitter.emit('chunk', '\n' + (laneSetting === 'chat' ? formatInfo('Fresh start.') : formatInfo('Fresh start · the next prompt picks a skill again')));
|
|
1374
|
+
return;
|
|
1375
|
+
}
|
|
1376
|
+
// /chat [on|off] (spec 2026-10-17 D6, owner amendment) — leave a workflow,
|
|
1377
|
+
// or switch the lane now and save it; the next message reads the new lane.
|
|
1378
|
+
if (trimmed === '/chat' || trimmed.startsWith('/chat ')) {
|
|
1379
|
+
if (!cfg.assistant)
|
|
1380
|
+
cfg.assistant = { ...DEFAULT_CONFIG.assistant };
|
|
1381
|
+
laneSetting = await handleChatCommand(trimmed.slice('/chat'.length), {
|
|
1382
|
+
lane: laneSetting, cfg, save: saveConfig, session, interactive: !!process.stdin.isTTY,
|
|
1383
|
+
emit: (s) => chunkEmitter.emit('chunk', '\n' + s + '\n'),
|
|
1384
|
+
});
|
|
1385
|
+
sapStateStore.update({ chatLane: laneSetting === 'chat' });
|
|
1388
1386
|
return;
|
|
1389
1387
|
}
|
|
1390
1388
|
// /transport — session current-transport state for Rule 9 (transport isolation)
|
|
@@ -1519,7 +1517,22 @@ export async function runRepl(opts) {
|
|
|
1519
1517
|
const previousObject = session.last_object ?? null;
|
|
1520
1518
|
const shortcutSkill = parsed.skill;
|
|
1521
1519
|
let skill = shortcutSkill;
|
|
1522
|
-
|
|
1520
|
+
// Chat lane (D1/D6/D-G) — decided here, once, before today's routing
|
|
1521
|
+
// block. Skills lane ⇒ always `explicit` or `route` (block unchanged).
|
|
1522
|
+
const laneDecision = decideLane({
|
|
1523
|
+
laneSetting, interactive: !!process.stdin.isTTY, explicitSkill: shortcutSkill, trimmed,
|
|
1524
|
+
sessionSkill: session.skill, awaitingSkillAnswer: session.awaitingSkillAnswer, resumedSkill,
|
|
1525
|
+
});
|
|
1526
|
+
if (laneDecision.kind === 'chat') {
|
|
1527
|
+
// M1 — just talk: no router, no routing line, no picker.
|
|
1528
|
+
skill = CHAT_SKILL;
|
|
1529
|
+
}
|
|
1530
|
+
else if (laneDecision.kind === 'continue') {
|
|
1531
|
+
// D6 — the workflow keeps the turn; the one existing line says so.
|
|
1532
|
+
chunkEmitter.emit('chunk', formatRoutingLine({ kind: 'continuing', skill: laneDecision.skill }));
|
|
1533
|
+
skill = laneDecision.skill;
|
|
1534
|
+
}
|
|
1535
|
+
else if (!skill) {
|
|
1523
1536
|
// v0.3.1 — if the last skill turn ended with a question, route the
|
|
1524
1537
|
// user's reply back to the same skill instead of reclassifying. This
|
|
1525
1538
|
// stops short answers like "1" or "S/4 on-prem" from being treated
|
|
@@ -1540,9 +1553,10 @@ export async function runRepl(opts) {
|
|
|
1540
1553
|
// Round 6 item 4c / review I2 — after a resume, a short follow-up
|
|
1541
1554
|
// ("continue", a number) stays in the session's skill; anything else
|
|
1542
1555
|
// is classified (applyResumedSkill below).
|
|
1543
|
-
|
|
1544
|
-
|
|
1545
|
-
skill
|
|
1556
|
+
const continuedSkill = wantsContinuation({ trimmed, sessionSkill: session.skill, awaitingSkillAnswer: session.awaitingSkillAnswer, resumedSkill });
|
|
1557
|
+
if (continuedSkill) {
|
|
1558
|
+
chunkEmitter.emit('chunk', formatRoutingLine({ kind: 'continuing', skill: continuedSkill }));
|
|
1559
|
+
skill = continuedSkill;
|
|
1546
1560
|
}
|
|
1547
1561
|
else if (isBareAck(trimmed)) {
|
|
1548
1562
|
// Bare "y" / "n" / "cancel" with no skill to continue — typing this
|
|
@@ -1968,7 +1982,7 @@ export async function runRepl(opts) {
|
|
|
1968
1982
|
// loop.ts) so the try/finally GUARANTEES the row clears even when the
|
|
1969
1983
|
// turn throws; loop.ts only narrates activity changes in between.
|
|
1970
1984
|
// (turnStatusEmitter is the module-level singleton import — BUG-2b.)
|
|
1971
|
-
turnStatusEmitter.start(
|
|
1985
|
+
turnStatusEmitter.start(turnStatusLabel(skill));
|
|
1972
1986
|
try {
|
|
1973
1987
|
// Task 6 (review M-2) — guarded chain start: announce and disable
|
|
1974
1988
|
// auto_approve for this phase's turn (request_approval consults the
|
|
@@ -2080,20 +2094,33 @@ export async function runRepl(opts) {
|
|
|
2080
2094
|
});
|
|
2081
2095
|
}
|
|
2082
2096
|
catch (err) {
|
|
2083
|
-
|
|
2084
|
-
|
|
2085
|
-
|
|
2086
|
-
|
|
2087
|
-
|
|
2088
|
-
|
|
2089
|
-
|
|
2090
|
-
|
|
2091
|
-
|
|
2092
|
-
|
|
2093
|
-
|
|
2094
|
-
|
|
2095
|
-
print
|
|
2096
|
-
|
|
2097
|
+
if (err instanceof ChatUnavailableError) {
|
|
2098
|
+
// D11 — this server has no chat lane (404/403 for _chat; the loop
|
|
2099
|
+
// already withdrew the turn). Switch this PROCESS to skill routing
|
|
2100
|
+
// (config is not saved) and re-run the same message once through
|
|
2101
|
+
// today's router; the queued step shows the one notice line in
|
|
2102
|
+
// place of a second echo. The re-run decides `route`, so it can
|
|
2103
|
+
// never throw ChatUnavailableError again.
|
|
2104
|
+
laneSetting = 'skills';
|
|
2105
|
+
sapStateStore.update({ chatLane: false });
|
|
2106
|
+
pendingDispatchAccessor.set(trimmed, chatFallbackOrigin());
|
|
2107
|
+
}
|
|
2108
|
+
else {
|
|
2109
|
+
// 2026-09-24 — print the turn-error block INTO the conversation (it
|
|
2110
|
+
// stays on screen) instead of the overlay box that vanished after 5 s
|
|
2111
|
+
// — a customer's dropped turn left no trace of what happened or what
|
|
2112
|
+
// to do. Same block classic mode prints; in-session it leads with
|
|
2113
|
+
// "type continue" (the partial turn is already saved).
|
|
2114
|
+
const { renderTurnError } = await import('./agent/turn-error-ux.js');
|
|
2115
|
+
turnCancelledByUser = currentTurnAbort?.signal.aborted ?? false;
|
|
2116
|
+
await renderTurnError({
|
|
2117
|
+
err,
|
|
2118
|
+
session,
|
|
2119
|
+
userInterrupted: turnCancelledByUser,
|
|
2120
|
+
inSession: true,
|
|
2121
|
+
print: (line) => chunkEmitter.emit('chunk', line + '\n'),
|
|
2122
|
+
});
|
|
2123
|
+
}
|
|
2097
2124
|
}
|
|
2098
2125
|
finally {
|
|
2099
2126
|
turnStatusEmitter.stop();
|
|
@@ -3133,7 +3160,7 @@ export async function runRepl(opts) {
|
|
|
3133
3160
|
}
|
|
3134
3161
|
// /help and /skills show the skill catalog flat-list.
|
|
3135
3162
|
if (trimmed === '/help' || trimmed === '/skills') {
|
|
3136
|
-
printHelp();
|
|
3163
|
+
printHelp(undefined, { lane: laneSetting });
|
|
3137
3164
|
continue;
|
|
3138
3165
|
}
|
|
3139
3166
|
// /usage (2026-10-09) — classic mirror of the Ink handler above.
|
|
@@ -3155,8 +3182,14 @@ export async function runRepl(opts) {
|
|
|
3155
3182
|
if (trimmed === '/export' || trimmed.startsWith('/export ')) {
|
|
3156
3183
|
const { parseExportArgs, runExportAuditCommand } = await import('./commands/export-audit.js');
|
|
3157
3184
|
const { sub, filename } = parseExportArgs(trimmed.slice('/export'.length));
|
|
3185
|
+
// Word export (2026-10-19): /export docx <file>.md, same body as `cspeach export`.
|
|
3186
|
+
if (sub === 'docx') {
|
|
3187
|
+
const { docxFileFromReplArgs, runExportDocxCommand } = await import('./commands/export-docx.js');
|
|
3188
|
+
await runExportDocxCommand({ file: docxFileFromReplArgs(trimmed.slice('/export'.length)), docx: true, system: session.sap_system, emit: (l) => console.log(l) });
|
|
3189
|
+
continue;
|
|
3190
|
+
}
|
|
3158
3191
|
if (sub !== 'audit') {
|
|
3159
|
-
console.log(chalk.dim('Usage: /export audit [filename]'));
|
|
3192
|
+
console.log(chalk.dim('Usage: /export audit [filename] · /export docx <file>.md'));
|
|
3160
3193
|
continue;
|
|
3161
3194
|
}
|
|
3162
3195
|
await runExportAuditCommand({
|
|
@@ -3290,6 +3323,21 @@ export async function runRepl(opts) {
|
|
|
3290
3323
|
await handleUiRendering(arg, cfg, saveConfig, (s) => console.log(s), shouldUseInk() ? 'ink' : 'classic');
|
|
3291
3324
|
continue;
|
|
3292
3325
|
}
|
|
3326
|
+
// /style [value] — talking style (spec 2026-10-17 D9); saved like /ui look.
|
|
3327
|
+
if (trimmed === '/style' || trimmed.startsWith('/style ')) {
|
|
3328
|
+
if (!cfg.assistant)
|
|
3329
|
+
cfg.assistant = { ...DEFAULT_CONFIG.assistant };
|
|
3330
|
+
await handleStyleCommand(trimmed.slice('/style'.length), cfg, saveConfig, (s) => console.log(s), pickStyleClassic);
|
|
3331
|
+
continue;
|
|
3332
|
+
}
|
|
3333
|
+
// /level [value] — who CSPeach is talking to (spec 2026-10-17 §10 D13); saved like /style.
|
|
3334
|
+
if (trimmed === '/level' || trimmed.startsWith('/level ')) {
|
|
3335
|
+
if (!cfg.assistant)
|
|
3336
|
+
cfg.assistant = { ...DEFAULT_CONFIG.assistant };
|
|
3337
|
+
const writeModeNow = getEffectiveWriteMode(cfg.write_mode, cfg.sap[selectedAlias]?.role);
|
|
3338
|
+
await handleLevelCommand(trimmed.slice('/level'.length), cfg, saveConfig, (s) => console.log(s), pickLevelClassic, writeModeNow);
|
|
3339
|
+
continue;
|
|
3340
|
+
}
|
|
3293
3341
|
// /reroute — re-dispatch session.lastUserPrompt to a different skill
|
|
3294
3342
|
if (trimmed === '/reroute' || trimmed.startsWith('/reroute ')) {
|
|
3295
3343
|
session.awaitingSkillAnswer = false;
|
|
@@ -3338,7 +3386,18 @@ export async function runRepl(opts) {
|
|
|
3338
3386
|
session.todos = []; // P2 — /new clears the plan (spec §4)
|
|
3339
3387
|
clearAppFlow(session); // Task 7 — /new ends an /abap-app flow
|
|
3340
3388
|
clearJourneyFlow(session); // Task 3 — and a clean-core journey
|
|
3341
|
-
console.log(formatInfo('Fresh start · the next prompt picks a skill again').trimEnd());
|
|
3389
|
+
console.log((laneSetting === 'chat' ? formatInfo('Fresh start.') : formatInfo('Fresh start · the next prompt picks a skill again')).trimEnd());
|
|
3390
|
+
continue;
|
|
3391
|
+
}
|
|
3392
|
+
// /chat [on|off] — classic mirror of the Ink handler above.
|
|
3393
|
+
if (trimmed === '/chat' || trimmed.startsWith('/chat ')) {
|
|
3394
|
+
if (!cfg.assistant)
|
|
3395
|
+
cfg.assistant = { ...DEFAULT_CONFIG.assistant };
|
|
3396
|
+
laneSetting = await handleChatCommand(trimmed.slice('/chat'.length), {
|
|
3397
|
+
lane: laneSetting, cfg, save: saveConfig, session, interactive: !!process.stdin.isTTY,
|
|
3398
|
+
emit: (s) => console.log(s),
|
|
3399
|
+
});
|
|
3400
|
+
sapStateStore.update({ chatLane: laneSetting === 'chat' });
|
|
3342
3401
|
continue;
|
|
3343
3402
|
}
|
|
3344
3403
|
// /transport — session current-transport state for Rule 9 (transport isolation)
|
|
@@ -3582,7 +3641,20 @@ export async function runRepl(opts) {
|
|
|
3582
3641
|
const previousSkill = session.skill; // snapshot before this turn dispatches
|
|
3583
3642
|
const previousObject = session.last_object ?? null; // carries from two turns ago
|
|
3584
3643
|
let skill = parsed.skill;
|
|
3585
|
-
|
|
3644
|
+
// Chat lane (D1/D6/D-G) — classic mirror of the Ink decision. Piped stdin
|
|
3645
|
+
// is a scripting surface and keeps today's router (interactive: false).
|
|
3646
|
+
const laneDecision = decideLane({
|
|
3647
|
+
laneSetting, interactive: !pipedStdin, explicitSkill: parsed.skill, trimmed,
|
|
3648
|
+
sessionSkill: session.skill, awaitingSkillAnswer: session.awaitingSkillAnswer, resumedSkill,
|
|
3649
|
+
});
|
|
3650
|
+
if (laneDecision.kind === 'chat') {
|
|
3651
|
+
skill = CHAT_SKILL;
|
|
3652
|
+
}
|
|
3653
|
+
else if (laneDecision.kind === 'continue') {
|
|
3654
|
+
console.log(formatRoutingLine({ kind: 'continuing', skill: laneDecision.skill }).trimEnd());
|
|
3655
|
+
skill = laneDecision.skill;
|
|
3656
|
+
}
|
|
3657
|
+
else if (!skill) {
|
|
3586
3658
|
// v0.3.1 — if the last skill turn ended with a question, route the
|
|
3587
3659
|
// user's reply back to the same skill instead of reclassifying. This
|
|
3588
3660
|
// stops short answers like "1" or "S/4 on-prem" from being treated
|
|
@@ -3597,9 +3669,10 @@ export async function runRepl(opts) {
|
|
|
3597
3669
|
continue;
|
|
3598
3670
|
}
|
|
3599
3671
|
// Round 6 item 4c / review I2 — see the Ink path.
|
|
3600
|
-
|
|
3601
|
-
|
|
3602
|
-
skill
|
|
3672
|
+
const continuedSkill = wantsContinuation({ trimmed, sessionSkill: session.skill, awaitingSkillAnswer: session.awaitingSkillAnswer, resumedSkill });
|
|
3673
|
+
if (continuedSkill) {
|
|
3674
|
+
console.log(formatRoutingLine({ kind: 'continuing', skill: continuedSkill }).trimEnd());
|
|
3675
|
+
skill = continuedSkill;
|
|
3603
3676
|
}
|
|
3604
3677
|
else if (isBareAck(trimmed)) {
|
|
3605
3678
|
// Bare "y" / "n" / "cancel" with no skill to continue — almost always
|
|
@@ -4092,18 +4165,27 @@ export async function runRepl(opts) {
|
|
|
4092
4165
|
});
|
|
4093
4166
|
}
|
|
4094
4167
|
catch (err) {
|
|
4095
|
-
|
|
4096
|
-
|
|
4097
|
-
|
|
4098
|
-
|
|
4099
|
-
|
|
4100
|
-
|
|
4101
|
-
|
|
4102
|
-
|
|
4103
|
-
|
|
4104
|
-
|
|
4105
|
-
|
|
4106
|
-
|
|
4168
|
+
if (err instanceof ChatUnavailableError) {
|
|
4169
|
+
// D11 — classic mirror of the Ink catch: process lane only, no save;
|
|
4170
|
+
// the loop-top slot consumer re-runs the message once.
|
|
4171
|
+
laneSetting = 'skills';
|
|
4172
|
+
sapStateStore.update({ chatLane: false });
|
|
4173
|
+
pendingDispatchAccessor.set(trimmed, chatFallbackOrigin());
|
|
4174
|
+
}
|
|
4175
|
+
else {
|
|
4176
|
+
// v0.6 — graceful turn-error UX. The loop's try/catch already
|
|
4177
|
+
// persisted any partial assistant content to session.messages and
|
|
4178
|
+
// wrote the streamed prose to ~/.cspeach/streams/. Here we only
|
|
4179
|
+
// surface a clean 4–6 line diagnostic + write a full log so the
|
|
4180
|
+
// user can resume cleanly. See agent/turn-error-ux.ts for details.
|
|
4181
|
+
await renderTurnError({
|
|
4182
|
+
err,
|
|
4183
|
+
session,
|
|
4184
|
+
userInterrupted: interruptedByUser,
|
|
4185
|
+
inSession: true,
|
|
4186
|
+
print: (line) => console.error(line),
|
|
4187
|
+
});
|
|
4188
|
+
}
|
|
4107
4189
|
}
|
|
4108
4190
|
finally {
|
|
4109
4191
|
rl.removeListener('SIGINT', onSigint);
|
|
@@ -35,6 +35,11 @@ export const FEATURES = Object.freeze({
|
|
|
35
35
|
'fugr.modules': { label: 'Function modules and function-group includes', collection: `${A}/functions/groups`, provenOnOurBox: true },
|
|
36
36
|
'prog.includes': { label: 'Program includes', collection: `${A}/programs/includes`, provenOnOurBox: true },
|
|
37
37
|
'ddic.tabletypes': { label: 'Table types', collection: `${A}/ddic/tabletypes`, provenOnOurBox: true },
|
|
38
|
+
// Part 3 (D15): no `since` (no cited release). The collections and the reads are
|
|
39
|
+
// proven on our box; the release is not, and these rows are about the collection.
|
|
40
|
+
'run.classrun': { label: 'Run a class (console)', collection: `${A}/oo/classrun`, provenOnOurBox: true },
|
|
41
|
+
'run.programrun': { label: 'Run a program', collection: `${A}/programs/programrun`, provenOnOurBox: true },
|
|
42
|
+
'cts.requests': { label: 'Transport requests (read, release)', collection: `${A}/cts/transportrequests`, provenOnOurBox: true },
|
|
38
43
|
});
|
|
39
44
|
export const FEATURE_IDS = Object.freeze(Object.keys(FEATURES));
|
|
40
45
|
export function isFeatureId(s) {
|
|
@@ -62,6 +62,8 @@ const WRITE_TOOLS = new Set([
|
|
|
62
62
|
// Final review I2 — SAP's RAP generator (tools/rap/generate-objects.ts) creates
|
|
63
63
|
// the skeleton objects in one call; its row names them.
|
|
64
64
|
'sap_generate_objects',
|
|
65
|
+
// Part 3 — a run executes code on the SAP system under an approval: it is audited.
|
|
66
|
+
'sap_run',
|
|
65
67
|
]);
|
|
66
68
|
/**
|
|
67
69
|
* Non-mutating tools that are interaction/plumbing, not object reads — excluded
|
|
@@ -156,6 +158,29 @@ function approvalCell(rec) {
|
|
|
156
158
|
return `declined ✗${risk}`;
|
|
157
159
|
}
|
|
158
160
|
// ── Object / snapshot extraction ──────────────────────────────────────────
|
|
161
|
+
/**
|
|
162
|
+
* The result as JSON, or its first line as JSON (a run's facts line), or the
|
|
163
|
+
* top-level fields a spilled preview still shows ("status": "stopped" comes
|
|
164
|
+
* before steps[] in the release envelope). {} when none of these read.
|
|
165
|
+
*/
|
|
166
|
+
function parseResultHead(result) {
|
|
167
|
+
const text = result ?? '';
|
|
168
|
+
try {
|
|
169
|
+
return asRecord(JSON.parse(text));
|
|
170
|
+
}
|
|
171
|
+
catch { /* next */ }
|
|
172
|
+
try {
|
|
173
|
+
return asRecord(JSON.parse(text.split('\n', 1)[0] ?? ''));
|
|
174
|
+
}
|
|
175
|
+
catch { /* next */ }
|
|
176
|
+
const top = (k) => new RegExp(`^ {2}"${k}": "([^"]+)"`, 'm').exec(text)?.[1];
|
|
177
|
+
const status = top('status');
|
|
178
|
+
if (!status)
|
|
179
|
+
return {};
|
|
180
|
+
const at = top('stopped_at');
|
|
181
|
+
const verdict = top('stop_verdict');
|
|
182
|
+
return { status, ...(at ? { stopped_at: at } : {}), ...(verdict ? { stop_verdict: verdict } : {}) };
|
|
183
|
+
}
|
|
159
184
|
function writeObject(tool, args, result) {
|
|
160
185
|
switch (tool) {
|
|
161
186
|
case 'sap_transport_create': {
|
|
@@ -180,8 +205,31 @@ function writeObject(tool, args, result) {
|
|
|
180
205
|
// Last resort: name the operation, not the raw description.
|
|
181
206
|
return 'transport';
|
|
182
207
|
}
|
|
183
|
-
case 'sap_transport_release':
|
|
184
|
-
|
|
208
|
+
case 'sap_transport_release': {
|
|
209
|
+
// Part 3 (D17): the row carries SAP's verdict; a stop names its step.
|
|
210
|
+
const tr = str(args.transport) ?? '—';
|
|
211
|
+
const r = parseResultHead(result);
|
|
212
|
+
const status = str(r.status);
|
|
213
|
+
if (!status)
|
|
214
|
+
return tr;
|
|
215
|
+
if (status === 'stopped') {
|
|
216
|
+
const last = Array.isArray(r.steps) && r.steps.length > 0 ? asRecord(r.steps[r.steps.length - 1]) : {};
|
|
217
|
+
const at = str(r.stopped_at) ?? str(last.number);
|
|
218
|
+
const verdict = str(r.stop_verdict) ?? str(last.verdict);
|
|
219
|
+
if (at && verdict)
|
|
220
|
+
return `${tr} · stopped at ${at}: ${verdict}`;
|
|
221
|
+
}
|
|
222
|
+
return `${tr} · ${status}`;
|
|
223
|
+
}
|
|
224
|
+
case 'sap_run': {
|
|
225
|
+
// Part 3: `CLAS ZCL_X · ran` or the error code (the facts line is line 1 of a ran result).
|
|
226
|
+
const r = parseResultHead(result);
|
|
227
|
+
const what = [str(args.type), str(args.name)].filter(Boolean).join(' ') || '—';
|
|
228
|
+
if (r.ran === true)
|
|
229
|
+
return `${what} · ran`;
|
|
230
|
+
const err = str(r.error);
|
|
231
|
+
return err ? `${what} · ${err}` : what;
|
|
232
|
+
}
|
|
185
233
|
case 'sap_update_method': {
|
|
186
234
|
const cls = str(args.class_name) ?? '—';
|
|
187
235
|
const m = str(args.method_name);
|
|
@@ -13,6 +13,7 @@
|
|
|
13
13
|
// the exact command was written by the model, so it shows as "Auto-routed: …",
|
|
14
14
|
// never as typed. An untagged command keeps the old echo.
|
|
15
15
|
import { shownUpTo } from '../register/atc-file.js';
|
|
16
|
+
import { paint } from '../renderer/theme.js';
|
|
16
17
|
export function createDispatchSlot() {
|
|
17
18
|
let cmd = null;
|
|
18
19
|
let origin = null;
|
|
@@ -41,5 +42,8 @@ export function queuedStepView(origin) {
|
|
|
41
42
|
if (!origin)
|
|
42
43
|
return { echo: true };
|
|
43
44
|
const line = origin.line ? shownUpTo(origin.line, 160).trim() : '';
|
|
44
|
-
|
|
45
|
+
if (!line)
|
|
46
|
+
return { echo: false, line: null };
|
|
47
|
+
// D11 — the chat-fallback line is dim; painted after shownUpTo flattened it.
|
|
48
|
+
return { echo: false, line: origin.by === 'chat_fallback' ? paint('dim', line) : line };
|
|
45
49
|
}
|
package/dist/session/schema.js
CHANGED
package/dist/session/store.js
CHANGED
|
@@ -72,6 +72,10 @@ function validateSchema(parsed) {
|
|
|
72
72
|
// Phase 4 chunk 4A: agentMode defaults to 'skill' for sessions written
|
|
73
73
|
// before this chunk landed. Mirrors the lastUserPrompt back-compat above.
|
|
74
74
|
parsed.agentMode ??= 'skill';
|
|
75
|
+
// Chat lane (2026-10-17 D7): older files have no lane — they ran skill turns.
|
|
76
|
+
// Anything but 'chat' (missing or hand-edited) loads as 'skill', the safe default.
|
|
77
|
+
if (parsed.lane !== 'chat')
|
|
78
|
+
parsed.lane = 'skill';
|
|
75
79
|
// 2026-10-08: sessions are listed per folder. Older files have no folder.
|
|
76
80
|
parsed.project_dir ??= null;
|
|
77
81
|
parsed.project_id ??= null;
|