@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.
Files changed (131) hide show
  1. package/README.md +1 -1
  2. package/dist/agent/intent-system-prompt.js +1 -1
  3. package/dist/agent/loop.js +209 -20
  4. package/dist/agent/providers/license-gate.js +44 -0
  5. package/dist/agent/skill-checkpoint.js +1 -1
  6. package/dist/agent/tool-dispatch.js +15 -0
  7. package/dist/approvals/canonical.js +91 -0
  8. package/dist/approvals/jwt.js +39 -2
  9. package/dist/auth/org-anthropic-key.js +25 -0
  10. package/dist/classifier/client.js +18 -3
  11. package/dist/commands/config-set.js +95 -0
  12. package/dist/commands/login.js +31 -14
  13. package/dist/commands/plan-model-tier.js +83 -0
  14. package/dist/commands/plan-resume.js +148 -21
  15. package/dist/config/loader.js +95 -1
  16. package/dist/doctor/checks/_http-probe.js +1 -0
  17. package/dist/doctor/checks/cert.js +14 -3
  18. package/dist/doctor/checks/sap.js +30 -8
  19. package/dist/doctor/checks/zcspeach.js +19 -4
  20. package/dist/one-shot.js +52 -4
  21. package/dist/projects/answer-blockers.js +137 -0
  22. package/dist/projects/extract-cca.js +108 -16
  23. package/dist/projects/extract-modernize.js +1 -1
  24. package/dist/projects/extract-plan.js +130 -37
  25. package/dist/projects/extract-spec-gap.js +34 -7
  26. package/dist/projects/extract-test-coverage.js +1 -1
  27. package/dist/projects/extract-upgrade.js +113 -22
  28. package/dist/projects/index.js +5 -2
  29. package/dist/projects/merge-cca.js +292 -0
  30. package/dist/projects/merge-upgrade.js +173 -0
  31. package/dist/projects/migration.js +103 -1
  32. package/dist/projects/output-paths.js +27 -0
  33. package/dist/projects/plan-run.js +159 -25
  34. package/dist/projects/plan-schema.js +63 -3
  35. package/dist/projects/promote-command.js +25 -2
  36. package/dist/projects/promote.js +128 -0
  37. package/dist/projects/save-command.js +247 -20
  38. package/dist/projects/status.js +3 -1
  39. package/dist/projects/validate.js +1 -1
  40. package/dist/projects/workspace.js +164 -20
  41. package/dist/renderer/notices.js +64 -0
  42. package/dist/renderer/progress-chatter.js +8 -0
  43. package/dist/renderer/tool-widget.js +18 -4
  44. package/dist/renderer/tty.js +43 -4
  45. package/dist/renderer/verify-chain.js +77 -0
  46. package/dist/repl/at-picker.js +60 -7
  47. package/dist/repl/builtin-commands.js +37 -0
  48. package/dist/repl/early-line-buffer.js +68 -0
  49. package/dist/repl/inquirer-guard.js +70 -5
  50. package/dist/repl/numbered-menu.js +131 -0
  51. package/dist/repl/post-turn-status.js +2 -2
  52. package/dist/repl/rule8-detector.js +17 -2
  53. package/dist/repl/safety-confirm.js +111 -2
  54. package/dist/repl/safety-mode-state.js +19 -3
  55. package/dist/repl/slash-picker.js +10 -15
  56. package/dist/repl.js +301 -35
  57. package/dist/router/classifier.js +150 -6
  58. package/dist/sap/capability-matrix.js +20 -0
  59. package/dist/sap/capability-matrix.json +11236 -0
  60. package/dist/sap/capability.js +146 -0
  61. package/dist/sap/connection-manager.js +19 -1
  62. package/dist/sap/onboarding.js +42 -4
  63. package/dist/session/pending.js +27 -0
  64. package/dist/skill-catalog.js +48 -43
  65. package/dist/skills/bundled-skills.js +279 -1
  66. package/dist/skills/promotion-dispatch.js +23 -0
  67. package/dist/tools/_command-shared.js +36 -12
  68. package/dist/tools/_filesystem-shared.js +139 -4
  69. package/dist/tools/_flag.js +25 -0
  70. package/dist/tools/approval.js +64 -21
  71. package/dist/tools/ask-question.js +96 -4
  72. package/dist/tools/capability/tool.js +74 -0
  73. package/dist/tools/dispatch-skill.js +22 -1
  74. package/dist/tools/extend-model/anchored-insert.js +810 -0
  75. package/dist/tools/extend-model/tool.js +188 -0
  76. package/dist/tools/filesystem/extract-document.js +57 -0
  77. package/dist/tools/filesystem/file-edit.js +12 -2
  78. package/dist/tools/filesystem/file-read.js +2 -2
  79. package/dist/tools/filesystem/file-write.js +11 -2
  80. package/dist/tools/filesystem/glob.js +11 -0
  81. package/dist/tools/filesystem/grep.js +10 -0
  82. package/dist/tools/filesystem/read-document.js +107 -0
  83. package/dist/tools/fiori/apply.js +50 -0
  84. package/dist/tools/fiori/bin.js +3 -0
  85. package/dist/tools/fiori/catalog/index.js +27 -0
  86. package/dist/tools/fiori/catalog/value-help.js +230 -0
  87. package/dist/tools/fiori/catalog/viz-chart.js +177 -0
  88. package/dist/tools/fiori/cli.js +71 -0
  89. package/dist/tools/fiori/deploy-config.js +73 -0
  90. package/dist/tools/fiori/fe-scaffold.js +45 -0
  91. package/dist/tools/fiori/i18n.js +39 -0
  92. package/dist/tools/fiori/manifest.js +70 -0
  93. package/dist/tools/fiori/render.js +77 -0
  94. package/dist/tools/fiori/scaffold.js +39 -0
  95. package/dist/tools/fiori/tools.js +356 -0
  96. package/dist/tools/fiori/types.js +1 -0
  97. package/dist/tools/local-build.js +76 -0
  98. package/dist/tools/local-files.js +31 -0
  99. package/dist/tools/project/_merge-shared.js +68 -0
  100. package/dist/tools/project/cca_merge.js +164 -0
  101. package/dist/tools/project/playbook_get.js +1 -1
  102. package/dist/tools/project/upgrade_merge_progress.js +206 -0
  103. package/dist/tools/sap-read.js +53 -9
  104. package/dist/tools/sap-write.js +530 -21
  105. package/dist/tools/shell/shell_exec.js +41 -6
  106. package/dist/tools/snapshot.js +37 -14
  107. package/dist/tools/subagent/background_run.js +17 -1
  108. package/dist/tools/transport-resolution.js +86 -0
  109. package/dist/tools/transport.js +224 -5
  110. package/dist/tools/write-mode.js +4 -0
  111. package/dist/ui/app.js +6 -2
  112. package/dist/ui/body.js +13 -0
  113. package/dist/ui/footer.js +20 -6
  114. package/dist/ui/line-resolution.js +17 -6
  115. package/dist/ui/session-timeline.js +1 -0
  116. package/dist/ui/text-input.js +150 -0
  117. package/dist/ui/widgets/ask-question-modal.js +4 -1
  118. package/package.json +19 -3
  119. package/bench/README.md +0 -78
  120. package/bench/prompts/abap-document-cds.md +0 -44
  121. package/bench/prompts/abap-explain-bdef-handler.md +0 -57
  122. package/bench/prompts/abap-test-method.md +0 -42
  123. package/bench/results/abap-document-cds/claude-haiku-4-5.md +0 -189
  124. package/bench/results/abap-document-cds/claude-opus-4-7.md +0 -120
  125. package/bench/results/abap-document-cds/claude-sonnet-4-6.md +0 -151
  126. package/bench/results/abap-explain-bdef-handler/claude-haiku-4-5.md +0 -112
  127. package/bench/results/abap-explain-bdef-handler/claude-opus-4-7.md +0 -101
  128. package/bench/results/abap-explain-bdef-handler/claude-sonnet-4-6.md +0 -101
  129. package/bench/results/abap-test-method/claude-haiku-4-5.md +0 -186
  130. package/bench/results/abap-test-method/claude-opus-4-7.md +0 -193
  131. 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
- import TextInput from 'ink-text-input';
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
- const HISTORY_FILE = path.join(os.homedir(), '.cspeach', 'history');
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(HISTORY_FILE, 'utf8').split('\n').filter(Boolean);
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
- fs.mkdirSync(path.dirname(HISTORY_FILE), { recursive: true });
23
- fs.appendFileSync(HISTORY_FILE, line + '\n');
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, filterFileChoices } from '../repl/at-picker.js';
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
- const seeded = filterFileChoices(atPrefix, files);
42
- if (seeded.length === 1) {
43
- return emptyBodyGuard(trimmed.replace(/@\S*$/, `@${seeded[0].filename}`));
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
- import TextInput from 'ink-text-input';
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.9.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
- "prepare": "husky"
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.3",
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
- ```