@moda-ai/cli 1.22.1 → 1.22.2

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 (115) hide show
  1. package/dist/cli-g8qs2tj9.js +9777 -0
  2. package/dist/cli-pwbvtes8.js +2312 -0
  3. package/dist/cli-qszcpda3.js +284 -0
  4. package/dist/cli.js +4775 -1159
  5. package/dist/index-6h0nc1r5.js +2765 -0
  6. package/dist/provision-j5gq8cvx.js +72 -0
  7. package/package.json +3 -2
  8. package/skills/integration/index.json +2 -2
  9. package/dist/agent-schemas.d.ts +0 -3
  10. package/dist/agent-schemas.js +0 -358
  11. package/dist/agent.d.ts +0 -5
  12. package/dist/agent.js +0 -477
  13. package/dist/analyst-tui.d.ts +0 -81
  14. package/dist/analyst-tui.js +0 -314
  15. package/dist/api-client.d.ts +0 -46
  16. package/dist/api-client.js +0 -120
  17. package/dist/ask-stream.d.ts +0 -146
  18. package/dist/ask-stream.js +0 -473
  19. package/dist/auth-storage.d.ts +0 -35
  20. package/dist/auth-storage.js +0 -159
  21. package/dist/auth.d.ts +0 -50
  22. package/dist/auth.js +0 -694
  23. package/dist/cli-session.d.ts +0 -26
  24. package/dist/cli-session.js +0 -169
  25. package/dist/cli.d.ts +0 -44
  26. package/dist/command-context.d.ts +0 -62
  27. package/dist/command-context.js +0 -57
  28. package/dist/command-registry.d.ts +0 -22
  29. package/dist/command-registry.js +0 -15
  30. package/dist/config.d.ts +0 -60
  31. package/dist/config.js +0 -110
  32. package/dist/doctor.d.ts +0 -79
  33. package/dist/doctor.js +0 -1182
  34. package/dist/errors.d.ts +0 -50
  35. package/dist/errors.js +0 -64
  36. package/dist/exit-funnel.d.ts +0 -14
  37. package/dist/exit-funnel.js +0 -53
  38. package/dist/harness-artifact-bodies.d.ts +0 -28
  39. package/dist/harness-artifact-bodies.js +0 -276
  40. package/dist/harness-candidates.d.ts +0 -77
  41. package/dist/harness-candidates.js +0 -482
  42. package/dist/harness-remote.d.ts +0 -135
  43. package/dist/harness-remote.js +0 -540
  44. package/dist/harness-report.d.ts +0 -145
  45. package/dist/harness-report.js +0 -1579
  46. package/dist/harness-sync-limits.d.ts +0 -8
  47. package/dist/harness-sync-limits.js +0 -21
  48. package/dist/harness.d.ts +0 -463
  49. package/dist/harness.js +0 -4009
  50. package/dist/init/index.d.ts +0 -78
  51. package/dist/init/index.js +0 -1442
  52. package/dist/init/mcp.d.ts +0 -2
  53. package/dist/init/mcp.js +0 -27
  54. package/dist/init/progress-board.d.ts +0 -80
  55. package/dist/init/progress-board.js +0 -225
  56. package/dist/init/rules.d.ts +0 -6
  57. package/dist/init/rules.js +0 -171
  58. package/dist/init/sdk-catalog.d.ts +0 -10
  59. package/dist/init/sdk-catalog.js +0 -80
  60. package/dist/init/sdk-guide.d.ts +0 -19
  61. package/dist/init/sdk-guide.js +0 -185
  62. package/dist/init/sdk-integration.d.ts +0 -53
  63. package/dist/init/sdk-integration.js +0 -835
  64. package/dist/init/sdk-protocol.d.ts +0 -135
  65. package/dist/init/sdk-protocol.js +0 -152
  66. package/dist/init/sdk-verification.d.ts +0 -27
  67. package/dist/init/sdk-verification.js +0 -123
  68. package/dist/init/tenant.d.ts +0 -67
  69. package/dist/init/tenant.js +0 -421
  70. package/dist/intelligence-schema.d.ts +0 -349
  71. package/dist/intelligence-schema.js +0 -174
  72. package/dist/keys.d.ts +0 -47
  73. package/dist/keys.js +0 -136
  74. package/dist/manifest.d.ts +0 -5
  75. package/dist/manifest.js +0 -114
  76. package/dist/markdown-terminal.d.ts +0 -49
  77. package/dist/markdown-terminal.js +0 -212
  78. package/dist/output.d.ts +0 -191
  79. package/dist/output.js +0 -624
  80. package/dist/production-intelligence.d.ts +0 -175
  81. package/dist/production-intelligence.js +0 -1430
  82. package/dist/profile-commands.d.ts +0 -10
  83. package/dist/profile-commands.js +0 -342
  84. package/dist/profiles.d.ts +0 -112
  85. package/dist/profiles.js +0 -573
  86. package/dist/prompts-ab.d.ts +0 -71
  87. package/dist/prompts-ab.js +0 -313
  88. package/dist/prompts-propose.d.ts +0 -15
  89. package/dist/prompts-propose.js +0 -212
  90. package/dist/prompts.d.ts +0 -58
  91. package/dist/prompts.js +0 -418
  92. package/dist/provision.d.ts +0 -28
  93. package/dist/provision.js +0 -104
  94. package/dist/redact.d.ts +0 -2
  95. package/dist/redact.js +0 -90
  96. package/dist/schemas.d.ts +0 -225
  97. package/dist/schemas.js +0 -126
  98. package/dist/skills-bundled.d.ts +0 -35
  99. package/dist/skills-bundled.js +0 -157
  100. package/dist/skills-manifest.d.ts +0 -33
  101. package/dist/skills-manifest.js +0 -244
  102. package/dist/skills.d.ts +0 -57
  103. package/dist/skills.js +0 -783
  104. package/dist/sync.d.ts +0 -5
  105. package/dist/sync.js +0 -36
  106. package/dist/telemetry.d.ts +0 -56
  107. package/dist/telemetry.js +0 -167
  108. package/dist/transforms.d.ts +0 -25
  109. package/dist/transforms.js +0 -102
  110. package/dist/tui.d.ts +0 -48
  111. package/dist/tui.js +0 -393
  112. package/dist/update-check.d.ts +0 -31
  113. package/dist/update-check.js +0 -141
  114. package/dist/workspace.d.ts +0 -12
  115. package/dist/workspace.js +0 -1294
@@ -1,473 +0,0 @@
1
- // src/ask-stream.ts
2
- //
3
- // SSE consumer for `POST /api/cli/ask/stream` (the backend's streaming ask
4
- // endpoint). Mirrors the `AgentStreamEvent` wire protocol defined in
5
- // apps/moda-backend/src/data-agent/agent/stream-events.ts: SSE frames of
6
- // `data: <json>` carrying phase / thinking / tool_start / tool_end / sources /
7
- // text / done / error events.
8
- //
9
- // Design contract: ANY transport, framing, or protocol problem throws
10
- // `AskStreamError` so the caller can transparently fall back to the buffered
11
- // `POST /api/cli/ask` path — a user must never get a worse outcome than the
12
- // buffered request. A terminal `error` event from the agent is NOT a transport
13
- // failure: it is returned as an outcome (`errorMessage`) and rendered the same
14
- // way buffered agent errors render.
15
- import { resolveApiKey, resolveModaBaseUrl } from './config.js';
16
- import { stripAnsi } from './markdown-terminal.js';
17
- /** Signals "fall back to the buffered ask endpoint" — never surfaced raw. */
18
- export class AskStreamError extends Error {
19
- /** Wall-clock ms the stream ran before failing (annotated by `streamAsk`). */
20
- elapsedMs;
21
- /** tool_start/tool_end events seen before failing (annotated by `streamAsk`). */
22
- toolEvents;
23
- constructor(message) {
24
- super(message);
25
- this.name = 'AskStreamError';
26
- }
27
- }
28
- /**
29
- * Thresholds for "substantial progress": a stream that ran at least this long,
30
- * or produced more tool events than this, did real work before dropping —
31
- * the buffered fallback then re-runs the WHOLE question, roughly doubling
32
- * total latency, so the user deserves a notice instead of silence. The elapsed
33
- * check is inclusive (`>=`) so a drop landing exactly on the boundary — where
34
- * the doubled wait is felt most acutely — still gets the notice.
35
- */
36
- export const SUBSTANTIAL_STREAM_PROGRESS_MS = 60_000;
37
- export const SUBSTANTIAL_STREAM_TOOL_EVENTS = 5;
38
- /**
39
- * One-line stderr notice for a stream that died after substantial progress.
40
- * Returns null for early failures (they keep today's silent fallback) and for
41
- * non-stream errors. The caller still falls back either way — the user must
42
- * still get an answer, just an informed wait instead of an apparent hang.
43
- */
44
- export function streamFallbackNotice(error) {
45
- if (!(error instanceof AskStreamError))
46
- return null;
47
- const elapsedMs = error.elapsedMs ?? 0;
48
- const toolEvents = error.toolEvents ?? 0;
49
- if (elapsedMs < SUBSTANTIAL_STREAM_PROGRESS_MS &&
50
- toolEvents <= SUBSTANTIAL_STREAM_TOOL_EVENTS) {
51
- return null;
52
- }
53
- return (`stream interrupted after ${Math.round(elapsedMs / 1000)}s; ` +
54
- 'retrying via buffered request — this may take another minute or two.');
55
- }
56
- /**
57
- * Incremental SSE frame parser. Feed decoded text chunks (arbitrarily split —
58
- * even mid `data:` line or mid multi-byte frame); complete `data:` payloads
59
- * are returned in order. Handles LF and CRLF framing, multi-line `data:`
60
- * fields, and ignores comments (`: ...`) and non-data fields (`event:`, `id:`).
61
- */
62
- export class SseParser {
63
- buffer = '';
64
- pendingCr = false;
65
- push(chunk) {
66
- let text = (this.pendingCr ? '\r' : '') + chunk;
67
- this.pendingCr = false;
68
- // Hold back a trailing '\r' — it may be half of a CRLF split across chunks.
69
- if (text.endsWith('\r')) {
70
- this.pendingCr = true;
71
- text = text.slice(0, -1);
72
- }
73
- this.buffer += text.replace(/\r\n/g, '\n').replace(/\r/g, '\n');
74
- const payloads = [];
75
- let boundary = this.buffer.indexOf('\n\n');
76
- while (boundary !== -1) {
77
- const frame = this.buffer.slice(0, boundary);
78
- this.buffer = this.buffer.slice(boundary + 2);
79
- const payload = dataPayloadOf(frame);
80
- if (payload !== null)
81
- payloads.push(payload);
82
- boundary = this.buffer.indexOf('\n\n');
83
- }
84
- return payloads;
85
- }
86
- /** Drain a final frame that was not terminated by a blank line. */
87
- flush() {
88
- const remaining = this.buffer + (this.pendingCr ? '\r' : '');
89
- this.buffer = '';
90
- this.pendingCr = false;
91
- const payload = dataPayloadOf(remaining.replace(/\r\n/g, '\n').replace(/\r/g, '\n'));
92
- return payload !== null ? [payload] : [];
93
- }
94
- }
95
- function dataPayloadOf(frame) {
96
- const dataLines = [];
97
- for (const line of frame.split('\n')) {
98
- if (line.startsWith('data:')) {
99
- dataLines.push(line.slice(5).replace(/^ /, ''));
100
- }
101
- }
102
- if (dataLines.length === 0)
103
- return null;
104
- const payload = dataLines.join('\n');
105
- return payload.trim().length > 0 ? payload : null;
106
- }
107
- /** Phases worth a human status line (terminal/idle phases are omitted). */
108
- const RENDERED_PHASES = new Set(['planning', 'searching', 'analyzing', 'writing']);
109
- // ─── Idle liveness ticker ────────────────────────────────────
110
- /** Quiet period before the first liveness line is printed. */
111
- export const IDLE_TICKER_THRESHOLD_MS = 10_000;
112
- /** Minimum spacing between liveness lines. */
113
- export const IDLE_TICKER_MIN_UPDATE_MS = 15_000;
114
- const IDLE_TICKER_POLL_MS = 1_000;
115
- /**
116
- * Client-side liveness for quiet stream periods: long tool-less LLM turns
117
- * (coverage gate, synthesis) emit no events for 30–90s, which reads as a
118
- * hang. Once no event has arrived for IDLE_TICKER_THRESHOLD_MS this prints an
119
- * unobtrusive "… still working (Ns)" line (at most every
120
- * IDLE_TICKER_MIN_UPDATE_MS) through the same progress channel as tool cards —
121
- * stderr only, never interleaved into the final answer.
122
- */
123
- export class IdleTicker {
124
- write;
125
- now;
126
- startedAt;
127
- lastEventAt;
128
- lastPrintAt = 0;
129
- timer;
130
- constructor(write, now = Date.now) {
131
- this.write = write;
132
- this.now = now;
133
- this.startedAt = this.now();
134
- this.lastEventAt = this.startedAt;
135
- }
136
- start() {
137
- if (this.timer)
138
- return;
139
- this.startedAt = this.now();
140
- this.lastEventAt = this.startedAt;
141
- this.timer = setInterval(() => this.poll(), IDLE_TICKER_POLL_MS);
142
- // Never hold the process open for a liveness timer.
143
- this.timer.unref?.();
144
- }
145
- /** Reset the idle clock — call on every real stream event. */
146
- touch() {
147
- this.lastEventAt = this.now();
148
- }
149
- stop() {
150
- if (this.timer !== undefined)
151
- clearInterval(this.timer);
152
- this.timer = undefined;
153
- }
154
- /** One liveness check; driven by the interval (public for tests). */
155
- poll() {
156
- const at = this.now();
157
- if (at - this.lastEventAt < IDLE_TICKER_THRESHOLD_MS)
158
- return;
159
- if (this.lastPrintAt !== 0 && at - this.lastPrintAt < IDLE_TICKER_MIN_UPDATE_MS)
160
- return;
161
- this.lastPrintAt = at;
162
- const secs = Math.round((at - this.startedAt) / 1000);
163
- try {
164
- this.write(`… still working (${secs}s)`);
165
- }
166
- catch {
167
- /* a liveness line must never break the stream */
168
- }
169
- }
170
- }
171
- /**
172
- * Consumes parsed stream events: renders live progress lines through
173
- * `onProgress` and accumulates the final outcome. `finish()` throws
174
- * `AskStreamError` unless a terminal event (`done` or `error`) arrived, so a
175
- * connection dropped mid-run triggers the buffered fallback instead of
176
- * rendering a truncated answer.
177
- */
178
- export class AskStreamCollector {
179
- onProgress;
180
- answer = '';
181
- confidence = 'medium';
182
- citations = [];
183
- nextCommands = [];
184
- elapsedMs;
185
- errorMessage;
186
- terminal = false;
187
- lastPhase;
188
- toolNames = new Map();
189
- toolEvents = 0;
190
- constructor(onProgress = () => { }) {
191
- this.onProgress = onProgress;
192
- }
193
- handle(raw) {
194
- if (this.terminal)
195
- return;
196
- const event = raw;
197
- if (!event || typeof event !== 'object' || typeof event.type !== 'string') {
198
- throw new AskStreamError('stream emitted a frame without an event type');
199
- }
200
- switch (event.type) {
201
- case 'phase': {
202
- const phase = typeof event.phase === 'string' ? event.phase : '';
203
- if (RENDERED_PHASES.has(phase) && phase !== this.lastPhase) {
204
- this.lastPhase = phase;
205
- this.onProgress(`▸ ${phase}…`);
206
- }
207
- break;
208
- }
209
- case 'thinking': {
210
- // Model narration (`t<N>` ids) is too chatty for one-line terminal
211
- // progress and stays hidden. Deliberation status lines the backend
212
- // emits on purpose ('plan', 'gate-*': multi-part plan, coverage
213
- // checks, nudge cycles) are concise and cover otherwise-silent LLM
214
- // calls — render them like tool cards so the gate never reads as a hang.
215
- const id = typeof event.id === 'string' ? event.id : '';
216
- if (id === 'plan' || id.startsWith('gate-')) {
217
- const delta = typeof event.delta === 'string' ? event.delta : '';
218
- if (delta.trim().length > 0)
219
- this.onProgress(`◦ ${oneLine(delta)}`);
220
- }
221
- break;
222
- }
223
- case 'tool_start': {
224
- this.toolEvents += 1;
225
- const tool = event.tool;
226
- const id = typeof tool?.id === 'string' ? tool.id : '';
227
- // Backend-controlled strings — strip ANSI so a rogue/compromised backend
228
- // (or prompt-injected LLM output) can't inject terminal control sequences.
229
- const name = stripAnsi(typeof tool?.tool === 'string' ? tool.tool : 'tool');
230
- const title = stripAnsi(typeof tool?.title === 'string' ? tool.title : name);
231
- if (id)
232
- this.toolNames.set(id, name);
233
- this.onProgress(`• ${name} — ${title}…`);
234
- break;
235
- }
236
- case 'tool_end': {
237
- this.toolEvents += 1;
238
- const id = typeof event.id === 'string' ? event.id : '';
239
- const name = this.toolNames.get(id) ?? 'tool';
240
- this.onProgress(`• ${name} — ${toolEndDetail(event)}`);
241
- break;
242
- }
243
- case 'sources': {
244
- this.citations = Array.isArray(event.citations)
245
- ? event.citations.flatMap(normalizeCitation)
246
- : [];
247
- break;
248
- }
249
- case 'text': {
250
- if (typeof event.delta === 'string')
251
- this.answer += event.delta;
252
- break;
253
- }
254
- case 'done': {
255
- this.terminal = true;
256
- this.confidence = normalizeStreamConfidence(event.confidence);
257
- if (typeof event.elapsedMs === 'number')
258
- this.elapsedMs = event.elapsedMs;
259
- this.nextCommands = Array.isArray(event.nextCommands)
260
- ? event.nextCommands.filter((item) => typeof item === 'string')
261
- : [];
262
- break;
263
- }
264
- case 'error': {
265
- this.terminal = true;
266
- this.errorMessage = typeof event.message === 'string' && event.message.trim().length > 0
267
- ? event.message
268
- : 'The analysis agent hit an error and could not complete the request.';
269
- break;
270
- }
271
- default:
272
- // Forward-compatible: unknown event types are ignored, not fatal.
273
- break;
274
- }
275
- }
276
- isTerminal() {
277
- return this.terminal;
278
- }
279
- /** tool_start/tool_end events seen so far (fallback-heuristic input). */
280
- get toolEventCount() {
281
- return this.toolEvents;
282
- }
283
- finish() {
284
- if (!this.terminal) {
285
- throw new AskStreamError('stream ended before a terminal done/error event');
286
- }
287
- if (!this.errorMessage && this.answer.trim().length === 0) {
288
- // Mirror the buffered contract: an empty answer means "no usable cloud
289
- // answer" and must trigger the fallback path, not render as blank prose.
290
- throw new AskStreamError('stream completed with an empty answer');
291
- }
292
- return {
293
- answer: this.answer,
294
- confidence: this.confidence,
295
- citations: this.citations,
296
- nextCommands: this.nextCommands,
297
- ...(this.elapsedMs !== undefined ? { elapsedMs: this.elapsedMs } : {}),
298
- ...(this.errorMessage ? { errorMessage: this.errorMessage } : {}),
299
- };
300
- }
301
- }
302
- /**
303
- * Tool cards must stay one line: backend summaries can be multi-line prose
304
- * (e.g. get_timeseries emits a day-by-day breakdown), which would spill the
305
- * whole table into the progress area. Collapse whitespace and truncate.
306
- */
307
- const TOOL_CARD_DETAIL_MAX = 100;
308
- function oneLine(text) {
309
- // Strip ANSI first: the source is backend-controlled tool summaries/errors,
310
- // and ESC bytes survive the whitespace collapse below.
311
- const collapsed = stripAnsi(text).replace(/\s+/g, ' ').trim();
312
- return collapsed.length > TOOL_CARD_DETAIL_MAX
313
- ? `${collapsed.slice(0, TOOL_CARD_DETAIL_MAX - 1)}…`
314
- : collapsed;
315
- }
316
- function toolEndDetail(event) {
317
- const summary = typeof event.summary === 'string' ? event.summary : '';
318
- const secs = typeof event.ms === 'number' ? `${(event.ms / 1000).toFixed(1)}s` : undefined;
319
- if (event.status === 'error') {
320
- const reason = typeof event.error === 'string' && event.error ? event.error : summary || 'failed';
321
- return joinDetail(`failed: ${oneLine(reason)}`, secs);
322
- }
323
- const detail = summary ||
324
- (typeof event.hits === 'number'
325
- ? `${event.hits} hits`
326
- : typeof event.rows === 'number'
327
- ? `${event.rows} rows`
328
- : 'done');
329
- return joinDetail(oneLine(detail), secs);
330
- }
331
- function joinDetail(detail, secs) {
332
- return secs ? `${detail}, ${secs}` : detail;
333
- }
334
- function normalizeCitation(item) {
335
- const record = item;
336
- if (!record || typeof record !== 'object')
337
- return [];
338
- const label = typeof record.label === 'string' ? record.label : undefined;
339
- const id = typeof record.id === 'string' ? record.id : undefined;
340
- if (!label && !id)
341
- return [];
342
- return [{
343
- id: id ?? `ref-${typeof record.n === 'number' ? record.n : 0}`,
344
- n: typeof record.n === 'number' ? record.n : 0,
345
- label: label ?? id ?? 'Cloud evidence',
346
- source: typeof record.source === 'string' ? record.source : 'evidence',
347
- ...(typeof record.snippet === 'string' ? { snippet: record.snippet } : {}),
348
- ...(typeof record.href === 'string' ? { href: record.href } : {}),
349
- }];
350
- }
351
- function normalizeStreamConfidence(value) {
352
- return value === 'high' || value === 'medium' || value === 'low' || value === 'unknown'
353
- ? value
354
- : 'medium';
355
- }
356
- /**
357
- * Iterate any chunk source (a fetch body, or test fixtures) through the SSE
358
- * parser into the collector. Exported separately from `streamAsk` so tests can
359
- * exercise chunk-splitting behavior without a network layer.
360
- */
361
- export async function consumeAskSse(chunks, collector,
362
- /**
363
- * Fires once per parsed `data:` event (keepalive comment frames never reach
364
- * it). Used to reset the idle liveness ticker on real progress.
365
- */
366
- onEvent) {
367
- const parser = new SseParser();
368
- const decoder = new TextDecoder();
369
- for await (const chunk of chunks) {
370
- const text = typeof chunk === 'string' ? chunk : decoder.decode(chunk, { stream: true });
371
- for (const payload of parser.push(text)) {
372
- collector.handle(parsePayload(payload));
373
- onEvent?.();
374
- if (collector.isTerminal())
375
- return collector.finish();
376
- }
377
- }
378
- for (const payload of parser.flush()) {
379
- collector.handle(parsePayload(payload));
380
- onEvent?.();
381
- if (collector.isTerminal())
382
- return collector.finish();
383
- }
384
- return collector.finish();
385
- }
386
- function parsePayload(payload) {
387
- try {
388
- return JSON.parse(payload);
389
- }
390
- catch {
391
- throw new AskStreamError('stream emitted an unparseable frame');
392
- }
393
- }
394
- const DEFAULT_STREAM_TIMEOUT_MS = 250_000;
395
- /**
396
- * POST the question to `/api/cli/ask/stream` and consume the SSE response.
397
- * Throws `AskStreamError` on any transport/framing problem (non-2xx, wrong
398
- * content type, network drop, truncated stream) so callers can fall back to
399
- * the buffered endpoint.
400
- */
401
- export async function streamAsk(options) {
402
- const profileOptions = options.profile ? { profile: options.profile } : {};
403
- const url = `${resolveModaBaseUrl(profileOptions)}/api/cli/ask/stream`;
404
- const controller = new AbortController();
405
- const timeout = setTimeout(() => controller.abort(), options.timeoutMs ?? DEFAULT_STREAM_TIMEOUT_MS);
406
- const startedAt = Date.now();
407
- const onProgress = options.onProgress ?? (() => { });
408
- const collector = new AskStreamCollector(onProgress);
409
- // Liveness lines share the progress channel (stderr in the CLI), so they
410
- // can never interleave into the final stdout answer.
411
- const ticker = new IdleTicker(onProgress);
412
- ticker.start();
413
- try {
414
- const fetchImpl = options.fetchImpl ?? fetch;
415
- let response;
416
- try {
417
- response = await fetchImpl(url, {
418
- method: 'POST',
419
- headers: {
420
- 'x-api-key': resolveApiKey(profileOptions) ?? '',
421
- 'Content-Type': 'application/json',
422
- Accept: 'text/event-stream',
423
- },
424
- body: JSON.stringify({
425
- question: options.question,
426
- daysBack: options.daysBack,
427
- profile: options.profile,
428
- harness: options.harness,
429
- }),
430
- signal: controller.signal,
431
- });
432
- }
433
- catch (error) {
434
- throw new AskStreamError(`stream request failed: ${error instanceof Error ? error.message : String(error)}`);
435
- }
436
- if (!response.ok) {
437
- throw new AskStreamError(`stream endpoint returned HTTP ${response.status}`);
438
- }
439
- const contentType = response.headers.get('content-type') ?? '';
440
- if (!contentType.includes('text/event-stream')) {
441
- // An old frontend deploy without the stream proxy can 200 with JSON/HTML.
442
- throw new AskStreamError(`stream endpoint returned unexpected content type '${contentType || 'unknown'}'`);
443
- }
444
- if (!response.body) {
445
- throw new AskStreamError('stream response had no body');
446
- }
447
- try {
448
- return await consumeAskSse(response.body, collector, () => ticker.touch());
449
- }
450
- catch (error) {
451
- if (error instanceof AskStreamError)
452
- throw error;
453
- throw new AskStreamError(`stream read failed: ${error instanceof Error ? error.message : String(error)}`);
454
- }
455
- }
456
- catch (error) {
457
- // Annotate stream failures with how far the run got, so the caller can
458
- // decide between today's silent fallback (early drop) and an informed
459
- // one (drop after substantial progress). See `streamFallbackNotice`.
460
- if (error instanceof AskStreamError) {
461
- if (error.elapsedMs === undefined)
462
- error.elapsedMs = Date.now() - startedAt;
463
- if (error.toolEvents === undefined)
464
- error.toolEvents = collector.toolEventCount;
465
- }
466
- throw error;
467
- }
468
- finally {
469
- ticker.stop();
470
- clearTimeout(timeout);
471
- }
472
- }
473
- //# sourceMappingURL=ask-stream.js.map
@@ -1,35 +0,0 @@
1
- import { type ProfileIOOptions } from './profiles.js';
2
- /**
3
- * Kind of credential stored in the auth session file. Absent means a legacy
4
- * Auth0 access token: legacy save paths deliberately leave the field out so
5
- * a downgraded CLI reading the same file sees a byte-identical shape (the
6
- * safest downgrade-compat choice). Only the PKCE exchange path writes
7
- * 'moda_ct'.
8
- */
9
- export type StoredAuthTokenKind = 'moda_ct' | 'auth0';
10
- export interface StoredAuth {
11
- access_token: string;
12
- expires_at: number;
13
- profile?: string;
14
- token_type?: 'bearer';
15
- token_kind?: StoredAuthTokenKind;
16
- user_email?: string;
17
- }
18
- export interface AuthSessionOptions extends ProfileIOOptions {
19
- includeExpired?: boolean;
20
- }
21
- /** moda_ct CLI session tokens get renewal + revocation semantics. */
22
- export declare function isModaCtSession(session: StoredAuth): boolean;
23
- export declare function isAuthSessionValid(data: StoredAuth | null, now?: number): data is StoredAuth;
24
- export declare function loadToken(options?: AuthSessionOptions): string | null;
25
- export declare function loadAuthSession(options?: AuthSessionOptions): StoredAuth | null;
26
- export declare function resolveStorageProfile(options: ProfileIOOptions): string | undefined;
27
- export declare function saveAuthSession(accessToken: string, expiresIn: number, options?: ProfileIOOptions, extras?: Pick<StoredAuth, 'token_kind' | 'user_email'>): StoredAuth;
28
- /**
29
- * Write a fully-formed session to disk, honoring the profile named on the
30
- * session itself (so renewals rewrite exactly the file the session was
31
- * loaded from, profile-scoped or legacy).
32
- */
33
- export declare function persistAuthSession(data: StoredAuth, options?: ProfileIOOptions): StoredAuth;
34
- export declare function clearAuthSession(options?: ProfileIOOptions): void;
35
- //# sourceMappingURL=auth-storage.d.ts.map
@@ -1,159 +0,0 @@
1
- // src/auth-storage.ts — StoredAuth model and on-disk auth session persistence
2
- // (legacy ~/.moda/auth.json and profile-scoped ~/.moda/secrets/auth/<profile>.json).
3
- import { existsSync, readFileSync, rmSync } from 'node:fs';
4
- import { getModaPaths, loadProfileConfig, saveProfileConfig, secretFilePathFromRef, writeSecretJsonFile, } from './profiles.js';
5
- function tokenPath(options = {}) {
6
- return getModaPaths(options).legacyAuthPath;
7
- }
8
- function authTokenRefForProfile(profile) {
9
- return `file:secrets/auth/${profile}.json`;
10
- }
11
- function parseStoredAuth(value) {
12
- if (!value || typeof value !== 'object' || Array.isArray(value)) {
13
- return null;
14
- }
15
- const data = value;
16
- if (typeof data.access_token !== 'string' ||
17
- typeof data.expires_at !== 'number') {
18
- return null;
19
- }
20
- return {
21
- access_token: data.access_token,
22
- expires_at: data.expires_at,
23
- profile: typeof data.profile === 'string' ? data.profile : undefined,
24
- token_type: data.token_type === 'bearer' ? 'bearer' : undefined,
25
- // Additive fields: old files simply lack them (-> undefined).
26
- token_kind: data.token_kind === 'moda_ct' || data.token_kind === 'auth0'
27
- ? data.token_kind
28
- : undefined,
29
- user_email: typeof data.user_email === 'string' ? data.user_email : undefined,
30
- };
31
- }
32
- /** moda_ct CLI session tokens get renewal + revocation semantics. */
33
- export function isModaCtSession(session) {
34
- return (session.token_kind === 'moda_ct' ||
35
- session.access_token.startsWith('moda_ct_'));
36
- }
37
- export function isAuthSessionValid(data, now = Date.now()) {
38
- if (!data)
39
- return false;
40
- return now / 1000 <= data.expires_at - 60;
41
- }
42
- function readStoredAuth(path) {
43
- try {
44
- if (!existsSync(path))
45
- return null;
46
- return parseStoredAuth(JSON.parse(readFileSync(path, 'utf8')));
47
- }
48
- catch {
49
- return null;
50
- }
51
- }
52
- export function loadToken(options = {}) {
53
- return loadAuthSession(options)?.access_token ?? null;
54
- }
55
- export function loadAuthSession(options = {}) {
56
- const profileSession = loadProfileAuthSession(options);
57
- if (profileSession)
58
- return profileSession;
59
- const legacy = readStoredAuth(tokenPath(options));
60
- if (!legacy)
61
- return null;
62
- if (!options.includeExpired && !isAuthSessionValid(legacy)) {
63
- return null;
64
- }
65
- return legacy;
66
- }
67
- function loadProfileAuthSession(options) {
68
- const profileName = options.profile ?? (options.env ?? process.env).MODA_PROFILE;
69
- if (!profileName)
70
- return null;
71
- try {
72
- const profile = loadProfileConfig(options).profiles[profileName];
73
- const tokenRef = profile?.auth_token_ref;
74
- if (!tokenRef)
75
- return null;
76
- const path = secretFilePathFromRef(tokenRef, options);
77
- if (!path)
78
- return null;
79
- const session = readStoredAuth(path);
80
- if (!session)
81
- return null;
82
- if (!options.includeExpired && !isAuthSessionValid(session))
83
- return null;
84
- return session;
85
- }
86
- catch {
87
- return null;
88
- }
89
- }
90
- export function resolveStorageProfile(options) {
91
- return options.profile ?? (options.env ?? process.env).MODA_PROFILE;
92
- }
93
- export function saveAuthSession(accessToken, expiresIn, options = {}, extras = {}) {
94
- return persistAuthSession({
95
- access_token: accessToken,
96
- expires_at: Math.floor(Date.now() / 1000) + expiresIn,
97
- profile: resolveStorageProfile(options),
98
- token_type: 'bearer',
99
- ...(extras.token_kind ? { token_kind: extras.token_kind } : {}),
100
- ...(extras.user_email ? { user_email: extras.user_email } : {}),
101
- }, options);
102
- }
103
- /**
104
- * Write a fully-formed session to disk, honoring the profile named on the
105
- * session itself (so renewals rewrite exactly the file the session was
106
- * loaded from, profile-scoped or legacy).
107
- */
108
- export function persistAuthSession(data, options = {}) {
109
- const profile = data.profile;
110
- if (profile) {
111
- const ref = authTokenRefForProfile(profile);
112
- const path = secretFilePathFromRef(ref, options);
113
- if (!path)
114
- throw new Error(`Unsupported auth token reference: ${ref}`);
115
- const config = loadProfileConfig(options);
116
- const existing = config.profiles[profile];
117
- if (!existing) {
118
- throw new Error(`Moda profile '${profile}' does not exist.`);
119
- }
120
- writeSecretJsonFile(path, data);
121
- saveProfileConfig({
122
- ...config,
123
- profiles: {
124
- ...config.profiles,
125
- [profile]: {
126
- ...existing,
127
- auth_token_ref: ref,
128
- },
129
- },
130
- }, options);
131
- return data;
132
- }
133
- writeSecretJsonFile(tokenPath(options), data);
134
- return data;
135
- }
136
- export function clearAuthSession(options = {}) {
137
- const profile = options.profile ?? (options.env ?? process.env).MODA_PROFILE;
138
- if (profile) {
139
- const ref = authTokenRefForProfile(profile);
140
- const path = secretFilePathFromRef(ref, options);
141
- if (path)
142
- rmSync(path, { force: true });
143
- const config = loadProfileConfig(options);
144
- const existing = config.profiles[profile];
145
- if (existing) {
146
- const { auth_token_ref: _drop, ...profileWithoutToken } = existing;
147
- saveProfileConfig({
148
- ...config,
149
- profiles: {
150
- ...config.profiles,
151
- [profile]: profileWithoutToken,
152
- },
153
- }, options);
154
- }
155
- return;
156
- }
157
- rmSync(tokenPath(options), { force: true });
158
- }
159
- //# sourceMappingURL=auth-storage.js.map