@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.
- package/dist/cli-g8qs2tj9.js +9777 -0
- package/dist/cli-pwbvtes8.js +2312 -0
- package/dist/cli-qszcpda3.js +284 -0
- package/dist/cli.js +4775 -1159
- package/dist/index-6h0nc1r5.js +2765 -0
- package/dist/provision-j5gq8cvx.js +72 -0
- package/package.json +3 -2
- package/skills/integration/index.json +2 -2
- package/dist/agent-schemas.d.ts +0 -3
- package/dist/agent-schemas.js +0 -358
- package/dist/agent.d.ts +0 -5
- package/dist/agent.js +0 -477
- package/dist/analyst-tui.d.ts +0 -81
- package/dist/analyst-tui.js +0 -314
- package/dist/api-client.d.ts +0 -46
- package/dist/api-client.js +0 -120
- package/dist/ask-stream.d.ts +0 -146
- package/dist/ask-stream.js +0 -473
- package/dist/auth-storage.d.ts +0 -35
- package/dist/auth-storage.js +0 -159
- package/dist/auth.d.ts +0 -50
- package/dist/auth.js +0 -694
- package/dist/cli-session.d.ts +0 -26
- package/dist/cli-session.js +0 -169
- package/dist/cli.d.ts +0 -44
- package/dist/command-context.d.ts +0 -62
- package/dist/command-context.js +0 -57
- package/dist/command-registry.d.ts +0 -22
- package/dist/command-registry.js +0 -15
- package/dist/config.d.ts +0 -60
- package/dist/config.js +0 -110
- package/dist/doctor.d.ts +0 -79
- package/dist/doctor.js +0 -1182
- package/dist/errors.d.ts +0 -50
- package/dist/errors.js +0 -64
- package/dist/exit-funnel.d.ts +0 -14
- package/dist/exit-funnel.js +0 -53
- package/dist/harness-artifact-bodies.d.ts +0 -28
- package/dist/harness-artifact-bodies.js +0 -276
- package/dist/harness-candidates.d.ts +0 -77
- package/dist/harness-candidates.js +0 -482
- package/dist/harness-remote.d.ts +0 -135
- package/dist/harness-remote.js +0 -540
- package/dist/harness-report.d.ts +0 -145
- package/dist/harness-report.js +0 -1579
- package/dist/harness-sync-limits.d.ts +0 -8
- package/dist/harness-sync-limits.js +0 -21
- package/dist/harness.d.ts +0 -463
- package/dist/harness.js +0 -4009
- package/dist/init/index.d.ts +0 -78
- package/dist/init/index.js +0 -1442
- package/dist/init/mcp.d.ts +0 -2
- package/dist/init/mcp.js +0 -27
- package/dist/init/progress-board.d.ts +0 -80
- package/dist/init/progress-board.js +0 -225
- package/dist/init/rules.d.ts +0 -6
- package/dist/init/rules.js +0 -171
- package/dist/init/sdk-catalog.d.ts +0 -10
- package/dist/init/sdk-catalog.js +0 -80
- package/dist/init/sdk-guide.d.ts +0 -19
- package/dist/init/sdk-guide.js +0 -185
- package/dist/init/sdk-integration.d.ts +0 -53
- package/dist/init/sdk-integration.js +0 -835
- package/dist/init/sdk-protocol.d.ts +0 -135
- package/dist/init/sdk-protocol.js +0 -152
- package/dist/init/sdk-verification.d.ts +0 -27
- package/dist/init/sdk-verification.js +0 -123
- package/dist/init/tenant.d.ts +0 -67
- package/dist/init/tenant.js +0 -421
- package/dist/intelligence-schema.d.ts +0 -349
- package/dist/intelligence-schema.js +0 -174
- package/dist/keys.d.ts +0 -47
- package/dist/keys.js +0 -136
- package/dist/manifest.d.ts +0 -5
- package/dist/manifest.js +0 -114
- package/dist/markdown-terminal.d.ts +0 -49
- package/dist/markdown-terminal.js +0 -212
- package/dist/output.d.ts +0 -191
- package/dist/output.js +0 -624
- package/dist/production-intelligence.d.ts +0 -175
- package/dist/production-intelligence.js +0 -1430
- package/dist/profile-commands.d.ts +0 -10
- package/dist/profile-commands.js +0 -342
- package/dist/profiles.d.ts +0 -112
- package/dist/profiles.js +0 -573
- package/dist/prompts-ab.d.ts +0 -71
- package/dist/prompts-ab.js +0 -313
- package/dist/prompts-propose.d.ts +0 -15
- package/dist/prompts-propose.js +0 -212
- package/dist/prompts.d.ts +0 -58
- package/dist/prompts.js +0 -418
- package/dist/provision.d.ts +0 -28
- package/dist/provision.js +0 -104
- package/dist/redact.d.ts +0 -2
- package/dist/redact.js +0 -90
- package/dist/schemas.d.ts +0 -225
- package/dist/schemas.js +0 -126
- package/dist/skills-bundled.d.ts +0 -35
- package/dist/skills-bundled.js +0 -157
- package/dist/skills-manifest.d.ts +0 -33
- package/dist/skills-manifest.js +0 -244
- package/dist/skills.d.ts +0 -57
- package/dist/skills.js +0 -783
- package/dist/sync.d.ts +0 -5
- package/dist/sync.js +0 -36
- package/dist/telemetry.d.ts +0 -56
- package/dist/telemetry.js +0 -167
- package/dist/transforms.d.ts +0 -25
- package/dist/transforms.js +0 -102
- package/dist/tui.d.ts +0 -48
- package/dist/tui.js +0 -393
- package/dist/update-check.d.ts +0 -31
- package/dist/update-check.js +0 -141
- package/dist/workspace.d.ts +0 -12
- package/dist/workspace.js +0 -1294
package/dist/ask-stream.js
DELETED
|
@@ -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
|
package/dist/auth-storage.d.ts
DELETED
|
@@ -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
|
package/dist/auth-storage.js
DELETED
|
@@ -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
|