@cspeach/cli 1.1.50 → 1.1.52
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 +4 -1
- package/dist/agent/conversation-prompt.js +30 -13
- package/dist/agent/inline-skill-block.js +341 -0
- package/dist/agent/loop.js +314 -44
- package/dist/agent/provider-shape.js +8 -4
- package/dist/agent/providers/ai-hub-provider.js +2 -2
- package/dist/agent/providers/azure-provider.js +2 -2
- package/dist/agent/providers/byok-provider.js +2 -2
- package/dist/agent/providers/local-provider.js +2 -2
- package/dist/agent/repair-partial.js +12 -0
- package/dist/agent/skill-block-key.js +134 -0
- package/dist/agent/steering-queue.js +50 -14
- package/dist/agent/system-prompt.js +22 -0
- package/dist/agent/tool-dispatch.js +19 -7
- package/dist/agent/turn-watchdog.js +1 -1
- package/dist/approvals/advisory-render.js +6 -3
- package/dist/approvals/approval-prompt.js +9 -4
- package/dist/approvals/op-labels.js +14 -0
- package/dist/approvals/plan-grant.js +8 -0
- package/dist/approvals/render.js +19 -8
- package/dist/approvals/risk-floor.js +32 -4
- package/dist/approvals/visible-changes.js +50 -0
- package/dist/cli.js +11 -3
- package/dist/commands/compact.js +12 -1
- package/dist/commands/config-set.js +14 -1
- package/dist/commands/config-show.js +16 -4
- package/dist/commands/export-audit.js +26 -3
- package/dist/commands/export-conversation.js +346 -0
- package/dist/commands/finish-check-guard.js +124 -0
- package/dist/commands/help.js +3 -0
- package/dist/commands/plan-boundary.js +5 -0
- package/dist/commands/plan-continue.js +11 -0
- package/dist/commands/plan-reground.js +27 -4
- package/dist/commands/plan-resume.js +111 -1
- package/dist/commands/plan-startup.js +5 -1
- package/dist/config/loader.js +5 -1
- package/dist/cost/cost-log.js +6 -0
- package/dist/cost/system-block.js +6 -0
- package/dist/docx/doc-meta.js +9 -1
- package/dist/docx/markdown-to-docx.js +14 -1
- package/dist/lane/app-chain.js +24 -1
- package/dist/models/server-config.js +1 -1
- package/dist/projects/document-save.js +65 -8
- package/dist/projects/document-skills.js +32 -1
- package/dist/projects/save-command.js +5 -1
- package/dist/projects/yes-no.js +5 -1
- package/dist/renderer/change-card.js +3 -1
- package/dist/renderer/steering-echo.js +53 -8
- package/dist/renderer/tool-labels.js +3 -1
- package/dist/repl/builtin-commands.js +1 -1
- package/dist/repl/chat-lane.js +5 -2
- package/dist/repl/inline-mode.js +51 -0
- package/dist/repl/leftover.js +90 -0
- package/dist/repl/reroute-turn.js +7 -1
- package/dist/repl/slash-completer.js +4 -0
- package/dist/repl/turn-stop.js +64 -0
- package/dist/repl.js +178 -32
- package/dist/router/classifier.js +1 -1
- package/dist/sap/reachability-state.js +7 -0
- package/dist/sap/unreachable.js +2 -2
- package/dist/session/cli-record.js +165 -0
- package/dist/session/fresh-start.js +4 -0
- package/dist/session/queued-step.js +3 -1
- package/dist/session/user-prompt.js +10 -1
- package/dist/skills/manifest-client.js +3 -2
- package/dist/skills/preamble.js +6 -4
- package/dist/skills/source-authed.js +3 -2
- package/dist/tools/approval.js +37 -7
- package/dist/tools/dispatch-skill.js +1 -1
- package/dist/tools/hand-off.js +49 -7
- package/dist/tools/lane/app-lane-resolve.js +9 -0
- package/dist/tools/method-only-change.js +241 -0
- package/dist/tools/rap/skeleton-resolve.js +4 -0
- package/dist/tools/sap-write.js +78 -3
- package/dist/tools/snapshot.js +7 -2
- package/dist/tools/subagent/agent_run.js +14 -0
- package/dist/tools/tool-loading.js +16 -8
- package/dist/tools/transport-fit-check.js +70 -0
- package/dist/tools/transport.js +17 -8
- package/dist/tools/write-pipeline.js +46 -5
- package/dist/ui/app.js +112 -13
- package/dist/ui/approval-modal.js +11 -6
- package/dist/ui/ascii-chars.js +21 -3
- package/dist/ui/card-slot.js +18 -1
- package/dist/ui/footer.js +128 -7
- package/dist/ui/interrupt.js +35 -1
- package/dist/ui/live-area.js +21 -4
- package/dist/ui/queue-box-emitter.js +14 -0
- package/dist/ui/rewind-panel.js +2 -1
- package/dist/ui/session-timeline.js +2 -1
- package/dist/ui/text-input.js +31 -2
- package/dist/ui/widgets/bar-chart.js +2 -1
- package/dist/ui/widgets/confirm-card.js +2 -1
- package/dist/ui/widgets/dep-graph.js +2 -1
- package/dist/ui/widgets/diff-viewer.js +2 -1
- package/dist/ui/widgets/question-card.js +2 -1
- package/dist/ui/widgets/shortcuts-sheet.js +2 -2
- package/dist/ui/widgets/stack-frames.js +2 -1
- package/dist/util/connect-retry.js +65 -0
- package/package.json +1 -1
|
@@ -100,8 +100,13 @@ export function signatureChange(diff) {
|
|
|
100
100
|
return false;
|
|
101
101
|
let state = 'unknown';
|
|
102
102
|
for (const raw of diff.split('\n')) {
|
|
103
|
-
|
|
103
|
+
// LT-10 (1A fix wave) — a file header starts another file (a diff can
|
|
104
|
+
// cover several includes): a declaration still open in the file before
|
|
105
|
+
// it never carries over.
|
|
106
|
+
if (raw.startsWith('+++') || raw.startsWith('---')) {
|
|
107
|
+
state = 'unknown';
|
|
104
108
|
continue;
|
|
109
|
+
}
|
|
105
110
|
if (raw.startsWith('@@')) {
|
|
106
111
|
// Task 7 (R2) / fix round 1 — a header naming the block
|
|
107
112
|
// (`@@ METHOD to_internal. @@`) hints that the hunk starts in its body.
|
|
@@ -157,6 +162,7 @@ export function effectiveRisk(declared, changes, ctx) {
|
|
|
157
162
|
// a body-only change read "may alter a method signature" when activation
|
|
158
163
|
// was the true reason.
|
|
159
164
|
const applied = [];
|
|
165
|
+
let syntaxObjectWord;
|
|
160
166
|
const raise = (r, label, always = false) => {
|
|
161
167
|
applied.push({ r, label, always });
|
|
162
168
|
};
|
|
@@ -176,6 +182,16 @@ export function effectiveRisk(declared, changes, ctx) {
|
|
|
176
182
|
// Final review A M-4 — the reason is always shown, even when the floor
|
|
177
183
|
// was already high (policy never, or declared high).
|
|
178
184
|
raise('high', 'syntax_errors', true);
|
|
185
|
+
// LT-10 — "this class" / "this program" from the RECORDED keys' types
|
|
186
|
+
// for the hit names (the match is by name, so the row's type may not be
|
|
187
|
+
// what was checked). Only when they all agree on one known word.
|
|
188
|
+
const hitNames = new Set(changes.filter(hit).map((c) => c.object.trim().split(/\s+/)[0].toUpperCase()));
|
|
189
|
+
const words = new Set(ctx.syntaxErrorObjects
|
|
190
|
+
.filter((k) => hitNames.has(k.slice(k.lastIndexOf(':') + 1).toUpperCase()))
|
|
191
|
+
.map((k) => ctx.typeWord?.(k.slice(0, k.lastIndexOf(':')))));
|
|
192
|
+
const [only] = [...words];
|
|
193
|
+
if (words.size === 1 && only)
|
|
194
|
+
syntaxObjectWord = only;
|
|
179
195
|
}
|
|
180
196
|
}
|
|
181
197
|
// Part 2b D7 — a BAdI implementation runs inside a standard SAP process:
|
|
@@ -231,7 +247,7 @@ export function effectiveRisk(declared, changes, ctx) {
|
|
|
231
247
|
raisedBy.splice(act, 1);
|
|
232
248
|
raisedBy.splice(sig, 0, 'activation');
|
|
233
249
|
}
|
|
234
|
-
return { level: floor, raisedBy };
|
|
250
|
+
return syntaxObjectWord ? { level: floor, raisedBy, syntaxObjectWord } : { level: floor, raisedBy };
|
|
235
251
|
}
|
|
236
252
|
/** Plain words for each risk reason code — the codes stay in audit data only. */
|
|
237
253
|
const RISK_REASON_TEXT = {
|
|
@@ -252,15 +268,17 @@ const RISK_REASON_TEXT = {
|
|
|
252
268
|
* Task 7 (R2) — one sentence on every request_approval result (and once in
|
|
253
269
|
* the journey fix block): the risk line is CSPeach's reading of the diff, so
|
|
254
270
|
* the model relays it rather than telling the person it is wrong.
|
|
271
|
+
* LT-6 (1A fix wave): the card already shows it, so the model neither repeats
|
|
272
|
+
* nor explains it unless asked (live: "rated medium only because…").
|
|
255
273
|
*/
|
|
256
|
-
export const RISK_NOTE = "The risk line is CSPeach's reading of the diff
|
|
274
|
+
export const RISK_NOTE = "The risk line is CSPeach's reading of the diff. Do not repeat, explain or dispute it unless the person asks.";
|
|
257
275
|
/**
|
|
258
276
|
* Task 7 (R2) — the risk fields of every request_approval result:
|
|
259
277
|
* effective_risk and raised_by as before (older readers), plus risk { level,
|
|
260
278
|
* reasons } in plain words and the note.
|
|
261
279
|
*/
|
|
262
280
|
export function riskResultFields(eff) {
|
|
263
|
-
const reasons = eff.raisedBy.map(
|
|
281
|
+
const reasons = eff.raisedBy.map((c) => reasonTextFor(c, eff)); // round 8 item 4: plain words, never codes
|
|
264
282
|
return { effective_risk: eff.level, raised_by: reasons, risk: { level: eff.level, reasons: [...reasons] }, note: RISK_NOTE };
|
|
265
283
|
}
|
|
266
284
|
/** Round 8 item 4 — every code the floor can raise (each has plain words above). */
|
|
@@ -279,3 +297,13 @@ const NOT_A_RISK_REASON = new Set(['non_prod_override']);
|
|
|
279
297
|
export function shownRiskReasons(raisedBy) {
|
|
280
298
|
return raisedBy.filter((c) => !NOT_A_RISK_REASON.has(c));
|
|
281
299
|
}
|
|
300
|
+
/** LT-10 — one reason in plain words; the syntax-errors one names the kind of object when known. */
|
|
301
|
+
function reasonTextFor(code, eff) {
|
|
302
|
+
if (code === 'syntax_errors' && eff.syntaxObjectWord)
|
|
303
|
+
return `the last syntax check of this ${eff.syntaxObjectWord} found errors`;
|
|
304
|
+
return riskReasonText(code);
|
|
305
|
+
}
|
|
306
|
+
/** LT-10 — the shown reasons of a result, in plain words (every approval surface). */
|
|
307
|
+
export function riskReasonTexts(eff) {
|
|
308
|
+
return shownRiskReasons(eff.raisedBy).map((c) => reasonTextFor(c, eff));
|
|
309
|
+
}
|
|
@@ -21,6 +21,56 @@ export function collapseImplicitActivates(changes) {
|
|
|
21
21
|
return !writeKeys.has(`${c.type}:${c.object}`);
|
|
22
22
|
});
|
|
23
23
|
}
|
|
24
|
+
/**
|
|
25
|
+
* LT-12 (1A fix wave) — the rows a plan list shows. On top of the collapse
|
|
26
|
+
* above, a `modify` of an object that also has a `create` in the list is
|
|
27
|
+
* hidden: the create already covers its source write and activation
|
|
28
|
+
* (approvals/jwt.ts OP_COVERAGE), so "Create X" + "Change X" read as two
|
|
29
|
+
* changes for one new object. Fix round 1: only a modify with no diff of its
|
|
30
|
+
* own, or the same diff as the create, is hidden; a modify carrying other
|
|
31
|
+
* content stays, so the person sees it before "Apply all". When the create
|
|
32
|
+
* row has no behaviour (or affects) and a hidden modify has one, the row
|
|
33
|
+
* shows the modify's — a display copy, never the caller's object. Display
|
|
34
|
+
* only: the `changes[]` behind the card (approval ids, grants) and the gate
|
|
35
|
+
* count (collapseImplicitActivates) are untouched.
|
|
36
|
+
*/
|
|
37
|
+
export function planRows(changes) {
|
|
38
|
+
const rows = collapseImplicitActivates(changes);
|
|
39
|
+
const createOf = new Map();
|
|
40
|
+
for (const c of rows)
|
|
41
|
+
if (c.op === 'create' && !createOf.has(`${c.type}:${c.object}`))
|
|
42
|
+
createOf.set(`${c.type}:${c.object}`, c);
|
|
43
|
+
const hidden = new Set();
|
|
44
|
+
const hiddenOf = new Map();
|
|
45
|
+
for (const c of rows) {
|
|
46
|
+
if (c.op !== 'modify')
|
|
47
|
+
continue;
|
|
48
|
+
const key = `${c.type}:${c.object}`;
|
|
49
|
+
const cr = createOf.get(key);
|
|
50
|
+
if (cr && (!c.diff || c.diff === cr.diff)) {
|
|
51
|
+
hidden.add(c);
|
|
52
|
+
if (!hiddenOf.has(key))
|
|
53
|
+
hiddenOf.set(key, c);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
const out = [];
|
|
57
|
+
for (const c of rows) {
|
|
58
|
+
if (hidden.has(c))
|
|
59
|
+
continue;
|
|
60
|
+
const m = c.op === 'create' ? hiddenOf.get(`${c.type}:${c.object}`) : undefined;
|
|
61
|
+
if (!m) {
|
|
62
|
+
out.push(c);
|
|
63
|
+
continue;
|
|
64
|
+
}
|
|
65
|
+
const row = { ...c };
|
|
66
|
+
if (row.behaviour === undefined && m.behaviour !== undefined)
|
|
67
|
+
row.behaviour = m.behaviour;
|
|
68
|
+
if (row.affects === undefined && m.affects !== undefined)
|
|
69
|
+
row.affects = m.affects;
|
|
70
|
+
out.push(row);
|
|
71
|
+
}
|
|
72
|
+
return out;
|
|
73
|
+
}
|
|
24
74
|
/**
|
|
25
75
|
* Heuristic: a real unified diff has at least one line starting with `+`,
|
|
26
76
|
* `-`, or `@@`. A summary or change description won't. Used to gate
|
package/dist/cli.js
CHANGED
|
@@ -157,12 +157,20 @@ export async function main(argv) {
|
|
|
157
157
|
// Task 12 — the full dump ends with "# model set by your account admin"
|
|
158
158
|
// when /v1/me/model-config says the admin's model is enforced. Capped at
|
|
159
159
|
// 1.5 s and fail-open: offline / signed out ⇒ the output is unchanged.
|
|
160
|
-
const [{ loadConfig }, { getStore }, { loadCustomerModelConfig }] = await Promise.all([
|
|
160
|
+
const [{ loadConfig }, { getStore }, { loadCustomerModelConfig, loadServerModelConfig }] = await Promise.all([
|
|
161
161
|
import('./config/loader.js'),
|
|
162
162
|
import('./auth/api-key.js'),
|
|
163
163
|
import('./models/server-config.js'),
|
|
164
164
|
]);
|
|
165
|
-
|
|
165
|
+
const showCfg = await loadConfig();
|
|
166
|
+
await Promise.all([
|
|
167
|
+
loadCustomerModelConfig(showCfg, async () => (await (await getStore()).get()) ?? '', { timeoutMs: 1500 }).catch(() => { }),
|
|
168
|
+
// Stage 1A — only a stored `assistant.skills = inline` needs the served
|
|
169
|
+
// flag (the "not active" comment). Managed only, same cap, fail-open.
|
|
170
|
+
showCfg.assistant?.skills === 'inline'
|
|
171
|
+
? loadServerModelConfig(showCfg, { timeoutMs: 1500 }).catch(() => { })
|
|
172
|
+
: Promise.resolve(),
|
|
173
|
+
]);
|
|
166
174
|
}
|
|
167
175
|
await runConfigShow(args.slice(2));
|
|
168
176
|
return 0;
|
|
@@ -308,7 +316,7 @@ Usage:
|
|
|
308
316
|
cspeach Start interactive REPL
|
|
309
317
|
cspeach "<prompt>" One-shot — runs one turn and exits
|
|
310
318
|
cspeach --one-shot "<prompt>" Same as above, explicit flag
|
|
311
|
-
cspeach export <file>.md --docx Write a Word copy next to a
|
|
319
|
+
cspeach export <file>.md --docx Write a Word copy next to a Markdown file
|
|
312
320
|
cspeach --sap-alias <name> Use a specific SAP alias (overrides default)
|
|
313
321
|
cspeach --no-sap Start without a SAP system (paste-mode skills only)
|
|
314
322
|
cspeach --continue, -c Continue the latest session in this folder
|
package/dist/commands/compact.js
CHANGED
|
@@ -33,6 +33,8 @@ import { paint } from '../renderer/theme.js';
|
|
|
33
33
|
import { getGlyphs } from '../renderer/glyphs.js';
|
|
34
34
|
import { saveSession } from '../session/store.js';
|
|
35
35
|
import { isUserPrompt } from '../session/user-prompt.js';
|
|
36
|
+
import { stripSkillBlocksInMessage } from '../agent/inline-skill-block.js';
|
|
37
|
+
import { rebuildCliMarksAfterCompact, COMPACT_LINE } from '../session/cli-record.js';
|
|
36
38
|
import { getActiveDeployments } from '../models/model-identity.js';
|
|
37
39
|
/** Keep this many recent user-prompt anchored turns verbatim. */
|
|
38
40
|
export const KEEP_RECENT_TURNS = 5;
|
|
@@ -130,7 +132,11 @@ export function buildSummarisationPrompt() {
|
|
|
130
132
|
*/
|
|
131
133
|
export function serialiseForSummariser(messages) {
|
|
132
134
|
const out = [];
|
|
133
|
-
for (const
|
|
135
|
+
for (const original of messages) {
|
|
136
|
+
// Stage 1A (S-A, M-3) — a playbook body never reaches the summariser: a
|
|
137
|
+
// hash-passing <skill> block in user text becomes [skill block: <name>].
|
|
138
|
+
// A message without one is the same object (byte-identical output).
|
|
139
|
+
const msg = stripSkillBlocksInMessage(original);
|
|
134
140
|
if (typeof msg.content === 'string') {
|
|
135
141
|
out.push({ role: msg.role, content: msg.content });
|
|
136
142
|
}
|
|
@@ -305,7 +311,12 @@ export async function runCompact(opts) {
|
|
|
305
311
|
// that produced it). The summary changed them, so the kept tail goes out
|
|
306
312
|
// without thinking; text and tool calls stay. The backup above keeps all.
|
|
307
313
|
const kept = plan.toKeep.map(stripThinking);
|
|
314
|
+
const oldMessages = session.messages;
|
|
308
315
|
session.messages = [summaryMsg, ...kept];
|
|
316
|
+
// /export docx conversation: the summary is CSPeach's, not the developer's
|
|
317
|
+
// (one plain line there). The record is rebuilt in message order so the kept
|
|
318
|
+
// plan steps, continuations and steers keep their marks (final-review I-1).
|
|
319
|
+
rebuildCliMarksAfterCompact(session, oldMessages, plan.cutAt, summaryMsg.content, { kind: 'compact', line: COMPACT_LINE });
|
|
309
320
|
// Task 19 — the recorded context size described the old history; the next
|
|
310
321
|
// call records the new one. Clearing it keeps the auto-compact gate honest.
|
|
311
322
|
delete session.last_context_tokens;
|
|
@@ -19,6 +19,7 @@ const MUTABLE_KEYS = new Set([
|
|
|
19
19
|
'assistant.lane',
|
|
20
20
|
'assistant.style',
|
|
21
21
|
'assistant.level',
|
|
22
|
+
'assistant.skills',
|
|
22
23
|
'local_build',
|
|
23
24
|
'local_files', // deprecated alias for local_build (2026-06-13) — still accepted
|
|
24
25
|
'show_cogs',
|
|
@@ -83,6 +84,7 @@ const PLAN_MODE_VALUES = ['step', 'guarded'];
|
|
|
83
84
|
const PLAN_AUDIT_VALUES = ['on', 'off'];
|
|
84
85
|
const USAGE_REPORTING_VALUES = ['on', 'off'];
|
|
85
86
|
const ASSISTANT_LANE_VALUES = ['chat', 'skills'];
|
|
87
|
+
const ASSISTANT_SKILLS_VALUES = ['system', 'inline'];
|
|
86
88
|
const APPROVALS_VALUES = ['plan', 'per-write'];
|
|
87
89
|
/**
|
|
88
90
|
* Thrown on validation failure. The outer CLI dispatcher converts to
|
|
@@ -547,6 +549,15 @@ export async function runConfigSet(args) {
|
|
|
547
549
|
cfg.assistant.lane = lane;
|
|
548
550
|
break;
|
|
549
551
|
}
|
|
552
|
+
// Stage 1A — explicit, like the lane. markExplicit below keeps the table.
|
|
553
|
+
case 'assistant.skills': {
|
|
554
|
+
const skills = value.trim().toLowerCase();
|
|
555
|
+
if (skills !== 'system' && skills !== 'inline') {
|
|
556
|
+
err(`assistant.skills must be one of: ${ASSISTANT_SKILLS_VALUES.join(', ')}`);
|
|
557
|
+
}
|
|
558
|
+
cfg.assistant.skills = skills;
|
|
559
|
+
break;
|
|
560
|
+
}
|
|
550
561
|
case 'assistant.style': {
|
|
551
562
|
const style = parseStyle(value);
|
|
552
563
|
if (!style)
|
|
@@ -616,7 +627,7 @@ export async function runConfigSet(args) {
|
|
|
616
627
|
if (b === null)
|
|
617
628
|
err(`${key} must be on/off (also accepts true/false, 1/0).`);
|
|
618
629
|
cfg.word_copy = b;
|
|
619
|
-
console.log(chalk.dim(b ? ' → a Word copy is written with every
|
|
630
|
+
console.log(chalk.dim(b ? ' → a Word copy is written with every document CSPeach saves.' : ' → CSPeach will not ask about Word copies.'));
|
|
620
631
|
break;
|
|
621
632
|
}
|
|
622
633
|
case 'write_mode':
|
|
@@ -800,6 +811,8 @@ export function validateValue(key, value) {
|
|
|
800
811
|
return value === 'true' || value === 'false';
|
|
801
812
|
case 'assistant.lane':
|
|
802
813
|
return ASSISTANT_LANE_VALUES.includes(value.trim().toLowerCase());
|
|
814
|
+
case 'assistant.skills':
|
|
815
|
+
return ASSISTANT_SKILLS_VALUES.includes(value.trim().toLowerCase());
|
|
803
816
|
case 'assistant.style':
|
|
804
817
|
return parseStyle(value) !== null;
|
|
805
818
|
case 'assistant.level':
|
|
@@ -3,6 +3,7 @@ import { loadConfig } from '../config/loader.js';
|
|
|
3
3
|
import { paint } from '../renderer/theme.js';
|
|
4
4
|
import { formatAttention } from '../renderer/notice-shape.js';
|
|
5
5
|
import { getCustomerModelConfig } from '../models/server-config.js';
|
|
6
|
+
import { inlineInactiveReason, inlineServed } from '../repl/inline-mode.js';
|
|
6
7
|
/**
|
|
7
8
|
* Dotted paths of keys that must never appear in plaintext config output.
|
|
8
9
|
* SAP passwords live in the keychain and should never land in config.toml,
|
|
@@ -74,7 +75,11 @@ function printLlmMissingHints(llm) {
|
|
|
74
75
|
console.log(`ai_hub_router_model = ${paint('dim', '<not set — skill routing uses ai_hub_model_alias>')}`);
|
|
75
76
|
}
|
|
76
77
|
}
|
|
77
|
-
|
|
78
|
+
/**
|
|
79
|
+
* `notes` maps a dotted key to a trailing comment (Stage 1A: a stored
|
|
80
|
+
* `assistant.skills = inline` that would not be active).
|
|
81
|
+
*/
|
|
82
|
+
function printConfigAsToml(cfg, prefix = '', notes = {}) {
|
|
78
83
|
// Emit scalars before nested objects — otherwise a top-level scalar that
|
|
79
84
|
// follows a nested table (e.g. `write_mode` declared after `ui` in the
|
|
80
85
|
// config shape) would render underneath `[ui]` in TOML, which reparses as
|
|
@@ -91,7 +96,8 @@ function printConfigAsToml(cfg, prefix = '') {
|
|
|
91
96
|
console.log(`${k} = [REDACTED]`);
|
|
92
97
|
}
|
|
93
98
|
else {
|
|
94
|
-
|
|
99
|
+
const note = notes[fullKey];
|
|
100
|
+
console.log(`${k} = ${formatTomlValue(v)}` + (note ? chalk.gray(` # ${note}`) : ''));
|
|
95
101
|
// Surface an explanatory hint for advisory-only mode so users understand
|
|
96
102
|
// the implication (writes proposed, never executed) without having to
|
|
97
103
|
// read the docs.
|
|
@@ -104,7 +110,7 @@ function printConfigAsToml(cfg, prefix = '') {
|
|
|
104
110
|
const fullKey = prefix ? `${prefix}.${k}` : k;
|
|
105
111
|
console.log('');
|
|
106
112
|
console.log(chalk.bold(`[${fullKey}]`));
|
|
107
|
-
printConfigAsToml(v, fullKey);
|
|
113
|
+
printConfigAsToml(v, fullKey, notes);
|
|
108
114
|
// Mode-aware extras for the `[llm]` section — print required keys that
|
|
109
115
|
// are empty as `<not set>` so a user who ran `cspeach config set llm.mode
|
|
110
116
|
// ai-hub` without also setting the base URL can see what is still
|
|
@@ -165,7 +171,13 @@ export async function runConfigShow(args) {
|
|
|
165
171
|
// Full config dump
|
|
166
172
|
console.log('');
|
|
167
173
|
console.log(chalk.gray('# ~/.cspeach/config.toml'));
|
|
168
|
-
|
|
174
|
+
// Stage 1A — say when a stored `inline` would not be active in an
|
|
175
|
+
// interactive session (the served flag is loaded by cli.ts).
|
|
176
|
+
// Read defensively: a config without an [assistant] table means the defaults.
|
|
177
|
+
const inlineNote = cfg.assistant?.skills === 'inline'
|
|
178
|
+
? inlineInactiveReason({ skills: 'inline', lane: cfg.assistant.lane, served: inlineServed(cfg.llm?.mode) })
|
|
179
|
+
: null;
|
|
180
|
+
printConfigAsToml(cfg, '', inlineNote ? { 'assistant.skills': inlineNote } : {});
|
|
169
181
|
console.log('');
|
|
170
182
|
// Task 12 — one trailing line when the account admin's model is enforced
|
|
171
183
|
// by the proxy (/v1/me/model-config). No snapshot ⇒ output unchanged.
|
|
@@ -17,10 +17,33 @@ import path from 'node:path';
|
|
|
17
17
|
import { buildAuditReport } from '../session/audit-export.js';
|
|
18
18
|
import { globalCredits } from '../cost/credits.js';
|
|
19
19
|
import { readCostLog } from '../cost/cost-log.js';
|
|
20
|
-
/**
|
|
20
|
+
/**
|
|
21
|
+
* The /export usage line (2026-10-23). Here, in the light audit module both
|
|
22
|
+
* REPL handlers already load, so `/export audit` and a mistyped `/export`
|
|
23
|
+
* never load the Word package (final-review I-2).
|
|
24
|
+
*/
|
|
25
|
+
export const EXPORT_REPL_USAGE = 'Usage: /export docx (the last answer to Word) · /export docx <name> · /export docx conversation · /export audit [file]';
|
|
26
|
+
/**
|
|
27
|
+
* Parse the text after `/export` into a subcommand + optional filename.
|
|
28
|
+
*
|
|
29
|
+
* 2026-10-23 (a customer's screenshot): the Word forms people actually type
|
|
30
|
+
* all mean "Word": `/export docx x.docx`, `/export x.md --docx`,
|
|
31
|
+
* `/export x.docx`, `/export conversation`, `/export word`. A `--docx` or
|
|
32
|
+
* `--word` flag anywhere is dropped. For sub 'docx', `docxArgs` is the line
|
|
33
|
+
* to hand to runExportDocxRepl (" docx <rest>").
|
|
34
|
+
*/
|
|
21
35
|
export function parseExportArgs(rest) {
|
|
22
|
-
|
|
23
|
-
|
|
36
|
+
let flag = false;
|
|
37
|
+
const cleaned = rest.replace(/(^|\s)--(?:docx|word)(?=\s|$)/gi, () => { flag = true; return ' '; }).trim();
|
|
38
|
+
const parts = cleaned.split(/\s+/).filter(Boolean);
|
|
39
|
+
const first = (parts[0] ?? '').toLowerCase();
|
|
40
|
+
if (first === 'docx' || first === 'word') {
|
|
41
|
+
return { sub: 'docx', docxArgs: ` docx ${cleaned.slice(parts[0].length).trim()}`.trimEnd() };
|
|
42
|
+
}
|
|
43
|
+
if (first !== 'audit' && (flag || first === 'conversation' || /\.(?:md|docx)["']?$/i.test(cleaned))) {
|
|
44
|
+
return { sub: 'docx', docxArgs: ` docx ${cleaned}`.trimEnd() };
|
|
45
|
+
}
|
|
46
|
+
return { sub: first, filename: parts[1] };
|
|
24
47
|
}
|
|
25
48
|
/** Deterministic default filename: date from session data, not now(). */
|
|
26
49
|
export function defaultAuditFilename(session) {
|