@moda-ai/cli 1.17.1 → 1.18.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/harness.d.ts CHANGED
@@ -1,3 +1,4 @@
1
+ import { type AnalystTuiEvent } from './analyst-tui.js';
1
2
  import type { CommandContext } from './command-context.js';
2
3
  import type { HarnessReport } from './harness-report.js';
3
4
  declare const SCHEMA_VERSION = "harness.v0.1";
@@ -271,6 +272,34 @@ export declare function writeHarnessAnalyzeResult(context: CommandContext, paylo
271
272
  */
272
273
  export declare function emitWrittenArtifacts(context: CommandContext, written: string[], description: string): void;
273
274
  export declare function repairExternalAnalystReport(report: HarnessReport, rootDir: string): HarnessReport;
275
+ /**
276
+ * A parsed coding-agent stream event handed to `onAgentEvent` consumers.
277
+ * Extends the analyst TUI event with an optional `detail` (file path or
278
+ * command extracted from tool input) so callers can render richer one-line
279
+ * progress cards. The analyst TUI ignores the extra field.
280
+ */
281
+ export type CodingAgentEvent = AnalystTuiEvent & {
282
+ detail?: string;
283
+ };
284
+ /**
285
+ * Translate a parsed coding-agent stream event into a one-line progress card
286
+ * ("editing src/llm/client.ts", "running bun add moda-ai", trimmed agent text).
287
+ * Returns undefined for low-signal events (system noise) so they never reach
288
+ * the user. Shared by the init progress board (via sdk-integration) and the
289
+ * agent-stream analyst progress events below.
290
+ */
291
+ export declare function describeCodingAgentEvent(event: CodingAgentEvent): string | undefined;
292
+ /**
293
+ * Map a parsed analyst stream event to the `progress` agent event the CLI
294
+ * writes in agent-stream mode. Tool/command events get a `tool_use` phase so
295
+ * downstream consumers (the hosted analyze runner's progress parser counts
296
+ * phases starting with "tool") can track tool-call activity; agent text gets
297
+ * the generic `analyst` phase. Low-signal events map to undefined.
298
+ */
299
+ export declare function analystEventProgress(event: CodingAgentEvent): {
300
+ phase: string;
301
+ message: string;
302
+ } | undefined;
274
303
  export declare function sleep(ms: number): Promise<void>;
275
304
  export declare function summarizeInvalidAnalystReport(report: unknown): Record<string, unknown>;
276
305
  /**
@@ -306,6 +335,12 @@ export declare function runCodingAgent(options: {
306
335
  timeoutMs?: number;
307
336
  progressToStderr?: boolean;
308
337
  onProgress?: (message: string, phase?: string) => void;
338
+ /**
339
+ * Receive parsed agent stream events incrementally while output stays
340
+ * buffered. Requires the streaming invocation, so passing it defaults
341
+ * `stream` to true when not set explicitly.
342
+ */
343
+ onAgentEvent?: (event: CodingAgentEvent) => void;
309
344
  }): Promise<CodingAgentRun>;
310
345
  export declare function truncateText(value: string, maxLength: number): string;
311
346
  export declare function scanHarness(options?: ScanOptions): HarnessGraph;
package/dist/harness.js CHANGED
@@ -8,7 +8,7 @@ import { callControlAPI } from './api-client.js';
8
8
  import { AnalystTui, shouldUseAnalystTui } from './analyst-tui.js';
9
9
  import { resolveApiKey, resolveTenantId } from './config.js';
10
10
  import { CliInputError, CliInputRequiredError } from './errors.js';
11
- import { isRemoteHarnessAnalyze, runRemoteHarnessAnalyst } from './harness-remote.js';
11
+ import { HARNESS_REMOTE_RUN_STATE_PATH, isRemoteHarnessAnalyze, readRemoteRunState, runHarnessPull, runRemoteHarnessAnalyst, } from './harness-remote.js';
12
12
  import { getActiveProfile } from './profiles.js';
13
13
  import { enrichScanCaptureMetadata, annotatedSourceCandidates, extractToolBody, hydratePromptAndToolBodies, readPromptOrToolBody, resolveAnnotatedSourcePath, } from './harness-artifact-bodies.js';
14
14
  import { approveHarnessReport, createHarnessReport, graphForReport, hashHarnessReport, hashStableJson as hashHarnessReportStableJson, HARNESS_REPORT_PATH, loadHarnessReportForSync, normalizeHarnessReport, readHarnessReport, resolveHarnessReportPath, validateHarnessReport, writeHarnessReportArtifacts, } from './harness-report.js';
@@ -325,6 +325,9 @@ export async function runHarnessCommand(context) {
325
325
  });
326
326
  return { exitCode: validation.status === 'pass' ? 0 : 1 };
327
327
  }
328
+ case 'pull': {
329
+ return runHarnessPull({ context, rootDir, scanPlan });
330
+ }
328
331
  case 'validate-report': {
329
332
  const reportPath = resolveHarnessReportPath(rootDir, context.positionals[1] ?? context.flags.report);
330
333
  const report = readHarnessReport(reportPath);
@@ -411,11 +414,20 @@ export async function runHarnessCommand(context) {
411
414
  });
412
415
  const currentGraphHash = hashStableJson(graphForSync(graph));
413
416
  const syncState = readHarnessSyncState(rootDir);
417
+ const remoteRun = readRemoteRunState(rootDir);
414
418
  context.output.writeData({
415
419
  manifest: HARNESS_PATH,
416
420
  exists: existsSync(manifestPath),
417
421
  summary: summarizeHarness(graph),
418
422
  graphHash: currentGraphHash,
423
+ remoteRun: remoteRun
424
+ ? {
425
+ statePath: HARNESS_REMOTE_RUN_STATE_PATH,
426
+ runId: remoteRun.run_id,
427
+ status: remoteRun.status,
428
+ startedAt: remoteRun.started_at_iso || undefined,
429
+ }
430
+ : undefined,
419
431
  sync: syncState
420
432
  ? {
421
433
  statePath: SYNC_STATE_PATH,
@@ -439,7 +451,7 @@ export async function runHarnessCommand(context) {
439
451
  return { exitCode: 0 };
440
452
  }
441
453
  default:
442
- throw new CliInputError(`Unknown harness command '${subcommand}'.`, 'Usage: moda harness analyze|validate-report|approve|scan|sync|agents|graph|explain|write-summary|status');
454
+ throw new CliInputError(`Unknown harness command '${subcommand}'.`, 'Usage: moda harness analyze|pull|validate-report|approve|scan|sync|agents|graph|explain|write-summary|status');
443
455
  }
444
456
  }
445
457
  export function writeHarnessAnalyzeResult(context, payload) {
@@ -527,12 +539,17 @@ async function runExternalHarnessAnalyst(options) {
527
539
  rootDir: options.rootDir,
528
540
  tenantId: options.tenant?.tenantId,
529
541
  });
542
+ // Agent-stream mode also runs the analyst child in stream-json mode so its
543
+ // tool activity can be forwarded as NDJSON `progress` events (consumed by
544
+ // the hosted analyze runner for live run-state progress). Report extraction
545
+ // is unaffected: the report is read from .moda/harness-report.json.
546
+ const streamAnalystProgress = options.context.outputMode === 'agent-stream';
530
547
  const invocation = resolveExternalHarnessAnalystInvocation({
531
548
  adapter: options.adapter,
532
549
  flags: options.context.flags,
533
550
  prompt,
534
551
  rootDir: options.rootDir,
535
- stream: options.context.outputMode === 'human',
552
+ stream: options.context.outputMode === 'human' || streamAnalystProgress,
536
553
  });
537
554
  options.context.output.writeEvent({
538
555
  event: 'progress',
@@ -584,6 +601,22 @@ async function runExternalHarnessAnalyst(options) {
584
601
  elapsed_ms: Date.now() - options.context.startedAt,
585
602
  });
586
603
  },
604
+ // Forward the analyst's parsed tool/text events as progress events so
605
+ // agent-stream consumers see live activity ("reading src/agent.ts"),
606
+ // not just the periodic still-investigating ticker.
607
+ onAgentEvent: streamAnalystProgress
608
+ ? (event) => {
609
+ const progress = analystEventProgress(event);
610
+ if (!progress)
611
+ return;
612
+ options.context.output.writeEvent({
613
+ event: 'progress',
614
+ phase: progress.phase,
615
+ message: progress.message,
616
+ elapsed_ms: Date.now() - options.context.startedAt,
617
+ });
618
+ }
619
+ : undefined,
587
620
  });
588
621
  if (result.error) {
589
622
  throw new CliInputError(`Harness analyst ${options.adapter} failed to run: ${result.error.message}`, 'Check that the selected coding agent CLI is installed and authenticated, or run `moda harness analyze --analyst=local-scan` for the deterministic fallback.');
@@ -835,6 +868,68 @@ function extractTextFromAgentEvent(event) {
835
868
  function escapeRegExp(value) {
836
869
  return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
837
870
  }
871
+ /**
872
+ * Tool cards must stay one line (same convention as ask-stream's oneLine):
873
+ * collapse whitespace and truncate so multi-line agent prose never spills
874
+ * into a progress line.
875
+ */
876
+ const EVENT_CARD_MAX = 100;
877
+ function eventCardLine(text) {
878
+ const collapsed = text.replace(/\s+/g, ' ').trim();
879
+ return collapsed.length > EVENT_CARD_MAX
880
+ ? `${collapsed.slice(0, EVENT_CARD_MAX - 1)}…`
881
+ : collapsed;
882
+ }
883
+ /**
884
+ * Translate a parsed coding-agent stream event into a one-line progress card
885
+ * ("editing src/llm/client.ts", "running bun add moda-ai", trimmed agent text).
886
+ * Returns undefined for low-signal events (system noise) so they never reach
887
+ * the user. Shared by the init progress board (via sdk-integration) and the
888
+ * agent-stream analyst progress events below.
889
+ */
890
+ export function describeCodingAgentEvent(event) {
891
+ if (event.kind === 'tool') {
892
+ const name = event.toolName ?? 'tool';
893
+ const detail = event.detail;
894
+ if (/edit|write|patch|create|notebook/i.test(name)) {
895
+ return eventCardLine(detail ? `editing ${detail}` : `editing files (${name})`);
896
+ }
897
+ if (/bash|shell|exec|command|terminal|run/i.test(name)) {
898
+ return eventCardLine(detail ? `running ${detail}` : `running a command (${name})`);
899
+ }
900
+ if (/read|grep|glob|search|ls|find|cat/i.test(name)) {
901
+ return eventCardLine(detail ? `reading ${detail}` : `reading files (${name})`);
902
+ }
903
+ return eventCardLine(detail ? `using ${name}: ${detail}` : `using ${name}`);
904
+ }
905
+ if (event.kind === 'command') {
906
+ const command = event.detail ?? event.message.replace(/^command\s+/, '');
907
+ return eventCardLine(`running ${command}`);
908
+ }
909
+ if (event.kind === 'agent_text') {
910
+ const text = eventCardLine(event.message);
911
+ return text.length > 0 ? text : undefined;
912
+ }
913
+ if (event.kind === 'rate_limit')
914
+ return 'waiting on a provider rate limit';
915
+ return undefined;
916
+ }
917
+ /**
918
+ * Map a parsed analyst stream event to the `progress` agent event the CLI
919
+ * writes in agent-stream mode. Tool/command events get a `tool_use` phase so
920
+ * downstream consumers (the hosted analyze runner's progress parser counts
921
+ * phases starting with "tool") can track tool-call activity; agent text gets
922
+ * the generic `analyst` phase. Low-signal events map to undefined.
923
+ */
924
+ export function analystEventProgress(event) {
925
+ const card = describeCodingAgentEvent(event);
926
+ if (!card)
927
+ return undefined;
928
+ return {
929
+ phase: event.kind === 'tool' || event.kind === 'command' ? 'tool_use' : 'analyst',
930
+ message: card,
931
+ };
932
+ }
838
933
  function runExternalAnalystProcess(invocation, options) {
839
934
  return new Promise((resolveProcess) => {
840
935
  const child = spawn(invocation.command, invocation.args, {
@@ -889,22 +984,22 @@ function runExternalAnalystProcess(invocation, options) {
889
984
  stdout += text;
890
985
  else
891
986
  stderr += text;
892
- if (options.inheritedOutput) {
893
- if (options.tui && stream === 'stderr') {
894
- recordExternalAgentTextChunk(text, options.tui);
895
- }
896
- else if (options.streamEvents && stream === 'stdout') {
897
- eventLineBuffer += text;
898
- const lines = eventLineBuffer.split(/\r?\n/);
899
- eventLineBuffer = lines.pop() ?? '';
900
- for (const line of lines) {
901
- recordExternalAgentEventLine(line, options);
902
- }
903
- }
904
- else {
905
- process.stderr.write(chunk);
987
+ const wantsEventLines = options.streamEvents && stream === 'stdout' &&
988
+ (options.inheritedOutput || Boolean(options.onAgentEvent));
989
+ if (options.inheritedOutput && options.tui && stream === 'stderr') {
990
+ recordExternalAgentTextChunk(text, options.tui);
991
+ }
992
+ else if (wantsEventLines) {
993
+ eventLineBuffer += text;
994
+ const lines = eventLineBuffer.split(/\r?\n/);
995
+ eventLineBuffer = lines.pop() ?? '';
996
+ for (const line of lines) {
997
+ recordExternalAgentEventLine(line, options);
906
998
  }
907
999
  }
1000
+ else if (options.inheritedOutput) {
1001
+ process.stderr.write(chunk);
1002
+ }
908
1003
  if (stdout.length + stderr.length > options.maxBuffer && !maxBufferExceeded) {
909
1004
  maxBufferExceeded = true;
910
1005
  child.kill('SIGTERM');
@@ -994,6 +1089,9 @@ function recordExternalAgentEventLine(line, options) {
994
1089
  const event = parseExternalAgentEventLine(line);
995
1090
  if (!event)
996
1091
  return;
1092
+ options.onAgentEvent?.(event);
1093
+ if (!options.inheritedOutput)
1094
+ return;
997
1095
  options.tui?.recordEvent(event);
998
1096
  if (options.tui)
999
1097
  return;
@@ -1036,7 +1134,8 @@ function parseExternalAgentEvent(event) {
1036
1134
  stringValue(event.toolName) ??
1037
1135
  stringValue(call?.name);
1038
1136
  if (toolName && /tool|call|exec|command|bash/i.test(`${type ?? ''} ${subtype ?? ''}`)) {
1039
- return { kind: 'tool', toolName, message: `tool ${toolName}` };
1137
+ const detail = toolInputDetail(event.input ?? event.arguments ?? call?.input ?? call?.arguments);
1138
+ return { kind: 'tool', toolName, message: `tool ${toolName}`, ...(detail ? { detail } : {}) };
1040
1139
  }
1041
1140
  const directText = stringValue(event.text) ??
1042
1141
  stringValue(event.message) ??
@@ -1063,8 +1162,9 @@ function parseExternalAgentEvent(event) {
1063
1162
  return parsed;
1064
1163
  }
1065
1164
  const command = stringValue(event.command) ?? stringValue(event.cmd);
1066
- if (command)
1067
- return { kind: 'command', message: `command ${truncateText(command, 300)}` };
1165
+ if (command) {
1166
+ return { kind: 'command', message: `command ${truncateText(command, 300)}`, detail: truncateText(command, 300) };
1167
+ }
1068
1168
  if (type && !isLowSignalEventType(type)) {
1069
1169
  return { kind: 'system', message: subtype ? `${type}:${subtype}` : type };
1070
1170
  }
@@ -1076,7 +1176,10 @@ function parseAgentContentPart(part) {
1076
1176
  const partType = stringValue(part.type);
1077
1177
  if (partType === 'tool_use' || partType === 'tool_call') {
1078
1178
  const name = stringValue(part.name) ?? stringValue(part.tool_name);
1079
- return name ? { kind: 'tool', toolName: name, message: `tool ${name}` } : undefined;
1179
+ if (!name)
1180
+ return undefined;
1181
+ const detail = toolInputDetail(part.input ?? part.arguments);
1182
+ return { kind: 'tool', toolName: name, message: `tool ${name}`, ...(detail ? { detail } : {}) };
1080
1183
  }
1081
1184
  if (partType === 'tool_result' || partType === 'tool_output')
1082
1185
  return undefined;
@@ -1089,6 +1192,20 @@ function parseAgentContentPart(part) {
1089
1192
  return { kind: 'tool', toolName: name, message: `tool ${name}` };
1090
1193
  return undefined;
1091
1194
  }
1195
+ /** Pull the most useful one-line detail (file path or command) out of a tool input payload. */
1196
+ function toolInputDetail(input) {
1197
+ if (!isRecord(input))
1198
+ return undefined;
1199
+ const value = stringValue(input.file_path) ??
1200
+ stringValue(input.filePath) ??
1201
+ stringValue(input.path) ??
1202
+ stringValue(input.notebook_path) ??
1203
+ stringValue(input.command) ??
1204
+ stringValue(input.cmd) ??
1205
+ stringValue(input.pattern) ??
1206
+ stringValue(input.query);
1207
+ return value ? truncateText(firstMeaningfulLine(value), 300) : undefined;
1208
+ }
1092
1209
  function firstMeaningfulLine(value) {
1093
1210
  return value.split(/\r?\n/).map((line) => line.trim()).find(Boolean) ?? value.trim();
1094
1211
  }
@@ -1308,7 +1425,7 @@ export function codingAgentDisplayName(adapter) {
1308
1425
  }
1309
1426
  /** Spawn an installed coding-agent CLI with a prompt; capture its output. */
1310
1427
  export async function runCodingAgent(options) {
1311
- const stream = options.stream ?? false;
1428
+ const stream = options.stream ?? Boolean(options.onAgentEvent);
1312
1429
  const invocation = resolveExternalHarnessAnalystInvocation({
1313
1430
  adapter: options.adapter,
1314
1431
  flags: {},
@@ -1331,6 +1448,7 @@ export async function runCodingAgent(options) {
1331
1448
  maxAttempts: externalAnalystMaxAttempts(options.adapter),
1332
1449
  backoffMs: numericEnvAllowZero('MODA_HARNESS_ANALYST_RETRY_BACKOFF_MS', 6_000),
1333
1450
  onProgress: options.onProgress,
1451
+ onAgentEvent: options.onAgentEvent,
1334
1452
  });
1335
1453
  return {
1336
1454
  adapter: invocation.adapter,