@trydreamstate/cli 1.2.0 → 1.2.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.
package/dist/index.js CHANGED
@@ -4,6 +4,8 @@ Object.defineProperty(exports, "__esModule", { value: true });
4
4
  exports.CliUsageError = exports.CLI_EXIT = void 0;
5
5
  exports.parseCliArgs = parseCliArgs;
6
6
  exports.toolTurnToken = toolTurnToken;
7
+ exports.omitsToolTurnForInlineTableRead = omitsToolTurnForInlineTableRead;
8
+ exports.capabilityIdempotencyKey = capabilityIdempotencyKey;
7
9
  exports.refusalEnvelope = refusalEnvelope;
8
10
  exports.openWebUrl = openWebUrl;
9
11
  exports.exitCodeForError = exitCodeForError;
@@ -17,6 +19,7 @@ const node_os_1 = require("node:os");
17
19
  const node_util_1 = require("node:util");
18
20
  const config_1 = require("./config");
19
21
  const turnCache_1 = require("./turnCache");
22
+ const runWait_1 = require("./runWait");
20
23
  const client_1 = require("./client");
21
24
  const skillInstaller_1 = require("./skillInstaller");
22
25
  exports.CLI_EXIT = { SUCCESS: 0, UNCLASSIFIED: 1, USAGE: 2, AUTH: 3, NOT_FOUND: 4, SERVER: 5, RATE_LIMITED: 6, APPROVAL: 7, TIMEOUT: 8, REFUSED: 9, TRANSPORT: 10 };
@@ -26,13 +29,15 @@ const USAGE = `Dreamstate CLI
26
29
  Usage:
27
30
  dreamstate --help
28
31
  dreamstate --version
29
- dreamstate login
30
- dreamstate login --token <ds_user_...|ds_live_...>
32
+ dreamstate login [--no-skills]
33
+ dreamstate login [--profile <name>] --token <ds_user_...|ds_live_...> [--no-skills]
31
34
  dreamstate logout
35
+ dreamstate status [--json-output|--json]
36
+ dreamstate profiles <list|use|current|remove|env> [name] [--json-output|--json]
32
37
  dreamstate whoami [--json-output|--json]
33
38
  dreamstate workspaces list [--json-output|--json]
34
39
  dreamstate workspaces use <id|slug>
35
- dreamstate auth login [--api-key-file <path>]
40
+ dreamstate auth login [--api-key-file <path>] [--no-skills]
36
41
  dreamstate auth whoami [--json-output|--json]
37
42
  dreamstate packs list [--json-output|--json]
38
43
  dreamstate packs get <pack_id> [--json-output|--json]
@@ -45,7 +50,7 @@ Usage:
45
50
  dreamstate commands search <term> [--json-output|--json]
46
51
  dreamstate tools search --goal <goal> [--terms <csv>] [--limit <n>] [--toolset <pack_id>] [--json-output|--json]
47
52
  dreamstate tools get <capability_id> [--json-output|--json]
48
- dreamstate tools run <capability_id> [--params <object>|--json <body>] [--turn-token <token>] (--live | --dry-run | --mode <smoke_test|dry_run|live>) [--agent-session-id <uuid>] [--approved] [--approved-effect-unknown] [--max-credits <n>] [--idempotency-key <key>] [--open] [--json-output|--json]
53
+ dreamstate tools run <capability_id> [--params <object>|--json <body>] [--turn-token <token>] (--live | --dry-run | --mode <smoke_test|dry_run|live>) [--agent-session-id <uuid>] [--approved] [--approved-effect-unknown] [--max-credits <n>] [--idempotency-key <key>] [--wait-timeout <seconds>] [--no-wait] [--open] [--json-output|--json]
49
54
  dreamstate sequences <verb> [args] [flags]
50
55
  dreamstate workflows <verb> [args] [flags]
51
56
  dreamstate tables import --table-id <uuid> --file <path> [--format <csv|json|jsonl|xlsx>] [--preview] [--upsert-key <header>] [--background] [--json-output|--json]
@@ -53,10 +58,11 @@ Usage:
53
58
  dreamstate runs get <run_id> [--json-output|--json]
54
59
  dreamstate runs resume <run_id> --state-version <n> [--reason <text>] [--idempotency-key <key>] [--open] [--json-output|--json]
55
60
  dreamstate runs events <run_id> [--json-output|--json]
56
- dreamstate runs cancel <run_id> --json <body> [--idempotency-key <key>] [--json-output|--json]
61
+ dreamstate runs cancel <run_id> [--state-version <n>] [--reason <text>] [--json <body>] [--idempotency-key <key>] [--json-output|--json]
57
62
 
58
63
  All catalog discovery reads live API records. Set DREAMSTATE_API_URL and DREAMSTATE_API_KEY.
59
- login opens browser approval; login --token stores a ds_user_ session or ds_live_ key (for SSH or remote machines) after verifying it with whoami.
64
+ Run responses wait for terminal run status by default, timeout default is 120 seconds, --no-wait returns immediate response. Timeout prints latest run envelope and exits ${runWait_1.RUN_WAIT_TIMEOUT_EXIT_CODE}.
65
+ login opens browser approval and installs Dreamstate skills for detected agents; --no-skills skips installation. login --token stores a ds_user_ session or ds_live_ key (for SSH or remote machines) after verifying it with whoami.
60
66
  Global flag --workspace <id|slug> (any command) or DREAMSTATE_WORKSPACE picks the workspace for one call; workspaces use stores a default. Flag beats env beats stored. A ds_live_ key is bound to one workspace and cannot switch.
61
67
  logout deletes the stored credentials file. --agents takes space-separated, comma-separated or repeated values.
62
68
  `;
@@ -106,6 +112,22 @@ function nonNegativeInteger(raw, flag) {
106
112
  throw new CliUsageError(`${flag} must be a non-negative integer`);
107
113
  return value;
108
114
  }
115
+ // runs.get returns { data: { run: { state_version } } }. Cancel posts
116
+ // expected_state_version, and a missing read must not invent 0.
117
+ function stateVersionFromRunRead(payload) {
118
+ const record = payload && typeof payload === 'object' && !Array.isArray(payload) ? payload : null;
119
+ const data = record && record.data && typeof record.data === 'object' && !Array.isArray(record.data)
120
+ ? record.data
121
+ : null;
122
+ const run = data && data.run && typeof data.run === 'object' && !Array.isArray(data.run)
123
+ ? data.run
124
+ : null;
125
+ const version = run?.state_version;
126
+ if (typeof version !== 'number' || !Number.isSafeInteger(version) || version < 0) {
127
+ throw new client_1.CliError('runs cancel could not read state_version from the run. Pass --state-version <n>.', 'invalid_response', 0);
128
+ }
129
+ return version;
130
+ }
109
131
  function parsePacks(argv) {
110
132
  const { values, positionals } = (0, node_util_1.parseArgs)({ args: argv, options: { goal: { type: 'string' }, 'json-output': { type: 'boolean' }, help: { type: 'boolean', short: 'h' } }, allowPositionals: true, strict: true });
111
133
  if (values.help)
@@ -215,7 +237,7 @@ function parseTools(argv) {
215
237
  'idempotency-key': { type: 'string' }, 'json-output': { type: 'boolean' }, open: { type: 'boolean' },
216
238
  'dry-run': { type: 'boolean' }, live: { type: 'boolean' }, mode: { type: 'string' },
217
239
  'agent-session-id': { type: 'string' }, approved: { type: 'boolean' }, 'approved-effect-unknown': { type: 'boolean' },
218
- 'max-credits': { type: 'string' }, 'turn-token': { type: 'string' }, help: { type: 'boolean', short: 'h' },
240
+ 'max-credits': { type: 'string' }, 'turn-token': { type: 'string' }, 'wait-timeout': { type: 'string' }, 'no-wait': { type: 'boolean' }, help: { type: 'boolean', short: 'h' },
219
241
  }, allowPositionals: true, strict: true });
220
242
  if (values.help)
221
243
  return { command: 'help' };
@@ -268,7 +290,8 @@ function parseTools(argv) {
268
290
  // run without one. What was missing was any way to ASK for live.
269
291
  if (body.mode !== undefined)
270
292
  requireToolsRunMode(body);
271
- return { command: 'tools_run', capabilityId, body, ...(values['turn-token'] ? { turnToken: values['turn-token'] } : {}), ...(values['idempotency-key'] ? { idempotencyKey: values['idempotency-key'] } : {}), ...(values.open ? { open: true } : {}), ...output };
293
+ const waitTimeout = values['wait-timeout'] === undefined ? 120 : nonNegativeNumber(values['wait-timeout'], '--wait-timeout');
294
+ return { command: 'tools_run', capabilityId, body, waitTimeout, ...(values['no-wait'] ? { noWait: true } : {}), ...(values['turn-token'] ? { turnToken: values['turn-token'] } : {}), ...(values['idempotency-key'] ? { idempotencyKey: values['idempotency-key'] } : {}), ...(values.open ? { open: true } : {}), ...output };
272
295
  }
273
296
  throw new CliUsageError('Usage: dreamstate tools <search|get|run>');
274
297
  }
@@ -323,9 +346,20 @@ function parseRuns(argv) {
323
346
  return { command: 'runs_resume', runId, stateVersion: nonNegativeInteger(values['state-version'], '--state-version'), ...(values.reason ? { reason: values.reason } : {}), ...(values['idempotency-key'] ? { idempotencyKey: values['idempotency-key'] } : {}), ...(values.open ? { open: true } : {}), ...output };
324
347
  }
325
348
  if (positionals[0] === 'cancel') {
326
- if (!values.json)
327
- throw new CliUsageError('runs cancel requires --json <body>');
328
- return { command: 'runs_cancel', runId, body: parseJsonObject(values.json), ...(values['idempotency-key'] ? { idempotencyKey: values['idempotency-key'] } : {}), ...output };
349
+ const idempotency = values['idempotency-key'] ? { idempotencyKey: values['idempotency-key'] } : {};
350
+ if (values.json && (values['state-version'] !== undefined || values.reason)) {
351
+ throw new CliUsageError('Use either --json <body> or --state-version and --reason with runs cancel, not both.');
352
+ }
353
+ if (values.json)
354
+ return { command: 'runs_cancel', runId, body: parseJsonObject(values.json), ...idempotency, ...output };
355
+ return {
356
+ command: 'runs_cancel',
357
+ runId,
358
+ ...(values['state-version'] !== undefined ? { stateVersion: nonNegativeInteger(values['state-version'], '--state-version') } : {}),
359
+ ...(values.reason ? { reason: values.reason } : {}),
360
+ ...idempotency,
361
+ ...output,
362
+ };
329
363
  }
330
364
  throw new CliUsageError('Usage: dreamstate runs <get|events|cancel> <run_id>');
331
365
  }
@@ -342,14 +376,14 @@ function parseCliArgs(argv) {
342
376
  return { command: 'version' };
343
377
  const [group, ...rest] = argv;
344
378
  if (group === 'login') {
345
- const { values, positionals } = (0, node_util_1.parseArgs)({ args: rest, options: { token: { type: 'string' }, 'api-key-file': { type: 'string' }, 'json-output': { type: 'boolean' }, help: { type: 'boolean', short: 'h' } }, allowPositionals: true, strict: true });
379
+ const { values, positionals } = (0, node_util_1.parseArgs)({ args: rest, options: { token: { type: 'string' }, profile: { type: 'string' }, 'api-key-file': { type: 'string' }, 'no-skills': { type: 'boolean' }, 'json-output': { type: 'boolean' }, help: { type: 'boolean', short: 'h' } }, allowPositionals: true, strict: true });
346
380
  if (values.help)
347
381
  return { command: 'help' };
348
382
  if (positionals.length)
349
383
  throw new CliUsageError('Usage: dreamstate login [--token <ds_user_...|ds_live_...>]');
350
384
  if (values.token !== undefined && values['api-key-file'])
351
385
  throw new CliUsageError('Use either --token or --api-key-file, not both.');
352
- return { command: 'login', ...(values.token !== undefined ? { token: values.token } : {}), ...(values['api-key-file'] ? { apiKeyFile: values['api-key-file'] } : {}), ...outputOption(values) };
386
+ return { command: 'login', ...(values.token !== undefined ? { token: values.token } : {}), ...(values['api-key-file'] ? { apiKeyFile: values['api-key-file'] } : {}), ...(values.profile ? { profile: (0, config_1.validateProfileName)(values.profile) } : {}), ...(values['no-skills'] ? { noSkills: true } : {}), ...outputOption(values) };
353
387
  }
354
388
  if (group === 'logout') {
355
389
  const { values, positionals } = (0, node_util_1.parseArgs)({ args: rest, options: { 'json-output': { type: 'boolean' }, help: { type: 'boolean', short: 'h' } }, allowPositionals: true, strict: true });
@@ -359,6 +393,27 @@ function parseCliArgs(argv) {
359
393
  throw new CliUsageError('Usage: dreamstate logout');
360
394
  return { command: 'logout', ...outputOption(values) };
361
395
  }
396
+ if (group === 'status') {
397
+ const { values, positionals } = (0, node_util_1.parseArgs)({ args: rest, options: { 'json-output': { type: 'boolean' } }, allowPositionals: true, strict: true });
398
+ if (!positionals.length)
399
+ return { command: 'status', ...outputOption(values) };
400
+ throw new CliUsageError('Usage: dreamstate status [--json-output]');
401
+ }
402
+ if (group === 'profiles') {
403
+ const { values, positionals } = (0, node_util_1.parseArgs)({ args: rest, options: { 'json-output': { type: 'boolean' } }, allowPositionals: true, strict: true });
404
+ const output = outputOption(values);
405
+ if (positionals.length === 1 && positionals[0] === 'list')
406
+ return { command: 'profiles_list', ...output };
407
+ if (positionals.length === 1 && positionals[0] === 'current')
408
+ return { command: 'profiles_current', ...output };
409
+ if (positionals.length === 2 && positionals[0] === 'use')
410
+ return { command: 'profiles_use', name: (0, config_1.validateProfileName)(positionals[1]), ...output };
411
+ if (positionals.length === 2 && positionals[0] === 'remove')
412
+ return { command: 'profiles_remove', name: (0, config_1.validateProfileName)(positionals[1]), ...output };
413
+ if (positionals.length === 2 && positionals[0] === 'env')
414
+ return { command: 'profiles_env', name: (0, config_1.validateProfileName)(positionals[1]), ...output };
415
+ throw new CliUsageError('Usage: dreamstate profiles <list|use <name>|current|remove <name>|env <name>>');
416
+ }
362
417
  if (group === 'whoami') {
363
418
  const { values, positionals } = (0, node_util_1.parseArgs)({ args: rest, options: { 'json-output': { type: 'boolean' } }, allowPositionals: true, strict: true });
364
419
  if (positionals.length === 0)
@@ -376,9 +431,9 @@ function parseCliArgs(argv) {
376
431
  throw new CliUsageError('Usage: dreamstate workspaces <list|use <id|slug>>');
377
432
  }
378
433
  if (group === 'auth') {
379
- const { values, positionals } = (0, node_util_1.parseArgs)({ args: rest, options: { 'api-key-file': { type: 'string' }, 'json-output': { type: 'boolean' } }, allowPositionals: true, strict: true });
434
+ const { values, positionals } = (0, node_util_1.parseArgs)({ args: rest, options: { 'api-key-file': { type: 'string' }, 'no-skills': { type: 'boolean' }, 'json-output': { type: 'boolean' } }, allowPositionals: true, strict: true });
380
435
  if (positionals.length === 1 && positionals[0] === 'login')
381
- return { command: 'login', ...(values['api-key-file'] ? { apiKeyFile: values['api-key-file'] } : {}), ...outputOption(values) };
436
+ return { command: 'login', ...(values['api-key-file'] ? { apiKeyFile: values['api-key-file'] } : {}), ...(values['no-skills'] ? { noSkills: true } : {}), ...outputOption(values) };
382
437
  if (positionals.length === 1 && positionals[0] === 'whoami')
383
438
  return { command: 'whoami', ...outputOption(values) };
384
439
  throw new CliUsageError('Usage: dreamstate auth <login|whoami>');
@@ -438,6 +493,57 @@ function toolTurnToken(value) {
438
493
  return data.tool_turn_token;
439
494
  throw new client_1.CliError('tools get did not return a tool_turn_token.', 'invalid_tool_turn_token', 0);
440
495
  }
496
+ /**
497
+ * null suppresses Idempotency-Key. undefined lets runCapability mint one.
498
+ * A fresh random key never replays, but the server treats any Idempotency-Key
499
+ * as a replay request and refuses the inline read (isInlineReadEligible).
500
+ * Omit the key only when the fetched contract says mutates === false and
501
+ * idempotency_required is not true. Missing flags keep the auto key: a
502
+ * caller-supplied turn token skips tools get, and live writes still need one.
503
+ */
504
+ /**
505
+ * Free sync reads. The server runs these on the inline read path when
506
+ * the body has no tool_turn_token and the request has no Idempotency-Key
507
+ * (isInlineReadEligible). A tools get before the run is a second HTTP the
508
+ * read does not need. Measured 2026-10-04: tables.workbooks.get full live CLI
509
+ * 0.726s against whoami 0.455s, about 0.27s of tools get per harness read-back.
510
+ * knowledge.brain.context.get is the same class (catalog: credit_class free,
511
+ * async_policy.mode sync, mutates not set). Live 2026-10-05 a standalone
512
+ * tools get of that id was 0.400s including node startup, next to whoami
513
+ * 0.422s, so the same-process skip is one HTTP, not a second process.
514
+ * The set name is historical. Writes are not in this set: they stay fetch-first.
515
+ */
516
+ const INLINE_READ_CAPABILITIES = new Set([
517
+ 'tables.workbooks.get',
518
+ 'tables.workbooks.overview',
519
+ 'tables.workbooks.list',
520
+ 'tables.columns.list',
521
+ 'tables.columns.stats',
522
+ 'tables.rows.query',
523
+ 'tables.rows.get',
524
+ 'tables.rows.count',
525
+ 'knowledge.brain.context.get',
526
+ // Both are stored, free, synchronous inbox reads. Keeping either behind a
527
+ // tools_get turns one list or thread open into two HTTP requests, while the
528
+ // canonical inline-read gate already validates their read-only contract.
529
+ 'unibox.inbox.unified_list',
530
+ 'unibox.inbox.conversation_get',
531
+ ]);
532
+ function omitsToolTurnForInlineTableRead(capabilityId) {
533
+ return INLINE_READ_CAPABILITIES.has(capabilityId);
534
+ }
535
+ function capabilityIdempotencyKey(fetched) {
536
+ const root = fetched && typeof fetched === 'object' ? fetched : {};
537
+ const data = root.data && typeof root.data === 'object' && !Array.isArray(root.data)
538
+ ? root.data
539
+ : {};
540
+ const capability = data.capability && typeof data.capability === 'object' && !Array.isArray(data.capability)
541
+ ? data.capability
542
+ : {};
543
+ if (capability.mutates === false && capability.idempotency_required !== true)
544
+ return null;
545
+ return undefined;
546
+ }
441
547
  function code(error) {
442
548
  return error && typeof error === 'object' && typeof error.code === 'string'
443
549
  ? error.code
@@ -617,14 +723,94 @@ function whoamiData(value) {
617
723
  const data = value && typeof value === 'object' ? value.data : undefined;
618
724
  return data && typeof data === 'object' ? data : {};
619
725
  }
726
+ function statusData(whoami, health) {
727
+ const meta = whoami && typeof whoami === 'object' ? whoami.meta : null;
728
+ const serverVersion = meta && typeof meta === 'object' ? meta.server_version : null;
729
+ const minimumCliVersion = meta && typeof meta === 'object' ? meta.minimum_cli_version : null;
730
+ const healthData = health && typeof health === 'object' ? health.data : null;
731
+ const healthVersion = healthData && typeof healthData === 'object' ? healthData.version : null;
732
+ const version = typeof serverVersion === 'string' ? serverVersion : typeof healthVersion === 'string' ? healthVersion : null;
733
+ const minimum = typeof minimumCliVersion === 'string' ? minimumCliVersion : null;
734
+ const parts = (value) => value.replace(/^v/, '').split('-')[0].split('.').map((part) => Number(part));
735
+ const [minimumMajor = 0, minimumMinor = 0, minimumPatch = 0] = minimum ? parts(minimum) : [];
736
+ const [cliMajor = 0, cliMinor = 0, cliPatch = 0] = parts(client_1.CLI_VERSION);
737
+ const compatible = !minimum || minimumMajor < cliMajor || (minimumMajor === cliMajor && (minimumMinor < cliMinor || (minimumMinor === cliMinor && minimumPatch <= cliPatch)));
738
+ return {
739
+ cli_version: client_1.CLI_VERSION,
740
+ api_version: version,
741
+ minimum_cli_version: minimum,
742
+ minimum_cli_version_published: minimum !== null,
743
+ compatible,
744
+ upgrade_hint: compatible ? null : 'Run: npm install -g @trydreamstate/cli@latest',
745
+ health,
746
+ whoami: whoamiData(whoami),
747
+ };
748
+ }
620
749
  function activeWorkspace(data) {
621
- const activeId = data.workspace_id ?? data.active_workspace_id ?? undefined;
750
+ const activeId = data.workspace_id;
622
751
  return activeId ? data.workspaces?.find((workspace) => workspace.id === activeId) : undefined;
623
752
  }
624
753
  function wait(milliseconds) {
625
754
  return new Promise((resolve) => setTimeout(resolve, milliseconds));
626
755
  }
627
- async function loginWithDeviceAuthorization(runtime, stdout, stderr) {
756
+ async function installHostedSkills(client, agents, runtime, options = {}) {
757
+ const listed = await client.listAllSkills();
758
+ const entries = listed && typeof listed === 'object' && listed.data?.skills;
759
+ const ids = Array.isArray(entries) ? entries.map((skill) => skill && typeof skill === 'object' ? skill.id : undefined).filter((id) => typeof id === 'string') : [];
760
+ if (!ids.length)
761
+ throw new client_1.CliError('Dreamstate API returned no installable skills.', 'invalid_response', 200);
762
+ if (ids.some((id) => !/^[a-z0-9][a-z0-9_-]*$/i.test(id)))
763
+ throw new CliUsageError('Dreamstate API returned an invalid skill id.');
764
+ const skills = [];
765
+ for (const id of ids) {
766
+ const skill = hostedSkill(await client.getSkill(id));
767
+ if (!/^[a-z0-9][a-z0-9_-]*$/i.test(skill.id))
768
+ throw new CliUsageError('Dreamstate API returned an invalid skill id.');
769
+ if (skill.id !== id)
770
+ throw new client_1.CliError('Dreamstate API returned a mismatched skill id.', 'invalid_response', 200);
771
+ skills.push(skill);
772
+ }
773
+ const packs = await client.listPacks();
774
+ const whoami = await client.getWhoami();
775
+ const workspaceId = typeof whoami?.data?.workspace_id === 'string' ? whoami.data.workspace_id : 'unknown';
776
+ const indexVersion = (0, node_crypto_1.createHash)('sha256')
777
+ .update(JSON.stringify(skills.map((skill) => [skill.id, skill.description, skill.body, skill.frontmatter ?? {}])))
778
+ .digest('hex').slice(0, 16);
779
+ const installed = await (0, skillInstaller_1.installSkills)([...skills, (0, skillInstaller_1.routerSkill)(packs)], {
780
+ agents,
781
+ homeDirectory: runtime.homeDirectory,
782
+ project: Boolean(options.project),
783
+ copy: Boolean(options.copy) || process.platform === 'win32',
784
+ workspaceId,
785
+ indexVersion,
786
+ });
787
+ return { files: installed.files, skillIds: [...ids, 'router'] };
788
+ }
789
+ async function installLoginSkills(client, runtime, skip, stderr) {
790
+ if (skip)
791
+ return { skipped: 'disabled' };
792
+ const agents = await (0, skillInstaller_1.detectInstalledSkillAgents)(runtime.homeDirectory);
793
+ if (!agents.length) {
794
+ stderr.write('Run dreamstate skills install to install Dreamstate skills.\n');
795
+ return { skipped: 'no_agents_detected' };
796
+ }
797
+ try {
798
+ const installed = await installHostedSkills(client, agents, runtime);
799
+ stderr.write(`Installed Dreamstate skills into: ${agents.join(', ')}.\n`);
800
+ return { installed: agents, files: installed.files.length };
801
+ }
802
+ catch {
803
+ stderr.write('Run dreamstate skills install to install Dreamstate skills.\n');
804
+ return { skipped: 'installation_failed' };
805
+ }
806
+ }
807
+ function loginResult(value, skills) {
808
+ if (!value || typeof value !== 'object')
809
+ return value;
810
+ const envelope = value;
811
+ return { ...envelope, data: { ...(envelope.data && typeof envelope.data === 'object' ? envelope.data : {}), skills } };
812
+ }
813
+ async function loginWithDeviceAuthorization(runtime, stdout, stderr, profile, noSkills = false, jsonOutput = false) {
628
814
  const environment = runtime.env ?? process.env;
629
815
  const baseUrl = (0, config_1.resolveApiBaseUrl)(environment);
630
816
  const state = runtime.deviceState ?? deviceState();
@@ -637,14 +823,19 @@ async function loginWithDeviceAuthorization(runtime, stdout, stderr) {
637
823
  for (;;) {
638
824
  const claim = await (0, client_1.claimDeviceAuthorization)(baseUrl, authorization.device_code);
639
825
  if (claim.status === 'ready') {
640
- await (0, config_1.storeApiKey)(claim.token, runtimeCredentialsPath(runtime), claim.workspace.id);
826
+ if (profile)
827
+ await (0, config_1.storeCredentialProfile)(profile, claim.token, runtimeCredentialsPath(runtime), claim.workspace.id);
828
+ else
829
+ await (0, config_1.storeApiKey)(claim.token, runtimeCredentialsPath(runtime), claim.workspace.id);
641
830
  const client = new client_1.DreamstateClient({
642
831
  baseUrl,
643
832
  apiKey: claim.token,
644
833
  requestTimeoutMs: runtime.requestTimeoutMs,
645
834
  onVersionWarning: (message) => (runtime.stderr ?? process.stderr).write(`${message}\n`),
646
835
  });
647
- print(await client.getWhoami(), stdout, false);
836
+ const whoami = await client.getWhoami();
837
+ const skills = await installLoginSkills(client, runtime, noSkills, stderr);
838
+ print(loginResult(whoami, skills), stdout, jsonOutput);
648
839
  return;
649
840
  }
650
841
  await (runtime.sleep ?? wait)(authorization.interval_seconds * 1_000);
@@ -670,8 +861,9 @@ async function runCli(argv = process.argv.slice(2), runtime = {}) {
670
861
  try {
671
862
  const extracted = extractWorkspaceFlag(argv);
672
863
  const command = parseCliArgs(extracted.argv);
673
- if (command.command === 'help') {
674
- print({ ok: true, data: { usage: USAGE }, meta: {} }, stdout, false);
864
+ if (command.command === 'help' || command.command === 'pack_help') {
865
+ const packHelp = command.command === 'pack_help' ? command : undefined;
866
+ print({ ok: true, data: { usage: packHelp?.usage ?? USAGE }, meta: {} }, stdout, Boolean(packHelp?.jsonOutput));
675
867
  process.exitCode = exports.CLI_EXIT.SUCCESS;
676
868
  return;
677
869
  }
@@ -699,6 +891,38 @@ async function runCli(argv = process.argv.slice(2), runtime = {}) {
699
891
  process.exitCode = exports.CLI_EXIT.SUCCESS;
700
892
  return;
701
893
  }
894
+ if (command.command === 'profiles_list' || command.command === 'profiles_current' || command.command === 'profiles_use' || command.command === 'profiles_remove' || command.command === 'profiles_env') {
895
+ const path = runtimeCredentialsPath(runtime);
896
+ const environment = runtime.env ?? process.env;
897
+ if (command.command === 'profiles_list') {
898
+ const current = await (0, config_1.currentCredentialProfile)(path, environment);
899
+ const profiles = await (0, config_1.listCredentialProfiles)(path);
900
+ print({ ok: true, data: { current_profile: current, profiles: profiles.map(({ name, workspace }) => ({ name, workspace, active: name === current })) }, meta: {} }, stdout, Boolean(command.jsonOutput));
901
+ }
902
+ else if (command.command === 'profiles_current') {
903
+ print({ ok: true, data: { profile: await (0, config_1.currentCredentialProfile)(path, environment) }, meta: {} }, stdout, Boolean(command.jsonOutput));
904
+ }
905
+ else if (command.command === 'profiles_use') {
906
+ await (0, config_1.useCredentialProfile)(command.name, path);
907
+ print({ ok: true, data: { profile: command.name }, meta: {} }, stdout, Boolean(command.jsonOutput));
908
+ }
909
+ else if (command.command === 'profiles_remove') {
910
+ const removed = await (0, config_1.removeCredentialProfile)(command.name, path);
911
+ print({ ok: true, data: { profile: command.name, removed }, meta: {} }, stdout, Boolean(command.jsonOutput));
912
+ }
913
+ else {
914
+ const profiles = await (0, config_1.listCredentialProfiles)(path);
915
+ if (!profiles.some((profile) => profile.name === command.name))
916
+ throw new CliUsageError(`Profile not found: ${command.name}.`);
917
+ const result = { ok: true, data: { profile: command.name, exports: [`DREAMSTATE_PROFILE=${command.name}`] }, meta: {} };
918
+ if (command.jsonOutput)
919
+ print(result, stdout, true);
920
+ else
921
+ stdout.write(`export DREAMSTATE_PROFILE=${command.name}\n`);
922
+ }
923
+ process.exitCode = exports.CLI_EXIT.SUCCESS;
924
+ return;
925
+ }
702
926
  if (command.command === 'login' && command.token !== undefined) {
703
927
  const token = command.token.trim();
704
928
  if (!(0, config_1.isDreamstateToken)(token))
@@ -715,8 +939,23 @@ async function runCli(argv = process.argv.slice(2), runtime = {}) {
715
939
  onVersionWarning: (message) => stderr.write(`${message}\n`),
716
940
  });
717
941
  const whoami = await client.getWhoami();
718
- await (0, config_1.storeApiKey)(token, runtimeCredentialsPath(runtime));
719
- print(whoami, stdout, Boolean(command.jsonOutput));
942
+ if (command.profile)
943
+ await (0, config_1.storeCredentialProfile)(command.profile, token, runtimeCredentialsPath(runtime));
944
+ else
945
+ await (0, config_1.storeApiKey)(token, runtimeCredentialsPath(runtime));
946
+ const skills = await installLoginSkills(client, runtime, Boolean(command.noSkills), stderr);
947
+ print(loginResult(whoami, skills), stdout, Boolean(command.jsonOutput));
948
+ process.exitCode = exports.CLI_EXIT.SUCCESS;
949
+ return;
950
+ }
951
+ if (command.command === 'status') {
952
+ const environment = runtime.env ?? process.env;
953
+ const credentialFilePath = runtimeCredentialsPath(runtime);
954
+ const apiKey = await (0, config_1.resolveApiKey)(environment, credentialFilePath);
955
+ const workspace = await (0, config_1.resolveWorkspace)(extracted.workspace, environment, credentialFilePath);
956
+ const client = new client_1.DreamstateClient({ baseUrl: (0, config_1.resolveApiBaseUrl)(environment), apiKey, ...(workspace ? { workspace } : {}), requestTimeoutMs: runtime.requestTimeoutMs });
957
+ const [whoami, health] = await Promise.all([client.getStatusWhoami(), client.getHealth()]);
958
+ print({ ok: true, data: statusData(whoami, health), meta: {} }, stdout, Boolean(command.jsonOutput));
720
959
  process.exitCode = exports.CLI_EXIT.SUCCESS;
721
960
  return;
722
961
  }
@@ -725,14 +964,16 @@ async function runCli(argv = process.argv.slice(2), runtime = {}) {
725
964
  const key = await readLoginKey(command, runtime);
726
965
  if (!(0, config_1.isDreamstateToken)(key))
727
966
  throw new CliUsageError('Dreamstate API keys must start with ds_user_ or ds_live_.');
728
- const path = await (0, config_1.storeApiKey)(key, runtimeCredentialsPath(runtime));
729
- print({ ok: true, data: { credentials_path: path }, meta: {} }, stdout, Boolean(command.jsonOutput));
730
- stderr.write('Run dreamstate skills install to install Dreamstate skills.\n');
967
+ const path = command.profile
968
+ ? await (0, config_1.storeCredentialProfile)(command.profile, key, runtimeCredentialsPath(runtime))
969
+ : await (0, config_1.storeApiKey)(key, runtimeCredentialsPath(runtime));
970
+ const client = new client_1.DreamstateClient({ baseUrl: (0, config_1.resolveApiBaseUrl)(runtime.env ?? process.env), apiKey: key, requestTimeoutMs: runtime.requestTimeoutMs, onVersionWarning: (message) => stderr.write(`${message}\n`) });
971
+ const skills = await installLoginSkills(client, runtime, Boolean(command.noSkills), stderr);
972
+ print({ ok: true, data: { credentials_path: path, skills }, meta: {} }, stdout, Boolean(command.jsonOutput));
731
973
  process.exitCode = exports.CLI_EXIT.SUCCESS;
732
974
  return;
733
975
  }
734
- await loginWithDeviceAuthorization(runtime, stdout, stderr);
735
- stderr.write('Run dreamstate skills install to install Dreamstate skills.\n');
976
+ await loginWithDeviceAuthorization(runtime, stdout, stderr, command.profile, Boolean(command.noSkills), Boolean(command.jsonOutput));
736
977
  process.exitCode = exports.CLI_EXIT.SUCCESS;
737
978
  return;
738
979
  }
@@ -824,7 +1065,7 @@ async function runCli(argv = process.argv.slice(2), runtime = {}) {
824
1065
  result = await client.getWhoami();
825
1066
  if (!command.jsonOutput) {
826
1067
  const data = whoamiData(result);
827
- const activeId = data.workspace_id ?? data.active_workspace_id ?? null;
1068
+ const activeId = data.workspace_id ?? null;
828
1069
  const rows = data.workspaces ?? [];
829
1070
  stdout.write(rows.length
830
1071
  ? `${rows.map((row) => `${row.id === activeId ? '*' : ' '} ${row.slug ?? '-'} ${row.name} ${row.role ?? '-'} ${row.id}`).join('\n')}\n`
@@ -841,10 +1082,10 @@ async function runCli(argv = process.argv.slice(2), runtime = {}) {
841
1082
  const match = data.workspaces?.find((row) => row.id.toLowerCase() === wanted || row.slug?.toLowerCase() === wanted);
842
1083
  if (!match)
843
1084
  throw new CliUsageError(`You are not a member of workspace "${command.workspace}". Run dreamstate workspaces list to see the workspaces you can use.`);
844
- const stored = await (0, config_1.readStoredCredentials)(credentialFilePath);
1085
+ const stored = await (0, config_1.readStoredCredentials)(await (0, config_1.selectedCredentialsPath)(credentialFilePath, environment));
845
1086
  if (!stored)
846
1087
  throw new CliUsageError('workspaces use saves the default next to a stored login. Run dreamstate login or dreamstate login --token first.');
847
- await (0, config_1.storeApiKey)(stored.token, credentialFilePath, match.id);
1088
+ await (0, config_1.storeApiKey)(stored.token, await (0, config_1.selectedCredentialsPath)(credentialFilePath, environment), match.id);
848
1089
  stderr.write(`Default workspace: ${match.name} (${match.slug})\n`);
849
1090
  result = { ok: true, data: { workspace: { id: match.id, name: match.name, slug: match.slug }, credentials_path: credentialFilePath }, meta: {} };
850
1091
  break;
@@ -868,18 +1109,17 @@ async function runCli(argv = process.argv.slice(2), runtime = {}) {
868
1109
  result = await client.searchSkillDocuments(command.query);
869
1110
  break;
870
1111
  case 'skills_install': {
871
- const listed = command.skillId === '*' ? await client.listAllSkills() : null;
872
- const entries = listed && typeof listed === 'object' && listed.data?.skills;
873
- const ids = command.skillId === '*'
874
- ? Array.isArray(entries) ? entries.map((skill) => skill && typeof skill === 'object' ? skill.id : undefined).filter((id) => typeof id === 'string') : []
875
- : [command.skillId];
876
- if (!ids.length)
877
- throw new client_1.CliError('Dreamstate API returned no installable skills.', 'invalid_response', 200);
1112
+ if (command.skillId === '*') {
1113
+ const installed = await installHostedSkills(client, command.agents, runtime, { project: command.project, copy: command.copy });
1114
+ result = { ok: true, data: { installed_files: installed.files, skill_ids: installed.skillIds, agents: command.agents, ...(command.project ? { lock_file: 'skills-lock.json' } : {}) }, meta: {} };
1115
+ break;
1116
+ }
1117
+ const ids = [command.skillId];
878
1118
  // A remote id becomes a directory name; refuse traversal before any request or write.
879
1119
  if (ids.some((id) => !/^[a-z0-9][a-z0-9_-]*$/i.test(id)))
880
1120
  throw new CliUsageError('Dreamstate API returned an invalid skill id.');
881
- // One at a time: each read comes from the workspace's private vault,
882
- // which admits two reads at once, and fifteen in parallel failed on prod.
1121
+ // Read each skill one at a time because the workspace's private vault
1122
+ // admits two reads at once, and fifteen parallel reads failed in production.
883
1123
  const skills = [];
884
1124
  for (const id of ids) {
885
1125
  const skill = hostedSkill(await client.getSkill(id));
@@ -937,42 +1177,67 @@ async function runCli(argv = process.argv.slice(2), runtime = {}) {
937
1177
  const pending = command.turnToken
938
1178
  ? null
939
1179
  : await (0, turnCache_1.reusableTurnToken)(turns, baseUrl, turnKey, command.capabilityId, Date.now(), warnTurnCache);
940
- const turnToken = command.turnToken ?? pending ?? toolTurnToken(await client.getTool(command.capabilityId));
941
- const run = () => client.runCapability(command.capabilityId, { ...command.body, tool_turn_token: turnToken }, command.idempotencyKey);
1180
+ const inlineRead = !command.turnToken && !pending && !command.idempotencyKey && omitsToolTurnForInlineTableRead(command.capabilityId);
1181
+ const fetchedForRun = command.turnToken || pending || inlineRead ? null : await client.getTool(command.capabilityId);
1182
+ let turnToken = command.turnToken ?? pending ?? (inlineRead ? undefined : toolTurnToken(fetchedForRun));
1183
+ let idempotencyKey = command.idempotencyKey ?? (inlineRead ? null : (fetchedForRun ? capabilityIdempotencyKey(fetchedForRun) : undefined));
1184
+ let recoveredInlineRead = false;
1185
+ const run = () => client.runCapability(command.capabilityId, {
1186
+ ...command.body,
1187
+ ...(turnToken ? { tool_turn_token: turnToken } : {}),
1188
+ }, idempotencyKey);
942
1189
  try {
943
1190
  try {
944
1191
  result = await run();
945
1192
  }
946
1193
  catch (error) {
947
- // A quote_required refusal binds a fresh quote to this turn, so the
948
- // identical call admits on the next try. Retry once here instead of
949
- // making the caller rerun the same command by hand.
950
- if (command.body.mode !== 'live' || code(error) !== 'quote_required')
1194
+ // The inline skip is only valid while the server still treats this
1195
+ // capability as a free sync read. capability_not_fetched means the
1196
+ // fetch-first fence is on, so retry once on the old get-then-run path.
1197
+ if (inlineRead && !recoveredInlineRead && code(error) === 'capability_not_fetched') {
1198
+ recoveredInlineRead = true;
1199
+ const fetched = await client.getTool(command.capabilityId);
1200
+ turnToken = toolTurnToken(fetched);
1201
+ idempotencyKey = command.idempotencyKey ?? capabilityIdempotencyKey(fetched);
1202
+ result = await run();
1203
+ }
1204
+ else if (command.body.mode !== 'live' || code(error) !== 'quote_required') {
1205
+ // A quote_required refusal binds a fresh quote to this turn, so the
1206
+ // identical call admits on the next try. Retry once here instead of
1207
+ // making the caller rerun the same command by hand.
951
1208
  throw error;
952
- result = await run();
1209
+ }
1210
+ else {
1211
+ result = await run();
1212
+ }
953
1213
  }
954
1214
  }
955
1215
  catch (error) {
956
- if (!command.turnToken && code(error) === 'quote_required') {
1216
+ if (!command.turnToken && turnToken && code(error) === 'quote_required') {
957
1217
  await (0, turnCache_1.rememberTurnToken)(turns, baseUrl, turnKey, command.capabilityId, turnToken, Date.now(), warnTurnCache);
958
1218
  }
959
1219
  throw error;
960
1220
  }
961
1221
  if (!command.turnToken) {
962
- if (command.body.mode === 'dry_run')
1222
+ if (command.body.mode === 'dry_run' && turnToken)
963
1223
  await (0, turnCache_1.rememberTurnToken)(turns, baseUrl, turnKey, command.capabilityId, turnToken, Date.now(), warnTurnCache);
964
1224
  else if (pending)
965
1225
  await (0, turnCache_1.forgetTurnToken)(turns, baseUrl, turnKey, command.capabilityId, warnTurnCache);
966
1226
  }
1227
+ if (!command.noWait && !(0, runWait_1.columnRunReturnsRunId)(command.capabilityId)) {
1228
+ const waited = await (0, runWait_1.waitForRun)(client, result, command.waitTimeout, runtime.sleep ?? wait);
1229
+ result = waited.value;
1230
+ if (waited.timedOut)
1231
+ process.exitCode = runWait_1.RUN_WAIT_TIMEOUT_EXIT_CODE;
1232
+ }
967
1233
  break;
968
1234
  }
969
1235
  case 'tables_import': {
970
1236
  await (0, promises_1.access)(command.file);
971
- const discovered = await client.listTools({
972
- goal: 'prepare a local file import into a table',
973
- query_terms: ['local', 'file', 'import'],
974
- });
975
- const importCapabilityId = discoveredCapabilityId(discovered);
1237
+ // Search ranking is not an import contract. This exact capability owns the
1238
+ // prepare handoff for caller-local files, while nearby upload capabilities
1239
+ // can also match these words and made a real import fail after local work.
1240
+ const importCapabilityId = 'tables.table_sources.csv_import_prepare';
976
1241
  result = await client.runCapability(importCapabilityId, {
977
1242
  // A mutation run needs the turn token its own get minted; without it
978
1243
  // an API-key import was refused after the local file work.
@@ -1002,15 +1267,20 @@ async function runCli(argv = process.argv.slice(2), runtime = {}) {
1002
1267
  case 'runs_events':
1003
1268
  result = await client.getRunEvents(command.runId);
1004
1269
  break;
1005
- case 'runs_cancel':
1006
- result = await client.cancelRun(command.runId, command.body, command.idempotencyKey);
1270
+ case 'runs_cancel': {
1271
+ const body = command.body ?? {
1272
+ expected_state_version: command.stateVersion ?? stateVersionFromRunRead(await client.getRun(command.runId)),
1273
+ ...(command.reason ? { reason: command.reason } : {}),
1274
+ };
1275
+ result = await client.cancelRun(command.runId, body, command.idempotencyKey);
1007
1276
  break;
1277
+ }
1008
1278
  case 'pack': {
1009
1279
  if (command.pack === 'sequences') {
1010
- await require('./commands/sequences').runSequencesCommand(client, command, stdout);
1280
+ await require('./commands/sequences').runSequencesCommand(client, command, stdout, runtime.sleep ?? wait);
1011
1281
  }
1012
1282
  else {
1013
- await require('./commands/workflows').runWorkflowsCommand(client, command, stdout);
1283
+ await require('./commands/workflows').runWorkflowsCommand(client, command, stdout, runtime.sleep ?? wait);
1014
1284
  }
1015
1285
  return;
1016
1286
  }
@@ -1022,7 +1292,8 @@ async function runCli(argv = process.argv.slice(2), runtime = {}) {
1022
1292
  printPackRoute(result, stdout);
1023
1293
  else
1024
1294
  print(result, stdout, Boolean(command.jsonOutput));
1025
- process.exitCode = exports.CLI_EXIT.SUCCESS;
1295
+ if (process.exitCode !== runWait_1.RUN_WAIT_TIMEOUT_EXIT_CODE)
1296
+ process.exitCode = exports.CLI_EXIT.SUCCESS;
1026
1297
  }
1027
1298
  catch (error) {
1028
1299
  const exitCode = exitCodeForError(error);
@@ -0,0 +1,9 @@
1
+ import type { DreamstateClient } from './client';
2
+ export declare const RUN_WAIT_TIMEOUT_EXIT_CODE = 8;
3
+ export declare function columnRunReturnsRunId(capabilityId: string): boolean;
4
+ export interface RunWaitResult {
5
+ value: unknown;
6
+ timedOut: boolean;
7
+ }
8
+ export declare function waitForRun(client: DreamstateClient, initial: unknown, timeoutSeconds: number, sleep: (milliseconds: number) => Promise<void>): Promise<RunWaitResult>;
9
+ //# sourceMappingURL=runWait.d.ts.map