@cspeach/cli 0.6.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 (210) hide show
  1. package/LICENSE +8 -0
  2. package/README.md +108 -0
  3. package/dist/agent/anthropic-provider.js +59 -0
  4. package/dist/agent/llm-provider.js +1 -0
  5. package/dist/agent/loop.js +709 -0
  6. package/dist/agent/maybe-build-project-context.js +126 -0
  7. package/dist/agent/providers/ai-hub-provider.js +58 -0
  8. package/dist/agent/providers/byok-provider.js +53 -0
  9. package/dist/agent/providers/factory.js +13 -0
  10. package/dist/agent/providers/local-provider.js +125 -0
  11. package/dist/agent/repair-partial.js +31 -0
  12. package/dist/agent/retry-key.js +58 -0
  13. package/dist/agent/retry.js +17 -0
  14. package/dist/agent/sap-connection-adapter.js +82 -0
  15. package/dist/agent/skill-checkpoint.js +119 -0
  16. package/dist/agent/tool-dispatch.js +47 -0
  17. package/dist/agent/turn-assistant-text.js +49 -0
  18. package/dist/agent/turn-error-ux.js +126 -0
  19. package/dist/agent/turn-stream.js +79 -0
  20. package/dist/agent/turn-watchdog.js +71 -0
  21. package/dist/approvals/advisory-prompt.js +40 -0
  22. package/dist/approvals/advisory-render.js +38 -0
  23. package/dist/approvals/approval-prompt.js +100 -0
  24. package/dist/approvals/jwt.js +33 -0
  25. package/dist/approvals/render.js +211 -0
  26. package/dist/approvals/risk-floor.js +26 -0
  27. package/dist/auth/api-key.js +40 -0
  28. package/dist/auth/auth-file.js +59 -0
  29. package/dist/auth/device.js +8 -0
  30. package/dist/auth/me.js +19 -0
  31. package/dist/classifier/client.js +58 -0
  32. package/dist/cli-args.js +38 -0
  33. package/dist/cli.js +148 -0
  34. package/dist/commands/config-set.js +245 -0
  35. package/dist/commands/config-show.js +159 -0
  36. package/dist/commands/help.js +93 -0
  37. package/dist/commands/login.js +122 -0
  38. package/dist/commands/logout.js +17 -0
  39. package/dist/commands/project-context-impact.js +215 -0
  40. package/dist/commands/reroute.js +60 -0
  41. package/dist/commands/spec-gap-status.js +52 -0
  42. package/dist/commands/whoami.js +35 -0
  43. package/dist/config/loader.js +67 -0
  44. package/dist/config/paths.js +20 -0
  45. package/dist/doctor/checks/_http-probe.js +56 -0
  46. package/dist/doctor/checks/auth.js +15 -0
  47. package/dist/doctor/checks/cert.js +24 -0
  48. package/dist/doctor/checks/forge-rules.js +102 -0
  49. package/dist/doctor/checks/keychain-fallback.js +14 -0
  50. package/dist/doctor/checks/keychain.js +23 -0
  51. package/dist/doctor/checks/llm-mode.js +27 -0
  52. package/dist/doctor/checks/proxy.js +13 -0
  53. package/dist/doctor/checks/sap.js +33 -0
  54. package/dist/doctor/checks/skill.js +24 -0
  55. package/dist/doctor/checks/write-mode.js +34 -0
  56. package/dist/doctor/checks/zcspeach.js +76 -0
  57. package/dist/doctor/run.js +46 -0
  58. package/dist/errors/codes.js +12 -0
  59. package/dist/index.js +6 -0
  60. package/dist/lock-contention.js +22 -0
  61. package/dist/one-shot.js +104 -0
  62. package/dist/project-context/conventions.js +309 -0
  63. package/dist/project-context/detect.js +250 -0
  64. package/dist/project-context/domain/abap-cloud.js +26 -0
  65. package/dist/project-context/domain/abapgit.js +177 -0
  66. package/dist/project-context/domain/cap.js +164 -0
  67. package/dist/project-context/domain/fiori.js +326 -0
  68. package/dist/project-context/git.js +115 -0
  69. package/dist/project-context/index-files.js +235 -0
  70. package/dist/project-context/index.js +117 -0
  71. package/dist/project-context/render.js +308 -0
  72. package/dist/project-context/types.js +14 -0
  73. package/dist/projects/build.js +20 -0
  74. package/dist/projects/canonicalize.js +39 -0
  75. package/dist/projects/email-template.js +54 -0
  76. package/dist/projects/extract-cca.js +139 -0
  77. package/dist/projects/extract-design.js +107 -0
  78. package/dist/projects/extract-estimate.js +93 -0
  79. package/dist/projects/extract-modernize.js +130 -0
  80. package/dist/projects/extract-spec-gap.js +101 -0
  81. package/dist/projects/extract-test-coverage.js +137 -0
  82. package/dist/projects/extract-upgrade.js +230 -0
  83. package/dist/projects/filename.js +18 -0
  84. package/dist/projects/index.js +8 -0
  85. package/dist/projects/migration.js +111 -0
  86. package/dist/projects/promote-command.js +96 -0
  87. package/dist/projects/promote.js +107 -0
  88. package/dist/projects/save-command.js +124 -0
  89. package/dist/projects/save.js +21 -0
  90. package/dist/projects/status.js +170 -0
  91. package/dist/projects/types.js +1 -0
  92. package/dist/projects/validate.js +146 -0
  93. package/dist/projects/workspace.js +478 -0
  94. package/dist/renderer/abap-inline.js +121 -0
  95. package/dist/renderer/banners.js +39 -0
  96. package/dist/renderer/highlighters/abap.js +126 -0
  97. package/dist/renderer/highlighters/bdef.js +81 -0
  98. package/dist/renderer/highlighters/cds.js +91 -0
  99. package/dist/renderer/markdown.js +291 -0
  100. package/dist/renderer/pipeline.js +201 -0
  101. package/dist/renderer/progress-chatter.js +237 -0
  102. package/dist/renderer/question-normalizer.js +306 -0
  103. package/dist/renderer/severity.js +61 -0
  104. package/dist/renderer/status-footer.js +50 -0
  105. package/dist/renderer/syntax.js +58 -0
  106. package/dist/renderer/tables.js +55 -0
  107. package/dist/renderer/thinking-heartbeat.js +70 -0
  108. package/dist/renderer/tool-widget.js +199 -0
  109. package/dist/renderer/tty.js +66 -0
  110. package/dist/renderer/widget-extractor.js +87 -0
  111. package/dist/renderer/widget-fallback.js +78 -0
  112. package/dist/renderer/widget-schemas.js +43 -0
  113. package/dist/repl/at-completer.js +64 -0
  114. package/dist/repl/at-picker.js +122 -0
  115. package/dist/repl/bracketed-paste.js +284 -0
  116. package/dist/repl/current-transport.js +46 -0
  117. package/dist/repl/diff-display.js +41 -0
  118. package/dist/repl/file-picker.js +219 -0
  119. package/dist/repl/inquirer-guard.js +130 -0
  120. package/dist/repl/inquirer-theme.js +41 -0
  121. package/dist/repl/rule8-detector.js +99 -0
  122. package/dist/repl/safety-confirm.js +106 -0
  123. package/dist/repl/safety-mode-state.js +36 -0
  124. package/dist/repl/slash-completer.js +59 -0
  125. package/dist/repl/slash-picker.js +124 -0
  126. package/dist/repl/update-method-preview-hook.js +45 -0
  127. package/dist/repl.js +1383 -0
  128. package/dist/router/classifier.js +38 -0
  129. package/dist/router/intent-extractor.js +140 -0
  130. package/dist/router/routing-decision.js +19 -0
  131. package/dist/sap/connection-manager.js +52 -0
  132. package/dist/sap/onboarding.js +178 -0
  133. package/dist/sap/system-info.js +515 -0
  134. package/dist/session/awaiting-answer.js +73 -0
  135. package/dist/session/gc.js +28 -0
  136. package/dist/session/pending.js +37 -0
  137. package/dist/session/resume.js +77 -0
  138. package/dist/session/schema.js +20 -0
  139. package/dist/session/store.js +147 -0
  140. package/dist/session/time-ago.js +41 -0
  141. package/dist/skill-catalog.js +222 -0
  142. package/dist/skills/bundled-skills.js +1 -0
  143. package/dist/skills/canonical.js +12 -0
  144. package/dist/skills/manifest-client.js +93 -0
  145. package/dist/skills/promotion-dispatch.js +24 -0
  146. package/dist/skills/signing-public-key.js +4 -0
  147. package/dist/skills/source-bundled.js +20 -0
  148. package/dist/skills/source-managed.js +26 -0
  149. package/dist/skills/source-manifest.js +26 -0
  150. package/dist/tools/_command-shared.js +110 -0
  151. package/dist/tools/_filesystem-shared.js +81 -0
  152. package/dist/tools/_flag.js +39 -0
  153. package/dist/tools/approval.js +228 -0
  154. package/dist/tools/ask-question.js +205 -0
  155. package/dist/tools/dispatch-skill.js +81 -0
  156. package/dist/tools/filesystem/file-edit.js +140 -0
  157. package/dist/tools/filesystem/file-read.js +89 -0
  158. package/dist/tools/filesystem/file-write.js +128 -0
  159. package/dist/tools/filesystem/glob.js +177 -0
  160. package/dist/tools/filesystem/grep.js +163 -0
  161. package/dist/tools/index.js +32 -0
  162. package/dist/tools/project/convention_get.js +91 -0
  163. package/dist/tools/project/playbook_get.js +132 -0
  164. package/dist/tools/project/project_context_get.js +101 -0
  165. package/dist/tools/sap-read.js +454 -0
  166. package/dist/tools/sap-write.js +746 -0
  167. package/dist/tools/shell/shell_exec.js +209 -0
  168. package/dist/tools/snapshot.js +107 -0
  169. package/dist/tools/subagent/_background-shared.js +133 -0
  170. package/dist/tools/subagent/agent_run.js +186 -0
  171. package/dist/tools/subagent/background_run.js +143 -0
  172. package/dist/tools/subagent/monitor_emit.js +65 -0
  173. package/dist/tools/subagent/schedule_create.js +131 -0
  174. package/dist/tools/transport.js +233 -0
  175. package/dist/tools/update-method-intercept.js +119 -0
  176. package/dist/tools/verify.js +39 -0
  177. package/dist/tools/web/_web-shared.js +251 -0
  178. package/dist/tools/web/web_fetch.js +257 -0
  179. package/dist/tools/web/web_search.js +195 -0
  180. package/dist/tools/write-mode.js +22 -0
  181. package/dist/ui/app.js +95 -0
  182. package/dist/ui/approval-emitter.js +10 -0
  183. package/dist/ui/approval-modal.js +53 -0
  184. package/dist/ui/ascii-chars.js +6 -0
  185. package/dist/ui/body.js +102 -0
  186. package/dist/ui/coaching-picker-classic.js +36 -0
  187. package/dist/ui/coaching-picker-emitter.js +27 -0
  188. package/dist/ui/command-palette.js +34 -0
  189. package/dist/ui/error-emitter.js +21 -0
  190. package/dist/ui/footer.js +103 -0
  191. package/dist/ui/header.js +17 -0
  192. package/dist/ui/ink-classifier-route.js +19 -0
  193. package/dist/ui/login-banner.js +72 -0
  194. package/dist/ui/rich-error-box.js +9 -0
  195. package/dist/ui/sap-state-store.js +65 -0
  196. package/dist/ui/session-timeline.js +31 -0
  197. package/dist/ui/sidebar.js +10 -0
  198. package/dist/ui/skill-picker.js +50 -0
  199. package/dist/ui/status-row.js +12 -0
  200. package/dist/ui/widget-control.js +4 -0
  201. package/dist/ui/widgets/bar-chart.js +15 -0
  202. package/dist/ui/widgets/coaching-picker.js +41 -0
  203. package/dist/ui/widgets/component-registry.js +12 -0
  204. package/dist/ui/widgets/dep-graph.js +9 -0
  205. package/dist/ui/widgets/diff-viewer.js +11 -0
  206. package/dist/ui/widgets/question-card.js +11 -0
  207. package/dist/ui/widgets/stack-frames.js +5 -0
  208. package/dist/upgrade-check.js +28 -0
  209. package/dist/upgrade.js +13 -0
  210. package/package.json +83 -0
@@ -0,0 +1,38 @@
1
+ import { loadConfig } from '../config/loader.js';
2
+ import { getStore } from '../auth/api-key.js';
3
+ export async function classifyPrompt(prompt, context) {
4
+ const cfg = await loadConfig();
5
+ const store = await getStore();
6
+ const key = await store.get();
7
+ if (!key)
8
+ throw new Error('no_api_key');
9
+ const r = await fetch(`${cfg.proxy_url}/v1/classify`, {
10
+ method: 'POST',
11
+ headers: {
12
+ 'Authorization': `Bearer ${key}`,
13
+ 'Content-Type': 'application/json',
14
+ },
15
+ body: JSON.stringify({ prompt, context }),
16
+ });
17
+ if (!r.ok)
18
+ throw new Error(`classifier HTTP ${r.status}`);
19
+ return (await r.json());
20
+ }
21
+ export function parseShortcut(input) {
22
+ // `/skill-name rest` → { skill: 'abap-skill-name', prompt: 'rest' }
23
+ // abap- prefix auto-prepended if missing (all skills in KV use abap-* namespace).
24
+ // `@OBJECT_NAME rest` → { object: 'OBJECT_NAME', prompt: 'rest' }
25
+ // otherwise → { prompt: input }
26
+ const slashMatch = input.match(/^\/([a-z0-9-]+)\s*(.*)$/i);
27
+ /** Auto-prepend ONLY inside the slash shortcut branch; plain prose goes through classifyPrompt. */
28
+ if (slashMatch) {
29
+ let skill = slashMatch[1];
30
+ if (!skill.startsWith('abap-'))
31
+ skill = 'abap-' + skill;
32
+ return { skill, prompt: slashMatch[2] };
33
+ }
34
+ const atMatch = input.match(/^@([A-Z0-9_]+)\s+(.*)$/i);
35
+ if (atMatch)
36
+ return { object: atMatch[1], prompt: atMatch[2] };
37
+ return { prompt: input };
38
+ }
@@ -0,0 +1,140 @@
1
+ /**
2
+ * Deterministic intent extractor — pulls structured facts out of the user's
3
+ * opening prompt BEFORE the LLM sees the turn. The facts become an XML
4
+ * `<session_context>` block prepended to the user message so the model
5
+ * reads them as structured data, not prose.
6
+ *
7
+ * Why this exists: skills kept ignoring prose rules like "if the user
8
+ * already said the package, don't ask about it." Prose compliance is
9
+ * stochastic. Running regex over the user's input is deterministic.
10
+ * Facts that are deterministically extracted cannot be silently forgotten
11
+ * by the model.
12
+ *
13
+ * This pattern is directly taken from Anthropic's context-engineering
14
+ * guidance (front-load structured XML sections, don't rely on the model
15
+ * to parse prose) and from Cursor / Aider's harness-injection pattern.
16
+ *
17
+ * Conservative by design: false negatives (miss an intent we could have
18
+ * extracted) are acceptable — the skill will just ask via ask_question.
19
+ * False positives (extract something the user didn't mean) are NOT
20
+ * acceptable — they'd override the user's actual intent. Patterns err
21
+ * on the strict side.
22
+ */
23
+ /**
24
+ * Package name pattern: after a "package"/"pkg"/"in"/"into" keyword, Z or Y
25
+ * followed by 2+ uppercase-alphanum-or-underscore chars. Requires the
26
+ * keyword to avoid false-positive matches on random Z* identifiers in the
27
+ * body (e.g. "ZCL_ORDER" is an object name, not a package).
28
+ */
29
+ const PACKAGE_RE = /(?:^|\s)(?:package|pkg|in\s+package|into\s+package|dev\s*class)\s+([ZY][A-Z0-9_]{1,29})(?:\b|\s|[.,;])/i;
30
+ /**
31
+ * Transport pattern: SAP transport IDs follow SID + K + 6 digits (SxxKnnnnnn
32
+ * e.g. S4HK903342). We match 1-char system letter + 1-2 alphanum + K + 5-7
33
+ * digits to catch variants.
34
+ */
35
+ const TRANSPORT_RE = /\b([A-Z][A-Z0-9]{1,2}K[0-9]{5,7})\b/;
36
+ /**
37
+ * Object name hints — keyword followed by a Z-prefixed or Y-prefixed name.
38
+ * Triggers: program, report, class, interface, table, structure, function
39
+ * module, CDS view.
40
+ */
41
+ const OBJECT_KEYWORD_RE = /(?:^|\s)(?:program|report|class|interface|table|structure|function(?:\s+module)?|cds\s+view|ddic)\s+(?:named|name|called)?\s*([ZY][A-Z0-9_]{2,29})\b/gi;
42
+ /**
43
+ * Environment hints — presence of these tokens in the opening prompt gives
44
+ * the skill a head-start without requiring a release probe.
45
+ */
46
+ const ENV_ONPREM_RE = /\bon[-\s]prem(?:ise)?\b|\bon[-\s]premises\b/i;
47
+ const ENV_CLOUD_RE = /\bBTP\b|\bsteampunk\b|\babap\s+cloud\b/i;
48
+ const ENV_ECC_RE = /\bECC(?:\s*6(?:\.0)?)?\b/i;
49
+ const ENV_S4_RE = /\bS\/?4(?:\s*HANA)?\b/i;
50
+ export function extractIntent(userPrompt) {
51
+ const result = {};
52
+ // Package
53
+ const pkg = PACKAGE_RE.exec(userPrompt);
54
+ if (pkg)
55
+ result.package = pkg[1].toUpperCase();
56
+ // Transport
57
+ const trp = TRANSPORT_RE.exec(userPrompt);
58
+ if (trp)
59
+ result.transport = trp[1].toUpperCase();
60
+ // Having a transport strongly implies the user wants SAP creation.
61
+ if (result.transport)
62
+ result.createIntent = true;
63
+ // Object names — only when explicitly keyword-prefixed, to avoid
64
+ // accidental matches on random Z* tokens in prose.
65
+ const names = [];
66
+ let m;
67
+ // Reset lastIndex since the regex uses /g.
68
+ OBJECT_KEYWORD_RE.lastIndex = 0;
69
+ while ((m = OBJECT_KEYWORD_RE.exec(userPrompt)) !== null) {
70
+ names.push(m[1].toUpperCase());
71
+ }
72
+ if (names.length > 0) {
73
+ result.objectNames = [...new Set(names)];
74
+ }
75
+ // Environment hints — check most specific first, so "ABAP Cloud on-prem"
76
+ // (contradictory) resolves to cloud.
77
+ if (ENV_CLOUD_RE.test(userPrompt))
78
+ result.environmentHint = 'btp-cloud';
79
+ else if (ENV_ONPREM_RE.test(userPrompt))
80
+ result.environmentHint = 's4-onprem';
81
+ else if (ENV_ECC_RE.test(userPrompt))
82
+ result.environmentHint = 'ecc';
83
+ else if (ENV_S4_RE.test(userPrompt))
84
+ result.environmentHint = 's4-onprem';
85
+ return result;
86
+ }
87
+ /**
88
+ * Format the extracted intent as an XML block suitable for prepending to
89
+ * the user message. Returns empty string when nothing was extracted — we
90
+ * don't want to clutter clean prompts with empty tags.
91
+ *
92
+ * The XML format (as opposed to JSON or prose) follows Anthropic's context-
93
+ * engineering guidance that XML-tagged sections are read reliably by the
94
+ * model as structured data.
95
+ */
96
+ export function formatFactBlock(facts, sapSystemBlock = '') {
97
+ const lines = [];
98
+ // SAP system info (release / platform / deployment) is fetched once per
99
+ // session by getSapSystemInfo and rendered by renderSapSystemBlock. It
100
+ // sits at the top of the context so skills consult it FIRST when
101
+ // deciding whether to ask "are we on S/4HANA?" — they shouldn't.
102
+ if (sapSystemBlock)
103
+ lines.push(' ' + sapSystemBlock);
104
+ if (facts.package)
105
+ lines.push(` <package>${facts.package}</package>`);
106
+ if (facts.transport)
107
+ lines.push(` <transport>${facts.transport}</transport>`);
108
+ if (facts.objectNames && facts.objectNames.length > 0) {
109
+ lines.push(` <object_names>${facts.objectNames.join(', ')}</object_names>`);
110
+ }
111
+ if (facts.environmentHint) {
112
+ lines.push(` <environment_hint>${facts.environmentHint}</environment_hint>`);
113
+ }
114
+ if (facts.createIntent)
115
+ lines.push(` <create_intent>true</create_intent>`);
116
+ if (lines.length === 0)
117
+ return '';
118
+ return ([
119
+ '<session_context>',
120
+ ' <!-- Facts already known. DO NOT ask the user about any field present here — they already told you, or the system reported it. -->',
121
+ ...lines,
122
+ '</session_context>',
123
+ '',
124
+ ].join('\n'));
125
+ }
126
+ /**
127
+ * Convenience: extract + format in one call. Returns the user message with
128
+ * the fact block prepended (empty if nothing was extracted AND no SAP
129
+ * system block was supplied).
130
+ *
131
+ * @param userPrompt the raw user prompt
132
+ * @param sapSystemBlock optional pre-rendered <sap_system .../> XML element
133
+ * (from renderSapSystemBlock); pass '' or omit when
134
+ * no SAP connection is active
135
+ */
136
+ export function enrichUserMessage(userPrompt, sapSystemBlock = '') {
137
+ const facts = extractIntent(userPrompt);
138
+ const block = formatFactBlock(facts, sapSystemBlock);
139
+ return block.length > 0 ? block + userPrompt : userPrompt;
140
+ }
@@ -0,0 +1,19 @@
1
+ // Hard-coded thresholds — not a config surface. Tunable by editing this file
2
+ // in the future if needed. Keeping them constant avoids the ceremony of a
3
+ // config interface + defaults for two numbers that rarely change.
4
+ //
5
+ // TODO: when cspeach-brain adds per-alternate confidences to ClassifyResult
6
+ // (Layer B spec), extend the silent-arm with a margin check: if
7
+ // top-2.confidence is within 0.15 of top-1, demote to 'coach'. One extra
8
+ // `if` — no API change to this file's exports.
9
+ const HIGH_THRESHOLD = 0.85;
10
+ const COACH_THRESHOLD = 0.60;
11
+ export function decideRouting(result) {
12
+ if (result.confidence >= HIGH_THRESHOLD) {
13
+ return { kind: 'silent', skill: result.skill };
14
+ }
15
+ if (result.confidence >= COACH_THRESHOLD) {
16
+ return { kind: 'coach', result };
17
+ }
18
+ return { kind: 'picker', result };
19
+ }
@@ -0,0 +1,52 @@
1
+ import keytar from 'keytar';
2
+ import { SapConnection } from '@cspeach/sap-client';
3
+ import { password as promptPassword } from '@inquirer/prompts';
4
+ const SAP_SERVICE = 'cspeach-sap';
5
+ const cache = new Map();
6
+ export async function getConnection(alias, cfg) {
7
+ const existing = cache.get(alias);
8
+ if (existing)
9
+ return existing;
10
+ let passwordValue;
11
+ if (cfg.authType === 'basic') {
12
+ try {
13
+ passwordValue = (await keytar.getPassword(SAP_SERVICE, alias)) ?? undefined;
14
+ }
15
+ catch { /* keychain unavailable */ }
16
+ if (!passwordValue) {
17
+ passwordValue = await promptPassword({
18
+ message: `SAP password for ${alias}/${cfg.username}:`,
19
+ mask: '*',
20
+ });
21
+ }
22
+ }
23
+ if (cfg.authType !== 'basic') {
24
+ throw new Error('x509 auth not yet supported in v0.1 — set authType=basic in config');
25
+ }
26
+ // SapConnection constructor is (config, username, password) — 3 positional args.
27
+ // SapConfig.authType is typed as 'basic' only (v1); SapSystemConfig carries the
28
+ // wider union so we cast to satisfy the narrower SapConfig shape.
29
+ const conn = new SapConnection({
30
+ host: cfg.host,
31
+ port: cfg.port,
32
+ client: cfg.client,
33
+ useSsl: cfg.useSsl,
34
+ sslVerify: cfg.sslVerify,
35
+ credentialRef: alias,
36
+ authType: 'basic',
37
+ }, cfg.username, passwordValue ?? '');
38
+ // Call login() once up front so the CSRF token is fetched and bound to
39
+ // the same SAP session our subsequent requests will use. Without this,
40
+ // GETs run "anonymously" (Basic auth, cookies auto-harvested, no token)
41
+ // and the first state-changing POST (lock) hits SAP with an empty CSRF
42
+ // header → 403 "CSRF token validation failed". Refreshing CSRF after
43
+ // that 403 doesn't recover, because fetchCsrfToken issues against a
44
+ // brand-new session and the token ends up bound to one session while
45
+ // our existing cookies belong to a different one.
46
+ await conn.login();
47
+ cache.set(alias, conn);
48
+ return conn;
49
+ }
50
+ export function clearConnectionCache() {
51
+ cache.clear();
52
+ }
@@ -0,0 +1,178 @@
1
+ import { input, select, password, confirm } from '@inquirer/prompts';
2
+ import chalk from 'chalk';
3
+ import keytar from 'keytar';
4
+ import { loadConfig, saveConfig } from '../config/loader.js';
5
+ const SAP_SERVICE = 'cspeach-sap';
6
+ export async function runAddSapWizard() {
7
+ console.log(chalk.cyan.bold('\nCSPeach — Add SAP system\n'));
8
+ const alias = await input({
9
+ message: 'System alias (short name like "S4H-DEV"):',
10
+ validate: (v) => /^[A-Z0-9_-]{2,32}$/.test(v) || 'Use uppercase letters, digits, _, - (2-32 chars)',
11
+ });
12
+ const host = await input({
13
+ message: 'SAP host or IP (e.g. sap-dev.company.com or 172.16.24.98):',
14
+ validate: (v) => v.length > 0 || 'Required',
15
+ });
16
+ const port = await input({
17
+ message: 'HTTPS port (default 44300):',
18
+ default: '44300',
19
+ validate: (v) => (!Number.isNaN(Number(v)) && Number(v) > 0 && Number(v) < 65536) || 'Invalid port',
20
+ });
21
+ const client = await input({
22
+ message: 'SAP client (e.g. 100):',
23
+ validate: (v) => /^\d{3}$/.test(v) || 'Must be a 3-digit client number',
24
+ });
25
+ const useSsl = await confirm({ message: 'Use HTTPS/TLS?', default: true });
26
+ const sslVerify = useSsl
27
+ ? await confirm({ message: 'Verify server certificate? (Say no for self-signed SAP certs)', default: false })
28
+ : false;
29
+ const authType = await select({
30
+ message: 'Authentication type:',
31
+ choices: [
32
+ { value: 'basic', name: 'Basic auth (username + password)' },
33
+ { value: 'x509', name: 'x509 client certificate (v1.1 — not yet supported in proxy)' },
34
+ ],
35
+ default: 'basic',
36
+ });
37
+ const username = await input({
38
+ message: 'SAP username (e.g. S.LAEEQ):',
39
+ validate: (v) => v.length > 0 || 'Required',
40
+ });
41
+ let certPath;
42
+ if (authType === 'x509') {
43
+ certPath = await input({
44
+ message: 'Path to PKCS#12 client certificate (.p12 or .pfx):',
45
+ validate: (v) => v.length > 0 || 'Required',
46
+ });
47
+ }
48
+ else {
49
+ const pwd = await password({
50
+ message: 'SAP password (stored in OS keychain):',
51
+ mask: '*',
52
+ });
53
+ try {
54
+ await keytar.setPassword(SAP_SERVICE, alias, pwd);
55
+ }
56
+ catch {
57
+ console.log(chalk.yellow('Warning: keychain unavailable; password will be prompted per session.'));
58
+ }
59
+ }
60
+ // auto_approve defaults to 'never' — user can change with:
61
+ // cspeach config set sap.<alias>.auto_approve low
62
+ // First low-risk approval will offer a progressive-disclosure prompt.
63
+ const cfg = await loadConfig();
64
+ // First-run onboarding: ask write_mode once, the first time the user adds a
65
+ // SAP system. On subsequent add-sap calls (2nd/3rd system) we skip the prompt
66
+ // — write_mode is a top-level CLI setting, not a per-SAP setting. User can
67
+ // always change later with `cspeach config set write_mode <value>`.
68
+ const isFirstRun = Object.keys(cfg.sap).length === 0;
69
+ if (isFirstRun) {
70
+ const write_mode = await select({
71
+ message: 'How should CSPeach apply SAP changes?',
72
+ choices: [
73
+ {
74
+ name: 'approval-gated (recommended) — ask before each write',
75
+ value: 'approval-gated',
76
+ description: 'CSPeach proposes changes; you approve each one; CSPeach executes.',
77
+ },
78
+ {
79
+ name: 'advisory-only — never execute writes automatically',
80
+ value: 'advisory-only',
81
+ description: 'CSPeach proposes changes; you apply them yourself in ADT/SE80. Zero write traffic to SAP. Most conservative; compliant with strict policy reviews.',
82
+ },
83
+ {
84
+ name: 'auto — apply low-risk changes automatically (advanced)',
85
+ value: 'auto',
86
+ description: 'Advanced. Requires setting auto_approve level per SAP system.',
87
+ },
88
+ ],
89
+ default: 'approval-gated',
90
+ });
91
+ cfg.write_mode = write_mode;
92
+ const llm_mode = await select({
93
+ message: 'How should CSPeach reach the AI model?',
94
+ choices: [
95
+ {
96
+ name: 'managed (recommended) — we handle the Anthropic account',
97
+ value: 'managed',
98
+ description: 'Your prompts route through api.cspeach.dev. We bill, we ratelimit, we sign skills. Simplest setup.',
99
+ },
100
+ {
101
+ name: 'byok — your own Anthropic account',
102
+ value: 'byok',
103
+ description: 'Your prompts go direct to Anthropic with your API key. You own the contract. Skills signed + fetched from our public manifest.',
104
+ },
105
+ {
106
+ name: 'ai-hub — your SAP AI Core tenant (endorsed architecture)',
107
+ value: 'ai-hub',
108
+ description: 'Your prompts route through your SAP BTP AI Core proxy. Data stays in your SAP landscape. Strongest compliance posture (short of SAP Partner Edge).',
109
+ },
110
+ {
111
+ name: 'local — your own Ollama / vLLM endpoint (air-gap)',
112
+ value: 'local',
113
+ description: 'No external AI. Skills bundled with the CLI binary. Tool-calling quality depends on the local model.',
114
+ },
115
+ ],
116
+ default: 'managed',
117
+ });
118
+ cfg.llm = { mode: llm_mode };
119
+ if (llm_mode === 'ai-hub') {
120
+ cfg.llm.ai_hub_base_url = await input({
121
+ message: 'AI Core proxy URL (e.g. https://proxy.yourcorp.com/v1):',
122
+ validate: v => v.startsWith('https://') || v.startsWith('http://') ? true : 'Must be a URL',
123
+ });
124
+ cfg.llm.ai_hub_model_alias = await input({
125
+ message: 'AI Core model alias (default: anthropic--claude-opus-4-6):',
126
+ default: 'anthropic--claude-opus-4-6',
127
+ });
128
+ const aiHubToken = await password({
129
+ message: 'AI Core API token (input masked):',
130
+ mask: '*',
131
+ });
132
+ try {
133
+ await keytar.setPassword('cspeach.ai-hub', 'token', aiHubToken);
134
+ }
135
+ catch {
136
+ console.log(chalk.yellow('Warning: keychain unavailable; AI Hub token will be prompted per session.'));
137
+ }
138
+ }
139
+ if (llm_mode === 'local') {
140
+ cfg.llm.local_base_url = await input({
141
+ message: 'Local endpoint base URL (default: http://localhost:11434/v1 for Ollama):',
142
+ default: 'http://localhost:11434/v1',
143
+ });
144
+ cfg.llm.local_model = await input({
145
+ message: 'Model name (e.g. llama3.1:70b):',
146
+ default: 'llama3.1:70b',
147
+ });
148
+ }
149
+ if (llm_mode === 'byok') {
150
+ const byokKey = await password({
151
+ message: 'Anthropic API key sk-ant-... (input masked):',
152
+ mask: '*',
153
+ });
154
+ try {
155
+ await keytar.setPassword('cspeach.anthropic', 'api-key', byokKey);
156
+ }
157
+ catch {
158
+ console.log(chalk.yellow('Warning: keychain unavailable; Anthropic key will be prompted per session.'));
159
+ }
160
+ }
161
+ }
162
+ cfg.sap[alias] = {
163
+ host,
164
+ port: Number(port),
165
+ client,
166
+ useSsl,
167
+ sslVerify,
168
+ username,
169
+ authType,
170
+ certPath,
171
+ auto_approve: 'never',
172
+ };
173
+ await saveConfig(cfg);
174
+ console.log(chalk.green(`\nSaved SAP system "${alias}" to ~/.cspeach/config.toml`));
175
+ console.log(chalk.gray(` Auto-approve policy: never (change with: cspeach config set sap.${alias}.auto_approve low)\n`));
176
+ console.log(chalk.gray('Just tell me what you need — no need to pick a skill.'));
177
+ console.log(chalk.gray('`/skill-name` is a shortcut for power users who know the skill catalog.'));
178
+ }