@assethub/cli 0.1.30 → 0.1.31

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 (70) hide show
  1. package/README.md +282 -8
  2. package/dist/appEnv.d.ts +20 -0
  3. package/dist/appEnv.d.ts.map +1 -0
  4. package/dist/appEnv.js +62 -0
  5. package/dist/canvasGraphDownload.d.ts +11 -0
  6. package/dist/canvasGraphDownload.d.ts.map +1 -0
  7. package/dist/canvasGraphDownload.js +108 -0
  8. package/dist/execution.d.ts +5 -2
  9. package/dist/execution.d.ts.map +1 -1
  10. package/dist/execution.js +11 -12
  11. package/dist/hooks/command.d.ts +14 -0
  12. package/dist/hooks/command.d.ts.map +1 -0
  13. package/dist/hooks/command.js +178 -0
  14. package/dist/hooks/graph.d.ts +40 -0
  15. package/dist/hooks/graph.d.ts.map +1 -0
  16. package/dist/hooks/graph.js +267 -0
  17. package/dist/hooks/index.d.ts +7 -0
  18. package/dist/hooks/index.d.ts.map +1 -0
  19. package/dist/hooks/index.js +6 -0
  20. package/dist/hooks/install.d.ts +43 -0
  21. package/dist/hooks/install.d.ts.map +1 -0
  22. package/dist/hooks/install.js +174 -0
  23. package/dist/hooks/paths.d.ts +31 -0
  24. package/dist/hooks/paths.d.ts.map +1 -0
  25. package/dist/hooks/paths.js +101 -0
  26. package/dist/hooks/redact.d.ts +5 -0
  27. package/dist/hooks/redact.d.ts.map +1 -0
  28. package/dist/hooks/redact.js +67 -0
  29. package/dist/hooks/save.d.ts +60 -0
  30. package/dist/hooks/save.d.ts.map +1 -0
  31. package/dist/hooks/save.js +330 -0
  32. package/dist/hooks/transcript.d.ts +23 -0
  33. package/dist/hooks/transcript.d.ts.map +1 -0
  34. package/dist/hooks/transcript.js +74 -0
  35. package/dist/hooks/trim.d.ts +15 -0
  36. package/dist/hooks/trim.d.ts.map +1 -0
  37. package/dist/hooks/trim.js +108 -0
  38. package/dist/hooks/types.d.ts +56 -0
  39. package/dist/hooks/types.d.ts.map +1 -0
  40. package/dist/hooks/types.js +0 -0
  41. package/dist/hooks/upload.d.ts +52 -0
  42. package/dist/hooks/upload.d.ts.map +1 -0
  43. package/dist/hooks/upload.js +252 -0
  44. package/dist/index.d.ts +12 -2
  45. package/dist/index.d.ts.map +1 -1
  46. package/dist/index.js +1569 -165
  47. package/dist/nodeMeshBatch.d.ts.map +1 -1
  48. package/dist/nodeMeshBatch.js +5 -4
  49. package/dist/productionBatch.d.ts +77 -0
  50. package/dist/productionBatch.d.ts.map +1 -0
  51. package/dist/productionBatch.js +204 -0
  52. package/dist/runProgress.d.ts +4 -0
  53. package/dist/runProgress.d.ts.map +1 -0
  54. package/dist/runProgress.js +16 -0
  55. package/dist/runsUpload/controlEndpoints.d.ts +1 -1
  56. package/dist/runsUpload/controlEndpoints.d.ts.map +1 -1
  57. package/dist/runsUpload/controlEndpoints.js +4 -2
  58. package/dist/runsUpload/uploadRun.d.ts +7 -1
  59. package/dist/runsUpload/uploadRun.d.ts.map +1 -1
  60. package/dist/runsUpload/uploadRun.js +7 -4
  61. package/dist/setupCommand.d.ts +127 -0
  62. package/dist/setupCommand.d.ts.map +1 -0
  63. package/dist/setupCommand.js +534 -0
  64. package/dist/setupHooksBridge.d.ts +8 -0
  65. package/dist/setupHooksBridge.d.ts.map +1 -0
  66. package/dist/setupHooksBridge.js +4 -0
  67. package/dist/update.d.ts +74 -0
  68. package/dist/update.d.ts.map +1 -0
  69. package/dist/update.js +205 -0
  70. package/package.json +2 -2
package/dist/index.js CHANGED
@@ -1,24 +1,33 @@
1
1
  #!/usr/bin/env node
2
+ import { childOperationId, expandProductionBatch, planProductionNodeMeshes, callApiOperation, describeApiOperation, discoverApiOperations, searchApiOperations, TERMINAL_WORKSPACE_SKILL_BUILD_STATUSES, WORKSPACE_SKILL_ENHANCE_SECTIONS, } from '@assethub/api-client';
2
3
  import { setTimeout as delay } from 'node:timers/promises';
3
4
  import { cliVersion, diagnose, mcpConfig } from './setup.js';
4
5
  import { AGENT_IDS, initAgents, syncInstalledSkill } from './agentSetup.js';
6
+ import { confirmNo, confirmYes, defaultFileDeps, formatSetupSummary, pickFromList, promptHidden, runProcess, runSetup, } from './setupCommand.js';
7
+ import { launchAgentProgram, loadKeyIntoLaunchd } from './appEnv.js';
8
+ import { defaultInstallHooks } from './setupHooksBridge.js';
9
+ import { canUploadSessions, installHooks as installSessionHooks, runHooksCommand } from './hooks/index.js';
10
+ import { createNodeUpdateDeps, runUpdate } from './update.js';
5
11
  import { ingestProjectSources } from './projectSources.js';
6
12
  import { downloadCanvas } from './canvasDownload.js';
13
+ import { downloadCanvasGraphZip } from './canvasGraphDownload.js';
7
14
  import { executeNodeMeshBatch, resumeNodeMeshBatch, } from './nodeMeshBatch.js';
8
15
  import { writeCanvasComparison } from './canvasComparison.js';
16
+ import { productionBatchLabels, readProductionBatch, runProductionBatch, } from './productionBatch.js';
9
17
  import { importCanvasAssetWithState } from './canvasAssetImport.js';
10
18
  import { execFile, spawn } from 'node:child_process';
11
19
  import { createHash, randomUUID } from 'node:crypto';
12
20
  import { realpathSync } from 'node:fs';
13
- import { chmod, mkdir, readFile, rename, unlink, writeFile, } from 'node:fs/promises';
21
+ import { chmod, mkdir, readdir, readFile, rename, unlink, writeFile, } from 'node:fs/promises';
14
22
  import { homedir, tmpdir } from 'node:os';
15
23
  import { basename, dirname, extname, join, resolve } from 'node:path';
16
24
  import { argv, env, exit, stdin, stderr, stdout } from 'node:process';
17
25
  import { fileURLToPath } from 'node:url';
18
26
  import { promisify } from 'node:util';
19
- import { AssetHubApiError, callApiOperation, describeApiOperation, discoverApiOperations, searchApiOperations, WorkspaceClientError, createWorkspaceClient, createAssetHubClient, } from '@assethub/api-client';
27
+ import { AssetHubApiError, WorkspaceClientError, createWorkspaceClient, createAssetHubClient, } from '@assethub/api-client';
20
28
  import { resolveCanvasSelection, saveCanvasSelection } from './canvas.js';
21
- import { activeExecution, childOperationId, CliExecutionError, executeRecorded, hasRecordedOperation, openExecutionSession, resumeRecorded, waitForExecution, } from './execution.js';
29
+ import { activeExecution, CliExecutionError, executeRecorded, hasRecordedOperation, openExecutionSession, resumeRecorded, waitForExecution, } from './execution.js';
30
+ import { runProgressLine } from './runProgress.js';
22
31
  import { buildRunUploadPlan, formatBytes } from './runsUpload/buildRunUpload.js';
23
32
  import { readGraphFolder } from './runsUpload/graphFolder.js';
24
33
  import { uploadRun } from './runsUpload/uploadRun.js';
@@ -49,6 +58,81 @@ const partExtractorOptions = [
49
58
  apiValue: 'ah_part_extractor_v21_alpha',
50
59
  aliases: ['v2.1 alpha', 'v2.1-alpha', '2.1 alpha'],
51
60
  },
61
+ {
62
+ publicName: 'V2.1-beta',
63
+ apiValue: 'ah_part_extractor_v6b',
64
+ aliases: ['v2.1 beta', 'v2.1-beta', '2.1 beta', '2.1-beta'],
65
+ },
66
+ {
67
+ publicName: 'V1',
68
+ apiValue: 'ah_part_extractor_v5',
69
+ aliases: ['v1'],
70
+ },
71
+ {
72
+ publicName: 'V3.0.1',
73
+ apiValue: 'ah_agent_graph_v3_0_1',
74
+ aliases: ['v3.0.1', '3.0.1'],
75
+ },
76
+ {
77
+ publicName: 'V3.5.3 Complete Objects',
78
+ apiValue: 'ah_agent_graph_v3_5_3',
79
+ aliases: ['v3.5.3', '3.5.3'],
80
+ },
81
+ {
82
+ publicName: 'V3.5.3.1 Reliable Analysis',
83
+ apiValue: 'ah_agent_graph_v3_5_3_1',
84
+ aliases: ['v3.5.3.1', '3.5.3.1'],
85
+ },
86
+ {
87
+ publicName: 'V3.5.4 Evidence Review',
88
+ apiValue: 'ah_agent_graph_v3_5_4',
89
+ aliases: ['v3.5.4', '3.5.4'],
90
+ },
91
+ {
92
+ publicName: 'V3.5.5 Complete Part Review',
93
+ apiValue: 'ah_agent_graph_v3_5_5',
94
+ aliases: ['v3.5.5', '3.5.5'],
95
+ },
96
+ {
97
+ publicName: 'V3.5.6 Golden Recovery',
98
+ apiValue: 'ah_agent_graph_v3_5_6',
99
+ aliases: ['v3.5.6', '3.5.6'],
100
+ },
101
+ {
102
+ publicName: 'V3.5.7 Original Source First',
103
+ apiValue: 'ah_agent_graph_v3_5_7',
104
+ aliases: ['v3.5.7', '3.5.7'],
105
+ },
106
+ {
107
+ publicName: 'V3.5.8 Concise Inventory',
108
+ apiValue: 'ah_agent_graph_v3_5_8',
109
+ aliases: ['v3.5.8', '3.5.8'],
110
+ },
111
+ {
112
+ publicName: 'V3.5.9 Observed Parts',
113
+ apiValue: 'ah_agent_graph_v3_5_9',
114
+ aliases: ['v3.5.9', '3.5.9'],
115
+ },
116
+ {
117
+ publicName: 'V3.6 Multiview References',
118
+ apiValue: 'ah_agent_graph_v3_6',
119
+ aliases: ['v3.6', '3.6'],
120
+ },
121
+ {
122
+ publicName: 'Chibi Character (Pluffy)',
123
+ apiValue: 'ah_agent_graph_pluffy_v3',
124
+ aliases: ['pluffy v3', 'pluffy 3'],
125
+ },
126
+ {
127
+ publicName: 'V3-2.1',
128
+ apiValue: 'ah_agent_graph_v21',
129
+ aliases: ['v3-2.1', '3-2.1'],
130
+ },
131
+ {
132
+ publicName: 'V3.6.2 Single Analysis',
133
+ apiValue: 'ah_agent_graph_v3_6_2',
134
+ aliases: ['v3.6.2', '3.6.2'],
135
+ },
52
136
  {
53
137
  publicName: 'V3.6.1',
54
138
  apiValue: 'ah_agent_graph_v3_6_1',
@@ -69,21 +153,15 @@ const partExtractorOptions = [
69
153
  apiValue: 'ah_agent_graph_v3_6_5',
70
154
  aliases: ['v3.6.5', '3.6.5'],
71
155
  },
72
- {
73
- publicName: 'Chibi Character (Pluffy) v3.1',
74
- apiValue: 'ah_agent_graph_pluffy_v3_1',
75
- aliases: ['pluffy', 'pluffy v3.1', 'pluffy 3.1'],
76
- },
77
156
  {
78
157
  publicName: 'V3.0.9 Garment Boundaries',
79
158
  apiValue: 'ah_agent_graph_v3_7_2',
80
159
  aliases: ['v3.0.9', '3.0.9'],
81
160
  },
82
161
  {
83
- publicName: 'V3.7 Artist Skills',
84
- apiValue: 'ah_agent_graph_v3_7',
85
- // The web picker and `production agents` call it V3.0.7.
86
- aliases: ['v3.7', '3.7', 'v3.0.7 artist skills', 'v3.0.7', '3.0.7'],
162
+ publicName: 'V3.0.10-dev.1 Body Fit',
163
+ apiValue: 'ah_agent_graph_v3_7_3',
164
+ aliases: ['v3.0.10-dev.1', '3.0.10-dev.1'],
87
165
  },
88
166
  {
89
167
  publicName: 'V3.7.1 Building Modules',
@@ -92,11 +170,31 @@ const partExtractorOptions = [
92
170
  aliases: ['v3.7.1', '3.7.1', 'v3.0.8 building modules', 'v3.0.8', '3.0.8'],
93
171
  },
94
172
  {
95
- publicName: 'Humanoid Assembly (internal)',
173
+ publicName: 'Chibi Character (Pluffy) v3.1',
174
+ apiValue: 'ah_agent_graph_pluffy_v3_1',
175
+ aliases: ['pluffy', 'pluffy v3.1', 'pluffy 3.1'],
176
+ },
177
+ {
178
+ publicName: 'V3.7 Artist Skills',
179
+ apiValue: 'ah_agent_graph_v3_7',
180
+ // The web picker and `production agents` call it V3.0.7.
181
+ aliases: ['v3.7', '3.7', 'v3.0.7 artist skills', 'v3.0.7', '3.0.7'],
182
+ },
183
+ {
184
+ publicName: 'Humanoid Assembly',
96
185
  apiValue: 'ah_agent_graph_humanoid_assembly_auto',
97
- aliases: ['humanoid assembly', 'humanoid-assembly', 'humanoid_assembly_auto'],
186
+ // The name CLI 0.1.26-0.1.31 used stays accepted.
187
+ aliases: ['humanoid assembly (internal)', 'humanoid-assembly', 'humanoid_assembly_auto'],
188
+ },
189
+ {
190
+ publicName: 'V4 Character Assembly',
191
+ apiValue: 'ah_agent_graph_harpy_assembly_v2',
192
+ aliases: ['v4', '4', 'v4 character assembly', 'character assembly'],
98
193
  },
99
194
  ];
195
+ // A V4 Character Assembly run (image → parts → meshes → assembled character) takes
196
+ // about an hour, so `--wait` defaults past the generic 15 minute limit.
197
+ const harpyAssemblyV2WaitMs = 2 * 60 * 60 * 1000;
100
198
  // Graph-engine extractors run through the artifact-graph endpoint. Derived from
101
199
  // the option list so a newly listed graph agent cannot miss the graph path.
102
200
  const artifactGraphPartExtractorApiValues = new Set(partExtractorOptions
@@ -124,6 +222,9 @@ CLI and MCP are available to all AssetHub users. Workspace permissions and featu
124
222
 
125
223
  Usage:
126
224
  assethub --version
225
+ assethub setup [--client claude|codex|both] [--profile <name>] [--workspace <id>] [--api-key-stdin] [--base-url <url>] [--no-app-env] [--dry-run] [--yes] [--print-env] [--json]
226
+ assethub env load [--profile <name>]
227
+ assethub update [--check] [--dry-run] [--yes]
127
228
  assethub doctor [--mcp] [--profile <name>] [--timeout-ms <n>]
128
229
  assethub init [--agent claude-code|codex|cursor...] [--project] [--dry-run] [--base-url <url>] [--workspace <id>] [--profile <name>]
129
230
  assethub mcp tools [tool-name] [--account] [--profile <name>] [--timeout-ms <n>]
@@ -167,31 +268,30 @@ Usage:
167
268
  assethub api search [query]
168
269
  assethub api describe "<METHOD /path>"
169
270
  assethub api call "<METHOD /path>" [--path-json <json|@file>] [--query-json <json|@file>] [--input-json <json|@file|@->] [--operation-id <uuid>]
170
- assethub skills list [--cursor <cursor>]
171
- assethub skills get <skill-id> [--revision <n>]
172
- assethub skills learn --input-json <json|@file|@-> --operation-id <uuid>
173
- assethub skills update <skill-id> --input-json <json|@file|@->
174
- assethub skills controls <skill-id> --revision <n> --grant-revision <n> --mode automatic|manual|off [--share]
175
- assethub skills prepare --input-json <json|@file|@->
176
- assethub skills proposal <proposal-id>
177
- assethub skills accept <proposal-id> --input-json <json|@file|@->
178
271
  assethub composer models
179
272
  assethub composer refine --list-modes
180
- assethub composer refine --from-run <run-id> --instruction <text> [--mode standard|thorough|placement|workshop|blender|codex] [--effort light|standard|thorough] [--max-rounds <n>] [--transforms-json <json|@file>] [--wait] [--download --out-dir <dir>]
273
+ assethub composer refine --from-run <run-id> --instruction <text> [--mode standard|thorough|placement|workshop|blender|codex] [--effort light|standard|thorough] [--max-rounds <n>] [--agent-model <provider:id|unambiguous-id>] [--transforms-json <json|@file>] [--wait] [--download --out-dir <dir>]
181
274
  assethub composer refine --input-json <json|@file> [--canvas <id>] [--operation-id <uuid>] [--wait] [--download --out-dir <dir>]
182
- assethub composer run --part <mesh-asset-id>[:<part-image-asset-id>] [--part <mesh-asset-id>[:<part-image-asset-id>]...] --reference <image-asset-id> [--model <id>] [--mode quick|quality] [--transforms-json <json|@file>] [--canvas <id>] [--wait]
275
+ assethub composer refine --from-run <run-id> --skill-assembly [--reasoning low|medium|high] [--agent-model <agents-api model>] [--wait]
276
+ assethub composer run --part <mesh-asset-id>[=<part-image-id>] [--part <mesh-asset-id>[=<part-image-id>]...] --reference <image-asset-id> [--model <id>] [--agent-model <provider:id|unambiguous-id>] [--mode quick|quality] [--optimize off|light|medium|heavy] [--transforms-json <json|@file>] [--canvas <id>] [--wait]
277
+ assethub composer run --part <mesh-asset-id>=<part-image-id>... --reference <image-asset-id> --model V6 [--optimize off|light|medium|heavy] [--canvas <id>] [--wait]
278
+ assethub composer run --part <mesh-asset-id>... --reference <image-asset-id> --model V5.1 [--reasoning low|medium|high] [--agent-model <agents-api model>] [--transforms-json <json|@file>] [--canvas <id>] [--wait]
183
279
  assethub composer run --input-json <json|@file> [--canvas <id>] [--operation-id <uuid>] [--wait] [--download --out-dir <dir>]
184
280
  assethub composer run --from-run <run-id> [--transforms-json <json|@file>] [--mode quick|quality] [--wait]
185
281
  assethub composer run --node <shape:id> --canvas <id> [--model <id>] [--mode quick|quality] [--operation-id <uuid>] [--wait]
186
282
  assethub canvas create [--name <name>] [--operation-id <uuid>]
187
283
  assethub canvas list [--cursor <cursor>] [--limit <n>]
188
284
  assethub canvas get|use|open [<id>]
285
+ assethub canvas graph-id <canvas-id>
189
286
  assethub runs list [--canvas <id>] [--cursor <cursor>] [--limit <n>]
190
- assethub runs get|watch|wait <run-id> [--interval-ms <ms>] [--timeout-ms <n>] [--summary]
287
+ assethub runs get|watch|wait <run-id> [--interval-ms <ms>] [--timeout-ms <n>] [--summary] [--download --out-dir <dir>]
191
288
  assethub runs resume <operation-id> [--wait]
192
289
  assethub graph list [--cursor <cursor>] [--limit <n>]
193
290
  assethub graph show|export [--canvas <id> | --graph <id>] [--source generated|upload] [--artifact <node-id>] [--out <path>] [--allow-truncated]
194
291
  assethub graph lineage --artifact <node-id> [--canvas <id> | --graph <id>] [--source generated|upload] [--direction ancestors|descendants|both] [--depth <n>] [--out <path>]
292
+ assethub graph snapshot <graph-id> [--offset <n>] [--limit <n>]
293
+ assethub graph node <graph-id> <node-id>
294
+ assethub graph image <graph-id> <node-id> [--out <path>]
195
295
  assethub evaluate list
196
296
  assethub evaluations submit --artifact <asset-id> --report <json|file|@file|@-> [--agent <name>] [--canvas <id>] [--reference <asset-id>...] [--run <run-id>] [--operation-id <uuid>] [--require-pass]
197
297
  assethub evaluations list [--canvas <id>] [--artifact <asset-id>]
@@ -212,7 +312,10 @@ Usage:
212
312
  assethub animate retarget --resource-id <id> --animation <preset-id> [--animation <preset-id>... up to 5 total] [--out-format glb|fbx] [--bake-animation true|false] [--export-with-geometry true|false] [--animate-in-place true|false] [--name <name>] [--wait] [--download --out-dir <dir>]
213
313
  assethub jobs get <job-id> [--download --out-dir <dir>]
214
314
  assethub jobs watch <job-id> [--interval-ms <ms>] [--timeout-ms <ms>] [--download --out-dir <dir>]
215
- assethub production analyze (--file <path> | --source-url <url> | --source-id <id> | --image-url <url> | --file-ref-json <json> | --stdin | --stdin-base64 | --stdin-data-uri | --stdin-json | --source-json <json|@file|@-> | --data-uri <uri> | --clipboard) [--file-name <name>] [--content-type <type>] [--part-extractor <name>] [--name <name>] [--skill-mode auto|manual|off] [--skill-id <id>...] [--context <canvas-id> --context-version <n>] [--wait] [--download --out-dir <dir>]
315
+ assethub production analyze (--file <path> | --source-url <url> | --source-id <id> | --image-url <url> | --file-ref-json <json> | --stdin | --stdin-base64 | --stdin-data-uri | --stdin-json | --source-json <json|@file|@-> | --data-uri <uri> | --clipboard) [--file-name <name>] [--content-type <type>] [--part-extractor <name>] [--name <name>] [--base-body <mesh-asset-id>] [--skill-planner-model <model>] [--auto-repair true|false] [--skill-mode auto|manual|off] [--skill-id <id>...] [--context <canvas-id> --context-version <n>] [--wait] [--download --out-dir <dir>]
316
+ Production graph options: --pipeline-depth parts|mesh|composition [--assembly-policy concept-to-character-v1] [--mesh-generation-json <json|@file>]
317
+ assethub production batch (--file <path>... | --files-dir <dir> | --source-id <id>... | --source-url <url>...) --part-extractor <name> [--repeat <n>] [--yes] [--canvas <id>] [--operation-id <uuid>] [--wait [--concurrency <n>]] [--download --out-dir <dir>]
318
+ One production analyze per image × --repeat, on one canvas. Takes the production analyze options. Rerun with the printed --operation-id to resume; nothing is started twice.
216
319
  assethub production agents
217
320
  assethub production automation --input-json <json|@file|@-> [--canvas <id>] [--operation-id <uuid>] [--wait]
218
321
  assethub production status <order-id> [--wait] [--download --out-dir <dir>]
@@ -222,10 +325,32 @@ Usage:
222
325
  assethub production watch <order-id> [--interval-ms <ms>] [--timeout-ms <ms>] [--download --out-dir <dir>]
223
326
  assethub production intervene <order-id> (--exclude <target-id> | --include <target-id> | --add-part <name> | --rename <target-id>=<name> | --reject <target-id>[=<reason>] | --regenerate <target-id>=<mode> | --set-param <key>=<value> | --ops-json <json|@file|@->)... [--idempotency-key <key>]
224
327
  assethub production interventions <order-id>
328
+ assethub production resume <run-id|order-id> [--expected-resume-count <n>] [--wait] [--timeout-ms <ms>] [--download --out-dir <dir>] (after a fix, pick a stopped or stalled graph run back up; no new charge)
225
329
  assethub runs upload <path> [--graph-id <id>] [--stream-id <id>] [--rev <n>] [--description <text>] [--tag <tag>...] [--skip-register] [--dry-run]
226
- assethub parts split (--file <path> | --source-url <url> | --source-id <id> | --image-url <url> | --file-ref-json <json> | --stdin | --stdin-base64 | --stdin-data-uri | --stdin-json | --source-json <json|@file|@-> | --data-uri <uri> | --clipboard | --order-id <id>) [--file-name <name>] [--content-type <type>] [--part-extractor <name>] [--skill-mode auto|manual|off] [--skill-id <id>...] [--wait] [--task-id <id>...] [--mission-id <id>] [--all-ready] [--download --out-dir <dir>]
330
+ assethub parts split (--file <path> | --source-url <url> | --source-id <id> | --image-url <url> | --file-ref-json <json> | --stdin | --stdin-base64 | --stdin-data-uri | --stdin-json | --source-json <json|@file|@-> | --data-uri <uri> | --clipboard | --order-id <id>) [--file-name <name>] [--content-type <type>] [--part-extractor <name>] [--base-body <mesh-asset-id>] [--skill-planner-model <model>] [--auto-repair true|false] [--skill-mode auto|manual|off] [--skill-id <id>...] [--wait] [--task-id <id>...] [--mission-id <id>] [--all-ready] [--download --out-dir <dir>]
227
331
  assethub parts compare (--file <path> | --source-url <url> | --source-id <id> | --image-url <url> | --file-ref-json <json> | --stdin | --stdin-base64 | --stdin-data-uri | --stdin-json | --source-json <json|@file|@-> | --data-uri <uri> | --clipboard) [--file-name <name>] [--content-type <type>] [--preprocess-prompt <text>] [--preprocess-model-id <id>] [--fail-on-preprocess-error] [--part-extractor <name>] [--wait] [--task-id <id>...] [--mission-id <id>] [--all-ready] [--download --out-dir <dir>]
228
332
  assethub autopilot [<image>] --demo [--yolo] [--json] [--max-regen <n>] [--stall-rounds <n>] [--tie-epsilon <x>] [--multiview 2-view|4-view|6-view] [--ab-mode serial|cross] [--credit-budget <n>]
333
+ assethub memory memorize --canvas <id> [--nodes <shape-id>,<shape-id>,...] [--wait] [--timeout <s>]
334
+ assethub memory replay <memory-id> --source <asset-id> [--wait] [--timeout <s>]
335
+ assethub skills list [--cursor <cursor>]
336
+ assethub skills get <skill-id> [--revision <n>]
337
+ assethub skills memory <skill-id> [--revision <n>] [--images <dir>]
338
+ assethub skills learn --input-json <json|@file|@-> --operation-id <uuid>
339
+ assethub skills update <skill-id> --input-json <json|@file|@->
340
+ assethub skills controls <skill-id> --revision <n> --grant-revision <n> --mode automatic|manual|off [--share]
341
+ assethub skills prepare --input-json <json|@file|@->
342
+ assethub skills proposal <proposal-id>
343
+ assethub skills accept <proposal-id> --input-json <json|@file|@->
344
+ assethub skills validate --input-json <json|@file|@-> [--build <build-id>]
345
+ assethub skills schema
346
+ assethub skills build --goal <text> (--canvas <id>... | --memory <id>...) [--instructions <text>] [--operation-id <uuid>] [--wait] [--dry-run]
347
+ assethub skills official list
348
+ assethub skills official install <skill-id> --revision <n> --content-sha256 <sha256> [--expected-revision <n>] [--expected-grant-revision <n>]
349
+ assethub skills official prepare <skill-id> --input-json <json|@file|@->
350
+ assethub skills official customize <skill-id> --input-json <json|@file|@->
351
+ assethub org search <query>
352
+ assethub org canvases --org <orgId>
353
+ assethub graph export-canvas --org <orgId> --canvas <id>[,<id>...] [--wait] [--timeout <s>] [--out-dir <dir>]
229
354
 
230
355
  Global options:
231
356
  --api-key <key> Overrides saved auth and ASSETHUB_API_KEY.
@@ -254,6 +379,8 @@ Examples:
254
379
  assethub rig create --source-resource-id mesh_x --rig-type biped --wait
255
380
  assethub animate retarget --resource-id rig_x --animation preset:idle --wait
256
381
  assethub production analyze --file ./input.png --part-extractor "V1.5" --wait
382
+ assethub production analyze --file ./concept.png --part-extractor v4 --wait --download --out-dir ./character
383
+ assethub production batch --file ./harpy.png --file ./satyr.png --repeat 2 --part-extractor v4 --yes --wait --download --out-dir ./batch
257
384
  assethub production run --order-id ord_x --mission-id ms_x --run-mode full_auto --max-cost-credits 200 --wait
258
385
  assethub production run --image ./character.png --estimate
259
386
  assethub production run --image ./character.png --compose v6 --max-cost 500 --wait --download --out-dir ./out/asset
@@ -262,12 +389,17 @@ Examples:
262
389
  assethub runs wait run_x --timeout-ms 3600000
263
390
  assethub production intervene ord_x --exclude 7 --rename 3="left wing" --idempotency-key review-2026-07-29
264
391
  assethub production interventions ord_x
392
+ assethub production resume ord_x
393
+ assethub production resume <run-id> --wait --download --out-dir ./character
265
394
  assethub runs upload ./out/pluffy-run-2026-07-30 --dry-run
266
395
  assethub runs upload ./out/pluffy-run-2026-07-30 --tag pluffy --description "V3 humanoid, 18 parts"
267
396
  assethub parts split --file ./input.png --part-extractor "V1.5" --wait --all-ready
268
397
  assethub parts compare --file ./input.png --preprocess-prompt "clean white background, centered product photo" --part-extractor "V1.5" --wait --all-ready
269
398
  `;
399
+ let printSuppressed = false;
270
400
  const print = (value) => {
401
+ if (printSuppressed)
402
+ return;
271
403
  stdout.write(`${JSON.stringify(value, null, 2)}\n`);
272
404
  };
273
405
  const parseArgs = (args) => {
@@ -369,8 +501,8 @@ const resolveProductionSkillSelection = (flags, agentVersion) => {
369
501
  }
370
502
  if (mode !== 'auto' && mode !== 'manual' && mode !== 'off')
371
503
  throw new Error('--skill-mode must be "auto", "manual", or "off"');
372
- if (!['ah_agent_graph_v3_7', 'ah_agent_graph_v3_7_1'].includes(agentVersion))
373
- throw new Error('--skill-mode requires V3.7 Artist Skills or V3.7.1 Building Modules');
504
+ if (agentVersion !== 'ah_agent_graph_v3_7' && agentVersion !== 'ah_agent_graph_v3_7_1' && agentVersion !== 'ah_agent_graph_v3_7_2' && agentVersion !== 'ah_agent_graph_v3_7_3')
505
+ throw new Error('--skill-mode is supported only with V3 Artist Skills');
374
506
  if (skillIds.length > 3)
375
507
  throw new Error('--skill-id accepts at most 3 values');
376
508
  if (new Set(skillIds).size !== skillIds.length)
@@ -383,6 +515,14 @@ const resolveProductionSkillSelection = (flags, agentVersion) => {
383
515
  throw new Error('--skill-mode off does not accept --skill-id');
384
516
  return { mode, skillIds };
385
517
  };
518
+ const resolveProductionBaseBody = (flags) => {
519
+ const value = getFlag(flags, 'base-body');
520
+ return value == null ? undefined : assertFlagHasValue(value, '--base-body <mesh-asset-id>');
521
+ };
522
+ const resolveSkillPlannerModel = (flags) => {
523
+ const value = getFlag(flags, 'skill-planner-model');
524
+ return value == null ? undefined : assertFlagHasValue(value, '--skill-planner-model <model>');
525
+ };
386
526
  const publicPartExtractorName = (value) => {
387
527
  if (value == null) {
388
528
  return null;
@@ -759,17 +899,6 @@ const assertFlagHasValue = (value, flagDescription) => {
759
899
  * attaches the image the part's mesh was generated from, matching what the
760
900
  * canvas node graph supplies automatically for UI-driven runs.
761
901
  */
762
- const parsePartFlagValue = (raw) => {
763
- const value = assertFlagHasValue(raw, '--part <mesh-asset-id>[:<part-image-asset-id>]');
764
- const separatorIndex = value.indexOf(':');
765
- if (separatorIndex === -1)
766
- return { assetId: value };
767
- const assetId = value.slice(0, separatorIndex);
768
- const partImageAssetId = value.slice(separatorIndex + 1);
769
- if (!assetId || !partImageAssetId)
770
- throw new Error('--part must be <mesh-asset-id> or <mesh-asset-id>:<part-image-asset-id>');
771
- return { assetId, partImageAssetId };
772
- };
773
902
  const sourceInputCount = (flags) => [
774
903
  getSourceResourceIdFlag(flags) != null,
775
904
  getSourceUrlFlag(flags) != null,
@@ -1408,68 +1537,6 @@ const commandApi = async (subcommand, positionals, ctx) => {
1408
1537
  throw error;
1409
1538
  }
1410
1539
  };
1411
- export const commandSkills = async (subcommand, positionals, ctx) => {
1412
- if (subcommand === 'list') {
1413
- print(await ctx.client.v2.listWorkspaceSkills({
1414
- cursor: getFlag(ctx.flags, 'cursor'),
1415
- }));
1416
- return;
1417
- }
1418
- if (subcommand === 'get') {
1419
- print(await ctx.client.v2.getWorkspaceSkill(requirePositional(positionals, 2, 'skill-id'), {
1420
- revision: hasFlag(ctx.flags, 'revision')
1421
- ? parsePositiveIntegerFlag(ctx.flags, 'revision', 1)
1422
- : undefined,
1423
- }));
1424
- return;
1425
- }
1426
- if (subcommand === 'controls') {
1427
- const mode = parseEnumFlag(ctx.flags, 'mode', [
1428
- 'automatic',
1429
- 'manual',
1430
- 'off',
1431
- ]);
1432
- if (!mode)
1433
- throw new Error('Missing required flag: --mode');
1434
- requireFlag(ctx.flags, 'revision');
1435
- requireFlag(ctx.flags, 'grant-revision');
1436
- print(await ctx.client.v2.setWorkspaceSkillControls(requirePositional(positionals, 2, 'skill-id'), {
1437
- expectedRevision: parsePositiveIntegerFlag(ctx.flags, 'revision', 1),
1438
- expectedGrantRevision: parsePositiveIntegerFlag(ctx.flags, 'grant-revision', 1),
1439
- mode,
1440
- ...(hasFlag(ctx.flags, 'share') ? { share: true } : {}),
1441
- }));
1442
- return;
1443
- }
1444
- if (subcommand === 'proposal') {
1445
- print(await ctx.client.v2.getWorkspaceSkillProposal(requirePositional(positionals, 2, 'proposal-id')));
1446
- return;
1447
- }
1448
- if (['learn', 'prepare'].includes(subcommand ?? '')) {
1449
- const input = await readJsonArgument(requireFlag(ctx.flags, 'input-json'), '--input-json');
1450
- const operationId = subcommand === 'learn'
1451
- ? requireFlag(ctx.flags, 'operation-id')
1452
- : undefined;
1453
- if (operationId != null &&
1454
- !/^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i.test(operationId))
1455
- throw new Error('--operation-id must be a UUID; reuse the same ID and input after an uncertain response');
1456
- if (operationId != null)
1457
- activeExecution.operationId = operationId;
1458
- print(subcommand === 'learn'
1459
- ? await ctx.client.v2.learnWorkspaceSkill(input, { idempotencyKey: operationId })
1460
- : await ctx.client.v2.prepareWorkspaceSkillProposal(input));
1461
- return;
1462
- }
1463
- if (['update', 'accept'].includes(subcommand ?? '')) {
1464
- const id = requirePositional(positionals, 2, subcommand === 'update' ? 'skill-id' : 'proposal-id');
1465
- const input = await readJsonArgument(requireFlag(ctx.flags, 'input-json'), '--input-json');
1466
- print(subcommand === 'update'
1467
- ? await ctx.client.v2.updateWorkspaceSkill(id, input)
1468
- : await ctx.client.v2.acceptWorkspaceSkillProposal(id, input));
1469
- return;
1470
- }
1471
- throw new Error('Use skills list|get|learn|update|controls|prepare|proposal|accept');
1472
- };
1473
1540
  const commandModels = async (subcommand, positionals, ctx) => {
1474
1541
  if (subcommand === 'list') {
1475
1542
  const domain = getFlag(ctx.flags, 'domain');
@@ -1837,6 +1904,133 @@ const commandAuth = async (subcommand, flags) => {
1837
1904
  }
1838
1905
  throw new Error('Unknown auth command. Use "auth login", "auth status", or "auth logout".');
1839
1906
  };
1907
+ // One-command onboarding. Login and workspace selection reuse commandAuth and
1908
+ // commandWorkspace (their JSON output is suppressed); setupCommand.ts owns the
1909
+ // client configuration and the summary.
1910
+ export const commandSetup = async (flags, installHooks = defaultInstallHooks) => {
1911
+ const client = getFlag(flags, 'client') ?? 'both';
1912
+ if (client !== 'claude' && client !== 'codex' && client !== 'both')
1913
+ throw new Error('--client must be claude, codex or both');
1914
+ const config = await readAuthConfig(getConfigPath(flags));
1915
+ // Only name a profile the user chose or one that is saved: a synthetic profile
1916
+ // would make resolveAuth reject a fresh machine before its ASSETHUB_API_KEY fallback.
1917
+ const base = { ...flags };
1918
+ const chosenProfile = hasFlag(flags, 'profile') ? requireFlag(flags, 'profile') : config.defaultProfile;
1919
+ if (chosenProfile)
1920
+ base.profile = chosenProfile;
1921
+ else
1922
+ delete base.profile;
1923
+ delete base.workspace;
1924
+ const apiKeyStdin = hasFlag(flags, 'api-key-stdin');
1925
+ const flagKey = hasFlag(flags, 'api-key') ? requireFlag(flags, 'api-key') : undefined;
1926
+ const envKey = env.ASSETHUB_API_KEY?.trim() || undefined;
1927
+ const quiet = async (fn) => {
1928
+ printSuppressed = true;
1929
+ try {
1930
+ return await fn();
1931
+ }
1932
+ finally {
1933
+ printSuppressed = false;
1934
+ }
1935
+ };
1936
+ const result = await runSetup({
1937
+ client,
1938
+ workspace: getFlag(flags, 'workspace'),
1939
+ apiKeyStdin,
1940
+ apiKey: flagKey ?? envKey,
1941
+ apiKeySource: flagKey ? 'flag' : envKey ? 'env' : undefined,
1942
+ noHook: hasFlag(flags, 'no-hook'),
1943
+ noAppEnv: hasFlag(flags, 'no-app-env'),
1944
+ saveSessions: hasFlag(flags, 'save-sessions'),
1945
+ dryRun: hasFlag(flags, 'dry-run'),
1946
+ yes: hasFlag(flags, 'yes'),
1947
+ printEnv: hasFlag(flags, 'print-env'),
1948
+ }, {
1949
+ ...defaultFileDeps(),
1950
+ launchAgentProgram: launchAgentProgram(process.execPath, realpathSync(argv[1] ?? fileURLToPath(import.meta.url)), ['env', 'load', ...(hasFlag(flags, 'profile') ? ['--profile', requireFlag(flags, 'profile')] : [])]),
1951
+ interactive: Boolean(stdin.isTTY && stderr.isTTY) && !apiKeyStdin,
1952
+ installHooks,
1953
+ log: line => {
1954
+ stderr.write(`${line}\n`);
1955
+ },
1956
+ hasWorkingKey: async () => {
1957
+ try {
1958
+ const auth = await resolveAuth(base);
1959
+ if (auth.source !== 'profile')
1960
+ return false;
1961
+ if (await personalAccount(auth))
1962
+ return true;
1963
+ await createAssetHubClient({ apiKey: auth.apiKey, baseUrl: auth.baseUrl }).v2.listModels();
1964
+ return true;
1965
+ }
1966
+ catch {
1967
+ return false;
1968
+ }
1969
+ },
1970
+ promptApiKey: () => promptHidden('AssetHub API key (input hidden): '),
1971
+ login: ({ apiKey }) => quiet(() => {
1972
+ const loginFlags = { ...base, profile: typeof base.profile === 'string' ? base.profile : defaultProfileName };
1973
+ if (apiKey) {
1974
+ delete loginFlags['api-key-stdin'];
1975
+ loginFlags['api-key'] = apiKey;
1976
+ }
1977
+ return commandAuth('login', loginFlags);
1978
+ }),
1979
+ resolveAuth: async () => {
1980
+ const auth = await resolveAuth(base);
1981
+ return { apiKey: auth.apiKey, baseUrl: auth.baseUrl, profile: auth.profile, workspaceId: auth.workspaceId };
1982
+ },
1983
+ discoverWorkspace: async (auth) => {
1984
+ try {
1985
+ const capabilities = await createAssetHubClient({ apiKey: auth.apiKey, baseUrl: auth.baseUrl }).v2.getCapabilities();
1986
+ return typeof capabilities.ownerId === 'string' && capabilities.ownerId ? capabilities.ownerId : undefined;
1987
+ }
1988
+ catch {
1989
+ return undefined;
1990
+ }
1991
+ },
1992
+ useWorkspace: async (id) => {
1993
+ await quiet(() => commandWorkspace('use', ['workspace', 'use', id], base));
1994
+ // A non-personal `workspace use` stores its key under a new default profile.
1995
+ const saved = await readAuthConfig(getConfigPath(flags));
1996
+ if (saved.defaultProfile)
1997
+ base.profile = saved.defaultProfile;
1998
+ },
1999
+ listWorkspaces: async () => (await (await workspaceAuth(base, true)).client.list()).workspaces.map(item => ({ id: item.id, name: item.name })),
2000
+ pickWorkspace: pickFromList,
2001
+ confirm: confirmYes,
2002
+ confirmOptIn: confirmNo,
2003
+ canSaveSessions: auth => canUploadSessions(auth),
2004
+ diagnose: () => diagnose({
2005
+ resolveAuth: () => resolveAuth(base),
2006
+ includeMcp: true,
2007
+ timeoutMs: parsePositiveIntegerFlag(flags, 'timeout-ms', 15000),
2008
+ }),
2009
+ });
2010
+ if (hasFlag(flags, 'json'))
2011
+ print(result);
2012
+ else {
2013
+ stdout.write(formatSetupSummary(result));
2014
+ if (result.exportLine)
2015
+ stdout.write(`${result.exportLine}\n`);
2016
+ }
2017
+ if (!result.ok)
2018
+ process.exitCode = 2;
2019
+ };
2020
+ // `env load` is what the macOS login agent installed by `setup` runs: it copies
2021
+ // the saved key into launchd's user environment so Claude and Codex, started
2022
+ // from the Dock, can expand ${ASSETHUB_API_KEY} in their MCP config.
2023
+ const commandEnv = async (subcommand, flags) => {
2024
+ if (subcommand !== 'load')
2025
+ throw new Error('Unknown env command. Use "env load".');
2026
+ if (process.platform !== 'darwin')
2027
+ throw new Error('env load is only needed on macOS.');
2028
+ const auth = await resolveAuth(flags);
2029
+ const loaded = await loadKeyIntoLaunchd(auth.apiKey, runProcess);
2030
+ if (!loaded.ok)
2031
+ throw new Error(loaded.detail);
2032
+ stderr.write(`ASSETHUB_API_KEY loaded for apps (profile ${auth.profile}).\n`);
2033
+ };
1840
2034
  const commandFiles = async (subcommand, positionals, ctx) => {
1841
2035
  const mediaType = parseMediaType(requireFlag(ctx.flags, 'media-type'));
1842
2036
  if (subcommand === 'import') {
@@ -1874,11 +2068,32 @@ const executionOptions = (flags) => ({
1874
2068
  }
1875
2069
  : undefined,
1876
2070
  });
2071
+ /** How long `--wait` waits by default: the selected agent's run length. */
2072
+ const defaultWaitMs = (flags) => {
2073
+ const extractor = getPartExtractorFlag(flags);
2074
+ const option = extractor == null
2075
+ ? undefined
2076
+ : partExtractorByInput.get(normalizePartExtractorName(extractor));
2077
+ return option?.apiValue === 'ah_agent_graph_harpy_assembly_v2'
2078
+ ? harpyAssemblyV2WaitMs
2079
+ : 900000;
2080
+ };
1877
2081
  const watchOptions = (flags) => ({
1878
2082
  intervalMs: parsePositiveIntegerFlag(flags, 'interval-ms', 5000),
1879
- timeoutMs: parsePositiveIntegerFlag(flags, 'timeout-ms', 900000),
1880
- onProgress: (run) => stderr.write(`[run] ${run.runId} ${run.status}\n`),
2083
+ timeoutMs: parsePositiveIntegerFlag(flags, 'timeout-ms', defaultWaitMs(flags)),
2084
+ onProgress: progressPrinter(run => `[run] ${run.runId} ${run.status}${run.progress ? ` · ${runProgressLine(run)}` : ''}`),
1881
2085
  });
2086
+ /** Prints a run's line only when it changed since the last poll. */
2087
+ const progressPrinter = (line) => {
2088
+ let last = '';
2089
+ return (run) => {
2090
+ const next = line(run);
2091
+ if (next === last)
2092
+ return;
2093
+ last = next;
2094
+ stderr.write(`${next}\n`);
2095
+ };
2096
+ };
1882
2097
  const readJsonArgument = async (value, flag) => {
1883
2098
  const text = value === '@-'
1884
2099
  ? await readStdin()
@@ -2049,14 +2264,7 @@ const commandNodeMesh = async (ctx, nodeId) => {
2049
2264
  throw new Error('Canvas node is unavailable; read canvas nodes again');
2050
2265
  const finishedOrBusy = (status) => ['complete', 'completed', 'generating'].includes(status ?? '');
2051
2266
  if (target.type === 'production-3d') {
2052
- if (!target.actions.includes('mesh.generate'))
2053
- throw new Error('Mesh generation is unavailable for this node');
2054
- const children = items.filter(item => item.sourceNodeId === nodeId &&
2055
- ['mesh-gen', 'preview-asset', 'part-group'].includes(item.type));
2056
- if (!children.length)
2057
- throw new Error('Production node has no saved part nodes to generate');
2058
- const pending = children.filter(item => item.actions.includes('mesh.generate') &&
2059
- !finishedOrBusy(item.meshStatus));
2267
+ const plan = planProductionNodeMeshes(items, nodeId);
2060
2268
  const batchId = operationId ?? randomUUID();
2061
2269
  stderr.write(`[nodes] batch ${batchId}\n`);
2062
2270
  await printNodeMeshBatch(ctx, await executeNodeMeshBatch(session, {
@@ -2064,10 +2272,7 @@ const commandNodeMesh = async (ctx, nodeId) => {
2064
2272
  operationId: batchId,
2065
2273
  nodeId,
2066
2274
  body,
2067
- children: pending.map(item => item.nodeId),
2068
- skipped: children
2069
- .filter(item => !pending.includes(item))
2070
- .map(({ nodeId, meshStatus }) => ({ nodeId, meshStatus })),
2275
+ ...plan,
2071
2276
  }));
2072
2277
  return;
2073
2278
  }
@@ -2163,6 +2368,49 @@ const meshRefinementModes = [
2163
2368
  'blender',
2164
2369
  'codex',
2165
2370
  ];
2371
+ const COMPOSER_V6_MODEL = 'part_composer_v6_auto_assemble';
2372
+ const COMPOSER_SKILL_ASSEMBLY_MODEL = 'part_composer_v5_1_skill_assembly';
2373
+ /** Same text the canvas node sends, so CLI and canvas runs read alike. */
2374
+ const COMPOSER_SKILL_ASSEMBLY_INSTRUCTION = 'Assemble these parts into one character with the Character Assembly skill.';
2375
+ const IDENTITY_TRANSFORM = [0, 0, 0, 0, 0, 0, 1, 1, 1, 1];
2376
+ const composerModelAliases = {
2377
+ v6: COMPOSER_V6_MODEL,
2378
+ 'auto-assemble': COMPOSER_V6_MODEL,
2379
+ 'v5.1': COMPOSER_SKILL_ASSEMBLY_MODEL,
2380
+ 'skill-assembly': COMPOSER_SKILL_ASSEMBLY_MODEL,
2381
+ };
2382
+ /** Short names for the picker labels; anything else goes to the server as given. */
2383
+ const resolveComposerModel = (value) => value == null ? undefined : (composerModelAliases[value.toLowerCase()] ?? value);
2384
+ /**
2385
+ * `--part <mesh-id>[=<part-image-id>]`: the image is the drawing the mesh was
2386
+ * generated from. `<mesh-id>:<part-image-id>` (CLI 0.1.27+) is accepted too.
2387
+ */
2388
+ const composerPartFlag = (value) => {
2389
+ const raw = assertFlagHasValue(value, '--part <mesh-asset-id>[=<part-image-id>]');
2390
+ const separator = raw.includes('=') ? '=' : ':';
2391
+ const index = raw.indexOf(separator);
2392
+ const assetId = index === -1 ? raw : raw.slice(0, index);
2393
+ const partImageAssetId = index === -1 ? undefined : raw.slice(index + 1);
2394
+ if (!assetId || partImageAssetId === '')
2395
+ throw new Error('--part must be <mesh-asset-id> or <mesh-asset-id>=<part-image-id>');
2396
+ return { assetId, ...(partImageAssetId ? { partImageAssetId } : {}) };
2397
+ };
2398
+ /** V6 waits up to 81 min on ml and V5.1's sandbox up to 2 h; 15 min cannot see either finish. */
2399
+ const defaultComposerWaitTimeout = (flags, timeoutMs) => {
2400
+ if (!hasFlag(flags, 'timeout-ms'))
2401
+ flags['timeout-ms'] = String(timeoutMs);
2402
+ };
2403
+ /** Discovery is the only model manifest; qualify duplicate IDs by their advertised provider. */
2404
+ const selectComposerAgentModel = (models, value) => {
2405
+ if (!value)
2406
+ return undefined;
2407
+ const matches = models?.filter(model => value === `${model.provider}:${model.id}` || value === model.id) ?? [];
2408
+ if (matches.length > 1)
2409
+ throw new Error(`Ambiguous Composer agent model ${value}; use --agent-model ${matches.map(model => `${model.provider}:${model.id}`).join(' or ')}`);
2410
+ if (!matches[0])
2411
+ throw new Error('Composer agent model is not available; inspect composer refine --list-modes');
2412
+ return matches[0];
2413
+ };
2166
2414
  const refinementTransforms = (value, flag) => {
2167
2415
  if (value == null || typeof value !== 'object' || Array.isArray(value))
2168
2416
  throw new Error(`${flag} must be an object keyed by part asset ID`);
@@ -2196,7 +2444,8 @@ const refinementModeInfo = (capabilities, mode) => {
2196
2444
  const refinementContinuationFields = [
2197
2445
  'assemblyPolicy', 'dressingGeneration', 'effort',
2198
2446
  'geometryBackend', 'referenceMode', 'calibration', 'referenceViews',
2199
- 'geometrySources', 'reviewContext', 'background',
2447
+ 'geometrySources', 'reviewContext', 'background', 'skillSelection',
2448
+ 'skillAssembly', 'skillAssemblyReasoning',
2200
2449
  ];
2201
2450
  /** Reuse the frozen sources and latest editable scene, never replay server-only receipt fields. */
2202
2451
  const composerInputFromRun = (previous, operation) => {
@@ -2248,6 +2497,15 @@ const composerInputFromRun = (previous, operation) => {
2248
2497
  if (previous.refinement?.[key] != null)
2249
2498
  input[key] = previous.refinement[key];
2250
2499
  }
2500
+ const generation = input.dressingGeneration;
2501
+ if (generation && Array.isArray(input.parts)) {
2502
+ const sources = input.geometrySources;
2503
+ input.dressingGeneration = { ...generation, sourceImageAssetIds: Object.fromEntries(input.parts.flatMap((part) => {
2504
+ const image = generation.sourceImageAssetIds[part.assetId] ??
2505
+ generation.sourceImageAssetIds[part.sourceAssetId ?? sources?.[part.assetId] ?? ''];
2506
+ return typeof image === 'string' ? [[part.assetId, image]] : [];
2507
+ })) };
2508
+ }
2251
2509
  const reviewContext = previous.refinement?.report?.reviewContext;
2252
2510
  if (reviewContext != null)
2253
2511
  input.reviewContext = reviewContext;
@@ -2260,7 +2518,7 @@ const composerInputFromRun = (previous, operation) => {
2260
2518
  }
2261
2519
  return input;
2262
2520
  };
2263
- const commandComposerRefine = async (ctx) => {
2521
+ const commandComposerRefine = async (ctx, options = {}) => {
2264
2522
  const capabilities = await ctx.client.v2.getMeshRefinementModes();
2265
2523
  if (hasFlag(ctx.flags, 'list-modes')) {
2266
2524
  print(capabilities);
@@ -2285,10 +2543,32 @@ const commandComposerRefine = async (ctx) => {
2285
2543
  : previous
2286
2544
  ? composerInputFromRun(previous, 'mesh.refine')
2287
2545
  : {
2288
- parts: parts.map(parsePartFlagValue),
2546
+ // Refinement reads meshes and the concept only; a paired part image is not sent.
2547
+ parts: parts.map(value => ({ assetId: composerPartFlag(value).assetId })),
2289
2548
  fullBodyImageAssetId: requireFlag(ctx.flags, 'reference'),
2290
2549
  };
2291
2550
  delete input.executionContext;
2551
+ const skillAssembly = options.skillAssembly ||
2552
+ hasFlag(ctx.flags, 'skill-assembly') ||
2553
+ input.skillAssembly === 'assemble-character';
2554
+ const reasoning = parseEnumFlag(ctx.flags, 'reasoning', [
2555
+ 'low',
2556
+ 'medium',
2557
+ 'high',
2558
+ ]);
2559
+ if (reasoning && !skillAssembly)
2560
+ throw new Error('--reasoning applies only to Skill assembly (V5.1)');
2561
+ if (skillAssembly) {
2562
+ // The bundled skill runs alone in the codex sandbox; the server refuses anything else.
2563
+ input.skillAssembly = 'assemble-character';
2564
+ if (reasoning)
2565
+ input.skillAssemblyReasoning = reasoning;
2566
+ input.mode = 'codex';
2567
+ input.instruction ??= COMPOSER_SKILL_ASSEMBLY_INSTRUCTION;
2568
+ if (previous?.operation === 'mesh.compose')
2569
+ delete input.agentRuntime;
2570
+ defaultComposerWaitTimeout(ctx.flags, 125 * 60_000);
2571
+ }
2292
2572
  if (!Array.isArray(input.parts) || input.parts.length === 0)
2293
2573
  throw new Error('Composer refinement requires parts[]');
2294
2574
  const refinementParts = input.parts.map((part, index) => {
@@ -2310,6 +2590,8 @@ const commandComposerRefine = async (ctx) => {
2310
2590
  capabilities.defaultMode;
2311
2591
  if (!meshRefinementModes.includes(mode))
2312
2592
  throw new Error(`--mode must be one of: ${meshRefinementModes.join(', ')}`);
2593
+ if (skillAssembly && mode !== 'codex')
2594
+ throw new Error('Skill assembly (V5.1) runs in codex mode only');
2313
2595
  if (mode !== 'codex' && (input.geometryBackend || input.referenceMode === 'all_angles' || input.background))
2314
2596
  throw new Error('Saved Blender-backend, all-angle or background settings require codex mode; use --input-json with settings matching the new mode');
2315
2597
  const modeInfo = refinementModeInfo(capabilities, mode);
@@ -2341,7 +2623,9 @@ const commandComposerRefine = async (ctx) => {
2341
2623
  const transformsFlag = getFlag(ctx.flags, 'transforms-json');
2342
2624
  const transforms = transformsFlag
2343
2625
  ? refinementTransforms(await readJsonArgument(transformsFlag, '--transforms-json'), '--transforms-json')
2344
- : refinementTransforms(input.transforms, 'refinement transforms');
2626
+ : skillAssembly && input.transforms == null
2627
+ ? Object.fromEntries(refinementParts.map(part => [part.assetId, IDENTITY_TRANSFORM]))
2628
+ : refinementTransforms(input.transforms, 'refinement transforms');
2345
2629
  const referenceTransform = input.referenceTransform == null
2346
2630
  ? undefined
2347
2631
  : refinementTransform(input.referenceTransform, 'referenceTransform');
@@ -2354,10 +2638,25 @@ const commandComposerRefine = async (ctx) => {
2354
2638
  canvasId,
2355
2639
  operation: 'mesh.refine',
2356
2640
  });
2641
+ const agentModelFlag = getFlag(ctx.flags, 'agent-model');
2642
+ if (skillAssembly &&
2643
+ agentModelFlag?.includes(':') &&
2644
+ !agentModelFlag.startsWith('agents-api:'))
2645
+ throw new Error('Skill assembly runs on an Agents API model');
2646
+ const modelOption = selectComposerAgentModel(skillAssembly
2647
+ ? capabilities.agentModels?.filter(model => model.provider === 'agents-api')
2648
+ : capabilities.agentModels, agentModelFlag);
2357
2649
  const body = {
2358
2650
  ...Object.fromEntries(refinementContinuationFields
2359
2651
  .filter(key => input[key] !== undefined)
2360
2652
  .map(key => [key, input[key]])),
2653
+ ...(modelOption
2654
+ ? { agentRuntime: { provider: modelOption.provider, model: modelOption.id } }
2655
+ : input.agentRuntime
2656
+ ? {
2657
+ agentRuntime: input.agentRuntime,
2658
+ }
2659
+ : {}),
2361
2660
  parts: refinementParts,
2362
2661
  fullBodyImageAssetId: input.fullBodyImageAssetId,
2363
2662
  transforms,
@@ -2366,15 +2665,18 @@ const commandComposerRefine = async (ctx) => {
2366
2665
  ...(effort ? { effort: effort } : {}),
2367
2666
  instruction,
2368
2667
  ...(maxRounds != null ? { maxRounds } : {}),
2369
- ...(input.agentRuntime
2370
- ? { agentRuntime: input.agentRuntime }
2371
- : {}),
2372
2668
  };
2373
2669
  await printRecorded(ctx, await executeRecorded(session, 'mesh.refine', body, {
2374
2670
  ...executionOptions(ctx.flags),
2375
2671
  ...(previous ? { parentRunId: previous.runId } : {}),
2376
2672
  }));
2377
2673
  };
2674
+ /**
2675
+ * The Part Composer node's "Optimize output" presets: the share of geometry the full-resolution bake keeps
2676
+ * (textures become WebP). Assembly always uses its own shrunk parts; this only sizes the delivered mesh.
2677
+ */
2678
+ const COMPOSER_OPTIMIZE_RATIOS = { light: 0.75, medium: 0.5, heavy: 0.25 };
2679
+ const composerOptimizePresets = ['off', 'light', 'medium', 'heavy'];
2378
2680
  const commandComposer = async (subcommand, ctx) => {
2379
2681
  if (subcommand === 'models') {
2380
2682
  print(await ctx.client.v2.getMeshComposers());
@@ -2385,8 +2687,29 @@ const commandComposer = async (subcommand, ctx) => {
2385
2687
  return;
2386
2688
  }
2387
2689
  if (subcommand !== 'run')
2388
- throw new Error('Use composer models|run');
2690
+ throw new Error('Use composer models|refine|run');
2691
+ const model = resolveComposerModel(getFlag(ctx.flags, 'model'));
2692
+ if (model === COMPOSER_SKILL_ASSEMBLY_MODEL) {
2693
+ // V5.1 is not a compose model: the server runs it as a codex refinement.
2694
+ if (hasFlag(ctx.flags, 'node'))
2695
+ throw new Error('Skill assembly (V5.1) takes --part and --reference, not --node');
2696
+ if (hasFlag(ctx.flags, 'mode'))
2697
+ throw new Error('Skill assembly (V5.1) has no quick/quality mode');
2698
+ if (hasFlag(ctx.flags, 'optimize'))
2699
+ throw new Error('--optimize applies to composition, not Skill assembly (V5.1)');
2700
+ await commandComposerRefine(ctx, { skillAssembly: true });
2701
+ return;
2702
+ }
2703
+ if (hasFlag(ctx.flags, 'reasoning'))
2704
+ throw new Error('--reasoning applies only to Skill assembly (V5.1)');
2705
+ const optimizeFlag = getFlag(ctx.flags, 'optimize');
2706
+ if (optimizeFlag != null &&
2707
+ !composerOptimizePresets.includes(optimizeFlag))
2708
+ throw new Error('--optimize must be off, light, medium or heavy');
2709
+ const optimize = optimizeFlag;
2389
2710
  const nodeId = canvasNodeFlag(ctx.flags);
2711
+ if (optimize && nodeId)
2712
+ throw new Error("--optimize is not available with --node; set the node's Optimize output instead");
2390
2713
  const fromRun = getFlag(ctx.flags, 'from-run');
2391
2714
  const json = getFlag(ctx.flags, 'input-json');
2392
2715
  const parts = getFlagValues(ctx.flags, 'part');
@@ -2403,7 +2726,7 @@ const commandComposer = async (subcommand, ctx) => {
2403
2726
  : json
2404
2727
  ? await readJsonArgument(json, '--input-json')
2405
2728
  : {
2406
- parts: parts.map(parsePartFlagValue),
2729
+ parts: parts.map(composerPartFlag),
2407
2730
  fullBodyImageAssetId: requireFlag(ctx.flags, 'reference'),
2408
2731
  };
2409
2732
  delete input.executionContext;
@@ -2417,9 +2740,34 @@ const commandComposer = async (subcommand, ctx) => {
2417
2740
  throw new Error('--mode must be quick or quality');
2418
2741
  if (mode)
2419
2742
  input.mode = mode;
2420
- const model = getFlag(ctx.flags, 'model');
2421
2743
  if (model)
2422
2744
  input.agentVersion = model;
2745
+ if (input.agentVersion === COMPOSER_V6_MODEL) {
2746
+ // V6 re-plans every part from its own drawing; ml fails the run without one.
2747
+ const missing = (Array.isArray(input.parts) ? input.parts : [])
2748
+ .filter((part) => !part.partImageAssetId && !part.partTaskId)
2749
+ .map((part) => part.assetId);
2750
+ if (missing.length)
2751
+ throw new Error(`Auto assemble (V6) needs each part's drawing; pass --part <mesh-id>=<part-image-id> for: ${missing.join(', ')}`);
2752
+ defaultComposerWaitTimeout(ctx.flags, 95 * 60_000);
2753
+ }
2754
+ const agentModel = getFlag(ctx.flags, 'agent-model');
2755
+ if (agentModel) {
2756
+ const capabilities = await ctx.client.v2.getMeshRefinementModes();
2757
+ const selected = selectComposerAgentModel(capabilities.agentModels, agentModel);
2758
+ input.agentRuntime = { provider: selected.provider, model: selected.id };
2759
+ }
2760
+ if (optimize) {
2761
+ // --from-run rebuilds its input without the base body, so the previous run is asked too.
2762
+ const previousSource = previous?.resolvedInput ?? previous?.input ?? previous?.requestedInput;
2763
+ if (optimize !== 'off' &&
2764
+ (input.baseBodyAssetId || previousSource?.baseBodyAssetId))
2765
+ throw new Error('--optimize cannot be combined with a base body (baseBodyAssetId)');
2766
+ if (optimize === 'off')
2767
+ delete input.decimateRatio;
2768
+ else
2769
+ input.decimateRatio = COMPOSER_OPTIMIZE_RATIOS[optimize];
2770
+ }
2423
2771
  const name = getFlag(ctx.flags, 'name');
2424
2772
  if (name)
2425
2773
  input.projectName = name;
@@ -3276,38 +3624,211 @@ const commandProductionRunOneShot = async (ctx) => {
3276
3624
  })));
3277
3625
  process.exitCode = executionExitCode(finalExecution);
3278
3626
  };
3627
+ const productionBatchImageExtensions = new Set(['.png', '.jpg', '.jpeg', '.webp']);
3628
+ const productionBatchUnsupportedSources = [
3629
+ 'image-url', 'url', 'source-resource-id', 'resource-id', 'file-ref-json',
3630
+ 'stdin', 'stdin-base64', 'stdin-data-uri', 'stdin-json', 'source-json',
3631
+ 'data-uri', 'clipboard', 'upload-id',
3632
+ ];
3633
+ /** Images in command order: --files-dir (by name), --file, --source-id, --source-url. */
3634
+ const productionBatchSources = async (flags) => {
3635
+ const unsupported = productionBatchUnsupportedSources.find(name => hasFlag(flags, name));
3636
+ if (unsupported)
3637
+ throw new Error(`production batch does not take --${unsupported}; pass images with --file, --files-dir, --source-id or --source-url`);
3638
+ const sources = [];
3639
+ for (const dir of getFlagValues(flags, 'files-dir')) {
3640
+ const names = (await readdir(resolve(dir), { withFileTypes: true }))
3641
+ .filter(entry => entry.isFile() && productionBatchImageExtensions.has(extname(entry.name).toLowerCase()))
3642
+ .map(entry => entry.name)
3643
+ .sort();
3644
+ if (names.length === 0)
3645
+ throw new Error(`--files-dir ${dir} has no .png, .jpg, .jpeg or .webp images`);
3646
+ sources.push(...names.map(name => ({ flag: 'file', value: resolve(dir, name) })));
3647
+ }
3648
+ for (const flag of ['file', 'source-id', 'source-url'])
3649
+ for (const value of getFlagValues(flags, flag))
3650
+ sources.push({ flag, value: flag === 'file' ? resolve(value) : value });
3651
+ if (sources.length === 0)
3652
+ throw new Error('production batch needs images: --file <path>... , --files-dir <dir>, --source-id <id>... or --source-url <url>...');
3653
+ return sources;
3654
+ };
3655
+ /**
3656
+ * One `production analyze` per image (× --repeat) with the same agent, on one
3657
+ * canvas. The batch is pinned locally before the first paid request, so
3658
+ * rerunning with its --operation-id resumes it and never starts an item twice.
3659
+ */
3660
+ const commandProductionBatch = async (ctx) => {
3661
+ const sources = await productionBatchSources(ctx.flags);
3662
+ const repeat = parsePositiveIntegerFlag(ctx.flags, 'repeat', 1);
3663
+ const entries = expandProductionBatch(sources, repeat);
3664
+ const labels = productionBatchLabels(sources);
3665
+ if (entries.length > 1 && !hasFlag(ctx.flags, 'yes'))
3666
+ throw new Error(`production batch would start ${entries.length} paid runs (${sources.length} image(s) × ${repeat}). Pass --yes to start them.`);
3667
+ const { options, projectContext } = await resolveProductionAnalyzeOptions(ctx);
3668
+ const { agent, parentRunId } = executionOptions(ctx.flags);
3669
+ const operationId = getFlag(ctx.flags, 'operation-id') ?? randomUUID();
3670
+ const state = stateOptions(ctx);
3671
+ const saved = await readProductionBatch(state.stateDir, operationId);
3672
+ const requestedCanvasId = selectedCanvasId(ctx.flags) ?? projectContext?.canvasId;
3673
+ if (saved && requestedCanvasId != null && requestedCanvasId !== saved.canvasId)
3674
+ throw new Error(`Batch ${operationId} runs on canvas ${saved.canvasId}; drop --canvas or pass that canvas`);
3675
+ const session = await productionSession(ctx, 'production.analyze', saved?.canvasId ?? requestedCanvasId);
3676
+ stderr.write(`[batch] ${operationId} · ${entries.length} run(s) on canvas ${session.canvas.id}. Rerun with --operation-id ${operationId} to resume.\n`);
3677
+ const wait = hasFlag(ctx.flags, 'wait');
3678
+ const concurrency = parsePositiveIntegerFlag(ctx.flags, 'concurrency', 2);
3679
+ const printItem = (item) => stderr.write(`[batch] ${item.key} ${item.label} ${item.status}${item.runId ? ` ${item.runId}` : ''}${item.execution?.progress ? ` · ${runProgressLine(item.execution)}` : ''}${item.message ? ` · ${item.message}` : ''}\n`);
3680
+ const result = await runProductionBatch(session, {
3681
+ operationId,
3682
+ request: { sources, repeat, options, agent, parentRunId },
3683
+ agent,
3684
+ parentRunId,
3685
+ buildItems: async () => {
3686
+ const resolved = [];
3687
+ for (const source of sources) {
3688
+ const flags = { ...ctx.flags };
3689
+ for (const flag of ['file', 'files-dir', 'source-id', 'source-url'])
3690
+ delete flags[flag];
3691
+ resolved.push(await resolveProductionAnalyzeSource({ ...ctx, flags: { ...flags, [source.flag]: source.value } }));
3692
+ }
3693
+ return entries.map((entry) => {
3694
+ const label = labels[entry.index];
3695
+ const name = `${options.name ? `${options.name} ` : ''}${label}${repeat > 1 ? ` #${entry.repeat}` : ''}`;
3696
+ return {
3697
+ key: entry.key,
3698
+ label,
3699
+ imageIndex: entry.index,
3700
+ repeat: entry.repeat,
3701
+ attempt: 0,
3702
+ body: { ...resolved[entry.index], ...options, name },
3703
+ };
3704
+ });
3705
+ },
3706
+ ...(wait
3707
+ ? {
3708
+ wait: {
3709
+ concurrency,
3710
+ intervalMs: parsePositiveIntegerFlag(ctx.flags, 'interval-ms', 5000),
3711
+ timeoutMs: parsePositiveIntegerFlag(ctx.flags, 'timeout-ms', defaultWaitMs(ctx.flags) * Math.ceil(entries.length / concurrency)),
3712
+ onProgress: printItem,
3713
+ },
3714
+ }
3715
+ : {}),
3716
+ });
3717
+ if (!wait)
3718
+ result.items.forEach(printItem);
3719
+ const downloads = [];
3720
+ if (hasFlag(ctx.flags, 'download')) {
3721
+ const outDir = getFlag(ctx.flags, 'out-dir') ?? getFlag(ctx.flags, 'out');
3722
+ for (const item of result.items) {
3723
+ if (!item.execution?.outputs.length)
3724
+ continue;
3725
+ const downloaded = await maybeDownloadUrls({ ...ctx.flags, 'out-dir': join(outDir ?? '', item.label, `run-${item.repeat}`) }, item.execution.outputs);
3726
+ if (downloaded)
3727
+ downloads.push({ key: item.key, ...downloaded });
3728
+ }
3729
+ }
3730
+ const count = (status) => result.items.filter(item => item.status === status).length;
3731
+ print({
3732
+ batchOperationId: result.batchOperationId,
3733
+ canvasId: result.canvasId,
3734
+ agentVersion: options.agentVersion,
3735
+ items: result.items,
3736
+ counts: {
3737
+ dispatched: count('dispatched'),
3738
+ deferred: count('deferred'),
3739
+ pending: count('pending'),
3740
+ failed: count('failed'),
3741
+ finished: result.items.filter(item => item.execution && !['dispatched', 'deferred', 'failed', 'pending'].includes(item.status)).length,
3742
+ },
3743
+ ...(result.timedOut ? { timedOut: true } : {}),
3744
+ ...(downloads.length ? { downloads } : {}),
3745
+ });
3746
+ process.exitCode = Math.max(0, ...result.items.map(item => item.status === 'failed'
3747
+ ? 1
3748
+ : item.status === 'deferred' || item.status === 'pending'
3749
+ ? 3
3750
+ : wait && item.execution && item.status !== 'dispatched'
3751
+ ? executionExitCode(item.execution)
3752
+ : wait
3753
+ ? 3
3754
+ : 0));
3755
+ };
3756
+ /**
3757
+ * Everything `production analyze` sends besides the image source; shared with
3758
+ * `production batch` so a batch item is the same request.
3759
+ */
3760
+ const resolveProductionAnalyzeOptions = async (ctx) => {
3761
+ const agentVersion = resolvePartExtractorForAnalyze(ctx.flags);
3762
+ const baseBodyAssetId = resolveProductionBaseBody(ctx.flags);
3763
+ const skillPlannerModel = resolveSkillPlannerModel(ctx.flags);
3764
+ const autoRepair = parseBooleanFlag(ctx.flags, 'auto-repair');
3765
+ const skillSelection = resolveProductionSkillSelection(ctx.flags, agentVersion);
3766
+ if (skillPlannerModel !== undefined && !['auto', 'manual'].includes(skillSelection?.mode ?? ''))
3767
+ throw new Error('--skill-planner-model requires --skill-mode auto or manual');
3768
+ const projectContext = hasFlag(ctx.flags, 'context') || hasFlag(ctx.flags, 'context-version')
3769
+ ? {
3770
+ canvasId: Number(requireFlag(ctx.flags, 'context')),
3771
+ version: Number(requireFlag(ctx.flags, 'context-version')),
3772
+ }
3773
+ : undefined;
3774
+ if (projectContext) {
3775
+ if (Object.values(projectContext).some(value => !Number.isSafeInteger(value) || value <= 0))
3776
+ throw new Error('--context and --context-version must be positive integers');
3777
+ const canvasId = selectedCanvasId(ctx.flags);
3778
+ if (canvasId != null && canvasId !== projectContext.canvasId)
3779
+ throw new Error('--context must match --canvas');
3780
+ }
3781
+ const pipelineDepth = getFlag(ctx.flags, 'pipeline-depth');
3782
+ if (pipelineDepth !== undefined &&
3783
+ pipelineDepth !== 'parts' &&
3784
+ pipelineDepth !== 'mesh' &&
3785
+ pipelineDepth !== 'composition')
3786
+ throw new Error('--pipeline-depth must be parts, mesh or composition');
3787
+ const assemblyPolicy = getFlag(ctx.flags, 'assembly-policy');
3788
+ const meshGenerationInput = getFlag(ctx.flags, 'mesh-generation-json');
3789
+ const meshGeneration = meshGenerationInput === undefined
3790
+ ? undefined
3791
+ : await readJsonArgument(meshGenerationInput, '--mesh-generation-json');
3792
+ if (assemblyPolicy !== undefined &&
3793
+ assemblyPolicy !== 'concept-to-character-v1')
3794
+ throw new Error('--assembly-policy must be concept-to-character-v1');
3795
+ if (assemblyPolicy && pipelineDepth !== 'composition')
3796
+ throw new Error('--assembly-policy requires --pipeline-depth composition');
3797
+ return {
3798
+ projectContext,
3799
+ options: {
3800
+ agentVersion,
3801
+ name: getFlag(ctx.flags, 'name'),
3802
+ ...(baseBodyAssetId === undefined ? {} : { baseBodyAssetId }),
3803
+ ...(skillPlannerModel === undefined ? {} : { skillPlannerModel }),
3804
+ ...(autoRepair === undefined ? {} : { autoRepair }),
3805
+ ...(skillSelection == null ? {} : { skillSelection }),
3806
+ ...(projectContext == null ? {} : { projectContext }),
3807
+ ...(pipelineDepth === undefined ? {} : { pipelineDepth }),
3808
+ ...(assemblyPolicy === undefined ? {} : { assemblyPolicy }),
3809
+ ...(meshGeneration === undefined ? {} : { meshGeneration }),
3810
+ },
3811
+ };
3812
+ };
3279
3813
  const commandProduction = async (subcommand, positionals, ctx) => {
3280
3814
  if (subcommand === 'agents') {
3281
3815
  print(sanitizeProductionOutput(await ctx.client.v1.getProductionAgents()));
3282
3816
  return;
3283
3817
  }
3284
3818
  if (subcommand === 'analyze') {
3285
- const agentVersion = resolvePartExtractorForAnalyze(ctx.flags);
3286
- const skillSelection = resolveProductionSkillSelection(ctx.flags, agentVersion);
3287
- const projectContext = hasFlag(ctx.flags, 'context') || hasFlag(ctx.flags, 'context-version')
3288
- ? {
3289
- canvasId: Number(requireFlag(ctx.flags, 'context')),
3290
- version: Number(requireFlag(ctx.flags, 'context-version')),
3291
- }
3292
- : undefined;
3293
- if (projectContext) {
3294
- if (Object.values(projectContext).some(value => !Number.isSafeInteger(value) || value <= 0))
3295
- throw new Error('--context and --context-version must be positive integers');
3296
- const canvasId = selectedCanvasId(ctx.flags);
3297
- if (canvasId != null && canvasId !== projectContext.canvasId)
3298
- throw new Error('--context must match --canvas');
3299
- }
3819
+ const { options, projectContext } = await resolveProductionAnalyzeOptions(ctx);
3300
3820
  const session = await productionSession(ctx, 'production.analyze', projectContext?.canvasId);
3301
3821
  const analyzed = await executeRecorded(session, 'production.analyze', {
3302
3822
  ...(await resolveProductionAnalyzeSource(ctx)),
3303
- agentVersion,
3304
- name: getFlag(ctx.flags, 'name'),
3305
- ...(skillSelection == null ? {} : { skillSelection }),
3306
- ...(projectContext == null ? {} : { projectContext }),
3823
+ ...options,
3307
3824
  }, executionOptions(ctx.flags));
3308
3825
  await printRecorded(ctx, analyzed);
3309
3826
  return;
3310
3827
  }
3828
+ if (subcommand === 'batch') {
3829
+ await commandProductionBatch(ctx);
3830
+ return;
3831
+ }
3311
3832
  if (subcommand === 'automation') {
3312
3833
  const session = await productionSession(ctx, 'production.automation');
3313
3834
  const input = getFlag(ctx.flags, 'input-json');
@@ -3430,6 +3951,39 @@ const commandProduction = async (subcommand, positionals, ctx) => {
3430
3951
  print(await withOptionalDownloads(ctx.flags, sanitizeProductionOutput({ started, status })));
3431
3952
  return;
3432
3953
  }
3954
+ if (subcommand === 'resume') {
3955
+ const id = requirePositional(positionals, 2, 'run-id|order-id');
3956
+ const expected = getFlag(ctx.flags, 'expected-resume-count');
3957
+ const expectedResumeCount = expected === undefined ? undefined : Number(expected);
3958
+ if (expectedResumeCount !== undefined &&
3959
+ (!Number.isSafeInteger(expectedResumeCount) || expectedResumeCount < 0))
3960
+ throw new Error('--expected-resume-count must be a whole number');
3961
+ // A run ID resumes that run's graph and can be followed with --wait; an
3962
+ // order ID resumes the graph only. Without --wait a failed run lookup
3963
+ // (not a run, or no access to runs) falls back to the order ID as before.
3964
+ const wait = hasFlag(ctx.flags, 'wait');
3965
+ const run = await ctx.client.v2.getRun(id).catch((error) => {
3966
+ if (!wait ||
3967
+ (error instanceof AssetHubApiError && error.status === 404))
3968
+ return undefined;
3969
+ throw error;
3970
+ });
3971
+ const resumed = await ctx.client.v2.resumeProduction(run?.orderIds[0] ?? id, expectedResumeCount === undefined ? {} : { expectedResumeCount });
3972
+ if (!wait) {
3973
+ print(run ? { ...resumed, runId: run.runId } : resumed);
3974
+ return;
3975
+ }
3976
+ if (!run)
3977
+ throw new Error('--wait follows a run: pass the run ID (runs get shows it) instead of the order ID.');
3978
+ const execution = await waitForExecution(ctx.client, run.runId, {
3979
+ ...watchOptions(ctx.flags),
3980
+ timeoutMs: parsePositiveIntegerFlag(ctx.flags, 'timeout-ms', harpyAssemblyV2WaitMs),
3981
+ });
3982
+ const downloads = await maybeDownloadUrls(ctx.flags, execution.outputs);
3983
+ print({ resume: resumed, execution, ...(downloads ? { downloads } : {}) });
3984
+ process.exitCode = executionExitCode(execution);
3985
+ return;
3986
+ }
3433
3987
  if (subcommand === 'interventions') {
3434
3988
  const orderId = requirePositional(positionals, 2, 'order-id');
3435
3989
  print(await ctx.client.v2.listInterventions(orderId));
@@ -3465,7 +4019,7 @@ const commandProduction = async (subcommand, positionals, ctx) => {
3465
4019
  print(await withOptionalDownloads(ctx.flags, sanitizeProductionOutput({ status })));
3466
4020
  return;
3467
4021
  }
3468
- throw new Error('Unknown production command. Use "production agents", "production analyze", "production automation", "production run", "production watch", "production status", "production execute", "production intervene", or "production interventions".');
4022
+ throw new Error('Unknown production command. Use "production agents", "production analyze", "production batch", "production automation", "production run", "production watch", "production status", "production execute", "production intervene", "production interventions", or "production resume".');
3469
4023
  };
3470
4024
  // Multi-phase commands may contain several receipts; only their durable outputs
3471
4025
  // are downloadable, never canvas links, source URLs or evaluation evidence URLs.
@@ -3491,8 +4045,17 @@ const isArtifactGraphPartSplit = (value, execution) => (value != null &&
3491
4045
  artifactGraphPartExtractorApiValues.has(String(execution.resolvedInput?.agentVersion));
3492
4046
  const commandParts = async (subcommand, ctx) => {
3493
4047
  const runPartSplit = async (source, operationId = getFlag(ctx.flags, 'operation-id') ?? randomUUID()) => {
4048
+ const autoRepair = parseBooleanFlag(ctx.flags, 'auto-repair');
4049
+ const baseBodyAssetId = resolveProductionBaseBody(ctx.flags);
4050
+ const skillPlannerModel = resolveSkillPlannerModel(ctx.flags);
3494
4051
  let orderId = source == null ? getFlag(ctx.flags, 'order-id') : undefined;
3495
4052
  if (orderId != null) {
4053
+ if (autoRepair !== undefined)
4054
+ throw new Error('--auto-repair can only be set when starting a new production run');
4055
+ if (baseBodyAssetId !== undefined)
4056
+ throw new Error('--base-body can only be set when starting a new production run');
4057
+ if (skillPlannerModel !== undefined)
4058
+ throw new Error('--skill-planner-model can only be set when starting a new production run');
3496
4059
  orderId = assertFlagHasValue(orderId, '--order-id <id>');
3497
4060
  if (hasFlag(ctx.flags, 'skill-mode') || hasFlag(ctx.flags, 'skill-id'))
3498
4061
  throw new Error('--skill-mode and --skill-id cannot be changed with --order-id; use runs resume to preserve the saved selection');
@@ -3501,9 +4064,11 @@ const commandParts = async (subcommand, ctx) => {
3501
4064
  const requestedAgentVersion = orderId == null || partExtractorFlag != null
3502
4065
  ? resolvePartExtractorApiValue(partExtractorFlag ?? defaultPartExtractorName)
3503
4066
  : undefined;
3504
- const skillSelection = requestedAgentVersion == null
3505
- ? undefined
3506
- : resolveProductionSkillSelection(ctx.flags, requestedAgentVersion);
4067
+ const skillSelection = orderId == null && requestedAgentVersion != null
4068
+ ? resolveProductionSkillSelection(ctx.flags, requestedAgentVersion)
4069
+ : undefined;
4070
+ if (skillPlannerModel !== undefined && !['auto', 'manual'].includes(skillSelection?.mode ?? ''))
4071
+ throw new Error('--skill-planner-model requires --skill-mode auto or manual');
3507
4072
  if (requestedAgentVersion != null &&
3508
4073
  artifactGraphPartExtractorApiValues.has(requestedAgentVersion)) {
3509
4074
  const publicName = publicPartExtractorName(requestedAgentVersion);
@@ -3523,6 +4088,9 @@ const commandParts = async (subcommand, ctx) => {
3523
4088
  ...(source ?? (await resolveProductionAnalyzeSource(ctx))),
3524
4089
  agentVersion: resolvePartExtractorForAnalyze(ctx.flags),
3525
4090
  name: getFlag(ctx.flags, 'name'),
4091
+ ...(baseBodyAssetId === undefined ? {} : { baseBodyAssetId }),
4092
+ ...(skillPlannerModel === undefined ? {} : { skillPlannerModel }),
4093
+ ...(autoRepair === undefined ? {} : { autoRepair }),
3526
4094
  ...(skillSelection == null ? {} : { skillSelection }),
3527
4095
  }, { ...executionOptions(ctx.flags), operationId });
3528
4096
  analyzed = result;
@@ -3538,7 +4106,7 @@ const commandParts = async (subcommand, ctx) => {
3538
4106
  if (!orderId)
3539
4107
  throw new CliExecutionError('Analysis receipt omitted its order', 3, operationId, execution);
3540
4108
  stderr.write(`[run] ${execution.runId} order=${orderId}\n`);
3541
- const executeOperationId = childOperationId(operationId, 'execute');
4109
+ const executeOperationId = await childOperationId(operationId, 'execute');
3542
4110
  if ((hasFlag(ctx.flags, 'all-ready') ||
3543
4111
  getFlagValues(ctx.flags, 'task-id').length > 0) &&
3544
4112
  (await hasRecordedOperation(session.stateDir, executeOperationId))) {
@@ -3613,7 +4181,7 @@ const commandParts = async (subcommand, ctx) => {
3613
4181
  }, {
3614
4182
  ...executionOptions(ctx.flags),
3615
4183
  operationId: execution
3616
- ? childOperationId(operationId, 'execute')
4184
+ ? await childOperationId(operationId, 'execute')
3617
4185
  : operationId,
3618
4186
  parentRunId: execution?.runId ?? executionOptions(ctx.flags).parentRunId,
3619
4187
  });
@@ -3682,7 +4250,7 @@ const commandParts = async (subcommand, ctx) => {
3682
4250
  const direct = await runPartSplit(durableInput, compareOperationId);
3683
4251
  let preprocessed = null;
3684
4252
  if (preprocessPrompt != null && preprocessPrompt !== 'true') {
3685
- const preprocessOperationId = childOperationId(compareOperationId, 'preprocess');
4253
+ const preprocessOperationId = await childOperationId(compareOperationId, 'preprocess');
3686
4254
  try {
3687
4255
  const queued = await executeRecorded(compareSession, 'image.generate', {
3688
4256
  source: sourceFromProductionAnalyzeSource(durableInput),
@@ -3719,7 +4287,7 @@ const commandParts = async (subcommand, ctx) => {
3719
4287
  job,
3720
4288
  source: generatedSource,
3721
4289
  },
3722
- split: await runPartSplit(generatedSource, childOperationId(compareOperationId, 'preprocessed-split')),
4290
+ split: await runPartSplit(generatedSource, await childOperationId(compareOperationId, 'preprocessed-split')),
3723
4291
  };
3724
4292
  }
3725
4293
  catch (error) {
@@ -3757,6 +4325,11 @@ const canvasForRead = (ctx) => openExecutionSession({
3757
4325
  create: false,
3758
4326
  });
3759
4327
  const commandCanvas = async (subcommand, positionals, ctx) => {
4328
+ if (subcommand === 'graph-id') {
4329
+ const canvasId = Number(requirePositional(positionals, 2, 'canvas-id'));
4330
+ print(await ctx.client.v2.getCanvasGraphIds(canvasId));
4331
+ return;
4332
+ }
3760
4333
  if (subcommand === 'nodes') {
3761
4334
  const { canvas } = await canvasForRead(ctx);
3762
4335
  print(await ctx.client.v2.listCanvasNodes(canvas.id));
@@ -3968,7 +4541,44 @@ const commandMoodboard = async (subcommand, positionals, ctx) => {
3968
4541
  if (revision.status === 'failed')
3969
4542
  process.exitCode = 1;
3970
4543
  };
3971
- const commandGraph = async (subcommand, ctx) => {
4544
+ export const commandGraph = async (subcommand, positionals, ctx) => {
4545
+ if (subcommand === 'export-canvas') {
4546
+ await runInternalCommand(() => runExportCanvas(ctx));
4547
+ return;
4548
+ }
4549
+ // Memorized-graph reads: the same store the Skill Builder agent's own tools
4550
+ // read, distinct from `graph show|lineage|export` below (the
4551
+ // authority/generated store via getGraph/getCanvasGraph).
4552
+ if (subcommand === 'snapshot') {
4553
+ const id = requirePositional(positionals, 2, 'graph-id');
4554
+ print(await ctx.client.v2.getGraphSnapshot(id, {
4555
+ offset: parseNonNegativeIntegerFlag(ctx.flags, 'offset'),
4556
+ limit: parsePositiveIntegerFlag(ctx.flags, 'limit', 80),
4557
+ }));
4558
+ return;
4559
+ }
4560
+ if (subcommand === 'node') {
4561
+ const id = requirePositional(positionals, 2, 'graph-id');
4562
+ const nodeId = requirePositional(positionals, 3, 'node-id');
4563
+ print(await ctx.client.v2.getGraphNode(id, nodeId));
4564
+ return;
4565
+ }
4566
+ if (subcommand === 'image') {
4567
+ const id = requirePositional(positionals, 2, 'graph-id');
4568
+ const nodeId = requirePositional(positionals, 3, 'node-id');
4569
+ const image = await ctx.client.v2.getGraphNodeImage(id, nodeId);
4570
+ const out = getFlag(ctx.flags, 'out');
4571
+ if (out) {
4572
+ await mkdir(dirname(resolve(out)), { recursive: true });
4573
+ await writeFile(resolve(out), Buffer.from(image.data, 'base64'));
4574
+ print({ mediaType: image.mediaType, bytes: image.bytes, path: resolve(out) });
4575
+ return;
4576
+ }
4577
+ // Without --out, print the metadata WITHOUT the base64 blob: a megabyte
4578
+ // of base64 in a terminal is not a result.
4579
+ print({ mediaType: image.mediaType, bytes: image.bytes });
4580
+ return;
4581
+ }
3972
4582
  const graphId = getFlag(ctx.flags, 'graph');
3973
4583
  if (getFlag(ctx.flags, 'source') && !graphId)
3974
4584
  throw new Error('--source requires --graph');
@@ -3988,7 +4598,10 @@ const commandGraph = async (subcommand, ctx) => {
3988
4598
  : getFlag(ctx.flags, 'artifact');
3989
4599
  const graph = graphId
3990
4600
  ? await ctx.client.v2.getGraph(graphId, {
3991
- source: parseEnumFlag(ctx.flags, 'source', ['generated', 'upload']),
4601
+ source: parseEnumFlag(ctx.flags, 'source', [
4602
+ 'generated',
4603
+ 'upload',
4604
+ ]),
3992
4605
  artifactId,
3993
4606
  direction: direction,
3994
4607
  depth: parsePositiveIntegerFlag(ctx.flags, 'depth', 5),
@@ -4097,7 +4710,8 @@ const commandRuns = async (subcommand, positionals, ctx) => {
4097
4710
  stdout.write(`${formatExecutionSummaryLines(execution).join('\n')}\n`);
4098
4711
  }
4099
4712
  else {
4100
- print({ execution });
4713
+ const downloads = await maybeDownloadUrls(ctx.flags, execution.outputs);
4714
+ print({ execution, ...(downloads ? { downloads } : {}) });
4101
4715
  }
4102
4716
  process.exitCode = executionExitCode(execution);
4103
4717
  return;
@@ -4217,6 +4831,734 @@ const parseNonNegativeIntegerFlag = (flags, name) => {
4217
4831
  }
4218
4832
  return parsed;
4219
4833
  };
4834
+ /**
4835
+ * `assethub memory memorize` and `assethub memory replay`.
4836
+ *
4837
+ * Both drive the trajectory-memory feature: memorize captures a canvas as a
4838
+ * reusable recipe, replay re-runs a memory's recipe onto a new image. Neither
4839
+ * route is documented at /docs/api — the four replay addresses are listed in
4840
+ * `INTERNAL_ONLY_EXPOSED_API_PATHS` and memorize does not exist yet — so
4841
+ * neither belongs in `CLI_OPERATIONS`. See the docblock on that file for the
4842
+ * full reasoning.
4843
+ *
4844
+ * `@assethub/api-client` has no memory methods, so this module talks to the
4845
+ * API directly with `fetch`, reusing the client's own envelope types and
4846
+ * error class rather than inventing new ones.
4847
+ */
4848
+ /** `POST /memory/memorize` rejects a `--nodes` selection larger than this. */
4849
+ const MAX_NODE_SELECTION = 256;
4850
+ /**
4851
+ * How many times `/memory/start` may answer `preparing` in a row before this
4852
+ * command gives up. Each round is a fresh pick-scoped search dispatched by the
4853
+ * server, not a client retry, so a bound here is what stops a
4854
+ * `preparing` -> `preparing` cycle from spinning forever.
4855
+ */
4856
+ const MAX_PREPARE_ROUNDS = 5;
4857
+ /** Default `--timeout`, in seconds. Matches `pollJob`'s 900_000ms default. */
4858
+ const DEFAULT_TIMEOUT_SECONDS = 900;
4859
+ /** Spacing between polls of a search, a pick-scoped search, or a run. */
4860
+ const MEMORY_POLL_INTERVAL_MS = 3000;
4861
+ /**
4862
+ * Every status this module treats as an input, auth, or capability problem
4863
+ * rather than a platform fault — see the "Exit codes" section of the brief.
4864
+ * A status outside this set (5xx, network) is left to propagate to the
4865
+ * top-level catch, which is the existing exit(1) path every other command
4866
+ * already uses for an infrastructure failure.
4867
+ */
4868
+ const CALLER_ERROR_STATUSES = new Set([400, 401, 403, 404, 409, 410, 422]);
4869
+ /**
4870
+ * Why 404 and 403 are not told apart here, and must not be.
4871
+ *
4872
+ * A canvas belonging to another organization answers 404, not 403 — the same
4873
+ * answer a canvas that does not exist gets, and the same choice `/memory/start`
4874
+ * makes. Canvas ids are sequential enough that a distinguishable 403 would let
4875
+ * a caller walk the id space and learn which ones exist, so the server refuses
4876
+ * to be that oracle.
4877
+ *
4878
+ * The CLI's job is to not undo that. Both statuses land in the set above, both
4879
+ * print the server's own message, and both exit 2. Do not add a branch that
4880
+ * words them differently, and do not "improve" the message by asserting the
4881
+ * canvas is missing — from here the two cases are genuinely indistinguishable,
4882
+ * which is the point.
4883
+ */
4884
+ const internalV2Request = async (ctx, method, path, body) => {
4885
+ const baseUrl = ctx.auth.baseUrl.replace(/\/+$/, '');
4886
+ const response = await fetch(`${baseUrl}/api/v2${path}`, {
4887
+ method,
4888
+ headers: {
4889
+ Authorization: `Bearer ${ctx.auth.apiKey}`,
4890
+ ...(ctx.auth.workspaceId ? { 'X-AssetHub-Workspace': ctx.auth.workspaceId } : {}),
4891
+ 'Content-Type': 'application/json',
4892
+ },
4893
+ body: body === undefined ? undefined : JSON.stringify(body),
4894
+ });
4895
+ const payload = (await response.json().catch(() => ({})));
4896
+ if (!response.ok || payload.success !== true) {
4897
+ const errorPayload = payload;
4898
+ throw new AssetHubApiError({
4899
+ status: response.status,
4900
+ code: errorPayload.error?.code ?? 'UNKNOWN_ERROR',
4901
+ message: errorPayload.error?.message ??
4902
+ `AssetHub API request failed with status ${response.status}`,
4903
+ payload,
4904
+ requestId: errorPayload.error?.requestId,
4905
+ });
4906
+ }
4907
+ return payload.data;
4908
+ };
4909
+ const parseNodeIds = (raw) => {
4910
+ const nodeIds = raw
4911
+ .split(',')
4912
+ .map(id => id.trim())
4913
+ .filter(id => id.length > 0);
4914
+ if (nodeIds.length > MAX_NODE_SELECTION) {
4915
+ throw new Error(`--nodes accepts at most ${MAX_NODE_SELECTION} node ids, got ${nodeIds.length}.`);
4916
+ }
4917
+ return nodeIds;
4918
+ };
4919
+ const runMemorize = async (ctx) => {
4920
+ const canvasId = Number(requireFlag(ctx.flags, 'canvas'));
4921
+ if (!Number.isSafeInteger(canvasId) || canvasId <= 0)
4922
+ throw new Error('--canvas must be a positive integer');
4923
+ const nodesFlag = getFlag(ctx.flags, 'nodes');
4924
+ const selection = nodesFlag == null
4925
+ ? { kind: 'whole' }
4926
+ : { kind: 'nodes', nodeIds: parseNodeIds(nodesFlag) };
4927
+ const started = await internalV2Request(ctx, 'POST', '/memory/memorize', {
4928
+ canvasId,
4929
+ selection,
4930
+ });
4931
+ if (!hasFlag(ctx.flags, 'wait')) {
4932
+ print(started);
4933
+ return;
4934
+ }
4935
+ const deadline = Date.now() +
4936
+ parsePositiveIntegerFlag(ctx.flags, 'timeout', DEFAULT_TIMEOUT_SECONDS) *
4937
+ 1000;
4938
+ let job = await internalV2Request(ctx, 'GET', `/memory/memorize/${started.memorizeJobId}`);
4939
+ while (job.status !== 'completed' && job.status !== 'failed') {
4940
+ if (Date.now() >= deadline) {
4941
+ print({ ...started, job, timedOut: true });
4942
+ process.exitCode = 3;
4943
+ return;
4944
+ }
4945
+ stderr.write(`[poll] memorizeJobId=${started.memorizeJobId} status=${job.status}\n`);
4946
+ await delay(MEMORY_POLL_INTERVAL_MS);
4947
+ job = await internalV2Request(ctx, 'GET', `/memory/memorize/${started.memorizeJobId}`);
4948
+ }
4949
+ print({ ...started, job });
4950
+ if (job.status === 'failed') {
4951
+ process.exitCode = 1;
4952
+ }
4953
+ };
4954
+ /**
4955
+ * Resolves a `searching` match to a terminal one (`ready` or `failed`),
4956
+ * polling `GET /memory/match/{matchId}` on `MEMORY_POLL_INTERVAL_MS`. Returns
4957
+ * `undefined` once `deadline` passes without a terminal answer — the caller
4958
+ * has already been told (`print` + `process.exitCode = 3`) and should return.
4959
+ */
4960
+ const resolveMatch = async (ctx, match, deadline) => {
4961
+ let current = match;
4962
+ while (current.status === 'searching') {
4963
+ if (Date.now() >= deadline) {
4964
+ print({ matchId: current.matchId, status: 'searching', timedOut: true });
4965
+ process.exitCode = 3;
4966
+ return undefined;
4967
+ }
4968
+ stderr.write(`[poll] matchId=${current.matchId} status=searching\n`);
4969
+ await delay(MEMORY_POLL_INTERVAL_MS);
4970
+ current = await internalV2Request(ctx, 'GET', `/memory/match/${current.matchId}`);
4971
+ }
4972
+ return current;
4973
+ };
4974
+ const runReplay = async (ctx, positionals) => {
4975
+ const memoryId = requirePositional(positionals, 2, 'memory-id');
4976
+ const source = requireFlag(ctx.flags, 'source');
4977
+ const wait = hasFlag(ctx.flags, 'wait');
4978
+ const deadline = Date.now() +
4979
+ parsePositiveIntegerFlag(ctx.flags, 'timeout', DEFAULT_TIMEOUT_SECONDS) *
4980
+ 1000;
4981
+ const initialMatch = await internalV2Request(ctx, 'POST', '/memory/match', {
4982
+ source: { resourceId: source },
4983
+ });
4984
+ const match = await resolveMatch(ctx, initialMatch, deadline);
4985
+ if (match == null) {
4986
+ return;
4987
+ }
4988
+ if (match.status === 'failed') {
4989
+ print({ matchId: match.matchId, status: 'failed', reason: match.reason });
4990
+ process.exitCode = 1;
4991
+ return;
4992
+ }
4993
+ if (match.preparedMemoryId == null) {
4994
+ print({
4995
+ matchId: match.matchId,
4996
+ status: 'ready',
4997
+ matched: false,
4998
+ message: 'The organization has curated no memories to replay.',
4999
+ candidates: match.candidates ?? [],
5000
+ });
5001
+ return;
5002
+ }
5003
+ let matchId = match.matchId;
5004
+ let dispatched;
5005
+ for (let round = 0; round < MAX_PREPARE_ROUNDS; round += 1) {
5006
+ const started = await internalV2Request(ctx, 'POST', '/memory/start', {
5007
+ matchId,
5008
+ memoryId,
5009
+ });
5010
+ if (started.status === 'queued') {
5011
+ dispatched = started;
5012
+ break;
5013
+ }
5014
+ // `preparing`: the server dispatched a fresh, pick-scoped search rather
5015
+ // than a walk. Poll it to `ready`, then loop to call /start again.
5016
+ stderr.write(`[poll] matchId=${started.matchId} status=preparing (pick-scoped search for ${memoryId})\n`);
5017
+ const preparedMatch = await resolveMatch(ctx, await internalV2Request(ctx, 'GET', `/memory/match/${started.matchId}`), deadline);
5018
+ if (preparedMatch == null) {
5019
+ return;
5020
+ }
5021
+ if (preparedMatch.status === 'failed') {
5022
+ print({
5023
+ matchId: preparedMatch.matchId,
5024
+ status: 'failed',
5025
+ reason: preparedMatch.reason,
5026
+ });
5027
+ process.exitCode = 1;
5028
+ return;
5029
+ }
5030
+ matchId = preparedMatch.matchId;
5031
+ }
5032
+ if (dispatched == null) {
5033
+ print({
5034
+ status: 'timeout',
5035
+ message: `Could not confirm memoryId ${memoryId} after ${MAX_PREPARE_ROUNDS} pick-scoped search rounds.`,
5036
+ matchId,
5037
+ });
5038
+ process.exitCode = 3;
5039
+ return;
5040
+ }
5041
+ if (!wait) {
5042
+ print(dispatched);
5043
+ return;
5044
+ }
5045
+ let run = await internalV2Request(ctx, 'GET', `/memory/runs/${dispatched.runId}`);
5046
+ while (run.status !== 'completed' && run.status !== 'failed') {
5047
+ if (Date.now() >= deadline) {
5048
+ print({ ...run, timedOut: true });
5049
+ process.exitCode = 3;
5050
+ return;
5051
+ }
5052
+ stderr.write(`[poll] runId=${run.runId} status=${run.status}\n`);
5053
+ await delay(MEMORY_POLL_INTERVAL_MS);
5054
+ run = await internalV2Request(ctx, 'GET', `/memory/runs/${dispatched.runId}`);
5055
+ }
5056
+ print(run);
5057
+ if (run.status === 'failed') {
5058
+ process.exitCode = 1;
5059
+ }
5060
+ };
5061
+ export const commandMemory = async (subcommand, positionals, ctx) => {
5062
+ try {
5063
+ if (subcommand === 'memorize') {
5064
+ await runMemorize(ctx);
5065
+ return;
5066
+ }
5067
+ if (subcommand === 'replay') {
5068
+ await runReplay(ctx, positionals);
5069
+ return;
5070
+ }
5071
+ if (subcommand === 'match') {
5072
+ // Finding a memory id is the step BEFORE `skills build --memory`:
5073
+ // without it that source is only usable by someone who already knows
5074
+ // the ids, which nobody does.
5075
+ print(await ctx.client.v2.matchMemory({
5076
+ goal: getFlag(ctx.flags, 'goal'),
5077
+ imageAssetId: getFlag(ctx.flags, 'image-asset'),
5078
+ imageUrl: getFlag(ctx.flags, 'image-url'),
5079
+ }));
5080
+ return;
5081
+ }
5082
+ }
5083
+ catch (error) {
5084
+ if (error instanceof AssetHubApiError &&
5085
+ CALLER_ERROR_STATUSES.has(error.status)) {
5086
+ print({
5087
+ error: { code: error.code, message: error.message, status: error.status },
5088
+ });
5089
+ process.exitCode = 2;
5090
+ return;
5091
+ }
5092
+ throw error;
5093
+ }
5094
+ throw new Error('Unknown memory command. Use "memory memorize" or "memory replay".');
5095
+ };
5096
+ /**
5097
+ * Runs one internal-API command with the same caller-error boundary
5098
+ * `commandMemory` uses: a 4xx in `CALLER_ERROR_STATUSES` prints the server's
5099
+ * own error and exits 2 (the 404 a non-internal key gets included — see the
5100
+ * note above `internalV2Request` on why it is not reworded); anything else
5101
+ * propagates to the top-level catch.
5102
+ */
5103
+ const runInternalCommand = async (operation) => {
5104
+ try {
5105
+ await operation();
5106
+ }
5107
+ catch (error) {
5108
+ if (error instanceof AssetHubApiError &&
5109
+ CALLER_ERROR_STATUSES.has(error.status)) {
5110
+ print({
5111
+ error: { code: error.code, message: error.message, status: error.status },
5112
+ });
5113
+ process.exitCode = 2;
5114
+ return;
5115
+ }
5116
+ throw error;
5117
+ }
5118
+ };
5119
+ /* ─── Canvas → artifact-graph export ─────────────────────────────────── */
5120
+ /** Mirrors `MAX_EXPORTS_PER_CALL` on the server: a longer POST is a 400, so
5121
+ * `export-canvas` chunks instead of letting the API refuse the batch. */
5122
+ const CANVAS_EXPORT_CHUNK_SIZE = 50;
5123
+ /** An export is a Trigger task that reads a room, builds a graph and zips its
5124
+ * blobs — minutes, not seconds — so poll slower than the memory family. */
5125
+ const CANVAS_EXPORT_POLL_INTERVAL_MS = 5000;
5126
+ /** Default `--timeout` for `export-canvas --wait`, in seconds. A 50-canvas
5127
+ * batch of large rooms can take a while; twice the memory default. */
5128
+ const CANVAS_EXPORT_DEFAULT_TIMEOUT_SECONDS = 1800;
5129
+ /** Per-file download deadline. The signal covers the WHOLE body, and a
5130
+ * canvas zip carries up to 400 MiB of blobs — the `canvas download` deadline
5131
+ * (120 s) would need >3 MB/s to finish one, so give a file the same budget
5132
+ * as the batch. */
5133
+ const CANVAS_EXPORT_DOWNLOAD_TIMEOUT_MS = 1_800_000;
5134
+ const CANVAS_EXPORT_TERMINAL_STATUSES = new Set(['ready', 'failed']);
5135
+ /** Global `setTimeout` rather than `timers/promises`' `delay`, so a test's
5136
+ * fake clock can drive the poll loop instead of waiting the real interval. */
5137
+ const sleep = (ms) => new Promise(resolvePromise => setTimeout(resolvePromise, ms));
5138
+ /** What `export-canvas` prints for a job: the status row WITHOUT the signed
5139
+ * archive URL. The zip is already on disk (or recorded under `failed`), and a
5140
+ * signed link in stdout is a credential in a log — same rule as
5141
+ * `canvas download`, which never writes signed URLs into its bundle. */
5142
+ const printableExportJob = (job) => {
5143
+ if (!('download' in job) || job.download == null)
5144
+ return job;
5145
+ const { url: _url, ...download } = job.download;
5146
+ return { ...job, download };
5147
+ };
5148
+ /** `--canvas 1,2,3` → `[1, 2, 3]`: positive integers, deduplicated, in order. */
5149
+ const parseCanvasIdList = (raw) => {
5150
+ const ids = raw
5151
+ .split(',')
5152
+ .map(part => part.trim())
5153
+ .filter(part => part.length > 0)
5154
+ .map(part => {
5155
+ const id = Number(part);
5156
+ if (!Number.isInteger(id) || id <= 0)
5157
+ throw new Error(`--canvas ids must be positive integers, got "${part}"`);
5158
+ return id;
5159
+ });
5160
+ const unique = [...new Set(ids)];
5161
+ if (unique.length === 0)
5162
+ throw new Error('--canvas needs at least one canvas id');
5163
+ return unique;
5164
+ };
5165
+ const chunk = (items, size) => {
5166
+ const chunks = [];
5167
+ for (let index = 0; index < items.length; index += size)
5168
+ chunks.push(items.slice(index, index + size));
5169
+ return chunks;
5170
+ };
5171
+ /**
5172
+ * `assethub graph export-canvas --org <orgId> --canvas <id>[,<id>...]`.
5173
+ *
5174
+ * Without `--wait` it prints the queued jobs. With it, it polls every job to
5175
+ * a terminal status, downloads each ready zip to `<out-dir>/<projectId>-rev<rev>.zip`
5176
+ * and exits 1 if any job failed or any download failed, 3 on the deadline
5177
+ * (with the partial job list printed), 0 otherwise.
5178
+ */
5179
+ const runExportCanvas = async (ctx) => {
5180
+ const orgId = requireFlag(ctx.flags, 'org');
5181
+ const canvasIds = parseCanvasIdList(requireFlag(ctx.flags, 'canvas'));
5182
+ const jobs = [];
5183
+ for (const ids of chunk(canvasIds, CANVAS_EXPORT_CHUNK_SIZE)) {
5184
+ const created = await internalV2Request(ctx, 'POST', '/artifact-graph/canvas-exports', { orgId, projectIds: ids });
5185
+ jobs.push(...created.jobs);
5186
+ }
5187
+ // The API silently drops an id that is not a workflow canvas owned by
5188
+ // `--org` (missing, foreign, wrong type). For a research batch that is an
5189
+ // incomplete export, not a success: name the ids and exit 1.
5190
+ const queuedIds = new Set(jobs.map(job => job.projectId));
5191
+ const omitted = canvasIds.filter(id => !queuedIds.has(id));
5192
+ if (omitted.length > 0)
5193
+ process.exitCode = 1;
5194
+ if (!hasFlag(ctx.flags, 'wait')) {
5195
+ print({ jobs, omitted });
5196
+ return;
5197
+ }
5198
+ const deadline = Date.now() +
5199
+ parsePositiveIntegerFlag(ctx.flags, 'timeout', CANVAS_EXPORT_DEFAULT_TIMEOUT_SECONDS) *
5200
+ 1000;
5201
+ const statuses = new Map();
5202
+ const pending = () => jobs.filter(job => {
5203
+ const status = statuses.get(job.jobId)?.status;
5204
+ return status == null || !CANVAS_EXPORT_TERMINAL_STATUSES.has(status);
5205
+ });
5206
+ let remaining = pending();
5207
+ while (remaining.length > 0) {
5208
+ for (const job of remaining) {
5209
+ statuses.set(job.jobId, await internalV2Request(ctx, 'GET', `/artifact-graph/canvas-exports/${job.jobId}`));
5210
+ }
5211
+ remaining = pending();
5212
+ if (remaining.length === 0)
5213
+ break;
5214
+ if (Date.now() >= deadline) {
5215
+ print({
5216
+ jobs: jobs.map(job => printableExportJob(statuses.get(job.jobId) ?? job)),
5217
+ omitted,
5218
+ timedOut: true,
5219
+ });
5220
+ process.exitCode = 3;
5221
+ return;
5222
+ }
5223
+ stderr.write(`[poll] ${remaining.length}/${jobs.length} canvas exports still running\n`);
5224
+ await sleep(CANVAS_EXPORT_POLL_INTERVAL_MS);
5225
+ }
5226
+ const outDir = resolve(getFlag(ctx.flags, 'out-dir') ?? '.');
5227
+ const downloaded = [];
5228
+ const failed = [];
5229
+ const finalJobs = jobs.map(job => statuses.get(job.jobId) ?? job);
5230
+ for (const job of finalJobs) {
5231
+ if (!('status' in job) || job.status !== 'ready')
5232
+ continue;
5233
+ const fileName = `${job.projectId ?? job.jobId}-rev${job.rev ?? 0}.zip`;
5234
+ try {
5235
+ // The poll stops re-reading a job once it is `ready`, so its signed URL
5236
+ // dates from that moment; by the time a long batch reaches this file
5237
+ // (every zip is downloaded after the LAST job settles) the hour may be
5238
+ // over. Re-read the row for a URL signed now.
5239
+ const fresh = await internalV2Request(ctx, 'GET', `/artifact-graph/canvas-exports/${job.jobId}`);
5240
+ if (fresh.download == null)
5241
+ throw new Error('No download URL is available');
5242
+ const file = await downloadCanvasGraphZip({
5243
+ url: fresh.download.url,
5244
+ destinationPath: join(outDir, fileName),
5245
+ timeoutMs: CANVAS_EXPORT_DOWNLOAD_TIMEOUT_MS,
5246
+ });
5247
+ downloaded.push({
5248
+ jobId: job.jobId,
5249
+ projectId: job.projectId,
5250
+ rev: job.rev,
5251
+ ...file,
5252
+ });
5253
+ }
5254
+ catch (error) {
5255
+ failed.push({
5256
+ jobId: job.jobId,
5257
+ projectId: job.projectId,
5258
+ error: error instanceof Error ? error.message : 'File download failed',
5259
+ });
5260
+ }
5261
+ }
5262
+ print({ jobs: finalJobs.map(printableExportJob), omitted, downloaded, failed });
5263
+ const anyJobFailed = finalJobs.some(job => 'status' in job && job.status === 'failed');
5264
+ if (anyJobFailed || failed.length > 0 || omitted.length > 0)
5265
+ process.exitCode = 1;
5266
+ };
5267
+ /**
5268
+ * Poll a build until it stops changing.
5269
+ *
5270
+ * `ready` counts as done: the agent has finished and a person now has
5271
+ * something to review. Waiting past it would block forever on a human.
5272
+ */
5273
+ const waitForSkillBuild = async (ctx, buildId) => {
5274
+ const deadline = Date.now() + 20 * 60_000;
5275
+ for (;;) {
5276
+ const build = await ctx.client.v2.getWorkspaceSkillBuild(buildId);
5277
+ if (build.status === 'ready' ||
5278
+ TERMINAL_WORKSPACE_SKILL_BUILD_STATUSES.includes(build.status))
5279
+ return build;
5280
+ if (Date.now() > deadline)
5281
+ throw new Error(`Timed out waiting for build ${buildId}; it is still ${build.status}. Re-run: skills build-status ${buildId}`);
5282
+ await new Promise(resolve => setTimeout(resolve, 5_000));
5283
+ }
5284
+ };
5285
+ export const commandSkills = async (subcommand, positionals, ctx) => {
5286
+ if (subcommand === 'list') {
5287
+ print(await ctx.client.v2.listWorkspaceSkills({
5288
+ cursor: getFlag(ctx.flags, 'cursor'),
5289
+ }));
5290
+ return;
5291
+ }
5292
+ if (subcommand === 'get') {
5293
+ print(await ctx.client.v2.getWorkspaceSkill(requirePositional(positionals, 2, 'skill-id'), {
5294
+ revision: hasFlag(ctx.flags, 'revision')
5295
+ ? parsePositiveIntegerFlag(ctx.flags, 'revision', 1)
5296
+ : undefined,
5297
+ }));
5298
+ return;
5299
+ }
5300
+ if (subcommand === 'memory') {
5301
+ // `skills get` hands back workflowRef and evidence[] as bare ids. This
5302
+ // resolves each one into the memory it points at, and gives every cited
5303
+ // memory a memoryId that `memory replay` takes directly.
5304
+ const skillId = requirePositional(positionals, 2, 'skill-id');
5305
+ const links = await ctx.client.v2.getSkillMemoryLinks(skillId, {
5306
+ revision: hasFlag(ctx.flags, 'revision')
5307
+ ? parsePositiveIntegerFlag(ctx.flags, 'revision', 1)
5308
+ : undefined,
5309
+ });
5310
+ const imagesDir = getFlag(ctx.flags, 'images');
5311
+ if (!imagesDir) {
5312
+ print(links);
5313
+ return;
5314
+ }
5315
+ // Images are fetched one node at a time through the EXISTING image
5316
+ // operation rather than inlined into the resolve response: a skill citing
5317
+ // eight memories would otherwise be a multi-megabyte JSON body on every
5318
+ // read. A node whose image is over that endpoint's cap answers 413 and is
5319
+ // recorded as a failure here, not silently skipped.
5320
+ const downloaded = [];
5321
+ const failed = [];
5322
+ // Every citing shape, flattened to the (graph, node) the image operation
5323
+ // takes: workflowRef, evidence[], and a built skill's references[].nodes,
5324
+ // whose graph id lives on the parent reference rather than the node.
5325
+ const candidates = [
5326
+ ...(links.workflowRef
5327
+ ? [
5328
+ {
5329
+ ref: links.workflowRef,
5330
+ graphId: links.workflowRef.graphId,
5331
+ nodeId: links.workflowRef.nodeId,
5332
+ },
5333
+ ]
5334
+ : []),
5335
+ ...links.evidence.map(ref => ({
5336
+ ref,
5337
+ graphId: ref.graphId ?? '',
5338
+ nodeId: ref.artifactId ?? '',
5339
+ })),
5340
+ ...links.references.flatMap(reference => reference.nodes.map(node => ({
5341
+ ref: node,
5342
+ graphId: reference.graphId ?? '',
5343
+ nodeId: node.nodeId,
5344
+ }))),
5345
+ ]
5346
+ .filter(entry => entry.ref.status === 'resolved' &&
5347
+ entry.ref.node?.hasImage === true &&
5348
+ entry.graphId !== '' &&
5349
+ entry.nodeId !== '')
5350
+ .map(entry => ({
5351
+ memoryId: entry.ref.memoryId ?? '',
5352
+ graphId: entry.graphId,
5353
+ nodeId: entry.nodeId,
5354
+ }));
5355
+ await mkdir(resolve(imagesDir), { recursive: true });
5356
+ for (const ref of candidates) {
5357
+ const memoryId = ref.memoryId;
5358
+ try {
5359
+ const image = await ctx.client.v2.getGraphNodeImage(ref.graphId, ref.nodeId);
5360
+ // The memory id carries colons and, for a node id, slashes are
5361
+ // possible — neither is safe in a file name.
5362
+ const name = `${memoryId.replaceAll(/[^A-Za-z0-9._-]/gu, '_')}.${image.mediaType.split('/')[1] ?? 'bin'}`;
5363
+ const path = resolve(imagesDir, name);
5364
+ await writeFile(path, Buffer.from(image.data, 'base64'));
5365
+ downloaded.push({ memoryId, path, bytes: image.bytes });
5366
+ }
5367
+ catch (error) {
5368
+ failed.push({
5369
+ memoryId,
5370
+ error: error instanceof Error ? error.message : 'download failed',
5371
+ });
5372
+ }
5373
+ }
5374
+ print({ ...links, images: { downloaded, failed } });
5375
+ if (failed.length > 0)
5376
+ process.exitCode = 1;
5377
+ return;
5378
+ }
5379
+ if (subcommand === 'controls') {
5380
+ const mode = parseEnumFlag(ctx.flags, 'mode', [
5381
+ 'automatic',
5382
+ 'manual',
5383
+ 'off',
5384
+ ]);
5385
+ if (!mode)
5386
+ throw new Error('Missing required flag: --mode');
5387
+ requireFlag(ctx.flags, 'revision');
5388
+ requireFlag(ctx.flags, 'grant-revision');
5389
+ print(await ctx.client.v2.setWorkspaceSkillControls(requirePositional(positionals, 2, 'skill-id'), {
5390
+ expectedRevision: parsePositiveIntegerFlag(ctx.flags, 'revision', 1),
5391
+ expectedGrantRevision: parsePositiveIntegerFlag(ctx.flags, 'grant-revision', 1),
5392
+ mode,
5393
+ ...(hasFlag(ctx.flags, 'share') ? { share: true } : {}),
5394
+ }));
5395
+ return;
5396
+ }
5397
+ if (subcommand === 'proposal') {
5398
+ print(await ctx.client.v2.getWorkspaceSkillProposal(requirePositional(positionals, 2, 'proposal-id')));
5399
+ return;
5400
+ }
5401
+ if (['learn', 'prepare'].includes(subcommand ?? '')) {
5402
+ const input = await readJsonArgument(requireFlag(ctx.flags, 'input-json'), '--input-json');
5403
+ const operationId = subcommand === 'learn'
5404
+ ? requireFlag(ctx.flags, 'operation-id')
5405
+ : undefined;
5406
+ if (operationId != null &&
5407
+ !/^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i.test(operationId))
5408
+ throw new Error('--operation-id must be a UUID; reuse the same ID and input after an uncertain response');
5409
+ if (operationId != null)
5410
+ activeExecution.operationId = operationId;
5411
+ print(subcommand === 'learn'
5412
+ ? await ctx.client.v2.learnWorkspaceSkill(input, { idempotencyKey: operationId })
5413
+ : await ctx.client.v2.prepareWorkspaceSkillProposal(input));
5414
+ return;
5415
+ }
5416
+ if (['update', 'accept'].includes(subcommand ?? '')) {
5417
+ const id = requirePositional(positionals, 2, subcommand === 'update' ? 'skill-id' : 'proposal-id');
5418
+ const input = await readJsonArgument(requireFlag(ctx.flags, 'input-json'), '--input-json');
5419
+ print(subcommand === 'update'
5420
+ ? await ctx.client.v2.updateWorkspaceSkill(id, input)
5421
+ : await ctx.client.v2.acceptWorkspaceSkillProposal(id, input));
5422
+ return;
5423
+ }
5424
+ if (subcommand === 'official') {
5425
+ const action = requirePositional(positionals, 2, 'list|install|prepare|customize');
5426
+ if (action === 'list') {
5427
+ print(await ctx.client.v2.listOfficialWorkspaceSkills());
5428
+ return;
5429
+ }
5430
+ const skillId = requirePositional(positionals, 3, 'skill-id');
5431
+ if (action === 'install') {
5432
+ requireFlag(ctx.flags, 'revision');
5433
+ print(await ctx.client.v2.installOfficialWorkspaceSkill(skillId, {
5434
+ revision: parsePositiveIntegerFlag(ctx.flags, 'revision', 1),
5435
+ expectedRevision: hasFlag(ctx.flags, 'expected-revision')
5436
+ ? parsePositiveIntegerFlag(ctx.flags, 'expected-revision', 1)
5437
+ : null,
5438
+ expectedGrantRevision: hasFlag(ctx.flags, 'expected-grant-revision')
5439
+ ? parsePositiveIntegerFlag(ctx.flags, 'expected-grant-revision', 1)
5440
+ : null,
5441
+ confirmedContentSha256: requireFlag(ctx.flags, 'content-sha256'),
5442
+ }));
5443
+ return;
5444
+ }
5445
+ if (action === 'prepare') {
5446
+ const input = await readJsonArgument(requireFlag(ctx.flags, 'input-json'), '--input-json');
5447
+ print(await ctx.client.v2.prepareOfficialWorkspaceSkillCustomization(skillId, input));
5448
+ return;
5449
+ }
5450
+ // `customize` submits the exact body `prepare` returned, unchanged: the
5451
+ // server checks the confirmed hash against a fresh re-read of the
5452
+ // installed source, so a hand-edited body fails the hash match.
5453
+ if (action === 'customize') {
5454
+ const input = await readJsonArgument(requireFlag(ctx.flags, 'input-json'), '--input-json');
5455
+ print(await ctx.client.v2.customizeOfficialWorkspaceSkill(skillId, input));
5456
+ return;
5457
+ }
5458
+ throw new Error('Use skills official list|install|prepare|customize');
5459
+ }
5460
+ if (subcommand === 'build') {
5461
+ const canvasIds = getFlagValues(ctx.flags, 'canvas').map(value => {
5462
+ const parsed = Number(value);
5463
+ if (!Number.isInteger(parsed) || parsed <= 0)
5464
+ throw new Error('--canvas must be a positive canvas id');
5465
+ return parsed;
5466
+ });
5467
+ const memoryIds = getFlagValues(ctx.flags, 'memory');
5468
+ // Exactly one source. Accepting both would leave the server to pick, and
5469
+ // the caller would not know which half of their request was ignored.
5470
+ if (canvasIds.length > 0 === memoryIds.length > 0)
5471
+ throw new Error('Pass either --canvas (1-5) or --memory (1-8), not both.');
5472
+ const request = {
5473
+ goal: requireFlag(ctx.flags, 'goal'),
5474
+ instructions: getFlag(ctx.flags, 'instructions'),
5475
+ taskKind: 'part_separation',
5476
+ source: canvasIds.length > 0
5477
+ ? { kind: 'canvas_graph', canvasIds }
5478
+ : { kind: 'memory_path', memoryIds },
5479
+ };
5480
+ // FREE preview: no build row, no reservation, no dispatch. Skips
5481
+ // --wait/--operation-id entirely -- there is nothing to poll or replay.
5482
+ if (hasFlag(ctx.flags, 'dry-run')) {
5483
+ print(await ctx.client.v2.dryRunWorkspaceSkillBuild(request));
5484
+ return;
5485
+ }
5486
+ const started = await ctx.client.v2.startWorkspaceSkillBuild(request,
5487
+ // Reusing --operation-id after a lost response returns the SAME build
5488
+ // instead of starting a second paid run.
5489
+ { idempotencyKey: getFlag(ctx.flags, 'operation-id') ?? randomUUID() });
5490
+ print(hasFlag(ctx.flags, 'wait')
5491
+ ? await waitForSkillBuild(ctx, started.buildId)
5492
+ : started);
5493
+ return;
5494
+ }
5495
+ if (subcommand === 'build-status') {
5496
+ const buildId = requirePositional(positionals, 2, 'build-id');
5497
+ print(hasFlag(ctx.flags, 'wait')
5498
+ ? await waitForSkillBuild(ctx, buildId)
5499
+ : await ctx.client.v2.getWorkspaceSkillBuild(buildId));
5500
+ return;
5501
+ }
5502
+ if (subcommand === 'build-accept') {
5503
+ const buildId = requirePositional(positionals, 2, 'build-id');
5504
+ print(await ctx.client.v2.acceptWorkspaceSkillBuild(buildId, { expectedDraftSha256: requireFlag(ctx.flags, 'draft-sha256') }, { idempotencyKey: getFlag(ctx.flags, 'operation-id') ?? randomUUID() }));
5505
+ return;
5506
+ }
5507
+ if (subcommand === 'build-enhance') {
5508
+ // Prints a SUGGESTION. Nothing is written — apply it yourself and then
5509
+ // accept, so a rewrite can never publish something nobody read.
5510
+ print(await ctx.client.v2.enhanceWorkspaceSkillBuildSection(requirePositional(positionals, 2, 'build-id'), {
5511
+ section: parseEnumFlag(ctx.flags, 'section', [
5512
+ ...WORKSPACE_SKILL_ENHANCE_SECTIONS,
5513
+ ]),
5514
+ current: JSON.parse(requireFlag(ctx.flags, 'current')),
5515
+ note: getFlag(ctx.flags, 'note'),
5516
+ }));
5517
+ return;
5518
+ }
5519
+ if (subcommand === 'build-discard') {
5520
+ print(await ctx.client.v2.discardWorkspaceSkillBuild(requirePositional(positionals, 2, 'build-id')));
5521
+ return;
5522
+ }
5523
+ // FREE pre-flight checks: neither reserves credits, inserts a build row,
5524
+ // or dispatches anything.
5525
+ if (subcommand === 'validate') {
5526
+ const skill = await readJsonArgument(requireFlag(ctx.flags, 'input-json'), '--input-json');
5527
+ const buildId = getFlag(ctx.flags, 'build');
5528
+ print(await ctx.client.v2.validateWorkspaceSkillDraft({
5529
+ skill: skill,
5530
+ ...(buildId ? { buildId } : {}),
5531
+ }));
5532
+ return;
5533
+ }
5534
+ if (subcommand === 'schema') {
5535
+ print(await ctx.client.v2.getWorkspaceSkillDraftSchema());
5536
+ return;
5537
+ }
5538
+ throw new Error('Use skills list|get|memory|learn|update|controls|prepare|proposal|accept|validate|schema|build|build-status|build-enhance|build-accept|build-discard|official');
5539
+ };
5540
+ /**
5541
+ * `assethub org search <query>` and `assethub org canvases --org <orgId>`:
5542
+ * the two reads an operator needs before `graph export-canvas`. Internal-only
5543
+ * on the server — a non-internal key gets 404, printed as-is with exit 2.
5544
+ */
5545
+ export const commandOrg = async (subcommand, positionals, ctx) => {
5546
+ if (subcommand === 'search') {
5547
+ const query = requirePositional(positionals, 2, 'query');
5548
+ await runInternalCommand(async () => {
5549
+ print(await internalV2Request(ctx, 'GET', `/orgs/search?q=${encodeURIComponent(query)}`));
5550
+ });
5551
+ return;
5552
+ }
5553
+ if (subcommand === 'canvases') {
5554
+ const orgId = requireFlag(ctx.flags, 'org');
5555
+ await runInternalCommand(async () => {
5556
+ print(await internalV2Request(ctx, 'GET', `/orgs/${encodeURIComponent(orgId)}/workflow-canvases`));
5557
+ });
5558
+ return;
5559
+ }
5560
+ throw new Error('Unknown org command. Use "org search" or "org canvases".');
5561
+ };
4220
5562
  /** Base URL and workspace an MCP entry should target: explicit flags, then the selected profile, then defaults. */
4221
5563
  const resolveMcpTarget = async (flags) => {
4222
5564
  const config = await readAuthConfig(getConfigPath(flags));
@@ -4263,7 +5605,13 @@ const run = async () => {
4263
5605
  if (!hasFlag(parsed.flags, 'account'))
4264
5606
  return resolveAuth(parsed.flags);
4265
5607
  const auth = await workspaceAuth(parsed.flags);
4266
- return { apiKey: auth.accessToken, workspaceMfaToken: auth.workspaceMfaToken, baseUrl: auth.baseUrl, profile: auth.profile, source: 'user' };
5608
+ return {
5609
+ apiKey: auth.accessToken,
5610
+ workspaceMfaToken: auth.workspaceMfaToken,
5611
+ baseUrl: auth.baseUrl,
5612
+ profile: auth.profile,
5613
+ source: 'user',
5614
+ };
4267
5615
  },
4268
5616
  account: hasFlag(parsed.flags, 'account'),
4269
5617
  includeMcp: true,
@@ -4276,7 +5624,7 @@ const run = async () => {
4276
5624
  return;
4277
5625
  }
4278
5626
  const check = report.checks.find(check => check.name === 'mcp');
4279
- const tools = check && 'tools' in check ? check.tools ?? [] : [];
5627
+ const tools = check && 'tools' in check ? (check.tools ?? []) : [];
4280
5628
  const name = parsed.positionals[2];
4281
5629
  if (name) {
4282
5630
  const tool = tools.find(tool => tool.name === name);
@@ -4285,17 +5633,43 @@ const run = async () => {
4285
5633
  print({ tool });
4286
5634
  }
4287
5635
  else {
4288
- print({ total: tools.length, tools: tools.map(({ name, title, annotations }) => ({ name, title, annotations })) });
5636
+ print({
5637
+ total: tools.length,
5638
+ tools: tools.map(({ name, title, annotations }) => ({
5639
+ name,
5640
+ title,
5641
+ annotations,
5642
+ })),
5643
+ });
4289
5644
  }
4290
5645
  return;
4291
5646
  }
4292
5647
  if (command === 'mcp') {
4293
5648
  if (subcommand !== 'config')
4294
5649
  throw new Error('Use mcp tools [tool-name] or mcp config --client cursor|codex');
4295
- const { baseUrl, workspaceId } = await resolveMcpTarget(parsed.flags);
5650
+ const config = await readAuthConfig(getConfigPath(parsed.flags));
5651
+ const explicitProfile = getFlag(parsed.flags, 'profile');
5652
+ const stored = config.profiles?.[explicitProfile ?? config.defaultProfile ?? defaultProfileName];
5653
+ if (explicitProfile && !stored)
5654
+ throw new Error('Selected profile does not exist');
5655
+ const selected = explicitProfile || stored?.workspaceId || isPersonalProfile(stored);
5656
+ const baseUrl = getFlag(parsed.flags, 'base-url') ??
5657
+ (selected ? stored?.baseUrl : env.ASSETHUB_API_BASE_URL ?? stored?.baseUrl) ?? defaultBaseUrl;
5658
+ const workspaceId = getFlag(parsed.flags, 'workspace') ??
5659
+ (stored && new URL(baseUrl).origin === new URL(stored.baseUrl).origin ? stored.workspaceId : undefined);
4296
5660
  stdout.write(mcpConfig(requireFlag(parsed.flags, 'client'), baseUrl, hasFlag(parsed.flags, 'account'), workspaceId));
4297
5661
  return;
4298
5662
  }
5663
+ if (command === 'update') {
5664
+ // The verifying doctor checks the same login this command was given.
5665
+ const forwardFlags = ['profile', 'config', 'base-url', 'workspace'].flatMap(name => {
5666
+ const value = getFlag(parsed.flags, name);
5667
+ return value ? [`--${name}`, value] : [];
5668
+ });
5669
+ const apiKey = getFlag(parsed.flags, 'api-key');
5670
+ print(await runUpdate({ check: hasFlag(parsed.flags, 'check'), dryRun: hasFlag(parsed.flags, 'dry-run'), yes: hasFlag(parsed.flags, 'yes') }, createNodeUpdateDeps(forwardFlags, apiKey ? { ASSETHUB_API_KEY: apiKey } : {})));
5671
+ return;
5672
+ }
4299
5673
  if (command === 'doctor') {
4300
5674
  stderr.write('Checking AssetHub connection…\n');
4301
5675
  const report = await diagnose({
@@ -4325,7 +5699,16 @@ const run = async () => {
4325
5699
  return;
4326
5700
  }
4327
5701
  if (command === 'workspace') {
4328
- if (['list', 'get', 'create', 'use', 'members', 'invite', 'set-role', 'remove-member'].includes(subcommand ?? '')) {
5702
+ if ([
5703
+ 'list',
5704
+ 'get',
5705
+ 'create',
5706
+ 'use',
5707
+ 'members',
5708
+ 'invite',
5709
+ 'set-role',
5710
+ 'remove-member',
5711
+ ].includes(subcommand ?? '')) {
4329
5712
  await commandWorkspace(subcommand, parsed.positionals, parsed.flags);
4330
5713
  return;
4331
5714
  }
@@ -4358,6 +5741,21 @@ const run = async () => {
4358
5741
  });
4359
5742
  return;
4360
5743
  }
5744
+ if (command === 'setup') {
5745
+ await commandSetup(parsed.flags, installSessionHooks);
5746
+ return;
5747
+ }
5748
+ if (command === 'env') {
5749
+ await commandEnv(subcommand, parsed.flags);
5750
+ return;
5751
+ }
5752
+ if (command === 'hooks') {
5753
+ // hooks parses its own flags: `save` reads the Claude Code hook JSON on stdin.
5754
+ const code = await runHooksCommand(argv.slice(3));
5755
+ if (code !== 0)
5756
+ process.exitCode = code;
5757
+ return;
5758
+ }
4361
5759
  if (command === 'auth') {
4362
5760
  await commandAuth(subcommand, parsed.flags);
4363
5761
  return;
@@ -4370,9 +5768,6 @@ const run = async () => {
4370
5768
  case 'api':
4371
5769
  await commandApi(subcommand, parsed.positionals, ctx);
4372
5770
  return;
4373
- case 'skills':
4374
- await commandSkills(subcommand, parsed.positionals, ctx);
4375
- return;
4376
5771
  case 'composer':
4377
5772
  await commandComposer(subcommand, ctx);
4378
5773
  return;
@@ -4386,7 +5781,7 @@ const run = async () => {
4386
5781
  await commandCanvas(subcommand, parsed.positionals, ctx);
4387
5782
  return;
4388
5783
  case 'graph':
4389
- await commandGraph(subcommand, ctx);
5784
+ await commandGraph(subcommand, parsed.positionals, ctx);
4390
5785
  return;
4391
5786
  case 'evaluations':
4392
5787
  await commandEvaluations(subcommand, parsed.positionals, ctx);
@@ -4429,6 +5824,15 @@ const run = async () => {
4429
5824
  case 'runs':
4430
5825
  await commandRuns(subcommand, parsed.positionals, ctx);
4431
5826
  return;
5827
+ case 'memory':
5828
+ await commandMemory(subcommand, parsed.positionals, ctx);
5829
+ return;
5830
+ case 'skills':
5831
+ await commandSkills(subcommand, parsed.positionals, ctx);
5832
+ return;
5833
+ case 'org':
5834
+ await commandOrg(subcommand, parsed.positionals, ctx);
5835
+ return;
4432
5836
  case 'parts':
4433
5837
  await commandParts(subcommand, ctx);
4434
5838
  return;