sim 2.0.0 → 2.1.0-dev.24.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 (3) hide show
  1. package/README.md +18 -4
  2. package/dist/index.js +1651 -522
  3. package/package.json +2 -2
package/dist/index.js CHANGED
@@ -2403,7 +2403,7 @@ function removeSection(doc, name) {
2403
2403
 
2404
2404
  // src/config/profile.ts
2405
2405
  var DEFAULT_PROFILE = "default";
2406
- var DEFAULT_ENDPOINT = "https://sim.ai";
2406
+ var DEFAULT_ENDPOINT = "https://www.sim.ai";
2407
2407
  var OUTPUT_FORMATS = ["table", "json", "yaml", "text"];
2408
2408
 
2409
2409
  class ProfileConfigError extends Error {
@@ -2466,8 +2466,18 @@ function deleteProfile(profile) {
2466
2466
  writeIni(credentialsPath(), credentialsDoc, true);
2467
2467
  return { config, credentials };
2468
2468
  }
2469
- function normalizeEndpoint(endpoint) {
2470
- return endpoint.replace(/\/+$/, "");
2469
+ function normalizeEndpoint(endpoint, source) {
2470
+ const trimmed = endpoint.replace(/\/+$/, "");
2471
+ let parsed;
2472
+ try {
2473
+ parsed = new URL(trimmed);
2474
+ } catch {
2475
+ throw new ProfileConfigError(`Invalid endpoint "${endpoint}" from ${source}. Use an absolute URL, e.g. ${DEFAULT_ENDPOINT} or http://localhost:3000`);
2476
+ }
2477
+ if (parsed.protocol !== "http:" && parsed.protocol !== "https:") {
2478
+ throw new ProfileConfigError(`Unsupported endpoint scheme "${parsed.protocol.replace(/:$/, "")}" from ${source}. Use http or https, e.g. ${DEFAULT_ENDPOINT}`);
2479
+ }
2480
+ return trimmed;
2471
2481
  }
2472
2482
  function resolve(candidates, fallback, fallbackSource) {
2473
2483
  for (const [source, value] of candidates) {
@@ -2505,7 +2515,7 @@ function resolveProfile(overrides = {}) {
2505
2515
  }
2506
2516
  return {
2507
2517
  name,
2508
- endpoint: normalizeEndpoint(endpoint.value),
2518
+ endpoint: normalizeEndpoint(endpoint.value, endpoint.source),
2509
2519
  apiKey: apiKey.value,
2510
2520
  workspaceId: workspaceId.value,
2511
2521
  output: output.value,
@@ -2517,6 +2527,65 @@ function resolveProfile(overrides = {}) {
2517
2527
  }
2518
2528
  };
2519
2529
  }
2530
+ // src/version.ts
2531
+ import { readFileSync as readFileSync2 } from "node:fs";
2532
+ function readPackageVersion() {
2533
+ const metadata = JSON.parse(readFileSync2(new URL("../package.json", import.meta.url), "utf8"));
2534
+ if (typeof metadata !== "object" || metadata === null || !("version" in metadata) || typeof metadata.version !== "string") {
2535
+ throw new Error("CLI package metadata is missing a valid version");
2536
+ }
2537
+ return metadata.version;
2538
+ }
2539
+ var CLI_VERSION = readPackageVersion();
2540
+ var USER_AGENT = `sim-cli/${CLI_VERSION} node/${process.versions.node} (${process.platform}; ${process.arch})`;
2541
+
2542
+ // src/http/environment.ts
2543
+ var reported = new Set;
2544
+ function once(key, message) {
2545
+ if (reported.has(key))
2546
+ return;
2547
+ reported.add(key);
2548
+ process.stderr.write(`warning: ${message}
2549
+ `);
2550
+ }
2551
+ var PROXY_VARIABLES = ["HTTPS_PROXY", "https_proxy", "HTTP_PROXY", "http_proxy"];
2552
+ var PROXY_SUPPORT = { 22: 21, 24: 5 };
2553
+ var FIRST_SUPPORTED_MAJOR = 24;
2554
+ function runtimeCanProxy(version) {
2555
+ const [major, minor] = version.replace(/^v/, "").split(".").map(Number);
2556
+ if (!Number.isFinite(major) || !Number.isFinite(minor))
2557
+ return false;
2558
+ const firstSupportedMinor = PROXY_SUPPORT[major];
2559
+ if (firstSupportedMinor !== undefined)
2560
+ return minor >= firstSupportedMinor;
2561
+ return major > FIRST_SUPPORTED_MAJOR;
2562
+ }
2563
+ function warnIfProxyIgnored(env2 = process.env, version = process.version) {
2564
+ const variable = PROXY_VARIABLES.find((name) => env2[name]);
2565
+ if (!variable)
2566
+ return;
2567
+ if (env2.NODE_USE_ENV_PROXY && runtimeCanProxy(version))
2568
+ return;
2569
+ once("proxy", runtimeCanProxy(version) ? `${variable} is set but Node only uses it when NODE_USE_ENV_PROXY=1. Re-run with NODE_USE_ENV_PROXY=1 to route through the proxy.` : `${variable} is set but Node ${version} cannot use it. Upgrade to Node 22.21 or 24.5 and set NODE_USE_ENV_PROXY=1 to route through the proxy.`);
2570
+ }
2571
+ var LOOPBACK = new Set(["localhost", "127.0.0.1", "[::1]", "::1", "0.0.0.0"]);
2572
+ function isLoopback(hostname) {
2573
+ return LOOPBACK.has(hostname) || hostname.endsWith(".localhost");
2574
+ }
2575
+ function warnIfKeyOverCleartext(endpoint, hasApiKey) {
2576
+ if (!hasApiKey)
2577
+ return;
2578
+ let url;
2579
+ try {
2580
+ url = new URL(endpoint);
2581
+ } catch {
2582
+ return;
2583
+ }
2584
+ if (url.protocol !== "http:" || isLoopback(url.hostname))
2585
+ return;
2586
+ once("cleartext", `sending your API key to ${url.host} over http. Anything on the path can read it — use https unless this network is trusted.`);
2587
+ }
2588
+
2520
2589
  // src/http/client.ts
2521
2590
  class SimApiError extends Error {
2522
2591
  status;
@@ -2539,13 +2608,28 @@ function buildUrl(endpoint, path, query) {
2539
2608
  }
2540
2609
  return url.toString();
2541
2610
  }
2542
- function toApiError(status, raw) {
2611
+ var REDIRECT_STATUSES = new Set([301, 302, 303, 307, 308]);
2612
+ var MARKUP_PREFIX = /^\s*<(?:!doctype|html|\?xml)/i;
2613
+ var KEY_SCOPE_REFUSALS = new Set([
2614
+ "WORKSPACE_KEY_OPERATION_NOT_PERMITTED",
2615
+ "PRINCIPAL_KIND_NOT_PERMITTED"
2616
+ ]);
2617
+ function toNonJsonError(url, status, contentType, raw) {
2618
+ const type = contentType?.split(";")[0]?.trim().toLowerCase();
2619
+ const isMarkup = type === "text/html" || type === "application/xhtml+xml" || MARKUP_PREFIX.test(raw);
2620
+ const kind = isMarkup ? "HTML" : type && type !== "application/json" ? type : "a non-JSON response";
2621
+ const text = raw.trim();
2622
+ const keepSnippet = !isMarkup && text.length > 0 && text.length <= 200;
2623
+ return new SimApiError(`${url} returned ${kind}, not JSON (HTTP ${status}) — check your endpoint.${keepSnippet ? ` Response: ${truncate(text, 200)}` : ""}`, status);
2624
+ }
2625
+ function toApiError(url, status, contentType, raw) {
2543
2626
  let parsed;
2544
2627
  try {
2545
2628
  parsed = JSON.parse(raw);
2546
2629
  } catch {
2547
- const text = raw.trim();
2548
- return new SimApiError(text ? truncate(text, 300) : `Request failed with status ${status}`, status);
2630
+ if (!raw.trim())
2631
+ return new SimApiError(`Request failed with status ${status}`, status);
2632
+ return toNonJsonError(url, status, contentType, raw);
2549
2633
  }
2550
2634
  const body = parsed;
2551
2635
  if (body.error && typeof body.error === "object") {
@@ -2561,8 +2645,79 @@ function toApiError(status, raw) {
2561
2645
  function truncate(value, max) {
2562
2646
  return value.length <= max ? value : `${value.slice(0, max)}…`;
2563
2647
  }
2648
+ function namesKeyScopeRefusal(error) {
2649
+ if (typeof error.code === "string" && KEY_SCOPE_REFUSALS.has(error.code))
2650
+ return true;
2651
+ const details = error.details;
2652
+ if (!details || typeof details !== "object")
2653
+ return false;
2654
+ const code = details.code;
2655
+ return typeof code === "string" && KEY_SCOPE_REFUSALS.has(code);
2656
+ }
2657
+ function isStrictPrefix(path, other) {
2658
+ if (path.length >= other.length)
2659
+ return false;
2660
+ return path.every((segment, index) => segment === other[index]);
2661
+ }
2662
+ function rejectsAKeyThatValidated(issue, other) {
2663
+ if (!issue.unrecognizedKeys || !isStrictPrefix(issue.path, other.path))
2664
+ return false;
2665
+ return issue.unrecognizedKeys.includes(other.path[issue.path.length]);
2666
+ }
2667
+ function dropUnionBranchNoise(issues) {
2668
+ if (issues.length < 2)
2669
+ return issues;
2670
+ const kept = issues.filter((issue) => !issues.some((other) => rejectsAKeyThatValidated(issue, other)));
2671
+ return kept.length > 0 ? kept : issues;
2672
+ }
2673
+ var DEFAULT_TIMEOUT_SECONDS = 3600;
2674
+ var MAX_TIMEOUT_MS = 2 ** 31 - 1;
2675
+ var RAISE_TIMEOUT_HINT = "Raise SIM_TIMEOUT_SECONDS, or set it to 0 to wait indefinitely.";
2676
+ function isRequestTimeout(error) {
2677
+ return error instanceof DOMException && error.name === "TimeoutError";
2678
+ }
2679
+ function resolveTimeoutMs(env2 = process.env) {
2680
+ const raw = env2.SIM_TIMEOUT_SECONDS;
2681
+ if (raw === undefined || raw.trim() === "")
2682
+ return DEFAULT_TIMEOUT_SECONDS * 1000;
2683
+ const seconds = Number(raw);
2684
+ if (!Number.isFinite(seconds) || seconds < 0) {
2685
+ throw new SimApiError(`Invalid SIM_TIMEOUT_SECONDS "${raw}". Use a non-negative number of seconds, or 0 to disable.`, 0);
2686
+ }
2687
+ const ms = seconds === 0 ? 0 : Math.max(1, Math.round(seconds * 1000));
2688
+ if (ms > MAX_TIMEOUT_MS) {
2689
+ throw new SimApiError(`SIM_TIMEOUT_SECONDS "${raw}" is longer than Node can wait (${Math.floor(MAX_TIMEOUT_MS / 1000)}s). Use 0 to wait indefinitely.`, 0);
2690
+ }
2691
+ return ms;
2692
+ }
2693
+ function combineSignals(caller, timeout) {
2694
+ if (!caller)
2695
+ return timeout;
2696
+ if (!timeout)
2697
+ return caller;
2698
+ if (typeof AbortSignal.any === "function")
2699
+ return AbortSignal.any([caller, timeout]);
2700
+ const controller = new AbortController;
2701
+ for (const signal of [caller, timeout]) {
2702
+ if (signal.aborted) {
2703
+ controller.abort(signal.reason);
2704
+ break;
2705
+ }
2706
+ signal.addEventListener("abort", () => controller.abort(signal.reason), { once: true });
2707
+ }
2708
+ return controller.signal;
2709
+ }
2710
+ function debugEnabled(env2 = process.env) {
2711
+ const raw = env2.SIM_DEBUG;
2712
+ return raw !== undefined && raw !== "" && raw !== "0" && raw.toLowerCase() !== "false";
2713
+ }
2714
+ function traceRequest(method, url, status, startedAt) {
2715
+ process.stderr.write(`${source_default.dim(`[sim] ${method} ${url} → ${status} ${Math.round(performance.now() - startedAt)}ms`)}
2716
+ `);
2717
+ }
2564
2718
  function formatApiErrorDetails(details) {
2565
- const issues = new Set;
2719
+ const issues = [];
2720
+ const seen = new Set;
2566
2721
  const visit = (value, parentPath = []) => {
2567
2722
  if (Array.isArray(value)) {
2568
2723
  value.forEach((item) => visit(item, parentPath));
@@ -2580,15 +2735,27 @@ function formatApiErrorDetails(details) {
2580
2735
  }
2581
2736
  if (typeof issue.message !== "string" || issue.message === "Invalid input")
2582
2737
  return;
2583
- issues.add(`${path.length > 0 ? path.join(".") : "request"}: ${issue.message}`);
2738
+ const line = `${path.join(".")}: ${issue.message}`;
2739
+ if (seen.has(line))
2740
+ return;
2741
+ seen.add(line);
2742
+ issues.push({
2743
+ path,
2744
+ message: issue.message,
2745
+ unrecognizedKeys: issue.code === "unrecognized_keys" && Array.isArray(issue.keys) ? issue.keys.map(String) : null
2746
+ });
2584
2747
  };
2585
2748
  visit(details);
2586
- if (issues.size === 0)
2749
+ if (issues.length === 0)
2587
2750
  return [` details: ${truncate(JSON.stringify(details), 1000)}`];
2588
- const visible = [...issues].slice(0, 8);
2589
- const lines = [" details:", ...visible.map((issue) => ` ${issue}`)];
2590
- if (issues.size > visible.length)
2591
- lines.push(` … ${issues.size - visible.length} more issues`);
2751
+ const kept = dropUnionBranchNoise(issues);
2752
+ const visible = kept.slice(0, 8);
2753
+ const lines = [
2754
+ " details:",
2755
+ ...visible.map((issue) => ` ${issue.path.length > 0 ? issue.path.join(".") : "request"}: ${issue.message}`)
2756
+ ];
2757
+ if (kept.length > visible.length)
2758
+ lines.push(` … ${kept.length - visible.length} more issues`);
2592
2759
  return lines;
2593
2760
  }
2594
2761
 
@@ -2614,65 +2781,140 @@ class SimClient {
2614
2781
  return workspaceId;
2615
2782
  }
2616
2783
  async requestRaw(path, options = {}) {
2784
+ return (await this.send(path, options)).response;
2785
+ }
2786
+ async request(path, options = {}) {
2787
+ const { response, url } = await this.send(path, options);
2788
+ const raw = await response.text();
2789
+ if (!raw)
2790
+ return;
2791
+ try {
2792
+ return JSON.parse(raw);
2793
+ } catch {
2794
+ throw toNonJsonError(url, response.status, response.headers.get("content-type"), raw);
2795
+ }
2796
+ }
2797
+ async send(path, options) {
2617
2798
  const apiKey = this.resolveApiKey(options.auth);
2618
2799
  const url = buildUrl(this.profile.endpoint, path, options.query);
2619
2800
  const hasBody = options.body !== undefined;
2801
+ const method = options.method ?? "GET";
2802
+ warnIfProxyIgnored();
2803
+ warnIfKeyOverCleartext(this.profile.endpoint, Boolean(apiKey));
2804
+ const timeoutMs = resolveTimeoutMs();
2805
+ const timeout = timeoutMs > 0 ? AbortSignal.timeout(timeoutMs) : undefined;
2806
+ const signal = combineSignals(options.signal, timeout);
2807
+ const trace = debugEnabled();
2808
+ const startedAt = performance.now();
2620
2809
  let response;
2621
2810
  try {
2622
2811
  response = await fetch(url, {
2623
- method: options.method ?? "GET",
2812
+ method,
2624
2813
  headers: {
2625
2814
  ...apiKey ? { "x-api-key": apiKey } : {},
2626
2815
  accept: "application/json",
2816
+ "user-agent": USER_AGENT,
2627
2817
  ...hasBody ? { "content-type": "application/json" } : {},
2628
2818
  ...options.headers
2629
2819
  },
2630
2820
  body: hasBody ? JSON.stringify(options.body) : undefined,
2631
- signal: options.signal
2821
+ signal,
2822
+ redirect: "manual"
2632
2823
  });
2633
2824
  } catch (cause) {
2825
+ if (trace)
2826
+ traceRequest(method, url, "failed", startedAt);
2634
2827
  if (options.signal?.aborted) {
2635
2828
  throw new SimApiError("Request cancelled.", 0);
2636
2829
  }
2830
+ if (timeout?.aborted) {
2831
+ throw new SimApiError(`${url} did not answer within ${timeoutMs / 1000}s. ${RAISE_TIMEOUT_HINT}`, 0);
2832
+ }
2637
2833
  throw new SimApiError(`Could not reach ${this.profile.endpoint}: ${cause.message}`, 0);
2638
2834
  }
2835
+ if (trace)
2836
+ traceRequest(method, url, response.status, startedAt);
2837
+ if (REDIRECT_STATUSES.has(response.status))
2838
+ throw this.toRedirectError(url, path, response);
2639
2839
  if (!response.ok) {
2640
2840
  const raw = await response.text();
2641
- const error = toApiError(response.status, raw);
2841
+ const error = toApiError(url, response.status, response.headers.get("content-type"), raw);
2642
2842
  if (response.status === 401) {
2643
2843
  error.message = `${error.message} — run: sim login --profile ${this.profile.name}`;
2644
2844
  }
2845
+ if (namesKeyScopeRefusal(error)) {
2846
+ error.message = `${error.message} — this operation needs a personal API key: sim login --profile ${this.profile.name}`;
2847
+ }
2645
2848
  throw error;
2646
2849
  }
2647
- return response;
2850
+ return { response, url };
2648
2851
  }
2649
- async request(path, options = {}) {
2650
- const response = await this.requestRaw(path, options);
2651
- const raw = await response.text();
2652
- if (!raw)
2653
- return;
2654
- return JSON.parse(raw);
2852
+ toRedirectError(url, path, response) {
2853
+ const location = response.headers.get("location")?.trim();
2854
+ let target = null;
2855
+ if (location) {
2856
+ try {
2857
+ target = new URL(location, url);
2858
+ } catch {
2859
+ target = null;
2860
+ }
2861
+ }
2862
+ if (!target) {
2863
+ return new SimApiError(`${url} answered HTTP ${response.status} with no usable redirect target. Check the endpoint for profile "${this.profile.name}".`, response.status);
2864
+ }
2865
+ const suggested = redirectEndpoint(this.profile.endpoint, path, target);
2866
+ if (!suggested) {
2867
+ return new SimApiError(`${url} redirected to ${target.href}. The CLI does not follow redirects, because a redirect can drop the request body and turn a write into a silent no-op.`, response.status);
2868
+ }
2869
+ return new SimApiError(`Endpoint redirected to ${suggested}. Run: sim configure --profile ${this.profile.name} --set-endpoint ${suggested}`, response.status);
2655
2870
  }
2656
2871
  }
2872
+ function redirectEndpoint(endpoint, requestPath, target) {
2873
+ const prefix = target.pathname.endsWith(requestPath) ? target.pathname.slice(0, target.pathname.length - requestPath.length) : "";
2874
+ const suggested = `${target.origin}${prefix}`.replace(/\/+$/, "");
2875
+ return suggested === endpoint.replace(/\/+$/, "") ? null : suggested;
2876
+ }
2877
+ function pageProgress() {
2878
+ let reported2 = false;
2879
+ return {
2880
+ advance: (fetched) => {
2881
+ if (!process.stderr.isTTY)
2882
+ return;
2883
+ reported2 = true;
2884
+ process.stderr.write(`\r${source_default.dim(`fetched ${fetched}…`)}\x1B[K`);
2885
+ },
2886
+ finish: () => {
2887
+ if (reported2)
2888
+ process.stderr.write("\r\x1B[K");
2889
+ }
2890
+ };
2891
+ }
2657
2892
  async function requestAllPages(client, path, options) {
2658
2893
  const { query, pageSize, limit: requestedLimit, ...requestOptions } = options;
2659
2894
  const limit = requestedLimit ?? Number.POSITIVE_INFINITY;
2660
2895
  if (limit <= 0)
2661
2896
  return [];
2662
2897
  const items = [];
2898
+ const progress = pageProgress();
2663
2899
  let cursor = null;
2664
- do {
2665
- const page = await client.request(path, {
2666
- ...requestOptions,
2667
- query: {
2668
- ...query,
2669
- limit: Math.min(pageSize, limit - items.length),
2670
- cursor
2671
- }
2672
- });
2673
- items.push(...page.data);
2674
- cursor = page.nextCursor;
2675
- } while (cursor && items.length < limit);
2900
+ try {
2901
+ do {
2902
+ const page = await client.request(path, {
2903
+ ...requestOptions,
2904
+ query: {
2905
+ ...query,
2906
+ limit: Math.min(pageSize, limit - items.length),
2907
+ cursor
2908
+ }
2909
+ });
2910
+ items.push(...page.data);
2911
+ cursor = page.nextCursor;
2912
+ if (cursor && items.length < limit)
2913
+ progress.advance(items.length);
2914
+ } while (cursor && items.length < limit);
2915
+ } finally {
2916
+ progress.finish();
2917
+ }
2676
2918
  return items.slice(0, limit);
2677
2919
  }
2678
2920
  function resolvePath(template, params = {}) {
@@ -5894,7 +6136,7 @@ function duration(ms) {
5894
6136
  if (ms === null || ms === undefined)
5895
6137
  return EMPTY;
5896
6138
  if (ms < 1000)
5897
- return `${ms}ms`;
6139
+ return `${Math.round(ms)}ms`;
5898
6140
  if (ms < 60000)
5899
6141
  return `${(ms / 1000).toFixed(1)}s`;
5900
6142
  return `${Math.floor(ms / 60000)}m${Math.round(ms % 60000 / 1000)}s`;
@@ -5914,17 +6156,18 @@ function oneLine(value) {
5914
6156
  return value.replace(/\s*[\r\n\t]+\s*/g, " ");
5915
6157
  }
5916
6158
  var MAX_CELL_WIDTH = 60;
5917
- function clampCell(value) {
5918
- if (visibleWidth(value) <= MAX_CELL_WIDTH || value !== value.replace(ANSI_PATTERN, "")) {
6159
+ var MAX_RECORD_WIDTH = 160;
6160
+ function clamp(value, width) {
6161
+ if (visibleWidth(value) <= width || value !== value.replace(ANSI_PATTERN, "")) {
5919
6162
  return value;
5920
6163
  }
5921
- return `${value.slice(0, MAX_CELL_WIDTH - 1)}…`;
6164
+ return `${value.slice(0, width - 1)}…`;
5922
6165
  }
5923
6166
  function renderTable(rows, columns) {
5924
6167
  if (rows.length === 0)
5925
6168
  return source_default.dim("No results.");
5926
6169
  const headers = columns.map((column) => sanitize(column.header));
5927
- const cells = rows.map((row) => columns.map((column) => clampCell(oneLine(column.value(row)))));
6170
+ const cells = rows.map((row) => columns.map((column) => clamp(oneLine(column.value(row)), MAX_CELL_WIDTH)));
5928
6171
  const widths = columns.map((_column, index) => Math.max(visibleWidth(headers[index]), ...cells.map((line) => visibleWidth(line[index]))));
5929
6172
  const header = headers.map((label, index) => source_default.dim(pad(label.toUpperCase(), widths[index]))).join(" ").trimEnd();
5930
6173
  const body = cells.map((line) => line.map((cell, index) => pad(cell, widths[index])).join(" ").trimEnd());
@@ -5970,13 +6213,10 @@ function printRecord(format, fields, raw) {
5970
6213
  }
5971
6214
  const width = Math.max(...safeFields.map(([label]) => visibleWidth(label)));
5972
6215
  for (const [label, value] of safeFields) {
5973
- console.log(`${source_default.dim(pad(`${label}:`, width + 1))} ${oneLine(value)}`);
6216
+ console.log(`${source_default.dim(pad(`${label}:`, width + 1))} ${clamp(oneLine(value), MAX_RECORD_WIDTH)}`);
5974
6217
  }
5975
6218
  }
5976
6219
 
5977
- // src/program.ts
5978
- import { readFileSync as readFileSync3 } from "node:fs";
5979
-
5980
6220
  // ../../node_modules/commander/esm.mjs
5981
6221
  var import__ = __toESM(require_commander(), 1);
5982
6222
  var {
@@ -6009,6 +6249,8 @@ function sleep(ms) {
6009
6249
  var PAIRING_ALPHABET = "ABCDEFGHJKLMNPQRSTUVWXYZ23456789";
6010
6250
  var POLL_INTERVAL_MS = 2000;
6011
6251
  var POLL_TIMEOUT_MS = 15 * 60 * 1000;
6252
+ var APPROVAL_PATH = "/cli/auth";
6253
+ var POLL_PATH = "/api/cli/auth/poll";
6012
6254
  var RETRYABLE_POLL_STATUSES = new Set([409, 429, 500, 502, 503, 504]);
6013
6255
  function token() {
6014
6256
  return randomBytes(32).toString("base64url");
@@ -6027,14 +6269,30 @@ function createAuthRequest() {
6027
6269
  };
6028
6270
  }
6029
6271
  function buildApprovalUrl(endpoint, auth, scope, workspaceId) {
6030
- const url = new URL("/cli/auth", endpoint);
6031
- url.searchParams.set("request", auth.request);
6032
- url.searchParams.set("challenge", auth.challenge);
6033
- url.searchParams.set("pairing", auth.pairing);
6034
- url.searchParams.set("scope", scope);
6035
- if (workspaceId)
6036
- url.searchParams.set("workspace", workspaceId);
6037
- return url.toString();
6272
+ return buildUrl(endpoint, APPROVAL_PATH, {
6273
+ request: auth.request,
6274
+ challenge: auth.challenge,
6275
+ pairing: auth.pairing,
6276
+ scope,
6277
+ workspace: workspaceId
6278
+ });
6279
+ }
6280
+ function toRedirectError(endpoint, response) {
6281
+ const location = response.headers.get("location")?.trim();
6282
+ let target = null;
6283
+ if (location) {
6284
+ try {
6285
+ target = new URL(location, endpoint);
6286
+ } catch {
6287
+ target = null;
6288
+ }
6289
+ }
6290
+ if (!target) {
6291
+ return new SimApiError(`${endpoint} answered the login poll with HTTP ${response.status} and no usable redirect target. Check the endpoint.`, response.status);
6292
+ }
6293
+ const refusal = `${endpoint} redirected the login poll to ${target.href}. The CLI does not follow redirects, because a redirect drops the request body and would carry the login secret to another origin.`;
6294
+ const suggested = redirectEndpoint(endpoint, POLL_PATH, target);
6295
+ return new SimApiError(suggested ? `${refusal} Re-run with --endpoint ${suggested}, or run: sim configure --set-endpoint ${suggested}` : refusal, response.status);
6038
6296
  }
6039
6297
  async function pollForKey(endpoint, auth, signal) {
6040
6298
  const deadline = Date.now() + POLL_TIMEOUT_MS;
@@ -6043,16 +6301,23 @@ async function pollForKey(endpoint, auth, signal) {
6043
6301
  throw new SimApiError("Login cancelled.", 0);
6044
6302
  let response = null;
6045
6303
  try {
6046
- response = await fetch(new URL("/api/cli/auth/poll", endpoint), {
6304
+ response = await fetch(buildUrl(endpoint, POLL_PATH), {
6047
6305
  method: "POST",
6048
- headers: { "content-type": "application/json", accept: "application/json" },
6306
+ headers: {
6307
+ "content-type": "application/json",
6308
+ accept: "application/json",
6309
+ "user-agent": USER_AGENT
6310
+ },
6049
6311
  body: JSON.stringify({ request: auth.request, verifier: auth.pollSecret }),
6050
- signal
6312
+ signal,
6313
+ redirect: "manual"
6051
6314
  });
6052
6315
  } catch {
6053
6316
  response = null;
6054
6317
  }
6055
6318
  if (response) {
6319
+ if (REDIRECT_STATUSES.has(response.status))
6320
+ throw toRedirectError(endpoint, response);
6056
6321
  const raw = await response.text();
6057
6322
  if (!response.ok) {
6058
6323
  if (!RETRYABLE_POLL_STATUSES.has(response.status)) {
@@ -6106,263 +6371,76 @@ function clientFrom(command) {
6106
6371
  return { client: new SimClient(profile), profile };
6107
6372
  }
6108
6373
 
6109
- // src/commands/auth.ts
6110
- function openBrowser(url) {
6111
- const [command, args] = process.platform === "win32" ? ["cmd", ["/c", "start", "", url]] : [process.platform === "darwin" ? "open" : "xdg-open", [url]];
6112
- try {
6113
- const child = spawn(command, args, { stdio: "ignore", detached: true });
6114
- child.on("error", () => {});
6115
- child.unref();
6116
- } catch {}
6117
- }
6118
- function presentAuthentication(source) {
6119
- switch (source) {
6120
- case "flag":
6121
- return { authenticated: true, source: "flag" };
6122
- case "env":
6123
- return { authenticated: true, source: "env" };
6124
- case "credentials":
6125
- return { authenticated: true, source: "credentials" };
6126
- case "unset":
6127
- return { authenticated: false, source: "unset" };
6128
- case "config":
6129
- case "default":
6130
- throw new SimApiError(`Unexpected API key source "${source}".`, 0);
6131
- }
6132
- }
6133
- async function confirmProfileOverwrite(profileName) {
6134
- if (!process.stdin.isTTY) {
6135
- throw new SimApiError(`Profile "${profileName}" already exists. Re-run with --yes to overwrite it.`, 0);
6136
- }
6137
- const prompt = createInterface({ input: process.stdin, output: process.stderr });
6138
- try {
6139
- const answer = await prompt.question(`Profile "${profileName}" already exists. Replace its API key and login defaults? (y/N) `);
6140
- return answer.trim().toLowerCase() === "y" || answer.trim().toLowerCase() === "yes";
6141
- } finally {
6142
- prompt.close();
6143
- }
6144
- }
6145
- function loginCommand() {
6146
- return new Command("login").description("Authorize this terminal and store an API key for the profile").option("--scope <scope>", "Key space to mint from: platform or copilot", "platform").option("--no-browser", "Print the URL instead of opening a browser").option("-y, --yes", "Overwrite an existing profile without prompting").action(async (options, command) => {
6147
- const profile = profileFrom(command);
6148
- if (options.scope !== "platform" && options.scope !== "copilot") {
6149
- throw new SimApiError(`Unknown scope "${options.scope}". Use platform or copilot.`, 0);
6150
- }
6151
- const scope = options.scope;
6152
- if (readCredentialsProfile(profile.name).api_key && !options.yes) {
6153
- const confirmed = await confirmProfileOverwrite(profile.name);
6154
- if (!confirmed) {
6155
- console.log(source_default.dim("Login cancelled; the existing profile was not changed."));
6156
- return;
6374
+ // src/generated/v2-api.ts
6375
+ var V2_OPERATIONS = {
6376
+ abortFileUpload: {
6377
+ method: "DELETE",
6378
+ path: "/api/v2/files/uploads/[uploadId]",
6379
+ pathParams: ["uploadId"],
6380
+ pathParamDocs: { uploadId: "Upload session identifier." },
6381
+ responseMode: "json",
6382
+ summary: "Abort File Upload",
6383
+ query: {
6384
+ workspaceId: {
6385
+ kind: "string",
6386
+ required: true,
6387
+ describe: "Workspace that owns the upload session."
6157
6388
  }
6158
6389
  }
6159
- const auth = createAuthRequest();
6160
- const url = buildApprovalUrl(profile.endpoint, auth, scope, profile.workspaceId ?? undefined);
6161
- console.log(`Signing in to ${source_default.bold(profile.endpoint)} as profile ${source_default.bold(profile.name)}`);
6162
- console.log(`
6163
- Pairing code: ${source_default.bold(auth.pairing)}`);
6164
- console.log(source_default.dim(`Confirm this code matches what the browser shows before approving.
6165
- `));
6166
- console.log(url);
6167
- if (options.browser)
6168
- openBrowser(url);
6169
- console.log(source_default.dim(`
6170
- Waiting for approval…`));
6171
- const key = await pollForKey(profile.endpoint, auth);
6172
- if (key.scope !== scope) {
6173
- throw new SimApiError(`Server issued a ${key.scope} key but this profile needs a ${scope} key. Update the Sim deployment, or run: sim login --scope ${key.scope}`, 0);
6390
+ },
6391
+ abortKnowledgeDocumentUpload: {
6392
+ method: "DELETE",
6393
+ path: "/api/v2/knowledge/[id]/documents/uploads/[uploadId]",
6394
+ pathParams: ["id", "uploadId"],
6395
+ pathParamDocs: {
6396
+ id: "Unique knowledge base identifier.",
6397
+ uploadId: "Upload session identifier returned when the upload was created."
6398
+ },
6399
+ responseMode: "json",
6400
+ summary: "Abort Document Upload",
6401
+ query: {
6402
+ workspaceId: {
6403
+ kind: "string",
6404
+ required: true,
6405
+ describe: "Workspace that owns the knowledge base."
6406
+ }
6174
6407
  }
6175
- writeCredentialsProfile(profile.name, key.apiKey);
6176
- const settings = {
6177
- endpoint: profile.endpoint,
6178
- workspace: key.workspaceId ?? null
6179
- };
6180
- writeConfigProfile(profile.name, settings);
6181
- console.log(source_default.green(`
6182
- Logged in. Key stored in ${credentialsPath()}`));
6183
- if (key.workspaceBound && key.workspaceId) {
6184
- console.log(source_default.dim(` Workspace-scoped key it can only reach ${key.workspaceId}.`));
6185
- } else if (key.workspaceId) {
6186
- console.log(source_default.dim(` Personal key, defaulting to ${key.workspaceId}. Override per command with --workspace.`));
6187
- } else {
6188
- console.log(source_default.dim(" Personal key with no default workspace. Set one with: sim configure --set-workspace <id>"));
6408
+ },
6409
+ addTableColumn: {
6410
+ method: "POST",
6411
+ path: "/api/v2/tables/[tableId]/columns",
6412
+ pathParams: ["tableId"],
6413
+ pathParamDocs: { tableId: "Unique table identifier." },
6414
+ responseMode: "json",
6415
+ summary: "Add Column",
6416
+ body: {
6417
+ workspaceId: { kind: "string", required: true, describe: "Workspace that owns the table." },
6418
+ column: { kind: "object", required: true, describe: "Column definition to add." }
6189
6419
  }
6190
- });
6191
- }
6192
- function logoutCommand() {
6193
- return new Command("logout").description("Remove the profile's stored API key").option("--all", "Remove the profile entirely, including its settings").action((options, command) => {
6194
- const profile = profileFrom(command);
6195
- if (options.all) {
6196
- const removed = deleteProfile(profile.name);
6197
- if (!removed.config && !removed.credentials) {
6198
- console.log(source_default.dim(`Nothing stored for profile "${profile.name}".`));
6199
- return;
6200
- }
6201
- console.log(source_default.green(`✓ Removed profile "${profile.name}".`));
6202
- return;
6203
- }
6204
- if (!readCredentialsProfile(profile.name).api_key) {
6205
- console.log(source_default.dim(`No stored key for profile "${profile.name}".`));
6206
- return;
6207
- }
6208
- writeCredentialsProfile(profile.name, null);
6209
- console.log(source_default.green(`✓ Removed the stored key for profile "${profile.name}".`));
6210
- console.log(source_default.dim(" The key itself is still active — revoke it in Settings → API keys."));
6211
- });
6212
- }
6213
- function whoamiCommand() {
6214
- return new Command("whoami").description("Show the resolved profile and where each setting came from").action((_options, command) => {
6215
- const profile = profileFrom(command);
6216
- const { sources } = profile;
6217
- const authentication = presentAuthentication(sources.apiKey);
6218
- const annotate = (value, source) => source === "unset" ? source_default.dim("not set") : `${value} ${source_default.dim(`(${source})`)}`;
6219
- printRecord(profile.output, [
6220
- ["Profile", profile.name],
6221
- ["Endpoint", annotate(profile.endpoint, sources.endpoint)],
6222
- [
6223
- "API key",
6224
- authentication.authenticated ? annotate("configured", authentication.source) : source_default.yellow("not logged in")
6225
- ],
6226
- ["Workspace", annotate(profile.workspaceId ?? "", sources.workspaceId)],
6227
- ["Output", annotate(profile.output, sources.output)]
6228
- ], {
6229
- profile: profile.name,
6230
- endpoint: profile.endpoint,
6231
- workspaceId: profile.workspaceId,
6232
- output: profile.output,
6233
- authenticated: authentication.authenticated,
6234
- sources: {
6235
- endpoint: sources.endpoint,
6236
- authentication: authentication.source,
6237
- workspaceId: sources.workspaceId,
6238
- output: sources.output
6239
- }
6240
- });
6241
- });
6242
- }
6243
- function profilesCommand() {
6244
- return new Command("profiles").alias("profile").description("List the profiles defined in the config and credentials files").action((_options, command) => {
6245
- const profiles = listProfiles();
6246
- if (profiles.length === 0) {
6247
- console.log(source_default.dim("No profiles yet. Run: sim login"));
6248
- return;
6249
- }
6250
- const active = profileFrom(command).name;
6251
- for (const name of profiles) {
6252
- const marker = name === active ? source_default.green("*") : " ";
6253
- const hasKey = Boolean(readCredentialsProfile(name).api_key);
6254
- console.log(`${marker} ${name}${hasKey ? "" : source_default.dim(" (no key)")}`);
6255
- }
6256
- });
6257
- }
6258
-
6259
- // src/commands/configure.ts
6260
- function configureCommand() {
6261
- return new Command("configure").description("Set a profile's endpoint, default workspace, or output format").option("--set-endpoint <url>", "Sim deployment to talk to").option("--set-workspace <id>", "Default workspace for workspace-scoped commands").option("--set-output <format>", `Default output format (${OUTPUT_FORMATS.join(" | ")})`).option("--unset <key...>", "Remove settings (endpoint, workspace, output)").action((options, command) => {
6262
- const profile = profileFrom(command);
6263
- const updates = {};
6264
- if (options.setEndpoint)
6265
- updates.endpoint = options.setEndpoint.replace(/\/+$/, "");
6266
- if (options.setWorkspace)
6267
- updates.workspace = options.setWorkspace;
6268
- if (options.setOutput) {
6269
- if (!OUTPUT_FORMATS.includes(options.setOutput)) {
6270
- throw new SimApiError(`Unknown output format "${options.setOutput}". Use one of: ${OUTPUT_FORMATS.join(", ")}`, 0);
6271
- }
6272
- updates.output = options.setOutput;
6273
- }
6274
- for (const key of options.unset ?? []) {
6275
- if (!["endpoint", "workspace", "output"].includes(key)) {
6276
- throw new SimApiError(`Cannot unset "${key}". Use endpoint, workspace, or output.`, 0);
6277
- }
6278
- updates[key] = null;
6279
- }
6280
- if (Object.keys(updates).length === 0) {
6281
- const current = readConfigProfile(profile.name);
6282
- if (Object.keys(current).length === 0) {
6283
- console.log(source_default.dim(`No settings stored for profile "${profile.name}".`));
6284
- return;
6285
- }
6286
- for (const [key, value] of Object.entries(current)) {
6287
- console.log(`${source_default.dim(`${key}:`)} ${value}`);
6288
- }
6289
- return;
6290
- }
6291
- writeConfigProfile(profile.name, updates);
6292
- console.log(source_default.green(`✓ Updated profile "${profile.name}" in ${configPath()}`));
6293
- });
6294
- }
6295
-
6296
- // src/generated/v2-api.ts
6297
- var V2_OPERATIONS = {
6298
- abortFileUpload: {
6299
- method: "DELETE",
6300
- path: "/api/v2/files/uploads/[uploadId]",
6301
- pathParams: ["uploadId"],
6302
- pathParamDocs: { uploadId: "Upload session identifier." },
6303
- responseMode: "json",
6304
- summary: "Abort File Upload",
6305
- query: {
6306
- workspaceId: {
6307
- kind: "string",
6308
- required: true,
6309
- describe: "Workspace that owns the upload session."
6310
- }
6311
- }
6312
- },
6313
- abortKnowledgeDocumentUpload: {
6314
- method: "DELETE",
6315
- path: "/api/v2/knowledge/[id]/documents/uploads/[uploadId]",
6316
- pathParams: ["id", "uploadId"],
6317
- pathParamDocs: {
6318
- id: "Unique knowledge base identifier.",
6319
- uploadId: "Upload session identifier returned when the upload was created."
6320
- },
6321
- responseMode: "json",
6322
- summary: "Abort Document Upload",
6323
- query: {
6324
- workspaceId: {
6325
- kind: "string",
6326
- required: true,
6327
- describe: "Workspace that owns the knowledge base."
6328
- }
6329
- }
6330
- },
6331
- addTableColumn: {
6332
- method: "POST",
6333
- path: "/api/v2/tables/[tableId]/columns",
6334
- pathParams: ["tableId"],
6335
- pathParamDocs: { tableId: "Unique table identifier." },
6336
- responseMode: "json",
6337
- summary: "Add Column",
6338
- body: {
6339
- workspaceId: { kind: "string", required: true, describe: "Workspace that owns the table." },
6340
- column: { kind: "object", required: true, describe: "Column definition to add." }
6341
- }
6342
- },
6343
- addWorkflowGroup: {
6344
- method: "POST",
6345
- path: "/api/v2/tables/[tableId]/groups",
6346
- pathParams: ["tableId"],
6347
- pathParamDocs: { tableId: "Unique table identifier." },
6348
- responseMode: "json",
6349
- summary: "Add Workflow Group",
6350
- body: {
6351
- workspaceId: { kind: "string", required: true, describe: "Unique workspace identifier." },
6352
- group: {
6353
- kind: "object",
6354
- required: true,
6355
- describe: "Workflow or enrichment producer definition."
6356
- },
6357
- outputColumns: {
6358
- kind: "array",
6359
- required: true,
6360
- describe: "Columns created for producer outputs."
6361
- },
6362
- autoRun: {
6363
- kind: "boolean",
6364
- default: false,
6365
- describe: "Whether to schedule existing rows after group creation."
6420
+ },
6421
+ addWorkflowGroup: {
6422
+ method: "POST",
6423
+ path: "/api/v2/tables/[tableId]/groups",
6424
+ pathParams: ["tableId"],
6425
+ pathParamDocs: { tableId: "Unique table identifier." },
6426
+ responseMode: "json",
6427
+ summary: "Add Workflow Group",
6428
+ body: {
6429
+ workspaceId: { kind: "string", required: true, describe: "Unique workspace identifier." },
6430
+ group: {
6431
+ kind: "object",
6432
+ required: true,
6433
+ describe: "Workflow or enrichment producer definition."
6434
+ },
6435
+ outputColumns: {
6436
+ kind: "array",
6437
+ required: true,
6438
+ describe: "Columns created for producer outputs."
6439
+ },
6440
+ autoRun: {
6441
+ kind: "boolean",
6442
+ default: false,
6443
+ describe: "Whether to schedule existing rows after group creation."
6366
6444
  }
6367
6445
  }
6368
6446
  },
@@ -8968,6 +9046,10 @@ var V2_OPERATIONS = {
8968
9046
  kind: "string",
8969
9047
  required: true,
8970
9048
  describe: "Write-only secret value. It is never returned."
9049
+ },
9050
+ description: {
9051
+ kind: "string",
9052
+ describe: "What the secret is for, shown to teammates. Workspace scope only — sending it for a personal secret is rejected. Omit it to leave an existing description untouched; send null or an empty string to clear one."
8971
9053
  }
8972
9054
  }
8973
9055
  },
@@ -9380,88 +9462,345 @@ var V2_OPERATIONS = {
9380
9462
  }
9381
9463
  };
9382
9464
 
9383
- // src/runtime/request.ts
9384
- import { existsSync as existsSync2, readFileSync as readFileSync2, readSync } from "node:fs";
9385
-
9386
- // src/contract/commands.ts
9387
- var TABLE_NAME_HELP = "Identifier: letters, numbers, and underscores; cannot start with a number";
9388
- var TABLE_FILTER_HELP = 'Predicate: {"all":[{"field":"status","op":"eq","value":"active"}]}; groups use all/any. Operators: eq, ne, gt, gte, lt, lte, in, nin, contains, ncontains, startsWith, endsWith, like, ilike, nlike, nilike, isEmpty, isNotEmpty, isNull, isNotNull';
9389
- var TABLE_SORT_HELP = 'Ordered sort keys: [{"field":"createdAt","direction":"desc"}] (direction: asc or desc)';
9390
- var CUSTOM_TOOL_SCHEMA_HELP = 'OpenAI function schema: {"type":"function","function":{"name":"...","parameters":{"type":"object","properties":{}}}}';
9391
- var FOLDER_PATH_INPUT = {
9392
- describe: "Folder path; the leading / is optional"
9393
- };
9394
- var FOLDER_PATH_FLAG = {
9395
- ...FOLDER_PATH_INPUT,
9396
- name: "folder"
9397
- };
9398
- var FOLDER_DELETE_FLAGS = {
9399
- path: FOLDER_PATH_INPUT,
9400
- recursive: { boolean: true, describe: "Delete the folder and its descendants" }
9401
- };
9402
- var KNOWLEDGE_DOCUMENT_PATH_ARGUMENTS = { id: "knowledgeBaseId" };
9403
- var WORKFLOW_RUN_SCOPE = {
9404
- id: {
9405
- name: "workflow",
9406
- placeholder: "workflowId",
9407
- describe: "Workflow ID"
9465
+ // src/commands/auth.ts
9466
+ function openBrowser(url) {
9467
+ const [command, args] = process.platform === "win32" ? ["cmd", ["/c", "start", "", url]] : [process.platform === "darwin" ? "open" : "xdg-open", [url]];
9468
+ try {
9469
+ const child = spawn(command, args, { stdio: "ignore", detached: true });
9470
+ child.on("error", () => {});
9471
+ child.unref();
9472
+ } catch {}
9473
+ }
9474
+ function presentAuthentication(source) {
9475
+ switch (source) {
9476
+ case "flag":
9477
+ return { authenticated: true, source: "flag" };
9478
+ case "env":
9479
+ return { authenticated: true, source: "env" };
9480
+ case "credentials":
9481
+ return { authenticated: true, source: "credentials" };
9482
+ case "unset":
9483
+ return { authenticated: false, source: "unset" };
9484
+ case "config":
9485
+ case "default":
9486
+ throw new SimApiError(`Unexpected API key source "${source}".`, 0);
9408
9487
  }
9409
- };
9410
- var FOLDER_LIST_COLUMNS = [
9411
- { header: "path" },
9412
- { header: "name" },
9413
- { header: "parent", path: "parentPath" },
9414
- { header: "updated", path: "updatedAt", format: "timestamp" }
9415
- ];
9416
- function moveResource(command, resource) {
9417
- return {
9418
- command,
9419
- positionals: ["folderPath"],
9420
- requestFields: ["folderPath"],
9421
- describe: `Move a ${resource} to a folder`
9422
- };
9423
9488
  }
9424
- var CLI_CONTRACT = {
9425
- createCredentialConnection: { hidden: true },
9426
- createServiceAccountCredential: { hidden: true },
9427
- getBillingStatus: {
9428
- command: "billing status",
9429
- allWorkspaces: true,
9430
- describe: "Show billing status and current-period credit usage",
9431
- fields: [
9432
- { header: "plan" },
9433
- { header: "status" },
9434
- { header: "workspace", path: "workspaceId" },
9435
- { header: "period start", path: "period.start", format: "timestamp" },
9436
- { header: "period end", path: "period.end", format: "timestamp" },
9437
- { header: "used credits", path: "credits.used" },
9438
- { header: "limit credits", path: "credits.limit" },
9439
- { header: "remaining credits", path: "credits.remaining" }
9440
- ]
9441
- },
9442
- listBillingLogs: {
9443
- command: "billing logs",
9444
- allWorkspaces: true,
9445
- describe: "List credit usage events",
9446
- flags: {
9447
- source: { describe: "Filter by usage source; sim-chat combines Copilot and workspace chat" },
9448
- period: { describe: "Billing period" },
9449
- startDate: { describe: "Custom period start (ISO 8601)" },
9450
- endDate: { describe: "Custom period end (ISO 8601)" }
9451
- },
9452
- columns: [
9453
- { header: "at", path: "createdAt", format: "timestamp" },
9454
- { header: "workspace", path: "workspaceId" },
9455
- { header: "source" },
9456
- { header: "workflow", path: "workflow.name" },
9457
- { header: "credits", path: "creditCost" },
9458
- { header: "run", path: "runId" },
9459
- { header: "id" }
9460
- ]
9461
- },
9462
- deleteTableRows: {
9463
- command: "tables rows batch-delete",
9464
- describe: "Delete rows matching a filter, or an explicit list of ids",
9489
+ async function confirmProfileOverwrite(profileName) {
9490
+ if (!process.stdin.isTTY) {
9491
+ throw new SimApiError(`Profile "${profileName}" already exists. Re-run with --yes to overwrite it.`, 0);
9492
+ }
9493
+ const prompt = createInterface({ input: process.stdin, output: process.stderr });
9494
+ try {
9495
+ const answer = await prompt.question(`Profile "${profileName}" already exists. Replace its API key and login defaults? (y/N) `);
9496
+ return answer.trim().toLowerCase() === "y" || answer.trim().toLowerCase() === "yes";
9497
+ } finally {
9498
+ prompt.close();
9499
+ }
9500
+ }
9501
+ function loginCommand() {
9502
+ return new Command("login").description("Authorize this terminal and store an API key for the profile").option("--scope <scope>", "Key space to mint from: platform or copilot", "platform").option("--no-browser", "Print the URL instead of opening a browser").option("-y, --yes", "Overwrite an existing profile without prompting").action(async (options, command) => {
9503
+ const profile = profileFrom(command);
9504
+ if (options.scope !== "platform" && options.scope !== "copilot") {
9505
+ throw new SimApiError(`Unknown scope "${options.scope}". Use platform or copilot.`, 0);
9506
+ }
9507
+ const scope = options.scope;
9508
+ if (readCredentialsProfile(profile.name).api_key && !options.yes) {
9509
+ const confirmed = await confirmProfileOverwrite(profile.name);
9510
+ if (!confirmed) {
9511
+ console.log(source_default.dim("Login cancelled; the existing profile was not changed."));
9512
+ return;
9513
+ }
9514
+ }
9515
+ const auth = createAuthRequest();
9516
+ const url = buildApprovalUrl(profile.endpoint, auth, scope, profile.workspaceId ?? undefined);
9517
+ console.log(`Signing in to ${source_default.bold(profile.endpoint)} as profile ${source_default.bold(profile.name)}`);
9518
+ console.log(`
9519
+ Pairing code: ${source_default.bold(auth.pairing)}`);
9520
+ console.log(source_default.dim(`Confirm this code matches what the browser shows before approving.
9521
+ `));
9522
+ console.log(url);
9523
+ if (options.browser)
9524
+ openBrowser(url);
9525
+ console.log(source_default.dim(`
9526
+ Waiting for approval…`));
9527
+ const key = await pollForKey(profile.endpoint, auth);
9528
+ if (key.scope !== scope) {
9529
+ throw new SimApiError(`Server issued a ${key.scope} key but this profile needs a ${scope} key. Update the Sim deployment, or run: sim login --scope ${key.scope}`, 0);
9530
+ }
9531
+ writeCredentialsProfile(profile.name, key.apiKey);
9532
+ const settings = {
9533
+ endpoint: profile.endpoint,
9534
+ workspace: key.workspaceId ?? null
9535
+ };
9536
+ writeConfigProfile(profile.name, settings);
9537
+ console.log(source_default.green(`
9538
+ ✓ Logged in. Key stored in ${credentialsPath()}`));
9539
+ if (key.workspaceBound && key.workspaceId) {
9540
+ console.log(source_default.dim(` Workspace-scoped key — it can only reach ${key.workspaceId}.`));
9541
+ } else if (key.workspaceId) {
9542
+ console.log(source_default.dim(` Personal key, defaulting to ${key.workspaceId}. Override per command with --workspace.`));
9543
+ } else {
9544
+ console.log(source_default.dim(" Personal key with no default workspace. Set one with: sim configure --set-workspace <id>"));
9545
+ }
9546
+ });
9547
+ }
9548
+ function logoutCommand() {
9549
+ return new Command("logout").description("Remove the profile's stored API key").option("--all", "Remove the profile entirely, including its settings").action((options, command) => {
9550
+ const profile = profileFrom(command);
9551
+ if (options.all) {
9552
+ const removed = deleteProfile(profile.name);
9553
+ if (!removed.config && !removed.credentials) {
9554
+ console.log(source_default.dim(`Nothing stored for profile "${profile.name}".`));
9555
+ return;
9556
+ }
9557
+ console.log(source_default.green(`✓ Removed profile "${profile.name}".`));
9558
+ return;
9559
+ }
9560
+ if (!readCredentialsProfile(profile.name).api_key) {
9561
+ console.log(source_default.dim(`No stored key for profile "${profile.name}".`));
9562
+ return;
9563
+ }
9564
+ writeCredentialsProfile(profile.name, null);
9565
+ console.log(source_default.green(`✓ Removed the stored key for profile "${profile.name}".`));
9566
+ console.log(source_default.dim(" The key itself is still active — revoke it in Settings → API keys."));
9567
+ });
9568
+ }
9569
+ var CREDENTIAL_VERDICT_STATUSES = new Set([401, 403, 404]);
9570
+ var WHOAMI_EXIT_CODES = {
9571
+ verified: 0,
9572
+ disabled: 0,
9573
+ unauthenticated: 1,
9574
+ rejected: 1,
9575
+ unreachable: 2,
9576
+ "no-workspace": 2
9577
+ };
9578
+ async function verifyProfile(client, profile) {
9579
+ if (!profile.apiKey) {
9580
+ return {
9581
+ status: "unauthenticated",
9582
+ workspace: null,
9583
+ detail: `no API key — run: sim login --profile ${profile.name}`
9584
+ };
9585
+ }
9586
+ if (!profile.workspaceId) {
9587
+ return {
9588
+ status: "no-workspace",
9589
+ workspace: null,
9590
+ detail: `no workspace to check against — run: sim configure --profile ${profile.name} --set-workspace <id>`
9591
+ };
9592
+ }
9593
+ const operation = V2_OPERATIONS.getWorkspace;
9594
+ try {
9595
+ const response = await client.request(resolvePath(operation.path, { workspaceId: profile.workspaceId }), { method: operation.method });
9596
+ const { id, name, memberCount } = response.data;
9597
+ return { status: "verified", workspace: { id, name, memberCount }, detail: null };
9598
+ } catch (error) {
9599
+ if (!(error instanceof SimApiError))
9600
+ throw error;
9601
+ return {
9602
+ status: CREDENTIAL_VERDICT_STATUSES.has(error.status) ? "rejected" : "unreachable",
9603
+ workspace: null,
9604
+ detail: error.message
9605
+ };
9606
+ }
9607
+ }
9608
+ function presentVerification(verification) {
9609
+ if (verification.status === "verified") {
9610
+ const { name, memberCount } = verification.workspace;
9611
+ const members = `${memberCount} ${memberCount === 1 ? "member" : "members"}`;
9612
+ return `${source_default.green("✓")} ${safeOneLine(name)} · ${members}`;
9613
+ }
9614
+ const detail = safeOneLine(verification.detail);
9615
+ switch (verification.status) {
9616
+ case "rejected":
9617
+ return `${source_default.red("✗")} ${detail}`;
9618
+ case "unauthenticated":
9619
+ return source_default.yellow(`not logged in — ${detail}`);
9620
+ case "disabled":
9621
+ return source_default.dim(detail);
9622
+ default:
9623
+ return source_default.yellow(`could not check — ${detail}`);
9624
+ }
9625
+ }
9626
+ function whoamiCommand() {
9627
+ return new Command("whoami").description("Show the resolved profile, where each setting came from, and whether it works").option("--no-verify", "Skip the API check and only print the resolved settings").action(async (options, command) => {
9628
+ const { client, profile } = clientFrom(command);
9629
+ const { sources } = profile;
9630
+ const authentication = presentAuthentication(sources.apiKey);
9631
+ const verification = options.verify ? await verifyProfile(client, profile) : { status: "disabled", workspace: null, detail: "not checked (--no-verify)" };
9632
+ const annotate = (value, source) => source === "unset" ? source_default.dim("not set") : `${value} ${source_default.dim(`(${source})`)}`;
9633
+ printRecord(profile.output, [
9634
+ ["Profile", profile.name],
9635
+ ["Endpoint", annotate(profile.endpoint, sources.endpoint)],
9636
+ [
9637
+ "API key",
9638
+ authentication.authenticated ? annotate("configured", authentication.source) : source_default.yellow("not logged in")
9639
+ ],
9640
+ ["Workspace", annotate(profile.workspaceId ?? "", sources.workspaceId)],
9641
+ ["Output", annotate(profile.output, sources.output)],
9642
+ ["Verified", presentVerification(verification)]
9643
+ ], {
9644
+ profile: profile.name,
9645
+ endpoint: profile.endpoint,
9646
+ workspaceId: profile.workspaceId,
9647
+ output: profile.output,
9648
+ authenticated: authentication.authenticated,
9649
+ sources: {
9650
+ endpoint: sources.endpoint,
9651
+ authentication: authentication.source,
9652
+ workspaceId: sources.workspaceId,
9653
+ output: sources.output
9654
+ },
9655
+ verification: {
9656
+ status: verification.status,
9657
+ workspace: verification.workspace,
9658
+ detail: verification.detail
9659
+ }
9660
+ });
9661
+ const exitCode = WHOAMI_EXIT_CODES[verification.status];
9662
+ if (exitCode !== 0)
9663
+ process.exitCode = exitCode;
9664
+ });
9665
+ }
9666
+ function profilesCommand() {
9667
+ return new Command("profiles").alias("profile").description("List the profiles defined in the config and credentials files").action((_options, command) => {
9668
+ const profiles = listProfiles();
9669
+ if (profiles.length === 0) {
9670
+ console.log(source_default.dim("No profiles yet. Run: sim login"));
9671
+ return;
9672
+ }
9673
+ const active = profileFrom(command).name;
9674
+ for (const name of profiles) {
9675
+ const marker = name === active ? source_default.green("*") : " ";
9676
+ const hasKey = Boolean(readCredentialsProfile(name).api_key);
9677
+ console.log(`${marker} ${name}${hasKey ? "" : source_default.dim(" (no key)")}`);
9678
+ }
9679
+ });
9680
+ }
9681
+
9682
+ // src/commands/configure.ts
9683
+ function configureCommand() {
9684
+ return new Command("configure").description("Set a profile's endpoint, default workspace, or output format").option("--set-endpoint <url>", "Sim deployment to talk to").option("--set-workspace <id>", "Default workspace for workspace-scoped commands").option("--set-output <format>", `Default output format (${OUTPUT_FORMATS.join(" | ")})`).option("--unset <key...>", "Remove settings (endpoint, workspace, output)").action((options, command) => {
9685
+ const profile = profileFrom(command);
9686
+ const updates = {};
9687
+ if (options.setEndpoint) {
9688
+ updates.endpoint = normalizeEndpoint(options.setEndpoint, "--set-endpoint");
9689
+ }
9690
+ if (options.setWorkspace)
9691
+ updates.workspace = options.setWorkspace;
9692
+ if (options.setOutput) {
9693
+ if (!OUTPUT_FORMATS.includes(options.setOutput)) {
9694
+ throw new SimApiError(`Unknown output format "${options.setOutput}". Use one of: ${OUTPUT_FORMATS.join(", ")}`, 0);
9695
+ }
9696
+ updates.output = options.setOutput;
9697
+ }
9698
+ for (const key of options.unset ?? []) {
9699
+ if (!["endpoint", "workspace", "output"].includes(key)) {
9700
+ throw new SimApiError(`Cannot unset "${key}". Use endpoint, workspace, or output.`, 0);
9701
+ }
9702
+ updates[key] = null;
9703
+ }
9704
+ if (Object.keys(updates).length === 0) {
9705
+ const current = readConfigProfile(profile.name);
9706
+ if (Object.keys(current).length === 0) {
9707
+ console.log(source_default.dim(`No settings stored for profile "${profile.name}".`));
9708
+ return;
9709
+ }
9710
+ for (const [key, value] of Object.entries(current)) {
9711
+ console.log(`${source_default.dim(`${key}:`)} ${value}`);
9712
+ }
9713
+ return;
9714
+ }
9715
+ writeConfigProfile(profile.name, updates);
9716
+ console.log(source_default.green(`✓ Updated profile "${profile.name}" in ${configPath()}`));
9717
+ });
9718
+ }
9719
+
9720
+ // src/runtime/request.ts
9721
+ import { existsSync as existsSync2, readFileSync as readFileSync3, readSync } from "node:fs";
9722
+
9723
+ // src/contract/commands.ts
9724
+ var TABLE_NAME_HELP = "Identifier: letters, numbers, and underscores; cannot start with a number";
9725
+ var TABLE_FILTER_HELP = 'Predicate: {"all":[{"field":"status","op":"eq","value":"active"}]}; groups use all/any. Operators: eq, ne, gt, gte, lt, lte, in, nin, contains, ncontains, startsWith, endsWith, like, ilike, nlike, nilike, isEmpty, isNotEmpty, isNull, isNotNull';
9726
+ var TABLE_SORT_HELP = 'Ordered sort keys: [{"field":"createdAt","direction":"desc"}] (direction: asc or desc)';
9727
+ var CUSTOM_TOOL_SCHEMA_HELP = 'OpenAI function schema: {"type":"function","function":{"name":"...","parameters":{"type":"object","properties":{}}}}';
9728
+ var FOLDER_PATH_INPUT = {
9729
+ describe: "Folder path as shown in the app; the leading / is optional",
9730
+ folderPath: true
9731
+ };
9732
+ var FOLDER_PATH_FLAG = {
9733
+ ...FOLDER_PATH_INPUT,
9734
+ name: "folder"
9735
+ };
9736
+ var FOLDER_DELETE_FLAGS = {
9737
+ path: FOLDER_PATH_INPUT,
9738
+ recursive: { boolean: true, describe: "Delete the folder and its descendants" }
9739
+ };
9740
+ var KNOWLEDGE_BASE_PATH_ARGUMENT = { id: "knowledgeBaseId" };
9741
+ var WORKFLOW_RUN_SCOPE = {
9742
+ id: {
9743
+ name: "workflow",
9744
+ placeholder: "workflowId",
9745
+ describe: "Workflow ID"
9746
+ }
9747
+ };
9748
+ var FOLDER_COLUMN = { header: "folder", path: "folderPath", format: "folder-path" };
9749
+ var FOLDER_LIST_COLUMNS = [
9750
+ { header: "path", format: "folder-path" },
9751
+ { header: "name" },
9752
+ { header: "parent", path: "parentPath", format: "folder-path" },
9753
+ { header: "updated", path: "updatedAt", format: "timestamp" }
9754
+ ];
9755
+ function moveResource(command, resource) {
9756
+ return {
9757
+ command,
9758
+ positionals: ["folderPath"],
9759
+ requestFields: ["folderPath"],
9760
+ describe: `Move a ${resource} to a folder`
9761
+ };
9762
+ }
9763
+ var CLI_CONTRACT = {
9764
+ createCredentialConnection: { hidden: true },
9765
+ createServiceAccountCredential: { hidden: true },
9766
+ getBillingStatus: {
9767
+ command: "billing status",
9768
+ allWorkspaces: true,
9769
+ describe: "Show billing status and current-period credit usage",
9770
+ fields: [
9771
+ { header: "plan" },
9772
+ { header: "status" },
9773
+ { header: "workspace", path: "workspaceId" },
9774
+ { header: "period start", path: "period.start", format: "timestamp" },
9775
+ { header: "period end", path: "period.end", format: "timestamp" },
9776
+ { header: "used credits", path: "credits.used" },
9777
+ { header: "limit credits", path: "credits.limit" },
9778
+ { header: "remaining credits", path: "credits.remaining" }
9779
+ ]
9780
+ },
9781
+ listBillingLogs: {
9782
+ command: "billing logs",
9783
+ allWorkspaces: true,
9784
+ describe: "List credit usage events",
9785
+ flags: {
9786
+ source: { describe: "Filter by usage source; sim-chat combines Copilot and workspace chat" },
9787
+ period: { describe: "Billing period" },
9788
+ startDate: { describe: "Custom period start (ISO 8601)" },
9789
+ endDate: { describe: "Custom period end (ISO 8601)" }
9790
+ },
9791
+ columns: [
9792
+ { header: "at", path: "createdAt", format: "timestamp" },
9793
+ { header: "workspace", path: "workspaceId" },
9794
+ { header: "source" },
9795
+ { header: "workflow", path: "workflow.name" },
9796
+ { header: "credits", path: "creditCost" },
9797
+ { header: "run", path: "runId" },
9798
+ { header: "id" }
9799
+ ]
9800
+ },
9801
+ deleteTableRows: {
9802
+ command: "tables rows batch-delete",
9803
+ describe: "Delete rows matching a filter, or an explicit list of ids",
9465
9804
  flags: {
9466
9805
  rowIds: { name: "row", list: true },
9467
9806
  filter: { json: true, describe: TABLE_FILTER_HELP }
@@ -9480,7 +9819,7 @@ var CLI_CONTRACT = {
9480
9819
  bulkUpdateKnowledgeDocuments: {
9481
9820
  command: "knowledge documents batch-update",
9482
9821
  describe: "Enable or disable every matching document",
9483
- pathArgumentNames: KNOWLEDGE_DOCUMENT_PATH_ARGUMENTS,
9822
+ pathArgumentNames: KNOWLEDGE_BASE_PATH_ARGUMENT,
9484
9823
  flags: {
9485
9824
  documentIds: { name: "document", list: true },
9486
9825
  selectAll: { boolean: true, describe: "Apply to every document in the knowledge base" }
@@ -9490,6 +9829,11 @@ var CLI_CONTRACT = {
9490
9829
  command: "workflows undeploy",
9491
9830
  describe: "Take a workflow out of deployment"
9492
9831
  },
9832
+ getWorkflowDeployment: {
9833
+ command: "workflows deployment status",
9834
+ renamedFrom: ["workflows deployment list"],
9835
+ describe: "Show a workflow’s current deployment"
9836
+ },
9493
9837
  setSecret: { hidden: true },
9494
9838
  deleteTable: { confirm: "This deletes the table and all of its rows." },
9495
9839
  deleteTableRow: { confirm: "This deletes the row." },
@@ -9499,7 +9843,7 @@ var CLI_CONTRACT = {
9499
9843
  },
9500
9844
  deleteKnowledgeBase: { confirm: "This deletes the knowledge base and every document in it." },
9501
9845
  deleteKnowledgeDocument: {
9502
- pathArgumentNames: KNOWLEDGE_DOCUMENT_PATH_ARGUMENTS,
9846
+ pathArgumentNames: KNOWLEDGE_BASE_PATH_ARGUMENT,
9503
9847
  confirm: "This deletes the document and its embeddings."
9504
9848
  },
9505
9849
  deleteFile: { confirm: "This archives the file." },
@@ -9529,7 +9873,10 @@ var CLI_CONTRACT = {
9529
9873
  workflowIds: { name: "workflow", list: true },
9530
9874
  folderPaths: { ...FOLDER_PATH_FLAG, list: true },
9531
9875
  triggers: { name: "trigger", list: true },
9532
- details: { describe: "Response detail level" },
9876
+ details: {
9877
+ requestDefault: "full",
9878
+ describe: "Response detail level; full is requested by default to name each run’s workflow"
9879
+ },
9533
9880
  includeTraceSpans: {
9534
9881
  boolean: true,
9535
9882
  describe: "Include trace spans in JSON or YAML output (implies full detail)"
@@ -9582,7 +9929,7 @@ var CLI_CONTRACT = {
9582
9929
  },
9583
9930
  itemsPath: "results",
9584
9931
  columns: [
9585
- { header: "score", path: "similarity" },
9932
+ { header: "score", path: "similarity", format: "score" },
9586
9933
  { header: "document", path: "documentName" },
9587
9934
  { header: "chunk", path: "chunkIndex" },
9588
9935
  { header: "content" }
@@ -9656,7 +10003,7 @@ var CLI_CONTRACT = {
9656
10003
  columns: [
9657
10004
  { header: "id" },
9658
10005
  { header: "name" },
9659
- { header: "folder", path: "folderPath" },
10006
+ FOLDER_COLUMN,
9660
10007
  { header: "rows", path: "rowCount" },
9661
10008
  { header: "updated", path: "updatedAt", format: "timestamp" }
9662
10009
  ]
@@ -9666,7 +10013,7 @@ var CLI_CONTRACT = {
9666
10013
  columns: [
9667
10014
  { header: "id" },
9668
10015
  { header: "name" },
9669
- { header: "folder", path: "folderPath" },
10016
+ FOLDER_COLUMN,
9670
10017
  { header: "deployed", path: "isDeployed", format: "bool" },
9671
10018
  { header: "runs", path: "runCount" },
9672
10019
  { header: "last run", path: "lastRunAt", format: "timestamp" }
@@ -9677,7 +10024,7 @@ var CLI_CONTRACT = {
9677
10024
  columns: [
9678
10025
  { header: "id" },
9679
10026
  { header: "name" },
9680
- { header: "folder", path: "folderPath" },
10027
+ FOLDER_COLUMN,
9681
10028
  { header: "size", format: "bytes" },
9682
10029
  { header: "type" },
9683
10030
  { header: "uploaded by", path: "uploadedByEmail" },
@@ -9690,15 +10037,17 @@ var CLI_CONTRACT = {
9690
10037
  columns: [
9691
10038
  { header: "id" },
9692
10039
  { header: "name" },
9693
- { header: "folder", path: "folderPath" },
10040
+ FOLDER_COLUMN,
9694
10041
  { header: "docs", path: "docCount" },
9695
10042
  { header: "tokens", path: "tokenCount" },
9696
10043
  { header: "model", path: "embeddingModel" }
9697
10044
  ]
9698
10045
  },
9699
- getKnowledgeDocument: { pathArgumentNames: KNOWLEDGE_DOCUMENT_PATH_ARGUMENTS },
10046
+ getKnowledgeDocument: { pathArgumentNames: KNOWLEDGE_BASE_PATH_ARGUMENT },
10047
+ updateKnowledgeDocument: { pathArgumentNames: KNOWLEDGE_BASE_PATH_ARGUMENT },
10048
+ listKnowledgeTags: { pathArgumentNames: KNOWLEDGE_BASE_PATH_ARGUMENT },
9700
10049
  listKnowledgeDocuments: {
9701
- pathArgumentNames: KNOWLEDGE_DOCUMENT_PATH_ARGUMENTS,
10050
+ pathArgumentNames: KNOWLEDGE_BASE_PATH_ARGUMENT,
9702
10051
  columns: [
9703
10052
  { header: "id" },
9704
10053
  { header: "filename" },
@@ -9742,12 +10091,24 @@ var CLI_CONTRACT = {
9742
10091
  { header: "updated", path: "updatedAt", format: "timestamp" }
9743
10092
  ]
9744
10093
  },
10094
+ listCredentialProviders: {
10095
+ columns: [
10096
+ { header: "type" },
10097
+ { header: "service", path: "serviceId" },
10098
+ { header: "provider", path: "providerId" },
10099
+ { header: "name" },
10100
+ { header: "family", path: "providerFamily" },
10101
+ { header: "available", format: "bool" },
10102
+ { header: "description" }
10103
+ ]
10104
+ },
9745
10105
  listSecrets: {
9746
10106
  columns: [
9747
10107
  { header: "name" },
9748
10108
  { header: "scope" },
9749
10109
  { header: "role" },
9750
- { header: "updated", path: "updatedAt", format: "timestamp" }
10110
+ { header: "updated", path: "updatedAt", format: "timestamp" },
10111
+ { header: "description" }
9751
10112
  ]
9752
10113
  },
9753
10114
  getWorkspace: {
@@ -9813,7 +10174,7 @@ var CLI_CONTRACT = {
9813
10174
  { header: "name" },
9814
10175
  { header: "size", format: "bytes" },
9815
10176
  { header: "type" },
9816
- { header: "folder", path: "folderPath" },
10177
+ FOLDER_COLUMN,
9817
10178
  { header: "uploaded by", path: "uploadedByEmail" },
9818
10179
  { header: "uploaded", path: "uploadedAt", format: "timestamp" },
9819
10180
  { header: "updated", path: "updatedAt", format: "timestamp" },
@@ -9840,6 +10201,11 @@ var CLI_CONTRACT = {
9840
10201
  command: "files rename",
9841
10202
  describe: "Rename a file"
9842
10203
  },
10204
+ restoreFile: {
10205
+ command: "files restore",
10206
+ renamedFrom: ["files restore create"],
10207
+ describe: "Restore an archived file"
10208
+ },
9843
10209
  updateFileContent: {
9844
10210
  command: "files set-content",
9845
10211
  describe: "Replace a file’s contents",
@@ -9992,13 +10358,26 @@ var CLI_CONTRACT = {
9992
10358
  command: "tables rows find",
9993
10359
  describe: "Find rows matching a predicate",
9994
10360
  flags: {
9995
- q: { describe: "Value to find" },
10361
+ q: { name: "query", renamedFrom: ["q"], describe: "Value to find" },
9996
10362
  predicate: { name: "filter", json: true, describe: TABLE_FILTER_HELP },
9997
10363
  sort: { json: true, describe: TABLE_SORT_HELP }
9998
10364
  },
9999
10365
  itemsPath: "matches",
10000
10366
  columns: [{ header: "ordinal" }, { header: "row", path: "rowId" }, { header: "column" }]
10001
10367
  },
10368
+ queryRowsCount: {
10369
+ command: "tables rows count",
10370
+ renamedFrom: ["tables count create"],
10371
+ describe: "Count rows matching a filter",
10372
+ flags: {
10373
+ predicate: {
10374
+ name: "filter",
10375
+ renamedFrom: ["predicate"],
10376
+ json: true,
10377
+ describe: TABLE_FILTER_HELP
10378
+ }
10379
+ }
10380
+ },
10002
10381
  runTableColumn: {
10003
10382
  command: "tables columns run",
10004
10383
  describe: "Run a column’s workflow",
@@ -10216,7 +10595,7 @@ function readArgumentSource(raw, flagName) {
10216
10595
  }
10217
10596
  }
10218
10597
  try {
10219
- return { text: readFileSync2(path, "utf8"), from: ` (read from ${path})` };
10598
+ return { text: readFileSync3(path, "utf8"), from: ` (read from ${path})` };
10220
10599
  } catch (error) {
10221
10600
  throw new SimApiError(`--${flagName} cannot read ${path}: ${error.message}`, 0);
10222
10601
  }
@@ -10251,6 +10630,26 @@ function readListValues(raw, flagName) {
10251
10630
  return trimmed;
10252
10631
  });
10253
10632
  }
10633
+ var PERCENT_ESCAPE = /%[0-9A-Fa-f]{2}/;
10634
+ var SUB_DELIMITERS = /[!'()*]/g;
10635
+ function encodeFolderPathSegment(name) {
10636
+ if (name === ".")
10637
+ return "%2E";
10638
+ if (name === "..")
10639
+ return "%2E%2E";
10640
+ return encodeURIComponent(name).replace(SUB_DELIMITERS, (character) => `%${character.charCodeAt(0).toString(16).toUpperCase()}`);
10641
+ }
10642
+ function encodeFolderPath(value) {
10643
+ return value.split("/").map((segment) => {
10644
+ if (!PERCENT_ESCAPE.test(segment))
10645
+ return encodeFolderPathSegment(segment);
10646
+ try {
10647
+ return encodeFolderPathSegment(decodeURIComponent(segment));
10648
+ } catch {
10649
+ return encodeFolderPathSegment(segment);
10650
+ }
10651
+ }).join("/");
10652
+ }
10254
10653
  function pathHint(raw) {
10255
10654
  if (raw.startsWith("@") || /^\s*[[{"\-\d]|^\s*(true|false|null)/.test(raw))
10256
10655
  return "";
@@ -10260,7 +10659,7 @@ function coerce(raw, field, flag, flagName) {
10260
10659
  if (raw === undefined)
10261
10660
  return;
10262
10661
  if (flag.list) {
10263
- const values = readListValues(raw, flagName);
10662
+ const values = readListValues(raw, flagName).map((value) => flag.folderPath ? encodeFolderPath(value) : value);
10264
10663
  return field.kind === "string" ? values.join(",") : values;
10265
10664
  }
10266
10665
  if (takesJson(field, flag)) {
@@ -10285,6 +10684,8 @@ function coerce(raw, field, flag, flagName) {
10285
10684
  if (choices && !choices.includes(String(raw))) {
10286
10685
  throw new SimApiError(`--${flagName} must be one of: ${choices.join(", ")}`, 0);
10287
10686
  }
10687
+ if (flag.folderPath && typeof raw === "string")
10688
+ return encodeFolderPath(raw);
10288
10689
  return raw;
10289
10690
  }
10290
10691
  function asQueryValue(value) {
@@ -10325,7 +10726,8 @@ function buildRequest(operation, positional, flags, workspaceId) {
10325
10726
  continue;
10326
10727
  const flagName = flagNameFor(operation, field);
10327
10728
  const omitProfileWorkspace = commandSpec.allWorkspaces && flags.allWorkspaces === true;
10328
- const raw = field === PROFILE_INJECTED_FIELD ? omitProfileWorkspace ? undefined : workspaceId : flags[camel(flagName)];
10729
+ const provided = field === PROFILE_INJECTED_FIELD ? omitProfileWorkspace ? undefined : workspaceId : flags[camel(flagName)];
10730
+ const raw = provided ?? flag.requestDefault;
10329
10731
  const value = coerce(raw ?? undefined, descriptor, flag, flagName);
10330
10732
  if (value === undefined) {
10331
10733
  if (descriptor.required) {
@@ -10498,6 +10900,15 @@ function countTraceSpans(value) {
10498
10900
  function at(row, path) {
10499
10901
  return path.split(".").reduce((value, key) => value && typeof value === "object" ? value[key] : undefined, row);
10500
10902
  }
10903
+ function decodeFolderPath(value) {
10904
+ return value.split("/").map((segment) => {
10905
+ try {
10906
+ return decodeURIComponent(segment);
10907
+ } catch {
10908
+ return segment;
10909
+ }
10910
+ }).join("/");
10911
+ }
10501
10912
  function renderCell(value, format, options = {}) {
10502
10913
  switch (format) {
10503
10914
  case "timestamp":
@@ -10510,8 +10921,12 @@ function renderCell(value, format, options = {}) {
10510
10921
  return bool2(value);
10511
10922
  case "cost":
10512
10923
  return typeof value === "number" ? `$${value.toFixed(4)}` : text(null);
10924
+ case "score":
10925
+ return typeof value === "number" ? value.toFixed(4) : text(null);
10513
10926
  case "count":
10514
10927
  return Array.isArray(value) ? String(value.length) : text(null);
10928
+ case "folder-path":
10929
+ return typeof value === "string" ? text(decodeFolderPath(value)) : text(value);
10515
10930
  case "trace-count": {
10516
10931
  const count = countTraceSpans(value);
10517
10932
  return `${count} ${count === 1 ? "span" : "spans"}${options.expandedTrace ? "" : " (use --trace)"}`;
@@ -10522,10 +10937,42 @@ function renderCell(value, format, options = {}) {
10522
10937
  return sanitize(typeof value === "object" ? JSON.stringify(value) : String(value));
10523
10938
  }
10524
10939
  }
10525
- var NESTED_CELL_WIDTH = 160;
10526
- function recordCell(value) {
10527
- const rendered = renderCell(value, "auto");
10528
- return rendered.length > NESTED_CELL_WIDTH ? `${rendered.slice(0, NESTED_CELL_WIDTH)}…` : rendered;
10940
+ var TIMESTAMP_KEY = /(?:At|Date)$/;
10941
+ var DURATION_KEY = /Ms$|^duration/;
10942
+ var BYTES_KEY = /^size$|(?:Size|Bytes)$/;
10943
+ var BOOL_KEY = /^(?:is|has)[A-Z]/;
10944
+ var RATIO_KEY = /^(?:similarity|score)$|(?:Similarity|Score)$/;
10945
+ var FOLDER_PATH_KEY = /^(?:path|parentPath|folderPath)$/;
10946
+ var ISO_TIMESTAMP = /^\d{4}-\d{2}-\d{2}[T ]\d{2}:\d{2}/;
10947
+ var RATIO_PRECISION = 4;
10948
+ function inferFormat(key, value) {
10949
+ if (typeof value === "boolean")
10950
+ return BOOL_KEY.test(key) ? "bool" : null;
10951
+ if (typeof value === "string") {
10952
+ if (FOLDER_PATH_KEY.test(key))
10953
+ return "folder-path";
10954
+ return TIMESTAMP_KEY.test(key) && ISO_TIMESTAMP.test(value) && !Number.isNaN(Date.parse(value)) ? "timestamp" : null;
10955
+ }
10956
+ if (typeof value !== "number" || !Number.isFinite(value))
10957
+ return null;
10958
+ if (DURATION_KEY.test(key))
10959
+ return "duration";
10960
+ if (BYTES_KEY.test(key))
10961
+ return "bytes";
10962
+ return null;
10963
+ }
10964
+ function inferredCell(key, value) {
10965
+ if (typeof value === "number" && Number.isFinite(value) && RATIO_KEY.test(key)) {
10966
+ return value.toFixed(RATIO_PRECISION);
10967
+ }
10968
+ return renderCell(value, inferFormat(key, value) ?? "auto");
10969
+ }
10970
+ function humanizeKey(key) {
10971
+ return key.replace(/([a-z0-9])([A-Z])/g, "$1 $2").replace(/([A-Z]+)([A-Z][a-z])/g, "$1 $2").replace(/[_-]+/g, " ").replace(/\s+/g, " ").trim().toLowerCase();
10972
+ }
10973
+ function inferHeader(key, format) {
10974
+ const trimmed = format === "duration" || format === "bytes" ? key.replace(/(?:Ms|Bytes)$/, "") : format === "bool" ? key.replace(/^is(?=[A-Z])/, "") : key;
10975
+ return humanizeKey(trimmed || key);
10529
10976
  }
10530
10977
  function columnsFrom(specs) {
10531
10978
  return specs.map((spec) => ({
@@ -10534,9 +10981,9 @@ function columnsFrom(specs) {
10534
10981
  }));
10535
10982
  }
10536
10983
  function fieldsFrom(data, specs, options = {}) {
10537
- return specs.flatMap((spec) => {
10984
+ return specs.map((spec) => {
10538
10985
  const value = at(data, spec.path ?? spec.header);
10539
- return value === undefined ? [] : [[spec.header, renderCell(value, spec.format, options)]];
10986
+ return [spec.header, value === undefined ? text(null) : renderCell(value, spec.format, options)];
10540
10987
  });
10541
10988
  }
10542
10989
  function inferColumns(rows, expand) {
@@ -10551,7 +10998,7 @@ function inferColumns(rows, expand) {
10551
10998
  if (value !== null && typeof value === "object")
10552
10999
  continue;
10553
11000
  seen.add(key);
10554
- paths.push({ path: key, header: key });
11001
+ paths.push({ path: key, key, header: inferHeader(key, inferFormat(key, value)), owned: true });
10555
11002
  }
10556
11003
  }
10557
11004
  if (expand) {
@@ -10564,13 +11011,18 @@ function inferColumns(rows, expand) {
10564
11011
  if (nested.has(key))
10565
11012
  continue;
10566
11013
  nested.add(key);
10567
- paths.push({ path: `${expand}.${key}`, header: seen.has(key) ? `${expand}.${key}` : key });
11014
+ paths.push({
11015
+ path: `${expand}.${key}`,
11016
+ key,
11017
+ header: seen.has(key) ? `${expand}.${key}` : key,
11018
+ owned: false
11019
+ });
10568
11020
  }
10569
11021
  }
10570
11022
  }
10571
- return paths.map(({ path, header }) => ({
11023
+ return paths.map(({ path, key, header, owned }) => ({
10572
11024
  header: sanitize(header),
10573
- value: (row) => renderCell(at(row, path), "auto")
11025
+ value: (row) => owned ? inferredCell(key, at(row, path)) : renderCell(at(row, path), "auto")
10574
11026
  }));
10575
11027
  }
10576
11028
  function unwrapResource(data) {
@@ -10603,7 +11055,10 @@ function renderResult(operation, format, raw, spec, options = {}) {
10603
11055
  printList(format, data, spec.columns ? columnsFrom(spec.columns) : inferColumns(data, spec.expand));
10604
11056
  return;
10605
11057
  }
10606
- const fields = spec.fields ? fieldsFrom(data, spec.fields, options) : data && typeof data === "object" ? Object.entries(data).map(([key, value]) => [key, recordCell(value)]) : [];
11058
+ const fields = spec.fields ? fieldsFrom(data, spec.fields, options) : data && typeof data === "object" ? Object.entries(data).map(([key, value]) => [
11059
+ inferHeader(key, inferFormat(key, value)),
11060
+ inferredCell(key, value)
11061
+ ]) : [];
10607
11062
  printRecord(format, fields, data);
10608
11063
  if (spec.expandedTrace && options.expandedTrace) {
10609
11064
  const traceSpans = at(data, "traceSpans");
@@ -10716,7 +11171,7 @@ function attachCredentialCommands(program2) {
10716
11171
  }
10717
11172
 
10718
11173
  // src/commands/protocol/files-get.ts
10719
- import { once } from "node:events";
11174
+ import { once as once2 } from "node:events";
10720
11175
  import { createWriteStream } from "node:fs";
10721
11176
  import { link, lstat, mkdtemp, readlink, rename, rm } from "node:fs/promises";
10722
11177
  import { dirname as dirname2, join as join2, resolve as resolve2 } from "node:path";
@@ -10731,6 +11186,9 @@ function printProtocolResult(format, result) {
10731
11186
 
10732
11187
  // src/commands/protocol/files-get.ts
10733
11188
  function writeFailure(path, error) {
11189
+ if (isRequestTimeout(error)) {
11190
+ return new SimApiError(`Downloading ${path} timed out. ${RAISE_TIMEOUT_HINT}`, 0);
11191
+ }
10734
11192
  const code = error.code;
10735
11193
  if (code === "EEXIST") {
10736
11194
  return new SimApiError(`${path} already exists. Pass --force to overwrite it, or choose another output path.`, 0);
@@ -10821,7 +11279,7 @@ async function streamToStdout(body, output = process.stdout) {
10821
11279
  if (done)
10822
11280
  return;
10823
11281
  if (!output.write(value))
10824
- await once(output, "drain");
11282
+ await once2(output, "drain");
10825
11283
  }
10826
11284
  } finally {
10827
11285
  reader.releaseLock();
@@ -11001,7 +11459,7 @@ async function finishUploadSession(client, workspaceId, session, path) {
11001
11459
 
11002
11460
  // src/commands/protocol/files-upload.ts
11003
11461
  function attachFileUpload(files) {
11004
- files.command("upload").argument("<path>", "Local file to upload").description("Upload a file to the workspace").option("--folder <path>", "Destination folder path (defaults to /)").option("--name <name>", "Store it under a different name").action(async (path, options, command) => {
11462
+ files.command("upload").argument("<path>", "Local file to upload").description("Upload a file to the workspace").option("--folder <path>", "Folder path as shown in the app; defaults to the root folder").option("--name <name>", "Store it under a different name").action(async (path, options, command) => {
11005
11463
  const { client, profile } = clientFrom(command);
11006
11464
  const workspaceId = client.requireWorkspace();
11007
11465
  const { name, size } = await localFile(path, options.name);
@@ -11012,7 +11470,7 @@ function attachFileUpload(files) {
11012
11470
  name,
11013
11471
  contentType: contentTypeFor(name),
11014
11472
  size,
11015
- ...options.folder !== undefined ? { folderPath: options.folder } : {}
11473
+ ...options.folder !== undefined ? { folderPath: encodeFolderPath(options.folder) } : {}
11016
11474
  }
11017
11475
  });
11018
11476
  const { session, uploadToken, transfer } = created.data;
@@ -11071,13 +11529,285 @@ function attachKnowledgeDocumentUpload(documents) {
11071
11529
  if (!completed.document) {
11072
11530
  throw new Error(`Knowledge upload ${session.id} completed without a document`);
11073
11531
  }
11074
- printProtocolResult(profile.output, {
11075
- id: completed.document.id,
11076
- knowledgeBaseId: completed.document.knowledgeBaseId,
11077
- name: completed.document.filename,
11078
- size: completed.document.fileSize,
11079
- status: completed.document.processingStatus
11080
- });
11532
+ printProtocolResult(profile.output, {
11533
+ id: completed.document.id,
11534
+ knowledgeBaseId: completed.document.knowledgeBaseId,
11535
+ name: completed.document.filename,
11536
+ size: completed.document.fileSize,
11537
+ status: completed.document.processingStatus
11538
+ });
11539
+ });
11540
+ }
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
+ }
11081
11811
  });
11082
11812
  }
11083
11813
 
@@ -11114,15 +11844,22 @@ function addFieldOption(command, operation, field, descriptor) {
11114
11844
  const placeholder = takesList ? "<value...>" : wantsJson ? "<json|@file>" : "<value>";
11115
11845
  const choices = flag.choices ?? descriptor.values;
11116
11846
  const describe = `${documented}${takesList ? " (space-separated, or @path / @- with one value per line)" : wantsJson ? " (JSON, or @path / @- to read a file or stdin)" : ""}${descriptor.required ? " (required)" : ""}`;
11847
+ const renamedFrom = flag.renamedFrom ?? [];
11117
11848
  const option = new Option(`${short}--${name} ${placeholder}`, describe);
11118
11849
  if (choices && !takesList)
11119
11850
  option.choices([...choices]);
11120
11851
  if (descriptor.default !== undefined && field !== "limit") {
11121
11852
  option.default(undefined, String(descriptor.default));
11122
11853
  }
11123
- if (descriptor.required)
11854
+ if (descriptor.required && renamedFrom.length === 0)
11124
11855
  option.makeOptionMandatory();
11125
11856
  command.addOption(option);
11857
+ for (const previous of renamedFrom) {
11858
+ const retired = new Option(`--${previous} ${placeholder}`).hideHelp();
11859
+ if (choices && !takesList)
11860
+ retired.choices([...choices]);
11861
+ command.addOption(retired);
11862
+ }
11126
11863
  }
11127
11864
  function addOperationOptions(command, operation, commandSpec, operationSpec) {
11128
11865
  for (const param of operationSpec.pathParams) {
@@ -11158,16 +11895,19 @@ function addOperationOptions(command, operation, commandSpec, operationSpec) {
11158
11895
  }
11159
11896
  }
11160
11897
  if (commandSpec.confirm) {
11161
- command.option("-y, --yes", "Skip the confirmation");
11898
+ command.option("-y, --yes", "Confirm this destructive operation (required)");
11162
11899
  }
11163
11900
  }
11164
11901
 
11165
11902
  // src/commands/protocol/resource-directory.ts
11166
- var COLUMNS = [
11903
+ var COLUMNS2 = [
11167
11904
  { header: "kind", value: (entry) => text(entry.kind) },
11168
11905
  { header: "name", value: (entry) => text(entry.name) },
11169
- { header: "ref", value: (entry) => text(entry.ref) },
11170
- { header: "folder", value: (entry) => text(entry.folderPath) },
11906
+ {
11907
+ header: "ref",
11908
+ value: (entry) => text(entry.kind === "folder" ? decodeFolderPath(entry.ref) : entry.ref)
11909
+ },
11910
+ { header: "folder", value: (entry) => text(decodeFolderPath(entry.folderPath)) },
11171
11911
  { header: "updated", value: (entry) => timestamp2(entry.updatedAt) }
11172
11912
  ];
11173
11913
  function operationPath(operation) {
@@ -11218,7 +11958,7 @@ function attachResourceDirectoryCommands(group, config) {
11218
11958
  throw new SimApiError("--limit must be a non-negative integer", 0);
11219
11959
  }
11220
11960
  const limit = rawLimit === 0 ? Number.POSITIVE_INFINITY : rawLimit;
11221
- const folderPath = path ?? "/";
11961
+ const folderPath = encodeFolderPath(path ?? "/");
11222
11962
  const { client, profile } = clientFrom(command);
11223
11963
  const workspaceId = client.requireWorkspace();
11224
11964
  const [folders, resources] = await Promise.all([
@@ -11226,14 +11966,14 @@ function attachResourceDirectoryCommands(group, config) {
11226
11966
  listResources(client, config, workspaceId, folderPath, options.search, limit)
11227
11967
  ]);
11228
11968
  const entries = entriesFor(config, folders, resources);
11229
- printList(profile.output, entries.slice(0, limit), COLUMNS);
11969
+ printList(profile.output, entries.slice(0, limit), COLUMNS2);
11230
11970
  });
11231
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) => {
11232
11972
  const { client, profile } = clientFrom(command);
11233
11973
  const operation = V2_OPERATIONS[config.createFolder];
11234
11974
  const result = await client.request(operation.path, {
11235
11975
  method: operation.method,
11236
- body: { workspaceId: client.requireWorkspace(), path }
11976
+ body: { workspaceId: client.requireWorkspace(), path: encodeFolderPath(path) }
11237
11977
  });
11238
11978
  renderResult(config.createFolder, profile.output, result.data ?? result, {});
11239
11979
  });
@@ -11255,17 +11995,17 @@ function jsonFlag(raw, flagName, kind) {
11255
11995
  }
11256
11996
  async function watchImport(client, workspaceId, job) {
11257
11997
  let current = job;
11258
- let reported = -1;
11998
+ let reported2 = -1;
11259
11999
  while (!IMPORT_SETTLED.has(current.status)) {
11260
12000
  await sleep2(IMPORT_POLL_MS);
11261
12001
  const next = await client.request(`/api/v2/tables/imports/${encodeURIComponent(current.id)}`, { query: { workspaceId } });
11262
12002
  current = next.data;
11263
- if (process.stderr.isTTY && current.rowsProcessed !== reported) {
11264
- reported = current.rowsProcessed;
11265
- process.stderr.write(`\r${source_default.dim(`${current.status}… ${reported} rows`)}\x1B[K`);
12003
+ if (process.stderr.isTTY && current.rowsProcessed !== reported2) {
12004
+ reported2 = current.rowsProcessed;
12005
+ process.stderr.write(`\r${source_default.dim(`${current.status}… ${reported2} rows`)}\x1B[K`);
11266
12006
  }
11267
12007
  }
11268
- if (process.stderr.isTTY && reported >= 0)
12008
+ if (process.stderr.isTTY && reported2 >= 0)
11269
12009
  process.stderr.write("\r\x1B[K");
11270
12010
  return current;
11271
12011
  }
@@ -11287,7 +12027,7 @@ function validateTargetOptions(options) {
11287
12027
  return intoExisting;
11288
12028
  }
11289
12029
  function attachTableImport(tables) {
11290
- tables.command("import").argument("[path]", "Local CSV file to import; omit when using --file-id").description("Import a CSV, into a new table by default").option("--name <name>", "Identifier for the new table: letters, numbers, and underscores; defaults to the sanitized file name").option("--table-id <id>", "Import into this existing table instead of creating one").addOption(new Option("--mode <append|replace>", "How to write into --table-id (default: append)").choices(["append", "replace"])).option("--folder <path>", "Folder path for the new table").option("--file-id <id>", "Import a file already in the workspace instead of a local path").option("--mapping <json|@file>", "Column mapping (--table-id only)").option("--create-columns <json|@file>", "Columns to create (--table-id only)").option("--timezone <iana>", "Timezone for date parsing, e.g. America/New_York").option("--no-wait", "Return once the import is queued instead of watching it").action(async (path, options, command) => {
12030
+ tables.command("import").argument("[path]", "Local CSV file to import; omit when using --file-id").description("Import a CSV, into a new table by default").option("--name <name>", "Identifier for the new table: letters, numbers, and underscores; defaults to the sanitized file name").option("--table-id <id>", "Import into this existing table instead of creating one").addOption(new Option("--mode <append|replace>", "How to write into --table-id (default: append)").choices(["append", "replace"])).option("--folder <path>", "Folder path for the new table, as shown in the app").option("--file-id <id>", "Import a file already in the workspace instead of a local path").option("--mapping <json|@file>", "Column mapping (--table-id only)").option("--create-columns <json|@file>", "Columns to create (--table-id only)").option("--timezone <iana>", "Timezone for date parsing, e.g. America/New_York").option("--no-wait", "Return once the import is queued instead of watching it").action(async (path, options, command) => {
11291
12031
  const { client, profile } = clientFrom(command);
11292
12032
  const workspaceId = client.requireWorkspace();
11293
12033
  if (Boolean(path) === Boolean(options.fileId)) {
@@ -11312,7 +12052,7 @@ function attachTableImport(tables) {
11312
12052
  target = {
11313
12053
  type: "new",
11314
12054
  name,
11315
- ...options.folder !== undefined ? { folderPath: options.folder } : {}
12055
+ ...options.folder !== undefined ? { folderPath: encodeFolderPath(options.folder) } : {}
11316
12056
  };
11317
12057
  }
11318
12058
  const started = await client.request(V2_OPERATIONS.createTableImport.path, {
@@ -11360,6 +12100,427 @@ function attachTableImport(tables) {
11360
12100
  });
11361
12101
  }
11362
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
+
11363
12524
  // src/commands/protocol/index.ts
11364
12525
  function group(program2, name) {
11365
12526
  const existing = program2.commands.find((command) => command.name() === name);
@@ -11395,12 +12556,16 @@ function attachProtocolCommands(program2) {
11395
12556
  folders: "listTableFolders",
11396
12557
  createFolder: "createTableFolder"
11397
12558
  });
11398
- attachResourceDirectoryCommands(group(program2, "workflows"), {
12559
+ const workflows = group(program2, "workflows");
12560
+ attachResourceDirectoryCommands(workflows, {
11399
12561
  kind: "workflow",
11400
12562
  resources: "listWorkflows",
11401
12563
  folders: "listWorkflowFolders",
11402
12564
  createFolder: "createWorkflowFolder"
11403
12565
  });
12566
+ attachWorkflowRunFollow(workflows);
12567
+ attachWorkflowRunWait(group(workflows, "runs"));
12568
+ attachLogsFollow(group(program2, "logs"));
11404
12569
  }
11405
12570
 
11406
12571
  // src/terminal/secret-input.ts
@@ -11481,7 +12646,8 @@ var SECRET_RESULT = {
11481
12646
  { header: "name" },
11482
12647
  { header: "scope" },
11483
12648
  { header: "role" },
11484
- { header: "updated", path: "updatedAt", format: "timestamp" }
12649
+ { header: "updated", path: "updatedAt", format: "timestamp" },
12650
+ { header: "description" }
11485
12651
  ]
11486
12652
  };
11487
12653
  function validateSecretValue(value) {
@@ -11492,7 +12658,16 @@ function validateSecretValue(value) {
11492
12658
  }
11493
12659
  return value;
11494
12660
  }
12661
+ function validateDescriptionScope(description, scope) {
12662
+ if (description === undefined)
12663
+ return;
12664
+ if (scope === "personal") {
12665
+ throw new SimApiError("--description is only supported for a workspace secret.", 0);
12666
+ }
12667
+ return description;
12668
+ }
11495
12669
  async function setSecret(name, options, command) {
12670
+ const description = validateDescriptionScope(options.description, options.scope);
11496
12671
  const value = validateSecretValue(options.value ?? await promptSecret());
11497
12672
  const { client, profile } = clientFrom(command);
11498
12673
  const operation = V2_OPERATIONS.setSecret;
@@ -11501,7 +12676,8 @@ async function setSecret(name, options, command) {
11501
12676
  body: {
11502
12677
  workspaceId: client.requireWorkspace(),
11503
12678
  scope: options.scope,
11504
- value
12679
+ value,
12680
+ description
11505
12681
  }
11506
12682
  });
11507
12683
  renderResult("setSecret", profile.output, response.data, SECRET_RESULT);
@@ -11510,72 +12686,7 @@ function attachSecretCommands(program2) {
11510
12686
  const secrets = program2.commands.find((command) => command.name() === "secrets");
11511
12687
  if (!secrets)
11512
12688
  throw new Error("The generated secrets command group is missing");
11513
- 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").action((name, options, command) => setSecret(name, options, command));
11514
- }
11515
-
11516
- // src/runtime/execute.ts
11517
- function cursorSlot(operationSpec) {
11518
- if (operationSpec.query && "cursor" in operationSpec.query)
11519
- return "query";
11520
- if (operationSpec.body && "cursor" in operationSpec.body)
11521
- return "body";
11522
- return null;
11523
- }
11524
- async function executeOperation(operation, commandSpec, operationSpec, invocation) {
11525
- const host = invocation[invocation.length - 1];
11526
- const inheritedFlags = host.optsWithGlobals();
11527
- const flags = {
11528
- ...inheritedFlags.workspace === undefined ? {} : { workspace: inheritedFlags.workspace },
11529
- ...inheritedFlags.allWorkspaces === undefined ? {} : { allWorkspaces: inheritedFlags.allWorkspaces },
11530
- ...invocation[invocation.length - 2]
11531
- };
11532
- const pathPositionalCount = operationSpec.pathParams.filter((param) => !commandSpec.pathFlags?.[param] && !isProfileWorkspacePath(commandSpec, param)).length;
11533
- const positional = invocation.slice(0, pathPositionalCount);
11534
- const requestFlags = { ...flags };
11535
- for (const [index, field] of (commandSpec.positionals ?? []).entries()) {
11536
- requestFlags[camel(flagNameFor(operation, field))] = invocation[pathPositionalCount + index];
11537
- }
11538
- if (commandSpec.confirm && !requestFlags.yes) {
11539
- throw new SimApiError(`${commandSpec.confirm} Re-run with --yes to confirm.`, 0);
11540
- }
11541
- if (commandSpec.allWorkspaces && requestFlags.allWorkspaces && requestFlags.workspace) {
11542
- throw new SimApiError("--all-workspaces cannot be combined with --workspace", 0);
11543
- }
11544
- const { client, profile } = clientFrom(host);
11545
- const hasWorkspaceField = Boolean(operationSpec.query && PROFILE_INJECTED_FIELD in operationSpec.query || operationSpec.body && PROFILE_INJECTED_FIELD in operationSpec.body);
11546
- const omitsWorkspace = commandSpec.allWorkspaces && requestFlags.allWorkspaces === true;
11547
- const request = buildRequest(operation, positional, requestFlags, hasWorkspaceField && !omitsWorkspace ? client.requireWorkspace() : profile.workspaceId);
11548
- const paging = cursorSlot(operationSpec);
11549
- if (paging) {
11550
- const rawLimit = Number.parseInt(String(requestFlags.limit ?? DEFAULT_LIMIT), 10);
11551
- if (Number.isNaN(rawLimit) || rawLimit < 0) {
11552
- throw new SimApiError("--limit must be a non-negative number", 0);
11553
- }
11554
- const limit = rawLimit === 0 ? Number.POSITIVE_INFINITY : rawLimit;
11555
- const pageSize = Math.min(Number.isFinite(limit) ? limit : DEFAULT_LIMIT, DEFAULT_LIMIT);
11556
- const pageLimit = "limit" in (operationSpec[paging] ?? {}) ? { limit: pageSize } : {};
11557
- const rows = [];
11558
- let cursor = null;
11559
- do {
11560
- const page = await client.request(request.path, {
11561
- method: operationSpec.method,
11562
- query: paging === "query" ? { ...request.query, ...pageLimit, cursor } : request.query,
11563
- body: paging === "body" ? { ...request.body ?? {}, ...pageLimit, ...cursor ? { cursor } : {} } : request.body
11564
- });
11565
- rows.push(...page.data);
11566
- cursor = page.nextCursor;
11567
- } while (cursor && rows.length < limit);
11568
- renderPage(profile.output, Number.isFinite(limit) ? rows.slice(0, limit) : rows, commandSpec);
11569
- return;
11570
- }
11571
- const result = await client.request(request.path, {
11572
- method: operationSpec.method,
11573
- query: request.query,
11574
- body: request.body
11575
- });
11576
- renderResult(operation, profile.output, result?.data ?? result, commandSpec, {
11577
- expandedTrace: requestFlags.trace === true
11578
- });
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));
11579
12690
  }
11580
12691
 
11581
12692
  // src/runtime/build.ts
@@ -11693,6 +12804,19 @@ function configureOperation(command, operation, spec) {
11693
12804
  function buildLeaf(operation, spec, leafName) {
11694
12805
  return addMissingArgumentExample(configureOperation(new Command(leafName), operation, spec));
11695
12806
  }
12807
+ function addRenamedCommand(groups, operation, spec, from, to) {
12808
+ const segments = from.split(" ");
12809
+ const [groupName, ...rest] = segments;
12810
+ if (rest.length === 0)
12811
+ throw new Error(`${operation}.renamedFrom "${from}" must include a verb`);
12812
+ let parent = groupFor(groups, groupName);
12813
+ for (const segment of rest.slice(0, -1)) {
12814
+ parent = nestedGroup(parent, segment, { hidden: true });
12815
+ }
12816
+ const leaf = buildLeaf(operation, spec, rest[rest.length - 1]);
12817
+ leaf.hook("preAction", () => warnRenamedCommand(from, to));
12818
+ parent.addCommand(leaf, { hidden: true });
12819
+ }
11696
12820
  function groupFor(groups, name) {
11697
12821
  const existing = groups.get(name);
11698
12822
  if (existing)
@@ -11708,12 +12832,12 @@ function resourceLabel(name) {
11708
12832
  const label = name.endsWith("s") ? name.slice(0, -1) : name;
11709
12833
  return label.replaceAll("-", " ");
11710
12834
  }
11711
- function nestedGroup(parent, name) {
12835
+ function nestedGroup(parent, name, options = {}) {
11712
12836
  const existing = parent.commands.find((candidate) => candidate.name() === name);
11713
12837
  if (existing)
11714
12838
  return existing;
11715
12839
  const created = new Command(name).description(`Manage ${resourceLabel(parent.name())} ${name.replaceAll("-", " ")}`);
11716
- parent.addCommand(created);
12840
+ parent.addCommand(created, { hidden: options.hidden });
11717
12841
  return created;
11718
12842
  }
11719
12843
  function addLeafCommand(groups, operation, spec, segments) {
@@ -11742,6 +12866,7 @@ function variantCommandSpec(spec, variant) {
11742
12866
  }
11743
12867
  function buildGeneratedCommands() {
11744
12868
  const groups = new Map;
12869
+ const renamed = [];
11745
12870
  for (const operation of Object.keys(V2_OPERATIONS)) {
11746
12871
  const spec = CLI_CONTRACT[operation] ?? {};
11747
12872
  const operationSpec = V2_OPERATIONS[operation];
@@ -11764,6 +12889,12 @@ function buildGeneratedCommands() {
11764
12889
  for (const variant of spec.variants ?? []) {
11765
12890
  addLeafCommand(groups, operation, variantCommandSpec(spec, variant), variant.command.split(" "));
11766
12891
  }
12892
+ for (const from of spec.renamedFrom ?? []) {
12893
+ renamed.push({ operation, spec, from, to: segments.join(" ") });
12894
+ }
12895
+ }
12896
+ for (const { operation, spec, from, to } of renamed) {
12897
+ addRenamedCommand(groups, operation, spec, from, to);
11767
12898
  }
11768
12899
  return [...groups.values()].sort((a, b) => a.name().localeCompare(b.name()));
11769
12900
  }
@@ -11772,7 +12903,8 @@ function buildGeneratedCommands() {
11772
12903
  var PROGRAM_DESCRIPTION = "Talk to the Sim API from your terminal";
11773
12904
  var HELP_EPILOGUE = `
11774
12905
  Profiles work like the AWS CLI: settings live in ~/.sim/config, keys in
11775
- ~/.sim/credentials (0600). Select one with -P, --profile, or SIM_PROFILE.
12906
+ ~/.sim/credentials (0600), or under SIM_CONFIG_DIR when it is set. Select one
12907
+ with -P, --profile, or SIM_PROFILE.
11776
12908
 
11777
12909
  Examples:
11778
12910
  $ sim login Authorize the default profile
@@ -11786,18 +12918,11 @@ Examples:
11786
12918
  $ sim workflows import --workflow @wf.json
11787
12919
  $ sim whoami --profile dev
11788
12920
  `;
11789
- function readPackageVersion() {
11790
- const metadata = JSON.parse(readFileSync3(new URL("../package.json", import.meta.url), "utf8"));
11791
- if (typeof metadata !== "object" || metadata === null || !("version" in metadata) || typeof metadata.version !== "string") {
11792
- throw new Error("CLI package metadata is missing a valid version");
11793
- }
11794
- return metadata.version;
11795
- }
11796
12921
  function buildProgram(options = {}) {
11797
12922
  const program2 = new Command;
11798
12923
  program2.name("sim").description(PROGRAM_DESCRIPTION);
11799
12924
  if (options.version !== false)
11800
- program2.version(readPackageVersion());
12925
+ program2.version(CLI_VERSION);
11801
12926
  program2.option("-P, --profile <name>", "Profile to use (env: SIM_PROFILE)").option("--endpoint <url>", "Sim deployment to talk to (env: SIM_ENDPOINT)").option("-w, --workspace <id>", "Workspace to target (env: SIM_WORKSPACE)").addOption(new Option("--output <format>", "Output format for this command").choices([...OUTPUT_FORMATS]));
11802
12927
  program2.addCommand(loginCommand());
11803
12928
  program2.addCommand(logoutCommand());
@@ -11823,6 +12948,10 @@ async function main() {
11823
12948
  console.error(source_default.red(`Error: ${sanitize(error.message)}`));
11824
12949
  process.exit(1);
11825
12950
  }
12951
+ if (isRequestTimeout(error)) {
12952
+ console.error(source_default.red(`Error: the request timed out. ${RAISE_TIMEOUT_HINT}`));
12953
+ process.exit(1);
12954
+ }
11826
12955
  if (error instanceof SimApiError) {
11827
12956
  console.error(source_default.red(`Error: ${sanitize(error.message)}`));
11828
12957
  if (error.code)