@cspeach/cli 1.1.24 → 1.1.26

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 (63) hide show
  1. package/dist/agent/loop.js +20 -8
  2. package/dist/agent/tool-dispatch.js +6 -0
  3. package/dist/auth/auth-file.js +173 -12
  4. package/dist/classifier/client.js +55 -30
  5. package/dist/cli.js +11 -0
  6. package/dist/commands/config-set.js +13 -0
  7. package/dist/commands/config-show.js +5 -1
  8. package/dist/commands/cost.js +24 -22
  9. package/dist/commands/deploy-card.js +135 -0
  10. package/dist/commands/login.js +31 -13
  11. package/dist/commands/plan-chain.js +31 -0
  12. package/dist/commands/plan-continue.js +1 -0
  13. package/dist/commands/plan-resume.js +49 -2
  14. package/dist/cost/cost-log.js +76 -4
  15. package/dist/cost/credits-wire.js +9 -0
  16. package/dist/cost/credits.js +24 -6
  17. package/dist/cost/router-spend.js +24 -0
  18. package/dist/cost/session-cost.js +16 -2
  19. package/dist/models/server-config.js +21 -1
  20. package/dist/one-shot.js +12 -2
  21. package/dist/projects/detail-path.js +117 -0
  22. package/dist/projects/merge-cca.js +19 -0
  23. package/dist/projects/merge-upgrade.js +19 -1
  24. package/dist/projects/plan-lane.js +47 -0
  25. package/dist/projects/plan-run.js +8 -0
  26. package/dist/projects/plan-schema.js +2 -0
  27. package/dist/projects/promote.js +7 -1
  28. package/dist/projects/save-command.js +3 -2
  29. package/dist/register/records.js +7 -2
  30. package/dist/renderer/footer-line.js +10 -4
  31. package/dist/renderer/status-footer.js +5 -4
  32. package/dist/renderer/tool-labels.js +2 -1
  33. package/dist/repl.js +41 -8
  34. package/dist/router/byok-classifier.js +159 -0
  35. package/dist/router/classifier.js +130 -6
  36. package/dist/router/router-core.js +307 -0
  37. package/dist/router/router-prompt.js +121 -0
  38. package/dist/router/routing-failure.js +65 -3
  39. package/dist/skills/bundled-skills.js +45 -45
  40. package/dist/tools/_filesystem-shared.js +363 -33
  41. package/dist/tools/cap/metadata-import.js +10 -4
  42. package/dist/tools/filesystem/file-write.js +12 -39
  43. package/dist/tools/filesystem/glob.js +25 -2
  44. package/dist/tools/filesystem/grep.js +25 -2
  45. package/dist/tools/fiori/apply.js +30 -21
  46. package/dist/tools/fiori/commit-gate.js +58 -0
  47. package/dist/tools/fiori/deploy/existing-bsp.js +67 -0
  48. package/dist/tools/fiori/deploy/run.js +608 -0
  49. package/dist/tools/fiori/deploy/scope.js +8 -0
  50. package/dist/tools/fiori/deploy/yaml-read.js +236 -0
  51. package/dist/tools/fiori/deploy-config.js +24 -6
  52. package/dist/tools/fiori/fe-extend.js +6 -1
  53. package/dist/tools/fiori/fe-scaffold.js +6 -1
  54. package/dist/tools/fiori/path-params.js +78 -0
  55. package/dist/tools/fiori/scaffold.js +2 -1
  56. package/dist/tools/fiori/tools.js +170 -19
  57. package/dist/tools/local-build.js +3 -0
  58. package/dist/tools/project/_merge-shared.js +36 -1
  59. package/dist/tools/project/cca_merge.js +4 -0
  60. package/dist/tools/project/upgrade_merge_progress.js +21 -10
  61. package/dist/tools/subagent/read_agent.js +4 -2
  62. package/dist/tools/tool-loading.js +1 -1
  63. package/package.json +1 -1
@@ -0,0 +1,121 @@
1
+ /**
2
+ * Where BYOK (and ai-hub) get the skill router's instructions (2026-10-02,
3
+ * local routing).
4
+ *
5
+ * In BYOK the CLI classifies prompts on the laptop (router/byok-classifier.ts)
6
+ * — the prompt never goes to a CSPeach host. The instructions it classifies
7
+ * with must not drift from the proxy's and must be tamper-evident, so they
8
+ * come from:
9
+ * 1. the `router` entry of the SIGNED skill manifest
10
+ * (manifest.cspeach.dev/v1.json), verified with the same ed25519 check as
11
+ * skill bodies — the proxy publishes it from the same source
12
+ * /v1/classify runs on (cspeach-proxy/src/classifier/router-core.ts) —
13
+ * used only when its revision is the same or NEWER than the bundled copy
14
+ * (review item 2: a stale published manifest never rolls back routing);
15
+ * 2. otherwise (older server without the entry, older revision, unknown
16
+ * schema version, bad signature, manifest unreachable or slow) the copy
17
+ * compiled into this CLI from that same source (router-core.ts) — never
18
+ * the proxy.
19
+ * The manifest is public and carries no prompt data.
20
+ *
21
+ * Latency (review items 1 + N2): the manifest is prefetched at startup
22
+ * (capped, alongside the model-config fetch). Only the session's FIRST prompt
23
+ * waits for the read, at most `waitMs`; every later prompt uses the verified
24
+ * entry if the read has landed, else the bundled copy at once — even while a
25
+ * slow read is still in flight. After a failed read, a refresh runs in the
26
+ * background once the cool-down has passed; no prompt waits on it. Ctrl+C
27
+ * ends a wait at once.
28
+ */
29
+ import { ManifestClient, DEFAULT_MANIFEST_URL } from '../skills/manifest-client.js';
30
+ import { ROUTER_PROMPT, asRouterPrompt } from './router-core.js';
31
+ /** After a failed manifest read, refresh in the background no sooner than this. */
32
+ const RETRY_AFTER_MS = 5 * 60 * 1000;
33
+ /** Longest a prompt waits for the session's first manifest read. */
34
+ export const PROMPT_WAIT_MS = 1500;
35
+ let cached = null;
36
+ let failedAt = null;
37
+ let inflight = null;
38
+ /** True once a prompt has waited on the manifest this session (review N2). */
39
+ let promptWaited = false;
40
+ /** Test-only reset of the session cache. */
41
+ export function __resetRouterPromptForTests() {
42
+ cached = null;
43
+ failedAt = null;
44
+ inflight = null;
45
+ promptWaited = false;
46
+ }
47
+ const bundled = () => ({ prompt: ROUTER_PROMPT, source: 'bundled' });
48
+ function cancelled() {
49
+ const e = new Error('routing cancelled');
50
+ e.name = 'AbortError';
51
+ return e;
52
+ }
53
+ /** Start (or join) the one manifest read. Resolves, never rejects. */
54
+ function startFetch(manifestUrl, now) {
55
+ if (!inflight) {
56
+ inflight = (async () => {
57
+ try {
58
+ const m = await new ManifestClient(manifestUrl).fetchAndVerifyManifest();
59
+ const entry = asRouterPrompt(m.router);
60
+ // Same-or-newer than the bundled copy, else the bundled copy (a
61
+ // definite answer for this manifest: cached, no refetch).
62
+ cached = entry && entry.revision >= ROUTER_PROMPT.revision ? { prompt: entry, source: 'manifest' } : bundled();
63
+ failedAt = null;
64
+ return cached;
65
+ }
66
+ catch {
67
+ // Unreachable, not yet published, or a signature failure (tampered).
68
+ failedAt = now();
69
+ return bundled();
70
+ }
71
+ finally {
72
+ inflight = null;
73
+ }
74
+ })();
75
+ }
76
+ return inflight;
77
+ }
78
+ /** Wait for `p` at most `ms`, or until `signal` aborts (rejects AbortError). */
79
+ function waitFor(p, ms, signal) {
80
+ return new Promise((resolve, reject) => {
81
+ const timer = setTimeout(() => { cleanup(); resolve(bundled()); }, Math.max(0, ms));
82
+ timer.unref?.();
83
+ const onAbort = () => { cleanup(); reject(cancelled()); };
84
+ const cleanup = () => { clearTimeout(timer); signal?.removeEventListener('abort', onAbort); };
85
+ signal?.addEventListener('abort', onAbort, { once: true });
86
+ p.then((v) => { cleanup(); resolve(v); }, () => { cleanup(); resolve(bundled()); });
87
+ });
88
+ }
89
+ /**
90
+ * Startup prefetch (byok / ai-hub). Waits at most `timeoutMs`; the read keeps
91
+ * going in the background if slower. Never throws.
92
+ */
93
+ export async function prefetchRouterPrompt(opts) {
94
+ if (cached || failedAt !== null)
95
+ return;
96
+ await waitFor(startFetch(opts.manifestUrl ?? DEFAULT_MANIFEST_URL, Date.now), opts.timeoutMs);
97
+ }
98
+ /**
99
+ * The router instructions for one classification. Never throws, except an
100
+ * AbortError when `signal` aborts while waiting.
101
+ */
102
+ export async function loadRouterPrompt(opts = {}) {
103
+ if (cached)
104
+ return cached;
105
+ if (opts.signal?.aborted)
106
+ throw cancelled();
107
+ const now = opts.now ?? Date.now;
108
+ const url = opts.manifestUrl ?? DEFAULT_MANIFEST_URL;
109
+ if (failedAt !== null) {
110
+ // A read already failed this session: never make a prompt wait again.
111
+ if (now() - failedAt >= RETRY_AFTER_MS)
112
+ void startFetch(url, now);
113
+ return bundled();
114
+ }
115
+ const read = startFetch(url, now);
116
+ // Only the first prompt of the session waits; later ones never do.
117
+ if (promptWaited)
118
+ return bundled();
119
+ promptWaited = true;
120
+ return waitFor(read, opts.waitMs ?? PROMPT_WAIT_MS, opts.signal);
121
+ }
@@ -1,17 +1,69 @@
1
+ /**
2
+ * Skill-routing failure: what happened, in true words (live fix 2026-09-26).
3
+ *
4
+ * Fix 9: the REPL used to print "Skill routing is offline · CSPeach service
5
+ * unreachable: This operation was aborted" when the classify call merely
6
+ * timed out (the proxy was healthy). The wording now comes from the
7
+ * classifier's machine-readable kind — never "offline"/"unreachable" for a
8
+ * timeout. Used by the Ink REPL, the classic REPL and one-shot.
9
+ */
10
+ import { localRulesResult } from './classifier.js';
1
11
  /** The failure behind an offline (fallback) classify result, or null when routing worked. */
2
12
  export function routingFailureFromResult(c) {
3
13
  if (!c.offline)
4
14
  return null;
5
- return c.transport_kind === 'timeout' ? { kind: 'timeout' } : { kind: 'network' };
15
+ if (c.transport_kind === 'timeout')
16
+ return { kind: 'timeout' };
17
+ if (c.transport_kind === 'http' && typeof c.http_status === 'number')
18
+ return { kind: 'http', status: c.http_status };
19
+ if (c.transport_kind === 'failed')
20
+ return { kind: 'failed' };
21
+ return { kind: 'network' };
6
22
  }
7
- /** The failure behind a thrown classify error (HTTP), or null for any other error. */
23
+ /** The failure behind a thrown classify error (HTTP / unreadable body), or null for any other error. */
8
24
  export function routingFailureFromError(err) {
9
25
  // Duck-typed (not instanceof) so a re-loaded module instance still matches.
10
26
  const e = err;
11
- if (e && e.transport_kind === 'http' && typeof e.status === 'number')
27
+ if (e && e.transport_kind === 'http' && typeof e.status === 'number') {
12
28
  return { kind: 'http', status: e.status };
29
+ }
30
+ if (e && e.transport_kind === 'failed')
31
+ return { kind: 'failed' };
13
32
  return null;
14
33
  }
34
+ /**
35
+ * Live fix 2026-10-02 — a classify failure (the proxy answered HTTP 500 for a
36
+ * prompt its router answered in prose; an unreadable body; anything else
37
+ * classifyPrompt throws) no longer ends the turn or escapes one-shot as
38
+ * "unhandled error". Every routing path (Ink REPL, classic REPL, one-shot)
39
+ * takes this result instead: the bundled local-rules pick at low confidence,
40
+ * marked offline with the failure kind, so the existing offline handling
41
+ * runs — the low-confidence path (picker / one-shot top-1), one notice, and
42
+ * fix 10's "stay in the active skill" mid-skill.
43
+ *
44
+ * Returns null (caller keeps its own handling) for a missing API key, a user
45
+ * cancel (AbortError), and the slash-guard CLI bug, which must stay loud.
46
+ */
47
+ export function routingFallbackFromError(err, prompt) {
48
+ const e = err;
49
+ if (e?.message === 'no_api_key')
50
+ return null;
51
+ if (e?.name === 'AbortError')
52
+ return null;
53
+ if (typeof e?.message === 'string' && e.message.startsWith('slash command reached the classifier'))
54
+ return null;
55
+ const f = routingFailureFromError(err) ?? { kind: 'failed' };
56
+ return {
57
+ ...localRulesResult(prompt),
58
+ offline: true,
59
+ transport_kind: f.kind === 'http' ? 'http' : 'failed',
60
+ ...(f.kind === 'http' ? { http_status: f.status } : {}),
61
+ };
62
+ }
63
+ /** "Couldn't pick a skill automatically (HTTP 500)" — the 2026-10-02 fallback headline. */
64
+ function couldNotPick(f) {
65
+ return `Couldn't pick a skill automatically${f.kind === 'http' ? ` (HTTP ${f.status})` : ''}`;
66
+ }
15
67
  /** Headline for the notice: "Skill routing timed out" etc. */
16
68
  export function routingFailureText(f) {
17
69
  switch (f.kind) {
@@ -20,15 +72,24 @@ export function routingFailureText(f) {
20
72
  case 'timeout': return 'Skill routing timed out';
21
73
  case 'network': return "Skill routing couldn't reach the CSPeach service";
22
74
  case 'http': return `Skill routing failed (HTTP ${f.status})`;
75
+ case 'failed': return 'Skill routing failed';
23
76
  }
24
77
  }
25
78
  /** The attention notice: headline, plus the real cause for network failures. */
26
79
  export function routingFailureNotice(f, transportError) {
80
+ // 2026-10-02: an HTTP / other classify failure now continues on local
81
+ // rules (routingFallbackFromError) — say so, and how to choose.
82
+ if (f.kind === 'http' || f.kind === 'failed') {
83
+ return { text: couldNotPick(f), fact: 'routed by local rules — start with /skill to choose' };
84
+ }
27
85
  const text = routingFailureText(f);
28
86
  return f.kind === 'network' && transportError ? { text, fact: transportError } : { text };
29
87
  }
30
88
  /** One-shot's dim stderr line (the turn continues on the bundled local rules). */
31
89
  export function routingFailureOneShotLine(f, transportError) {
90
+ if (f.kind === 'http' || f.kind === 'failed') {
91
+ return `[${couldNotPick(f)} — routed by local rules. Start with /skill to choose.]`;
92
+ }
32
93
  const n = routingFailureNotice(f, transportError);
33
94
  return `[${n.text}${n.fact ? `: ${n.fact}` : ''} — routed by local rules]`;
34
95
  }
@@ -38,6 +99,7 @@ function routingFailureShort(f) {
38
99
  case 'timeout': return 'Routing timed out';
39
100
  case 'network': return "Routing couldn't reach the CSPeach service";
40
101
  case 'http': return `Routing failed (HTTP ${f.status})`;
102
+ case 'failed': return 'Routing failed';
41
103
  }
42
104
  }
43
105
  /**