@cspeach/cli 1.1.12 → 1.1.14
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/one-shot.js +5 -1
- 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/skills/bundled-skills.js +1 -1
- package/dist/tools/local-build.js +21 -0
- package/dist/ui/widgets/ask-form.js +11 -3
- package/dist/ui/widgets/ask-question-modal.js +11 -3
- package/package.json +1 -1
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())) {
|
|
@@ -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
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// GENERATED FILE — bundled skills for local mode.
|
|
2
2
|
// Source: https://manifest.cspeach.dev/v1.json
|
|
3
|
-
// Generated: 2026-09-
|
|
3
|
+
// Generated: 2026-09-17T22:46:40.872Z
|
|
4
4
|
// Manifest signature verified at build time.
|
|
5
5
|
export const BUNDLED_SKILLS = {
|
|
6
6
|
"abap-atc-fix": {
|
|
@@ -76,6 +76,27 @@ export const LOCAL_BUILD_TOOLS = [
|
|
|
76
76
|
* which directory tree read/write/edit operate under; the shell safelist is
|
|
77
77
|
* surfaced so the user sees which commands shell_exec may run.
|
|
78
78
|
*/
|
|
79
|
+
/**
|
|
80
|
+
* Standalone mode (2026-09-17). The standalone note tells the model to write
|
|
81
|
+
* every generated object plus GUIDE.md under ./cspeach-out — but the file
|
|
82
|
+
* tools were only switched on by `local_build`, a separate manual step. On
|
|
83
|
+
* Laeeq's other-laptop run the whole RAP stack ended up in the transcript
|
|
84
|
+
* with "There are no files". Standalone therefore enables the FILE tools
|
|
85
|
+
* itself. Only the file tools: no shell_exec, no Fiori/agent tools — those
|
|
86
|
+
* stay behind the explicit local_build consent.
|
|
87
|
+
*/
|
|
88
|
+
export const STANDALONE_FILE_TOOLS = [
|
|
89
|
+
'file_read',
|
|
90
|
+
'read_document',
|
|
91
|
+
'file_write',
|
|
92
|
+
'file_edit',
|
|
93
|
+
'glob',
|
|
94
|
+
'grep',
|
|
95
|
+
];
|
|
96
|
+
export function applyStandaloneFileTools(cwd) {
|
|
97
|
+
enableToolFlags([...STANDALONE_FILE_TOOLS]);
|
|
98
|
+
return `standalone: generated objects and GUIDE.md are written under ${cwd}\\cspeach-out`;
|
|
99
|
+
}
|
|
79
100
|
export function applyLocalBuildConfig(localBuild, cwd) {
|
|
80
101
|
if (localBuild !== true)
|
|
81
102
|
return undefined;
|
|
@@ -199,8 +199,16 @@ export function AskForm({ request, onResolve }) {
|
|
|
199
199
|
const values = q.options
|
|
200
200
|
.filter((o) => multiSel[focus].has(o.value))
|
|
201
201
|
.map((o) => o.value);
|
|
202
|
-
if (values.length === 0)
|
|
203
|
-
|
|
202
|
+
if (values.length === 0) {
|
|
203
|
+
// 2026-09-17 — nothing toggled: Enter used to do NOTHING, silently.
|
|
204
|
+
// Every other question answers on Enter, so a user who arrows to a
|
|
205
|
+
// row and presses Enter (Glenn's first session) sat there stuck.
|
|
206
|
+
// Enter alone now picks the highlighted option, single-choice style.
|
|
207
|
+
const opt = q.options[cursor];
|
|
208
|
+
if (opt)
|
|
209
|
+
answerAndAdvance(focus, opt.value);
|
|
210
|
+
return;
|
|
211
|
+
}
|
|
204
212
|
answerAndAdvance(focus, values.join(','));
|
|
205
213
|
}
|
|
206
214
|
else {
|
|
@@ -220,7 +228,7 @@ export function AskForm({ request, onResolve }) {
|
|
|
220
228
|
const cur = i === focus;
|
|
221
229
|
const done = answers[i] !== null;
|
|
222
230
|
return (_jsxs(Text, { color: cur ? PEACH : undefined, bold: cur, dimColor: !cur, children: [i > 0 ? ' ' : '', done ? '✓ ' : '', qq.header ?? qq.id] }, qq.id));
|
|
223
|
-
}) }) }), isSubmit ? (_jsx(Text, { dimColor: true, wrap: "truncate-end", children: "All questions answered \u2014 Tab/\u2190 to review, Enter to submit \u00B7 Esc cancel" })) : q ? (_jsxs(_Fragment, { children: [
|
|
231
|
+
}) }) }), isSubmit ? (_jsx(Text, { dimColor: true, wrap: "truncate-end", children: "All questions answered \u2014 Tab/\u2190 to review, Enter to submit \u00B7 Esc cancel" })) : q ? (_jsxs(_Fragment, { children: [_jsxs(Text, { bold: true, wrap: "truncate-end", children: [q.question, q.multiSelect ? ' (choose one or more)' : ''] }), q.context && _jsx(Text, { dimColor: true, wrap: "truncate-end", children: q.context }), !q.freeText && (_jsxs(_Fragment, { children: [winStart > 0 && _jsxs(Text, { dimColor: true, wrap: "truncate-end", children: [" \u2191 ", winStart, " more"] }), visible.map((o, vi) => {
|
|
224
232
|
const idx = winStart + vi;
|
|
225
233
|
const isCur = !otherFocused && idx === cursor;
|
|
226
234
|
const mark = q.multiSelect
|
|
@@ -24,7 +24,9 @@ import TextInput from '../text-input.js';
|
|
|
24
24
|
const PEACH = '#FFCBA4';
|
|
25
25
|
export function askHelpLine(kind, opts) {
|
|
26
26
|
switch (kind) {
|
|
27
|
-
|
|
27
|
+
// 2026-09-17 — spelled out after a first-session user could not tell a
|
|
28
|
+
// multi-select from a single pick ("was it a multi select?").
|
|
29
|
+
case 'multi': return 'Space ticks a row · Enter confirms · Enter alone picks the highlighted row · Esc cancels';
|
|
28
30
|
case 'pick': return opts?.single ? 'Enter pick · Esc cancel' : 'Enter pick · Tab next · Esc cancel';
|
|
29
31
|
default: return opts?.single ? 'Enter submit · Esc cancel' : 'Tab next · Esc cancel';
|
|
30
32
|
}
|
|
@@ -153,14 +155,20 @@ function MultiForm({ params, onResolve }) {
|
|
|
153
155
|
.filter((c) => selected.has(c.value))
|
|
154
156
|
.map((c) => c.value);
|
|
155
157
|
if (values.length === 0) {
|
|
156
|
-
|
|
158
|
+
// 2026-09-17 — nothing toggled: Enter alone picks the highlighted
|
|
159
|
+
// option (single-choice muscle memory) instead of cancelling.
|
|
160
|
+
const current = params.choices[cursor];
|
|
161
|
+
if (current)
|
|
162
|
+
onResolve({ answer: current.value });
|
|
163
|
+
else
|
|
164
|
+
onResolve({ answer: null, cancelled: true });
|
|
157
165
|
}
|
|
158
166
|
else {
|
|
159
167
|
onResolve({ answer: values.join(',') });
|
|
160
168
|
}
|
|
161
169
|
}
|
|
162
170
|
}, { isActive: true });
|
|
163
|
-
return (_jsxs(Box, { flexDirection: "column", paddingX: 1, flexShrink: 0, children: [
|
|
171
|
+
return (_jsxs(Box, { flexDirection: "column", paddingX: 1, flexShrink: 0, children: [_jsxs(Text, { bold: true, children: [params.question, " (choose one or more)"] }), params.context && (_jsx(Box, { marginTop: 1, children: _jsx(Text, { dimColor: true, children: params.context }) })), _jsx(Box, { marginTop: 1, flexDirection: "column", children: params.choices.map((c, i) => {
|
|
164
172
|
const isCursor = i === cursor;
|
|
165
173
|
const isSelected = selected.has(c.value);
|
|
166
174
|
const mark = isSelected ? '◉' : '○';
|