@cspeach/cli 1.1.7 → 1.1.9

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.
@@ -1,5 +1,5 @@
1
1
  import Anthropic from '@anthropic-ai/sdk';
2
- import keytar from 'keytar';
2
+ import keychain from '../../auth/keychain.js';
3
3
  import { createManifestSkillSource } from '../../skills/source-manifest.js';
4
4
  import { DEFAULT_MANIFEST_URL } from '../../skills/manifest-client.js';
5
5
  const KEYCHAIN_SERVICE = 'cspeach.ai-hub';
@@ -26,7 +26,7 @@ export function createAiHubProvider(cfg) {
26
26
  return sdk.messages.stream({ ...params, model: modelAlias }, options.signal ? { signal: options.signal } : undefined);
27
27
  },
28
28
  async healthCheck() {
29
- const token = await keytar.getPassword(KEYCHAIN_SERVICE, KEYCHAIN_ACCOUNT);
29
+ const token = await keychain.getPassword(KEYCHAIN_SERVICE, KEYCHAIN_ACCOUNT);
30
30
  if (!token)
31
31
  return { ok: false, message: 'AI Hub token not in keychain', hint: 'Run: cspeach config set llm.ai_hub_token <token>' };
32
32
  try {
@@ -51,7 +51,7 @@ export function createAiHubProvider(cfg) {
51
51
  };
52
52
  }
53
53
  async function getToken() {
54
- const t = await keytar.getPassword(KEYCHAIN_SERVICE, KEYCHAIN_ACCOUNT);
54
+ const t = await keychain.getPassword(KEYCHAIN_SERVICE, KEYCHAIN_ACCOUNT);
55
55
  if (!t)
56
56
  throw new Error('AI Hub token not configured');
57
57
  return t;
@@ -1,5 +1,5 @@
1
1
  import Anthropic from '@anthropic-ai/sdk';
2
- import keytar from 'keytar';
2
+ import keychain from '../../auth/keychain.js';
3
3
  import { createManifestSkillSource } from '../../skills/source-manifest.js';
4
4
  import { DEFAULT_MANIFEST_URL } from '../../skills/manifest-client.js';
5
5
  const KEYCHAIN_SERVICE = 'cspeach.anthropic';
@@ -21,7 +21,7 @@ export function createByokProvider(_cfg) {
21
21
  return sdk.messages.stream(params, options.signal ? { signal: options.signal } : undefined);
22
22
  },
23
23
  async healthCheck() {
24
- const key = await keytar.getPassword(KEYCHAIN_SERVICE, KEYCHAIN_ACCOUNT);
24
+ const key = await keychain.getPassword(KEYCHAIN_SERVICE, KEYCHAIN_ACCOUNT);
25
25
  if (!key)
26
26
  return { ok: false, message: 'Anthropic API key not in keychain', hint: 'Run: cspeach config set llm.byok_key <your-anthropic-key>' };
27
27
  try {
@@ -46,7 +46,7 @@ export function createByokProvider(_cfg) {
46
46
  };
47
47
  }
48
48
  async function getKey() {
49
- const key = await keytar.getPassword(KEYCHAIN_SERVICE, KEYCHAIN_ACCOUNT);
49
+ const key = await keychain.getPassword(KEYCHAIN_SERVICE, KEYCHAIN_ACCOUNT);
50
50
  if (!key)
51
51
  throw new Error('BYOK Anthropic API key not configured — run: cspeach config set llm.byok_key <key>');
52
52
  return key;
@@ -1,16 +1,16 @@
1
- import keytar from 'keytar';
1
+ import keychain from './keychain.js';
2
2
  const SERVICE = 'cspeach';
3
3
  const ACCOUNT = 'api-key';
4
- class KeytarStore {
5
- async get() { return keytar.getPassword(SERVICE, ACCOUNT); }
6
- async set(key) { await keytar.setPassword(SERVICE, ACCOUNT, key); }
7
- async delete() { await keytar.deletePassword(SERVICE, ACCOUNT); }
4
+ class KeychainStore {
5
+ async get() { return keychain.getPassword(SERVICE, ACCOUNT); }
6
+ async set(key) { await keychain.setPassword(SERVICE, ACCOUNT, key); }
7
+ async delete() { await keychain.deletePassword(SERVICE, ACCOUNT); }
8
8
  async isKeychainAvailable() {
9
9
  try {
10
10
  const probe = `probe-${Date.now()}`;
11
- await keytar.setPassword(SERVICE, 'probe', probe);
12
- const ok = (await keytar.getPassword(SERVICE, 'probe')) === probe;
13
- await keytar.deletePassword(SERVICE, 'probe');
11
+ await keychain.setPassword(SERVICE, 'probe', probe);
12
+ const ok = (await keychain.getPassword(SERVICE, 'probe')) === probe;
13
+ await keychain.deletePassword(SERVICE, 'probe');
14
14
  return ok;
15
15
  }
16
16
  catch {
@@ -29,7 +29,7 @@ let store;
29
29
  export async function getStore() {
30
30
  if (store)
31
31
  return store;
32
- const ktr = new KeytarStore();
32
+ const ktr = new KeychainStore();
33
33
  if (await ktr.isKeychainAvailable()) {
34
34
  store = ktr;
35
35
  }
Binary file
package/dist/cli.js CHANGED
@@ -162,32 +162,32 @@ export async function main(argv) {
162
162
  return runRepl({ sapAlias, noSap });
163
163
  }
164
164
  function printHelp() {
165
- console.log(`
166
- CSPeach — AI-assisted ABAP development CLI
167
-
168
- Usage:
169
- cspeach Start interactive REPL
170
- cspeach "<prompt>" One-shot — runs one turn and exits
171
- cspeach --one-shot "<prompt>" Same as above, explicit flag
172
- cspeach --sap-alias <name> Use a specific SAP alias (overrides default)
173
- cspeach --no-sap Start without a SAP system (paste-mode skills only)
174
- cspeach --resume [id] Resume a previous session (list if no id)
175
- cspeach config add-sap Add a SAP system interactively
176
- cspeach config standalone Set the standalone SAP profile (no system)
177
- cspeach standards init Capture your team's ABAP development standards
178
- cspeach configure Alias for \`cspeach config add-sap\`
179
- cspeach config set <key> <value> Update a config key
180
- cspeach config unset <key> Remove a config key (revert to default)
181
- cspeach config show [key] Show config (full or single key)
182
- cspeach doctor Diagnose install + connectivity
183
- cspeach login Sign in via device-code flow
184
- cspeach logout Sign out (keep keytar key, if any)
185
- cspeach whoami Print current account info
186
- cspeach analyze project-context Compare flag-on vs flag-off session metrics
187
- cspeach --upgrade Upgrade to latest via npm
188
- cspeach --version Print version
189
- cspeach --help Show this help
190
-
191
- Docs: https://cspeach.dev
165
+ console.log(`
166
+ CSPeach — AI-assisted ABAP development CLI
167
+
168
+ Usage:
169
+ cspeach Start interactive REPL
170
+ cspeach "<prompt>" One-shot — runs one turn and exits
171
+ cspeach --one-shot "<prompt>" Same as above, explicit flag
172
+ cspeach --sap-alias <name> Use a specific SAP alias (overrides default)
173
+ cspeach --no-sap Start without a SAP system (paste-mode skills only)
174
+ cspeach --resume [id] Resume a previous session (list if no id)
175
+ cspeach config add-sap Add a SAP system interactively
176
+ cspeach config standalone Set the standalone SAP profile (no system)
177
+ cspeach standards init Capture your team's ABAP development standards
178
+ cspeach configure Alias for \`cspeach config add-sap\`
179
+ cspeach config set <key> <value> Update a config key
180
+ cspeach config unset <key> Remove a config key (revert to default)
181
+ cspeach config show [key] Show config (full or single key)
182
+ cspeach doctor Diagnose install + connectivity
183
+ cspeach login Sign in via device-code flow
184
+ cspeach logout Sign out (keep keychain key, if any)
185
+ cspeach whoami Print current account info
186
+ cspeach analyze project-context Compare flag-on vs flag-off session metrics
187
+ cspeach --upgrade Upgrade to latest via npm
188
+ cspeach --version Print version
189
+ cspeach --help Show this help
190
+
191
+ Docs: https://cspeach.dev
192
192
  `);
193
193
  }
@@ -400,12 +400,12 @@ export async function runConfigSet(args) {
400
400
  else {
401
401
  console.log(chalk.yellow(`WARN: secret passed as shell argument — will land in shell history. Prefer: cat key.txt | cspeach config set ${key} -`));
402
402
  }
403
- const { default: keytar } = await import('keytar');
403
+ const { default: keychain } = await import('../auth/keychain.js');
404
404
  if (key === 'llm.byok_key') {
405
- await keytar.setPassword('cspeach.anthropic', 'api-key', secret);
405
+ await keychain.setPassword('cspeach.anthropic', 'api-key', secret);
406
406
  }
407
407
  else {
408
- await keytar.setPassword('cspeach.ai-hub', 'token', secret);
408
+ await keychain.setPassword('cspeach.ai-hub', 'token', secret);
409
409
  }
410
410
  console.log(chalk.green(`✓ Secret stored in OS keychain (service: ${key === 'llm.byok_key' ? 'cspeach.anthropic' : 'cspeach.ai-hub'})`));
411
411
  return;
@@ -13,7 +13,7 @@
13
13
  * Dependencies (open, print, prompt) are injected so the command is
14
14
  * unit-testable without stubbing platform globals.
15
15
  */
16
- import keytar from 'keytar';
16
+ import keychain from '../auth/keychain.js';
17
17
  import { writeAuthFile } from '../auth/auth-file.js';
18
18
  import { fetchOrgAnthropicKey } from '../auth/org-anthropic-key.js';
19
19
  const ANTHROPIC_KEYCHAIN_SERVICE = 'cspeach.anthropic';
@@ -101,7 +101,7 @@ export async function runLoginCommand(deps) {
101
101
  fetchImpl: deps.fetchImpl,
102
102
  });
103
103
  const setKey = deps.setKeychainKey ??
104
- ((svc, acct, k) => keytar.setPassword(svc, acct, k));
104
+ ((svc, acct, k) => keychain.setPassword(svc, acct, k));
105
105
  if (orgKey !== null) {
106
106
  await setKey(ANTHROPIC_KEYCHAIN_SERVICE, ANTHROPIC_KEYCHAIN_ACCOUNT, orgKey);
107
107
  deps.print('Using your organization\'s Anthropic key.');
@@ -8,7 +8,7 @@ export async function runLogoutCommand(deps) {
8
8
  await deleteAuthFile();
9
9
  if (before) {
10
10
  deps.print('✓ Signed out — removed ~/.cspeach/auth.json');
11
- deps.print(' (the REPL will use the keytar-stored key if you have one)');
11
+ deps.print(' (the REPL will use the keychain-stored key if you have one)');
12
12
  }
13
13
  else {
14
14
  deps.print('Already signed out — no auth.json present.');
@@ -10,7 +10,7 @@ export async function runWhoamiCommand(deps) {
10
10
  const auth = await readAuthFile();
11
11
  if (!auth) {
12
12
  deps.print('Not signed in via cspeach login (no ~/.cspeach/auth.json).');
13
- deps.print('Your REPL may still authenticate via the keytar-stored key — run `cspeach` to check.');
13
+ deps.print('Your REPL may still authenticate via the keychain-stored key — run `cspeach` to check.');
14
14
  return 0;
15
15
  }
16
16
  const me = await fetchMe({ proxyUrl: deps.proxyUrl, apiKey: auth.apiKey });
@@ -1,10 +1,10 @@
1
- import keytar from 'keytar';
1
+ import keychain from '../../auth/keychain.js';
2
2
  export async function checkKeychain() {
3
3
  try {
4
4
  const probe = `probe-${Date.now()}`;
5
- await keytar.setPassword('cspeach', 'probe', probe);
6
- const ok = (await keytar.getPassword('cspeach', 'probe')) === probe;
7
- await keytar.deletePassword('cspeach', 'probe');
5
+ await keychain.setPassword('cspeach', 'probe', probe);
6
+ const ok = (await keychain.getPassword('cspeach', 'probe')) === probe;
7
+ await keychain.deletePassword('cspeach', 'probe');
8
8
  if (ok)
9
9
  return { name: 'keychain', ok: true, message: 'OS keychain working' };
10
10
  return { name: 'keychain', ok: false, message: 'probe write/read mismatch', hint: 'Per-session fallback mode will be used' };
@@ -1,4 +1,4 @@
1
- import keytar from 'keytar';
1
+ import keychain from '../../auth/keychain.js';
2
2
  import { readFile } from 'node:fs/promises';
3
3
  import { loadConfig, resolveSapTls } from '../../config/loader.js';
4
4
  import { probeGet } from './_http-probe.js';
@@ -26,7 +26,7 @@ export async function checkZcspeach() {
26
26
  const url = `${protocol}://${sys.host}:${sys.port}/sap/bc/zcspeach/discovery`;
27
27
  let pwd = null;
28
28
  try {
29
- pwd = await keytar.getPassword(SAP_SERVICE, alias);
29
+ pwd = await keychain.getPassword(SAP_SERVICE, alias);
30
30
  }
31
31
  catch {
32
32
  // keychain unavailable — fall through, will be skipped below
package/dist/repl.js CHANGED
@@ -384,9 +384,9 @@ export async function runRepl(opts) {
384
384
  // Having aliases configured does NOT override the request.
385
385
  // A session that WAS standalone stays standalone on `--resume`: its
386
386
  // sap_system is the reserved 'none' alias, which has no entry in cfg.sap.
387
- const standalone = resolveStandalone(cfg, opts?.noSap === true) ||
387
+ let standalone = resolveStandalone(cfg, opts?.noSap === true) ||
388
388
  opts?.resumedSession?.sap_system === STANDALONE_ALIAS;
389
- if (standalone) {
389
+ const enterStandalone = async () => {
390
390
  console.log(chalk.dim('\nStandalone mode: no SAP system connected — skills work from pasted code. ' +
391
391
  'Connect later with: cspeach config add-sap'));
392
392
  // Ask the profile questions ONCE — they become the <sap_system> element
@@ -396,11 +396,30 @@ export async function runRepl(opts) {
396
396
  await runStandaloneProfileWizard({ cwd: process.cwd() });
397
397
  cfg = await loadConfig();
398
398
  }
399
+ };
400
+ if (standalone) {
401
+ await enterStandalone();
399
402
  }
400
403
  else if (Object.keys(cfg.sap).length === 0) {
401
- console.log(chalk.yellow('\nNo SAP systems configured. Starting onboarding wizard.'));
402
- await runAddSapWizard();
403
- cfg = await loadConfig();
404
+ // First run with nothing configured (2026-09-17): let the user choose.
405
+ // Someone without SAP access used to be pushed into the SAP wizard with
406
+ // no way out except knowing --no-sap. Non-TTY runs skip the question and
407
+ // keep the previous behaviour exactly.
408
+ const { chooseFirstRunMode } = await import('./sap/first-run-choice.js');
409
+ const mode = await chooseFirstRunMode();
410
+ if (mode === 'standalone') {
411
+ // Persist so the next plain `cspeach` is standalone too — same effect
412
+ // as `cspeach config set no_sap on`, which the user can undo any time.
413
+ cfg.no_sap = true;
414
+ await saveConfig(cfg);
415
+ standalone = true;
416
+ await enterStandalone();
417
+ }
418
+ else {
419
+ console.log(chalk.yellow('\nNo SAP systems configured. Starting onboarding wizard.'));
420
+ await runAddSapWizard();
421
+ cfg = await loadConfig();
422
+ }
404
423
  }
405
424
  // model-governance step 2d — fire-and-forget fetch of the served model-config
406
425
  // (managed mode only). NEVER blocks startup: a turn that starts before the
@@ -1,4 +1,4 @@
1
- import keytar from 'keytar';
1
+ import keychain from '../auth/keychain.js';
2
2
  import { SapConnection } from '@cspeach/sap-client';
3
3
  import { resolveSapTls } from '../config/loader.js';
4
4
  import { password as promptPassword } from '@inquirer/prompts';
@@ -12,7 +12,7 @@ export async function getConnection(alias, cfg) {
12
12
  let passwordValue;
13
13
  if (cfg.authType === 'basic') {
14
14
  try {
15
- passwordValue = (await keytar.getPassword(SAP_SERVICE, alias)) ?? undefined;
15
+ passwordValue = (await keychain.getPassword(SAP_SERVICE, alias)) ?? undefined;
16
16
  }
17
17
  catch { /* keychain unavailable */ }
18
18
  if (!passwordValue) {
@@ -0,0 +1,34 @@
1
+ /**
2
+ * First-run choice (2026-09-17). Before this, a fresh install that started
3
+ * with plain `cspeach` (no --no-sap flag, no persisted no_sap) was pushed
4
+ * straight into the "Add SAP system" wizard — someone with no SAP access had
5
+ * no way forward except to know the flag. Seen live on Laeeq's dry run.
6
+ *
7
+ * Now, when no SAP alias is configured, the user picks how to start. The
8
+ * standalone answer is listed first: it needs nothing, and it is the path a
9
+ * trial user is on. Non-interactive runs (piped stdin, CI) never see the
10
+ * prompt and keep the exact pre-existing behaviour (the SAP wizard branch).
11
+ */
12
+ import { select } from '@inquirer/prompts';
13
+ export const FIRST_RUN_CHOICES = [
14
+ {
15
+ name: 'Work standalone — no SAP connection',
16
+ value: 'standalone',
17
+ description: 'Paste code or a requirement in; get code, reviews, designs and step-by-step implementation guides out. Connect a system later with: cspeach config add-sap',
18
+ },
19
+ {
20
+ name: 'Connect to an SAP system now',
21
+ value: 'sap',
22
+ description: 'Host, client, user and password for an SAP system CSPeach may read and write through ADT.',
23
+ },
24
+ ];
25
+ export async function chooseFirstRunMode(deps = {}) {
26
+ const isTTY = deps.isTTY ?? process.stdin.isTTY === true;
27
+ if (!isTTY)
28
+ return 'sap';
29
+ const ask = deps.select ?? select;
30
+ return ask({
31
+ message: 'No SAP system is set up yet. How do you want to start?',
32
+ choices: FIRST_RUN_CHOICES,
33
+ });
34
+ }
@@ -1,6 +1,6 @@
1
1
  import { input, select, password, confirm } from '@inquirer/prompts';
2
2
  import chalk from 'chalk';
3
- import keytar from 'keytar';
3
+ import keychain from '../auth/keychain.js';
4
4
  import { loadConfig, saveConfig } from '../config/loader.js';
5
5
  const SAP_SERVICE = 'cspeach-sap';
6
6
  export async function runAddSapWizard() {
@@ -87,7 +87,7 @@ export async function runAddSapWizard() {
87
87
  mask: '*',
88
88
  });
89
89
  try {
90
- await keytar.setPassword(SAP_SERVICE, alias, pwd);
90
+ await keychain.setPassword(SAP_SERVICE, alias, pwd);
91
91
  }
92
92
  catch {
93
93
  console.log(chalk.yellow('Warning: keychain unavailable; password will be prompted per session.'));
@@ -166,7 +166,7 @@ export async function runAddSapWizard() {
166
166
  mask: '*',
167
167
  });
168
168
  try {
169
- await keytar.setPassword('cspeach.ai-hub', 'token', aiHubToken);
169
+ await keychain.setPassword('cspeach.ai-hub', 'token', aiHubToken);
170
170
  }
171
171
  catch {
172
172
  console.log(chalk.yellow('Warning: keychain unavailable; AI Hub token will be prompted per session.'));
@@ -188,7 +188,7 @@ export async function runAddSapWizard() {
188
188
  mask: '*',
189
189
  });
190
190
  try {
191
- await keytar.setPassword('cspeach.anthropic', 'api-key', byokKey);
191
+ await keychain.setPassword('cspeach.anthropic', 'api-key', byokKey);
192
192
  }
193
193
  catch {
194
194
  console.log(chalk.yellow('Warning: keychain unavailable; Anthropic key will be prompted per session.'));
@@ -1,6 +1,6 @@
1
1
  // GENERATED FILE — bundled skills for local mode.
2
2
  // Source: https://manifest.cspeach.dev/v1.json
3
- // Generated: 2026-09-17T15:05:10.303Z
3
+ // Generated: 2026-09-17T16:36:01.136Z
4
4
  // Manifest signature verified at build time.
5
5
  export const BUNDLED_SKILLS = {
6
6
  "abap-atc-fix": {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cspeach/cli",
3
- "version": "1.1.7",
3
+ "version": "1.1.9",
4
4
  "description": "CSPeach — AI-assisted ABAP development CLI. Direct SAP access, 34 skills, safety gates.",
5
5
  "keywords": [
6
6
  "abap",
@@ -71,7 +71,7 @@
71
71
  "ink": "^5.0.0",
72
72
  "ink-text-input": "^6.0.0",
73
73
  "jose": "^5.9.0",
74
- "keytar": "^7.9.0",
74
+ "@napi-rs/keyring": "2.1.0",
75
75
  "mammoth": "^1.8.0",
76
76
  "marked": "^11.2.0",
77
77
  "marked-terminal": "^6.2.0",
@@ -102,7 +102,6 @@
102
102
  },
103
103
  "pnpm": {
104
104
  "onlyBuiltDependencies": [
105
- "keytar",
106
105
  "esbuild"
107
106
  ]
108
107
  }