@assethub/cli 0.1.25 → 0.1.28

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 CHANGED
@@ -67,6 +67,8 @@ authentication. Existing workspace keys and user-token login remain supported.
67
67
  ```sh
68
68
  assethub --version
69
69
  assethub doctor --mcp --profile my-workspace
70
+ assethub init --dry-run
71
+ assethub init
70
72
  assethub mcp config --client cursor
71
73
  assethub mcp config --client codex
72
74
  ```
@@ -87,6 +89,19 @@ Use the same key as your CLI profile. Personal profiles include the selected
87
89
  For a different API origin, pass
88
90
  `--base-url https://your-host/prefix` (HTTP is accepted only for localhost).
89
91
 
92
+ `init` sets up every supported coding agent found on this machine — Claude Code,
93
+ Codex, and Cursor — in one step. It writes the hosted MCP entry into each agent's
94
+ own configuration (merging with what is already there), installs the bundled
95
+ `assethub` skill under `~/.agents/skills/assethub`, and links it into each agent's
96
+ skill directory so the agent knows the correct command sequence, cost rules, and
97
+ recovery steps without being told. Pass `--agent <name>` to choose agents
98
+ explicitly, `--project` to write project-level MCP files into the current
99
+ directory instead of your home, and `--dry-run` to see the plan without writing.
100
+ `init` never stores a key: the MCP entries read `ASSETHUB_API_KEY` from the
101
+ agent's environment. Directories that `init` did not create are left untouched.
102
+ After a CLI upgrade, the next command refreshes the installed skill automatically.
103
+ Set `ASSETHUB_CLI_HOME` to redirect all of these writes, for example in tests.
104
+
90
105
  Explicit `--profile` uses that profile's key and origin, ahead of environment
91
106
  variables, including user tokens and workspace MFA proofs. A missing profile fails.
92
107
  Without an explicit profile, a personal profile or a workspace selected with
@@ -247,8 +262,10 @@ has its own saved operation ID, with the execution linked to the analysis run.
247
262
  For Internal `V3.6.1` (`V3.6.1 Primary Images First`), `V3.6.3`
248
263
  (`V3.6.3 Fast Analysis`, CLI 0.1.16+), `V3.6.4`
249
264
  (`V3.6.4 Fast Analysis`, CLI 0.1.17+), `V3.6.5 Primary Images` and
250
- `Chibi Character (Pluffy) v3.1` (both CLI 0.1.20+), analysis runs the complete
251
- Artifact Graph split. `--all-ready` waits for that one receipt and does not start a
265
+ `Chibi Character (Pluffy) v3.1` (both CLI 0.1.20+), `V3.7 Artist Skills`,
266
+ `V3.7.1 Building Modules` (both CLI 0.1.23+/0.1.24+), `V3.0.9 Garment
267
+ Boundaries`, and `Humanoid Assembly (internal)` (both CLI 0.1.26+), analysis
268
+ runs the complete Artifact Graph split. `--all-ready` waits for that one receipt and does not start a
252
269
  second production execution. Resume an interrupted graph split with
253
270
  `runs resume <operation-id>`; `--task-id`, `--mission-id`,
254
271
  `--part-extraction-mode`, and classic `--order-id` continuation are unsupported.
@@ -259,6 +276,79 @@ or an `imageUrl` to import), `config`, and optional `maxCostCredits`.
259
276
  An uncertain Trigger acknowledgement remains recoverable as `needs_review` with
260
277
  pending history; retrying the same operation never silently launches another run.
261
278
 
279
+ ### One-shot: image to finished asset
280
+
281
+ `production run --image <file|asset-id>` is the fastest path from a single image to
282
+ a finished, composed asset. It is a thin wrapper around `production automation`
283
+ (split, mesh generation per part, and — by default — server-side compose, all in
284
+ one call) plus waiting, cost estimation and downloads. It never re-runs the manual
285
+ split/mesh/compose sequence itself.
286
+
287
+ ```sh
288
+ # Estimate cost first; nothing is submitted, nothing is billed.
289
+ assethub production run --image ./character.png --estimate
290
+
291
+ # Run it, wait for every part and the compose stage, download the result.
292
+ assethub production run --image ./character.png --wait --download --out-dir ./out/asset
293
+
294
+ # An existing asset id works the same way as a local file.
295
+ assethub production run --image "$IMAGE_ASSET_ID" --canvas "$CANVAS_ID" --wait
296
+ ```
297
+
298
+ `--image` auto-detects its argument: an existing local path is uploaded like
299
+ `--file`; anything else is treated like `--source-id`. Do not combine `--image`
300
+ with `--file`/`--source-id`/other source flags — it already resolves one.
301
+
302
+ `--compose` controls the compose stage:
303
+
304
+ - Omitted: automation's own server-side default applies, untouched.
305
+ - `--compose none`: disables automation's compose stage (`config.autoCompose:
306
+ false`); the run stops once every part's mesh is generated.
307
+ - `--compose v6`: requests an explicit Composer V6 pass via
308
+ `config.partComposerAgentVersion: "part_composer_v6_auto_assemble"`
309
+ (`autoCompose` is left at its default of `true` — sending both together is
310
+ rejected by the server). A server that has shipped this (assethub-web PR
311
+ #8179) does the V6 compose itself, in the same automation call, pairing
312
+ each part's mesh with its source image automatically. Against an older,
313
+ currently-deployed server (detected by the automation response lacking
314
+ `estimatedCostBreakdown`), the CLI instead reads the batch's own
315
+ `mesh.generate` executions itself, pairs each produced mesh with the part
316
+ image it was generated from, and runs an explicit `composer run` — same
317
+ end result, either way you never pair mesh and image assets by hand.
318
+
319
+ `--mesh-model <id>` and `--max-parts <n>` (default 24, matching the server's
320
+ own conservative constant for this math) only affect `--estimate`'s math (mesh
321
+ unit cost x an assumed worst-case part count — the CLI cannot know the real
322
+ part count before splitting runs); they do not cap or select anything on the
323
+ real run. `--max-cost <credits>` refuses to run at all, before anything is
324
+ submitted, once the best-effort estimate exceeds it, and is also sent to the
325
+ server as `maxCostCredits` on the real run as a backstop. `--estimate`'s numbers
326
+ come only from side-effect-free catalog reads (mesh model credit cost, compose
327
+ quotes) shaped like the server's own `split`/`meshGeneration`/`compose`
328
+ breakdown; part-extraction/split has no dry-run price today, so that line is
329
+ reported as unknown rather than guessed. On a real (non-`--estimate`) run
330
+ against a server that has shipped #8179, the printed output additionally
331
+ includes the server's own `estimatedTotalCredits`/`estimatedCostBreakdown`
332
+ in place of the CLI's own preflight guess.
333
+
334
+ This depends on assethub-web PR #8179 for the `partComposerAgentVersion` field
335
+ and the richer cost breakdown; it is not merged/deployed as of this writing.
336
+ The CLI degrades gracefully against the currently-deployed API (see above).
337
+
338
+ `runs get <run-id> --summary` prints a handful of plain-text lines (status, stage,
339
+ credits, output asset, error) instead of the full JSON execution. `runs wait
340
+ <run-id>` is `runs watch` under a name that matches a long-running wait; both
341
+ accept `--interval-ms`/`--timeout-ms` and retry rate limits the same way every
342
+ other poll loop in the CLI does.
343
+
344
+ The one-shot command's stdout is always a single JSON object (`batchId`,
345
+ `estimate`, `batchStatus`, `compose`, `execution`, and `downloads` when
346
+ requested) — the same convention every other command in this CLI already
347
+ follows. `--json` is accepted for compatibility with that convention but has
348
+ no effect, since there is no alternate plain-text mode to opt out of (`runs
349
+ get --summary` is the one deliberate exception, and it is opt-in by its own
350
+ flag).
351
+
262
352
  `evaluate list` discovers server evaluators. Server evaluation dispatch is currently
263
353
  unavailable. Graph exports are bounded to 100 runs/evaluations and 1,000 nodes;
264
354
  truncated exports require `--allow-truncated`. Other commands, including production run/intervene, rig and retopology,
@@ -522,6 +612,13 @@ Part extraction uses the same names shown in the AssetHub product: `V1.5`,
522
612
  `V3.6.4` (`V3.6.4 Fast Analysis`), `V3.6.5` (`V3.6.5 Primary Images`),
523
613
  and `pluffy` (`Chibi Character (Pluffy) v3.1`). CLI 0.1.20 adds the latter two;
524
614
  `3.6.5`, `pluffy v3.1`, and `pluffy 3.1` are also accepted aliases.
615
+ `V3.7 Artist Skills` (`v3.7`; also known as `V3.0.7 Artist Skills`) and
616
+ `V3.7.1 Building Modules` (`v3.7.1`; also known as `V3.0.8 Building Modules`)
617
+ support `--skill-mode`. `V3.0.9 Garment Boundaries` (`v3.0.9`) and
618
+ `Humanoid Assembly (internal)` (`humanoid assembly`, `humanoid-assembly`,
619
+ `humanoid_assembly_auto`; CLI 0.1.26+) are additional Internal-only graph
620
+ extractors; use the exact quoted public name or one of its aliases with
621
+ `--part-extractor`.
525
622
  Run `production agents` to check availability and `supportedOnGraphEndpoint`
526
623
  before selecting a graph version. These aliases do not change the V1.5 default
527
624
  or grant access to Internal models; the server still authorizes the account.
@@ -545,6 +642,11 @@ assethub canvas list
545
642
  assethub canvas use <canvas-id>
546
643
  assethub composer models
547
644
  assethub composer run --part <mesh-id> --part <mesh-id> --reference <image-id> --wait
645
+ # Composer V6 (auto-assemble) requires each part's reference image; attach it
646
+ # with `<mesh-id>:<part-image-asset-id>` (the image that mesh was generated
647
+ # from) when the mesh has no other lineage the run can resolve it from.
648
+ assethub composer run --part <mesh-id>:<part-image-id> --part <mesh-id>:<part-image-id> \
649
+ --reference <image-id> --model part_composer_v6_auto_assemble --wait
548
650
  assethub composer run --from-run <run-id> --transforms-json @transforms.json --wait
549
651
  assethub composer refine --list-modes
550
652
  assethub composer refine --from-run <run-id> --instruction "align the feet" \
@@ -0,0 +1,69 @@
1
+ export declare const AGENT_IDS: readonly ["claude-code", "codex", "cursor"];
2
+ export type AgentId = (typeof AGENT_IDS)[number];
3
+ type AgentSpec = {
4
+ id: AgentId;
5
+ name: string;
6
+ /** Presence of this directory under home means the agent is installed. */
7
+ detectDir: string;
8
+ globalMcpPath: string;
9
+ projectMcpPath: string;
10
+ skillDir: string;
11
+ format: 'claude' | 'cursor' | 'codex';
12
+ };
13
+ /** `ASSETHUB_CLI_HOME` keeps tests and sandboxes from writing into the real home directory. */
14
+ export declare const cliHome: () => string;
15
+ export declare const packagedSkillDir: () => string;
16
+ export declare const canonicalSkillDir: (home: string) => string;
17
+ export declare const mcpServerEntry: (format: AgentSpec["format"], baseUrl: string, workspaceId?: string) => Record<string, unknown>;
18
+ type WriteStatus = 'created' | 'updated' | 'unchanged';
19
+ type SkillStatus = 'installed' | 'updated' | 'unchanged' | 'kept-existing';
20
+ export declare const installSkill: ({ home, version, dryRun, }: {
21
+ home: string;
22
+ version: string;
23
+ dryRun: boolean;
24
+ }) => Promise<{
25
+ path: string;
26
+ status: SkillStatus;
27
+ version?: string;
28
+ }>;
29
+ type LinkStatus = 'linked' | 'relinked' | 'unchanged' | 'kept-existing';
30
+ /**
31
+ * Refresh an installed skill after a CLI upgrade. Runs before every command;
32
+ * the steady-state cost is one small file read. Never throws — a read-only or
33
+ * missing home must not fail the user's actual command.
34
+ */
35
+ export declare const syncInstalledSkill: () => Promise<void>;
36
+ export type InitOptions = {
37
+ baseUrl: string;
38
+ workspaceId?: string;
39
+ /** Agent ids to configure; defaults to every agent detected under home. */
40
+ agents?: string[];
41
+ /** Write project-level MCP config into the working directory instead of the user's home. */
42
+ project?: boolean;
43
+ dryRun?: boolean;
44
+ cwd?: string;
45
+ home?: string;
46
+ };
47
+ export type InitReport = {
48
+ version: string;
49
+ dryRun: boolean;
50
+ scope: 'global' | 'project';
51
+ skill: Awaited<ReturnType<typeof installSkill>>;
52
+ agents: {
53
+ id: AgentId;
54
+ name: string;
55
+ detected: boolean;
56
+ mcp: {
57
+ path: string;
58
+ status: WriteStatus;
59
+ };
60
+ skill: {
61
+ path: string;
62
+ status: LinkStatus;
63
+ };
64
+ }[];
65
+ next: string[];
66
+ };
67
+ export declare const initAgents: (options: InitOptions) => Promise<InitReport>;
68
+ export {};
69
+ //# sourceMappingURL=agentSetup.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"agentSetup.d.ts","sourceRoot":"","sources":["../src/agentSetup.ts"],"names":[],"mappings":"AAgCA,eAAO,MAAM,SAAS,6CAA8C,CAAA;AACpE,MAAM,MAAM,OAAO,GAAG,CAAC,OAAO,SAAS,CAAC,CAAC,MAAM,CAAC,CAAA;AAEhD,KAAK,SAAS,GAAG;IACf,EAAE,EAAE,OAAO,CAAA;IACX,IAAI,EAAE,MAAM,CAAA;IACZ,0EAA0E;IAC1E,SAAS,EAAE,MAAM,CAAA;IACjB,aAAa,EAAE,MAAM,CAAA;IACrB,cAAc,EAAE,MAAM,CAAA;IACtB,QAAQ,EAAE,MAAM,CAAA;IAChB,MAAM,EAAE,QAAQ,GAAG,QAAQ,GAAG,OAAO,CAAA;CACtC,CAAA;AAoCD,+FAA+F;AAC/F,eAAO,MAAM,OAAO,QAAO,MAA4C,CAAA;AAEvE,eAAO,MAAM,gBAAgB,QAAO,MACiC,CAAA;AAErE,eAAO,MAAM,iBAAiB,GAAI,MAAM,MAAM,KAAG,MACJ,CAAA;AA2B7C,eAAO,MAAM,cAAc,GACzB,QAAQ,SAAS,CAAC,QAAQ,CAAC,EAC3B,SAAS,MAAM,EACf,cAAc,MAAM,KACnB,MAAM,CAAC,MAAM,EAAE,OAAO,CAYxB,CAAA;AAED,KAAK,WAAW,GAAG,SAAS,GAAG,SAAS,GAAG,WAAW,CAAA;AA2EtD,KAAK,WAAW,GAAG,WAAW,GAAG,SAAS,GAAG,WAAW,GAAG,eAAe,CAAA;AAmC1E,eAAO,MAAM,YAAY,GAAU,4BAIhC;IACD,IAAI,EAAE,MAAM,CAAA;IACZ,OAAO,EAAE,MAAM,CAAA;IACf,MAAM,EAAE,OAAO,CAAA;CAChB,KAAG,OAAO,CAAC;IAAC,IAAI,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,WAAW,CAAC;IAAC,OAAO,CAAC,EAAE,MAAM,CAAA;CAAC,CAwBhE,CAAA;AAED,KAAK,UAAU,GAAG,QAAQ,GAAG,UAAU,GAAG,WAAW,GAAG,eAAe,CAAA;AAkCvE;;;;GAIG;AACH,eAAO,MAAM,kBAAkB,QAAa,OAAO,CAAC,IAAI,CAevD,CAAA;AAID,MAAM,MAAM,WAAW,GAAG;IACxB,OAAO,EAAE,MAAM,CAAA;IACf,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB,2EAA2E;IAC3E,MAAM,CAAC,EAAE,MAAM,EAAE,CAAA;IACjB,4FAA4F;IAC5F,OAAO,CAAC,EAAE,OAAO,CAAA;IACjB,MAAM,CAAC,EAAE,OAAO,CAAA;IAChB,GAAG,CAAC,EAAE,MAAM,CAAA;IACZ,IAAI,CAAC,EAAE,MAAM,CAAA;CACd,CAAA;AAED,MAAM,MAAM,UAAU,GAAG;IACvB,OAAO,EAAE,MAAM,CAAA;IACf,MAAM,EAAE,OAAO,CAAA;IACf,KAAK,EAAE,QAAQ,GAAG,SAAS,CAAA;IAC3B,KAAK,EAAE,OAAO,CAAC,UAAU,CAAC,OAAO,YAAY,CAAC,CAAC,CAAA;IAC/C,MAAM,EAAE;QACN,EAAE,EAAE,OAAO,CAAA;QACX,IAAI,EAAE,MAAM,CAAA;QACZ,QAAQ,EAAE,OAAO,CAAA;QACjB,GAAG,EAAE;YAAC,IAAI,EAAE,MAAM,CAAC;YAAC,MAAM,EAAE,WAAW,CAAA;SAAC,CAAA;QACxC,KAAK,EAAE;YAAC,IAAI,EAAE,MAAM,CAAC;YAAC,MAAM,EAAE,UAAU,CAAA;SAAC,CAAA;KAC1C,EAAE,CAAA;IACH,IAAI,EAAE,MAAM,EAAE,CAAA;CACf,CAAA;AAED,eAAO,MAAM,UAAU,GAAU,SAAS,WAAW,KAAG,OAAO,CAAC,UAAU,CA4EzE,CAAA"}
@@ -0,0 +1,311 @@
1
+ /**
2
+ * `assethub init` and the skill sync that keeps its output current.
3
+ *
4
+ * Coding agents learn a tool from two local files: an MCP server entry in their
5
+ * own config, and a skill directory they read on demand. `init` writes both for
6
+ * every supported agent found on this machine, merging into existing config
7
+ * rather than replacing it, and never touching credentials — the MCP entries
8
+ * reference `ASSETHUB_API_KEY` from the agent's environment.
9
+ *
10
+ * The skill is copied once to `~/.agents/skills/assethub` and symlinked into
11
+ * each agent's skill directory. A version marker lets every later command
12
+ * refresh the copy when the CLI has been upgraded, so agents never read rules
13
+ * written for a version they are no longer running.
14
+ */
15
+ import { cp, lstat, mkdir, readFile, readlink, rename, rm, stat, symlink, writeFile, } from 'node:fs/promises';
16
+ import { homedir } from 'node:os';
17
+ import { dirname, join, relative, resolve } from 'node:path';
18
+ import { cwd as processCwd, env, pid } from 'node:process';
19
+ import { fileURLToPath } from 'node:url';
20
+ import { cliVersion, mcpConfig, validatedBaseUrl } from './setup.js';
21
+ export const AGENT_IDS = ['claude-code', 'codex', 'cursor'];
22
+ const AGENTS = [
23
+ {
24
+ id: 'claude-code',
25
+ name: 'Claude Code',
26
+ detectDir: '.claude',
27
+ globalMcpPath: '.claude.json',
28
+ projectMcpPath: '.mcp.json',
29
+ skillDir: '.claude/skills',
30
+ format: 'claude',
31
+ },
32
+ {
33
+ id: 'codex',
34
+ name: 'Codex',
35
+ detectDir: '.codex',
36
+ globalMcpPath: '.codex/config.toml',
37
+ projectMcpPath: '.codex/config.toml',
38
+ skillDir: '.codex/skills',
39
+ format: 'codex',
40
+ },
41
+ {
42
+ id: 'cursor',
43
+ name: 'Cursor',
44
+ detectDir: '.cursor',
45
+ globalMcpPath: '.cursor/mcp.json',
46
+ projectMcpPath: '.cursor/mcp.json',
47
+ skillDir: '.cursor/skills',
48
+ format: 'cursor',
49
+ },
50
+ ];
51
+ const SERVER_NAME = 'assethub';
52
+ const SKILL_NAME = 'assethub';
53
+ const VERSION_MARKER = '.assethub-cli-version';
54
+ /** `ASSETHUB_CLI_HOME` keeps tests and sandboxes from writing into the real home directory. */
55
+ export const cliHome = () => env.ASSETHUB_CLI_HOME || homedir();
56
+ export const packagedSkillDir = () => fileURLToPath(new URL(`../skills/${SKILL_NAME}/`, import.meta.url));
57
+ export const canonicalSkillDir = (home) => join(home, '.agents', 'skills', SKILL_NAME);
58
+ const isEnoent = (error) => typeof error === 'object' &&
59
+ error !== null &&
60
+ error.code === 'ENOENT';
61
+ const readText = async (path) => {
62
+ try {
63
+ return await readFile(path, 'utf8');
64
+ }
65
+ catch (error) {
66
+ if (isEnoent(error))
67
+ return undefined;
68
+ throw error;
69
+ }
70
+ };
71
+ const isDirectory = async (path) => {
72
+ try {
73
+ return (await stat(path)).isDirectory();
74
+ }
75
+ catch (error) {
76
+ if (isEnoent(error))
77
+ return false;
78
+ throw error;
79
+ }
80
+ };
81
+ // ── MCP configuration ────────────────────────────────────────────────
82
+ export const mcpServerEntry = (format, baseUrl, workspaceId) => {
83
+ const url = `${validatedBaseUrl(baseUrl)}/api/mcp`;
84
+ // Cursor expands `${env:NAME}`; Claude Code expands `${NAME}`. Neither file ever holds the key.
85
+ const key = format === 'cursor' ? '${env:ASSETHUB_API_KEY}' : '${ASSETHUB_API_KEY}';
86
+ return {
87
+ ...(format === 'claude' ? { type: 'http' } : {}),
88
+ url,
89
+ headers: {
90
+ Authorization: `Bearer ${key}`,
91
+ ...(workspaceId ? { 'X-AssetHub-Workspace': workspaceId } : {}),
92
+ },
93
+ };
94
+ };
95
+ const writeJsonServer = async (path, entry, dryRun) => {
96
+ const text = await readText(path);
97
+ let document = {};
98
+ if (text !== undefined) {
99
+ let parsed;
100
+ try {
101
+ parsed = JSON.parse(text);
102
+ }
103
+ catch {
104
+ throw new Error(`${path} is not valid JSON. Fix or move it, then rerun init.`);
105
+ }
106
+ if (typeof parsed !== 'object' || parsed === null || Array.isArray(parsed))
107
+ throw new Error(`${path} must contain a JSON object.`);
108
+ document = parsed;
109
+ }
110
+ // A malformed `mcpServers` is the user's data too; refuse rather than replace it.
111
+ if (document.mcpServers !== undefined &&
112
+ (typeof document.mcpServers !== 'object' ||
113
+ document.mcpServers === null ||
114
+ Array.isArray(document.mcpServers)))
115
+ throw new Error(`${path} has a non-object "mcpServers" value. Fix or move it, then rerun init.`);
116
+ const servers = (document.mcpServers ?? {});
117
+ if (JSON.stringify(servers[SERVER_NAME]) === JSON.stringify(entry))
118
+ return 'unchanged';
119
+ const next = { ...document, mcpServers: { ...servers, [SERVER_NAME]: entry } };
120
+ if (!dryRun) {
121
+ await mkdir(dirname(path), { recursive: true });
122
+ await writeFile(path, `${JSON.stringify(next, null, 2)}\n`);
123
+ }
124
+ return text === undefined ? 'created' : 'updated';
125
+ };
126
+ /** Replaces or appends one `[mcp_servers.assethub]` table; every other line is kept verbatim. */
127
+ const writeTomlSection = async (path, section, dryRun) => {
128
+ const header = `[mcp_servers.${SERVER_NAME}]`;
129
+ const body = section.trimEnd();
130
+ const text = await readText(path);
131
+ const lines = (text ?? '').split('\n');
132
+ const start = lines.findIndex(line => line.trim() === header);
133
+ let next;
134
+ if (start === -1) {
135
+ const base = (text ?? '').trimEnd();
136
+ next = `${base ? `${base}\n\n` : ''}${body}\n`;
137
+ }
138
+ else {
139
+ const endOffset = lines
140
+ .slice(start + 1)
141
+ .findIndex(line => /^\s*\[/.test(line));
142
+ const end = endOffset === -1 ? lines.length : start + 1 + endOffset;
143
+ if (lines.slice(start, end).join('\n').trimEnd() === body)
144
+ return 'unchanged';
145
+ const before = lines.slice(0, start).join('\n').trimEnd();
146
+ const after = lines.slice(end).join('\n').trim();
147
+ next = `${before ? `${before}\n\n` : ''}${body}\n${after ? `\n${after}\n` : ''}`;
148
+ }
149
+ if (!dryRun) {
150
+ await mkdir(dirname(path), { recursive: true });
151
+ await writeFile(path, next);
152
+ }
153
+ return text === undefined ? 'created' : 'updated';
154
+ };
155
+ const readMarker = async (dir) => (await readText(join(dir, VERSION_MARKER)))?.trim();
156
+ /**
157
+ * Stage the new copy beside the target, then swap with two renames, so the
158
+ * installed skill is never deleted before its replacement is complete. A
159
+ * failed swap puts the previous copy back. Two CLIs syncing at the same
160
+ * moment use distinct staging names; whichever copy lands is complete, and
161
+ * the loser's error is swallowed by the caller.
162
+ */
163
+ const replaceSkillDir = async (source, target, version) => {
164
+ const staging = `${target}.next-${pid}`;
165
+ const previous = `${target}.prev-${pid}`;
166
+ await rm(staging, { recursive: true, force: true });
167
+ await mkdir(dirname(target), { recursive: true });
168
+ await cp(source, staging, { recursive: true });
169
+ await writeFile(join(staging, VERSION_MARKER), `${version}\n`);
170
+ const hadTarget = await isDirectory(target);
171
+ if (hadTarget)
172
+ await rename(target, previous);
173
+ try {
174
+ await rename(staging, target);
175
+ }
176
+ catch (error) {
177
+ if (hadTarget)
178
+ await rename(previous, target).catch(() => undefined);
179
+ await rm(staging, { recursive: true, force: true });
180
+ throw error;
181
+ }
182
+ if (hadTarget)
183
+ await rm(previous, { recursive: true, force: true });
184
+ };
185
+ export const installSkill = async ({ home, version, dryRun, }) => {
186
+ const source = packagedSkillDir();
187
+ if (!(await isDirectory(source)))
188
+ throw new Error('This CLI installation does not include the packaged agent skill.');
189
+ const target = canonicalSkillDir(home);
190
+ const marker = await readMarker(target);
191
+ // A directory without our marker is the user's own; leave it alone.
192
+ const status = marker === undefined
193
+ ? (await isDirectory(target))
194
+ ? 'kept-existing'
195
+ : 'installed'
196
+ : marker === version
197
+ ? 'unchanged'
198
+ : 'updated';
199
+ if (!dryRun && (status === 'installed' || status === 'updated'))
200
+ await replaceSkillDir(source, target, version);
201
+ return {
202
+ path: target,
203
+ status,
204
+ ...(status === 'kept-existing' ? {} : { version }),
205
+ };
206
+ };
207
+ const linkSkill = async (agentSkillsDir, canonical, dryRun) => {
208
+ const link = join(agentSkillsDir, SKILL_NAME);
209
+ const relativeTarget = relative(agentSkillsDir, canonical);
210
+ let existing;
211
+ try {
212
+ existing = await lstat(link);
213
+ }
214
+ catch (error) {
215
+ if (!isEnoent(error))
216
+ throw error;
217
+ }
218
+ if (existing?.isSymbolicLink()) {
219
+ if (resolve(agentSkillsDir, await readlink(link)) === canonical)
220
+ return { path: link, status: 'unchanged' };
221
+ if (!dryRun) {
222
+ await rm(link);
223
+ await symlink(relativeTarget, link, 'dir');
224
+ }
225
+ return { path: link, status: 'relinked' };
226
+ }
227
+ if (existing)
228
+ return { path: link, status: 'kept-existing' };
229
+ if (!dryRun) {
230
+ await mkdir(agentSkillsDir, { recursive: true });
231
+ await symlink(relativeTarget, link, 'dir');
232
+ }
233
+ return { path: link, status: 'linked' };
234
+ };
235
+ let synced = false;
236
+ /**
237
+ * Refresh an installed skill after a CLI upgrade. Runs before every command;
238
+ * the steady-state cost is one small file read. Never throws — a read-only or
239
+ * missing home must not fail the user's actual command.
240
+ */
241
+ export const syncInstalledSkill = async () => {
242
+ if (synced)
243
+ return;
244
+ synced = true;
245
+ try {
246
+ const target = canonicalSkillDir(cliHome());
247
+ const installed = await readMarker(target);
248
+ if (installed === undefined)
249
+ return;
250
+ const version = await cliVersion();
251
+ if (installed === version)
252
+ return;
253
+ const source = packagedSkillDir();
254
+ if (!(await isDirectory(source)))
255
+ return;
256
+ await replaceSkillDir(source, target, version);
257
+ }
258
+ catch {
259
+ // best effort
260
+ }
261
+ };
262
+ export const initAgents = async (options) => {
263
+ const home = options.home ?? cliHome();
264
+ const cwd = options.cwd ?? processCwd();
265
+ const dryRun = options.dryRun ?? false;
266
+ const version = await cliVersion();
267
+ const requested = options.agents ?? [];
268
+ const unknown = requested.filter(id => !AGENT_IDS.includes(id));
269
+ if (unknown.length > 0)
270
+ throw new Error(`Unknown agent: ${unknown.join(', ')}. Use --agent ${AGENT_IDS.join('|')}.`);
271
+ const detected = new Map(await Promise.all(AGENTS.map(async (agent) => [agent.id, await isDirectory(join(home, agent.detectDir))])));
272
+ const selected = requested.length > 0
273
+ ? AGENTS.filter(agent => requested.includes(agent.id))
274
+ : AGENTS.filter(agent => detected.get(agent.id));
275
+ if (selected.length === 0)
276
+ throw new Error(`No supported coding agent found under ${home}. Pass --agent ${AGENT_IDS.join('|')} to choose one.`);
277
+ const skill = await installSkill({ home, version, dryRun });
278
+ const agents = [];
279
+ for (const spec of selected) {
280
+ const mcpPath = options.project
281
+ ? join(cwd, spec.projectMcpPath)
282
+ : join(home, spec.globalMcpPath);
283
+ const mcp = spec.format === 'codex'
284
+ ? await writeTomlSection(mcpPath, mcpConfig('codex', options.baseUrl, false, options.workspaceId), dryRun)
285
+ : await writeJsonServer(mcpPath, mcpServerEntry(spec.format, options.baseUrl, options.workspaceId), dryRun);
286
+ const link = await linkSkill(join(home, spec.skillDir), skill.path, dryRun);
287
+ agents.push({
288
+ id: spec.id,
289
+ name: spec.name,
290
+ detected: detected.get(spec.id) ?? false,
291
+ mcp: { path: mcpPath, status: mcp },
292
+ skill: link,
293
+ });
294
+ }
295
+ return {
296
+ version,
297
+ dryRun,
298
+ scope: options.project ? 'project' : 'global',
299
+ skill,
300
+ agents,
301
+ next: [
302
+ ...(env.ASSETHUB_API_KEY
303
+ ? []
304
+ : [
305
+ "Provide ASSETHUB_API_KEY in each agent's environment; the MCP entries read it from there and never store it.",
306
+ ]),
307
+ 'Restart the agent so it reloads MCP servers and skills.',
308
+ 'Run `assethub doctor --mcp` to verify the connection.',
309
+ ],
310
+ };
311
+ };
@@ -1 +1 @@
1
- {"version":3,"file":"execution.d.ts","sourceRoot":"","sources":["../src/execution.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"execution.d.ts","sourceRoot":"","sources":["../src/execution.ts"],"names":[],"mappings":"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"}
package/dist/execution.js CHANGED
@@ -43,6 +43,9 @@ export class CliExecutionError extends Error {
43
43
  this.runId = runId;
44
44
  }
45
45
  }
46
+ /** A floor under `--interval-ms` for `waitForExecution`'s own poll cadence. */
47
+ const MIN_POLL_INTERVAL_MS = 1_000;
48
+ const POLL_JITTER_MS = 250;
46
49
  /** Kept only for SIGINT reporting; interrupting the client never cancels server work. */
47
50
  export const activeExecution = {};
48
51
  export const childOperationId = (parent, step) => {
@@ -247,6 +250,11 @@ export const waitForExecution = async (client, runId, options = {}) => {
247
250
  activeExecution.runId = runId;
248
251
  const deadline = Date.now() + (options.timeoutMs ?? 900_000);
249
252
  const timeout = () => new CliExecutionError(`Wait timed out; server execution continues. Use runs watch ${runId}.`, 3, activeExecution.operationId, activeExecution.execution, runId);
253
+ // A floor under --interval-ms and a touch of jitter on every poll: several
254
+ // `--wait` invocations started together (e.g. parallel `mesh generate`)
255
+ // otherwise poll in lockstep and collide on the same rate-limit window.
256
+ const pollIntervalMs = () => Math.max(options.intervalMs ?? 5_000, MIN_POLL_INTERVAL_MS) +
257
+ Math.floor(Math.random() * POLL_JITTER_MS);
250
258
  while (true) {
251
259
  const signal = AbortSignal.timeout(Math.max(1, deadline - Date.now()));
252
260
  let execution;
@@ -256,6 +264,16 @@ export const waitForExecution = async (client, runId, options = {}) => {
256
264
  catch (error) {
257
265
  if (signal.aborted)
258
266
  throw timeout();
267
+ // The client already retries a 429 internally (Retry-After/backoff);
268
+ // one only reaches here once that budget is exhausted. Server work
269
+ // keeps running regardless, so a rate limit is never a wait failure —
270
+ // treat it like "no update yet" and keep polling within the deadline.
271
+ if (error instanceof AssetHubApiError && error.status === 429) {
272
+ if (Date.now() >= deadline)
273
+ throw timeout();
274
+ await delay(Math.min(pollIntervalMs(), Math.max(0, deadline - Date.now())));
275
+ continue;
276
+ }
259
277
  throw new CliExecutionError(error instanceof Error ? error.message : String(error), error instanceof AssetHubApiError && error.status < 500 ? 2 : 1, activeExecution.operationId, activeExecution.execution, runId);
260
278
  }
261
279
  activeExecution.execution = execution;
@@ -267,6 +285,6 @@ export const waitForExecution = async (client, runId, options = {}) => {
267
285
  return execution;
268
286
  if (Date.now() >= deadline)
269
287
  throw timeout();
270
- await delay(Math.min(options.intervalMs ?? 5_000, Math.max(0, deadline - Date.now())));
288
+ await delay(Math.min(pollIntervalMs(), Math.max(0, deadline - Date.now())));
271
289
  }
272
290
  };