@cspeach/cli 1.1.11 → 1.1.13
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/loop.js +25 -13
- package/dist/agent/providers/license-gate.js +1 -1
- package/dist/cost/display-mode.js +6 -2
- package/dist/one-shot.js +5 -1
- package/dist/projects/answer-blockers.js +5 -2
- package/dist/projects/extract-design.js +36 -8
- package/dist/projects/extract-estimate.js +3 -1
- package/dist/projects/extract-spec-gap.js +3 -1
- package/dist/projects/save-command.js +10 -0
- package/dist/repl.js +6 -1
- package/dist/sap/onboarding.js +1 -1
- package/dist/skills/bundled-skills.js +63 -63
- package/dist/tools/local-build.js +21 -0
- package/package.json +1 -1
package/dist/agent/loop.js
CHANGED
|
@@ -905,19 +905,31 @@ export async function runTurn(params) {
|
|
|
905
905
|
// carry the manifest. See ./turn-assistant-text.ts for details.
|
|
906
906
|
turnStatusEmitter.activity('finishing up');
|
|
907
907
|
const assistantText = collectTurnAssistantText(session.messages, turnStartMessageCount);
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
908
|
+
// 2026-09-17 — the save hook below asks the user things (save y/N,
|
|
909
|
+
// blocker pickers). Those are not tool dispatches, so nothing paused
|
|
910
|
+
// the status row: it kept showing "finishing up" with a running timer
|
|
911
|
+
// while the user was the one being waited on (Laeeq's dry run). Pause
|
|
912
|
+
// for the whole hook — the row hides while paused — and resume after,
|
|
913
|
+
// so any real work that follows (auto-compact) shows again.
|
|
914
|
+
turnStatusEmitter.pause();
|
|
915
|
+
try {
|
|
916
|
+
if (!params.suppressSaveHook) {
|
|
917
|
+
await maybeOfferSave({
|
|
918
|
+
skill: params.skill,
|
|
919
|
+
assistantText,
|
|
920
|
+
userMessage: userMessageForSave,
|
|
921
|
+
tokensUsed: Math.max(0, (session.usage?.output_tokens ?? 0) - turnStartOutputTokens),
|
|
922
|
+
// A4 — record the actual turn model if an override ran the stream
|
|
923
|
+
// (unreachable today: plan-resume turns suppress this hook, and
|
|
924
|
+
// they are the only modelOverride caller — kept honest anyway).
|
|
925
|
+
model: params.modelOverride ?? resolveModelRole('session_default', cfg) ?? session.model ?? 'unknown',
|
|
926
|
+
emit,
|
|
927
|
+
promotedFrom: promotedFromForSave,
|
|
928
|
+
});
|
|
929
|
+
}
|
|
930
|
+
}
|
|
931
|
+
finally {
|
|
932
|
+
turnStatusEmitter.resume();
|
|
921
933
|
}
|
|
922
934
|
// Phase 2 #9 (2026-05-16) — auto-compact end-of-turn hook.
|
|
923
935
|
// Runs only on the success path (after maybeOfferSave) so an interrupted
|
|
@@ -9,7 +9,7 @@ export class CspeachLicenseError extends Error {
|
|
|
9
9
|
super(`CSPeach requires a license to run in '${mode}' mode.\n` +
|
|
10
10
|
`In this mode your prompts go straight to your own LLM — but the CSPeach skills\n` +
|
|
11
11
|
`and CLI are licensed software. Run \`cspeach login\` to activate your license.\n` +
|
|
12
|
-
`Need an access key? Email
|
|
12
|
+
`Need an access key? Email info@cremencing.com.`);
|
|
13
13
|
this.name = 'CspeachLicenseError';
|
|
14
14
|
this.mode = mode;
|
|
15
15
|
}
|
|
@@ -21,9 +21,13 @@
|
|
|
21
21
|
export function costDisplayMode(cfg, balanceInfo) {
|
|
22
22
|
if (cfg.llm?.mode !== 'managed')
|
|
23
23
|
return 'usd';
|
|
24
|
-
if (balanceInfo?.metered !== true)
|
|
25
|
-
return 'usd';
|
|
26
24
|
if (cfg.show_cogs === true)
|
|
27
25
|
return 'usd';
|
|
26
|
+
// 2026-09-17 — a managed account NEVER falls back to dollars. Before, a
|
|
27
|
+
// balance lookup that failed or took over 2.5s at start (slow network on a
|
|
28
|
+
// customer laptop) dropped the display to USD — our cost on their screen,
|
|
29
|
+
// exactly what credits mode exists to prevent. Unknown or unmetered
|
|
30
|
+
// balances render "credits: updating…" until the next good sample.
|
|
31
|
+
void balanceInfo;
|
|
28
32
|
return 'credits';
|
|
29
33
|
}
|
package/dist/one-shot.js
CHANGED
|
@@ -17,7 +17,7 @@ import { setRenderingOverride, setHeadlessOverride } from './renderer/tty.js';
|
|
|
17
17
|
import { alwaysDeclinePreviewHook } from './repl/update-method-preview-hook.js';
|
|
18
18
|
import { createClassicOutputEmitter } from './renderer/notices.js';
|
|
19
19
|
import { todoEmitter } from './ui/todo-emitter.js';
|
|
20
|
-
import { applyLocalBuildConfig } from './tools/local-build.js';
|
|
20
|
+
import { applyLocalBuildConfig, applyStandaloneFileTools } from './tools/local-build.js';
|
|
21
21
|
import { getEffectiveWriteMode } from './repl/mode-cycle.js';
|
|
22
22
|
import { initCredits } from './cost/credits-wire.js';
|
|
23
23
|
// Side-effect imports for tool registration.
|
|
@@ -92,6 +92,10 @@ export async function runOneShot(prompt, opts) {
|
|
|
92
92
|
const announce = applyLocalBuildConfig(resolveLocalBuild(cfg), process.cwd());
|
|
93
93
|
if (announce)
|
|
94
94
|
console.log(chalk.dim(announce));
|
|
95
|
+
// 2026-09-17 — standalone writes files by contract; enable the file tools
|
|
96
|
+
// without the separate local_build step (see repl.tsx for the live case).
|
|
97
|
+
else if (standalone)
|
|
98
|
+
console.log(chalk.dim(applyStandaloneFileTools(process.cwd())));
|
|
95
99
|
}
|
|
96
100
|
const store = await getStore();
|
|
97
101
|
if (!(await store.get())) {
|
|
@@ -87,8 +87,9 @@ export async function offerAnswerBlockers(args) {
|
|
|
87
87
|
const answers = new Map();
|
|
88
88
|
for (let i = 0; i < blockers.length; i++) {
|
|
89
89
|
const b = blockers[i];
|
|
90
|
-
|
|
91
|
-
|
|
90
|
+
// 2026-09-17 — the "Blocker i/n: …" line is printed only on the free-text
|
|
91
|
+
// path below. When a picker handles the question it shows the same text
|
|
92
|
+
// itself, and printing it here too showed every blocker twice.
|
|
92
93
|
// Choices first, where the question has them and a picker is available.
|
|
93
94
|
// 'write' falls through to the text box; 'skip' leaves the blocker open.
|
|
94
95
|
let askText = true;
|
|
@@ -111,6 +112,8 @@ export async function offerAnswerBlockers(args) {
|
|
|
111
112
|
// c.kind === 'write' → askText stays true
|
|
112
113
|
}
|
|
113
114
|
if (askText) {
|
|
115
|
+
args.log('');
|
|
116
|
+
args.log(`Blocker ${i + 1}/${blockers.length}: ${b.text}`);
|
|
114
117
|
const a = (await args.prompt(' Your answer (Enter to skip): ')).trim();
|
|
115
118
|
if (a.length > 0)
|
|
116
119
|
answers.set(b.id, a);
|
|
@@ -1,4 +1,8 @@
|
|
|
1
|
-
|
|
1
|
+
// 2026-09-17 — tolerant on heading level and dash: the skill prescribes
|
|
2
|
+
// "## ABAP Design — …" but a live standalone run wrote "# ABAP Design — …"
|
|
3
|
+
// and the whole design was silently lost (no file, so /abap-rap --from had
|
|
4
|
+
// nothing to start from). H1..H3 and em/en/plain dash all count.
|
|
5
|
+
const TITLE_RE = /^#{1,3} ABAP Design [—–-] (.+)$/m;
|
|
2
6
|
const OBJECTS_HEADING_RE = /^### Objects to Create\s*$/m;
|
|
3
7
|
const DEPENDENCY_HEADING_RE = /^### Dependency Sequence\s*$/m;
|
|
4
8
|
const NEXT_HEADING_RE = /^###\s/m;
|
|
@@ -14,25 +18,49 @@ function sliceSection(markdown, headingRe) {
|
|
|
14
18
|
function parseObjectsTable(section) {
|
|
15
19
|
const lines = section.split(/\r?\n/);
|
|
16
20
|
const rows = [];
|
|
21
|
+
// 2026-09-17 — header-aware, tolerant of a missing 5th column. The skill's
|
|
22
|
+
// template is "# | Name | Type | Purpose | Transport", but a standalone
|
|
23
|
+
// run (no transports) wrote four columns and every row was skipped by the
|
|
24
|
+
// old "at least 5 cells" rule — "Objects to Create table is empty", design
|
|
25
|
+
// lost. Columns are located by header name when a header row exists;
|
|
26
|
+
// otherwise the positional default applies. Package/transport is optional.
|
|
27
|
+
let nameIdx = 1;
|
|
28
|
+
let typeIdx = 2;
|
|
29
|
+
let pkgIdx = 4;
|
|
30
|
+
let headerSeen = false;
|
|
17
31
|
for (const raw of lines) {
|
|
18
32
|
const line = raw.trim();
|
|
19
33
|
if (!line.startsWith('|'))
|
|
20
34
|
continue;
|
|
21
|
-
if (/^\|\s
|
|
22
|
-
continue;
|
|
23
|
-
if (/^\|\s*#\s*\|/.test(line))
|
|
35
|
+
if (/^\|\s*:?-+:?\s*\|/.test(line))
|
|
24
36
|
continue;
|
|
25
37
|
const cells = line.split('|').slice(1, -1).map((c) => c.trim());
|
|
26
|
-
if (cells
|
|
38
|
+
if (!headerSeen && /^#$|^no\.?$|^nr\.?$/i.test(cells[0] ?? '')) {
|
|
39
|
+
headerSeen = true;
|
|
40
|
+
const find = (re) => cells.findIndex((c) => re.test(c));
|
|
41
|
+
const n = find(/^(object\s+)?name$/i);
|
|
42
|
+
const t = find(/^(object\s+)?type$/i);
|
|
43
|
+
const p = find(/^(package|transport|package\s*\/\s*transport|transport\s*\/\s*package)$/i);
|
|
44
|
+
if (n >= 0)
|
|
45
|
+
nameIdx = n;
|
|
46
|
+
if (t >= 0)
|
|
47
|
+
typeIdx = t;
|
|
48
|
+
pkgIdx = p >= 0 ? p : null;
|
|
49
|
+
continue;
|
|
50
|
+
}
|
|
51
|
+
if (cells.length < 3)
|
|
27
52
|
continue;
|
|
28
53
|
const numStr = cells[0];
|
|
29
54
|
if (!/^\d+$/.test(numStr))
|
|
30
55
|
continue;
|
|
56
|
+
const name = cells[nameIdx] ?? '';
|
|
57
|
+
if (!/^[A-Za-z0-9_/]+$/.test(name))
|
|
58
|
+
continue; // a continuation row, not an object
|
|
31
59
|
rows.push({
|
|
32
60
|
num: Number(numStr),
|
|
33
|
-
name
|
|
34
|
-
type: cells[
|
|
35
|
-
package: cells[
|
|
61
|
+
name,
|
|
62
|
+
type: cells[typeIdx] ?? '',
|
|
63
|
+
package: pkgIdx !== null ? (cells[pkgIdx] ?? '') : '',
|
|
36
64
|
});
|
|
37
65
|
}
|
|
38
66
|
return rows;
|
|
@@ -1,4 +1,6 @@
|
|
|
1
|
-
|
|
1
|
+
// 2026-09-17 — tolerant on heading level (H1..H3) and dash, same reason as
|
|
2
|
+
// extract-design.ts.
|
|
3
|
+
const TITLE_RE = /^#{1,3} Effort Estimate [—–-] (.+)$/m;
|
|
2
4
|
const COMPONENTS_HEADING_RE = /^### Component Breakdown\s*$/m;
|
|
3
5
|
const NEXT_HEADING_RE = /^###\s/m;
|
|
4
6
|
function sliceSection(markdown, headingRe) {
|
|
@@ -1,4 +1,6 @@
|
|
|
1
|
-
|
|
1
|
+
// 2026-09-17 — tolerant on heading level (H1..H3) and dash, same reason as
|
|
2
|
+
// extract-design.ts: a title written one level off must not lose the file.
|
|
3
|
+
const TITLE_RE = /^#{1,3} Spec Gap Analysis [—–-] (.+)$/m;
|
|
2
4
|
const DOMAIN_RE = /^\*\*Inferred Domain:\*\*\s*(.+?)(?:\s*\[INFERRED.*?\])?$/m;
|
|
3
5
|
const INPUT_TYPE_RE = /^\*\*Input Type:\*\*\s*(.+)$/m;
|
|
4
6
|
const SOURCE_DOC_RE = /^\*\*Source Document:\*\*\s*(.+)$/m;
|
|
@@ -292,6 +292,16 @@ export async function runSaveCommand(args) {
|
|
|
292
292
|
args.log(`[save] ${e instanceof Error ? e.message : String(e)}`);
|
|
293
293
|
args.log(`[save] The output above is otherwise complete — re-run the skill or report this as a manifest bug.`);
|
|
294
294
|
}
|
|
295
|
+
else {
|
|
296
|
+
// 2026-09-17 — marker-less skills (design, estimate, spec-gap) used to
|
|
297
|
+
// fail here in silence: no file, no line, and the next --from step had
|
|
298
|
+
// nothing to start from (seen live: a design titled "# ABAP Design"
|
|
299
|
+
// instead of "## ABAP Design"). Say what happened and what to do.
|
|
300
|
+
args.log('');
|
|
301
|
+
args.log(`[save] ${args.skillName}: could not read the output as a ${spec.artefactType} — nothing was saved.`);
|
|
302
|
+
args.log(`[save] ${e instanceof Error ? e.message : String(e)}`);
|
|
303
|
+
args.log(`[save] Ask CSPeach to "re-issue the ${spec.artefactType} in the standard structure" and it will be saved then.`);
|
|
304
|
+
}
|
|
295
305
|
return null;
|
|
296
306
|
}
|
|
297
307
|
// C1 review fix — name the revision target BEFORE the confirm so the user
|
package/dist/repl.js
CHANGED
|
@@ -95,7 +95,7 @@ import { maybeExpandNumberSelection, lastAssistantText } from './repl/numbered-m
|
|
|
95
95
|
import { attachEarlyLineBuffer, noopEarlyLineBuffer } from './repl/early-line-buffer.js';
|
|
96
96
|
import { snapshotTurnStart, pushPostTurnStatus } from './repl/post-turn-status.js';
|
|
97
97
|
import { looksLikeCspeachShellCommand, CSPEACH_SHELL_NUDGE } from './repl/cspeach-shell-detect.js';
|
|
98
|
-
import { applyLocalBuildConfig } from './tools/local-build.js';
|
|
98
|
+
import { applyLocalBuildConfig, applyStandaloneFileTools } from './tools/local-build.js';
|
|
99
99
|
// Side-effect imports so tool registrations happen before listTools() is called.
|
|
100
100
|
import './tools/sap-read.js';
|
|
101
101
|
import './tools/sap-write.js';
|
|
@@ -464,6 +464,11 @@ export async function runRepl(opts) {
|
|
|
464
464
|
const announce = applyLocalBuildConfig(resolveLocalBuild(cfg), process.cwd());
|
|
465
465
|
if (announce)
|
|
466
466
|
console.log(chalk.dim(announce + '\n'));
|
|
467
|
+
// 2026-09-17 — standalone writes its outputs to files by contract, so the
|
|
468
|
+
// file tools must be on without a separate local_build step (the RAP
|
|
469
|
+
// stack landed in the transcript with "There are no files" otherwise).
|
|
470
|
+
else if (standalone)
|
|
471
|
+
console.log(chalk.dim(applyStandaloneFileTools(process.cwd()) + '\n'));
|
|
467
472
|
}
|
|
468
473
|
const aliases = Object.keys(cfg.sap);
|
|
469
474
|
// C.1 — --sap-alias flag override skips the interactive picker
|
package/dist/sap/onboarding.js
CHANGED
|
@@ -71,7 +71,7 @@ export async function runAddSapWizard() {
|
|
|
71
71
|
default: 'basic',
|
|
72
72
|
});
|
|
73
73
|
const username = await input({
|
|
74
|
-
message: 'SAP username (e.g.
|
|
74
|
+
message: 'SAP username (e.g. JSMITH):',
|
|
75
75
|
validate: (v) => v.length > 0 || 'Required',
|
|
76
76
|
});
|
|
77
77
|
let certPath;
|