@cspeach/cli 0.8.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 +330 -36
- package/dist/agent/providers/license-gate.js +44 -0
- package/dist/agent/skill-checkpoint.js +1 -1
- package/dist/agent/steering-queue.js +27 -0
- package/dist/agent/tool-dispatch.js +15 -0
- package/dist/approvals/canonical.js +91 -0
- package/dist/approvals/jwt.js +84 -7
- package/dist/approvals/render.js +38 -0
- package/dist/auth/org-anthropic-key.js +25 -0
- package/dist/classifier/client.js +24 -5
- 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 +435 -0
- package/dist/config/loader.js +102 -3
- package/dist/cost/pricing.js +14 -5
- 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/email-template.js +2 -0
- package/dist/projects/extract-cca.js +108 -16
- package/dist/projects/extract-modernize.js +1 -1
- package/dist/projects/extract-plan.js +178 -0
- 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 +7 -1
- 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 +254 -0
- package/dist/projects/plan-schema.js +210 -0
- package/dist/projects/promote-command.js +26 -2
- package/dist/projects/promote.js +128 -0
- package/dist/projects/save-command.js +263 -20
- package/dist/projects/status.js +22 -0
- package/dist/projects/validate.js +2 -0
- 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/syntax.js +16 -1
- package/dist/renderer/thinking-heartbeat.js +13 -1
- 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 +93 -21
- 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 +25 -19
- package/dist/repl.js +470 -22
- 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 +54 -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 +84 -11
- package/dist/ui/body.js +13 -0
- package/dist/ui/command-palette.js +46 -10
- package/dist/ui/file-palette.js +44 -0
- package/dist/ui/footer.js +28 -11
- package/dist/ui/line-resolution.js +92 -0
- package/dist/ui/session-timeline.js +1 -0
- package/dist/ui/text-input.js +150 -0
- package/dist/ui/turn-status-emitter.js +52 -0
- package/dist/ui/turn-status.js +59 -0
- package/dist/ui/widgets/ask-question-modal.js +30 -2
- package/package.json +23 -4
- 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
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { loadConfig } from '../config/loader.js';
|
|
2
2
|
import { getStore } from '../auth/api-key.js';
|
|
3
3
|
import { classify as classifyLocal } from '../classifier/client.js';
|
|
4
|
+
import { SKILL_CATALOG, SKILL_ALIASES, resolveSkillAlias } from '../skill-catalog.js';
|
|
5
|
+
import { isBuiltinCommandHead } from '../repl/builtin-commands.js';
|
|
4
6
|
// 8s — sized to cover the observed Fly cold-start (~3.2s on the 2026-05-20
|
|
5
7
|
// probe) plus headroom for a slow network. Tighter values risk timing out
|
|
6
8
|
// legitimate cold-start hits and burning the retry budget on the same.
|
|
@@ -42,6 +44,18 @@ async function tryFetchOnce(proxyUrl, key, prompt, context) {
|
|
|
42
44
|
}
|
|
43
45
|
}
|
|
44
46
|
export async function classifyPrompt(prompt, context) {
|
|
47
|
+
// D22/D35 structural invariant (2026-06-11) — an explicit slash command
|
|
48
|
+
// must NEVER reach the classifier. The S3 battery proved the failure mode:
|
|
49
|
+
// `/abap-plan <multiline>` failed the old parseShortcut regex, the whole
|
|
50
|
+
// line (slash included) was classified, routed to /abap-rap, and the
|
|
51
|
+
// plan-save hook keyed off the wrong skill label silently lost the
|
|
52
|
+
// envelope. Routing layers (repl.tsx, one-shot.ts) handle slash input
|
|
53
|
+
// before classification; this guard is the backstop that turns any
|
|
54
|
+
// future regression into a loud error instead of a silent misroute.
|
|
55
|
+
// Fires BEFORE config/key loading so no network call is ever made.
|
|
56
|
+
if (prompt.trimStart().startsWith('/')) {
|
|
57
|
+
throw new Error('slash command reached the classifier — explicit /commands must be routed before classification (CLI bug, please report)');
|
|
58
|
+
}
|
|
45
59
|
const cfg = await loadConfig();
|
|
46
60
|
const store = await getStore();
|
|
47
61
|
const key = await store.get();
|
|
@@ -82,23 +96,153 @@ export async function classifyPrompt(prompt, context) {
|
|
|
82
96
|
}
|
|
83
97
|
if (!r.ok)
|
|
84
98
|
throw new Error(`classifier HTTP ${r.status}`);
|
|
85
|
-
|
|
99
|
+
const result = (await r.json());
|
|
100
|
+
// Portfolio-merge resilience (2026-06-12): the proxy classifier's skill
|
|
101
|
+
// list can lag a rename (it still knows 'abap-dump-analyze' until its
|
|
102
|
+
// SKILL_LIST is redeployed). Map retired names — primary and alternates —
|
|
103
|
+
// to their successor so the CLI never dispatches a deleted skill.
|
|
104
|
+
result.skill = resolveSkillAlias(result.skill);
|
|
105
|
+
if (result.alternates?.length) {
|
|
106
|
+
result.alternates = [...new Set(result.alternates.map(resolveSkillAlias))]
|
|
107
|
+
.filter((s) => s !== result.skill);
|
|
108
|
+
}
|
|
109
|
+
return result;
|
|
86
110
|
}
|
|
87
111
|
export function parseShortcut(input) {
|
|
88
112
|
// `/skill-name rest` → { skill: 'abap-skill-name', prompt: 'rest' }
|
|
89
113
|
// abap- prefix auto-prepended if missing (all skills in KV use abap-* namespace).
|
|
90
114
|
// `@OBJECT_NAME rest` → { object: 'OBJECT_NAME', prompt: 'rest' }
|
|
115
|
+
// `@OBJECT_NAME /skill rest` → { object, skill, prompt: 'rest' } (composed)
|
|
91
116
|
// otherwise → { prompt: input }
|
|
92
|
-
|
|
117
|
+
//
|
|
118
|
+
// D22/D34 (2026-06-11): both regexes use `[\s\S]*` for the prompt body —
|
|
119
|
+
// `.` does not match `\n`, so the old `(.*)$` failed on any prompt with
|
|
120
|
+
// 2+ lines, fell through to { prompt: input }, and the slash command was
|
|
121
|
+
// silently fed to the intent classifier. Leading separator whitespace is
|
|
122
|
+
// stripped (greedy `\s*` / `\s+`); the prompt body's own newlines and
|
|
123
|
+
// indentation are preserved.
|
|
124
|
+
const slashMatch = input.match(/^\/([a-z0-9-]+)\s*([\s\S]*)$/i);
|
|
93
125
|
/** Auto-prepend ONLY inside the slash shortcut branch; plain prose goes through classifyPrompt. */
|
|
94
126
|
if (slashMatch) {
|
|
127
|
+
const typedHead = `/${slashMatch[1]}`;
|
|
95
128
|
let skill = slashMatch[1];
|
|
96
129
|
if (!skill.startsWith('abap-'))
|
|
97
130
|
skill = 'abap-' + skill;
|
|
98
|
-
|
|
131
|
+
// Retired-name redirect: '/abap-dump-analyze ...' keeps routing (to
|
|
132
|
+
// 'abap-dump') for one release. renamedFrom drives the dim notice.
|
|
133
|
+
if (skill in SKILL_ALIASES) {
|
|
134
|
+
return { skill: SKILL_ALIASES[skill], prompt: slashMatch[2], typedHead, renamedFrom: skill };
|
|
135
|
+
}
|
|
136
|
+
return { skill, prompt: slashMatch[2], typedHead };
|
|
137
|
+
}
|
|
138
|
+
const atMatch = input.match(/^@([A-Z0-9_]+)\s+([\s\S]*)$/i);
|
|
139
|
+
if (atMatch) {
|
|
140
|
+
const object = atMatch[1];
|
|
141
|
+
const body = atMatch[2];
|
|
142
|
+
// Composition (2026-06-11): `@ZCL_X /abap-explain why` — explicit skill
|
|
143
|
+
// after the object token. Re-parse the body so the skill routes; without
|
|
144
|
+
// this, prompt='/abap-explain why' leaks to classifyPrompt, whose slash
|
|
145
|
+
// guard throws (REPL: "[classifier error]"; one-shot: uncaught). The
|
|
146
|
+
// re-parsed skill goes through the same isKnownSkill / rejectUnknownSkill
|
|
147
|
+
// checks at the call sites — the object variant gets no bypass.
|
|
148
|
+
if (body.startsWith('/')) {
|
|
149
|
+
const inner = parseShortcut(body);
|
|
150
|
+
if (inner.skill) {
|
|
151
|
+
return { object, skill: inner.skill, prompt: inner.prompt, typedHead: inner.typedHead, renamedFrom: inner.renamedFrom };
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
return { object, prompt: body };
|
|
99
155
|
}
|
|
100
|
-
const atMatch = input.match(/^@([A-Z0-9_]+)\s+(.*)$/i);
|
|
101
|
-
if (atMatch)
|
|
102
|
-
return { object: atMatch[1], prompt: atMatch[2] };
|
|
103
156
|
return { prompt: input };
|
|
104
157
|
}
|
|
158
|
+
/** True when `skill` (full name, e.g. 'abap-plan') exists in the catalog. */
|
|
159
|
+
export function isKnownSkill(skill) {
|
|
160
|
+
return SKILL_CATALOG.some((s) => s.name === skill);
|
|
161
|
+
}
|
|
162
|
+
/**
|
|
163
|
+
* Levenshtein edit distance. Inputs are skill-name sized (≤ ~25 chars),
|
|
164
|
+
* so the O(a·b) two-row implementation is plenty.
|
|
165
|
+
*/
|
|
166
|
+
function levenshtein(a, b) {
|
|
167
|
+
let prev = Array.from({ length: b.length + 1 }, (_, i) => i);
|
|
168
|
+
for (let i = 1; i <= a.length; i++) {
|
|
169
|
+
const curr = [i];
|
|
170
|
+
for (let j = 1; j <= b.length; j++) {
|
|
171
|
+
curr[j] = Math.min(prev[j] + 1, // deletion
|
|
172
|
+
curr[j - 1] + 1, // insertion
|
|
173
|
+
prev[j - 1] + (a[i - 1] === b[j - 1] ? 0 : 1));
|
|
174
|
+
}
|
|
175
|
+
prev = curr;
|
|
176
|
+
}
|
|
177
|
+
return prev[b.length];
|
|
178
|
+
}
|
|
179
|
+
/**
|
|
180
|
+
* D35 — closest catalog skills for an unknown `/cmd`. Prefix completions
|
|
181
|
+
* rank first (typed a truncated name), then close typos (edit distance ≤ 3).
|
|
182
|
+
*/
|
|
183
|
+
export function suggestSkills(unknown, limit = 3) {
|
|
184
|
+
const u = unknown.toLowerCase();
|
|
185
|
+
return SKILL_CATALOG
|
|
186
|
+
.map((s) => ({ name: s.name, rank: s.name.startsWith(u) ? 0 : levenshtein(u, s.name) }))
|
|
187
|
+
.filter((s) => s.rank <= 3)
|
|
188
|
+
.sort((a, b) => a.rank - b.rank)
|
|
189
|
+
.slice(0, limit)
|
|
190
|
+
.map((s) => s.name);
|
|
191
|
+
}
|
|
192
|
+
/**
|
|
193
|
+
* D35 — user-facing notice for an explicit slash command that names a
|
|
194
|
+
* skill missing from the catalog. The routing layer prints this and ends
|
|
195
|
+
* the turn; the input must NEVER fall through to the classifier (the
|
|
196
|
+
* classifyPrompt guard above enforces that structurally).
|
|
197
|
+
*/
|
|
198
|
+
export function unknownSkillMessage(skill) {
|
|
199
|
+
const suggestions = suggestSkills(skill);
|
|
200
|
+
const didYouMean = suggestions.length > 0
|
|
201
|
+
? ` — did you mean ${suggestions.map((s) => `/${s}`).join(' or ')}?`
|
|
202
|
+
: '.';
|
|
203
|
+
return `unknown skill /${skill}${didYouMean} Type /help to see all skills.`;
|
|
204
|
+
}
|
|
205
|
+
/** Notice for a builtin command typed with arguments (e.g. `/help me`). */
|
|
206
|
+
export function builtinTakesNoArgsMessage(head) {
|
|
207
|
+
return `${head} doesn't take arguments — just type ${head}.`;
|
|
208
|
+
}
|
|
209
|
+
/** One-line notice when a retired skill name redirects to its successor. */
|
|
210
|
+
export function renamedSkillMessage(oldName, newName) {
|
|
211
|
+
return `/${oldName} is now /${newName} — routing there. The old name goes away next release.`;
|
|
212
|
+
}
|
|
213
|
+
/**
|
|
214
|
+
* Shared unknown-skill gate for all three routing layers (Ink REPL,
|
|
215
|
+
* classic REPL, one-shot). Call right after parseShortcut, BEFORE any
|
|
216
|
+
* dispatch or classification:
|
|
217
|
+
*
|
|
218
|
+
* if (rejectUnknownSkill(parsed, emit)) return; // turn ends here
|
|
219
|
+
*
|
|
220
|
+
* Returns true (and emits exactly one notice) when the parsed slash names
|
|
221
|
+
* a skill missing from the catalog — the input must NEVER fall through to
|
|
222
|
+
* the classifier (classifyPrompt asserts on it) and must never dispatch
|
|
223
|
+
* to a nonexistent skill. Returns false when there is nothing to reject
|
|
224
|
+
* (no slash, or a known skill).
|
|
225
|
+
*
|
|
226
|
+
* Builtin disambiguation (2026-06-11): `/help me` parses to skill
|
|
227
|
+
* 'abap-help' (unknown) — but the user meant the builtin. Builtins with
|
|
228
|
+
* arguments are never handled by the REPLs' exact-match builtin handlers,
|
|
229
|
+
* so they land here; emit a "doesn't take arguments" notice instead of
|
|
230
|
+
* the misleading "unknown skill /abap-help — did you mean ...".
|
|
231
|
+
*/
|
|
232
|
+
export function rejectUnknownSkill(parsed, emit) {
|
|
233
|
+
// Renamed-skill notice (2026-06-12): a retired name was redirected by
|
|
234
|
+
// parseShortcut. Tell the user once, then continue the turn normally —
|
|
235
|
+
// this is informational, NOT a rejection (return false).
|
|
236
|
+
if (parsed.skill && parsed.renamedFrom) {
|
|
237
|
+
emit(renamedSkillMessage(parsed.renamedFrom, parsed.skill));
|
|
238
|
+
return false;
|
|
239
|
+
}
|
|
240
|
+
if (!parsed.skill || isKnownSkill(parsed.skill))
|
|
241
|
+
return false;
|
|
242
|
+
if (parsed.typedHead && isBuiltinCommandHead(parsed.typedHead) && parsed.prompt.trim().length > 0) {
|
|
243
|
+
emit(builtinTakesNoArgsMessage(parsed.typedHead.toLowerCase()));
|
|
244
|
+
return true;
|
|
245
|
+
}
|
|
246
|
+
emit(unknownSkillMessage(parsed.skill));
|
|
247
|
+
return true;
|
|
248
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Snapshot loader for the committed ABAP feature matrix.
|
|
3
|
+
*
|
|
4
|
+
* The snapshot (capability-matrix.json) is DATA, not code — it is refreshed
|
|
5
|
+
* by re-pulling from the sap-docs MCP and overwriting the file, never
|
|
6
|
+
* hand-edited.
|
|
7
|
+
*
|
|
8
|
+
* Loading mechanism: a direct JSON import. The project's tsconfig enables
|
|
9
|
+
* `resolveJsonModule` with `module: NodeNext`, so `tsc` both type-checks the
|
|
10
|
+
* import AND copies the .json into `dist/sap/` alongside the emitted .js
|
|
11
|
+
* (verified: the emitted module does `require("./capability-matrix.json")`
|
|
12
|
+
* and the file is present in outDir). This is build-safe with the project's
|
|
13
|
+
* plain-`tsc` build — no asset-copy step or bundler is involved — and matches
|
|
14
|
+
* the fiori catalog's intent of keeping bundled data deterministic.
|
|
15
|
+
*/
|
|
16
|
+
import matrix from './capability-matrix.json' with { type: 'json' };
|
|
17
|
+
/** Returns the committed feature-matrix snapshot. Side-effect-free. */
|
|
18
|
+
export function loadCapabilityMatrix() {
|
|
19
|
+
return matrix;
|
|
20
|
+
}
|