@cspeach/cli 0.9.0 → 1.0.0
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 +1 -1
- package/dist/agent/intent-system-prompt.js +1 -1
- package/dist/agent/loop.js +209 -20
- package/dist/agent/providers/license-gate.js +44 -0
- package/dist/agent/skill-checkpoint.js +1 -1
- package/dist/agent/tool-dispatch.js +15 -0
- package/dist/approvals/canonical.js +91 -0
- package/dist/approvals/jwt.js +39 -2
- package/dist/auth/org-anthropic-key.js +25 -0
- package/dist/classifier/client.js +18 -3
- package/dist/commands/config-set.js +95 -0
- package/dist/commands/login.js +31 -14
- package/dist/commands/plan-model-tier.js +83 -0
- package/dist/commands/plan-resume.js +148 -21
- package/dist/config/loader.js +95 -1
- package/dist/doctor/checks/_http-probe.js +1 -0
- package/dist/doctor/checks/cert.js +14 -3
- package/dist/doctor/checks/sap.js +30 -8
- package/dist/doctor/checks/zcspeach.js +19 -4
- package/dist/one-shot.js +52 -4
- package/dist/projects/answer-blockers.js +137 -0
- package/dist/projects/extract-cca.js +108 -16
- package/dist/projects/extract-modernize.js +1 -1
- package/dist/projects/extract-plan.js +130 -37
- package/dist/projects/extract-spec-gap.js +34 -7
- package/dist/projects/extract-test-coverage.js +1 -1
- package/dist/projects/extract-upgrade.js +113 -22
- package/dist/projects/index.js +5 -2
- package/dist/projects/merge-cca.js +292 -0
- package/dist/projects/merge-upgrade.js +173 -0
- package/dist/projects/migration.js +103 -1
- package/dist/projects/output-paths.js +27 -0
- package/dist/projects/plan-run.js +159 -25
- package/dist/projects/plan-schema.js +63 -3
- package/dist/projects/promote-command.js +25 -2
- package/dist/projects/promote.js +128 -0
- package/dist/projects/save-command.js +247 -20
- package/dist/projects/status.js +3 -1
- package/dist/projects/validate.js +1 -1
- package/dist/projects/workspace.js +164 -20
- package/dist/renderer/notices.js +64 -0
- package/dist/renderer/progress-chatter.js +8 -0
- package/dist/renderer/tool-widget.js +18 -4
- package/dist/renderer/tty.js +43 -4
- package/dist/renderer/verify-chain.js +77 -0
- package/dist/repl/at-picker.js +60 -7
- package/dist/repl/builtin-commands.js +37 -0
- package/dist/repl/early-line-buffer.js +68 -0
- package/dist/repl/inquirer-guard.js +70 -5
- package/dist/repl/numbered-menu.js +131 -0
- package/dist/repl/post-turn-status.js +2 -2
- package/dist/repl/rule8-detector.js +17 -2
- package/dist/repl/safety-confirm.js +111 -2
- package/dist/repl/safety-mode-state.js +19 -3
- package/dist/repl/slash-picker.js +10 -15
- package/dist/repl.js +301 -35
- package/dist/router/classifier.js +150 -6
- package/dist/sap/capability-matrix.js +20 -0
- package/dist/sap/capability-matrix.json +11236 -0
- package/dist/sap/capability.js +146 -0
- package/dist/sap/connection-manager.js +19 -1
- package/dist/sap/onboarding.js +42 -4
- package/dist/session/pending.js +27 -0
- package/dist/skill-catalog.js +48 -43
- package/dist/skills/bundled-skills.js +279 -1
- package/dist/skills/promotion-dispatch.js +23 -0
- package/dist/tools/_command-shared.js +36 -12
- package/dist/tools/_filesystem-shared.js +139 -4
- package/dist/tools/_flag.js +25 -0
- package/dist/tools/approval.js +64 -21
- package/dist/tools/ask-question.js +96 -4
- package/dist/tools/capability/tool.js +74 -0
- package/dist/tools/dispatch-skill.js +22 -1
- package/dist/tools/extend-model/anchored-insert.js +810 -0
- package/dist/tools/extend-model/tool.js +188 -0
- package/dist/tools/filesystem/extract-document.js +57 -0
- package/dist/tools/filesystem/file-edit.js +12 -2
- package/dist/tools/filesystem/file-read.js +2 -2
- package/dist/tools/filesystem/file-write.js +11 -2
- package/dist/tools/filesystem/glob.js +11 -0
- package/dist/tools/filesystem/grep.js +10 -0
- package/dist/tools/filesystem/read-document.js +107 -0
- package/dist/tools/fiori/apply.js +50 -0
- package/dist/tools/fiori/bin.js +3 -0
- package/dist/tools/fiori/catalog/index.js +27 -0
- package/dist/tools/fiori/catalog/value-help.js +230 -0
- package/dist/tools/fiori/catalog/viz-chart.js +177 -0
- package/dist/tools/fiori/cli.js +71 -0
- package/dist/tools/fiori/deploy-config.js +73 -0
- package/dist/tools/fiori/fe-scaffold.js +45 -0
- package/dist/tools/fiori/i18n.js +39 -0
- package/dist/tools/fiori/manifest.js +70 -0
- package/dist/tools/fiori/render.js +77 -0
- package/dist/tools/fiori/scaffold.js +39 -0
- package/dist/tools/fiori/tools.js +356 -0
- package/dist/tools/fiori/types.js +1 -0
- package/dist/tools/local-build.js +76 -0
- package/dist/tools/local-files.js +31 -0
- package/dist/tools/project/_merge-shared.js +68 -0
- package/dist/tools/project/cca_merge.js +164 -0
- package/dist/tools/project/playbook_get.js +1 -1
- package/dist/tools/project/upgrade_merge_progress.js +206 -0
- package/dist/tools/sap-read.js +53 -9
- package/dist/tools/sap-write.js +530 -21
- package/dist/tools/shell/shell_exec.js +41 -6
- package/dist/tools/snapshot.js +37 -14
- package/dist/tools/subagent/background_run.js +17 -1
- package/dist/tools/transport-resolution.js +86 -0
- package/dist/tools/transport.js +224 -5
- package/dist/tools/write-mode.js +4 -0
- package/dist/ui/app.js +6 -2
- package/dist/ui/body.js +13 -0
- package/dist/ui/footer.js +20 -6
- package/dist/ui/line-resolution.js +17 -6
- package/dist/ui/session-timeline.js +1 -0
- package/dist/ui/text-input.js +150 -0
- package/dist/ui/widgets/ask-question-modal.js +4 -1
- package/package.json +19 -3
- package/bench/README.md +0 -78
- package/bench/prompts/abap-document-cds.md +0 -44
- package/bench/prompts/abap-explain-bdef-handler.md +0 -57
- package/bench/prompts/abap-test-method.md +0 -42
- package/bench/results/abap-document-cds/claude-haiku-4-5.md +0 -189
- package/bench/results/abap-document-cds/claude-opus-4-7.md +0 -120
- package/bench/results/abap-document-cds/claude-sonnet-4-6.md +0 -151
- package/bench/results/abap-explain-bdef-handler/claude-haiku-4-5.md +0 -112
- package/bench/results/abap-explain-bdef-handler/claude-opus-4-7.md +0 -101
- package/bench/results/abap-explain-bdef-handler/claude-sonnet-4-6.md +0 -101
- package/bench/results/abap-test-method/claude-haiku-4-5.md +0 -186
- package/bench/results/abap-test-method/claude-opus-4-7.md +0 -193
- package/bench/results/abap-test-method/claude-sonnet-4-6.md +0 -234
package/dist/ui/footer.js
CHANGED
|
@@ -1,15 +1,22 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import React, { useState, useEffect, useCallback } from 'react';
|
|
3
3
|
import { Box, Text, useInput } from 'ink';
|
|
4
|
-
|
|
4
|
+
// D29 paste fix (2026-06-12): vendored paste-safe replacement for
|
|
5
|
+
// ink-text-input — multi-chunk pastes were losing every chunk but the
|
|
6
|
+
// last (stale-prop closure in the upstream component). See text-input.tsx.
|
|
7
|
+
import TextInput from './text-input.js';
|
|
5
8
|
import Fuse from 'fuse.js';
|
|
6
9
|
import * as fs from 'node:fs';
|
|
7
10
|
import * as os from 'node:os';
|
|
8
11
|
import * as path from 'node:path';
|
|
9
|
-
|
|
12
|
+
// Resolved lazily (not at module load) so it honours the HOME/USERPROFILE
|
|
13
|
+
// active at render time — lets tests point it at a temp dir.
|
|
14
|
+
function historyFile() {
|
|
15
|
+
return path.join(os.homedir(), '.cspeach', 'history');
|
|
16
|
+
}
|
|
10
17
|
function loadHistory() {
|
|
11
18
|
try {
|
|
12
|
-
return fs.readFileSync(
|
|
19
|
+
return fs.readFileSync(historyFile(), 'utf8').split('\n').filter(Boolean);
|
|
13
20
|
}
|
|
14
21
|
catch {
|
|
15
22
|
return [];
|
|
@@ -19,8 +26,9 @@ function appendHistory(line, history) {
|
|
|
19
26
|
// Skip consecutive duplicates (matches bash HISTCONTROL=ignoredups).
|
|
20
27
|
if (history[history.length - 1] === line)
|
|
21
28
|
return;
|
|
22
|
-
|
|
23
|
-
fs.
|
|
29
|
+
const file = historyFile();
|
|
30
|
+
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
31
|
+
fs.appendFileSync(file, line + '\n');
|
|
24
32
|
history.push(line);
|
|
25
33
|
}
|
|
26
34
|
const PEACH = '#FFCBA4';
|
|
@@ -82,7 +90,13 @@ export function Footer({ onSubmit, onSlashTrigger, onHistoryTrigger, onValueChan
|
|
|
82
90
|
setValue(next === -1 ? '' : history[history.length - 1 - next] ?? '');
|
|
83
91
|
return;
|
|
84
92
|
}
|
|
85
|
-
|
|
93
|
+
// 2026-06-16 — go dormant while a modal (ask_question / coaching picker)
|
|
94
|
+
// owns input. Without this, arrow-key navigation INSIDE the modal also
|
|
95
|
+
// hits this handler (Ink delivers every keystroke to every active
|
|
96
|
+
// useInput), recalling a history line into the hidden Footer — so the
|
|
97
|
+
// last command "comes back" in the input once the modal closes. Mirrors
|
|
98
|
+
// why the TextInput is swapped for a placeholder when disabled.
|
|
99
|
+
}, { isActive: !disabled });
|
|
86
100
|
const handleSubmit = useCallback((line) => {
|
|
87
101
|
if (!line.trim())
|
|
88
102
|
return;
|
|
@@ -19,8 +19,8 @@
|
|
|
19
19
|
// pick, so a line flows slash → at → empty-body guard → submit no
|
|
20
20
|
// matter where it entered the pipeline.
|
|
21
21
|
import { isExactSlashMatch, filterSlashEntries } from '../repl/slash-picker.js';
|
|
22
|
-
import { pendingAtTokenPrefix
|
|
23
|
-
import { SKILL_CATALOG } from '../skill-catalog.js';
|
|
22
|
+
import { pendingAtTokenPrefix } from '../repl/at-picker.js';
|
|
23
|
+
import { SKILL_CATALOG, SKILL_ALIASES } from '../skill-catalog.js';
|
|
24
24
|
export function nextResolutionStep(line, files) {
|
|
25
25
|
const trimmed = line.trim();
|
|
26
26
|
if (trimmed.startsWith('/') && !isExactSlashMatch(trimmed)) {
|
|
@@ -38,10 +38,11 @@ export function nextResolutionStep(line, files) {
|
|
|
38
38
|
const filenames = files.map((f) => f.filename);
|
|
39
39
|
const atPrefix = pendingAtTokenPrefix(trimmed, filenames);
|
|
40
40
|
if (atPrefix !== null && files.length > 0) {
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
41
|
+
// Always open the picker when the line ends in an unresolved @-token —
|
|
42
|
+
// even when it narrows to a single file. The old single-match shortcut
|
|
43
|
+
// silently auto-resolved AND auto-submitted, so the user never saw which
|
|
44
|
+
// file was attached and couldn't keep typing (e.g. add `--scope`). Showing
|
|
45
|
+
// the palette + inserting-on-pick (App handles that) is the correct flow.
|
|
45
46
|
return { kind: 'open-at', prefix: atPrefix, line: trimmed };
|
|
46
47
|
}
|
|
47
48
|
// Empty workspace: nothing to pick — let the downstream @-resolution
|
|
@@ -68,6 +69,16 @@ function emptyBodyGuard(line) {
|
|
|
68
69
|
const splitIdx = line.search(/\s/);
|
|
69
70
|
const head = splitIdx === -1 ? line : line.slice(0, splitIdx);
|
|
70
71
|
const body = splitIdx === -1 ? '' : line.slice(splitIdx).trim();
|
|
72
|
+
// Retired-but-redirected name with no body: prefill the NEW name so the
|
|
73
|
+
// user both learns the rename and doesn't dispatch an empty prompt.
|
|
74
|
+
const aliasTarget = SKILL_ALIASES[head.toLowerCase().slice(1)];
|
|
75
|
+
if (aliasTarget !== undefined && body.length === 0) {
|
|
76
|
+
return {
|
|
77
|
+
kind: 'prefill',
|
|
78
|
+
text: `/${aliasTarget} `,
|
|
79
|
+
hint: `↪ ${head.toLowerCase()} is now /${aliasTarget} — type your prompt and press Enter`,
|
|
80
|
+
};
|
|
81
|
+
}
|
|
71
82
|
const isSkill = SKILL_CATALOG.some((s) => `/${s.name}` === head.toLowerCase());
|
|
72
83
|
if (isSkill && body.length === 0) {
|
|
73
84
|
return {
|
|
@@ -6,6 +6,7 @@ const MUTATING_TOOLS = new Set([
|
|
|
6
6
|
'sap_set_source', 'sap_update_method', 'sap_delete_object', 'sap_activate',
|
|
7
7
|
'sap_create_object', 'sap_transport_release', 'sap_message_maintain',
|
|
8
8
|
'sap_number_range_intervals', 'sap_service_binding_publish',
|
|
9
|
+
'sap_service_binding_unpublish',
|
|
9
10
|
]);
|
|
10
11
|
export function toTimelineEntry(call) {
|
|
11
12
|
// args shape depends on the tool; extract defensively.
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
/**
|
|
3
|
+
* PasteSafeTextInput — drop-in replacement for `ink-text-input` (D29,
|
|
4
|
+
* "chewed words" during seed-session paste; 2026-06-12).
|
|
5
|
+
*
|
|
6
|
+
* ROOT CAUSE of the chewing, verified by repro test: Ink's App drains ALL
|
|
7
|
+
* pending stdin chunks in one synchronous loop (components/App.js
|
|
8
|
+
* `handleReadable`: `while ((chunk = stdin.read()) !== null) emit('input')`),
|
|
9
|
+
* and a large terminal paste arrives as MULTIPLE chunks. ink-text-input
|
|
10
|
+
* computes `nextValue = value.slice(0, cursor) + input + …` from the
|
|
11
|
+
* `value` PROP of the last committed render — a stale closure for every
|
|
12
|
+
* chunk after the first in the same tick. With chunks A then B, B's
|
|
13
|
+
* handler still sees the pre-A value, so A is silently dropped:
|
|
14
|
+
* `stdin.write('hello '); stdin.write('world')` → value `"world"`.
|
|
15
|
+
*
|
|
16
|
+
* FIX: mirror value + cursor in refs that are updated SYNCHRONOUSLY inside
|
|
17
|
+
* the input handler, so consecutive same-tick chunks compound. The refs
|
|
18
|
+
* re-sync from props on commit, so external value pushes (submit-clear,
|
|
19
|
+
* palette prefill) still win between keystrokes. This also fixes the
|
|
20
|
+
* sibling hazard: an Enter that lands in the same drain as the paste tail
|
|
21
|
+
* now submits the COMPLETE value instead of the stale prop.
|
|
22
|
+
*
|
|
23
|
+
* Rendering, cursor drawing, placeholder, and mask behaviour are vendored
|
|
24
|
+
* verbatim from ink-text-input v6 (MIT, © Vadim Demedes) minus
|
|
25
|
+
* `highlightPastedText` (Footer never used it).
|
|
26
|
+
*/
|
|
27
|
+
import { useState, useEffect, useRef } from 'react';
|
|
28
|
+
import { Text, useInput } from 'ink';
|
|
29
|
+
import chalk from 'chalk';
|
|
30
|
+
export function PasteSafeTextInput({ value: originalValue, placeholder = '', focus = true, mask, showCursor = true, onChange, onSubmit, }) {
|
|
31
|
+
const [cursorOffset, setCursorOffset] = useState((originalValue || '').length);
|
|
32
|
+
// The paste-safety core: synchronous mirrors of value + cursor. The
|
|
33
|
+
// input handler reads/writes THESE, never the (possibly stale) props.
|
|
34
|
+
const valueRef = useRef(originalValue);
|
|
35
|
+
const cursorRef = useRef(cursorOffset);
|
|
36
|
+
// Values we've sent through onChange whose prop echo hasn't been
|
|
37
|
+
// observed yet. Needed because passive effects flush LATE: with N
|
|
38
|
+
// same-tick chunks, the echo effect for chunk k can run AFTER the
|
|
39
|
+
// handler for chunk k+1, and blindly assigning `valueRef.current =
|
|
40
|
+
// originalValue` would rewind the mirror to a stale value (observed in
|
|
41
|
+
// the repro as words landing out of order). An echo — even a stale
|
|
42
|
+
// one — must never overwrite the mirror; only EXTERNAL pushes may.
|
|
43
|
+
const pendingEchoes = useRef([]);
|
|
44
|
+
// Re-sync the mirrors on EXTERNAL value pushes only (parent setValue('')
|
|
45
|
+
// after submit, palette prefill). Echoes of our own onChange — current
|
|
46
|
+
// or stale — are recognised via pendingEchoes and skipped: the mirror
|
|
47
|
+
// is already at or ahead of them.
|
|
48
|
+
useEffect(() => {
|
|
49
|
+
const echoIdx = pendingEchoes.current.indexOf(originalValue);
|
|
50
|
+
if (echoIdx !== -1) {
|
|
51
|
+
pendingEchoes.current.splice(0, echoIdx + 1);
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
pendingEchoes.current = [];
|
|
55
|
+
valueRef.current = originalValue;
|
|
56
|
+
if (cursorRef.current > originalValue.length) {
|
|
57
|
+
cursorRef.current = originalValue.length;
|
|
58
|
+
setCursorOffset(originalValue.length);
|
|
59
|
+
}
|
|
60
|
+
}, [originalValue]);
|
|
61
|
+
// Clamp rendered cursor when the value shrinks under it (vendored
|
|
62
|
+
// semantic from ink-text-input's first effect).
|
|
63
|
+
useEffect(() => {
|
|
64
|
+
if (!focus || !showCursor)
|
|
65
|
+
return;
|
|
66
|
+
setCursorOffset((previous) => previous > originalValue.length ? originalValue.length : previous);
|
|
67
|
+
}, [originalValue, focus, showCursor]);
|
|
68
|
+
const value = mask ? mask.repeat(originalValue.length) : originalValue;
|
|
69
|
+
let renderedValue = value;
|
|
70
|
+
let renderedPlaceholder = placeholder ? chalk.grey(placeholder) : undefined;
|
|
71
|
+
// Fake cursor — vendored rendering from ink-text-input v6.
|
|
72
|
+
if (showCursor && focus) {
|
|
73
|
+
renderedPlaceholder =
|
|
74
|
+
placeholder.length > 0
|
|
75
|
+
? chalk.inverse(placeholder[0]) + chalk.grey(placeholder.slice(1))
|
|
76
|
+
: chalk.inverse(' ');
|
|
77
|
+
renderedValue = value.length > 0 ? '' : chalk.inverse(' ');
|
|
78
|
+
let i = 0;
|
|
79
|
+
for (const char of value) {
|
|
80
|
+
renderedValue += i === cursorOffset ? chalk.inverse(char) : char;
|
|
81
|
+
i++;
|
|
82
|
+
}
|
|
83
|
+
if (value.length > 0 && cursorOffset === value.length) {
|
|
84
|
+
renderedValue += chalk.inverse(' ');
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
useInput((input, key) => {
|
|
88
|
+
if (key.upArrow ||
|
|
89
|
+
key.downArrow ||
|
|
90
|
+
(key.ctrl && input === 'c') ||
|
|
91
|
+
key.tab ||
|
|
92
|
+
(key.shift && key.tab)) {
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
if (key.return) {
|
|
96
|
+
// valueRef (not the prop) — so an Enter in the same stdin drain as
|
|
97
|
+
// the paste tail submits the complete line.
|
|
98
|
+
if (onSubmit)
|
|
99
|
+
onSubmit(valueRef.current);
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
const currentValue = valueRef.current;
|
|
103
|
+
let nextCursorOffset = cursorRef.current;
|
|
104
|
+
let nextValue = currentValue;
|
|
105
|
+
if (key.leftArrow) {
|
|
106
|
+
if (showCursor)
|
|
107
|
+
nextCursorOffset--;
|
|
108
|
+
}
|
|
109
|
+
else if (key.rightArrow) {
|
|
110
|
+
if (showCursor)
|
|
111
|
+
nextCursorOffset++;
|
|
112
|
+
}
|
|
113
|
+
else if (key.backspace || key.delete) {
|
|
114
|
+
if (cursorRef.current > 0) {
|
|
115
|
+
nextValue =
|
|
116
|
+
currentValue.slice(0, cursorRef.current - 1) +
|
|
117
|
+
currentValue.slice(cursorRef.current, currentValue.length);
|
|
118
|
+
nextCursorOffset--;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
else {
|
|
122
|
+
nextValue =
|
|
123
|
+
currentValue.slice(0, cursorRef.current) +
|
|
124
|
+
input +
|
|
125
|
+
currentValue.slice(cursorRef.current, currentValue.length);
|
|
126
|
+
nextCursorOffset += input.length;
|
|
127
|
+
}
|
|
128
|
+
if (nextCursorOffset < 0)
|
|
129
|
+
nextCursorOffset = 0;
|
|
130
|
+
if (nextCursorOffset > nextValue.length)
|
|
131
|
+
nextCursorOffset = nextValue.length;
|
|
132
|
+
// Synchronous mirror update FIRST — the next same-tick chunk builds
|
|
133
|
+
// on this, not on the stale render.
|
|
134
|
+
valueRef.current = nextValue;
|
|
135
|
+
cursorRef.current = nextCursorOffset;
|
|
136
|
+
setCursorOffset(nextCursorOffset);
|
|
137
|
+
if (nextValue !== currentValue) {
|
|
138
|
+
// Register the outgoing value so its (possibly late) prop echo is
|
|
139
|
+
// recognised and never rewinds the mirror.
|
|
140
|
+
pendingEchoes.current.push(nextValue);
|
|
141
|
+
onChange(nextValue);
|
|
142
|
+
}
|
|
143
|
+
}, { isActive: focus });
|
|
144
|
+
return (_jsx(Text, { children: placeholder
|
|
145
|
+
? value.length > 0
|
|
146
|
+
? renderedValue
|
|
147
|
+
: renderedPlaceholder
|
|
148
|
+
: renderedValue }));
|
|
149
|
+
}
|
|
150
|
+
export default PasteSafeTextInput;
|
|
@@ -17,7 +17,10 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
|
17
17
|
*/
|
|
18
18
|
import { useState, useEffect } from 'react';
|
|
19
19
|
import { Box, Text, useInput } from 'ink';
|
|
20
|
-
|
|
20
|
+
// D29 paste fix (2026-06-12): paste-safe replacement for ink-text-input —
|
|
21
|
+
// pasted free-form answers were losing every chunk but the last on large
|
|
22
|
+
// pastes (stale-prop closure upstream). See ../text-input.tsx.
|
|
23
|
+
import TextInput from '../text-input.js';
|
|
21
24
|
const PEACH = '#FFCBA4';
|
|
22
25
|
/**
|
|
23
26
|
* Reclaim raw-mode stdin when an Ink modal mounts (2026-06-08).
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cspeach/cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "1.0.0",
|
|
4
4
|
"description": "CSPeach — AI-assisted ABAP development CLI. Direct SAP access, 34 skills, safety gates.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"abap",
|
|
@@ -41,42 +41,58 @@
|
|
|
41
41
|
"smoke:all": "pnpm zcspeach:smoke && pnpm adt:smoke",
|
|
42
42
|
"zcspeach:smoke": "tsx scripts/zcspeach-smoke.ts",
|
|
43
43
|
"bench:models": "tsx scripts/bench-models.ts",
|
|
44
|
-
"
|
|
44
|
+
"fiori": "tsx src/tools/fiori/bin.ts",
|
|
45
|
+
"prepare": "husky",
|
|
46
|
+
"prepublishOnly": "node -e \"const d=(require('./package.json').dependencies||{})['@cspeach/sap-client']||''; if(d.startsWith('file:')||d.startsWith('link:')){console.error('publish blocked: @cspeach/sap-client is a local link ('+d+') — point it at the published version before npm publish'); process.exit(1)}\""
|
|
45
47
|
},
|
|
46
48
|
"engines": {
|
|
47
49
|
"node": ">=20.0.0"
|
|
48
50
|
},
|
|
49
51
|
"dependencies": {
|
|
50
52
|
"@anthropic-ai/sdk": "^0.90.0",
|
|
51
|
-
"@cspeach/sap-client": "^0.1.
|
|
53
|
+
"@cspeach/sap-client": "^0.1.10",
|
|
52
54
|
"@iarna/toml": "^2.2.5",
|
|
53
55
|
"@inkjs/ui": "^2.0.0",
|
|
54
56
|
"@inquirer/prompts": "^7.0.0",
|
|
55
57
|
"@noble/ed25519": "2.1.0",
|
|
56
58
|
"@noble/hashes": "1.4.0",
|
|
59
|
+
"@sap-ux/fiori-elements-writer": "^3.0.39",
|
|
60
|
+
"@sap-ux/fiori-freestyle-writer": "3.0.12",
|
|
57
61
|
"chalk": "^5.3.0",
|
|
58
62
|
"cli-highlight": "^2.1.11",
|
|
59
63
|
"cli-table3": "^0.6.5",
|
|
60
64
|
"clipboardy": "^5.3.1",
|
|
65
|
+
"cross-spawn": "^7.0.6",
|
|
61
66
|
"diff": "^9.0.0",
|
|
67
|
+
"ejs": "3.1.10",
|
|
62
68
|
"fuse.js": "^7.0.0",
|
|
63
69
|
"ink": "^5.0.0",
|
|
64
70
|
"ink-text-input": "^6.0.0",
|
|
65
71
|
"jose": "^5.9.0",
|
|
66
72
|
"keytar": "^7.9.0",
|
|
73
|
+
"mammoth": "^1.8.0",
|
|
67
74
|
"marked": "^11.2.0",
|
|
68
75
|
"marked-terminal": "^6.2.0",
|
|
76
|
+
"mem-fs": "2.1.0",
|
|
77
|
+
"mem-fs-editor": "9.4.0",
|
|
69
78
|
"node-machine-id": "^1.1.12",
|
|
70
79
|
"openai": "4.67.3",
|
|
71
80
|
"react": "^18.3.0",
|
|
81
|
+
"turndown": "^7.2.0",
|
|
82
|
+
"unpdf": "^0.12.1",
|
|
72
83
|
"zod": "^3.23.0"
|
|
73
84
|
},
|
|
74
85
|
"devDependencies": {
|
|
86
|
+
"@types/cross-spawn": "^6.0.6",
|
|
75
87
|
"@types/diff": "^7.0.2",
|
|
88
|
+
"@types/ejs": "^3.1.5",
|
|
76
89
|
"@types/node": "^20.17.0",
|
|
77
90
|
"@types/react": "^18.3.0",
|
|
91
|
+
"@types/turndown": "^5.0.5",
|
|
92
|
+
"docx": "^9.0.2",
|
|
78
93
|
"husky": "^9.1.7",
|
|
79
94
|
"ink-testing-library": "^4.0.0",
|
|
95
|
+
"pdf-lib": "^1.17.1",
|
|
80
96
|
"tsx": "^4.19.0",
|
|
81
97
|
"typescript": "^5.7.0",
|
|
82
98
|
"vitest": "^2.1.0"
|
package/bench/README.md
DELETED
|
@@ -1,78 +0,0 @@
|
|
|
1
|
-
# Model bench
|
|
2
|
-
|
|
3
|
-
Side-by-side outputs from Opus, Sonnet, and Haiku on the same ABAP prompt so
|
|
4
|
-
you can decide per-skill which model carries the load before changing any
|
|
5
|
-
default.
|
|
6
|
-
|
|
7
|
-
## Run
|
|
8
|
-
|
|
9
|
-
```bash
|
|
10
|
-
pnpm bench:models
|
|
11
|
-
pnpm bench:models --prompt abap-explain-bdef-handler
|
|
12
|
-
pnpm bench:models --models claude-opus-4-7,claude-sonnet-4-6
|
|
13
|
-
```
|
|
14
|
-
|
|
15
|
-
Routes through whichever LLM mode `cspeach config` is in — managed (proxy),
|
|
16
|
-
BYOK, AI Hub, or local. In managed mode the proxy injects the **real skill
|
|
17
|
-
body from KV** as the system prompt, so each bench call is end-to-end
|
|
18
|
-
identical to what a real `/abap-explain` invocation would do — just on a
|
|
19
|
-
different model.
|
|
20
|
-
|
|
21
|
-
## Prompts
|
|
22
|
-
|
|
23
|
-
Drop new prompts in `prompts/<name>.md`. Format:
|
|
24
|
-
|
|
25
|
-
```markdown
|
|
26
|
-
---
|
|
27
|
-
skill: abap-explain # required — must be a real CSPeach skill name
|
|
28
|
-
description: short label
|
|
29
|
-
---
|
|
30
|
-
|
|
31
|
-
## user
|
|
32
|
-
[the user-side prompt the model receives]
|
|
33
|
-
```
|
|
34
|
-
|
|
35
|
-
Optionally include a `## system` section — it's **only** used in non-managed
|
|
36
|
-
modes (BYOK, AI Hub, local) where there's no proxy to inject a skill body.
|
|
37
|
-
In managed mode `## system` is ignored.
|
|
38
|
-
|
|
39
|
-
## Results
|
|
40
|
-
|
|
41
|
-
`results/<prompt-name>/<model>.md` — full assistant text plus latency, tokens,
|
|
42
|
-
and a USD estimate (using the same pricing table the cost footer uses).
|
|
43
|
-
|
|
44
|
-
Read both files side by side. If Sonnet looks like a write-off on a given
|
|
45
|
-
skill, keep the skill on Opus. If it looks "as good or better," that skill
|
|
46
|
-
graduates down on the next config refactor.
|
|
47
|
-
|
|
48
|
-
## Cost note
|
|
49
|
-
|
|
50
|
-
Each prompt × model call is one full skill turn through the proxy. The proxy
|
|
51
|
-
loads the full skill body as system prompt, so input is in the 5-15k token
|
|
52
|
-
range. A full 3-prompt × 3-model bench is roughly $0.30–$0.80 depending on
|
|
53
|
-
output verbosity. (Haiku columns will be much cheaper than Opus.)
|
|
54
|
-
|
|
55
|
-
## Verdict log
|
|
56
|
-
|
|
57
|
-
Findings from each bench run, oldest first. Each entry should name the run
|
|
58
|
-
date, the prompts that were benched, and the model decision per skill — so
|
|
59
|
-
the config refactor (`resolveModelForSkill` + `[models.skills]`) can be
|
|
60
|
-
driven from evidence, not guesses.
|
|
61
|
-
|
|
62
|
-
### 2026-05-18 — initial 3-prompt run
|
|
63
|
-
|
|
64
|
-
Prompts: `abap-explain-bdef-handler`, `abap-document-cds`, `abap-test-method`.
|
|
65
|
-
|
|
66
|
-
| Skill | Sonnet 4.6 | Haiku 4.5 | Decision |
|
|
67
|
-
|---|---|---|---|
|
|
68
|
-
| `abap-explain` | matches Opus, catches `IN LOCAL MODE` non-retrigger nuance | typo `cl_abap_behav_handler` (wrong class name) | **Sonnet OK; Haiku unsafe** |
|
|
69
|
-
| `abap-document` | matches Opus + adds client-handling + `@VDM.viewType` value | invented `DSOD` authority object; misread fuzzinessThreshold direction | **Sonnet OK; Haiku unsafe** |
|
|
70
|
-
| `abap-test` | comparable rigor; asserts exception **textid** (Opus only `assert_bound`) | clean code; coverage-estimate prose shaky | **Sonnet OK; Haiku borderline** |
|
|
71
|
-
|
|
72
|
-
Pattern: SAP/ABAP corpus is thinner than Python/JS — Haiku hallucinates SAP
|
|
73
|
-
terms on multi-paragraph synthesis. Sonnet does not. Do NOT route any
|
|
74
|
-
ABAP-content skill to Haiku based on this evidence.
|
|
75
|
-
|
|
76
|
-
Skills NOT yet benched (kept on Opus by default): `abap-generate`, `-rap`,
|
|
77
|
-
`-modernize`, `-design`, `-refactor`, `-review`, `-cca`, anything else
|
|
78
|
-
producing new code or architectural decisions. Bench before routing.
|
|
@@ -1,44 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
skill: abap-document
|
|
3
|
-
description: Document a CDS interface view with associations + currency annotation
|
|
4
|
-
---
|
|
5
|
-
|
|
6
|
-
## user
|
|
7
|
-
|
|
8
|
-
Please document this CDS view:
|
|
9
|
-
|
|
10
|
-
```cds
|
|
11
|
-
@AccessControl.authorizationCheck: #CHECK
|
|
12
|
-
@EndUserText.label: 'Sales Order — Interface View'
|
|
13
|
-
@Search.searchable: true
|
|
14
|
-
define view entity ZI_SalesOrder
|
|
15
|
-
as select from zsales_order as Header
|
|
16
|
-
|
|
17
|
-
association [0..1] to ZI_Customer as _Customer on $projection.CustomerID = _Customer.CustomerID
|
|
18
|
-
association [0..*] to ZI_SalesOrderItem as _SalesOrderItem on $projection.SalesOrderID = _SalesOrderItem.SalesOrderID
|
|
19
|
-
association [0..1] to I_Currency as _Currency on $projection.CurrencyCode = _Currency.Currency
|
|
20
|
-
|
|
21
|
-
{
|
|
22
|
-
key Header.sales_order_id as SalesOrderID,
|
|
23
|
-
Header.customer_id as CustomerID,
|
|
24
|
-
Header.order_date as OrderDate,
|
|
25
|
-
|
|
26
|
-
@Search.defaultSearchElement: true
|
|
27
|
-
@Search.fuzzinessThreshold: 0.8
|
|
28
|
-
@ObjectModel.text.element: [ '_Customer.CustomerName' ]
|
|
29
|
-
Header.customer_id as CustomerIDForSearch,
|
|
30
|
-
|
|
31
|
-
@Semantics.amount.currencyCode: 'CurrencyCode'
|
|
32
|
-
Header.net_amount as NetAmount,
|
|
33
|
-
Header.currency_code as CurrencyCode,
|
|
34
|
-
|
|
35
|
-
Header.created_at as CreatedAt,
|
|
36
|
-
Header.created_by as CreatedBy,
|
|
37
|
-
Header.last_changed_at as LastChangedAt,
|
|
38
|
-
Header.last_changed_by as LastChangedBy,
|
|
39
|
-
|
|
40
|
-
_Customer,
|
|
41
|
-
_SalesOrderItem,
|
|
42
|
-
_Currency
|
|
43
|
-
}
|
|
44
|
-
```
|
|
@@ -1,57 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
skill: abap-explain
|
|
3
|
-
description: Explain a managed RAP behavior pool with a validation + determination
|
|
4
|
-
---
|
|
5
|
-
|
|
6
|
-
## user
|
|
7
|
-
|
|
8
|
-
Please explain this behavior pool:
|
|
9
|
-
|
|
10
|
-
```abap
|
|
11
|
-
CLASS lhc_salesorder DEFINITION INHERITING FROM cl_abap_behavior_handler.
|
|
12
|
-
PRIVATE SECTION.
|
|
13
|
-
METHODS validateorderdate FOR VALIDATE ON SAVE
|
|
14
|
-
IMPORTING keys FOR SalesOrder~validateorderdate.
|
|
15
|
-
METHODS determinenetamount FOR DETERMINE ON MODIFY
|
|
16
|
-
IMPORTING keys FOR SalesOrder~determinenetamount.
|
|
17
|
-
ENDCLASS.
|
|
18
|
-
|
|
19
|
-
CLASS lhc_salesorder IMPLEMENTATION.
|
|
20
|
-
METHOD validateorderdate.
|
|
21
|
-
READ ENTITIES OF zr_salesorder IN LOCAL MODE
|
|
22
|
-
ENTITY SalesOrder FIELDS ( OrderDate ) WITH CORRESPONDING #( keys )
|
|
23
|
-
RESULT DATA(lt_orders).
|
|
24
|
-
|
|
25
|
-
LOOP AT lt_orders INTO DATA(ls_order).
|
|
26
|
-
IF ls_order-OrderDate < cl_abap_context_info=>get_system_date( ).
|
|
27
|
-
APPEND VALUE #( %tky = ls_order-%tky ) TO failed-salesorder.
|
|
28
|
-
APPEND VALUE #(
|
|
29
|
-
%tky = ls_order-%tky
|
|
30
|
-
%msg = new_message_with_text(
|
|
31
|
-
severity = if_abap_behv_message=>severity-error
|
|
32
|
-
text = 'Order date must be today or in the future' )
|
|
33
|
-
) TO reported-salesorder.
|
|
34
|
-
ENDIF.
|
|
35
|
-
ENDLOOP.
|
|
36
|
-
ENDMETHOD.
|
|
37
|
-
|
|
38
|
-
METHOD determinenetamount.
|
|
39
|
-
READ ENTITIES OF zr_salesorder IN LOCAL MODE
|
|
40
|
-
ENTITY SalesOrder FIELDS ( Quantity UnitPrice ) WITH CORRESPONDING #( keys )
|
|
41
|
-
RESULT DATA(lt_orders).
|
|
42
|
-
|
|
43
|
-
DATA lt_update TYPE TABLE FOR UPDATE zr_salesorder\\SalesOrder.
|
|
44
|
-
LOOP AT lt_orders INTO DATA(ls_order).
|
|
45
|
-
APPEND VALUE #(
|
|
46
|
-
%tky = ls_order-%tky
|
|
47
|
-
NetAmount = ls_order-Quantity * ls_order-UnitPrice
|
|
48
|
-
%control-NetAmount = if_abap_behv=>mk-on
|
|
49
|
-
) TO lt_update.
|
|
50
|
-
ENDLOOP.
|
|
51
|
-
|
|
52
|
-
MODIFY ENTITIES OF zr_salesorder IN LOCAL MODE
|
|
53
|
-
ENTITY SalesOrder UPDATE FIELDS ( NetAmount )
|
|
54
|
-
WITH lt_update.
|
|
55
|
-
ENDMETHOD.
|
|
56
|
-
ENDCLASS.
|
|
57
|
-
```
|
|
@@ -1,42 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
skill: abap-test
|
|
3
|
-
description: Generate an ABAP Unit test class for a small business rule method
|
|
4
|
-
---
|
|
5
|
-
|
|
6
|
-
## user
|
|
7
|
-
|
|
8
|
-
Please write an ABAP Unit test class for this method:
|
|
9
|
-
|
|
10
|
-
```abap
|
|
11
|
-
CLASS zcl_discount_calculator DEFINITION
|
|
12
|
-
PUBLIC FINAL CREATE PUBLIC.
|
|
13
|
-
|
|
14
|
-
PUBLIC SECTION.
|
|
15
|
-
"! Returns the discount percentage (0..30) for a given net amount.
|
|
16
|
-
"! < 1000 → 0%
|
|
17
|
-
"! < 5000 → 5%
|
|
18
|
-
"! < 10000 → 10%
|
|
19
|
-
"! >= 10000 → 15%
|
|
20
|
-
"! Raises zcx_discount_error on negative input.
|
|
21
|
-
METHODS calculate_discount
|
|
22
|
-
IMPORTING iv_net_amount TYPE p LENGTH 13 DECIMALS 2
|
|
23
|
-
RETURNING VALUE(rv_discount) TYPE p LENGTH 3 DECIMALS 2
|
|
24
|
-
RAISING zcx_discount_error.
|
|
25
|
-
ENDCLASS.
|
|
26
|
-
|
|
27
|
-
CLASS zcl_discount_calculator IMPLEMENTATION.
|
|
28
|
-
METHOD calculate_discount.
|
|
29
|
-
IF iv_net_amount < 0.
|
|
30
|
-
RAISE EXCEPTION TYPE zcx_discount_error
|
|
31
|
-
EXPORTING textid = zcx_discount_error=>negative_input.
|
|
32
|
-
ENDIF.
|
|
33
|
-
|
|
34
|
-
rv_discount = COND #(
|
|
35
|
-
WHEN iv_net_amount < 1000 THEN '0.00'
|
|
36
|
-
WHEN iv_net_amount < 5000 THEN '5.00'
|
|
37
|
-
WHEN iv_net_amount < 10000 THEN '10.00'
|
|
38
|
-
ELSE '15.00'
|
|
39
|
-
).
|
|
40
|
-
ENDMETHOD.
|
|
41
|
-
ENDCLASS.
|
|
42
|
-
```
|