sim 2.0.0-preview.18.1 → 2.0.0-preview.21.1

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 (2) hide show
  1. package/dist/index.js +700 -110
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -11539,6 +11539,278 @@ function attachKnowledgeDocumentUpload(documents) {
11539
11539
  });
11540
11540
  }
11541
11541
 
11542
+ // src/commands/protocol/logs-follow.ts
11543
+ var DEFAULT_BACKLOG = 10;
11544
+ var DEFAULT_INTERVAL_SECONDS = 3;
11545
+ var MIN_INTERVAL_SECONDS = 0.1;
11546
+ var MAX_BACKOFF_MS = 30000;
11547
+ var POLL_PAGE_SIZE = 100;
11548
+ var MAX_PAGES_PER_POLL = 10;
11549
+ var MAX_REMEMBERED_RUNS = 5000;
11550
+ var MAX_CELL_WIDTH2 = 60;
11551
+ var WAIT_SLICE_MS = 250;
11552
+ var RETRYABLE_CLIENT_STATUSES = new Set([408, 425, 429]);
11553
+ var ERASE_LINE = `${String.fromCharCode(27)}[K`;
11554
+ function collect(value, previous) {
11555
+ return [...previous, value];
11556
+ }
11557
+ function at2(row, path) {
11558
+ return path.split(".").reduce((value, key) => value && typeof value === "object" ? value[key] : undefined, row);
11559
+ }
11560
+ function renderCell2(value, format) {
11561
+ switch (format) {
11562
+ case "timestamp":
11563
+ return timestamp2(value);
11564
+ case "duration":
11565
+ return duration(value);
11566
+ case "bytes":
11567
+ return bytes(value);
11568
+ case "bool":
11569
+ return bool2(value);
11570
+ case "cost":
11571
+ return typeof value === "number" ? `$${value.toFixed(4)}` : text(null);
11572
+ default:
11573
+ return text(typeof value === "object" && value !== null ? JSON.stringify(value) : value);
11574
+ }
11575
+ }
11576
+ var COLUMNS = (CLI_CONTRACT.listLogs?.columns ?? []).map((spec) => ({
11577
+ header: spec.header,
11578
+ value: (row) => renderCell2(at2(row, spec.path ?? spec.header), spec.format)
11579
+ }));
11580
+ function oneLine2(value) {
11581
+ return value.replace(/\s*[\r\n\t]+\s*/g, " ");
11582
+ }
11583
+ function pad2(value, width) {
11584
+ return value + " ".repeat(Math.max(0, width - visibleWidth(value)));
11585
+ }
11586
+ function clamp2(value, width) {
11587
+ if (visibleWidth(value) <= width || visibleWidth(value) !== value.length)
11588
+ return value;
11589
+ return `${value.slice(0, Math.max(1, width - 1))}…`;
11590
+ }
11591
+ function createTableWriter() {
11592
+ let widths = null;
11593
+ return (rows) => {
11594
+ const lines = rows.map((row) => COLUMNS.map((column) => oneLine2(column.value(row))));
11595
+ if (!widths) {
11596
+ widths = COLUMNS.map((column, index) => Math.min(MAX_CELL_WIDTH2, Math.max(visibleWidth(column.header), ...lines.map((line) => visibleWidth(line[index])))));
11597
+ const header = widths;
11598
+ console.log(source_default.dim(COLUMNS.map((column, index) => pad2(column.header.toUpperCase(), header[index])).join(" ").trimEnd()));
11599
+ }
11600
+ const locked = widths;
11601
+ for (const line of lines) {
11602
+ console.log(line.map((cell, index) => pad2(clamp2(cell, locked[index]), locked[index])).join(" ").trimEnd());
11603
+ }
11604
+ };
11605
+ }
11606
+ function createWriter(format) {
11607
+ if (format === "json") {
11608
+ return (rows) => {
11609
+ for (const row of rows)
11610
+ console.log(JSON.stringify(row));
11611
+ };
11612
+ }
11613
+ if (format === "yaml") {
11614
+ return (rows) => {
11615
+ for (const row of rows) {
11616
+ console.log(`---
11617
+ ${dump(row, { lineWidth: 0, noRefs: true }).trimEnd()}`);
11618
+ }
11619
+ };
11620
+ }
11621
+ if (format === "text") {
11622
+ return (rows) => {
11623
+ if (rows.length > 0)
11624
+ printList("text", rows, COLUMNS);
11625
+ };
11626
+ }
11627
+ return createTableWriter();
11628
+ }
11629
+ function followStatus() {
11630
+ let reported2 = false;
11631
+ return {
11632
+ note: (message) => {
11633
+ if (!process.stderr.isTTY)
11634
+ return;
11635
+ reported2 = true;
11636
+ process.stderr.write(`\r${source_default.dim(message)}${ERASE_LINE}`);
11637
+ },
11638
+ warn: (message) => {
11639
+ if (reported2) {
11640
+ reported2 = false;
11641
+ process.stderr.write(`\r${ERASE_LINE}`);
11642
+ }
11643
+ process.stderr.write(`warning: ${message}
11644
+ `);
11645
+ },
11646
+ clear: () => {
11647
+ if (!reported2)
11648
+ return;
11649
+ reported2 = false;
11650
+ process.stderr.write(`\r${ERASE_LINE}`);
11651
+ }
11652
+ };
11653
+ }
11654
+ function watchForInterrupt() {
11655
+ let stopped = false;
11656
+ const stop = () => {
11657
+ stopped = true;
11658
+ };
11659
+ process.on("SIGINT", stop);
11660
+ process.on("SIGTERM", stop);
11661
+ return {
11662
+ interrupted: () => stopped,
11663
+ dispose: () => {
11664
+ process.off("SIGINT", stop);
11665
+ process.off("SIGTERM", stop);
11666
+ }
11667
+ };
11668
+ }
11669
+ async function waitFor(ms, interrupted) {
11670
+ let remaining = ms;
11671
+ while (remaining > 0 && !interrupted()) {
11672
+ const step = Math.min(WAIT_SLICE_MS, remaining);
11673
+ await sleep(step);
11674
+ remaining -= step;
11675
+ }
11676
+ }
11677
+ function isUnprinted(state, row) {
11678
+ if (state.seen.has(row.runId))
11679
+ return false;
11680
+ return state.floor === null || row.startedAt >= state.floor;
11681
+ }
11682
+ function remember(state, rows) {
11683
+ for (const row of rows)
11684
+ state.seen.set(row.runId, row.startedAt);
11685
+ let excess = state.seen.size - MAX_REMEMBERED_RUNS;
11686
+ if (excess <= 0)
11687
+ return;
11688
+ for (const [runId, startedAt] of state.seen) {
11689
+ if (excess <= 0)
11690
+ break;
11691
+ if (state.floor === null || startedAt > state.floor)
11692
+ state.floor = startedAt;
11693
+ state.seen.delete(runId);
11694
+ excess -= 1;
11695
+ }
11696
+ }
11697
+ async function collectUnprinted(client, path, query, state, pageSize, maxPages) {
11698
+ const rows = [];
11699
+ let cursor = null;
11700
+ let truncated = false;
11701
+ for (let page = 0;page < maxPages; page += 1) {
11702
+ const response = await client.request(path, {
11703
+ query: { ...query, limit: pageSize, cursor }
11704
+ });
11705
+ const page_rows = response?.data ?? [];
11706
+ const unprinted = page_rows.filter((row) => isUnprinted(state, row));
11707
+ rows.push(...unprinted);
11708
+ cursor = response?.nextCursor ?? null;
11709
+ if (!cursor || page_rows.length === 0 || unprinted.length < page_rows.length)
11710
+ break;
11711
+ if (page === maxPages - 1)
11712
+ truncated = true;
11713
+ }
11714
+ return { rows, truncated };
11715
+ }
11716
+ function isTransient(error) {
11717
+ if (!(error instanceof SimApiError))
11718
+ return false;
11719
+ if (error.status === 0 || error.status >= 500)
11720
+ return true;
11721
+ return RETRYABLE_CLIENT_STATUSES.has(error.status);
11722
+ }
11723
+ function nonNegativeInteger(raw, flag) {
11724
+ const value = Number(raw);
11725
+ if (!Number.isSafeInteger(value) || value < 0) {
11726
+ throw new SimApiError(`${flag} must be a non-negative integer`, 0);
11727
+ }
11728
+ return value;
11729
+ }
11730
+ function intervalMs(raw) {
11731
+ const seconds = Number(raw);
11732
+ if (!Number.isFinite(seconds) || seconds < MIN_INTERVAL_SECONDS) {
11733
+ throw new SimApiError(`--interval must be at least ${MIN_INTERVAL_SECONDS} seconds`, 0);
11734
+ }
11735
+ return Math.round(seconds * 1000);
11736
+ }
11737
+ function inSeconds(ms) {
11738
+ return Math.round(ms / 100) / 10;
11739
+ }
11740
+ function attachLogsFollow(logs) {
11741
+ logs.command("follow").description("Watch runs as they arrive, printing each new run once").option("--workflow <id>", "Only follow runs of this workflow (repeatable)", collect, []).option("--folder <path>", "Only follow runs of workflows in this folder (repeatable)", collect, []).option("--trigger <type>", "Only follow runs with this trigger type (repeatable)", collect, []).addOption(new Option("--level <level>", "Only follow runs at this severity").choices([
11742
+ ...V2_OPERATIONS.listLogs.query.level.values
11743
+ ])).addOption(new Option("--details <level>", "Response detail level; full names each run’s workflow").choices([...V2_OPERATIONS.listLogs.query.details.values]).default("full")).option("-n, --lines <count>", "Recent runs to print before watching", String(DEFAULT_BACKLOG)).option("--interval <seconds>", "Seconds between polls", String(DEFAULT_INTERVAL_SECONDS)).addHelpText("after", `
11744
+ Each run prints once, when it is first seen, so its status is the status it had
11745
+ at that moment. With --output json every run is a JSON object on its own line
11746
+ (JSONL) rather than a member of an array, because a follow never ends and so can
11747
+ never close one; --output yaml emits a --- separated document stream. Progress
11748
+ and retries go to stderr, leaving stdout a clean stream of rows. Ctrl-C stops the
11749
+ follow.
11750
+
11751
+ Examples:
11752
+ $ sim logs follow --level error
11753
+ $ sim logs follow --workflow wf_123 -n 0
11754
+ $ sim --output json logs follow | jq -r '.runId'
11755
+ `).action(async (options, command) => {
11756
+ const lines = nonNegativeInteger(options.lines, "--lines");
11757
+ const delay = intervalMs(options.interval);
11758
+ const { client, profile } = clientFrom(command);
11759
+ const path = V2_OPERATIONS.listLogs.path;
11760
+ const query = {
11761
+ workspaceId: client.requireWorkspace(),
11762
+ workflowIds: options.workflow?.length ? options.workflow.join(",") : undefined,
11763
+ folderPaths: options.folder?.length ? options.folder.map(encodeFolderPath).join(",") : undefined,
11764
+ triggers: options.trigger?.length ? options.trigger.join(",") : undefined,
11765
+ level: options.level,
11766
+ details: options.details,
11767
+ order: "desc"
11768
+ };
11769
+ const write = createWriter(profile.output);
11770
+ const status = followStatus();
11771
+ const interrupt = watchForInterrupt();
11772
+ const state = { seen: new Map, floor: null };
11773
+ try {
11774
+ const seed = await collectUnprinted(client, path, query, state, Math.max(lines, 1), 1);
11775
+ remember(state, seed.rows);
11776
+ state.floor = seed.rows.at(-1)?.startedAt ?? null;
11777
+ if (seed.truncated && seed.rows.length < lines) {
11778
+ status.warn(`asked for ${lines} earlier runs but a page holds ${seed.rows.length}; following from there — see sim logs list for more`);
11779
+ }
11780
+ write(lines > 0 ? seed.rows.slice(0, lines).reverse() : []);
11781
+ let failures = 0;
11782
+ while (!interrupt.interrupted()) {
11783
+ await waitFor(failures === 0 ? delay : Math.min(delay * 2 ** failures, MAX_BACKOFF_MS), interrupt.interrupted);
11784
+ if (interrupt.interrupted())
11785
+ break;
11786
+ let fresh;
11787
+ try {
11788
+ fresh = await collectUnprinted(client, path, query, state, POLL_PAGE_SIZE, MAX_PAGES_PER_POLL);
11789
+ } catch (error) {
11790
+ if (!isTransient(error))
11791
+ throw error;
11792
+ failures += 1;
11793
+ const next = Math.min(delay * 2 ** failures, MAX_BACKOFF_MS);
11794
+ status.note(`poll failed (${error.message}); retrying in ${inSeconds(next)}s…`);
11795
+ continue;
11796
+ }
11797
+ failures = 0;
11798
+ status.clear();
11799
+ if (fresh.truncated) {
11800
+ status.warn(`more than ${MAX_PAGES_PER_POLL * POLL_PAGE_SIZE} runs arrived at once; older ones were skipped — see sim logs list`);
11801
+ }
11802
+ if (fresh.rows.length === 0)
11803
+ continue;
11804
+ remember(state, fresh.rows);
11805
+ write(fresh.rows.reverse());
11806
+ }
11807
+ } finally {
11808
+ status.clear();
11809
+ interrupt.dispose();
11810
+ }
11811
+ });
11812
+ }
11813
+
11542
11814
  // src/runtime/options.ts
11543
11815
  var DEFAULT_LIMIT = 100;
11544
11816
  function describeField(flag, descriptor, name, field) {
@@ -11628,7 +11900,7 @@ function addOperationOptions(command, operation, commandSpec, operationSpec) {
11628
11900
  }
11629
11901
 
11630
11902
  // src/commands/protocol/resource-directory.ts
11631
- var COLUMNS = [
11903
+ var COLUMNS2 = [
11632
11904
  { header: "kind", value: (entry) => text(entry.kind) },
11633
11905
  { header: "name", value: (entry) => text(entry.name) },
11634
11906
  {
@@ -11694,7 +11966,7 @@ function attachResourceDirectoryCommands(group, config) {
11694
11966
  listResources(client, config, workspaceId, folderPath, options.search, limit)
11695
11967
  ]);
11696
11968
  const entries = entriesFor(config, folders, resources);
11697
- printList(profile.output, entries.slice(0, limit), COLUMNS);
11969
+ printList(profile.output, entries.slice(0, limit), COLUMNS2);
11698
11970
  });
11699
11971
  group.command("mkdir").argument("<path>", "Folder path to create; the leading / is optional").allowExcessArguments(false).description(`Create a ${config.kind} directory at a path`).action(async (path, _options, command) => {
11700
11972
  const { client, profile } = clientFrom(command);
@@ -11828,6 +12100,427 @@ function attachTableImport(tables) {
11828
12100
  });
11829
12101
  }
11830
12102
 
12103
+ // src/runtime/renamed.ts
12104
+ var warned = new Set;
12105
+ function warn(kind, from, to) {
12106
+ const key = `${kind}:${from}`;
12107
+ if (warned.has(key))
12108
+ return;
12109
+ warned.add(key);
12110
+ process.stderr.write(`warning: ${kind} "${from}" has been renamed to "${to}". The old name still works.
12111
+ `);
12112
+ }
12113
+ function warnRenamedCommand(from, to) {
12114
+ warn("command", `sim ${from}`, `sim ${to}`);
12115
+ }
12116
+ function warnRenamedFlag(from, to) {
12117
+ warn("flag", `--${from}`, `--${to}`);
12118
+ }
12119
+
12120
+ // src/runtime/execute.ts
12121
+ function cursorSlot(operationSpec) {
12122
+ if (operationSpec.query && "cursor" in operationSpec.query)
12123
+ return "query";
12124
+ if (operationSpec.body && "cursor" in operationSpec.body)
12125
+ return "body";
12126
+ return null;
12127
+ }
12128
+ function foldRenamedFlags(operation, commandSpec, flags) {
12129
+ for (const [field, flag] of Object.entries(commandSpec.flags ?? {})) {
12130
+ if (!flag.renamedFrom?.length)
12131
+ continue;
12132
+ const current = flagNameFor(operation, field);
12133
+ for (const previous of flag.renamedFrom) {
12134
+ const supplied = flags[camel(previous)];
12135
+ if (supplied === undefined)
12136
+ continue;
12137
+ if (flags[camel(current)] !== undefined) {
12138
+ throw new SimApiError(`--${previous} is the former name of --${current}; pass one, not both`, 0);
12139
+ }
12140
+ warnRenamedFlag(previous, current);
12141
+ flags[camel(current)] = supplied;
12142
+ }
12143
+ }
12144
+ }
12145
+ async function executeOperation(operation, commandSpec, operationSpec, invocation) {
12146
+ const host = invocation[invocation.length - 1];
12147
+ const inheritedFlags = host.optsWithGlobals();
12148
+ const flags = {
12149
+ ...inheritedFlags.workspace === undefined ? {} : { workspace: inheritedFlags.workspace },
12150
+ ...inheritedFlags.allWorkspaces === undefined ? {} : { allWorkspaces: inheritedFlags.allWorkspaces },
12151
+ ...invocation[invocation.length - 2]
12152
+ };
12153
+ const pathPositionalCount = operationSpec.pathParams.filter((param) => !commandSpec.pathFlags?.[param] && !isProfileWorkspacePath(commandSpec, param)).length;
12154
+ const positional = invocation.slice(0, pathPositionalCount);
12155
+ const requestFlags = { ...flags };
12156
+ for (const [index, field] of (commandSpec.positionals ?? []).entries()) {
12157
+ requestFlags[camel(flagNameFor(operation, field))] = invocation[pathPositionalCount + index];
12158
+ }
12159
+ foldRenamedFlags(operation, commandSpec, requestFlags);
12160
+ if (commandSpec.confirm && !requestFlags.yes) {
12161
+ throw new SimApiError(`${commandSpec.confirm} Re-run with --yes to confirm.`, 0);
12162
+ }
12163
+ if (commandSpec.allWorkspaces && requestFlags.allWorkspaces && requestFlags.workspace) {
12164
+ throw new SimApiError("--all-workspaces cannot be combined with --workspace", 0);
12165
+ }
12166
+ const { client, profile } = clientFrom(host);
12167
+ const hasWorkspaceField = Boolean(operationSpec.query && PROFILE_INJECTED_FIELD in operationSpec.query || operationSpec.body && PROFILE_INJECTED_FIELD in operationSpec.body);
12168
+ const omitsWorkspace = commandSpec.allWorkspaces && requestFlags.allWorkspaces === true;
12169
+ const request = buildRequest(operation, positional, requestFlags, hasWorkspaceField && !omitsWorkspace ? client.requireWorkspace() : profile.workspaceId);
12170
+ const paging = cursorSlot(operationSpec);
12171
+ if (paging) {
12172
+ const rawLimit = Number.parseInt(String(requestFlags.limit ?? DEFAULT_LIMIT), 10);
12173
+ if (Number.isNaN(rawLimit) || rawLimit < 0) {
12174
+ throw new SimApiError("--limit must be a non-negative number", 0);
12175
+ }
12176
+ const limit = rawLimit === 0 ? Number.POSITIVE_INFINITY : rawLimit;
12177
+ const pageSize = Math.min(Number.isFinite(limit) ? limit : DEFAULT_LIMIT, DEFAULT_LIMIT);
12178
+ const pageLimit = "limit" in (operationSpec[paging] ?? {}) ? { limit: pageSize } : {};
12179
+ const rows = [];
12180
+ const progress = pageProgress();
12181
+ let cursor = null;
12182
+ try {
12183
+ do {
12184
+ const page = await client.request(request.path, {
12185
+ method: operationSpec.method,
12186
+ query: paging === "query" ? { ...request.query, ...pageLimit, cursor } : request.query,
12187
+ body: paging === "body" ? { ...request.body ?? {}, ...pageLimit, ...cursor ? { cursor } : {} } : request.body
12188
+ });
12189
+ rows.push(...page.data);
12190
+ cursor = page.nextCursor;
12191
+ if (cursor && rows.length < limit)
12192
+ progress.advance(rows.length);
12193
+ } while (cursor && rows.length < limit);
12194
+ } finally {
12195
+ progress.finish();
12196
+ }
12197
+ renderPage(profile.output, Number.isFinite(limit) ? rows.slice(0, limit) : rows, commandSpec);
12198
+ return;
12199
+ }
12200
+ const result = await client.request(request.path, {
12201
+ method: operationSpec.method,
12202
+ query: request.query,
12203
+ body: request.body
12204
+ });
12205
+ renderResult(operation, profile.output, result?.data ?? result, commandSpec, {
12206
+ expandedTrace: requestFlags.trace === true
12207
+ });
12208
+ }
12209
+
12210
+ // src/commands/protocol/workflow-run-follow.ts
12211
+ var AGENT_STREAM_PROTOCOL_HEADER = "x-sim-stream-protocol";
12212
+ var AGENT_STREAM_PROTOCOL_V1 = "agent-events-v1";
12213
+ var DONE_SENTINEL = "[DONE]";
12214
+ function isRecord(value) {
12215
+ return typeof value === "object" && value !== null && !Array.isArray(value);
12216
+ }
12217
+ function stringField(frame, key) {
12218
+ const value = frame[key];
12219
+ return typeof value === "string" ? value : null;
12220
+ }
12221
+ async function* sseData(body) {
12222
+ const reader = body.getReader();
12223
+ const decoder = new TextDecoder;
12224
+ let buffer = "";
12225
+ try {
12226
+ while (true) {
12227
+ const { done, value } = await reader.read();
12228
+ buffer += done ? decoder.decode() : decoder.decode(value, { stream: true });
12229
+ const lines = buffer.split(`
12230
+ `);
12231
+ buffer = done ? "" : lines.pop() ?? "";
12232
+ for (const rawLine of lines) {
12233
+ const line = rawLine.endsWith("\r") ? rawLine.slice(0, -1) : rawLine;
12234
+ if (!line.startsWith("data:"))
12235
+ continue;
12236
+ const payload = line.slice(5).startsWith(" ") ? line.slice(6) : line.slice(5);
12237
+ if (payload.length > 0)
12238
+ yield payload;
12239
+ }
12240
+ if (done)
12241
+ return;
12242
+ }
12243
+ } finally {
12244
+ reader.releaseLock();
12245
+ }
12246
+ }
12247
+
12248
+ class Commentary {
12249
+ sink;
12250
+ atLineStart = true;
12251
+ constructor(sink) {
12252
+ this.sink = sink;
12253
+ }
12254
+ inline(text2) {
12255
+ if (text2.length === 0)
12256
+ return;
12257
+ this.sink.write(text2);
12258
+ this.atLineStart = text2.endsWith(`
12259
+ `);
12260
+ }
12261
+ line(text2) {
12262
+ this.sink.write(`${this.atLineStart ? "" : `
12263
+ `}${text2}
12264
+ `);
12265
+ this.atLineStart = true;
12266
+ }
12267
+ endLine() {
12268
+ if (this.atLineStart)
12269
+ return;
12270
+ this.sink.write(`
12271
+ `);
12272
+ this.atLineStart = true;
12273
+ }
12274
+ }
12275
+ function toolNotice(frame) {
12276
+ const name = safeOneLine(stringField(frame, "name") ?? "tool");
12277
+ if (frame.phase === "start")
12278
+ return source_default.dim(`→ ${name}`);
12279
+ const status = stringField(frame, "status");
12280
+ if (status && status !== "success")
12281
+ return source_default.yellow(`✗ ${name} (${safeOneLine(status)})`);
12282
+ return source_default.dim(`✓ ${name}`);
12283
+ }
12284
+ async function renderRunStream(body, options) {
12285
+ const commentary = new Commentary(options.stderr);
12286
+ let final = null;
12287
+ for await (const payload of sseData(body)) {
12288
+ let frame;
12289
+ try {
12290
+ frame = JSON.parse(payload);
12291
+ } catch {
12292
+ continue;
12293
+ }
12294
+ if (frame === DONE_SENTINEL)
12295
+ break;
12296
+ if (!isRecord(frame))
12297
+ continue;
12298
+ if (frame.event === undefined && typeof frame.chunk === "string") {
12299
+ commentary.inline(sanitize(frame.chunk));
12300
+ continue;
12301
+ }
12302
+ switch (frame.event) {
12303
+ case "chunk_reset":
12304
+ commentary.line(source_default.dim("… retracted; that turn resolved to tool calls"));
12305
+ break;
12306
+ case "thinking":
12307
+ if (options.includeThinking && typeof frame.data === "string") {
12308
+ commentary.inline(source_default.dim(sanitize(frame.data)));
12309
+ }
12310
+ break;
12311
+ case "tool":
12312
+ if (options.includeToolCalls)
12313
+ commentary.line(toolNotice(frame));
12314
+ break;
12315
+ case "stream_error":
12316
+ commentary.line(source_default.yellow(`warning: ${safeOneLine(stringField(frame, "error") ?? "stream read failed")}`));
12317
+ break;
12318
+ case "error":
12319
+ commentary.endLine();
12320
+ throw new SimApiError(safeOneLine(stringField(frame, "error") ?? "The workflow run failed."), 0);
12321
+ case "final":
12322
+ if (isRecord(frame.data))
12323
+ final = frame.data;
12324
+ break;
12325
+ default:
12326
+ break;
12327
+ }
12328
+ }
12329
+ commentary.endLine();
12330
+ if (!final) {
12331
+ throw new SimApiError("The run stream ended before the workflow reported a result. The run may still be in progress — check: sim workflows runs list", 0);
12332
+ }
12333
+ return final;
12334
+ }
12335
+ async function followRun(workflowId, command) {
12336
+ const flags = command.optsWithGlobals();
12337
+ if (flags.async === true) {
12338
+ throw new SimApiError("--follow streams a run as it happens and --async returns before it starts; pass one, not both", 0);
12339
+ }
12340
+ const includeThinking = flags.includeThinking === true;
12341
+ const includeToolCalls = flags.includeToolCalls === true;
12342
+ const negotiates = includeThinking || includeToolCalls;
12343
+ const { client, profile } = clientFrom(command);
12344
+ const operation = V2_OPERATIONS.executeWorkflow;
12345
+ const request = buildRequest("executeWorkflow", [workflowId], flags, profile.workspaceId);
12346
+ const response = await client.requestRaw(request.path, {
12347
+ method: "POST",
12348
+ query: request.query,
12349
+ body: {
12350
+ ...request.body ?? {},
12351
+ stream: true,
12352
+ ...includeThinking ? { includeThinking: true } : {},
12353
+ ...includeToolCalls ? { includeToolCalls: true } : {}
12354
+ },
12355
+ headers: {
12356
+ accept: "text/event-stream",
12357
+ ...negotiates ? { [AGENT_STREAM_PROTOCOL_HEADER]: AGENT_STREAM_PROTOCOL_V1 } : {}
12358
+ }
12359
+ });
12360
+ const contentType = response.headers.get("content-type") ?? "";
12361
+ if (!contentType.toLowerCase().includes("text/event-stream")) {
12362
+ await response.body?.cancel();
12363
+ throw new SimApiError(`${operation.path} answered ${contentType || "an unknown content type"} instead of an event stream. This deployment may predate streaming runs — re-run without --follow.`, response.status);
12364
+ }
12365
+ if (!response.body) {
12366
+ throw new SimApiError("The run stream had no body.", response.status);
12367
+ }
12368
+ const final = await renderRunStream(response.body, {
12369
+ includeThinking,
12370
+ includeToolCalls,
12371
+ stderr: process.stderr
12372
+ });
12373
+ renderResult("executeWorkflow", profile.output, final, CLI_CONTRACT.executeWorkflow ?? {});
12374
+ if (final.success === false) {
12375
+ throw new SimApiError(safeOneLine(typeof final.error === "string" ? final.error : "The workflow run failed."), 0);
12376
+ }
12377
+ }
12378
+ function followOrDelegate(previous) {
12379
+ return async (workflowId, _options, command) => {
12380
+ const flags = command.optsWithGlobals();
12381
+ if (flags.follow !== true) {
12382
+ if (flags.includeThinking === true || flags.includeToolCalls === true) {
12383
+ throw new SimApiError("--include-thinking and --include-tool-calls describe a stream; add --follow", 0);
12384
+ }
12385
+ if (previous) {
12386
+ await previous(command.processedArgs);
12387
+ return;
12388
+ }
12389
+ await executeOperation("executeWorkflow", CLI_CONTRACT.executeWorkflow ?? {}, V2_OPERATIONS.executeWorkflow, [workflowId, command.opts(), command]);
12390
+ return;
12391
+ }
12392
+ await followRun(workflowId, command);
12393
+ };
12394
+ }
12395
+ function attachWorkflowRunFollow(workflows) {
12396
+ const run = workflows.commands.find((command) => command.name() === "run");
12397
+ if (!run) {
12398
+ throw new Error("workflows run must be registered before --follow can be attached to it");
12399
+ }
12400
+ const held = run._actionHandler;
12401
+ const previous = typeof held === "function" ? held : null;
12402
+ run.option("--follow", "Stream the run as it happens; progress on stderr, result on stdout. The stream reports only success and output, so the result omits the run id and timings a non-streaming run returns").option("--include-thinking", "Show model reasoning while following (requires --follow)").option("--include-tool-calls", "Show tool calls while following (requires --follow)").action(followOrDelegate(previous));
12403
+ }
12404
+
12405
+ // src/commands/protocol/workflow-run-wait.ts
12406
+ var TERMINAL_STATUSES = new Set(["completed", "failed", "cancelled"]);
12407
+ var WAIT_EXIT_CODES = {
12408
+ completed: 0,
12409
+ failed: 1,
12410
+ cancelled: 2,
12411
+ paused: 3,
12412
+ timeout: 4
12413
+ };
12414
+ var FIRST_POLL_DELAY_MS = 2000;
12415
+ var MAX_POLL_DELAY_MS = 15000;
12416
+ var POLL_BACKOFF_FACTOR = 2;
12417
+ var DEFAULT_WAIT_TIMEOUT_SECONDS = 3600;
12418
+ var WAIT_TIMEOUT_FLAG = "--wait-timeout <seconds>";
12419
+ function isRecord2(value) {
12420
+ return typeof value === "object" && value !== null && !Array.isArray(value);
12421
+ }
12422
+ function optionalString(value) {
12423
+ return typeof value === "string" && value !== "" ? value : null;
12424
+ }
12425
+ function readRun(raw) {
12426
+ const run = isRecord2(raw) && isRecord2(raw.data) ? raw.data : raw;
12427
+ if (!isRecord2(run) || typeof run.status !== "string") {
12428
+ throw new SimApiError("Run status response carried no status.", 0);
12429
+ }
12430
+ const paused = isRecord2(run.paused) ? run.paused : null;
12431
+ return {
12432
+ status: run.status,
12433
+ pauseKind: paused ? optionalString(paused.pauseKind) : null,
12434
+ resumeAt: paused ? optionalString(paused.resumeAt) : null,
12435
+ contextId: paused ? optionalString(paused.contextId) : null
12436
+ };
12437
+ }
12438
+ function classify(snapshot) {
12439
+ if (snapshot.status === "paused")
12440
+ return snapshot.pauseKind === "time" ? null : "paused";
12441
+ if (!TERMINAL_STATUSES.has(snapshot.status))
12442
+ return null;
12443
+ return snapshot.status === "completed" ? "completed" : snapshot.status === "cancelled" ? "cancelled" : "failed";
12444
+ }
12445
+ function waitProgress() {
12446
+ let reported2 = false;
12447
+ return {
12448
+ advance: (status, elapsedMs) => {
12449
+ if (!process.stderr.isTTY)
12450
+ return;
12451
+ reported2 = true;
12452
+ process.stderr.write(`\r${source_default.dim(`${status} — waiting ${Math.round(elapsedMs / 1000)}s…`)}\x1B[K`);
12453
+ },
12454
+ finish: () => {
12455
+ if (!reported2)
12456
+ return;
12457
+ reported2 = false;
12458
+ process.stderr.write("\r\x1B[K");
12459
+ }
12460
+ };
12461
+ }
12462
+ function parseWaitTimeout(raw) {
12463
+ const seconds = Number(raw);
12464
+ if (!Number.isFinite(seconds) || seconds < 0) {
12465
+ throw new SimApiError(`Invalid ${WAIT_TIMEOUT_FLAG} "${raw}". Use a non-negative number of seconds, or 0 to wait indefinitely.`, 0);
12466
+ }
12467
+ return seconds;
12468
+ }
12469
+ function explain(outcome, runId, workflowId, snapshot) {
12470
+ if (outcome === "completed")
12471
+ return null;
12472
+ if (outcome === "failed")
12473
+ return `Run ${runId} failed.`;
12474
+ if (outcome === "cancelled")
12475
+ return `Run ${runId} was cancelled.`;
12476
+ const context = snapshot.contextId ? ` --context ${snapshot.contextId}` : "";
12477
+ return `Run ${runId} is paused waiting for input. Resume it: sim workflows runs resume ${runId} --workflow ${workflowId}${context}`;
12478
+ }
12479
+ function runSpec() {
12480
+ return CLI_CONTRACT.getWorkflowRun ?? {};
12481
+ }
12482
+ function attachWorkflowRunWait(runs) {
12483
+ runs.command("wait").argument("<runId>", V2_OPERATIONS.getWorkflowRun.pathParamDocs?.runId).description("Wait for a run to reach a terminal state, then show it").addOption(new Option("--workflow <workflowId>", "Workflow ID (required)").makeOptionMandatory()).addOption(new Option(WAIT_TIMEOUT_FLAG, `Give up after this many seconds, or 0 to wait indefinitely (default: ${DEFAULT_WAIT_TIMEOUT_SECONDS}). Bounds the whole wait; SIM_TIMEOUT_SECONDS bounds one request`)).action(async (runId, options, command) => {
12484
+ const timeoutSeconds = options.waitTimeout === undefined ? DEFAULT_WAIT_TIMEOUT_SECONDS : parseWaitTimeout(options.waitTimeout);
12485
+ const { client, profile } = clientFrom(command);
12486
+ const operation = V2_OPERATIONS.getWorkflowRun;
12487
+ const path = resolvePath(operation.path, { id: options.workflow, runId });
12488
+ const startedAt = Date.now();
12489
+ const deadline = timeoutSeconds === 0 ? Number.POSITIVE_INFINITY : startedAt + timeoutSeconds * 1000;
12490
+ const progress = waitProgress();
12491
+ let delayMs = FIRST_POLL_DELAY_MS;
12492
+ try {
12493
+ while (true) {
12494
+ const raw = await client.request(path, { method: operation.method });
12495
+ const snapshot = readRun(raw);
12496
+ const outcome = classify(snapshot);
12497
+ if (outcome) {
12498
+ progress.finish();
12499
+ renderResult("getWorkflowRun", profile.output, raw, runSpec());
12500
+ const message = explain(outcome, runId, options.workflow, snapshot);
12501
+ if (message)
12502
+ console.error(source_default.red(message));
12503
+ process.exitCode = WAIT_EXIT_CODES[outcome];
12504
+ return;
12505
+ }
12506
+ const remainingMs = deadline - Date.now();
12507
+ if (remainingMs <= 0) {
12508
+ progress.finish();
12509
+ renderResult("getWorkflowRun", profile.output, raw, runSpec());
12510
+ console.error(source_default.red(`Timed out after ${timeoutSeconds}s waiting for run ${runId} (status: ${snapshot.status}${snapshot.resumeAt ? `, resuming at ${snapshot.resumeAt}` : ""}). Raise ${WAIT_TIMEOUT_FLAG}, or set it to 0 to wait indefinitely.`));
12511
+ process.exitCode = WAIT_EXIT_CODES.timeout;
12512
+ return;
12513
+ }
12514
+ progress.advance(snapshot.status, Date.now() - startedAt);
12515
+ await sleep(Math.min(delayMs, remainingMs));
12516
+ delayMs = Math.min(delayMs * POLL_BACKOFF_FACTOR, MAX_POLL_DELAY_MS);
12517
+ }
12518
+ } finally {
12519
+ progress.finish();
12520
+ }
12521
+ });
12522
+ }
12523
+
11831
12524
  // src/commands/protocol/index.ts
11832
12525
  function group(program2, name) {
11833
12526
  const existing = program2.commands.find((command) => command.name() === name);
@@ -11863,12 +12556,16 @@ function attachProtocolCommands(program2) {
11863
12556
  folders: "listTableFolders",
11864
12557
  createFolder: "createTableFolder"
11865
12558
  });
11866
- attachResourceDirectoryCommands(group(program2, "workflows"), {
12559
+ const workflows = group(program2, "workflows");
12560
+ attachResourceDirectoryCommands(workflows, {
11867
12561
  kind: "workflow",
11868
12562
  resources: "listWorkflows",
11869
12563
  folders: "listWorkflowFolders",
11870
12564
  createFolder: "createWorkflowFolder"
11871
12565
  });
12566
+ attachWorkflowRunFollow(workflows);
12567
+ attachWorkflowRunWait(group(workflows, "runs"));
12568
+ attachLogsFollow(group(program2, "logs"));
11872
12569
  }
11873
12570
 
11874
12571
  // src/terminal/secret-input.ts
@@ -11992,113 +12689,6 @@ function attachSecretCommands(program2) {
11992
12689
  secrets.command("set").argument("<name>", "Secret name, as referenced in workflows").description("Create or replace a named secret").addOption(new Option("--scope <scope>", "Secret ownership scope").choices([...SECRET_SCOPES]).makeOptionMandatory()).option("--value <value>", "Secret value; visible to shell history when supplied directly").option("--description <description>", "What the secret is for, shown to teammates; workspace scope only. Omit to leave an existing description unchanged").action((name, options, command) => setSecret(name, options, command));
11993
12690
  }
11994
12691
 
11995
- // src/runtime/renamed.ts
11996
- var warned = new Set;
11997
- function warn(kind, from, to) {
11998
- const key = `${kind}:${from}`;
11999
- if (warned.has(key))
12000
- return;
12001
- warned.add(key);
12002
- process.stderr.write(`warning: ${kind} "${from}" has been renamed to "${to}". The old name still works.
12003
- `);
12004
- }
12005
- function warnRenamedCommand(from, to) {
12006
- warn("command", `sim ${from}`, `sim ${to}`);
12007
- }
12008
- function warnRenamedFlag(from, to) {
12009
- warn("flag", `--${from}`, `--${to}`);
12010
- }
12011
-
12012
- // src/runtime/execute.ts
12013
- function cursorSlot(operationSpec) {
12014
- if (operationSpec.query && "cursor" in operationSpec.query)
12015
- return "query";
12016
- if (operationSpec.body && "cursor" in operationSpec.body)
12017
- return "body";
12018
- return null;
12019
- }
12020
- function foldRenamedFlags(operation, commandSpec, flags) {
12021
- for (const [field, flag] of Object.entries(commandSpec.flags ?? {})) {
12022
- if (!flag.renamedFrom?.length)
12023
- continue;
12024
- const current = flagNameFor(operation, field);
12025
- for (const previous of flag.renamedFrom) {
12026
- const supplied = flags[camel(previous)];
12027
- if (supplied === undefined)
12028
- continue;
12029
- if (flags[camel(current)] !== undefined) {
12030
- throw new SimApiError(`--${previous} is the former name of --${current}; pass one, not both`, 0);
12031
- }
12032
- warnRenamedFlag(previous, current);
12033
- flags[camel(current)] = supplied;
12034
- }
12035
- }
12036
- }
12037
- async function executeOperation(operation, commandSpec, operationSpec, invocation) {
12038
- const host = invocation[invocation.length - 1];
12039
- const inheritedFlags = host.optsWithGlobals();
12040
- const flags = {
12041
- ...inheritedFlags.workspace === undefined ? {} : { workspace: inheritedFlags.workspace },
12042
- ...inheritedFlags.allWorkspaces === undefined ? {} : { allWorkspaces: inheritedFlags.allWorkspaces },
12043
- ...invocation[invocation.length - 2]
12044
- };
12045
- const pathPositionalCount = operationSpec.pathParams.filter((param) => !commandSpec.pathFlags?.[param] && !isProfileWorkspacePath(commandSpec, param)).length;
12046
- const positional = invocation.slice(0, pathPositionalCount);
12047
- const requestFlags = { ...flags };
12048
- for (const [index, field] of (commandSpec.positionals ?? []).entries()) {
12049
- requestFlags[camel(flagNameFor(operation, field))] = invocation[pathPositionalCount + index];
12050
- }
12051
- foldRenamedFlags(operation, commandSpec, requestFlags);
12052
- if (commandSpec.confirm && !requestFlags.yes) {
12053
- throw new SimApiError(`${commandSpec.confirm} Re-run with --yes to confirm.`, 0);
12054
- }
12055
- if (commandSpec.allWorkspaces && requestFlags.allWorkspaces && requestFlags.workspace) {
12056
- throw new SimApiError("--all-workspaces cannot be combined with --workspace", 0);
12057
- }
12058
- const { client, profile } = clientFrom(host);
12059
- const hasWorkspaceField = Boolean(operationSpec.query && PROFILE_INJECTED_FIELD in operationSpec.query || operationSpec.body && PROFILE_INJECTED_FIELD in operationSpec.body);
12060
- const omitsWorkspace = commandSpec.allWorkspaces && requestFlags.allWorkspaces === true;
12061
- const request = buildRequest(operation, positional, requestFlags, hasWorkspaceField && !omitsWorkspace ? client.requireWorkspace() : profile.workspaceId);
12062
- const paging = cursorSlot(operationSpec);
12063
- if (paging) {
12064
- const rawLimit = Number.parseInt(String(requestFlags.limit ?? DEFAULT_LIMIT), 10);
12065
- if (Number.isNaN(rawLimit) || rawLimit < 0) {
12066
- throw new SimApiError("--limit must be a non-negative number", 0);
12067
- }
12068
- const limit = rawLimit === 0 ? Number.POSITIVE_INFINITY : rawLimit;
12069
- const pageSize = Math.min(Number.isFinite(limit) ? limit : DEFAULT_LIMIT, DEFAULT_LIMIT);
12070
- const pageLimit = "limit" in (operationSpec[paging] ?? {}) ? { limit: pageSize } : {};
12071
- const rows = [];
12072
- const progress = pageProgress();
12073
- let cursor = null;
12074
- try {
12075
- do {
12076
- const page = await client.request(request.path, {
12077
- method: operationSpec.method,
12078
- query: paging === "query" ? { ...request.query, ...pageLimit, cursor } : request.query,
12079
- body: paging === "body" ? { ...request.body ?? {}, ...pageLimit, ...cursor ? { cursor } : {} } : request.body
12080
- });
12081
- rows.push(...page.data);
12082
- cursor = page.nextCursor;
12083
- if (cursor && rows.length < limit)
12084
- progress.advance(rows.length);
12085
- } while (cursor && rows.length < limit);
12086
- } finally {
12087
- progress.finish();
12088
- }
12089
- renderPage(profile.output, Number.isFinite(limit) ? rows.slice(0, limit) : rows, commandSpec);
12090
- return;
12091
- }
12092
- const result = await client.request(request.path, {
12093
- method: operationSpec.method,
12094
- query: request.query,
12095
- body: request.body
12096
- });
12097
- renderResult(operation, profile.output, result?.data ?? result, commandSpec, {
12098
- expandedTrace: requestFlags.trace === true
12099
- });
12100
- }
12101
-
12102
12692
  // src/runtime/build.ts
12103
12693
  var GROUP_ALIASES = {
12104
12694
  "audit-logs": "audit-log",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sim",
3
- "version": "2.0.0-preview.18.1",
3
+ "version": "2.0.0-preview.21.1",
4
4
  "description": "Sim CLI - talk to the Sim API from your terminal",
5
5
  "type": "module",
6
6
  "bin": {