@assethub/cli 0.1.30 → 0.1.32
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +282 -8
- package/dist/appEnv.d.ts +20 -0
- package/dist/appEnv.d.ts.map +1 -0
- package/dist/appEnv.js +62 -0
- package/dist/canvasGraphDownload.d.ts +11 -0
- package/dist/canvasGraphDownload.d.ts.map +1 -0
- package/dist/canvasGraphDownload.js +108 -0
- package/dist/execution.d.ts +5 -2
- package/dist/execution.d.ts.map +1 -1
- package/dist/execution.js +11 -12
- package/dist/hooks/command.d.ts +14 -0
- package/dist/hooks/command.d.ts.map +1 -0
- package/dist/hooks/command.js +181 -0
- package/dist/hooks/graph.d.ts +40 -0
- package/dist/hooks/graph.d.ts.map +1 -0
- package/dist/hooks/graph.js +269 -0
- package/dist/hooks/index.d.ts +7 -0
- package/dist/hooks/index.d.ts.map +1 -0
- package/dist/hooks/index.js +6 -0
- package/dist/hooks/install.d.ts +58 -0
- package/dist/hooks/install.d.ts.map +1 -0
- package/dist/hooks/install.js +286 -0
- package/dist/hooks/paths.d.ts +31 -0
- package/dist/hooks/paths.d.ts.map +1 -0
- package/dist/hooks/paths.js +101 -0
- package/dist/hooks/redact.d.ts +5 -0
- package/dist/hooks/redact.d.ts.map +1 -0
- package/dist/hooks/redact.js +67 -0
- package/dist/hooks/save.d.ts +62 -0
- package/dist/hooks/save.d.ts.map +1 -0
- package/dist/hooks/save.js +331 -0
- package/dist/hooks/transcript.d.ts +23 -0
- package/dist/hooks/transcript.d.ts.map +1 -0
- package/dist/hooks/transcript.js +78 -0
- package/dist/hooks/trim.d.ts +25 -0
- package/dist/hooks/trim.d.ts.map +1 -0
- package/dist/hooks/trim.js +220 -0
- package/dist/hooks/types.d.ts +56 -0
- package/dist/hooks/types.d.ts.map +1 -0
- package/dist/hooks/types.js +0 -0
- package/dist/hooks/upload.d.ts +52 -0
- package/dist/hooks/upload.d.ts.map +1 -0
- package/dist/hooks/upload.js +252 -0
- package/dist/index.d.ts +12 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1569 -165
- package/dist/nodeMeshBatch.d.ts.map +1 -1
- package/dist/nodeMeshBatch.js +5 -4
- package/dist/productionBatch.d.ts +77 -0
- package/dist/productionBatch.d.ts.map +1 -0
- package/dist/productionBatch.js +204 -0
- package/dist/runProgress.d.ts +4 -0
- package/dist/runProgress.d.ts.map +1 -0
- package/dist/runProgress.js +16 -0
- package/dist/runsUpload/controlEndpoints.d.ts +1 -1
- package/dist/runsUpload/controlEndpoints.d.ts.map +1 -1
- package/dist/runsUpload/controlEndpoints.js +4 -2
- package/dist/runsUpload/uploadRun.d.ts +7 -1
- package/dist/runsUpload/uploadRun.d.ts.map +1 -1
- package/dist/runsUpload/uploadRun.js +7 -4
- package/dist/setupCommand.d.ts +130 -0
- package/dist/setupCommand.d.ts.map +1 -0
- package/dist/setupCommand.js +569 -0
- package/dist/setupHooksBridge.d.ts +12 -0
- package/dist/setupHooksBridge.d.ts.map +1 -0
- package/dist/setupHooksBridge.js +4 -0
- package/dist/update.d.ts +74 -0
- package/dist/update.d.ts.map +1 -0
- package/dist/update.js +205 -0
- 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,
|
|
27
|
+
import { AssetHubApiError, WorkspaceClientError, createWorkspaceClient, createAssetHubClient, } from '@assethub/api-client';
|
|
20
28
|
import { resolveCanvasSelection, saveCanvasSelection } from './canvas.js';
|
|
21
|
-
import { activeExecution,
|
|
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.
|
|
84
|
-
apiValue: '
|
|
85
|
-
|
|
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: '
|
|
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
|
-
|
|
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
|
|
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 (
|
|
373
|
-
throw new Error('--skill-mode
|
|
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',
|
|
1880
|
-
onProgress: (run
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
:
|
|
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(
|
|
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
|
|
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
|
-
|
|
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
|
|
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 =
|
|
3505
|
-
?
|
|
3506
|
-
:
|
|
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', [
|
|
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
|
-
|
|
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 {
|
|
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({
|
|
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
|
|
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 ([
|
|
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;
|