@trydreamstate/cli 1.3.1 → 1.3.3

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 (76) hide show
  1. package/README.md +29 -16
  2. package/agent-plugin/.claude-plugin/plugin.json +1 -1
  3. package/agent-plugin/.codex-plugin/plugin.json +4 -1
  4. package/agent-plugin/assets/logo.png +0 -0
  5. package/agent-plugin/hooks/approve-readonly.sh +15 -0
  6. package/agent-plugin/hooks/hooks.json +17 -0
  7. package/agent-plugin/skills/crm/SKILL.md +2 -2
  8. package/agent-plugin/skills/diagnostics/SKILL.md +1 -1
  9. package/agent-plugin/skills/dreamstate/SKILL.md +13 -5
  10. package/agent-plugin/skills/first-win/SKILL.md +2 -2
  11. package/agent-plugin/skills/inbox-replies/SKILL.md +1 -1
  12. package/agent-plugin/skills/knowledge/SKILL.md +1 -1
  13. package/agent-plugin/skills/linkedin-posts/SKILL.md +1 -1
  14. package/agent-plugin/skills/linkedin-warm-signals/SKILL.md +2 -2
  15. package/agent-plugin/skills/list-building/SKILL.md +4 -4
  16. package/agent-plugin/skills/outreach/SKILL.md +1 -1
  17. package/agent-plugin/skills/outreach/steps-and-timing.md +1 -1
  18. package/agent-plugin/skills/plays/SKILL.md +2 -2
  19. package/agent-plugin/skills/reddit-posts/SKILL.md +1 -1
  20. package/agent-plugin/skills/skill-authoring/SKILL.md +1 -1
  21. package/agent-plugin/skills/social/SKILL.md +2 -2
  22. package/agent-plugin/skills/visibility/SKILL.md +2 -2
  23. package/agent-plugin/skills/workflows/SKILL.md +3 -3
  24. package/agent-plugin/skills/worksheet-tidy/SKILL.md +1 -1
  25. package/agent-plugin/skills/writing/SKILL.md +1 -1
  26. package/agent-plugin/skills/x-posts/SKILL.md +1 -1
  27. package/dist/agentGuidance.generated.json +35 -0
  28. package/dist/client.d.ts +31 -2
  29. package/dist/client.d.ts.map +1 -1
  30. package/dist/client.js +115 -33
  31. package/dist/commandTable.d.ts +26 -0
  32. package/dist/commandTable.d.ts.map +1 -0
  33. package/dist/commandTable.js +54 -0
  34. package/dist/commands/sequences.d.ts +1 -1
  35. package/dist/commands/sequences.d.ts.map +1 -1
  36. package/dist/commands/sequences.js +37 -36
  37. package/dist/commands/shared.d.ts +4 -26
  38. package/dist/commands/shared.d.ts.map +1 -1
  39. package/dist/commands/shared.js +129 -127
  40. package/dist/commands/workflows.d.ts +1 -1
  41. package/dist/commands/workflows.d.ts.map +1 -1
  42. package/dist/commands/workflows.js +35 -41
  43. package/dist/errors.d.ts +78 -0
  44. package/dist/errors.d.ts.map +1 -0
  45. package/dist/errors.js +117 -0
  46. package/dist/gatedRun.d.ts +81 -0
  47. package/dist/gatedRun.d.ts.map +1 -0
  48. package/dist/gatedRun.js +370 -0
  49. package/dist/idempotency.d.ts +38 -0
  50. package/dist/idempotency.d.ts.map +1 -0
  51. package/dist/idempotency.js +106 -0
  52. package/dist/index.d.ts +21 -35
  53. package/dist/index.d.ts.map +1 -1
  54. package/dist/index.js +372 -366
  55. package/dist/runWait.d.ts +2 -1
  56. package/dist/runWait.d.ts.map +1 -1
  57. package/dist/runWait.js +3 -1
  58. package/dist/schemaCoercion.d.ts +20 -0
  59. package/dist/schemaCoercion.d.ts.map +1 -0
  60. package/dist/schemaCoercion.js +123 -0
  61. package/dist/skillInstaller.d.ts +40 -0
  62. package/dist/skillInstaller.d.ts.map +1 -1
  63. package/dist/skillInstaller.js +108 -9
  64. package/dist/tableFilePreview.d.ts +14 -0
  65. package/dist/tableFilePreview.d.ts.map +1 -1
  66. package/dist/tableFilePreview.js +19 -8
  67. package/dist/tableFileReader.d.ts +26 -0
  68. package/dist/tableFileReader.d.ts.map +1 -0
  69. package/dist/tableFileReader.js +108 -0
  70. package/dist/tablesImport.d.ts +57 -0
  71. package/dist/tablesImport.d.ts.map +1 -0
  72. package/dist/tablesImport.js +263 -0
  73. package/dist/turnCache.d.ts +14 -1
  74. package/dist/turnCache.d.ts.map +1 -1
  75. package/dist/turnCache.js +25 -8
  76. package/package.json +3 -2
package/dist/index.js CHANGED
@@ -1,15 +1,13 @@
1
1
  #!/usr/bin/env node
2
2
  "use strict";
3
3
  Object.defineProperty(exports, "__esModule", { value: true });
4
- exports.CliUsageError = exports.CLI_EXIT = void 0;
4
+ exports.toolTurnToken = exports.omitsToolTurnForInlineTableRead = exports.capabilityIdempotencyKey = exports.CliUsageError = exports.CLI_EXIT = void 0;
5
5
  exports.parseCliArgs = parseCliArgs;
6
- exports.toolTurnToken = toolTurnToken;
7
- exports.omitsToolTurnForInlineTableRead = omitsToolTurnForInlineTableRead;
8
- exports.capabilityIdempotencyKey = capabilityIdempotencyKey;
9
6
  exports.refusalEnvelope = refusalEnvelope;
10
7
  exports.openWebUrl = openWebUrl;
11
8
  exports.exitCodeForError = exitCodeForError;
12
9
  exports.extractWorkspaceFlag = extractWorkspaceFlag;
10
+ exports.statusData = statusData;
13
11
  exports.runCli = runCli;
14
12
  const node_child_process_1 = require("node:child_process");
15
13
  const node_crypto_1 = require("node:crypto");
@@ -21,11 +19,21 @@ const node_util_1 = require("node:util");
21
19
  const config_1 = require("./config");
22
20
  const pluginInstaller_1 = require("./pluginInstaller");
23
21
  const turnCache_1 = require("./turnCache");
22
+ const idempotency_1 = require("./idempotency");
24
23
  const runWait_1 = require("./runWait");
25
24
  const client_1 = require("./client");
25
+ const errors_1 = require("./errors");
26
+ Object.defineProperty(exports, "CLI_EXIT", { enumerable: true, get: function () { return errors_1.CLI_EXIT; } });
27
+ Object.defineProperty(exports, "CliUsageError", { enumerable: true, get: function () { return errors_1.CliUsageError; } });
28
+ const gatedRun_1 = require("./gatedRun");
29
+ Object.defineProperty(exports, "capabilityIdempotencyKey", { enumerable: true, get: function () { return gatedRun_1.capabilityIdempotencyKey; } });
30
+ Object.defineProperty(exports, "omitsToolTurnForInlineTableRead", { enumerable: true, get: function () { return gatedRun_1.omitsToolTurnForInlineTableRead; } });
31
+ Object.defineProperty(exports, "toolTurnToken", { enumerable: true, get: function () { return gatedRun_1.toolTurnToken; } });
32
+ const tablesImport_1 = require("./tablesImport");
33
+ const commandTable_1 = require("./commandTable");
26
34
  const skillInstaller_1 = require("./skillInstaller");
27
- 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 };
28
35
  const TOOLS_RUN_MODES = ['smoke_test', 'dry_run', 'live'];
36
+ const UUID_PATTERN = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
29
37
  const USAGE = `Dreamstate CLI
30
38
 
31
39
  Usage:
@@ -53,26 +61,22 @@ Usage:
53
61
  dreamstate commands search <term> [--json-output|--json]
54
62
  dreamstate tools search --goal <goal> [--terms <csv>] [--limit <n>] [--toolset <pack_id>] [--json-output|--json]
55
63
  dreamstate tools get <capability_id> [--json-output|--json]
56
- 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]
64
+ dreamstate tools run <capability_id> [--params <object>|--json <body>] [--turn-token <token>] (--live | --dry-run | --mode <smoke_test|dry_run|live>) [--approved] [--approval-id <uuid>] [--approved-effect-unknown] [--max-credits <n>] [--quote-hash <sha256:...>] [--idempotency-key <key>] [--wait-timeout <seconds>] [--no-wait] [--open] [--json-output|--json]
57
65
  dreamstate sequences <verb> [args] [flags]
58
66
  dreamstate workflows <verb> [args] [flags]
59
- dreamstate tables import --table-id <uuid> --file <path> [--format <csv|json|jsonl|xlsx>] [--preview] [--upsert-key <header>] [--background] [--json-output|--json]
67
+ dreamstate tables import --table-id <uuid> --file <path> [--format <csv|json|jsonl|xlsx>] [--preview] [--upsert-key <header>] [--row-kind <person|company>] [--keep-unmapped-columns <true|false>] [--json-output|--json]
60
68
  dreamstate runs list [--limit <n>] [--cursor <cursor>] [--json <query>] [--json-output|--json]
61
69
  dreamstate runs get <run_id> [--json-output|--json]
62
- dreamstate runs resume <run_id> --state-version <n> [--reason <text>] [--idempotency-key <key>] [--open] [--json-output|--json]
63
70
  dreamstate runs events <run_id> [--json-output|--json]
64
71
  dreamstate runs cancel <run_id> [--state-version <n>] [--reason <text>] [--json <body>] [--idempotency-key <key>] [--json-output|--json]
65
72
 
66
73
  All catalog discovery reads live API records. Set DREAMSTATE_API_URL and DREAMSTATE_API_KEY.
67
74
  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}.
75
+ A write you re-run unchanged presents the same Idempotency-Key (printed as meta.idempotency_key), so the server replays instead of running it twice; --idempotency-key sets one yourself. Exit codes: 0 ok, 2 usage, 3 auth, 4 not found, 5 server failure, 6 rate limited, 7 approval/consent/spend gate required, 8 CLI update required, 9 refused, 10 transport, 11 timeout.
68
76
  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.
69
77
  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.
70
78
  logout deletes the stored credentials file. --agents takes space-separated, comma-separated or repeated values.
71
79
  `;
72
- class CliUsageError extends Error {
73
- constructor(message) { super(message); this.name = 'CliUsageError'; }
74
- }
75
- exports.CliUsageError = CliUsageError;
76
80
  function outputOption(values) {
77
81
  return values['json-output'] ? { jsonOutput: true } : {};
78
82
  }
@@ -84,35 +88,35 @@ function parseJsonObject(raw, flag = '--json') {
84
88
  return value;
85
89
  }
86
90
  catch (error) {
87
- throw new CliUsageError(`${flag} ${error instanceof Error ? error.message : 'is invalid'}`);
91
+ throw new errors_1.CliUsageError(`${flag} ${error instanceof Error ? error.message : 'is invalid'}`);
88
92
  }
89
93
  }
90
94
  function requireOne(positionals, usage) {
91
95
  if (positionals.length !== 2 || !positionals[1])
92
- throw new CliUsageError(`Usage: ${usage}`);
96
+ throw new errors_1.CliUsageError(`Usage: ${usage}`);
93
97
  return positionals[1];
94
98
  }
95
99
  function csv(raw, flag = '--terms') {
96
100
  const values = raw.split(',').map((value) => value.trim()).filter(Boolean);
97
101
  if (!values.length)
98
- throw new CliUsageError(`${flag} must contain at least one value`);
102
+ throw new errors_1.CliUsageError(`${flag} must contain at least one value`);
99
103
  return [...new Set(values)];
100
104
  }
101
105
  function requireToolsRunMode(body) {
102
106
  if (TOOLS_RUN_MODES.includes(body.mode))
103
107
  return;
104
- throw new CliUsageError('mode must be one of: smoke_test, dry_run, live.');
108
+ throw new errors_1.CliUsageError('mode must be one of: smoke_test, dry_run, live.');
105
109
  }
106
110
  function nonNegativeNumber(raw, flag) {
107
111
  const value = Number(raw);
108
112
  if (!Number.isFinite(value) || value < 0)
109
- throw new CliUsageError(`${flag} must be a non-negative number`);
113
+ throw new errors_1.CliUsageError(`${flag} must be a non-negative number`);
110
114
  return value;
111
115
  }
112
116
  function nonNegativeInteger(raw, flag) {
113
117
  const value = Number(raw);
114
118
  if (!Number.isSafeInteger(value) || value < 0)
115
- throw new CliUsageError(`${flag} must be a non-negative integer`);
119
+ throw new errors_1.CliUsageError(`${flag} must be a non-negative integer`);
116
120
  return value;
117
121
  }
118
122
  // runs.get returns { data: { run: { state_version } } }. Cancel posts
@@ -141,7 +145,7 @@ function parsePacks(argv) {
141
145
  return { command: 'packs_get', packId: requireOne(positionals, 'dreamstate packs get <pack_id>'), ...outputOption(values) };
142
146
  if (positionals.length === 1 && positionals[0] === 'route' && values.goal?.trim())
143
147
  return { command: 'packs_route', goal: values.goal.trim(), ...outputOption(values) };
144
- throw new CliUsageError('Usage: dreamstate packs <list|get|route>');
148
+ throw new errors_1.CliUsageError('Usage: dreamstate packs <list|get|route>');
145
149
  }
146
150
  /** Rewrites `--agents a b c` into `--agents=a --agents=b --agents=c` so parseArgs sees every value. */
147
151
  function spaceSeparatedAgents(argv) {
@@ -172,7 +176,7 @@ function parseSkills(argv) {
172
176
  const requested = [...new Set(values.agents.flatMap((raw) => csv(raw, '--agents')))];
173
177
  const invalid = requested.filter((agent) => !skillInstaller_1.SKILL_INSTALL_AGENTS.includes(agent));
174
178
  if (invalid.length)
175
- throw new CliUsageError(`--agents values must be one of: ${skillInstaller_1.SKILL_INSTALL_AGENTS.join(', ')} (got: ${invalid.join(', ')})`);
179
+ throw new errors_1.CliUsageError(`--agents values must be one of: ${skillInstaller_1.SKILL_INSTALL_AGENTS.join(', ')} (got: ${invalid.join(', ')})`);
176
180
  return requested;
177
181
  };
178
182
  if (positionals.length === 1 && positionals[0] === 'list') {
@@ -195,14 +199,14 @@ function parseSkills(argv) {
195
199
  return { command: 'skills_install', skillId: values.skill?.trim() || '*', agents: agents(), ...(values.project ? { project: true } : {}), ...(values.copy ? { copy: true } : {}), ...outputOption(values) };
196
200
  if (positionals.length === 1 && positionals[0] === 'doctor')
197
201
  return { command: 'skills_doctor', agents: agents(), ...(values.project ? { project: true } : {}), ...outputOption(values) };
198
- throw new CliUsageError('Usage: dreamstate skills <list|get|search|install|doctor>');
202
+ throw new errors_1.CliUsageError('Usage: dreamstate skills <list|get|search|install|doctor>');
199
203
  }
200
204
  function parseCommands(argv) {
201
205
  const { values, positionals } = (0, node_util_1.parseArgs)({ args: argv, options: { 'json-output': { type: 'boolean' }, help: { type: 'boolean', short: 'h' } }, allowPositionals: true, strict: true });
202
206
  if (values.help)
203
207
  return { command: 'help' };
204
208
  if (positionals[0] !== 'search' || positionals.length < 2)
205
- throw new CliUsageError('Usage: dreamstate commands search <term>');
209
+ throw new errors_1.CliUsageError('Usage: dreamstate commands search <term>');
206
210
  return { command: 'commands_search', term: positionals.slice(1).join(' '), ...outputOption(values) };
207
211
  }
208
212
  /**
@@ -210,10 +214,8 @@ function parseCommands(argv) {
210
214
  * are mutually exclusive ways of saying the same thing, and disagreeing ones are a
211
215
  * usage error rather than a silent winner.
212
216
  *
213
- * --approved is deliberately NOT here. It is a retained no-op: the run body schema
214
- * is strict and has no `approved` key, so sending it failed the entire call rather
215
- * than authorizing anything. Spend authority is max_credits and a human grant
216
- * arrives as approval_id.
217
+ * --approved is deliberately NOT here: it is not a mode. It rides on a live run
218
+ * as the caller's assertion that it asked a person (see parseTools).
217
219
  */
218
220
  function toolsRunMode(values) {
219
221
  const chosen = new Set();
@@ -224,12 +226,12 @@ function toolsRunMode(values) {
224
226
  if (values.mode !== undefined) {
225
227
  const mode = values.mode.trim();
226
228
  if (!TOOLS_RUN_MODES.includes(mode)) {
227
- throw new CliUsageError(`--mode must be one of: ${TOOLS_RUN_MODES.join(', ')}`);
229
+ throw new errors_1.CliUsageError(`--mode must be one of: ${TOOLS_RUN_MODES.join(', ')}`);
228
230
  }
229
231
  chosen.add(mode);
230
232
  }
231
233
  if (chosen.size > 1) {
232
- throw new CliUsageError(`Conflicting execution modes requested: ${[...chosen].sort().join(', ')}. Pass exactly one of --live, --dry-run, --mode.`);
234
+ throw new errors_1.CliUsageError(`Conflicting execution modes requested: ${[...chosen].sort().join(', ')}. Pass exactly one of --live, --dry-run, --mode.`);
233
235
  }
234
236
  return [...chosen][0];
235
237
  }
@@ -239,7 +241,8 @@ function parseTools(argv) {
239
241
  limit: { type: 'string' }, toolset: { type: 'string' },
240
242
  'idempotency-key': { type: 'string' }, 'json-output': { type: 'boolean' }, open: { type: 'boolean' },
241
243
  'dry-run': { type: 'boolean' }, live: { type: 'boolean' }, mode: { type: 'string' },
242
- 'agent-session-id': { type: 'string' }, approved: { type: 'boolean' }, 'approved-effect-unknown': { type: 'boolean' },
244
+ approved: { type: 'boolean' }, 'approved-effect-unknown': { type: 'boolean' },
245
+ 'quote-hash': { type: 'string' }, 'approval-id': { type: 'string' },
243
246
  'max-credits': { type: 'string' }, 'turn-token': { type: 'string' }, 'wait-timeout': { type: 'string' }, 'no-wait': { type: 'boolean' }, help: { type: 'boolean', short: 'h' },
244
247
  }, allowPositionals: true, strict: true });
245
248
  if (values.help)
@@ -256,7 +259,7 @@ function parseTools(argv) {
256
259
  if (values.toolset !== undefined)
257
260
  query.toolset = values.toolset;
258
261
  if (typeof query.goal !== 'string' && !Array.isArray(query.query_terms))
259
- throw new CliUsageError('tools search requires --goal, --terms, or --json query fields');
262
+ throw new errors_1.CliUsageError('tools search requires --goal, --terms, or --json query fields');
260
263
  return { command: 'tools_search', query, ...output };
261
264
  }
262
265
  if (positionals[0] === 'get')
@@ -264,11 +267,11 @@ function parseTools(argv) {
264
267
  if (positionals[0] === 'run') {
265
268
  const capabilityId = requireOne(positionals, 'dreamstate tools run <capability_id> [--params <object>|--json <body>]');
266
269
  if (values.json && values.params)
267
- throw new CliUsageError('Use either --params for tool parameters or --json for the complete request, not both.');
270
+ throw new errors_1.CliUsageError('Use either --params for tool parameters or --json for the complete request, not both.');
268
271
  if (!values.json && !values.params)
269
- throw new CliUsageError('tools run requires --params <object> or --json <body>; fetch live schema first with tools get');
272
+ throw new errors_1.CliUsageError('tools run requires --params <object> or --json <body>; fetch live schema first with tools get');
270
273
  const json = values.json ? parseJsonObject(values.json) : undefined;
271
- const body = json && ('params' in json || 'mode' in json || 'agent_session_id' in json || 'approved_effect_unknown' in json || 'max_credits' in json)
274
+ const body = json && ('params' in json || 'mode' in json || 'agent_session_id' in json || 'approved_effect_unknown' in json || 'max_credits' in json || 'quote_hash' in json || 'approved' in json || 'approval_id' in json)
272
275
  ? json
273
276
  : {};
274
277
  if (values.params)
@@ -277,17 +280,37 @@ function parseTools(argv) {
277
280
  body.params = json;
278
281
  // The server never defaults a mode, so a mutating run with none is refused
279
282
  // mode_required; --live and --mode exist so a caller can actually ask for one.
280
- // `approved` is no longer sent: the run body schema is strict and has no such
281
- // key, so passing it failed the whole call instead of authorizing anything.
282
283
  const chosenMode = toolsRunMode(values);
283
284
  if (chosenMode)
284
285
  body.mode = chosenMode;
285
- if (values['agent-session-id'])
286
- body.agent_session_id = values['agent-session-id'];
286
+ // agent_session_id selects the in-app Architect surface and the server refuses it for every
287
+ // other caller (an API key may name a session only while an Architect turn is running on it,
288
+ // which a CLI user never has). There is no flag for it, and a full --json body must not smuggle it.
289
+ if (body.agent_session_id !== undefined) {
290
+ throw new errors_1.CliUsageError('agent_session_id is not accepted by the CLI: only the in-app Architect may name an Architect session, and the server refuses it for every other caller. Remove it from the request.');
291
+ }
287
292
  if (values['approved-effect-unknown'])
288
293
  body.approved_effect_unknown = true;
289
294
  if (values['max-credits'] !== undefined)
290
295
  body.max_credits = nonNegativeNumber(values['max-credits'], '--max-credits');
296
+ if (values['quote-hash'] !== undefined) {
297
+ if (!/^sha256:[a-f0-9]{64}$/.test(values['quote-hash']))
298
+ throw new errors_1.CliUsageError('--quote-hash must be the sha256:<64 hex> quote_hash a dry run returned');
299
+ body.quote_hash = values['quote-hash'];
300
+ }
301
+ // --approved is the caller's assertion that it showed a person the dry run's
302
+ // rows and cost and they agreed. A paid run above the size or dollar
303
+ // threshold is refused without it. It only means something on a live run,
304
+ // so any other mode never sends it.
305
+ if (values.approved && body.mode === 'live')
306
+ body.approved = true;
307
+ // --approval-id names a recorded approval (a uuid the approvals capabilities returned)
308
+ // the door should honour for this run. It is a door input, never a param.
309
+ if (values['approval-id'] !== undefined) {
310
+ if (!UUID_PATTERN.test(values['approval-id']))
311
+ throw new errors_1.CliUsageError('--approval-id must be the uuid of a recorded approval');
312
+ body.approval_id = values['approval-id'];
313
+ }
291
314
  // No client-side mode requirement: the server owns that refusal and answers
292
315
  // mode_required with a repair, and the parse grammar is expected to accept a
293
316
  // run without one. What was missing was any way to ASK for live.
@@ -296,18 +319,24 @@ function parseTools(argv) {
296
319
  const waitTimeout = values['wait-timeout'] === undefined ? 120 : nonNegativeNumber(values['wait-timeout'], '--wait-timeout');
297
320
  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 };
298
321
  }
299
- throw new CliUsageError('Usage: dreamstate tools <search|get|run>');
322
+ throw new errors_1.CliUsageError('Usage: dreamstate tools <search|get|run>');
300
323
  }
301
324
  function parseTables(argv) {
325
+ // Servers before this release put --background on the import command they hand out. It
326
+ // never did anything (the import always ran to completion in this process), so 1.3.3
327
+ // dropped it; say so plainly instead of the generic "unknown option" error.
328
+ if (argv.includes('--background')) {
329
+ throw new errors_1.CliUsageError('--background was removed in 1.3.3: dreamstate tables import always runs to completion in this process. Run the same command without it.');
330
+ }
302
331
  const { values, positionals } = (0, node_util_1.parseArgs)({ args: argv, options: {
303
- 'table-id': { type: 'string' }, file: { type: 'string' }, format: { type: 'string' }, background: { type: 'boolean' },
332
+ 'table-id': { type: 'string' }, file: { type: 'string' }, format: { type: 'string' },
304
333
  preview: { type: 'boolean' }, 'upsert-key': { type: 'string' }, 'row-kind': { type: 'string' }, 'keep-unmapped-columns': { type: 'string' },
305
334
  'json-output': { type: 'boolean' }, help: { type: 'boolean', short: 'h' },
306
335
  }, allowPositionals: true, strict: true });
307
336
  if (values.help)
308
337
  return { command: 'help' };
309
338
  if (positionals.length !== 1 || positionals[0] !== 'import' || !values['table-id'] || !values.file) {
310
- throw new CliUsageError('Usage: dreamstate tables import --table-id <uuid> --file <path> [--format <csv|json|jsonl|xlsx>] [--preview] [--upsert-key <header>] [--row-kind <person|company>] [--keep-unmapped-columns <true|false>] [--background]');
339
+ throw new errors_1.CliUsageError('Usage: dreamstate tables import --table-id <uuid> --file <path> [--format <csv|json|jsonl|xlsx>] [--preview] [--upsert-key <header>] [--row-kind <person|company>] [--keep-unmapped-columns <true|false>]');
311
340
  }
312
341
  // --format is an override, not a requirement. Requiring it made the caller restate
313
342
  // what the file path already says, and an omitted flag produced a usage error rather
@@ -315,16 +344,16 @@ function parseTables(argv) {
315
344
  // the kind from the file it receives.
316
345
  const format = values.format;
317
346
  if (format !== undefined && !['csv', 'json', 'jsonl', 'xlsx'].includes(format)) {
318
- throw new CliUsageError('--format must be one of csv, json, jsonl, xlsx');
347
+ throw new errors_1.CliUsageError('--format must be one of csv, json, jsonl, xlsx');
319
348
  }
320
349
  const upsertKey = values['upsert-key']?.trim();
321
350
  const rowKind = values['row-kind'];
322
351
  if (rowKind !== undefined && rowKind !== 'person' && rowKind !== 'company') {
323
- throw new CliUsageError('--row-kind must be person or company');
352
+ throw new errors_1.CliUsageError('--row-kind must be person or company');
324
353
  }
325
354
  const keepUnmappedColumns = values['keep-unmapped-columns'];
326
355
  if (keepUnmappedColumns !== undefined && keepUnmappedColumns !== 'true' && keepUnmappedColumns !== 'false') {
327
- throw new CliUsageError('--keep-unmapped-columns must be true or false');
356
+ throw new errors_1.CliUsageError('--keep-unmapped-columns must be true or false');
328
357
  }
329
358
  return {
330
359
  command: 'tables_import',
@@ -335,14 +364,13 @@ function parseTables(argv) {
335
364
  ...(upsertKey ? { upsertKey } : {}),
336
365
  ...(rowKind ? { rowKind } : {}),
337
366
  ...(keepUnmappedColumns === 'false' ? { keepUnmappedColumns: false } : {}),
338
- background: Boolean(values.background),
339
367
  ...outputOption(values),
340
368
  };
341
369
  }
342
370
  function runsListLimit(raw) {
343
371
  const value = nonNegativeInteger(raw, '--limit');
344
372
  if (value < 1 || value > 500)
345
- throw new CliUsageError('--limit must be an integer from 1 to 500');
373
+ throw new errors_1.CliUsageError('--limit must be an integer from 1 to 500');
346
374
  return value;
347
375
  }
348
376
  function parseRuns(argv) {
@@ -357,30 +385,28 @@ function parseRuns(argv) {
357
385
  if (values.cursor !== undefined) {
358
386
  const cursor = values.cursor.trim();
359
387
  if (!cursor)
360
- throw new CliUsageError('--cursor must be a non-empty string');
388
+ throw new errors_1.CliUsageError('--cursor must be a non-empty string');
361
389
  query.cursor = cursor;
362
390
  }
363
391
  return { command: 'runs_list', query, ...output };
364
392
  }
365
393
  if (values.limit !== undefined || values.cursor !== undefined) {
366
- throw new CliUsageError('--limit and --cursor are only valid on runs list');
394
+ throw new errors_1.CliUsageError('--limit and --cursor are only valid on runs list');
395
+ }
396
+ if (positionals[0] === 'resume') {
397
+ // The server retired generic run resume (verifyExecutionDoorCollapse.ts blocks the
398
+ // route), so there is nothing to call. Say where the replacement is.
399
+ throw new errors_1.CliUsageError('runs resume was retired: the server has no generic run resume. A table run checkpoint resumes with: dreamstate tools run tables.table_runs.resume --params \'{...}\' --live. Start a new run for anything else.');
367
400
  }
368
401
  const runId = requireOne(positionals, `dreamstate runs ${positionals[0] ?? '<get|events|cancel>'} <run_id>`);
369
402
  if (positionals[0] === 'get')
370
403
  return { command: 'runs_get', runId, ...output };
371
404
  if (positionals[0] === 'events')
372
405
  return { command: 'runs_events', runId, ...output };
373
- if (positionals[0] === 'resume') {
374
- if (values.json)
375
- throw new CliUsageError('Use --state-version and --reason with runs resume, not --json.');
376
- if (values['state-version'] === undefined)
377
- throw new CliUsageError('--state-version is required for runs resume.');
378
- 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 };
379
- }
380
406
  if (positionals[0] === 'cancel') {
381
407
  const idempotency = values['idempotency-key'] ? { idempotencyKey: values['idempotency-key'] } : {};
382
408
  if (values.json && (values['state-version'] !== undefined || values.reason)) {
383
- throw new CliUsageError('Use either --json <body> or --state-version and --reason with runs cancel, not both.');
409
+ throw new errors_1.CliUsageError('Use either --json <body> or --state-version and --reason with runs cancel, not both.');
384
410
  }
385
411
  if (values.json)
386
412
  return { command: 'runs_cancel', runId, body: parseJsonObject(values.json), ...idempotency, ...output };
@@ -393,7 +419,7 @@ function parseRuns(argv) {
393
419
  ...output,
394
420
  };
395
421
  }
396
- throw new CliUsageError('Usage: dreamstate runs <get|events|cancel> <run_id>');
422
+ throw new errors_1.CliUsageError('Usage: dreamstate runs <list|get|events|cancel> <run_id>');
397
423
  }
398
424
  function normalizeJsonOutputAlias(argv) {
399
425
  return argv.map((argument, index) => argument === '--json' && (index === argv.length - 1 || argv[index + 1]?.startsWith('-'))
@@ -412,9 +438,9 @@ function parseCliArgs(argv) {
412
438
  if (values.help)
413
439
  return { command: 'help' };
414
440
  if (positionals.length)
415
- throw new CliUsageError('Usage: dreamstate login [--token <ds_user_...|ds_live_...>]');
441
+ throw new errors_1.CliUsageError('Usage: dreamstate login [--token <ds_user_...|ds_live_...>]');
416
442
  if (values.token !== undefined && values['api-key-file'])
417
- throw new CliUsageError('Use either --token or --api-key-file, not both.');
443
+ throw new errors_1.CliUsageError('Use either --token or --api-key-file, not both.');
418
444
  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) };
419
445
  }
420
446
  if (group === 'install') {
@@ -422,11 +448,11 @@ function parseCliArgs(argv) {
422
448
  if (values.help)
423
449
  return { command: 'help' };
424
450
  if (positionals.length)
425
- throw new CliUsageError('Usage: dreamstate install [--agents <claude codex grok>]');
451
+ throw new errors_1.CliUsageError('Usage: dreamstate install [--agents <claude codex grok>]');
426
452
  const requested = values.agents ? [...new Set(values.agents.flatMap((raw) => csv(raw, '--agents')))] : [...pluginInstaller_1.PLUGIN_AGENTS];
427
453
  const invalid = requested.filter((agent) => !pluginInstaller_1.PLUGIN_AGENTS.includes(agent));
428
454
  if (invalid.length)
429
- throw new CliUsageError(`--agents values must be one of: ${pluginInstaller_1.PLUGIN_AGENTS.join(', ')} (got: ${invalid.join(', ')})`);
455
+ throw new errors_1.CliUsageError(`--agents values must be one of: ${pluginInstaller_1.PLUGIN_AGENTS.join(', ')} (got: ${invalid.join(', ')})`);
430
456
  return { command: 'install', agents: requested, ...outputOption(values) };
431
457
  }
432
458
  if (group === 'logout') {
@@ -434,14 +460,14 @@ function parseCliArgs(argv) {
434
460
  if (values.help)
435
461
  return { command: 'help' };
436
462
  if (positionals.length)
437
- throw new CliUsageError('Usage: dreamstate logout');
463
+ throw new errors_1.CliUsageError('Usage: dreamstate logout');
438
464
  return { command: 'logout', ...outputOption(values) };
439
465
  }
440
466
  if (group === 'status') {
441
467
  const { values, positionals } = (0, node_util_1.parseArgs)({ args: rest, options: { 'json-output': { type: 'boolean' } }, allowPositionals: true, strict: true });
442
468
  if (!positionals.length)
443
469
  return { command: 'status', ...outputOption(values) };
444
- throw new CliUsageError('Usage: dreamstate status [--json-output]');
470
+ throw new errors_1.CliUsageError('Usage: dreamstate status [--json-output]');
445
471
  }
446
472
  if (group === 'profiles') {
447
473
  const { values, positionals } = (0, node_util_1.parseArgs)({ args: rest, options: { 'json-output': { type: 'boolean' } }, allowPositionals: true, strict: true });
@@ -456,13 +482,13 @@ function parseCliArgs(argv) {
456
482
  return { command: 'profiles_remove', name: (0, config_1.validateProfileName)(positionals[1]), ...output };
457
483
  if (positionals.length === 2 && positionals[0] === 'env')
458
484
  return { command: 'profiles_env', name: (0, config_1.validateProfileName)(positionals[1]), ...output };
459
- throw new CliUsageError('Usage: dreamstate profiles <list|use <name>|current|remove <name>|env <name>>');
485
+ throw new errors_1.CliUsageError('Usage: dreamstate profiles <list|use <name>|current|remove <name>|env <name>>');
460
486
  }
461
487
  if (group === 'whoami') {
462
488
  const { values, positionals } = (0, node_util_1.parseArgs)({ args: rest, options: { 'json-output': { type: 'boolean' } }, allowPositionals: true, strict: true });
463
489
  if (positionals.length === 0)
464
490
  return { command: 'whoami', ...outputOption(values) };
465
- throw new CliUsageError('Usage: dreamstate whoami [--json-output]');
491
+ throw new errors_1.CliUsageError('Usage: dreamstate whoami [--json-output]');
466
492
  }
467
493
  if (group === 'workspaces') {
468
494
  const { values, positionals } = (0, node_util_1.parseArgs)({ args: rest, options: { 'json-output': { type: 'boolean' }, help: { type: 'boolean', short: 'h' } }, allowPositionals: true, strict: true });
@@ -472,7 +498,7 @@ function parseCliArgs(argv) {
472
498
  return { command: 'workspaces_list', ...outputOption(values) };
473
499
  if (positionals.length === 2 && positionals[0] === 'use' && positionals[1])
474
500
  return { command: 'workspaces_use', workspace: positionals[1], ...outputOption(values) };
475
- throw new CliUsageError('Usage: dreamstate workspaces <list|use <id|slug>>');
501
+ throw new errors_1.CliUsageError('Usage: dreamstate workspaces <list|use <id|slug>>');
476
502
  }
477
503
  if (group === 'auth') {
478
504
  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 });
@@ -480,7 +506,7 @@ function parseCliArgs(argv) {
480
506
  return { command: 'login', ...(values['api-key-file'] ? { apiKeyFile: values['api-key-file'] } : {}), ...(values['no-skills'] ? { noSkills: true } : {}), ...outputOption(values) };
481
507
  if (positionals.length === 1 && positionals[0] === 'whoami')
482
508
  return { command: 'whoami', ...outputOption(values) };
483
- throw new CliUsageError('Usage: dreamstate auth <login|whoami>');
509
+ throw new errors_1.CliUsageError('Usage: dreamstate auth <login|whoami>');
484
510
  }
485
511
  if (group === 'packs')
486
512
  return parsePacks(rest);
@@ -498,10 +524,14 @@ function parseCliArgs(argv) {
498
524
  return require('./commands/sequences').parseSequencesCommand(rest);
499
525
  if (group === 'workflows')
500
526
  return require('./commands/workflows').parseWorkflowsCommand(rest);
501
- throw new CliUsageError(`Unknown command: ${group}`);
527
+ throw new errors_1.CliUsageError(`Unknown command: ${group}`);
502
528
  }
503
529
  function print(value, stdout, compact) {
504
- stdout.write(`${JSON.stringify(value, (key, nested) => key === 'tool_turn_token' ? undefined : nested, compact ? 0 : 2)}\n`);
530
+ // Printed exactly as the server returned it. tool_turn_token used to be stripped
531
+ // here, which made --turn-token impossible to use: a caller could never read the
532
+ // token from `tools get` to pass back. The token is a short-lived fetch fence tied
533
+ // to the caller's own session, not a credential, and MCP and REST return it too.
534
+ stdout.write(`${JSON.stringify(value, null, compact ? 0 : 2)}\n`);
505
535
  }
506
536
  function printPackRoute(value, stdout) {
507
537
  const data = value && typeof value === 'object' && !Array.isArray(value)
@@ -530,103 +560,32 @@ function printPackRoute(value, stdout) {
530
560
  stdout.write(`${packId}${typeof reason === 'string' ? `: ${reason}` : ''}\n`);
531
561
  }
532
562
  }
533
- function toolTurnToken(value) {
534
- const root = value && typeof value === 'object' ? value : {};
535
- const data = root.data && typeof root.data === 'object' ? root.data : {};
536
- if (typeof data.tool_turn_token === 'string' && data.tool_turn_token.trim())
537
- return data.tool_turn_token;
538
- throw new client_1.CliError('tools get did not return a tool_turn_token.', 'invalid_tool_turn_token', 0);
539
- }
540
- /**
541
- * Free sync reads. The server runs these on the inline read path when
542
- * the body has no tool_turn_token and the request has no Idempotency-Key
543
- * (isInlineReadEligible). A tools get before the run is a second HTTP the
544
- * read does not need. Measured 2026-10-04: tables.workbooks.get full live CLI
545
- * 0.726s against whoami 0.455s, about 0.27s of tools get per harness read-back.
546
- * knowledge.brain.context.get is the same class (catalog: credit_class free,
547
- * async_policy.mode sync, mutates not set). Live 2026-10-05 a standalone
548
- * tools get of that id was 0.400s including node startup, next to whoami
549
- * 0.422s, so the same-process skip is one HTTP, not a second process.
550
- * The set name is historical. Writes are not in this set: they stay fetch-first.
551
- */
552
- const INLINE_READ_CAPABILITIES = new Set([
553
- 'tables.workbooks.get',
554
- 'tables.workbooks.overview',
555
- 'tables.workbooks.list',
556
- 'tables.columns.list',
557
- 'tables.columns.stats',
558
- 'tables.rows.query',
559
- 'tables.rows.get',
560
- 'tables.rows.count',
561
- 'knowledge.brain.context.get',
562
- // Both are stored, free, synchronous inbox reads. Keeping either behind a
563
- // tools_get turns one list or thread open into two HTTP requests, while the
564
- // canonical inline-read gate already validates their read-only contract.
565
- 'unibox.inbox.unified_list',
566
- 'unibox.inbox.conversation_get',
567
- ]);
568
- function omitsToolTurnForInlineTableRead(capabilityId) {
569
- return INLINE_READ_CAPABILITIES.has(capabilityId);
570
- }
571
- function readIdempotencyContract(fetched) {
572
- if (!fetched || typeof fetched !== 'object' || Array.isArray(fetched))
573
- return null;
574
- const root = fetched;
575
- const data = root.data && typeof root.data === 'object' && !Array.isArray(root.data)
576
- ? root.data
577
- : null;
578
- const candidates = [data?.capability, root.capability, data, root];
579
- for (const candidate of candidates) {
580
- if (!candidate || typeof candidate !== 'object' || Array.isArray(candidate))
581
- continue;
582
- if ('mutates' in candidate || 'idempotency_required' in candidate) {
583
- return candidate;
584
- }
585
- }
586
- return null;
587
- }
588
563
  /**
589
- * Header value, or null when the request must not carry Idempotency-Key.
590
- * Any key forces a read onto the durable run path (isInlineReadEligible).
591
- * Send one only for an explicit key, or when the tools get contract says
592
- * mutates or idempotency_required. Missing flags omit the key: a read whose
593
- * contract does not set mutates must not be treated as a write.
564
+ * The refusal envelope, in the shared shape { ok:false, error, meta }. The server's
565
+ * own message, retry decision, repair, details and meta (trace_id, server_version,
566
+ * minimum_cli_version, release_sha) are forwarded unchanged; retry_after and
567
+ * idempotency_key are request receipts, so they stay in meta. Process exit status
568
+ * remains process-level state: ToolEnvelopeMeta intentionally has no exit_code.
594
569
  */
595
- function capabilityIdempotencyKey(fetched, explicit) {
596
- if (typeof explicit === 'string' && explicit.length > 0)
597
- return explicit;
598
- const contract = readIdempotencyContract(fetched);
599
- if (contract?.mutates === true || contract?.idempotency_required === true)
600
- return (0, node_crypto_1.randomUUID)();
601
- return null;
602
- }
603
- function parameterIdempotencyKey(body) {
604
- const params = body.params;
605
- if (!params || typeof params !== 'object' || Array.isArray(params))
606
- return undefined;
607
- const key = params.idempotency_key;
608
- return typeof key === 'string' && key.length > 0 ? key : undefined;
609
- }
610
- function code(error) {
611
- return error && typeof error === 'object' && typeof error.code === 'string'
612
- ? error.code
613
- : undefined;
614
- }
615
570
  function refusalEnvelope(error, exitCode) {
616
571
  const cliError = error instanceof client_1.CliError ? error : null;
617
572
  return {
618
573
  ok: false,
619
- executed: false,
620
574
  error: {
621
- code: code(error) ?? 'invalid_request',
575
+ code: (0, gatedRun_1.code)(error) ?? 'invalid_request',
622
576
  message: error instanceof Error ? error.message : String(error),
623
- ...(cliError?.details === undefined ? {} : { details: cliError.details }),
577
+ ...(cliError?.retryable === undefined ? {} : { retryable: cliError.retryable }),
624
578
  // The client keeps the server's retry decision and repair; dropping them
625
579
  // here left CLI automation without the recovery Architect and MCP get.
626
- ...(cliError?.retryable === undefined ? {} : { retryable: cliError.retryable }),
627
580
  ...(cliError?.repair === undefined ? {} : { repair: cliError.repair }),
581
+ ...(cliError?.details === undefined ? {} : { details: cliError.details }),
582
+ },
583
+ meta: {
584
+ ...(cliError?.meta ?? {}),
585
+ ...(cliError?.traceId !== undefined && cliError.meta?.trace_id === undefined ? { trace_id: cliError.traceId } : {}),
586
+ ...(cliError?.retryAfter !== undefined ? { retry_after: cliError.retryAfter } : {}),
587
+ ...(cliError?.idempotencyKey !== undefined ? { idempotency_key: cliError.idempotencyKey } : {}),
628
588
  },
629
- meta: { exit_code: exitCode },
630
589
  };
631
590
  }
632
591
  function openWebUrl(rawUrl, launch = node_child_process_1.spawn) {
@@ -687,7 +646,7 @@ function discoveredCapabilityId(value) {
687
646
  .filter((candidate) => typeof candidate === 'string' && candidate.length > 0);
688
647
  const ids = [...new Set(candidates)];
689
648
  if (ids.length !== 1)
690
- throw new CliUsageError('Local file import discovery must return exactly one capability.');
649
+ throw new errors_1.CliUsageError('Local file import discovery must return exactly one capability.');
691
650
  return ids[0];
692
651
  }
693
652
  function hostedSkill(value) {
@@ -705,31 +664,83 @@ function hostedSkill(value) {
705
664
  const documents = Array.isArray(entry.documents)
706
665
  ? entry.documents.filter((document) => Boolean(document) && typeof document === 'object' && typeof document.name === 'string' && typeof document.content === 'string')
707
666
  : undefined;
708
- return { id: entry.id, name: entry.name, description: entry.description, body: entry.body, frontmatter, documents };
667
+ // The server lists a skill's supporting pages beside the skill (data.files), each read
668
+ // with `skills get <id> --file <path>`; they are not inline in the skill record.
669
+ const files = Array.isArray(data.files)
670
+ ? data.files
671
+ .filter((file) => Boolean(file) && typeof file === 'object' && typeof file.path === 'string')
672
+ .map((file) => ({ path: file.path, ...(typeof file.revision_number === 'number' ? { revision_number: file.revision_number } : {}) }))
673
+ : undefined;
674
+ return {
675
+ id: entry.id, name: entry.name, description: entry.description, body: entry.body, frontmatter, documents,
676
+ ...(typeof entry.revision_number === 'number' ? { revision_number: entry.revision_number } : {}),
677
+ ...(files ? { files } : {}),
678
+ };
679
+ }
680
+ const SAFE_SKILL_ID = /^[a-z0-9][a-z0-9_-]*$/i;
681
+ /**
682
+ * One hosted skill with its supporting pages read in. Reads are one at a time because
683
+ * the workspace's private vault admits two reads at once, and fifteen parallel reads
684
+ * failed in production.
685
+ */
686
+ async function loadHostedSkill(client, id, withPages) {
687
+ if (!SAFE_SKILL_ID.test(id))
688
+ throw new errors_1.CliUsageError('Dreamstate API returned an invalid skill id.');
689
+ const skill = hostedSkill(await client.getSkill(id));
690
+ if (!SAFE_SKILL_ID.test(skill.id))
691
+ throw new errors_1.CliUsageError('Dreamstate API returned an invalid skill id.');
692
+ if (skill.id !== id)
693
+ throw new client_1.CliError('Dreamstate API returned a mismatched skill id.', 'invalid_response', 200);
694
+ if (!withPages || !skill.files?.length)
695
+ return skill;
696
+ const documents = [];
697
+ for (const file of skill.files) {
698
+ const page = hostedSkill(await client.getSkill(id, { file: file.path }));
699
+ documents.push({ name: file.path, content: page.body });
700
+ }
701
+ return { ...skill, documents };
702
+ }
703
+ /** Ids of every skill the server lists. */
704
+ async function listedSkillIds(client) {
705
+ const listed = await client.listAllSkills();
706
+ const entries = listed && typeof listed === 'object' && listed.data?.skills;
707
+ const ids = Array.isArray(entries) ? entries.map((skill) => skill && typeof skill === 'object' ? skill.id : undefined).filter((id) => typeof id === 'string') : [];
708
+ if (ids.some((id) => !SAFE_SKILL_ID.test(id)))
709
+ throw new errors_1.CliUsageError('Dreamstate API returned an invalid skill id.');
710
+ return ids;
709
711
  }
710
712
  function exitCodeForError(error) {
711
- if (code(error)?.startsWith('ERR_PARSE_ARGS'))
712
- return exports.CLI_EXIT.USAGE;
713
- if (error instanceof CliUsageError || error instanceof config_1.ApiKeyFileError || error instanceof config_1.ApiBaseUrlError || error instanceof config_1.MissingApiUrlError)
714
- return exports.CLI_EXIT.USAGE;
713
+ const errorCode = (0, gatedRun_1.code)(error);
714
+ if (errorCode?.startsWith('ERR_PARSE_ARGS'))
715
+ return errors_1.CLI_EXIT.USAGE;
716
+ if (error instanceof errors_1.CliUsageError || error instanceof config_1.ApiKeyFileError || error instanceof config_1.ApiBaseUrlError || error instanceof config_1.MissingApiUrlError)
717
+ return errors_1.CLI_EXIT.USAGE;
715
718
  if (error instanceof config_1.MissingApiKeyError)
716
- return exports.CLI_EXIT.AUTH;
717
- if (code(error) === 'request_timeout')
718
- return exports.CLI_EXIT.TIMEOUT;
719
+ return errors_1.CLI_EXIT.AUTH;
720
+ // By code before by status: a gate refusal arrives as 403 or 409 and must still read as
721
+ // "a person has to decide", and an update-required refusal is 426.
722
+ if (errorCode === 'cli_update_required')
723
+ return errors_1.CLI_EXIT.UPDATE_REQUIRED;
724
+ if ((0, errors_1.isApprovalGateCode)(errorCode))
725
+ return errors_1.CLI_EXIT.APPROVAL;
726
+ if (errorCode === 'request_timeout')
727
+ return errors_1.CLI_EXIT.TIMEOUT;
728
+ if ((0, errors_1.isLocalRefusalCode)(errorCode))
729
+ return errors_1.CLI_EXIT.REFUSED;
719
730
  const status = error instanceof client_1.CliError ? error.status : 0;
720
731
  if (status === 401 || status === 403)
721
- return exports.CLI_EXIT.AUTH;
732
+ return errors_1.CLI_EXIT.AUTH;
722
733
  if (status === 404)
723
- return exports.CLI_EXIT.NOT_FOUND;
734
+ return errors_1.CLI_EXIT.NOT_FOUND;
724
735
  if (status === 429)
725
- return exports.CLI_EXIT.RATE_LIMITED;
726
- if (code(error) === 'network_error')
727
- return exports.CLI_EXIT.TRANSPORT;
728
- if (status >= 500 || code(error) === 'invalid_response')
729
- return exports.CLI_EXIT.SERVER;
736
+ return errors_1.CLI_EXIT.RATE_LIMITED;
737
+ if (errorCode === 'network_error')
738
+ return errors_1.CLI_EXIT.TRANSPORT;
739
+ if (status >= 500 || errorCode === 'invalid_response')
740
+ return errors_1.CLI_EXIT.SERVER;
730
741
  if (status >= 400)
731
- return exports.CLI_EXIT.REFUSED;
732
- return exports.CLI_EXIT.UNCLASSIFIED;
742
+ return errors_1.CLI_EXIT.REFUSED;
743
+ return errors_1.CLI_EXIT.UNCLASSIFIED;
733
744
  }
734
745
  async function readLoginKey(command, runtime) {
735
746
  if (command.apiKeyFile)
@@ -747,7 +758,7 @@ async function readLoginKey(command, runtime) {
747
758
  }
748
759
  if (runtime.promptHidden)
749
760
  return runtime.promptHidden('API key (input hidden): ');
750
- throw new CliUsageError('No API key provided. Pipe it on stdin, set DREAMSTATE_API_KEY, or pass --api-key-file.');
761
+ throw new errors_1.CliUsageError('No API key provided. Pipe it on stdin, set DREAMSTATE_API_KEY, or pass --api-key-file.');
751
762
  }
752
763
  function deviceState() {
753
764
  return (0, node_crypto_1.randomBytes)(32).toString('base64url');
@@ -767,14 +778,14 @@ function extractWorkspaceFlag(argv) {
767
778
  if (argument === '--workspace') {
768
779
  const value = argv[index + 1];
769
780
  if (value === undefined || value.startsWith('-') || !value.trim())
770
- throw new CliUsageError('--workspace requires a workspace id or slug.');
781
+ throw new errors_1.CliUsageError('--workspace requires a workspace id or slug.');
771
782
  workspace = value.trim();
772
783
  index += 1;
773
784
  }
774
785
  else if (argument.startsWith('--workspace=')) {
775
786
  workspace = argument.slice('--workspace='.length).trim();
776
787
  if (!workspace)
777
- throw new CliUsageError('--workspace requires a workspace id or slug.');
788
+ throw new errors_1.CliUsageError('--workspace requires a workspace id or slug.');
778
789
  }
779
790
  else {
780
791
  rest.push(argument);
@@ -786,25 +797,29 @@ function whoamiData(value) {
786
797
  const data = value && typeof value === 'object' ? value.data : undefined;
787
798
  return data && typeof data === 'object' ? data : {};
788
799
  }
789
- function statusData(whoami, health) {
790
- const meta = whoami && typeof whoami === 'object' ? whoami.meta : null;
791
- const serverVersion = meta && typeof meta === 'object' ? meta.server_version : null;
792
- const minimumCliVersion = meta && typeof meta === 'object' ? meta.minimum_cli_version : null;
800
+ /**
801
+ * What `dreamstate status` reports. The release identity comes from the server's
802
+ * own answers (envelope meta and X-Dreamstate-* headers on every response), so
803
+ * skew is measured, not assumed: with a server that publishes nothing the skew is
804
+ * "unknown", never "compatible".
805
+ */
806
+ function statusData(whoami, health, info) {
793
807
  const healthData = health && typeof health === 'object' ? health.data : null;
794
808
  const healthVersion = healthData && typeof healthData === 'object' ? healthData.version : null;
795
- const version = typeof serverVersion === 'string' ? serverVersion : typeof healthVersion === 'string' ? healthVersion : null;
796
- const minimum = typeof minimumCliVersion === 'string' ? minimumCliVersion : null;
797
- const parts = (value) => value.replace(/^v/, '').split('-')[0].split('.').map((part) => Number(part));
798
- const [minimumMajor = 0, minimumMinor = 0, minimumPatch = 0] = minimum ? parts(minimum) : [];
799
- const [cliMajor = 0, cliMinor = 0, cliPatch = 0] = parts(client_1.CLI_VERSION);
800
- const compatible = !minimum || minimumMajor < cliMajor || (minimumMajor === cliMajor && (minimumMinor < cliMinor || (minimumMinor === cliMinor && minimumPatch <= cliPatch)));
809
+ const serverVersion = info.server_version ?? (typeof healthVersion === 'string' ? healthVersion : null);
810
+ const resolved = { ...info, server_version: serverVersion };
811
+ const skew = (0, client_1.describeVersionSkew)(resolved);
812
+ const compatible = skew !== 'cli_below_minimum';
801
813
  return {
802
814
  cli_version: client_1.CLI_VERSION,
803
- api_version: version,
804
- minimum_cli_version: minimum,
805
- minimum_cli_version_published: minimum !== null,
815
+ api_version: serverVersion,
816
+ server_version: serverVersion,
817
+ release_sha: info.release_sha,
818
+ minimum_cli_version: info.minimum_cli_version,
819
+ minimum_cli_version_published: info.minimum_cli_version !== null,
820
+ skew,
806
821
  compatible,
807
- upgrade_hint: compatible ? null : 'Run: npm install -g @trydreamstate/cli@latest',
822
+ upgrade_hint: skew === 'cli_below_minimum' || skew === 'cli_behind_server' ? `Run: ${client_1.CLI_UPDATE_COMMAND}` : null,
808
823
  health,
809
824
  whoami: whoamiData(whoami),
810
825
  };
@@ -816,36 +831,31 @@ function activeWorkspace(data) {
816
831
  function wait(milliseconds) {
817
832
  return new Promise((resolve) => setTimeout(resolve, milliseconds));
818
833
  }
834
+ /**
835
+ * The one way the CLI installs skills: every skill from the hosted catalog (the same
836
+ * records MCP skills_get and `skills get` read), with its supporting pages, plus the
837
+ * router. `skills install`, `login` and the single-skill form all come through here, so
838
+ * there is no second source of skill text on this machine.
839
+ */
819
840
  async function installHostedSkills(client, agents, runtime, options = {}) {
820
- const listed = await client.listAllSkills();
821
- const entries = listed && typeof listed === 'object' && listed.data?.skills;
822
- const ids = Array.isArray(entries) ? entries.map((skill) => skill && typeof skill === 'object' ? skill.id : undefined).filter((id) => typeof id === 'string') : [];
841
+ const ids = options.only ?? await listedSkillIds(client);
823
842
  if (!ids.length)
824
843
  throw new client_1.CliError('Dreamstate API returned no installable skills.', 'invalid_response', 200);
825
- if (ids.some((id) => !/^[a-z0-9][a-z0-9_-]*$/i.test(id)))
826
- throw new CliUsageError('Dreamstate API returned an invalid skill id.');
827
844
  const skills = [];
828
- for (const id of ids) {
829
- const skill = hostedSkill(await client.getSkill(id));
830
- if (!/^[a-z0-9][a-z0-9_-]*$/i.test(skill.id))
831
- throw new CliUsageError('Dreamstate API returned an invalid skill id.');
832
- if (skill.id !== id)
833
- throw new client_1.CliError('Dreamstate API returned a mismatched skill id.', 'invalid_response', 200);
834
- skills.push(skill);
835
- }
845
+ for (const id of ids)
846
+ skills.push(await loadHostedSkill(client, id, true));
836
847
  const packs = await client.listPacks();
837
848
  const whoami = await client.getWhoami();
838
849
  const workspaceId = typeof whoami?.data?.workspace_id === 'string' ? whoami.data.workspace_id : 'unknown';
839
- const indexVersion = (0, node_crypto_1.createHash)('sha256')
840
- .update(JSON.stringify(skills.map((skill) => [skill.id, skill.description, skill.body, skill.frontmatter ?? {}])))
841
- .digest('hex').slice(0, 16);
842
- const installed = await (0, skillInstaller_1.installSkills)([...skills, (0, skillInstaller_1.routerSkill)(packs)], {
850
+ const all = [...skills, (0, skillInstaller_1.routerSkill)(packs)];
851
+ // Stamped into every file so a stale install is visible against the live skills.
852
+ const installed = await (0, skillInstaller_1.installSkills)(all, {
843
853
  agents,
844
854
  homeDirectory: runtime.homeDirectory,
845
855
  project: Boolean(options.project),
846
856
  copy: Boolean(options.copy) || process.platform === 'win32',
847
857
  workspaceId,
848
- indexVersion,
858
+ indexVersion: (0, skillInstaller_1.skillIndexVersion)(all),
849
859
  });
850
860
  return { files: installed.files, skillIds: [...ids, 'router'] };
851
861
  }
@@ -930,12 +940,12 @@ async function runCli(argv = process.argv.slice(2), runtime = {}) {
930
940
  if (command.command === 'help' || command.command === 'pack_help') {
931
941
  const packHelp = command.command === 'pack_help' ? command : undefined;
932
942
  print({ ok: true, data: { usage: packHelp?.usage ?? USAGE }, meta: {} }, stdout, Boolean(packHelp?.jsonOutput));
933
- process.exitCode = exports.CLI_EXIT.SUCCESS;
943
+ process.exitCode = errors_1.CLI_EXIT.SUCCESS;
934
944
  return;
935
945
  }
936
946
  if (command.command === 'version') {
937
947
  print({ ok: true, data: { version: client_1.CLI_VERSION }, meta: {} }, stdout, false);
938
- process.exitCode = exports.CLI_EXIT.SUCCESS;
948
+ process.exitCode = errors_1.CLI_EXIT.SUCCESS;
939
949
  return;
940
950
  }
941
951
  if (command.command === 'install') {
@@ -948,7 +958,7 @@ async function runCli(argv = process.argv.slice(2), runtime = {}) {
948
958
  const removedSkills = done.includes('claude') ? await (0, pluginInstaller_1.removeClaudeSkillCopies)(runtime.homeDirectory) : [];
949
959
  stderr.write(`Dreamstate plugin installed for ${done.join(', ')}. Start a new session; the first Dreamstate tool call asks you to sign in.\n`);
950
960
  print({ ok: true, data: { ...installed, removed_skill_copies: removedSkills }, meta: {} }, stdout, Boolean(command.jsonOutput));
951
- process.exitCode = exports.CLI_EXIT.SUCCESS;
961
+ process.exitCode = errors_1.CLI_EXIT.SUCCESS;
952
962
  return;
953
963
  }
954
964
  if (command.command === 'logout') {
@@ -967,7 +977,7 @@ async function runCli(argv = process.argv.slice(2), runtime = {}) {
967
977
  const message = removed ? `Removed ${path}` : `Already logged out: no credentials at ${path}`;
968
978
  stderr.write(`${message}\n`);
969
979
  print({ ok: true, data: { credentials_path: path, removed }, meta: {} }, stdout, Boolean(command.jsonOutput));
970
- process.exitCode = exports.CLI_EXIT.SUCCESS;
980
+ process.exitCode = errors_1.CLI_EXIT.SUCCESS;
971
981
  return;
972
982
  }
973
983
  if (command.command === 'profiles_list' || command.command === 'profiles_current' || command.command === 'profiles_use' || command.command === 'profiles_remove' || command.command === 'profiles_env') {
@@ -992,20 +1002,20 @@ async function runCli(argv = process.argv.slice(2), runtime = {}) {
992
1002
  else {
993
1003
  const profiles = await (0, config_1.listCredentialProfiles)(path);
994
1004
  if (!profiles.some((profile) => profile.name === command.name))
995
- throw new CliUsageError(`Profile not found: ${command.name}.`);
1005
+ throw new errors_1.CliUsageError(`Profile not found: ${command.name}.`);
996
1006
  const result = { ok: true, data: { profile: command.name, exports: [`DREAMSTATE_PROFILE=${command.name}`] }, meta: {} };
997
1007
  if (command.jsonOutput)
998
1008
  print(result, stdout, true);
999
1009
  else
1000
1010
  stdout.write(`export DREAMSTATE_PROFILE=${command.name}\n`);
1001
1011
  }
1002
- process.exitCode = exports.CLI_EXIT.SUCCESS;
1012
+ process.exitCode = errors_1.CLI_EXIT.SUCCESS;
1003
1013
  return;
1004
1014
  }
1005
1015
  if (command.command === 'login' && command.token !== undefined) {
1006
1016
  const token = command.token.trim();
1007
1017
  if (!(0, config_1.isDreamstateToken)(token))
1008
- throw new CliUsageError('--token must be a Dreamstate session or API key starting with ds_user_ or ds_live_.');
1018
+ throw new errors_1.CliUsageError('--token must be a Dreamstate session or API key starting with ds_user_ or ds_live_.');
1009
1019
  const environment = runtime.env ?? process.env;
1010
1020
  const baseUrl = (0, config_1.resolveApiBaseUrl)(environment);
1011
1021
  // Only an explicit flag or env picks the workspace here: a stored default belongs to the previous login.
@@ -1024,7 +1034,7 @@ async function runCli(argv = process.argv.slice(2), runtime = {}) {
1024
1034
  await (0, config_1.storeApiKey)(token, runtimeCredentialsPath(runtime));
1025
1035
  const skills = await installLoginSkills(client, runtime, Boolean(command.noSkills), stderr);
1026
1036
  print(loginResult(whoami, skills), stdout, Boolean(command.jsonOutput));
1027
- process.exitCode = exports.CLI_EXIT.SUCCESS;
1037
+ process.exitCode = errors_1.CLI_EXIT.SUCCESS;
1028
1038
  return;
1029
1039
  }
1030
1040
  if (command.command === 'status') {
@@ -1033,27 +1043,47 @@ async function runCli(argv = process.argv.slice(2), runtime = {}) {
1033
1043
  const apiKey = await (0, config_1.resolveApiKey)(environment, credentialFilePath);
1034
1044
  const workspace = await (0, config_1.resolveWorkspace)(extracted.workspace, environment, credentialFilePath);
1035
1045
  const client = new client_1.DreamstateClient({ baseUrl: (0, config_1.resolveApiBaseUrl)(environment), apiKey, ...(workspace ? { workspace } : {}), requestTimeoutMs: runtime.requestTimeoutMs });
1036
- const [whoami, health] = await Promise.all([client.getStatusWhoami(), client.getHealth()]);
1037
- print({ ok: true, data: statusData(whoami, health), meta: {} }, stdout, Boolean(command.jsonOutput));
1038
- process.exitCode = exports.CLI_EXIT.SUCCESS;
1046
+ let whoami = null;
1047
+ let health = null;
1048
+ let refusedAsTooOld = null;
1049
+ try {
1050
+ [whoami, health] = await Promise.all([client.getStatusWhoami(), client.getHealth()]);
1051
+ }
1052
+ catch (error) {
1053
+ // A server that refuses this CLI as too old (426) is the clearest skew report there is:
1054
+ // print it as a status, with the update command, instead of only an error.
1055
+ if (!(error instanceof client_1.CliError) || error.code !== 'cli_update_required')
1056
+ throw error;
1057
+ refusedAsTooOld = error;
1058
+ }
1059
+ const info = client.serverInfo;
1060
+ const refusal = refusedAsTooOld?.details && typeof refusedAsTooOld.details === 'object' ? refusedAsTooOld.details : {};
1061
+ const data = statusData(whoami, health, {
1062
+ ...info,
1063
+ minimum_cli_version: info.minimum_cli_version ?? (typeof refusal.minimum_cli_version === 'string' ? refusal.minimum_cli_version : null),
1064
+ });
1065
+ print({ ok: true, data, meta: {} }, stdout, Boolean(command.jsonOutput));
1066
+ process.exitCode = data.skew === 'cli_below_minimum' ? errors_1.CLI_EXIT.UPDATE_REQUIRED : errors_1.CLI_EXIT.SUCCESS;
1067
+ if (process.exitCode === errors_1.CLI_EXIT.UPDATE_REQUIRED)
1068
+ stderr.write(`This CLI (${client_1.CLI_VERSION}) is below the server minimum${data.minimum_cli_version ? ` ${String(data.minimum_cli_version)}` : ''}. ${String(data.upgrade_hint)}\n`);
1039
1069
  return;
1040
1070
  }
1041
1071
  if (command.command === 'login') {
1042
1072
  if (command.apiKeyFile || (runtime.env ?? process.env).DREAMSTATE_API_KEY || runtime.promptHidden) {
1043
1073
  const key = await readLoginKey(command, runtime);
1044
1074
  if (!(0, config_1.isDreamstateToken)(key))
1045
- throw new CliUsageError('Dreamstate API keys must start with ds_user_ or ds_live_.');
1075
+ throw new errors_1.CliUsageError('Dreamstate API keys must start with ds_user_ or ds_live_.');
1046
1076
  const path = command.profile
1047
1077
  ? await (0, config_1.storeCredentialProfile)(command.profile, key, runtimeCredentialsPath(runtime))
1048
1078
  : await (0, config_1.storeApiKey)(key, runtimeCredentialsPath(runtime));
1049
1079
  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`) });
1050
1080
  const skills = await installLoginSkills(client, runtime, Boolean(command.noSkills), stderr);
1051
1081
  print({ ok: true, data: { credentials_path: path, skills }, meta: {} }, stdout, Boolean(command.jsonOutput));
1052
- process.exitCode = exports.CLI_EXIT.SUCCESS;
1082
+ process.exitCode = errors_1.CLI_EXIT.SUCCESS;
1053
1083
  return;
1054
1084
  }
1055
1085
  await loginWithDeviceAuthorization(runtime, stdout, stderr, command.profile, Boolean(command.noSkills), Boolean(command.jsonOutput));
1056
- process.exitCode = exports.CLI_EXIT.SUCCESS;
1086
+ process.exitCode = errors_1.CLI_EXIT.SUCCESS;
1057
1087
  return;
1058
1088
  }
1059
1089
  if (command.command === 'skills_doctor') {
@@ -1065,8 +1095,56 @@ async function runCli(argv = process.argv.slice(2), runtime = {}) {
1065
1095
  const client = new client_1.DreamstateClient({ baseUrl, apiKey, ...(workspace ? { workspace } : {}), requestTimeoutMs: runtime.requestTimeoutMs });
1066
1096
  await client.listSkills({ limit: 1 });
1067
1097
  const installers = await (0, skillInstaller_1.inspectSkillInstallers)(command.agents, runtime.homeDirectory, Boolean(command.project));
1068
- print({ ok: true, data: { index_reachable: true, installers }, meta: {} }, stdout, Boolean(command.jsonOutput));
1069
- process.exitCode = exports.CLI_EXIT.SUCCESS;
1098
+ // Age says nothing about whether the install matches the server. Compare the content
1099
+ // stamp of every installed skill with the live skill's, the way install computes it.
1100
+ const ids = await listedSkillIds(client);
1101
+ const live = [];
1102
+ for (const id of ids)
1103
+ live.push(await loadHostedSkill(client, id, false));
1104
+ live.push((0, skillInstaller_1.routerSkill)(await client.listPacks()));
1105
+ const drift = await (0, skillInstaller_1.compareInstalledSkills)(live, runtime.homeDirectory, Boolean(command.project));
1106
+ const pluginAgents = await (0, pluginInstaller_1.agentsWithDreamstatePlugin)(runtime.homeDirectory);
1107
+ const duplicateSources = pluginAgents.filter((agent) => command.agents.includes(agent) && drift.installed_skill_ids.length > 0);
1108
+ print({ ok: true, data: { index_reachable: true, installers, content: drift, plugin_agents: pluginAgents, ...(duplicateSources.length ? { duplicate_skill_sources: duplicateSources } : {}) }, meta: {} }, stdout, Boolean(command.jsonOutput));
1109
+ process.exitCode = errors_1.CLI_EXIT.SUCCESS;
1110
+ return;
1111
+ }
1112
+ // The CLI's own commands answer from its parser tables, so they need no key;
1113
+ // matching capabilities are added when a key is available.
1114
+ if (command.command === 'commands_search') {
1115
+ const commands = (0, commandTable_1.searchCliCommands)(command.term, USAGE);
1116
+ let capabilities = null;
1117
+ let capabilitiesError = null;
1118
+ try {
1119
+ const environment = runtime.env ?? process.env;
1120
+ const credentialFilePath = runtimeCredentialsPath(runtime);
1121
+ const apiKey = await (0, config_1.resolveApiKey)(environment, credentialFilePath);
1122
+ const workspace = await (0, config_1.resolveWorkspace)(extracted.workspace, environment, credentialFilePath);
1123
+ const client = new client_1.DreamstateClient({
1124
+ baseUrl: (0, config_1.resolveApiBaseUrl)(environment),
1125
+ apiKey,
1126
+ ...(workspace ? { workspace } : {}),
1127
+ requestTimeoutMs: runtime.requestTimeoutMs,
1128
+ onVersionWarning: (message) => stderr.write(`${message}\n`),
1129
+ });
1130
+ capabilities = await client.listTools({ goal: command.term });
1131
+ }
1132
+ catch (error) {
1133
+ if (!commands.length || (error instanceof client_1.CliError && error.code === 'cli_update_required'))
1134
+ throw error;
1135
+ capabilitiesError = { code: (0, gatedRun_1.code)(error) ?? 'request_failed', message: error instanceof Error ? error.message : String(error) };
1136
+ }
1137
+ print({
1138
+ ok: true,
1139
+ data: {
1140
+ commands,
1141
+ // Catalog capabilities that match the same words; run one with `dreamstate tools run <capability_id>`.
1142
+ capabilities: capabilities?.data ?? null,
1143
+ ...(capabilitiesError ? { capabilities_error: capabilitiesError } : {}),
1144
+ },
1145
+ meta: capabilities?.meta && typeof capabilities.meta === 'object' ? capabilities.meta : {},
1146
+ }, stdout, Boolean(command.jsonOutput));
1147
+ process.exitCode = errors_1.CLI_EXIT.SUCCESS;
1070
1148
  return;
1071
1149
  }
1072
1150
  // Preview parses the local file and writes nothing, so it must not require a key.
@@ -1081,9 +1159,9 @@ async function runCli(argv = process.argv.slice(2), runtime = {}) {
1081
1159
  }, stdout, Boolean(command.jsonOutput));
1082
1160
  }
1083
1161
  catch (error) {
1084
- throw new CliUsageError(error instanceof CliUsageError ? error.message : error instanceof Error ? error.message : String(error));
1162
+ throw new errors_1.CliUsageError(error instanceof errors_1.CliUsageError ? error.message : error instanceof Error ? error.message : String(error));
1085
1163
  }
1086
- process.exitCode = exports.CLI_EXIT.SUCCESS;
1164
+ process.exitCode = errors_1.CLI_EXIT.SUCCESS;
1087
1165
  return;
1088
1166
  }
1089
1167
  // init writes a local scaffold and calls no API, so it must not require a key.
@@ -1097,7 +1175,7 @@ async function runCli(argv = process.argv.slice(2), runtime = {}) {
1097
1175
  await require('./commands/workflows')
1098
1176
  .runWorkflowsCommand(undefined, command, stdout);
1099
1177
  }
1100
- process.exitCode = exports.CLI_EXIT.SUCCESS;
1178
+ process.exitCode = errors_1.CLI_EXIT.SUCCESS;
1101
1179
  return;
1102
1180
  }
1103
1181
  const environment = runtime.env ?? process.env;
@@ -1117,7 +1195,7 @@ async function runCli(argv = process.argv.slice(2), runtime = {}) {
1117
1195
  }
1118
1196
  if (command.command === 'workspaces_use' && apiKey.startsWith('ds_live_')) {
1119
1197
  // Decided locally: a workspace key can never switch, so no request is worth making.
1120
- throw new CliUsageError('This key is bound to one workspace. Run dreamstate login for a user session that can switch.');
1198
+ throw new errors_1.CliUsageError('This key is bound to one workspace. Run dreamstate login for a user session that can switch.');
1121
1199
  }
1122
1200
  const workspace = command.command === 'workspaces_use'
1123
1201
  ? command.workspace
@@ -1129,6 +1207,19 @@ async function runCli(argv = process.argv.slice(2), runtime = {}) {
1129
1207
  requestTimeoutMs: runtime.requestTimeoutMs,
1130
1208
  onVersionWarning: (message) => (runtime.stderr ?? process.stderr).write(`${message}\n`),
1131
1209
  });
1210
+ const turnsPath = (0, turnCache_1.turnCachePath)(credentialFilePath);
1211
+ const idempotencyPath = (0, idempotency_1.pendingIdempotencyPath)(credentialFilePath);
1212
+ // A turn belongs to one workspace, so the workspace is part of the cache identity.
1213
+ const gatedContext = () => ({
1214
+ client,
1215
+ baseUrl,
1216
+ turnKey: workspace ? `${apiKey}\n${workspace}` : apiKey,
1217
+ turnsPath,
1218
+ idempotencyPath,
1219
+ ...(workspace ? { workspace } : {}),
1220
+ stderr,
1221
+ sleep: runtime.sleep ?? wait,
1222
+ });
1132
1223
  let result;
1133
1224
  switch (command.command) {
1134
1225
  case 'whoami': {
@@ -1149,7 +1240,7 @@ async function runCli(argv = process.argv.slice(2), runtime = {}) {
1149
1240
  stdout.write(rows.length
1150
1241
  ? `${rows.map((row) => `${row.id === activeId ? '*' : ' '} ${row.slug ?? '-'} ${row.name} ${row.role ?? '-'} ${row.id}`).join('\n')}\n`
1151
1242
  : 'No workspaces. Ask a workspace owner to invite you.\n');
1152
- process.exitCode = exports.CLI_EXIT.SUCCESS;
1243
+ process.exitCode = errors_1.CLI_EXIT.SUCCESS;
1153
1244
  return;
1154
1245
  }
1155
1246
  break;
@@ -1160,10 +1251,10 @@ async function runCli(argv = process.argv.slice(2), runtime = {}) {
1160
1251
  const wanted = command.workspace.toLowerCase();
1161
1252
  const match = data.workspaces?.find((row) => row.id.toLowerCase() === wanted || row.slug?.toLowerCase() === wanted);
1162
1253
  if (!match)
1163
- throw new CliUsageError(`You are not a member of workspace "${command.workspace}". Run dreamstate workspaces list to see the workspaces you can use.`);
1254
+ throw new errors_1.CliUsageError(`You are not a member of workspace "${command.workspace}". Run dreamstate workspaces list to see the workspaces you can use.`);
1164
1255
  const stored = await (0, config_1.readStoredCredentials)(await (0, config_1.selectedCredentialsPath)(credentialFilePath, environment));
1165
1256
  if (!stored)
1166
- throw new CliUsageError('workspaces use saves the default next to a stored login. Run dreamstate login or dreamstate login --token first.');
1257
+ throw new errors_1.CliUsageError('workspaces use saves the default next to a stored login. Run dreamstate login or dreamstate login --token first.');
1167
1258
  await (0, config_1.storeApiKey)(stored.token, await (0, config_1.selectedCredentialsPath)(credentialFilePath, environment), match.id);
1168
1259
  stderr.write(`Default workspace: ${match.name} (${match.slug})\n`);
1169
1260
  result = { ok: true, data: { workspace: { id: match.id, name: match.name, slug: match.slug }, credentials_path: credentialFilePath }, meta: {} };
@@ -1188,47 +1279,26 @@ async function runCli(argv = process.argv.slice(2), runtime = {}) {
1188
1279
  result = await client.searchSkillDocuments(command.query);
1189
1280
  break;
1190
1281
  case 'skills_install': {
1191
- if (command.skillId === '*') {
1192
- const installed = await installHostedSkills(client, command.agents, runtime, { project: command.project, copy: command.copy });
1193
- result = { ok: true, data: { installed_files: installed.files, skill_ids: installed.skillIds, agents: command.agents, ...(command.project ? { lock_file: 'skills-lock.json' } : {}) }, meta: {} };
1282
+ // An agent that already loads Dreamstate skills from the plugin gets them from there:
1283
+ // installing hosted copies too would load the same skill twice under two names, from
1284
+ // two sources that drift apart. login skips such agents the same way.
1285
+ const withPlugin = await (0, pluginInstaller_1.agentsWithDreamstatePlugin)(runtime.homeDirectory);
1286
+ const targets = command.agents.filter((agent) => !withPlugin.includes(agent));
1287
+ const skipped = command.agents.filter((agent) => !targets.includes(agent)).map((agent) => ({ agent, reason: 'plugin_installed' }));
1288
+ if (skipped.length)
1289
+ stderr.write(`Skipped ${skipped.map((entry) => entry.agent).join(', ')}: the Dreamstate plugin already provides skills there. Update it with dreamstate install.\n`);
1290
+ if (!targets.length) {
1291
+ result = { ok: true, data: { installed_files: [], skill_ids: [], agents: [], skipped_agents: skipped }, meta: {} };
1194
1292
  break;
1195
1293
  }
1196
- const ids = [command.skillId];
1197
- // A remote id becomes a directory name; refuse traversal before any request or write.
1198
- if (ids.some((id) => !/^[a-z0-9][a-z0-9_-]*$/i.test(id)))
1199
- throw new CliUsageError('Dreamstate API returned an invalid skill id.');
1200
- // Read each skill one at a time because the workspace's private vault
1201
- // admits two reads at once, and fifteen parallel reads failed in production.
1202
- const skills = [];
1203
- for (const id of ids) {
1204
- const skill = hostedSkill(await client.getSkill(id));
1205
- if (!/^[a-z0-9][a-z0-9_-]*$/i.test(skill.id))
1206
- throw new CliUsageError('Dreamstate API returned an invalid skill id.');
1207
- if (skill.id !== id)
1208
- throw new client_1.CliError('Dreamstate API returned a mismatched skill id.', 'invalid_response', 200);
1209
- skills.push(skill);
1210
- }
1211
- const packs = await client.listPacks();
1212
- const whoami = await client.getWhoami();
1213
- const workspaceId = typeof whoami?.data?.workspace_id === 'string' ? whoami.data.workspace_id : 'unknown';
1214
- // Stamped into every file so a stale install is visible against the live skills.
1215
- const indexVersion = (0, node_crypto_1.createHash)('sha256')
1216
- .update(JSON.stringify(skills.map((skill) => [skill.id, skill.description, skill.body, skill.frontmatter ?? {}])))
1217
- .digest('hex').slice(0, 16);
1218
- const installed = await (0, skillInstaller_1.installSkills)([...skills, (0, skillInstaller_1.routerSkill)(packs)], {
1219
- agents: command.agents,
1220
- homeDirectory: runtime.homeDirectory,
1221
- project: Boolean(command.project),
1222
- copy: Boolean(command.copy) || process.platform === 'win32',
1223
- workspaceId,
1224
- indexVersion,
1294
+ const installed = await installHostedSkills(client, targets, runtime, {
1295
+ project: command.project,
1296
+ copy: command.copy,
1297
+ ...(command.skillId === '*' ? {} : { only: [command.skillId] }),
1225
1298
  });
1226
- result = { ok: true, data: { installed_files: installed.files, skill_ids: [...ids, 'router'], agents: command.agents, ...(command.project ? { lock_file: 'skills-lock.json' } : {}) }, meta: {} };
1299
+ result = { ok: true, data: { installed_files: installed.files, skill_ids: installed.skillIds, agents: targets, ...(skipped.length ? { skipped_agents: skipped } : {}), ...(command.project ? { lock_file: 'skills-lock.json' } : {}) }, meta: {} };
1227
1300
  break;
1228
1301
  }
1229
- case 'commands_search':
1230
- result = await client.listTools({ goal: command.term });
1231
- break;
1232
1302
  case 'tools_search':
1233
1303
  result = await client.listTools(command.query);
1234
1304
  break;
@@ -1239,100 +1309,30 @@ async function runCli(argv = process.argv.slice(2), runtime = {}) {
1239
1309
  result = await client.getTool(command.capabilityId);
1240
1310
  break;
1241
1311
  case 'tools_run': {
1242
- // A quote binds to the turn that asked for it. Reuse a turn only while
1243
- // a quote is pending (after a dry run or a quote_required refusal), and
1244
- // forget it once the paid run is admitted, so every other call keeps
1245
- // fetching a fresh turn exactly as before.
1246
- const turns = (0, turnCache_1.turnCachePath)(credentialFilePath);
1247
- // A turn belongs to one workspace, so the workspace is part of the cache identity.
1248
- const turnKey = workspace ? `${apiKey}\n${workspace}` : apiKey;
1249
- let warnedAboutTurnCache = false;
1250
- const warnTurnCache = () => {
1251
- if (warnedAboutTurnCache)
1252
- return;
1253
- warnedAboutTurnCache = true;
1254
- stderr.write('Warning: Dreamstate turn cache unavailable; continuing without turn reuse.\n');
1255
- };
1256
- const pending = command.turnToken
1257
- ? null
1258
- : await (0, turnCache_1.reusableTurnToken)(turns, baseUrl, turnKey, command.capabilityId, Date.now(), warnTurnCache);
1259
- const inlineRead = !command.turnToken && !pending && !command.idempotencyKey && omitsToolTurnForInlineTableRead(command.capabilityId);
1260
- const fetchedForRun = command.turnToken || pending || inlineRead ? null : await client.getTool(command.capabilityId);
1261
- let turnToken = command.turnToken ?? pending ?? (inlineRead ? undefined : toolTurnToken(fetchedForRun));
1262
- const callerParameterKey = parameterIdempotencyKey(command.body);
1263
- let idempotencyKey = capabilityIdempotencyKey(fetchedForRun, callerParameterKey ?? command.idempotencyKey);
1264
- let recoveredInlineRead = false;
1265
- const run = () => client.runCapability(command.capabilityId, {
1266
- ...command.body,
1267
- ...(turnToken ? { tool_turn_token: turnToken } : {}),
1268
- }, idempotencyKey);
1269
- try {
1270
- try {
1271
- result = await run();
1272
- }
1273
- catch (error) {
1274
- // The inline skip is only valid while the server still treats this
1275
- // capability as a free sync read. capability_not_fetched means the
1276
- // fetch-first fence is on, so retry once on the old get-then-run path.
1277
- if (inlineRead && !recoveredInlineRead && code(error) === 'capability_not_fetched') {
1278
- recoveredInlineRead = true;
1279
- const fetched = await client.getTool(command.capabilityId);
1280
- turnToken = toolTurnToken(fetched);
1281
- idempotencyKey = capabilityIdempotencyKey(fetched, callerParameterKey ?? command.idempotencyKey);
1282
- result = await run();
1283
- }
1284
- else if (command.body.mode !== 'live' || code(error) !== 'quote_required') {
1285
- // A quote_required refusal binds a fresh quote to this turn, so the
1286
- // identical call admits on the next try. Retry once here instead of
1287
- // making the caller rerun the same command by hand.
1288
- throw error;
1289
- }
1290
- else {
1291
- result = await run();
1292
- }
1293
- }
1294
- }
1295
- catch (error) {
1296
- if (!command.turnToken && turnToken && code(error) === 'quote_required') {
1297
- await (0, turnCache_1.rememberTurnToken)(turns, baseUrl, turnKey, command.capabilityId, turnToken, Date.now(), warnTurnCache);
1298
- }
1299
- throw error;
1300
- }
1301
- if (!command.turnToken) {
1302
- if (command.body.mode === 'dry_run' && turnToken)
1303
- await (0, turnCache_1.rememberTurnToken)(turns, baseUrl, turnKey, command.capabilityId, turnToken, Date.now(), warnTurnCache);
1304
- else if (pending)
1305
- await (0, turnCache_1.forgetTurnToken)(turns, baseUrl, turnKey, command.capabilityId, warnTurnCache);
1306
- }
1307
- if (!command.noWait && !(0, runWait_1.columnRunReturnsRunId)(command.capabilityId)) {
1308
- const waited = await (0, runWait_1.waitForRun)(client, result, command.waitTimeout, runtime.sleep ?? wait);
1309
- result = waited.value;
1310
- if (waited.timedOut)
1311
- process.exitCode = runWait_1.RUN_WAIT_TIMEOUT_EXIT_CODE;
1312
- }
1312
+ const outcome = await (0, gatedRun_1.runGated)(gatedContext(), {
1313
+ capabilityId: command.capabilityId,
1314
+ body: command.body,
1315
+ ...(command.turnToken ? { turnToken: command.turnToken } : {}),
1316
+ ...(command.idempotencyKey ? { idempotencyKey: command.idempotencyKey } : {}),
1317
+ waitTimeout: command.waitTimeout,
1318
+ ...(command.noWait ? { noWait: true } : {}),
1319
+ });
1320
+ result = outcome.value;
1321
+ if (outcome.timedOut)
1322
+ process.exitCode = runWait_1.RUN_WAIT_TIMEOUT_EXIT_CODE;
1313
1323
  break;
1314
1324
  }
1315
1325
  case 'tables_import': {
1316
1326
  await (0, promises_1.access)(command.file);
1317
- // Search ranking is not an import contract. This exact capability owns the
1318
- // prepare handoff for caller-local files, while nearby upload capabilities
1319
- // can also match these words and made a real import fail after local work.
1320
- const importCapabilityId = 'tables.table_sources.csv_import_prepare';
1321
- const importContract = await client.getTool(importCapabilityId);
1322
- result = await client.runCapability(importCapabilityId, {
1323
- // A mutation run needs the turn token its own get minted; without it
1324
- // an API-key import was refused after the local file work.
1325
- tool_turn_token: toolTurnToken(importContract),
1326
- mode: 'live',
1327
- params: {
1328
- table_id: command.tableId,
1329
- file_path: command.file,
1330
- ...(command.format ? { file_kind: command.format } : {}),
1331
- ...(command.upsertKey ? { upsert_key: command.upsertKey } : {}),
1332
- ...(command.rowKind ? { row_kind: command.rowKind } : {}),
1333
- ...(command.keepUnmappedColumns === false ? { keep_unmapped_columns: false } : {}),
1334
- },
1335
- }, capabilityIdempotencyKey(importContract));
1327
+ const imported = await (0, tablesImport_1.importLocalTable)(gatedContext(), {
1328
+ tableId: command.tableId,
1329
+ file: command.file,
1330
+ ...(command.format ? { format: command.format } : {}),
1331
+ ...(command.upsertKey ? { upsertKey: command.upsertKey } : {}),
1332
+ ...(command.rowKind ? { rowKind: command.rowKind } : {}),
1333
+ ...(command.keepUnmappedColumns === false ? { keepUnmappedColumns: false } : {}),
1334
+ });
1335
+ result = { ok: true, data: { import: imported }, meta: {} };
1336
1336
  break;
1337
1337
  }
1338
1338
  case 'runs_list':
@@ -1341,12 +1341,6 @@ async function runCli(argv = process.argv.slice(2), runtime = {}) {
1341
1341
  case 'runs_get':
1342
1342
  result = await client.getRun(command.runId);
1343
1343
  break;
1344
- case 'runs_resume':
1345
- result = await client.resumeRun(command.runId, {
1346
- expected_state_version: command.stateVersion,
1347
- ...(command.reason ? { reason: command.reason } : {}),
1348
- }, command.idempotencyKey);
1349
- break;
1350
1344
  case 'runs_events':
1351
1345
  result = await client.getRunEvents(command.runId);
1352
1346
  break;
@@ -1355,28 +1349,40 @@ async function runCli(argv = process.argv.slice(2), runtime = {}) {
1355
1349
  expected_state_version: command.stateVersion ?? stateVersionFromRunRead(await client.getRun(command.runId)),
1356
1350
  ...(command.reason ? { reason: command.reason } : {}),
1357
1351
  };
1358
- result = await client.cancelRun(command.runId, body, command.idempotencyKey);
1352
+ const identity = { scope: 'runs.cancel', workspace, body: { run_id: command.runId, ...body } };
1353
+ const key = command.idempotencyKey ?? await (0, idempotency_1.pendingIdempotencyKey)(idempotencyPath, identity);
1354
+ try {
1355
+ result = await client.cancelRun(command.runId, body, key);
1356
+ if (!command.idempotencyKey)
1357
+ await (0, idempotency_1.finishIdempotencyAttempt)(idempotencyPath, identity);
1358
+ }
1359
+ catch (error) {
1360
+ const transient = error instanceof client_1.CliError && (error.code === 'request_timeout' || error.code === 'network_error' || error.retryable === true || error.status >= 500);
1361
+ if (!command.idempotencyKey && !transient)
1362
+ await (0, idempotency_1.finishIdempotencyAttempt)(idempotencyPath, identity);
1363
+ throw error;
1364
+ }
1359
1365
  break;
1360
1366
  }
1361
1367
  case 'pack': {
1362
1368
  if (command.pack === 'sequences') {
1363
- await require('./commands/sequences').runSequencesCommand(client, command, stdout, runtime.sleep ?? wait);
1369
+ await require('./commands/sequences').runSequencesCommand(gatedContext(), command, stdout);
1364
1370
  }
1365
1371
  else {
1366
- await require('./commands/workflows').runWorkflowsCommand(client, command, stdout, runtime.sleep ?? wait);
1372
+ await require('./commands/workflows').runWorkflowsCommand(gatedContext(), command, stdout);
1367
1373
  }
1368
1374
  return;
1369
1375
  }
1370
1376
  default: return;
1371
1377
  }
1372
- if ((command.command === 'tools_run' || command.command === 'runs_resume') && command.open)
1378
+ if (command.command === 'tools_run' && command.open)
1373
1379
  openResult(result, runtime);
1374
1380
  if (command.command === 'packs_route' && !command.jsonOutput)
1375
1381
  printPackRoute(result, stdout);
1376
1382
  else
1377
1383
  print(result, stdout, Boolean(command.jsonOutput));
1378
1384
  if (process.exitCode !== runWait_1.RUN_WAIT_TIMEOUT_EXIT_CODE)
1379
- process.exitCode = exports.CLI_EXIT.SUCCESS;
1385
+ process.exitCode = errors_1.CLI_EXIT.SUCCESS;
1380
1386
  }
1381
1387
  catch (error) {
1382
1388
  const exitCode = exitCodeForError(error);