@trydreamstate/cli 1.3.2 → 1.3.4

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 (91) hide show
  1. package/README.md +36 -16
  2. package/agent-plugin/.claude-plugin/plugin.json +1 -1
  3. package/agent-plugin/.codex-plugin/plugin.json +1 -1
  4. package/agent-plugin/hooks/approve-readonly.sh +15 -0
  5. package/agent-plugin/hooks/hooks.json +17 -0
  6. package/agent-plugin/skills/crm/SKILL.md +2 -2
  7. package/agent-plugin/skills/diagnostics/SKILL.md +3 -1
  8. package/agent-plugin/skills/dreamstate/SKILL.md +15 -5
  9. package/agent-plugin/skills/first-win/SKILL.md +2 -2
  10. package/agent-plugin/skills/inbox-replies/SKILL.md +3 -1
  11. package/agent-plugin/skills/knowledge/SKILL.md +3 -5
  12. package/agent-plugin/skills/linkedin-posts/SKILL.md +3 -1
  13. package/agent-plugin/skills/linkedin-warm-signals/SKILL.md +2 -2
  14. package/agent-plugin/skills/list-building/SKILL.md +5 -10
  15. package/agent-plugin/skills/list-building/column-authoring.md +1 -1
  16. package/agent-plugin/skills/list-building/sourcing.md +2 -2
  17. package/agent-plugin/skills/outreach/SKILL.md +2 -2
  18. package/agent-plugin/skills/outreach/steps-and-timing.md +1 -1
  19. package/agent-plugin/skills/plays/SKILL.md +3 -3
  20. package/agent-plugin/skills/reddit-posts/SKILL.md +3 -1
  21. package/agent-plugin/skills/skill-authoring/SKILL.md +3 -1
  22. package/agent-plugin/skills/social/SKILL.md +4 -6
  23. package/agent-plugin/skills/visibility/SKILL.md +2 -6
  24. package/agent-plugin/skills/workflows/SKILL.md +3 -3
  25. package/agent-plugin/skills/workflows/lifecycle-and-enrollment.md +1 -1
  26. package/agent-plugin/skills/worksheet-tidy/SKILL.md +3 -1
  27. package/agent-plugin/skills/writing/SKILL.md +3 -5
  28. package/agent-plugin/skills/x-posts/SKILL.md +3 -1
  29. package/dist/agentGuidance.generated.json +35 -0
  30. package/dist/capabilityPosture.d.ts +12 -0
  31. package/dist/capabilityPosture.d.ts.map +1 -0
  32. package/dist/capabilityPosture.js +39 -0
  33. package/dist/client.d.ts +33 -2
  34. package/dist/client.d.ts.map +1 -1
  35. package/dist/client.js +155 -64
  36. package/dist/commandTable.d.ts +26 -0
  37. package/dist/commandTable.d.ts.map +1 -0
  38. package/dist/commandTable.js +54 -0
  39. package/dist/commands/sequences.d.ts +3 -2
  40. package/dist/commands/sequences.d.ts.map +1 -1
  41. package/dist/commands/sequences.js +17 -34
  42. package/dist/commands/shared.d.ts +4 -26
  43. package/dist/commands/shared.d.ts.map +1 -1
  44. package/dist/commands/shared.js +136 -158
  45. package/dist/commands/workflows.d.ts +3 -2
  46. package/dist/commands/workflows.d.ts.map +1 -1
  47. package/dist/commands/workflows.js +17 -39
  48. package/dist/errors.d.ts +74 -0
  49. package/dist/errors.d.ts.map +1 -0
  50. package/dist/errors.js +84 -0
  51. package/dist/gatedRun.d.ts +79 -0
  52. package/dist/gatedRun.d.ts.map +1 -0
  53. package/dist/gatedRun.js +346 -0
  54. package/dist/generated/packVerbs.generated.json +492 -0
  55. package/dist/generated/runContract.generated.d.ts +4 -0
  56. package/dist/generated/runContract.generated.d.ts.map +1 -0
  57. package/dist/generated/runContract.generated.js +7 -0
  58. package/dist/idempotency.d.ts +38 -0
  59. package/dist/idempotency.d.ts.map +1 -0
  60. package/dist/idempotency.js +106 -0
  61. package/dist/index.d.ts +22 -35
  62. package/dist/index.d.ts.map +1 -1
  63. package/dist/index.js +405 -378
  64. package/dist/registryManifest.d.ts +6 -0
  65. package/dist/registryManifest.d.ts.map +1 -0
  66. package/dist/registryManifest.js +66 -0
  67. package/dist/runContract.d.ts +10 -0
  68. package/dist/runContract.d.ts.map +1 -0
  69. package/dist/runContract.js +13 -0
  70. package/dist/runWait.d.ts +2 -2
  71. package/dist/runWait.d.ts.map +1 -1
  72. package/dist/runWait.js +3 -14
  73. package/dist/schemaCoercion.d.ts +20 -0
  74. package/dist/schemaCoercion.d.ts.map +1 -0
  75. package/dist/schemaCoercion.js +123 -0
  76. package/dist/skillInstaller.d.ts +40 -0
  77. package/dist/skillInstaller.d.ts.map +1 -1
  78. package/dist/skillInstaller.js +108 -9
  79. package/dist/tableFilePreview.d.ts +14 -0
  80. package/dist/tableFilePreview.d.ts.map +1 -1
  81. package/dist/tableFilePreview.js +19 -8
  82. package/dist/tableFileReader.d.ts +26 -0
  83. package/dist/tableFileReader.d.ts.map +1 -0
  84. package/dist/tableFileReader.js +108 -0
  85. package/dist/tablesImport.d.ts +57 -0
  86. package/dist/tablesImport.d.ts.map +1 -0
  87. package/dist/tablesImport.js +263 -0
  88. package/dist/turnCache.d.ts +14 -1
  89. package/dist/turnCache.d.ts.map +1 -1
  90. package/dist/turnCache.js +25 -8
  91. package/package.json +6 -3
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.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,19 @@ 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
+ const tablesImport_1 = require("./tablesImport");
31
+ const commandTable_1 = require("./commandTable");
26
32
  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
- const TOOLS_RUN_MODES = ['smoke_test', 'dry_run', 'live'];
33
+ const runContract_1 = require("./runContract");
34
+ 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
35
  const USAGE = `Dreamstate CLI
30
36
 
31
37
  Usage:
@@ -53,26 +59,22 @@ Usage:
53
59
  dreamstate commands search <term> [--json-output|--json]
54
60
  dreamstate tools search --goal <goal> [--terms <csv>] [--limit <n>] [--toolset <pack_id>] [--json-output|--json]
55
61
  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]
62
+ 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>] [--max-live-sends <n>] [--quote-hash <sha256:...>] [--idempotency-key <key>] [--wait-timeout <seconds>] [--no-wait] [--open] [--json-output]
57
63
  dreamstate sequences <verb> [args] [flags]
58
64
  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]
65
+ 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
66
  dreamstate runs list [--limit <n>] [--cursor <cursor>] [--json <query>] [--json-output|--json]
61
67
  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
68
  dreamstate runs events <run_id> [--json-output|--json]
64
69
  dreamstate runs cancel <run_id> [--state-version <n>] [--reason <text>] [--json <body>] [--idempotency-key <key>] [--json-output|--json]
65
70
 
66
71
  All catalog discovery reads live API records. Set DREAMSTATE_API_URL and DREAMSTATE_API_KEY.
67
72
  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}.
73
+ 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 repair.kind ask_user, 8 CLI update required, 9 refused, 10 transport, 11 timeout.
68
74
  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
75
  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
76
  logout deletes the stored credentials file. --agents takes space-separated, comma-separated or repeated values.
71
77
  `;
72
- class CliUsageError extends Error {
73
- constructor(message) { super(message); this.name = 'CliUsageError'; }
74
- }
75
- exports.CliUsageError = CliUsageError;
76
78
  function outputOption(values) {
77
79
  return values['json-output'] ? { jsonOutput: true } : {};
78
80
  }
@@ -84,35 +86,41 @@ function parseJsonObject(raw, flag = '--json') {
84
86
  return value;
85
87
  }
86
88
  catch (error) {
87
- throw new CliUsageError(`${flag} ${error instanceof Error ? error.message : 'is invalid'}`);
89
+ throw new errors_1.CliUsageError(`${flag} ${error instanceof Error ? error.message : 'is invalid'}`);
88
90
  }
89
91
  }
90
92
  function requireOne(positionals, usage) {
91
93
  if (positionals.length !== 2 || !positionals[1])
92
- throw new CliUsageError(`Usage: ${usage}`);
94
+ throw new errors_1.CliUsageError(`Usage: ${usage}`);
93
95
  return positionals[1];
94
96
  }
95
97
  function csv(raw, flag = '--terms') {
96
98
  const values = raw.split(',').map((value) => value.trim()).filter(Boolean);
97
99
  if (!values.length)
98
- throw new CliUsageError(`${flag} must contain at least one value`);
100
+ throw new errors_1.CliUsageError(`${flag} must contain at least one value`);
99
101
  return [...new Set(values)];
100
102
  }
101
103
  function requireToolsRunMode(body) {
102
- if (TOOLS_RUN_MODES.includes(body.mode))
104
+ if (runContract_1.RUN_MODES.includes(body.mode))
103
105
  return;
104
- throw new CliUsageError('mode must be one of: smoke_test, dry_run, live.');
106
+ throw new errors_1.CliUsageError('mode must be one of: smoke_test, dry_run, live.');
105
107
  }
106
108
  function nonNegativeNumber(raw, flag) {
107
109
  const value = Number(raw);
108
110
  if (!Number.isFinite(value) || value < 0)
109
- throw new CliUsageError(`${flag} must be a non-negative number`);
111
+ throw new errors_1.CliUsageError(`${flag} must be a non-negative number`);
110
112
  return value;
111
113
  }
112
114
  function nonNegativeInteger(raw, flag) {
113
115
  const value = Number(raw);
114
116
  if (!Number.isSafeInteger(value) || value < 0)
115
- throw new CliUsageError(`${flag} must be a non-negative integer`);
117
+ throw new errors_1.CliUsageError(`${flag} must be a non-negative integer`);
118
+ return value;
119
+ }
120
+ function positiveInteger(raw, flag) {
121
+ const value = Number(raw);
122
+ if (!Number.isSafeInteger(value) || value < 1)
123
+ throw new errors_1.CliUsageError(`${flag} must be an integer greater than or equal to 1`);
116
124
  return value;
117
125
  }
118
126
  // runs.get returns { data: { run: { state_version } } }. Cancel posts
@@ -141,7 +149,7 @@ function parsePacks(argv) {
141
149
  return { command: 'packs_get', packId: requireOne(positionals, 'dreamstate packs get <pack_id>'), ...outputOption(values) };
142
150
  if (positionals.length === 1 && positionals[0] === 'route' && values.goal?.trim())
143
151
  return { command: 'packs_route', goal: values.goal.trim(), ...outputOption(values) };
144
- throw new CliUsageError('Usage: dreamstate packs <list|get|route>');
152
+ throw new errors_1.CliUsageError('Usage: dreamstate packs <list|get|route>');
145
153
  }
146
154
  /** Rewrites `--agents a b c` into `--agents=a --agents=b --agents=c` so parseArgs sees every value. */
147
155
  function spaceSeparatedAgents(argv) {
@@ -172,7 +180,7 @@ function parseSkills(argv) {
172
180
  const requested = [...new Set(values.agents.flatMap((raw) => csv(raw, '--agents')))];
173
181
  const invalid = requested.filter((agent) => !skillInstaller_1.SKILL_INSTALL_AGENTS.includes(agent));
174
182
  if (invalid.length)
175
- throw new CliUsageError(`--agents values must be one of: ${skillInstaller_1.SKILL_INSTALL_AGENTS.join(', ')} (got: ${invalid.join(', ')})`);
183
+ throw new errors_1.CliUsageError(`--agents values must be one of: ${skillInstaller_1.SKILL_INSTALL_AGENTS.join(', ')} (got: ${invalid.join(', ')})`);
176
184
  return requested;
177
185
  };
178
186
  if (positionals.length === 1 && positionals[0] === 'list') {
@@ -195,14 +203,14 @@ function parseSkills(argv) {
195
203
  return { command: 'skills_install', skillId: values.skill?.trim() || '*', agents: agents(), ...(values.project ? { project: true } : {}), ...(values.copy ? { copy: true } : {}), ...outputOption(values) };
196
204
  if (positionals.length === 1 && positionals[0] === 'doctor')
197
205
  return { command: 'skills_doctor', agents: agents(), ...(values.project ? { project: true } : {}), ...outputOption(values) };
198
- throw new CliUsageError('Usage: dreamstate skills <list|get|search|install|doctor>');
206
+ throw new errors_1.CliUsageError('Usage: dreamstate skills <list|get|search|install|doctor>');
199
207
  }
200
208
  function parseCommands(argv) {
201
209
  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
210
  if (values.help)
203
211
  return { command: 'help' };
204
212
  if (positionals[0] !== 'search' || positionals.length < 2)
205
- throw new CliUsageError('Usage: dreamstate commands search <term>');
213
+ throw new errors_1.CliUsageError('Usage: dreamstate commands search <term>');
206
214
  return { command: 'commands_search', term: positionals.slice(1).join(' '), ...outputOption(values) };
207
215
  }
208
216
  /**
@@ -210,10 +218,8 @@ function parseCommands(argv) {
210
218
  * are mutually exclusive ways of saying the same thing, and disagreeing ones are a
211
219
  * usage error rather than a silent winner.
212
220
  *
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.
221
+ * --approved is deliberately NOT here: it is not a mode. It rides on a live run
222
+ * as the caller's assertion that it asked a person (see parseTools).
217
223
  */
218
224
  function toolsRunMode(values) {
219
225
  const chosen = new Set();
@@ -223,13 +229,13 @@ function toolsRunMode(values) {
223
229
  chosen.add('live');
224
230
  if (values.mode !== undefined) {
225
231
  const mode = values.mode.trim();
226
- if (!TOOLS_RUN_MODES.includes(mode)) {
227
- throw new CliUsageError(`--mode must be one of: ${TOOLS_RUN_MODES.join(', ')}`);
232
+ if (!runContract_1.RUN_MODES.includes(mode)) {
233
+ throw new errors_1.CliUsageError(`--mode must be one of: ${runContract_1.RUN_MODES.join(', ')}`);
228
234
  }
229
235
  chosen.add(mode);
230
236
  }
231
237
  if (chosen.size > 1) {
232
- throw new CliUsageError(`Conflicting execution modes requested: ${[...chosen].sort().join(', ')}. Pass exactly one of --live, --dry-run, --mode.`);
238
+ throw new errors_1.CliUsageError(`Conflicting execution modes requested: ${[...chosen].sort().join(', ')}. Pass exactly one of --live, --dry-run, --mode.`);
233
239
  }
234
240
  return [...chosen][0];
235
241
  }
@@ -239,8 +245,9 @@ function parseTools(argv) {
239
245
  limit: { type: 'string' }, toolset: { type: 'string' },
240
246
  'idempotency-key': { type: 'string' }, 'json-output': { type: 'boolean' }, open: { type: 'boolean' },
241
247
  'dry-run': { type: 'boolean' }, live: { type: 'boolean' }, mode: { type: 'string' },
242
- 'agent-session-id': { type: 'string' }, approved: { type: 'boolean' }, 'approved-effect-unknown': { type: 'boolean' },
243
- 'max-credits': { type: 'string' }, 'turn-token': { type: 'string' }, 'wait-timeout': { type: 'string' }, 'no-wait': { type: 'boolean' }, help: { type: 'boolean', short: 'h' },
248
+ approved: { type: 'boolean' }, 'approved-effect-unknown': { type: 'boolean' },
249
+ 'quote-hash': { type: 'string' }, 'approval-id': { type: 'string' },
250
+ 'max-credits': { type: 'string' }, 'max-live-sends': { type: 'string' }, 'turn-token': { type: 'string' }, 'wait-timeout': { type: 'string' }, 'no-wait': { type: 'boolean' }, help: { type: 'boolean', short: 'h' },
244
251
  }, allowPositionals: true, strict: true });
245
252
  if (values.help)
246
253
  return { command: 'help' };
@@ -256,7 +263,7 @@ function parseTools(argv) {
256
263
  if (values.toolset !== undefined)
257
264
  query.toolset = values.toolset;
258
265
  if (typeof query.goal !== 'string' && !Array.isArray(query.query_terms))
259
- throw new CliUsageError('tools search requires --goal, --terms, or --json query fields');
266
+ throw new errors_1.CliUsageError('tools search requires --goal, --terms, or --json query fields');
260
267
  return { command: 'tools_search', query, ...output };
261
268
  }
262
269
  if (positionals[0] === 'get')
@@ -264,11 +271,11 @@ function parseTools(argv) {
264
271
  if (positionals[0] === 'run') {
265
272
  const capabilityId = requireOne(positionals, 'dreamstate tools run <capability_id> [--params <object>|--json <body>]');
266
273
  if (values.json && values.params)
267
- throw new CliUsageError('Use either --params for tool parameters or --json for the complete request, not both.');
274
+ throw new errors_1.CliUsageError('Use either --params for tool parameters or --json for the complete request, not both.');
268
275
  if (!values.json && !values.params)
269
- throw new CliUsageError('tools run requires --params <object> or --json <body>; fetch live schema first with tools get');
276
+ throw new errors_1.CliUsageError('tools run requires --params <object> or --json <body>; fetch live schema first with tools get');
270
277
  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)
278
+ 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
279
  ? json
273
280
  : {};
274
281
  if (values.params)
@@ -277,37 +284,75 @@ function parseTools(argv) {
277
284
  body.params = json;
278
285
  // The server never defaults a mode, so a mutating run with none is refused
279
286
  // 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
287
  const chosenMode = toolsRunMode(values);
283
288
  if (chosenMode)
284
289
  body.mode = chosenMode;
285
- if (values['agent-session-id'])
286
- body.agent_session_id = values['agent-session-id'];
290
+ // agent_session_id selects the in-app Architect surface and the server refuses it for every
291
+ // other caller (an API key may name a session only while an Architect turn is running on it,
292
+ // which a CLI user never has). There is no flag for it, and a full --json body must not smuggle it.
293
+ if (body.agent_session_id !== undefined) {
294
+ 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.');
295
+ }
287
296
  if (values['approved-effect-unknown'])
288
297
  body.approved_effect_unknown = true;
289
298
  if (values['max-credits'] !== undefined)
290
299
  body.max_credits = nonNegativeNumber(values['max-credits'], '--max-credits');
300
+ if (body.max_live_sends !== undefined) {
301
+ throw new errors_1.CliUsageError('max_live_sends is a capability parameter. Put it in params.max_live_sends or pass --max-live-sends.');
302
+ }
303
+ if (values['max-live-sends'] !== undefined) {
304
+ const params = body.params;
305
+ if (!params || typeof params !== 'object' || Array.isArray(params))
306
+ throw new errors_1.CliUsageError('tools run params must be an object');
307
+ params.max_live_sends = positiveInteger(values['max-live-sends'], '--max-live-sends');
308
+ }
309
+ if (values['quote-hash'] !== undefined) {
310
+ if (!runContract_1.QUOTE_HASH_PATTERN.test(values['quote-hash']))
311
+ throw new errors_1.CliUsageError('--quote-hash must be the sha256:<64 hex> quote_hash a dry run returned');
312
+ body.quote_hash = values['quote-hash'];
313
+ }
314
+ // --approved is the caller's assertion that it showed a person the dry run's
315
+ // rows and cost and they agreed. A paid run above the size or dollar
316
+ // threshold is refused without it. It only means something on a live run,
317
+ // so any other mode never sends it.
318
+ if (values.approved && body.mode === 'live')
319
+ body.approved = true;
320
+ // --approval-id names a recorded approval (a uuid the approvals capabilities returned)
321
+ // the door should honour for this run. It is a door input, never a param.
322
+ if (values['approval-id'] !== undefined) {
323
+ if (!UUID_PATTERN.test(values['approval-id']))
324
+ throw new errors_1.CliUsageError('--approval-id must be the uuid of a recorded approval');
325
+ body.approval_id = values['approval-id'];
326
+ }
291
327
  // No client-side mode requirement: the server owns that refusal and answers
292
328
  // mode_required with a repair, and the parse grammar is expected to accept a
293
329
  // run without one. What was missing was any way to ASK for live.
294
330
  if (body.mode !== undefined)
295
331
  requireToolsRunMode(body);
296
332
  const waitTimeout = values['wait-timeout'] === undefined ? 120 : nonNegativeNumber(values['wait-timeout'], '--wait-timeout');
333
+ if (values['idempotency-key'] !== undefined && values['idempotency-key'].length > runContract_1.IDEMPOTENCY_KEY_MAX_LENGTH) {
334
+ throw new errors_1.CliUsageError(`--idempotency-key must be at most ${runContract_1.IDEMPOTENCY_KEY_MAX_LENGTH} characters`);
335
+ }
297
336
  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
337
  }
299
- throw new CliUsageError('Usage: dreamstate tools <search|get|run>');
338
+ throw new errors_1.CliUsageError('Usage: dreamstate tools <search|get|run>');
300
339
  }
301
340
  function parseTables(argv) {
341
+ // Servers before this release put --background on the import command they hand out. It
342
+ // never did anything (the import always ran to completion in this process), so 1.3.3
343
+ // dropped it; say so plainly instead of the generic "unknown option" error.
344
+ if (argv.includes('--background')) {
345
+ 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.');
346
+ }
302
347
  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' },
348
+ 'table-id': { type: 'string' }, file: { type: 'string' }, format: { type: 'string' },
304
349
  preview: { type: 'boolean' }, 'upsert-key': { type: 'string' }, 'row-kind': { type: 'string' }, 'keep-unmapped-columns': { type: 'string' },
305
350
  'json-output': { type: 'boolean' }, help: { type: 'boolean', short: 'h' },
306
351
  }, allowPositionals: true, strict: true });
307
352
  if (values.help)
308
353
  return { command: 'help' };
309
354
  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]');
355
+ 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
356
  }
312
357
  // --format is an override, not a requirement. Requiring it made the caller restate
313
358
  // what the file path already says, and an omitted flag produced a usage error rather
@@ -315,16 +360,16 @@ function parseTables(argv) {
315
360
  // the kind from the file it receives.
316
361
  const format = values.format;
317
362
  if (format !== undefined && !['csv', 'json', 'jsonl', 'xlsx'].includes(format)) {
318
- throw new CliUsageError('--format must be one of csv, json, jsonl, xlsx');
363
+ throw new errors_1.CliUsageError('--format must be one of csv, json, jsonl, xlsx');
319
364
  }
320
365
  const upsertKey = values['upsert-key']?.trim();
321
366
  const rowKind = values['row-kind'];
322
367
  if (rowKind !== undefined && rowKind !== 'person' && rowKind !== 'company') {
323
- throw new CliUsageError('--row-kind must be person or company');
368
+ throw new errors_1.CliUsageError('--row-kind must be person or company');
324
369
  }
325
370
  const keepUnmappedColumns = values['keep-unmapped-columns'];
326
371
  if (keepUnmappedColumns !== undefined && keepUnmappedColumns !== 'true' && keepUnmappedColumns !== 'false') {
327
- throw new CliUsageError('--keep-unmapped-columns must be true or false');
372
+ throw new errors_1.CliUsageError('--keep-unmapped-columns must be true or false');
328
373
  }
329
374
  return {
330
375
  command: 'tables_import',
@@ -335,14 +380,13 @@ function parseTables(argv) {
335
380
  ...(upsertKey ? { upsertKey } : {}),
336
381
  ...(rowKind ? { rowKind } : {}),
337
382
  ...(keepUnmappedColumns === 'false' ? { keepUnmappedColumns: false } : {}),
338
- background: Boolean(values.background),
339
383
  ...outputOption(values),
340
384
  };
341
385
  }
342
386
  function runsListLimit(raw) {
343
387
  const value = nonNegativeInteger(raw, '--limit');
344
388
  if (value < 1 || value > 500)
345
- throw new CliUsageError('--limit must be an integer from 1 to 500');
389
+ throw new errors_1.CliUsageError('--limit must be an integer from 1 to 500');
346
390
  return value;
347
391
  }
348
392
  function parseRuns(argv) {
@@ -357,30 +401,28 @@ function parseRuns(argv) {
357
401
  if (values.cursor !== undefined) {
358
402
  const cursor = values.cursor.trim();
359
403
  if (!cursor)
360
- throw new CliUsageError('--cursor must be a non-empty string');
404
+ throw new errors_1.CliUsageError('--cursor must be a non-empty string');
361
405
  query.cursor = cursor;
362
406
  }
363
407
  return { command: 'runs_list', query, ...output };
364
408
  }
365
409
  if (values.limit !== undefined || values.cursor !== undefined) {
366
- throw new CliUsageError('--limit and --cursor are only valid on runs list');
410
+ throw new errors_1.CliUsageError('--limit and --cursor are only valid on runs list');
411
+ }
412
+ if (positionals[0] === 'resume') {
413
+ // The server retired generic run resume (verifyExecutionDoorCollapse.ts blocks the
414
+ // route), so there is nothing to call. Say where the replacement is.
415
+ 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
416
  }
368
417
  const runId = requireOne(positionals, `dreamstate runs ${positionals[0] ?? '<get|events|cancel>'} <run_id>`);
369
418
  if (positionals[0] === 'get')
370
419
  return { command: 'runs_get', runId, ...output };
371
420
  if (positionals[0] === 'events')
372
421
  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
422
  if (positionals[0] === 'cancel') {
381
423
  const idempotency = values['idempotency-key'] ? { idempotencyKey: values['idempotency-key'] } : {};
382
424
  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.');
425
+ throw new errors_1.CliUsageError('Use either --json <body> or --state-version and --reason with runs cancel, not both.');
384
426
  }
385
427
  if (values.json)
386
428
  return { command: 'runs_cancel', runId, body: parseJsonObject(values.json), ...idempotency, ...output };
@@ -393,7 +435,13 @@ function parseRuns(argv) {
393
435
  ...output,
394
436
  };
395
437
  }
396
- throw new CliUsageError('Usage: dreamstate runs <get|events|cancel> <run_id>');
438
+ throw new errors_1.CliUsageError('Usage: dreamstate runs <list|get|events|cancel> <run_id>');
439
+ }
440
+ function sequencesCommands() {
441
+ return require('./commands/sequences');
442
+ }
443
+ function workflowsCommands() {
444
+ return require('./commands/workflows');
397
445
  }
398
446
  function normalizeJsonOutputAlias(argv) {
399
447
  return argv.map((argument, index) => argument === '--json' && (index === argv.length - 1 || argv[index + 1]?.startsWith('-'))
@@ -412,9 +460,9 @@ function parseCliArgs(argv) {
412
460
  if (values.help)
413
461
  return { command: 'help' };
414
462
  if (positionals.length)
415
- throw new CliUsageError('Usage: dreamstate login [--token <ds_user_...|ds_live_...>]');
463
+ throw new errors_1.CliUsageError('Usage: dreamstate login [--token <ds_user_...|ds_live_...>]');
416
464
  if (values.token !== undefined && values['api-key-file'])
417
- throw new CliUsageError('Use either --token or --api-key-file, not both.');
465
+ throw new errors_1.CliUsageError('Use either --token or --api-key-file, not both.');
418
466
  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
467
  }
420
468
  if (group === 'install') {
@@ -422,11 +470,11 @@ function parseCliArgs(argv) {
422
470
  if (values.help)
423
471
  return { command: 'help' };
424
472
  if (positionals.length)
425
- throw new CliUsageError('Usage: dreamstate install [--agents <claude codex grok>]');
473
+ throw new errors_1.CliUsageError('Usage: dreamstate install [--agents <claude codex grok>]');
426
474
  const requested = values.agents ? [...new Set(values.agents.flatMap((raw) => csv(raw, '--agents')))] : [...pluginInstaller_1.PLUGIN_AGENTS];
427
475
  const invalid = requested.filter((agent) => !pluginInstaller_1.PLUGIN_AGENTS.includes(agent));
428
476
  if (invalid.length)
429
- throw new CliUsageError(`--agents values must be one of: ${pluginInstaller_1.PLUGIN_AGENTS.join(', ')} (got: ${invalid.join(', ')})`);
477
+ throw new errors_1.CliUsageError(`--agents values must be one of: ${pluginInstaller_1.PLUGIN_AGENTS.join(', ')} (got: ${invalid.join(', ')})`);
430
478
  return { command: 'install', agents: requested, ...outputOption(values) };
431
479
  }
432
480
  if (group === 'logout') {
@@ -434,14 +482,14 @@ function parseCliArgs(argv) {
434
482
  if (values.help)
435
483
  return { command: 'help' };
436
484
  if (positionals.length)
437
- throw new CliUsageError('Usage: dreamstate logout');
485
+ throw new errors_1.CliUsageError('Usage: dreamstate logout');
438
486
  return { command: 'logout', ...outputOption(values) };
439
487
  }
440
488
  if (group === 'status') {
441
489
  const { values, positionals } = (0, node_util_1.parseArgs)({ args: rest, options: { 'json-output': { type: 'boolean' } }, allowPositionals: true, strict: true });
442
490
  if (!positionals.length)
443
491
  return { command: 'status', ...outputOption(values) };
444
- throw new CliUsageError('Usage: dreamstate status [--json-output]');
492
+ throw new errors_1.CliUsageError('Usage: dreamstate status [--json-output]');
445
493
  }
446
494
  if (group === 'profiles') {
447
495
  const { values, positionals } = (0, node_util_1.parseArgs)({ args: rest, options: { 'json-output': { type: 'boolean' } }, allowPositionals: true, strict: true });
@@ -456,13 +504,13 @@ function parseCliArgs(argv) {
456
504
  return { command: 'profiles_remove', name: (0, config_1.validateProfileName)(positionals[1]), ...output };
457
505
  if (positionals.length === 2 && positionals[0] === 'env')
458
506
  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>>');
507
+ throw new errors_1.CliUsageError('Usage: dreamstate profiles <list|use <name>|current|remove <name>|env <name>>');
460
508
  }
461
509
  if (group === 'whoami') {
462
510
  const { values, positionals } = (0, node_util_1.parseArgs)({ args: rest, options: { 'json-output': { type: 'boolean' } }, allowPositionals: true, strict: true });
463
511
  if (positionals.length === 0)
464
512
  return { command: 'whoami', ...outputOption(values) };
465
- throw new CliUsageError('Usage: dreamstate whoami [--json-output]');
513
+ throw new errors_1.CliUsageError('Usage: dreamstate whoami [--json-output]');
466
514
  }
467
515
  if (group === 'workspaces') {
468
516
  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 +520,7 @@ function parseCliArgs(argv) {
472
520
  return { command: 'workspaces_list', ...outputOption(values) };
473
521
  if (positionals.length === 2 && positionals[0] === 'use' && positionals[1])
474
522
  return { command: 'workspaces_use', workspace: positionals[1], ...outputOption(values) };
475
- throw new CliUsageError('Usage: dreamstate workspaces <list|use <id|slug>>');
523
+ throw new errors_1.CliUsageError('Usage: dreamstate workspaces <list|use <id|slug>>');
476
524
  }
477
525
  if (group === 'auth') {
478
526
  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 +528,7 @@ function parseCliArgs(argv) {
480
528
  return { command: 'login', ...(values['api-key-file'] ? { apiKeyFile: values['api-key-file'] } : {}), ...(values['no-skills'] ? { noSkills: true } : {}), ...outputOption(values) };
481
529
  if (positionals.length === 1 && positionals[0] === 'whoami')
482
530
  return { command: 'whoami', ...outputOption(values) };
483
- throw new CliUsageError('Usage: dreamstate auth <login|whoami>');
531
+ throw new errors_1.CliUsageError('Usage: dreamstate auth <login|whoami>');
484
532
  }
485
533
  if (group === 'packs')
486
534
  return parsePacks(rest);
@@ -495,13 +543,17 @@ function parseCliArgs(argv) {
495
543
  if (group === 'runs')
496
544
  return parseRuns(rest);
497
545
  if (group === 'sequences')
498
- return require('./commands/sequences').parseSequencesCommand(rest);
546
+ return sequencesCommands().parseSequencesCommand(rest);
499
547
  if (group === 'workflows')
500
- return require('./commands/workflows').parseWorkflowsCommand(rest);
501
- throw new CliUsageError(`Unknown command: ${group}`);
548
+ return workflowsCommands().parseWorkflowsCommand(rest);
549
+ throw new errors_1.CliUsageError(`Unknown command: ${group}`);
502
550
  }
503
551
  function print(value, stdout, compact) {
504
- stdout.write(`${JSON.stringify(value, (key, nested) => key === 'tool_turn_token' ? undefined : nested, compact ? 0 : 2)}\n`);
552
+ // Printed exactly as the server returned it. tool_turn_token used to be stripped
553
+ // here, which made --turn-token impossible to use: a caller could never read the
554
+ // token from `tools get` to pass back. The token is a short-lived fetch fence tied
555
+ // to the caller's own session, not a credential, and MCP and REST return it too.
556
+ stdout.write(`${JSON.stringify(value, null, compact ? 0 : 2)}\n`);
505
557
  }
506
558
  function printPackRoute(value, stdout) {
507
559
  const data = value && typeof value === 'object' && !Array.isArray(value)
@@ -530,103 +582,32 @@ function printPackRoute(value, stdout) {
530
582
  stdout.write(`${packId}${typeof reason === 'string' ? `: ${reason}` : ''}\n`);
531
583
  }
532
584
  }
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
585
  /**
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.
586
+ * The refusal envelope, in the shared shape { ok:false, error, meta }. The server's
587
+ * own message, retry decision, repair, details and meta (trace_id, server_version,
588
+ * minimum_cli_version, release_sha) are forwarded unchanged; retry_after and
589
+ * idempotency_key are request receipts, so they stay in meta. Process exit status
590
+ * remains process-level state: ToolEnvelopeMeta intentionally has no exit_code.
594
591
  */
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
592
  function refusalEnvelope(error, exitCode) {
616
593
  const cliError = error instanceof client_1.CliError ? error : null;
617
594
  return {
618
595
  ok: false,
619
- executed: false,
620
596
  error: {
621
- code: code(error) ?? 'invalid_request',
597
+ code: (0, gatedRun_1.code)(error) ?? 'invalid_request',
622
598
  message: error instanceof Error ? error.message : String(error),
623
- ...(cliError?.details === undefined ? {} : { details: cliError.details }),
599
+ ...(cliError?.retryable === undefined ? {} : { retryable: cliError.retryable }),
624
600
  // The client keeps the server's retry decision and repair; dropping them
625
601
  // here left CLI automation without the recovery Architect and MCP get.
626
- ...(cliError?.retryable === undefined ? {} : { retryable: cliError.retryable }),
627
602
  ...(cliError?.repair === undefined ? {} : { repair: cliError.repair }),
603
+ ...(cliError?.details === undefined ? {} : { details: cliError.details }),
604
+ },
605
+ meta: {
606
+ ...(cliError?.meta ?? {}),
607
+ ...(cliError?.traceId !== undefined && cliError.meta?.trace_id === undefined ? { trace_id: cliError.traceId } : {}),
608
+ ...(cliError?.retryAfter !== undefined ? { retry_after: cliError.retryAfter } : {}),
609
+ ...(cliError?.idempotencyKey !== undefined ? { idempotency_key: cliError.idempotencyKey } : {}),
628
610
  },
629
- meta: { exit_code: exitCode },
630
611
  };
631
612
  }
632
613
  function openWebUrl(rawUrl, launch = node_child_process_1.spawn) {
@@ -687,7 +668,7 @@ function discoveredCapabilityId(value) {
687
668
  .filter((candidate) => typeof candidate === 'string' && candidate.length > 0);
688
669
  const ids = [...new Set(candidates)];
689
670
  if (ids.length !== 1)
690
- throw new CliUsageError('Local file import discovery must return exactly one capability.');
671
+ throw new errors_1.CliUsageError('Local file import discovery must return exactly one capability.');
691
672
  return ids[0];
692
673
  }
693
674
  function hostedSkill(value) {
@@ -705,31 +686,85 @@ function hostedSkill(value) {
705
686
  const documents = Array.isArray(entry.documents)
706
687
  ? entry.documents.filter((document) => Boolean(document) && typeof document === 'object' && typeof document.name === 'string' && typeof document.content === 'string')
707
688
  : undefined;
708
- return { id: entry.id, name: entry.name, description: entry.description, body: entry.body, frontmatter, documents };
689
+ // The server lists a skill's supporting pages beside the skill (data.files), each read
690
+ // with `skills get <id> --file <path>`; they are not inline in the skill record.
691
+ const files = Array.isArray(data.files)
692
+ ? data.files
693
+ .filter((file) => Boolean(file) && typeof file === 'object' && typeof file.path === 'string')
694
+ .map((file) => ({ path: file.path, ...(typeof file.revision_number === 'number' ? { revision_number: file.revision_number } : {}) }))
695
+ : undefined;
696
+ return {
697
+ id: entry.id, name: entry.name, description: entry.description, body: entry.body, frontmatter, documents,
698
+ ...(typeof entry.revision_number === 'number' ? { revision_number: entry.revision_number } : {}),
699
+ ...(files ? { files } : {}),
700
+ };
701
+ }
702
+ const SAFE_SKILL_ID = /^[a-z0-9][a-z0-9_-]*$/i;
703
+ /**
704
+ * One hosted skill with its supporting pages read in. Reads are one at a time because
705
+ * the workspace's private vault admits two reads at once, and fifteen parallel reads
706
+ * failed in production.
707
+ */
708
+ async function loadHostedSkill(client, id, withPages) {
709
+ if (!SAFE_SKILL_ID.test(id))
710
+ throw new errors_1.CliUsageError('Dreamstate API returned an invalid skill id.');
711
+ const skill = hostedSkill(await client.getSkill(id));
712
+ if (!SAFE_SKILL_ID.test(skill.id))
713
+ throw new errors_1.CliUsageError('Dreamstate API returned an invalid skill id.');
714
+ if (skill.id !== id)
715
+ throw new client_1.CliError('Dreamstate API returned a mismatched skill id.', 'invalid_response', 200);
716
+ if (!withPages || !skill.files?.length)
717
+ return skill;
718
+ const documents = [];
719
+ for (const file of skill.files) {
720
+ const page = hostedSkill(await client.getSkill(id, { file: file.path }));
721
+ documents.push({ name: file.path, content: page.body });
722
+ }
723
+ return { ...skill, documents };
724
+ }
725
+ /** Ids of every skill the server lists. */
726
+ async function listedSkillIds(client) {
727
+ const listed = await client.listAllSkills();
728
+ const entries = listed && typeof listed === 'object' && listed.data?.skills;
729
+ const ids = Array.isArray(entries) ? entries.map((skill) => skill && typeof skill === 'object' ? skill.id : undefined).filter((id) => typeof id === 'string') : [];
730
+ if (ids.some((id) => !SAFE_SKILL_ID.test(id)))
731
+ throw new errors_1.CliUsageError('Dreamstate API returned an invalid skill id.');
732
+ return ids;
709
733
  }
710
734
  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;
735
+ const errorCode = (0, gatedRun_1.code)(error);
736
+ if (errorCode?.startsWith('ERR_PARSE_ARGS'))
737
+ return errors_1.CLI_EXIT.USAGE;
738
+ if (error instanceof errors_1.CliUsageError || error instanceof config_1.ApiKeyFileError || error instanceof config_1.ApiBaseUrlError || error instanceof config_1.MissingApiUrlError)
739
+ return errors_1.CLI_EXIT.USAGE;
715
740
  if (error instanceof config_1.MissingApiKeyError)
716
- return exports.CLI_EXIT.AUTH;
717
- if (code(error) === 'request_timeout')
718
- return exports.CLI_EXIT.TIMEOUT;
741
+ return errors_1.CLI_EXIT.AUTH;
742
+ // By code before by status: a gate refusal arrives as 403 or 409 and must still read as
743
+ // "a person has to decide", and an update-required refusal is 426.
744
+ if (errorCode === 'cli_update_required')
745
+ return errors_1.CLI_EXIT.UPDATE_REQUIRED;
746
+ // Local transport failures stamp repair.kind ask_user (read the target back,
747
+ // then ask a person). Their published exits win over that repair.
748
+ if (errorCode === 'request_timeout')
749
+ return errors_1.CLI_EXIT.TIMEOUT;
750
+ if (errorCode === 'network_error')
751
+ return errors_1.CLI_EXIT.TRANSPORT;
752
+ if ((0, errors_1.isLocalRefusalCode)(errorCode))
753
+ return errors_1.CLI_EXIT.REFUSED;
754
+ if ((0, errors_1.isApprovalRepair)(error))
755
+ return errors_1.CLI_EXIT.APPROVAL;
719
756
  const status = error instanceof client_1.CliError ? error.status : 0;
720
757
  if (status === 401 || status === 403)
721
- return exports.CLI_EXIT.AUTH;
758
+ return errors_1.CLI_EXIT.AUTH;
722
759
  if (status === 404)
723
- return exports.CLI_EXIT.NOT_FOUND;
760
+ return errors_1.CLI_EXIT.NOT_FOUND;
724
761
  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;
762
+ return errors_1.CLI_EXIT.RATE_LIMITED;
763
+ if (status >= 500 || errorCode === 'invalid_response')
764
+ return errors_1.CLI_EXIT.SERVER;
730
765
  if (status >= 400)
731
- return exports.CLI_EXIT.REFUSED;
732
- return exports.CLI_EXIT.UNCLASSIFIED;
766
+ return errors_1.CLI_EXIT.REFUSED;
767
+ return errors_1.CLI_EXIT.UNCLASSIFIED;
733
768
  }
734
769
  async function readLoginKey(command, runtime) {
735
770
  if (command.apiKeyFile)
@@ -747,7 +782,7 @@ async function readLoginKey(command, runtime) {
747
782
  }
748
783
  if (runtime.promptHidden)
749
784
  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.');
785
+ throw new errors_1.CliUsageError('No API key provided. Pipe it on stdin, set DREAMSTATE_API_KEY, or pass --api-key-file.');
751
786
  }
752
787
  function deviceState() {
753
788
  return (0, node_crypto_1.randomBytes)(32).toString('base64url');
@@ -767,14 +802,14 @@ function extractWorkspaceFlag(argv) {
767
802
  if (argument === '--workspace') {
768
803
  const value = argv[index + 1];
769
804
  if (value === undefined || value.startsWith('-') || !value.trim())
770
- throw new CliUsageError('--workspace requires a workspace id or slug.');
805
+ throw new errors_1.CliUsageError('--workspace requires a workspace id or slug.');
771
806
  workspace = value.trim();
772
807
  index += 1;
773
808
  }
774
809
  else if (argument.startsWith('--workspace=')) {
775
810
  workspace = argument.slice('--workspace='.length).trim();
776
811
  if (!workspace)
777
- throw new CliUsageError('--workspace requires a workspace id or slug.');
812
+ throw new errors_1.CliUsageError('--workspace requires a workspace id or slug.');
778
813
  }
779
814
  else {
780
815
  rest.push(argument);
@@ -786,25 +821,28 @@ function whoamiData(value) {
786
821
  const data = value && typeof value === 'object' ? value.data : undefined;
787
822
  return data && typeof data === 'object' ? data : {};
788
823
  }
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;
793
- const healthData = health && typeof health === 'object' ? health.data : null;
794
- 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)));
824
+ /**
825
+ * What `dreamstate status` reports. The release identity comes from the server's
826
+ * own answers (envelope meta and X-Dreamstate-* headers on every response), so
827
+ * skew is measured, not assumed: with a server that publishes nothing the skew is
828
+ * "unknown", never "compatible".
829
+ */
830
+ function statusData(whoami, health, info) {
831
+ // health.data.version is not the release. server_version comes only from envelope meta.
832
+ const serverVersion = info.server_version;
833
+ const skew = (0, client_1.describeVersionSkew)(info);
834
+ const compatible = skew !== 'cli_below_minimum';
801
835
  return {
802
836
  cli_version: client_1.CLI_VERSION,
803
- api_version: version,
804
- minimum_cli_version: minimum,
805
- minimum_cli_version_published: minimum !== null,
837
+ api_version: serverVersion,
838
+ server_version: serverVersion,
839
+ registry_hash: info.registry_hash,
840
+ release_sha: info.release_sha,
841
+ minimum_cli_version: info.minimum_cli_version,
842
+ minimum_cli_version_published: info.minimum_cli_version !== null,
843
+ skew,
806
844
  compatible,
807
- upgrade_hint: compatible ? null : 'Run: npm install -g @trydreamstate/cli@latest',
845
+ upgrade_hint: skew === 'cli_below_minimum' || skew === 'cli_behind_server' ? `Run: ${client_1.CLI_UPDATE_COMMAND}` : null,
808
846
  health,
809
847
  whoami: whoamiData(whoami),
810
848
  };
@@ -816,36 +854,31 @@ function activeWorkspace(data) {
816
854
  function wait(milliseconds) {
817
855
  return new Promise((resolve) => setTimeout(resolve, milliseconds));
818
856
  }
857
+ /**
858
+ * The one way the CLI installs skills: every skill from the hosted catalog (the same
859
+ * records MCP skills_get and `skills get` read), with its supporting pages, plus the
860
+ * router. `skills install`, `login` and the single-skill form all come through here, so
861
+ * there is no second source of skill text on this machine.
862
+ */
819
863
  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') : [];
864
+ const ids = options.only ?? await listedSkillIds(client);
823
865
  if (!ids.length)
824
866
  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
867
  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
- }
868
+ for (const id of ids)
869
+ skills.push(await loadHostedSkill(client, id, true));
836
870
  const packs = await client.listPacks();
837
871
  const whoami = await client.getWhoami();
838
872
  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)], {
873
+ const all = [...skills, (0, skillInstaller_1.routerSkill)(packs)];
874
+ // Stamped into every file so a stale install is visible against the live skills.
875
+ const installed = await (0, skillInstaller_1.installSkills)(all, {
843
876
  agents,
844
877
  homeDirectory: runtime.homeDirectory,
845
878
  project: Boolean(options.project),
846
879
  copy: Boolean(options.copy) || process.platform === 'win32',
847
880
  workspaceId,
848
- indexVersion,
881
+ indexVersion: (0, skillInstaller_1.skillIndexVersion)(all),
849
882
  });
850
883
  return { files: installed.files, skillIds: [...ids, 'router'] };
851
884
  }
@@ -930,12 +963,12 @@ async function runCli(argv = process.argv.slice(2), runtime = {}) {
930
963
  if (command.command === 'help' || command.command === 'pack_help') {
931
964
  const packHelp = command.command === 'pack_help' ? command : undefined;
932
965
  print({ ok: true, data: { usage: packHelp?.usage ?? USAGE }, meta: {} }, stdout, Boolean(packHelp?.jsonOutput));
933
- process.exitCode = exports.CLI_EXIT.SUCCESS;
966
+ process.exitCode = errors_1.CLI_EXIT.SUCCESS;
934
967
  return;
935
968
  }
936
969
  if (command.command === 'version') {
937
970
  print({ ok: true, data: { version: client_1.CLI_VERSION }, meta: {} }, stdout, false);
938
- process.exitCode = exports.CLI_EXIT.SUCCESS;
971
+ process.exitCode = errors_1.CLI_EXIT.SUCCESS;
939
972
  return;
940
973
  }
941
974
  if (command.command === 'install') {
@@ -948,7 +981,7 @@ async function runCli(argv = process.argv.slice(2), runtime = {}) {
948
981
  const removedSkills = done.includes('claude') ? await (0, pluginInstaller_1.removeClaudeSkillCopies)(runtime.homeDirectory) : [];
949
982
  stderr.write(`Dreamstate plugin installed for ${done.join(', ')}. Start a new session; the first Dreamstate tool call asks you to sign in.\n`);
950
983
  print({ ok: true, data: { ...installed, removed_skill_copies: removedSkills }, meta: {} }, stdout, Boolean(command.jsonOutput));
951
- process.exitCode = exports.CLI_EXIT.SUCCESS;
984
+ process.exitCode = errors_1.CLI_EXIT.SUCCESS;
952
985
  return;
953
986
  }
954
987
  if (command.command === 'logout') {
@@ -967,7 +1000,7 @@ async function runCli(argv = process.argv.slice(2), runtime = {}) {
967
1000
  const message = removed ? `Removed ${path}` : `Already logged out: no credentials at ${path}`;
968
1001
  stderr.write(`${message}\n`);
969
1002
  print({ ok: true, data: { credentials_path: path, removed }, meta: {} }, stdout, Boolean(command.jsonOutput));
970
- process.exitCode = exports.CLI_EXIT.SUCCESS;
1003
+ process.exitCode = errors_1.CLI_EXIT.SUCCESS;
971
1004
  return;
972
1005
  }
973
1006
  if (command.command === 'profiles_list' || command.command === 'profiles_current' || command.command === 'profiles_use' || command.command === 'profiles_remove' || command.command === 'profiles_env') {
@@ -992,20 +1025,20 @@ async function runCli(argv = process.argv.slice(2), runtime = {}) {
992
1025
  else {
993
1026
  const profiles = await (0, config_1.listCredentialProfiles)(path);
994
1027
  if (!profiles.some((profile) => profile.name === command.name))
995
- throw new CliUsageError(`Profile not found: ${command.name}.`);
1028
+ throw new errors_1.CliUsageError(`Profile not found: ${command.name}.`);
996
1029
  const result = { ok: true, data: { profile: command.name, exports: [`DREAMSTATE_PROFILE=${command.name}`] }, meta: {} };
997
1030
  if (command.jsonOutput)
998
1031
  print(result, stdout, true);
999
1032
  else
1000
1033
  stdout.write(`export DREAMSTATE_PROFILE=${command.name}\n`);
1001
1034
  }
1002
- process.exitCode = exports.CLI_EXIT.SUCCESS;
1035
+ process.exitCode = errors_1.CLI_EXIT.SUCCESS;
1003
1036
  return;
1004
1037
  }
1005
1038
  if (command.command === 'login' && command.token !== undefined) {
1006
1039
  const token = command.token.trim();
1007
1040
  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_.');
1041
+ throw new errors_1.CliUsageError('--token must be a Dreamstate session or API key starting with ds_user_ or ds_live_.');
1009
1042
  const environment = runtime.env ?? process.env;
1010
1043
  const baseUrl = (0, config_1.resolveApiBaseUrl)(environment);
1011
1044
  // Only an explicit flag or env picks the workspace here: a stored default belongs to the previous login.
@@ -1024,7 +1057,7 @@ async function runCli(argv = process.argv.slice(2), runtime = {}) {
1024
1057
  await (0, config_1.storeApiKey)(token, runtimeCredentialsPath(runtime));
1025
1058
  const skills = await installLoginSkills(client, runtime, Boolean(command.noSkills), stderr);
1026
1059
  print(loginResult(whoami, skills), stdout, Boolean(command.jsonOutput));
1027
- process.exitCode = exports.CLI_EXIT.SUCCESS;
1060
+ process.exitCode = errors_1.CLI_EXIT.SUCCESS;
1028
1061
  return;
1029
1062
  }
1030
1063
  if (command.command === 'status') {
@@ -1033,27 +1066,47 @@ async function runCli(argv = process.argv.slice(2), runtime = {}) {
1033
1066
  const apiKey = await (0, config_1.resolveApiKey)(environment, credentialFilePath);
1034
1067
  const workspace = await (0, config_1.resolveWorkspace)(extracted.workspace, environment, credentialFilePath);
1035
1068
  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;
1069
+ let whoami = null;
1070
+ let health = null;
1071
+ let refusedAsTooOld = null;
1072
+ try {
1073
+ [whoami, health] = await Promise.all([client.getStatusWhoami(), client.getHealth()]);
1074
+ }
1075
+ catch (error) {
1076
+ // A server that refuses this CLI as too old (426) is the clearest skew report there is:
1077
+ // print it as a status, with the update command, instead of only an error.
1078
+ if (!(error instanceof client_1.CliError) || error.code !== 'cli_update_required')
1079
+ throw error;
1080
+ refusedAsTooOld = error;
1081
+ }
1082
+ const info = client.serverInfo;
1083
+ const refusal = refusedAsTooOld?.details && typeof refusedAsTooOld.details === 'object' ? refusedAsTooOld.details : {};
1084
+ const data = statusData(whoami, health, {
1085
+ ...info,
1086
+ minimum_cli_version: info.minimum_cli_version ?? (typeof refusal.minimum_cli_version === 'string' ? refusal.minimum_cli_version : null),
1087
+ });
1088
+ print({ ok: true, data, meta: {} }, stdout, Boolean(command.jsonOutput));
1089
+ process.exitCode = data.skew === 'cli_below_minimum' ? errors_1.CLI_EXIT.UPDATE_REQUIRED : errors_1.CLI_EXIT.SUCCESS;
1090
+ if (process.exitCode === errors_1.CLI_EXIT.UPDATE_REQUIRED)
1091
+ 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
1092
  return;
1040
1093
  }
1041
1094
  if (command.command === 'login') {
1042
1095
  if (command.apiKeyFile || (runtime.env ?? process.env).DREAMSTATE_API_KEY || runtime.promptHidden) {
1043
1096
  const key = await readLoginKey(command, runtime);
1044
1097
  if (!(0, config_1.isDreamstateToken)(key))
1045
- throw new CliUsageError('Dreamstate API keys must start with ds_user_ or ds_live_.');
1098
+ throw new errors_1.CliUsageError('Dreamstate API keys must start with ds_user_ or ds_live_.');
1046
1099
  const path = command.profile
1047
1100
  ? await (0, config_1.storeCredentialProfile)(command.profile, key, runtimeCredentialsPath(runtime))
1048
1101
  : await (0, config_1.storeApiKey)(key, runtimeCredentialsPath(runtime));
1049
1102
  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
1103
  const skills = await installLoginSkills(client, runtime, Boolean(command.noSkills), stderr);
1051
1104
  print({ ok: true, data: { credentials_path: path, skills }, meta: {} }, stdout, Boolean(command.jsonOutput));
1052
- process.exitCode = exports.CLI_EXIT.SUCCESS;
1105
+ process.exitCode = errors_1.CLI_EXIT.SUCCESS;
1053
1106
  return;
1054
1107
  }
1055
1108
  await loginWithDeviceAuthorization(runtime, stdout, stderr, command.profile, Boolean(command.noSkills), Boolean(command.jsonOutput));
1056
- process.exitCode = exports.CLI_EXIT.SUCCESS;
1109
+ process.exitCode = errors_1.CLI_EXIT.SUCCESS;
1057
1110
  return;
1058
1111
  }
1059
1112
  if (command.command === 'skills_doctor') {
@@ -1065,8 +1118,56 @@ async function runCli(argv = process.argv.slice(2), runtime = {}) {
1065
1118
  const client = new client_1.DreamstateClient({ baseUrl, apiKey, ...(workspace ? { workspace } : {}), requestTimeoutMs: runtime.requestTimeoutMs });
1066
1119
  await client.listSkills({ limit: 1 });
1067
1120
  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;
1121
+ // Age says nothing about whether the install matches the server. Compare the content
1122
+ // stamp of every installed skill with the live skill's, the way install computes it.
1123
+ const ids = await listedSkillIds(client);
1124
+ const live = [];
1125
+ for (const id of ids)
1126
+ live.push(await loadHostedSkill(client, id, false));
1127
+ live.push((0, skillInstaller_1.routerSkill)(await client.listPacks()));
1128
+ const drift = await (0, skillInstaller_1.compareInstalledSkills)(live, runtime.homeDirectory, Boolean(command.project));
1129
+ const pluginAgents = await (0, pluginInstaller_1.agentsWithDreamstatePlugin)(runtime.homeDirectory);
1130
+ const duplicateSources = pluginAgents.filter((agent) => command.agents.includes(agent) && drift.installed_skill_ids.length > 0);
1131
+ print({ ok: true, data: { index_reachable: true, installers, content: drift, plugin_agents: pluginAgents, ...(duplicateSources.length ? { duplicate_skill_sources: duplicateSources } : {}) }, meta: {} }, stdout, Boolean(command.jsonOutput));
1132
+ process.exitCode = errors_1.CLI_EXIT.SUCCESS;
1133
+ return;
1134
+ }
1135
+ // The CLI's own commands answer from its parser tables, so they need no key;
1136
+ // matching capabilities are added when a key is available.
1137
+ if (command.command === 'commands_search') {
1138
+ const commands = (0, commandTable_1.searchCliCommands)(command.term, USAGE);
1139
+ let capabilities = null;
1140
+ let capabilitiesError = null;
1141
+ try {
1142
+ const environment = runtime.env ?? process.env;
1143
+ const credentialFilePath = runtimeCredentialsPath(runtime);
1144
+ const apiKey = await (0, config_1.resolveApiKey)(environment, credentialFilePath);
1145
+ const workspace = await (0, config_1.resolveWorkspace)(extracted.workspace, environment, credentialFilePath);
1146
+ const client = new client_1.DreamstateClient({
1147
+ baseUrl: (0, config_1.resolveApiBaseUrl)(environment),
1148
+ apiKey,
1149
+ ...(workspace ? { workspace } : {}),
1150
+ requestTimeoutMs: runtime.requestTimeoutMs,
1151
+ onVersionWarning: (message) => stderr.write(`${message}\n`),
1152
+ });
1153
+ capabilities = await client.listTools({ goal: command.term });
1154
+ }
1155
+ catch (error) {
1156
+ if (!commands.length || (error instanceof client_1.CliError && error.code === 'cli_update_required'))
1157
+ throw error;
1158
+ capabilitiesError = { code: (0, gatedRun_1.code)(error) ?? 'request_failed', message: error instanceof Error ? error.message : String(error) };
1159
+ }
1160
+ print({
1161
+ ok: true,
1162
+ data: {
1163
+ commands,
1164
+ // Catalog capabilities that match the same words; run one with `dreamstate tools run <capability_id>`.
1165
+ capabilities: capabilities?.data ?? null,
1166
+ ...(capabilitiesError ? { capabilities_error: capabilitiesError } : {}),
1167
+ },
1168
+ meta: capabilities?.meta && typeof capabilities.meta === 'object' ? capabilities.meta : {},
1169
+ }, stdout, Boolean(command.jsonOutput));
1170
+ process.exitCode = errors_1.CLI_EXIT.SUCCESS;
1070
1171
  return;
1071
1172
  }
1072
1173
  // Preview parses the local file and writes nothing, so it must not require a key.
@@ -1081,23 +1182,21 @@ async function runCli(argv = process.argv.slice(2), runtime = {}) {
1081
1182
  }, stdout, Boolean(command.jsonOutput));
1082
1183
  }
1083
1184
  catch (error) {
1084
- throw new CliUsageError(error instanceof CliUsageError ? error.message : error instanceof Error ? error.message : String(error));
1185
+ throw new errors_1.CliUsageError(error instanceof errors_1.CliUsageError ? error.message : error instanceof Error ? error.message : String(error));
1085
1186
  }
1086
- process.exitCode = exports.CLI_EXIT.SUCCESS;
1187
+ process.exitCode = errors_1.CLI_EXIT.SUCCESS;
1087
1188
  return;
1088
1189
  }
1089
1190
  // init writes a local scaffold and calls no API, so it must not require a key.
1090
1191
  // Resolving credentials first would refuse it with not_authenticated.
1091
1192
  if (command.command === 'pack' && command.verb === 'init') {
1092
1193
  if (command.pack === 'sequences') {
1093
- await require('./commands/sequences')
1094
- .runSequencesCommand(undefined, command, stdout);
1194
+ await sequencesCommands().runSequencesCommand(undefined, command, stdout);
1095
1195
  }
1096
1196
  else {
1097
- await require('./commands/workflows')
1098
- .runWorkflowsCommand(undefined, command, stdout);
1197
+ await workflowsCommands().runWorkflowsCommand(undefined, command, stdout);
1099
1198
  }
1100
- process.exitCode = exports.CLI_EXIT.SUCCESS;
1199
+ process.exitCode = errors_1.CLI_EXIT.SUCCESS;
1101
1200
  return;
1102
1201
  }
1103
1202
  const environment = runtime.env ?? process.env;
@@ -1117,7 +1216,7 @@ async function runCli(argv = process.argv.slice(2), runtime = {}) {
1117
1216
  }
1118
1217
  if (command.command === 'workspaces_use' && apiKey.startsWith('ds_live_')) {
1119
1218
  // 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.');
1219
+ throw new errors_1.CliUsageError('This key is bound to one workspace. Run dreamstate login for a user session that can switch.');
1121
1220
  }
1122
1221
  const workspace = command.command === 'workspaces_use'
1123
1222
  ? command.workspace
@@ -1129,6 +1228,19 @@ async function runCli(argv = process.argv.slice(2), runtime = {}) {
1129
1228
  requestTimeoutMs: runtime.requestTimeoutMs,
1130
1229
  onVersionWarning: (message) => (runtime.stderr ?? process.stderr).write(`${message}\n`),
1131
1230
  });
1231
+ const turnsPath = (0, turnCache_1.turnCachePath)(credentialFilePath);
1232
+ const idempotencyPath = (0, idempotency_1.pendingIdempotencyPath)(credentialFilePath);
1233
+ // A turn belongs to one workspace, so the workspace is part of the cache identity.
1234
+ const gatedContext = () => ({
1235
+ client,
1236
+ baseUrl,
1237
+ turnKey: workspace ? `${apiKey}\n${workspace}` : apiKey,
1238
+ turnsPath,
1239
+ idempotencyPath,
1240
+ ...(workspace ? { workspace } : {}),
1241
+ stderr,
1242
+ sleep: runtime.sleep ?? wait,
1243
+ });
1132
1244
  let result;
1133
1245
  switch (command.command) {
1134
1246
  case 'whoami': {
@@ -1149,7 +1261,7 @@ async function runCli(argv = process.argv.slice(2), runtime = {}) {
1149
1261
  stdout.write(rows.length
1150
1262
  ? `${rows.map((row) => `${row.id === activeId ? '*' : ' '} ${row.slug ?? '-'} ${row.name} ${row.role ?? '-'} ${row.id}`).join('\n')}\n`
1151
1263
  : 'No workspaces. Ask a workspace owner to invite you.\n');
1152
- process.exitCode = exports.CLI_EXIT.SUCCESS;
1264
+ process.exitCode = errors_1.CLI_EXIT.SUCCESS;
1153
1265
  return;
1154
1266
  }
1155
1267
  break;
@@ -1160,10 +1272,10 @@ async function runCli(argv = process.argv.slice(2), runtime = {}) {
1160
1272
  const wanted = command.workspace.toLowerCase();
1161
1273
  const match = data.workspaces?.find((row) => row.id.toLowerCase() === wanted || row.slug?.toLowerCase() === wanted);
1162
1274
  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.`);
1275
+ 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
1276
  const stored = await (0, config_1.readStoredCredentials)(await (0, config_1.selectedCredentialsPath)(credentialFilePath, environment));
1165
1277
  if (!stored)
1166
- throw new CliUsageError('workspaces use saves the default next to a stored login. Run dreamstate login or dreamstate login --token first.');
1278
+ throw new errors_1.CliUsageError('workspaces use saves the default next to a stored login. Run dreamstate login or dreamstate login --token first.');
1167
1279
  await (0, config_1.storeApiKey)(stored.token, await (0, config_1.selectedCredentialsPath)(credentialFilePath, environment), match.id);
1168
1280
  stderr.write(`Default workspace: ${match.name} (${match.slug})\n`);
1169
1281
  result = { ok: true, data: { workspace: { id: match.id, name: match.name, slug: match.slug }, credentials_path: credentialFilePath }, meta: {} };
@@ -1188,47 +1300,26 @@ async function runCli(argv = process.argv.slice(2), runtime = {}) {
1188
1300
  result = await client.searchSkillDocuments(command.query);
1189
1301
  break;
1190
1302
  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: {} };
1303
+ // An agent that already loads Dreamstate skills from the plugin gets them from there:
1304
+ // installing hosted copies too would load the same skill twice under two names, from
1305
+ // two sources that drift apart. login skips such agents the same way.
1306
+ const withPlugin = await (0, pluginInstaller_1.agentsWithDreamstatePlugin)(runtime.homeDirectory);
1307
+ const targets = command.agents.filter((agent) => !withPlugin.includes(agent));
1308
+ const skipped = command.agents.filter((agent) => !targets.includes(agent)).map((agent) => ({ agent, reason: 'plugin_installed' }));
1309
+ if (skipped.length)
1310
+ stderr.write(`Skipped ${skipped.map((entry) => entry.agent).join(', ')}: the Dreamstate plugin already provides skills there. Update it with dreamstate install.\n`);
1311
+ if (!targets.length) {
1312
+ result = { ok: true, data: { installed_files: [], skill_ids: [], agents: [], skipped_agents: skipped }, meta: {} };
1194
1313
  break;
1195
1314
  }
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,
1315
+ const installed = await installHostedSkills(client, targets, runtime, {
1316
+ project: command.project,
1317
+ copy: command.copy,
1318
+ ...(command.skillId === '*' ? {} : { only: [command.skillId] }),
1225
1319
  });
1226
- result = { ok: true, data: { installed_files: installed.files, skill_ids: [...ids, 'router'], agents: command.agents, ...(command.project ? { lock_file: 'skills-lock.json' } : {}) }, meta: {} };
1320
+ 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
1321
  break;
1228
1322
  }
1229
- case 'commands_search':
1230
- result = await client.listTools({ goal: command.term });
1231
- break;
1232
1323
  case 'tools_search':
1233
1324
  result = await client.listTools(command.query);
1234
1325
  break;
@@ -1239,100 +1330,30 @@ async function runCli(argv = process.argv.slice(2), runtime = {}) {
1239
1330
  result = await client.getTool(command.capabilityId);
1240
1331
  break;
1241
1332
  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
- }
1333
+ const outcome = await (0, gatedRun_1.runGated)(gatedContext(), {
1334
+ capabilityId: command.capabilityId,
1335
+ body: command.body,
1336
+ ...(command.turnToken ? { turnToken: command.turnToken } : {}),
1337
+ ...(command.idempotencyKey ? { idempotencyKey: command.idempotencyKey } : {}),
1338
+ waitTimeout: command.waitTimeout,
1339
+ ...(command.noWait ? { noWait: true } : {}),
1340
+ });
1341
+ result = outcome.value;
1342
+ if (outcome.timedOut)
1343
+ process.exitCode = runWait_1.RUN_WAIT_TIMEOUT_EXIT_CODE;
1313
1344
  break;
1314
1345
  }
1315
1346
  case 'tables_import': {
1316
1347
  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));
1348
+ const imported = await (0, tablesImport_1.importLocalTable)(gatedContext(), {
1349
+ tableId: command.tableId,
1350
+ file: command.file,
1351
+ ...(command.format ? { format: command.format } : {}),
1352
+ ...(command.upsertKey ? { upsertKey: command.upsertKey } : {}),
1353
+ ...(command.rowKind ? { rowKind: command.rowKind } : {}),
1354
+ ...(command.keepUnmappedColumns === false ? { keepUnmappedColumns: false } : {}),
1355
+ });
1356
+ result = { ok: true, data: { import: imported }, meta: {} };
1336
1357
  break;
1337
1358
  }
1338
1359
  case 'runs_list':
@@ -1341,12 +1362,6 @@ async function runCli(argv = process.argv.slice(2), runtime = {}) {
1341
1362
  case 'runs_get':
1342
1363
  result = await client.getRun(command.runId);
1343
1364
  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
1365
  case 'runs_events':
1351
1366
  result = await client.getRunEvents(command.runId);
1352
1367
  break;
@@ -1355,28 +1370,40 @@ async function runCli(argv = process.argv.slice(2), runtime = {}) {
1355
1370
  expected_state_version: command.stateVersion ?? stateVersionFromRunRead(await client.getRun(command.runId)),
1356
1371
  ...(command.reason ? { reason: command.reason } : {}),
1357
1372
  };
1358
- result = await client.cancelRun(command.runId, body, command.idempotencyKey);
1373
+ const identity = { scope: 'runs.cancel', workspace, body: { run_id: command.runId, ...body } };
1374
+ const key = command.idempotencyKey ?? await (0, idempotency_1.pendingIdempotencyKey)(idempotencyPath, identity);
1375
+ try {
1376
+ result = await client.cancelRun(command.runId, body, key);
1377
+ if (!command.idempotencyKey)
1378
+ await (0, idempotency_1.finishIdempotencyAttempt)(idempotencyPath, identity);
1379
+ }
1380
+ catch (error) {
1381
+ const transient = error instanceof client_1.CliError && (error.code === 'request_timeout' || error.code === 'network_error' || error.retryable === true || error.status >= 500);
1382
+ if (!command.idempotencyKey && !transient)
1383
+ await (0, idempotency_1.finishIdempotencyAttempt)(idempotencyPath, identity);
1384
+ throw error;
1385
+ }
1359
1386
  break;
1360
1387
  }
1361
1388
  case 'pack': {
1362
1389
  if (command.pack === 'sequences') {
1363
- await require('./commands/sequences').runSequencesCommand(client, command, stdout, runtime.sleep ?? wait);
1390
+ await sequencesCommands().runSequencesCommand(gatedContext(), command, stdout);
1364
1391
  }
1365
1392
  else {
1366
- await require('./commands/workflows').runWorkflowsCommand(client, command, stdout, runtime.sleep ?? wait);
1393
+ await workflowsCommands().runWorkflowsCommand(gatedContext(), command, stdout);
1367
1394
  }
1368
1395
  return;
1369
1396
  }
1370
1397
  default: return;
1371
1398
  }
1372
- if ((command.command === 'tools_run' || command.command === 'runs_resume') && command.open)
1399
+ if (command.command === 'tools_run' && command.open)
1373
1400
  openResult(result, runtime);
1374
1401
  if (command.command === 'packs_route' && !command.jsonOutput)
1375
1402
  printPackRoute(result, stdout);
1376
1403
  else
1377
1404
  print(result, stdout, Boolean(command.jsonOutput));
1378
1405
  if (process.exitCode !== runWait_1.RUN_WAIT_TIMEOUT_EXIT_CODE)
1379
- process.exitCode = exports.CLI_EXIT.SUCCESS;
1406
+ process.exitCode = errors_1.CLI_EXIT.SUCCESS;
1380
1407
  }
1381
1408
  catch (error) {
1382
1409
  const exitCode = exitCodeForError(error);