sim 2.1.8-preview.97.1 → 2.1.8-preview.99.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 +16 -5
  2. package/dist/index.js +146 -76
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -33,16 +33,27 @@ Sign in to the default profile:
33
33
  sim login
34
34
  ```
35
35
 
36
- The CLI opens Sim in your browser, asks you to approve the requested access,
36
+ With no `--method`, the CLI prefers OAuth when the server offers it and a local
37
+ browser callback is possible. It selects API-key pairing for remote terminals
38
+ or servers without OAuth. Use `sim login --method oauth` to require OAuth;
39
+ if the server does not offer it, login fails without creating an API key.
40
+
41
+ OAuth login opens Sim in your browser, asks you to approve the requested access,
37
42
  and receives the one-time authorization code on a loopback callback. It stores
38
43
  a short-lived OAuth login that renews automatically and can be revoked under
39
44
  **Settings → General → Authorized apps**. Choose a default workspace afterward with
40
45
  `sim configure --set-workspace <id>`.
41
46
 
42
- Use `sim login --no-browser` to print the OAuth URL without opening it. The
43
- browser must still be able to reach the CLI's loopback callback. Over SSH or in
44
- a container without port forwarding, use `sim login --browserless`; that
45
- pairing-code fallback creates a permanent personal API key instead.
47
+ Use `--no-browser` with either method to print the approval URL without opening
48
+ it. OAuth still needs the browser to reach the CLI's loopback callback. Over SSH
49
+ or in a container without port forwarding, use
50
+ `sim login --method api-key --no-browser` to approve from another device and
51
+ create a permanent personal API key. `--method api-key` creates a new key;
52
+ set `SIM_API_KEY` to supply an existing one.
53
+
54
+ Pairing requires a server that supports `platform` API keys. Upgrade older
55
+ deployments that only issue `copilot` keys before login; they are not compatible
56
+ with the platform CLI. OAuth discovery does not check pairing compatibility.
46
57
 
47
58
  Check the active profile and verify that its endpoint, credential, and workspace
48
59
  work together:
package/dist/index.js CHANGED
@@ -4460,6 +4460,22 @@ function toApiError(url, status, contentType, raw) {
4460
4460
  function truncate(value, max) {
4461
4461
  return value.length <= max ? value : `${value.slice(0, max)}…`;
4462
4462
  }
4463
+ function transportErrorMessage(error) {
4464
+ const messages = [];
4465
+ const seen = new Set;
4466
+ let current = error;
4467
+ while (current && typeof current === "object" && messages.length < 4 && !seen.has(current)) {
4468
+ seen.add(current);
4469
+ const candidate = current;
4470
+ const message = typeof candidate.message === "string" ? truncate(candidate.message.replace(/\s+/g, " ").trim(), 300) : "";
4471
+ const code = typeof candidate.code === "string" ? candidate.code : "";
4472
+ const detail = `${message}${code && !message.includes(code) ? ` (${code})` : ""}`;
4473
+ if (detail && messages.at(-1) !== detail)
4474
+ messages.push(detail);
4475
+ current = candidate.cause;
4476
+ }
4477
+ return messages.join(": ") || "Unknown network error";
4478
+ }
4463
4479
  function namesKeyScopeRefusal(error) {
4464
4480
  if (typeof error.code === "string" && KEY_SCOPE_REFUSALS.has(error.code))
4465
4481
  return true;
@@ -4683,7 +4699,7 @@ class SimClient {
4683
4699
  if (timeout?.aborted) {
4684
4700
  throw new SimApiError(`${url} did not answer within ${timeoutMs / 1000}s. ${RAISE_TIMEOUT_HINT}`, 0);
4685
4701
  }
4686
- throw new SimApiError(`Could not reach ${this.profile.endpoint}: ${cause.message}`, 0);
4702
+ throw new SimApiError(`Could not reach ${this.profile.endpoint}: ${transportErrorMessage(cause)}`, 0);
4687
4703
  }
4688
4704
  if (trace)
4689
4705
  traceRequest(method, url, response.status, startedAt);
@@ -8140,12 +8156,12 @@ function createAuthRequest() {
8140
8156
  pairing: pairingCode()
8141
8157
  };
8142
8158
  }
8143
- function buildApprovalUrl(endpoint, auth, scope, workspaceId) {
8159
+ function buildApprovalUrl(endpoint, auth, workspaceId) {
8144
8160
  return buildUrl(endpoint, APPROVAL_PATH, {
8145
8161
  request: auth.request,
8146
8162
  challenge: auth.challenge,
8147
8163
  pairing: auth.pairing,
8148
- scope,
8164
+ scope: "platform",
8149
8165
  workspace: workspaceId
8150
8166
  });
8151
8167
  }
@@ -8451,7 +8467,7 @@ function listenForCallback(server, expectedState, completionUrl, signal, timeout
8451
8467
  const onAbort = () => finish({ ok: false, error: new SimApiError("Login cancelled.", 0) });
8452
8468
  const timer = setTimeout(() => finish({
8453
8469
  ok: false,
8454
- error: new SimApiError(`Timed out after ${Math.round(timeoutMs / 60000)} minutes waiting for the browser. Run sim login again, or use --browserless if this terminal's browser cannot reach it.`, 0)
8470
+ error: new SimApiError(`Timed out after ${Math.round(timeoutMs / 60000)} minutes waiting for the browser. Run sim login again, or use --method api-key if this terminal's browser cannot reach it.`, 0)
8455
8471
  }), timeoutMs);
8456
8472
  signal?.addEventListener("abort", onAbort, { once: true });
8457
8473
  server.on("request", (request, response) => {
@@ -13944,12 +13960,12 @@ function addProfileCommand() {
13944
13960
  console.log(source_default.dim(` Try: sim --profile ${safeOneLine(profileName)} whoami`));
13945
13961
  });
13946
13962
  }
13947
- async function chooseLoginFlow(profile, options, scope) {
13963
+ async function chooseLoginFlow(profile, options) {
13948
13964
  requireSecureEndpoint(profile.endpoint);
13949
- if (options.browserless || scope === "copilot")
13965
+ if (options.method === "api-key")
13950
13966
  return "handoff";
13951
- if (isLikelyRemoteSession() && options.callbackPort === undefined) {
13952
- console.log(source_default.dim(`This looks like a remote session, so the browser on this machine cannot finish an OAuth login; using the pairing code instead. Forward a port and pass --callback-port <port> to sign in through the browser anyway.
13967
+ if (options.method === undefined && isLikelyRemoteSession() && options.callbackPort === undefined) {
13968
+ console.log(source_default.dim(`This looks like a remote session; using the pairing code to create a personal API key. To use OAuth, forward a port and pass --method oauth --callback-port <port>.
13953
13969
  `));
13954
13970
  return "handoff";
13955
13971
  }
@@ -13958,7 +13974,10 @@ async function chooseLoginFlow(profile, options, scope) {
13958
13974
  throw new SimApiError(`Could not reach ${profile.endpoint}. Check the endpoint.`, 0);
13959
13975
  }
13960
13976
  if (status === "unavailable") {
13961
- console.log(source_default.dim(`${profile.endpoint} does not offer OAuth sign-in; using the pairing code instead.
13977
+ if (options.method === "oauth") {
13978
+ throw new SimApiError(`${profile.endpoint} does not offer OAuth sign-in. Enable OAuth on the server or explicitly choose sim login --method api-key.`, 0);
13979
+ }
13980
+ console.log(source_default.dim(`${profile.endpoint} does not offer OAuth sign-in; using the pairing code to create a personal API key.
13962
13981
  `));
13963
13982
  return "handoff";
13964
13983
  }
@@ -14036,16 +14055,12 @@ Waiting for you to approve in the browser…`));
14036
14055
  }
14037
14056
  }
14038
14057
  function loginCommand() {
14039
- return new Command("login").description("Sign in through the browser and store the login for the profile").option("--scope <scope>", 'Key space for the pairing-code handoff; only "copilot" changes anything, and it forces that flow', "platform").option("--no-browser", "Print the URL instead of opening a browser").option("--browserless", "Use the pairing-code handoff for a terminal whose browser cannot reach it (SSH, containers)").option("--read-only", "Ask only for permission to read, never to change anything").option("--callback-port <port>", "Pin the local port the browser returns to").option("-y, --yes", "Overwrite an existing API-key profile without prompting").action(async (options, command) => {
14058
+ return new Command("login").description("Sign in through the browser and store the login for the profile").addOption(new Option("--method <method>", "Credential to obtain: oauth requires OAuth support; api-key creates a permanent key through pairing (auto-selects when omitted)").choices(["oauth", "api-key"])).option("--no-browser", "Print the approval URL without opening it (either login method)").option("--read-only", "Ask only for permission to read, never to change anything").option("--callback-port <port>", "Pin the local port the browser returns to").option("-y, --yes", "Overwrite an existing API-key profile without prompting").action(async (options, command) => {
14040
14059
  const profile = profileFrom(command, { allowUnknownProfile: true });
14041
14060
  const authProfile = resolveAuthenticationProfileName(profile.name);
14042
14061
  if (authProfile !== profile.name) {
14043
14062
  throw new SimApiError(`Profile "${redact(profile.name)}" shares authentication with "${redact(authProfile)}". Run: sim login --profile ${redact(authProfile)}`, 0);
14044
14063
  }
14045
- if (options.scope !== "platform" && options.scope !== "copilot") {
14046
- throw new SimApiError(`Unknown scope "${options.scope}". Use platform or copilot.`, 0);
14047
- }
14048
- const scope = options.scope;
14049
14064
  const snapshot = readLoginProfileSnapshot(profile.name);
14050
14065
  const storedCredential = snapshot.credential;
14051
14066
  if (storedCredential?.kind === "oauth") {
@@ -14059,13 +14074,13 @@ function loginCommand() {
14059
14074
  }
14060
14075
  }
14061
14076
  const callbackPort = parseCallbackPort(options.callbackPort);
14062
- const loginFlow = await chooseLoginFlow(profile, options, scope);
14077
+ const loginFlow = await chooseLoginFlow(profile, options);
14063
14078
  if (loginFlow === "handoff") {
14064
14079
  if (options.readOnly) {
14065
- throw new SimApiError("The pairing-code handoff cannot issue a read-only login; it mints a full API key. Drop --read-only, or sign in through the browser.", 0);
14080
+ throw new SimApiError("API-key login cannot issue a read-only login. Use --method oauth, or drop --read-only.", 0);
14066
14081
  }
14067
14082
  if (callbackPort !== undefined) {
14068
- throw new SimApiError("The pairing-code handoff has no local callback, so --callback-port does not apply. Drop it, or sign in through the browser.", 0);
14083
+ throw new SimApiError("API-key login has no local callback, so --callback-port does not apply. Use --method oauth, or drop --callback-port.", 0);
14069
14084
  }
14070
14085
  }
14071
14086
  await withProfileLoginLease(profile.name, async () => {
@@ -14074,13 +14089,13 @@ function loginCommand() {
14074
14089
  await loginWithOAuth(profile, options, callbackPort, snapshot);
14075
14090
  return;
14076
14091
  }
14077
- await loginWithHandoff(profile, options, scope, snapshot);
14092
+ await loginWithHandoff(profile, options, snapshot);
14078
14093
  });
14079
14094
  });
14080
14095
  }
14081
- async function loginWithHandoff(profile, options, scope, expected) {
14096
+ async function loginWithHandoff(profile, options, expected) {
14082
14097
  const auth = createAuthRequest();
14083
- const url = buildApprovalUrl(profile.endpoint, auth, scope, profile.workspaceId ?? undefined);
14098
+ const url = buildApprovalUrl(profile.endpoint, auth, profile.workspaceId ?? undefined);
14084
14099
  console.log(`Signing in to ${source_default.bold(profile.endpoint)} as profile ${source_default.bold(safeOneLine(profile.name))}`);
14085
14100
  console.log(`
14086
14101
  Pairing code: ${source_default.bold(auth.pairing)}`);
@@ -14093,8 +14108,8 @@ Pairing code: ${source_default.bold(auth.pairing)}`);
14093
14108
  Waiting for approval…`));
14094
14109
  const key = await pollForKey(profile.endpoint, auth);
14095
14110
  try {
14096
- if (key.scope !== scope) {
14097
- 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);
14111
+ if (key.scope !== "platform") {
14112
+ throw new SimApiError(`Server issued a ${key.scope} key, but the CLI requires a platform API key. Update the Sim deployment and try again.`, 0);
14098
14113
  }
14099
14114
  const settings = {
14100
14115
  endpoint: profile.endpoint,
@@ -17240,6 +17255,47 @@ function attachCredentialCommands(program) {
17240
17255
  credentials.command("reconnect").argument("<credentialId>", "Existing OAuth credential to re-authorize").description(describeOperation(V2_OPERATIONS.createCredentialConnection, "Create a short-lived link for reconnecting an OAuth credential")).action((credentialId, _options, command) => createConnectionLink(command, { credentialId }));
17241
17256
  }
17242
17257
 
17258
+ // src/http/ndjson.ts
17259
+ async function* readNdjson(body, protocol) {
17260
+ if (!body) {
17261
+ throw new SimApiError(`${protocol} ended without a response body`, 0);
17262
+ }
17263
+ const reader = body.getReader();
17264
+ const decoder = new TextDecoder;
17265
+ let buffer = "";
17266
+ const parse = (line) => {
17267
+ const trimmed = line.trim();
17268
+ if (!trimmed)
17269
+ return;
17270
+ try {
17271
+ return JSON.parse(trimmed);
17272
+ } catch {
17273
+ throw new SimApiError(`${protocol} returned malformed data`, 0);
17274
+ }
17275
+ };
17276
+ try {
17277
+ while (true) {
17278
+ const { done, value } = await reader.read();
17279
+ buffer += done ? decoder.decode() : decoder.decode(value, { stream: true });
17280
+ const lines = buffer.split(`
17281
+ `);
17282
+ buffer = done ? "" : lines.pop() ?? "";
17283
+ for (const line of lines) {
17284
+ const event = parse(line);
17285
+ if (event !== undefined)
17286
+ yield event;
17287
+ }
17288
+ if (done)
17289
+ return;
17290
+ }
17291
+ } finally {
17292
+ reader.cancel().catch(() => {
17293
+ return;
17294
+ });
17295
+ reader.releaseLock();
17296
+ }
17297
+ }
17298
+
17243
17299
  // src/commands/protocol/result.ts
17244
17300
  function printProtocolResult(format, result) {
17245
17301
  const fields = Object.entries(result).map(([key, value]) => [key, text(value)]);
@@ -17247,72 +17303,28 @@ function printProtocolResult(format, result) {
17247
17303
  }
17248
17304
 
17249
17305
  // src/commands/protocol/chat.ts
17250
- function parseChatStreamLine(line) {
17251
- const trimmed = line.trim();
17252
- if (!trimmed)
17253
- return;
17254
- try {
17255
- return JSON.parse(trimmed);
17256
- } catch {
17257
- throw new SimApiError("Chat stream returned malformed data", 0);
17258
- }
17259
- }
17260
17306
  async function readChatStream(response, onChunk) {
17261
- if (!response.body) {
17262
- throw new SimApiError("Chat stream ended without a response body", 0);
17263
- }
17264
- const reader = response.body.getReader();
17265
- const decoder = new TextDecoder;
17266
- let buffer = "";
17267
- let finalResult;
17268
- const processLine = (line) => {
17269
- const event = parseChatStreamLine(line);
17270
- if (!event || event.type === "heartbeat")
17271
- return false;
17307
+ for await (const value of readNdjson(response.body, "Chat stream")) {
17308
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
17309
+ throw new SimApiError("Chat stream returned an unknown event", 0);
17310
+ }
17311
+ const event = value;
17312
+ if (event.type === "heartbeat")
17313
+ continue;
17272
17314
  if (event.type === "chunk") {
17273
17315
  if (event.content)
17274
17316
  onChunk(sanitize(event.content));
17275
- return false;
17317
+ continue;
17276
17318
  }
17277
17319
  if (event.type === "error") {
17278
17320
  throw new SimApiError(event.error || "Chat request failed", 0);
17279
17321
  }
17280
17322
  if (event.type === "final") {
17281
- finalResult = event.data;
17282
- return true;
17323
+ return event.data;
17283
17324
  }
17284
17325
  throw new SimApiError("Chat stream returned an unknown event", 0);
17285
- };
17286
- try {
17287
- let ended = false;
17288
- while (!ended) {
17289
- const { done, value } = await reader.read();
17290
- if (done)
17291
- break;
17292
- buffer += decoder.decode(value, { stream: true });
17293
- const lines = buffer.split(`
17294
- `);
17295
- buffer = lines.pop() ?? "";
17296
- for (const line of lines) {
17297
- if (processLine(line)) {
17298
- ended = true;
17299
- break;
17300
- }
17301
- }
17302
- }
17303
- if (!ended) {
17304
- buffer += decoder.decode();
17305
- processLine(buffer);
17306
- }
17307
- if (!finalResult) {
17308
- throw new SimApiError("Chat stream ended without a final result", 0);
17309
- }
17310
- return finalResult;
17311
- } finally {
17312
- reader.cancel().catch(() => {
17313
- return;
17314
- });
17315
17326
  }
17327
+ throw new SimApiError("Chat stream ended without a final result", 0);
17316
17328
  }
17317
17329
  function ignoreBrokenPipe(stream) {
17318
17330
  const onError = (error) => {
@@ -18303,6 +18315,7 @@ function attachTableImport(tables) {
18303
18315
  // src/commands/protocol/workflow-run-follow.ts
18304
18316
  var AGENT_STREAM_PROTOCOL_HEADER = "x-sim-stream-protocol";
18305
18317
  var AGENT_STREAM_PROTOCOL_V1 = "agent-events-v1";
18318
+ var WORKFLOW_RESULT_STREAM_CONTENT_TYPE = "application/x-ndjson";
18306
18319
  var DONE_SENTINEL = "[DONE]";
18307
18320
  function resolveWorkflowRunSelection(flags) {
18308
18321
  const manual = flags.manual === true;
@@ -18354,6 +18367,59 @@ function stringField(frame, key) {
18354
18367
  const value = frame[key];
18355
18368
  return typeof value === "string" ? value : null;
18356
18369
  }
18370
+ async function readWorkflowResult(response) {
18371
+ const contentType = (response.headers.get("content-type") ?? "").toLowerCase();
18372
+ if (!contentType.includes(WORKFLOW_RESULT_STREAM_CONTENT_TYPE)) {
18373
+ let envelope;
18374
+ try {
18375
+ envelope = await response.json();
18376
+ } catch {
18377
+ throw new SimApiError(`Workflow run returned malformed JSON${contentType ? ` as ${contentType}` : ""}`, response.status);
18378
+ }
18379
+ if (!isRecord(envelope)) {
18380
+ throw new SimApiError("Workflow run returned an invalid result envelope", response.status);
18381
+ }
18382
+ return isRecord(envelope.data) ? envelope.data : envelope;
18383
+ }
18384
+ for await (const value of readNdjson(response.body, "Workflow result stream")) {
18385
+ if (!isRecord(value) || typeof value.type !== "string") {
18386
+ throw new SimApiError("Workflow result stream returned an unknown event", response.status);
18387
+ }
18388
+ if (value.type === "heartbeat")
18389
+ continue;
18390
+ if (value.type === "error") {
18391
+ throw new SimApiError(safeOneLine(typeof value.error === "string" ? value.error : "Workflow run failed"), typeof value.status === "number" ? value.status : 0, typeof value.code === "string" ? value.code : null);
18392
+ }
18393
+ if (value.type === "final" && isRecord(value.data))
18394
+ return value.data;
18395
+ throw new SimApiError("Workflow result stream returned an unknown event", response.status);
18396
+ }
18397
+ throw new SimApiError("Workflow result stream ended without a final result", response.status);
18398
+ }
18399
+ async function runWithResultStream(workflowId, command) {
18400
+ const flags = command.optsWithGlobals();
18401
+ const { client, profile } = clientFrom(command);
18402
+ const operation = V2_OPERATIONS.executeWorkflow;
18403
+ const commandSpec = CLI_CONTRACT.executeWorkflow ?? {};
18404
+ try {
18405
+ const request = buildRequest("executeWorkflow", [workflowId], flags, profile.workspaceId);
18406
+ const response = await client.requestRaw(request.path, {
18407
+ method: operation.method,
18408
+ query: request.query,
18409
+ body: request.body,
18410
+ headers: { ...request.headers, accept: WORKFLOW_RESULT_STREAM_CONTENT_TYPE }
18411
+ });
18412
+ const payload = await readWorkflowResult(response);
18413
+ renderResult("executeWorkflow", profile.output, payload, commandSpec, {
18414
+ expandedTrace: flags.trace === true
18415
+ });
18416
+ const failure = runFailureMessage("executeWorkflow", payload);
18417
+ if (failure)
18418
+ throw new SimApiError(failure, 0);
18419
+ } catch (error) {
18420
+ throw retypeApiError(error, "executeWorkflow", commandSpec, operation);
18421
+ }
18422
+ }
18357
18423
  async function* sseData(body) {
18358
18424
  const reader = body.getReader();
18359
18425
  const decoder = new TextDecoder;
@@ -18525,6 +18591,10 @@ function followOrDelegate(previous) {
18525
18591
  if (flags.includeThinking === true || flags.includeToolCalls === true) {
18526
18592
  throw new SimApiError("--include-thinking and --include-tool-calls describe a stream; add --follow", 0);
18527
18593
  }
18594
+ if (flags.async !== true) {
18595
+ await runWithResultStream(workflowId, command);
18596
+ return;
18597
+ }
18528
18598
  if (previous) {
18529
18599
  await previous(command.processedArgs);
18530
18600
  return;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sim",
3
- "version": "2.1.8-preview.97.1",
3
+ "version": "2.1.8-preview.99.1",
4
4
  "description": "Sim CLI - talk to the Sim API from your terminal",
5
5
  "type": "module",
6
6
  "bin": {