@trydreamstate/cli 1.1.1 → 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,26 +29,28 @@ 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]
39
44
  dreamstate packs route --goal <text> [--json-output|--json]
40
45
  dreamstate skills list [--query <query>] [--limit <n>] [--json-output|--json]
41
- dreamstate skills get <skill_id> [--json-output|--json]
46
+ dreamstate skills get <skill_id> [--file <path>] [--json-output|--json]
42
47
  dreamstate skills search <query> [--limit <n>] [--json-output|--json]
43
48
  dreamstate skills install [--skill <skill_id|*>] [--agents <codex claude-code cursor>] [--project] [--copy] [--json-output|--json]
44
49
  dreamstate skills doctor [--agents <codex claude-code cursor>] [--project] [--json-output|--json]
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)
@@ -138,7 +160,7 @@ function spaceSeparatedAgents(argv) {
138
160
  return out;
139
161
  }
140
162
  function parseSkills(argv) {
141
- const { values, positionals } = (0, node_util_1.parseArgs)({ args: spaceSeparatedAgents(argv), options: { query: { type: 'string' }, limit: { type: 'string' }, skill: { type: 'string' }, agents: { type: 'string', multiple: true }, project: { type: 'boolean' }, copy: { type: 'boolean' }, 'json-output': { type: 'boolean' }, help: { type: 'boolean', short: 'h' } }, allowPositionals: true, strict: true });
163
+ const { values, positionals } = (0, node_util_1.parseArgs)({ args: spaceSeparatedAgents(argv), options: { query: { type: 'string' }, limit: { type: 'string' }, skill: { type: 'string' }, file: { type: 'string' }, agents: { type: 'string', multiple: true }, project: { type: 'boolean' }, copy: { type: 'boolean' }, 'json-output': { type: 'boolean' }, help: { type: 'boolean', short: 'h' } }, allowPositionals: true, strict: true });
142
164
  if (values.help)
143
165
  return { command: 'help' };
144
166
  const agents = () => {
@@ -159,7 +181,7 @@ function parseSkills(argv) {
159
181
  return { command: 'skills_list', query, ...outputOption(values) };
160
182
  }
161
183
  if (positionals[0] === 'get')
162
- return { command: 'skills_get', skillId: requireOne(positionals, 'dreamstate skills get <skill_id>'), ...outputOption(values) };
184
+ return { command: 'skills_get', skillId: requireOne(positionals, 'dreamstate skills get <skill_id>'), ...(values.file?.trim() ? { file: values.file.trim() } : {}), ...outputOption(values) };
163
185
  if (positionals[0] === 'search' && positionals.length > 1) {
164
186
  const query = { query: positionals.slice(1).join(' ') };
165
187
  if (values.limit !== undefined)
@@ -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' };
@@ -242,9 +264,14 @@ function parseTools(argv) {
242
264
  throw new CliUsageError('Use either --params for tool parameters or --json for the complete request, not both.');
243
265
  if (!values.json && !values.params)
244
266
  throw new CliUsageError('tools run requires --params <object> or --json <body>; fetch live schema first with tools get');
245
- const body = values.json ? parseJsonObject(values.json) : {};
267
+ const json = values.json ? parseJsonObject(values.json) : undefined;
268
+ const body = json && ('params' in json || 'mode' in json || 'agent_session_id' in json || 'approved_effect_unknown' in json || 'max_credits' in json)
269
+ ? json
270
+ : {};
246
271
  if (values.params)
247
272
  body.params = parseJsonObject(values.params, '--params');
273
+ else if (json && body !== json)
274
+ body.params = json;
248
275
  // The server never defaults a mode, so a mutating run with none is refused
249
276
  // mode_required; --live and --mode exist so a caller can actually ask for one.
250
277
  // `approved` is no longer sent: the run body schema is strict and has no such
@@ -263,7 +290,8 @@ function parseTools(argv) {
263
290
  // run without one. What was missing was any way to ASK for live.
264
291
  if (body.mode !== undefined)
265
292
  requireToolsRunMode(body);
266
- 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 };
267
295
  }
268
296
  throw new CliUsageError('Usage: dreamstate tools <search|get|run>');
269
297
  }
@@ -318,9 +346,20 @@ function parseRuns(argv) {
318
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 };
319
347
  }
320
348
  if (positionals[0] === 'cancel') {
321
- if (!values.json)
322
- throw new CliUsageError('runs cancel requires --json <body>');
323
- 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
+ };
324
363
  }
325
364
  throw new CliUsageError('Usage: dreamstate runs <get|events|cancel> <run_id>');
326
365
  }
@@ -337,14 +376,14 @@ function parseCliArgs(argv) {
337
376
  return { command: 'version' };
338
377
  const [group, ...rest] = argv;
339
378
  if (group === 'login') {
340
- 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 });
341
380
  if (values.help)
342
381
  return { command: 'help' };
343
382
  if (positionals.length)
344
383
  throw new CliUsageError('Usage: dreamstate login [--token <ds_user_...|ds_live_...>]');
345
384
  if (values.token !== undefined && values['api-key-file'])
346
385
  throw new CliUsageError('Use either --token or --api-key-file, not both.');
347
- 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) };
348
387
  }
349
388
  if (group === 'logout') {
350
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 });
@@ -354,6 +393,27 @@ function parseCliArgs(argv) {
354
393
  throw new CliUsageError('Usage: dreamstate logout');
355
394
  return { command: 'logout', ...outputOption(values) };
356
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
+ }
357
417
  if (group === 'whoami') {
358
418
  const { values, positionals } = (0, node_util_1.parseArgs)({ args: rest, options: { 'json-output': { type: 'boolean' } }, allowPositionals: true, strict: true });
359
419
  if (positionals.length === 0)
@@ -371,9 +431,9 @@ function parseCliArgs(argv) {
371
431
  throw new CliUsageError('Usage: dreamstate workspaces <list|use <id|slug>>');
372
432
  }
373
433
  if (group === 'auth') {
374
- 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 });
375
435
  if (positionals.length === 1 && positionals[0] === 'login')
376
- 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) };
377
437
  if (positionals.length === 1 && positionals[0] === 'whoami')
378
438
  return { command: 'whoami', ...outputOption(values) };
379
439
  throw new CliUsageError('Usage: dreamstate auth <login|whoami>');
@@ -433,6 +493,57 @@ function toolTurnToken(value) {
433
493
  return data.tool_turn_token;
434
494
  throw new client_1.CliError('tools get did not return a tool_turn_token.', 'invalid_tool_turn_token', 0);
435
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
+ }
436
547
  function code(error) {
437
548
  return error && typeof error === 'object' && typeof error.code === 'string'
438
549
  ? error.code
@@ -612,14 +723,94 @@ function whoamiData(value) {
612
723
  const data = value && typeof value === 'object' ? value.data : undefined;
613
724
  return data && typeof data === 'object' ? data : {};
614
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
+ }
615
749
  function activeWorkspace(data) {
616
- const activeId = data.workspace_id ?? data.active_workspace_id ?? undefined;
750
+ const activeId = data.workspace_id;
617
751
  return activeId ? data.workspaces?.find((workspace) => workspace.id === activeId) : undefined;
618
752
  }
619
753
  function wait(milliseconds) {
620
754
  return new Promise((resolve) => setTimeout(resolve, milliseconds));
621
755
  }
622
- 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) {
623
814
  const environment = runtime.env ?? process.env;
624
815
  const baseUrl = (0, config_1.resolveApiBaseUrl)(environment);
625
816
  const state = runtime.deviceState ?? deviceState();
@@ -632,14 +823,19 @@ async function loginWithDeviceAuthorization(runtime, stdout, stderr) {
632
823
  for (;;) {
633
824
  const claim = await (0, client_1.claimDeviceAuthorization)(baseUrl, authorization.device_code);
634
825
  if (claim.status === 'ready') {
635
- 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);
636
830
  const client = new client_1.DreamstateClient({
637
831
  baseUrl,
638
832
  apiKey: claim.token,
639
833
  requestTimeoutMs: runtime.requestTimeoutMs,
640
834
  onVersionWarning: (message) => (runtime.stderr ?? process.stderr).write(`${message}\n`),
641
835
  });
642
- 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);
643
839
  return;
644
840
  }
645
841
  await (runtime.sleep ?? wait)(authorization.interval_seconds * 1_000);
@@ -665,8 +861,9 @@ async function runCli(argv = process.argv.slice(2), runtime = {}) {
665
861
  try {
666
862
  const extracted = extractWorkspaceFlag(argv);
667
863
  const command = parseCliArgs(extracted.argv);
668
- if (command.command === 'help') {
669
- 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));
670
867
  process.exitCode = exports.CLI_EXIT.SUCCESS;
671
868
  return;
672
869
  }
@@ -694,6 +891,38 @@ async function runCli(argv = process.argv.slice(2), runtime = {}) {
694
891
  process.exitCode = exports.CLI_EXIT.SUCCESS;
695
892
  return;
696
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
+ }
697
926
  if (command.command === 'login' && command.token !== undefined) {
698
927
  const token = command.token.trim();
699
928
  if (!(0, config_1.isDreamstateToken)(token))
@@ -710,8 +939,23 @@ async function runCli(argv = process.argv.slice(2), runtime = {}) {
710
939
  onVersionWarning: (message) => stderr.write(`${message}\n`),
711
940
  });
712
941
  const whoami = await client.getWhoami();
713
- await (0, config_1.storeApiKey)(token, runtimeCredentialsPath(runtime));
714
- 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));
715
959
  process.exitCode = exports.CLI_EXIT.SUCCESS;
716
960
  return;
717
961
  }
@@ -720,14 +964,16 @@ async function runCli(argv = process.argv.slice(2), runtime = {}) {
720
964
  const key = await readLoginKey(command, runtime);
721
965
  if (!(0, config_1.isDreamstateToken)(key))
722
966
  throw new CliUsageError('Dreamstate API keys must start with ds_user_ or ds_live_.');
723
- const path = await (0, config_1.storeApiKey)(key, runtimeCredentialsPath(runtime));
724
- print({ ok: true, data: { credentials_path: path }, meta: {} }, stdout, Boolean(command.jsonOutput));
725
- 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));
726
973
  process.exitCode = exports.CLI_EXIT.SUCCESS;
727
974
  return;
728
975
  }
729
- await loginWithDeviceAuthorization(runtime, stdout, stderr);
730
- 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));
731
977
  process.exitCode = exports.CLI_EXIT.SUCCESS;
732
978
  return;
733
979
  }
@@ -819,7 +1065,7 @@ async function runCli(argv = process.argv.slice(2), runtime = {}) {
819
1065
  result = await client.getWhoami();
820
1066
  if (!command.jsonOutput) {
821
1067
  const data = whoamiData(result);
822
- const activeId = data.workspace_id ?? data.active_workspace_id ?? null;
1068
+ const activeId = data.workspace_id ?? null;
823
1069
  const rows = data.workspaces ?? [];
824
1070
  stdout.write(rows.length
825
1071
  ? `${rows.map((row) => `${row.id === activeId ? '*' : ' '} ${row.slug ?? '-'} ${row.name} ${row.role ?? '-'} ${row.id}`).join('\n')}\n`
@@ -836,10 +1082,10 @@ async function runCli(argv = process.argv.slice(2), runtime = {}) {
836
1082
  const match = data.workspaces?.find((row) => row.id.toLowerCase() === wanted || row.slug?.toLowerCase() === wanted);
837
1083
  if (!match)
838
1084
  throw new CliUsageError(`You are not a member of workspace "${command.workspace}". Run dreamstate workspaces list to see the workspaces you can use.`);
839
- const stored = await (0, config_1.readStoredCredentials)(credentialFilePath);
1085
+ const stored = await (0, config_1.readStoredCredentials)(await (0, config_1.selectedCredentialsPath)(credentialFilePath, environment));
840
1086
  if (!stored)
841
1087
  throw new CliUsageError('workspaces use saves the default next to a stored login. Run dreamstate login or dreamstate login --token first.');
842
- 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);
843
1089
  stderr.write(`Default workspace: ${match.name} (${match.slug})\n`);
844
1090
  result = { ok: true, data: { workspace: { id: match.id, name: match.name, slug: match.slug }, credentials_path: credentialFilePath }, meta: {} };
845
1091
  break;
@@ -857,24 +1103,23 @@ async function runCli(argv = process.argv.slice(2), runtime = {}) {
857
1103
  result = await client.listSkills(command.query);
858
1104
  break;
859
1105
  case 'skills_get':
860
- result = await client.getSkill(command.skillId);
1106
+ result = await client.getSkill(command.skillId, command.file ? { file: command.file } : {});
861
1107
  break;
862
1108
  case 'skills_search':
863
1109
  result = await client.searchSkillDocuments(command.query);
864
1110
  break;
865
1111
  case 'skills_install': {
866
- const listed = command.skillId === '*' ? await client.listAllSkills() : null;
867
- const entries = listed && typeof listed === 'object' && listed.data?.skills;
868
- const ids = command.skillId === '*'
869
- ? Array.isArray(entries) ? entries.map((skill) => skill && typeof skill === 'object' ? skill.id : undefined).filter((id) => typeof id === 'string') : []
870
- : [command.skillId];
871
- if (!ids.length)
872
- 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];
873
1118
  // A remote id becomes a directory name; refuse traversal before any request or write.
874
1119
  if (ids.some((id) => !/^[a-z0-9][a-z0-9_-]*$/i.test(id)))
875
1120
  throw new CliUsageError('Dreamstate API returned an invalid skill id.');
876
- // One at a time: each read comes from the workspace's private vault,
877
- // 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.
878
1123
  const skills = [];
879
1124
  for (const id of ids) {
880
1125
  const skill = hostedSkill(await client.getSkill(id));
@@ -908,6 +1153,9 @@ async function runCli(argv = process.argv.slice(2), runtime = {}) {
908
1153
  case 'tools_search':
909
1154
  result = await client.listTools(command.query);
910
1155
  break;
1156
+ // Exact GET mints its own turn server-side. No prior search is required:
1157
+ // search ranking can omit a valid exact capability, which made standalone
1158
+ // CLI contract reads fail despite the capability being runnable.
911
1159
  case 'tools_get':
912
1160
  result = await client.getTool(command.capabilityId);
913
1161
  break;
@@ -929,31 +1177,67 @@ async function runCli(argv = process.argv.slice(2), runtime = {}) {
929
1177
  const pending = command.turnToken
930
1178
  ? null
931
1179
  : await (0, turnCache_1.reusableTurnToken)(turns, baseUrl, turnKey, command.capabilityId, Date.now(), warnTurnCache);
932
- const turnToken = command.turnToken ?? pending ?? toolTurnToken(await client.getTool(command.capabilityId));
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);
933
1189
  try {
934
- result = await client.runCapability(command.capabilityId, { ...command.body, tool_turn_token: turnToken }, command.idempotencyKey);
1190
+ try {
1191
+ result = await run();
1192
+ }
1193
+ catch (error) {
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.
1208
+ throw error;
1209
+ }
1210
+ else {
1211
+ result = await run();
1212
+ }
1213
+ }
935
1214
  }
936
1215
  catch (error) {
937
- if (!command.turnToken && code(error) === 'quote_required') {
1216
+ if (!command.turnToken && turnToken && code(error) === 'quote_required') {
938
1217
  await (0, turnCache_1.rememberTurnToken)(turns, baseUrl, turnKey, command.capabilityId, turnToken, Date.now(), warnTurnCache);
939
1218
  }
940
1219
  throw error;
941
1220
  }
942
1221
  if (!command.turnToken) {
943
- if (command.body.mode === 'dry_run')
1222
+ if (command.body.mode === 'dry_run' && turnToken)
944
1223
  await (0, turnCache_1.rememberTurnToken)(turns, baseUrl, turnKey, command.capabilityId, turnToken, Date.now(), warnTurnCache);
945
1224
  else if (pending)
946
1225
  await (0, turnCache_1.forgetTurnToken)(turns, baseUrl, turnKey, command.capabilityId, warnTurnCache);
947
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
+ }
948
1233
  break;
949
1234
  }
950
1235
  case 'tables_import': {
951
1236
  await (0, promises_1.access)(command.file);
952
- const discovered = await client.listTools({
953
- goal: 'prepare a local file import into a table',
954
- query_terms: ['local', 'file', 'import'],
955
- });
956
- 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';
957
1241
  result = await client.runCapability(importCapabilityId, {
958
1242
  // A mutation run needs the turn token its own get minted; without it
959
1243
  // an API-key import was refused after the local file work.
@@ -983,15 +1267,20 @@ async function runCli(argv = process.argv.slice(2), runtime = {}) {
983
1267
  case 'runs_events':
984
1268
  result = await client.getRunEvents(command.runId);
985
1269
  break;
986
- case 'runs_cancel':
987
- 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);
988
1276
  break;
1277
+ }
989
1278
  case 'pack': {
990
1279
  if (command.pack === 'sequences') {
991
- await require('./commands/sequences').runSequencesCommand(client, command, stdout);
1280
+ await require('./commands/sequences').runSequencesCommand(client, command, stdout, runtime.sleep ?? wait);
992
1281
  }
993
1282
  else {
994
- await require('./commands/workflows').runWorkflowsCommand(client, command, stdout);
1283
+ await require('./commands/workflows').runWorkflowsCommand(client, command, stdout, runtime.sleep ?? wait);
995
1284
  }
996
1285
  return;
997
1286
  }
@@ -1003,7 +1292,8 @@ async function runCli(argv = process.argv.slice(2), runtime = {}) {
1003
1292
  printPackRoute(result, stdout);
1004
1293
  else
1005
1294
  print(result, stdout, Boolean(command.jsonOutput));
1006
- process.exitCode = exports.CLI_EXIT.SUCCESS;
1295
+ if (process.exitCode !== runWait_1.RUN_WAIT_TIMEOUT_EXIT_CODE)
1296
+ process.exitCode = exports.CLI_EXIT.SUCCESS;
1007
1297
  }
1008
1298
  catch (error) {
1009
1299
  const exitCode = exitCodeForError(error);