@mokuyoaxis/dsh-iris 0.2.6 → 0.3.0-alpha.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (177) hide show
  1. package/CHANGELOG.md +103 -1
  2. package/README.en.md +104 -304
  3. package/README.md +101 -302
  4. package/bin/dsh-iris.js +446 -64
  5. package/docs/AGENT_CORE_QUICKSTART.md +64 -0
  6. package/docs/AGENT_QUICKSTART.md +7 -5
  7. package/docs/ARCHITECTURE.md +20 -4
  8. package/docs/AUDIO_FLOWS.md +71 -0
  9. package/docs/CLI_MANAGEMENT.md +1 -1
  10. package/docs/CONTACT_SHEET.md +62 -0
  11. package/docs/CORE_CONNECTIONS.md +99 -0
  12. package/docs/CORE_QUICKSTART.md +108 -0
  13. package/docs/CORE_RUNTIME_CONTRACT.md +41 -5
  14. package/docs/CORE_SERVICE.md +97 -0
  15. package/docs/CORE_SKILLS.md +94 -0
  16. package/docs/DSH_COMPATIBILITY.md +14 -4
  17. package/docs/DSH_CORE_MIGRATION.md +4 -2
  18. package/docs/DSH_PROVIDER_CONNECTIONS.md +1 -1
  19. package/docs/DSH_RC2_ADAPTATION.md +1 -1
  20. package/docs/DSH_RUNTIME.md +40 -0
  21. package/docs/FUNCTION_MODEL_POOLS.md +1 -1
  22. package/docs/HEADLESS_CLI.md +12 -4
  23. package/docs/HOST_DOCTOR.md +13 -1
  24. package/docs/IMAGE_FLOW.md +75 -0
  25. package/docs/IMAGE_GENERATION.md +127 -0
  26. package/docs/LOCAL_API.md +62 -0
  27. package/docs/LOCAL_API_CONTRACT.md +140 -0
  28. package/docs/LOCAL_CREATION.md +56 -0
  29. package/docs/LOCAL_OPERATIONS.md +46 -0
  30. package/docs/LOCAL_SELECTION.md +57 -0
  31. package/docs/LOCAL_WEB.md +201 -0
  32. package/docs/MCP.md +191 -0
  33. package/docs/MUSIC.md +2 -0
  34. package/docs/PANEL_EXTENSIONS.md +64 -0
  35. package/docs/PLUGINS.md +189 -0
  36. package/docs/PLUGIN_STARTUP.md +100 -0
  37. package/docs/PROVIDER_ADAPTER_CONTRACT.md +1 -1
  38. package/docs/PROVIDER_EXTENSIONS.md +78 -0
  39. package/docs/PROVIDER_HEALTH.md +4 -2
  40. package/docs/PROVIDER_SETTINGS.md +85 -0
  41. package/docs/PROVIDER_SUBMISSION_CONTRACT.md +6 -0
  42. package/docs/ROADMAP.md +28 -6
  43. package/docs/SECURITY.md +9 -1
  44. package/docs/SYNC_DELIVERY_RECOVERY.md +2 -0
  45. package/docs/VISION_CLI.md +2 -2
  46. package/docs/WEB_PROTOTYPE.md +62 -0
  47. package/docs/WEB_RECOVERY.md +46 -0
  48. package/docs/WORKBENCH_LOGIN.md +63 -0
  49. package/docs/assets/screenshots/core-connections-preview.webp +0 -0
  50. package/docs/assets/screenshots/core-model-pools-preview.webp +0 -0
  51. package/docs/assets/screenshots/core-overview-preview.webp +0 -0
  52. package/docs/assets/screenshots/core-spectrum-preview.webp +0 -0
  53. package/docs/assets/screenshots/core-studio-dark-preview.webp +0 -0
  54. package/docs/assets/screenshots/core-studio-mobile-preview.webp +0 -0
  55. package/docs/assets/screenshots/core-studio-preview.webp +0 -0
  56. package/docs/releases/0.2.7.md +62 -0
  57. package/docs/releases/0.3.0-alpha.1.md +51 -0
  58. package/docs/screenshots.md +41 -1
  59. package/lib/actions.js +132 -70
  60. package/lib/adapters.js +20 -11
  61. package/lib/api.js +38 -38
  62. package/lib/application-runtime.js +213 -0
  63. package/lib/audio-flow-input.js +37 -0
  64. package/lib/client.js +434 -55
  65. package/lib/comfyui-adapter.js +94 -0
  66. package/lib/comfyui-client.js +51 -0
  67. package/lib/comfyui-inspection.js +73 -0
  68. package/lib/comfyui-workflow.js +94 -0
  69. package/lib/config.js +36 -1
  70. package/lib/configured-image-runtime.js +50 -0
  71. package/lib/connection-skill.js +89 -0
  72. package/lib/connection-template.js +23 -0
  73. package/lib/core-artifact-store.js +21 -2
  74. package/lib/core-bridge-selection.js +31 -0
  75. package/lib/core-bridge-tools.js +83 -0
  76. package/lib/core-capability-catalog.js +182 -0
  77. package/lib/core-connection-diagnostics.js +49 -0
  78. package/lib/core-connection-store.js +277 -0
  79. package/lib/core-connections.js +225 -0
  80. package/lib/core-maintenance.js +31 -2
  81. package/lib/core-runtime.js +20 -0
  82. package/lib/core-service-client.js +170 -0
  83. package/lib/core-tasks.js +84 -13
  84. package/lib/core-writer-lease.js +16 -0
  85. package/lib/creation-request-store.js +133 -0
  86. package/lib/doctor.js +12 -1
  87. package/lib/dsh-core-adapter.js +666 -572
  88. package/lib/dsh-host-adapter.js +8 -2
  89. package/lib/dsh-service-bridge.js +66 -0
  90. package/lib/execution-ownership.js +43 -0
  91. package/lib/flow-store.js +290 -0
  92. package/lib/generation-input.js +10 -1
  93. package/lib/headless-vision.js +7 -7
  94. package/lib/image-edit-input.js +1 -1
  95. package/lib/image-flow.js +232 -0
  96. package/lib/index.js +94 -68
  97. package/lib/local-api-projection.js +32 -0
  98. package/lib/local-api-token-directory.js +31 -0
  99. package/lib/local-api.js +1083 -0
  100. package/lib/local-creation.js +150 -0
  101. package/lib/local-operations.js +36 -0
  102. package/lib/local-request-root.js +86 -0
  103. package/lib/local-web-assets.js +37 -0
  104. package/lib/mcp-bridge.js +58 -0
  105. package/lib/mcp-check.js +26 -0
  106. package/lib/mcp-config.js +42 -0
  107. package/lib/mcp-delivery.js +46 -0
  108. package/lib/mcp-http.js +101 -0
  109. package/lib/mcp-server.js +47 -0
  110. package/lib/model-directory-service.js +16 -6
  111. package/lib/model-directory.js +3 -3
  112. package/lib/models.js +2 -2
  113. package/lib/music-transport.js +2 -1
  114. package/lib/operation-runner.js +236 -0
  115. package/lib/operation-store.js +216 -0
  116. package/lib/panel-extension.js +36 -0
  117. package/lib/plugin-contract.js +194 -0
  118. package/lib/plugin-execution.js +51 -0
  119. package/lib/plugin-launch.js +86 -0
  120. package/lib/plugin-operation-store.js +101 -0
  121. package/lib/plugin-operations.js +28 -0
  122. package/lib/plugin-registry.js +109 -0
  123. package/lib/plugin-selection.js +15 -0
  124. package/lib/plugin-source-image.js +5 -0
  125. package/lib/plugin-web-assets.js +54 -0
  126. package/lib/plugins.d.ts +288 -0
  127. package/lib/plugins.js +58 -0
  128. package/lib/private-storage.js +12 -0
  129. package/lib/provider-adapter.js +5 -1
  130. package/lib/provider-adapters.js +56 -18
  131. package/lib/provider-catalog.js +40 -29
  132. package/lib/provider-config-service.js +105 -23
  133. package/lib/provider-diagnostics.js +56 -0
  134. package/lib/provider-extensions.js +29 -0
  135. package/lib/provider-health.js +8 -1
  136. package/lib/provider-monitor.js +79 -0
  137. package/lib/provider-protocol.js +70 -20
  138. package/lib/provider-registry.js +118 -0
  139. package/lib/provider-response.js +28 -0
  140. package/lib/provider-settings.js +134 -0
  141. package/lib/provider-task-runner.js +40 -7
  142. package/lib/render-address.js +35 -0
  143. package/lib/render.js +52 -13
  144. package/lib/skill-installation.js +211 -0
  145. package/lib/vision-model-routing.js +3 -0
  146. package/lib/web/app.js +642 -0
  147. package/lib/web/appearance.js +111 -0
  148. package/lib/web/comfyui.js +146 -0
  149. package/lib/web/connections.js +377 -0
  150. package/lib/web/creation.js +355 -0
  151. package/lib/web/flow-view.js +33 -0
  152. package/lib/web/gallery.js +132 -0
  153. package/lib/web/index.html +291 -0
  154. package/lib/web/media-client.js +204 -0
  155. package/lib/web/plugin-panel.js +221 -0
  156. package/lib/web/plugin-surface.css +17 -0
  157. package/lib/web/providers.js +492 -0
  158. package/lib/web/recovery.js +105 -0
  159. package/lib/web/startup.js +18 -0
  160. package/lib/web/styles.css +518 -0
  161. package/lib/web/transport.js +96 -0
  162. package/lib/web/workbench-login.js +165 -0
  163. package/lib/workbench-artifacts.js +1 -2
  164. package/lib/workbench-auth-cli.js +47 -0
  165. package/lib/workbench-auth.js +200 -0
  166. package/lib/workbench-management.js +8 -9
  167. package/package.json +22 -6
  168. package/plugins/contact-sheet/index.d.mts +4 -0
  169. package/plugins/contact-sheet/index.mjs +33 -0
  170. package/plugins/contact-sheet/web/board.css +14 -0
  171. package/plugins/contact-sheet/web/board.mjs +93 -0
  172. package/skills/iris-core/SKILL.md +44 -0
  173. package/skills/iris-core/agents/openai.yaml +4 -0
  174. package/skills/iris-core/references/cli.md +95 -0
  175. package/skills/iris-core/references/comfyui.md +69 -0
  176. package/skills/iris-core/references/tasks.md +60 -0
  177. package/user_guide.md +19 -9
package/bin/dsh-iris.js CHANGED
@@ -3,11 +3,13 @@ import fs from 'node:fs';
3
3
  import path from 'node:path';
4
4
  import { createCommandService } from '../lib/command-service.js';
5
5
  import { createCoreRuntime } from '../lib/core-runtime.js';
6
+ import { createApplicationRuntime } from '../lib/application-runtime.js';
6
7
  import { recoverCoreWriterLease } from '../lib/core-lease-recovery.js';
7
8
  import { doctor, formatDoctorReport } from '../lib/doctor.js';
8
9
  import { loadProviderCatalog, providerCatalogSnapshot, catalogCapabilitySnapshot, imageCandidatesFromCatalog, musicCandidatesFromCatalog, providerForTaskFromCatalog, providerTaskBinding, transcribeCandidatesFromCatalog, ttsCandidatesFromCatalog, videoCandidatesFromCatalog } from '../lib/provider-catalog.js';
9
10
  import { createConfiguredProviderAdapter } from '../lib/provider-adapters.js';
10
- import { providerForImageModel, selectMediaProtocol, dashscopeTtsInputIssue } from '../lib/provider-protocol.js';
11
+ import { providerForImageModel, selectMediaProtocol, dashscopeTtsInputIssue, supportsImageEdit } from '../lib/provider-protocol.js';
12
+ import { loadProviderRegistry } from '../lib/provider-extensions.js';
11
13
  import { prepareProviderInput } from '../lib/provider-adapter.js';
12
14
  import { createProviderTaskRunner } from '../lib/provider-task-runner.js';
13
15
  import { generateMusicTask } from '../lib/music-generation.js';
@@ -20,19 +22,24 @@ import { probeVisionModel } from '../lib/vision-model-routing.js';
20
22
  import { RED_TEST_IMAGE } from '../lib/vision.js';
21
23
  import { parseModelRef, modelRef as qualifiedModelRef } from '../lib/models.js';
22
24
  import { coreTaskWaitFinished } from '../lib/task-wait.js';
25
+ import { createLocalReadApi, writeLocalApiTokenFile } from '../lib/local-api.js';
26
+ import { manageCoreSkill } from '../lib/skill-installation.js';
27
+ import { createTrackedProviderAdapter } from '../lib/configured-image-runtime.js';
28
+ import { inspectExecutionLease, recoverExecutionLease } from '../lib/operation-runner.js';
29
+ import { createConnectionPair } from '../lib/core-connections.js';
30
+ import { builtinLaunchPlugins, loadPluginLaunch } from '../lib/plugin-launch.js';
31
+ import { checkLocalApiTokenDirectory, writeLocalApiTokenDirectory } from '../lib/local-api-token-directory.js';
32
+ import { resolveWorkbenchAuthDirectory } from '../lib/workbench-auth.js';
33
+ import { manageWorkbenchPassword } from '../lib/workbench-auth-cli.js';
23
34
 
24
35
  function trackedProviderAdapter(provider, options, task) {
25
- return createConfiguredProviderAdapter(provider, {
26
- modelId: parseModelRef(task?.modelRef)?.modelId,
27
- onModelAvailability: event => recordCatalogAvailability(options['provider-config'], event),
28
- onRateLimit: event => recordCatalogRateLimit(options['provider-config'], event)
29
- });
36
+ return createTrackedProviderAdapter(provider, options['provider-config'], task, options.providerRegistry);
30
37
  }
31
38
 
32
- async function generationCatalog(options, capability, modelRef) {
39
+ async function generationCatalog(options, capability, modelRef, signal = cliAbortController.signal) {
33
40
  const file = required(options['provider-config'], 'provider-config');
34
41
  const catalog = loadProviderCatalog(file);
35
- if (!modelRef) await refreshCatalogDirectories(file, catalog, capability, { signal: cliAbortController.signal });
42
+ if (!modelRef) await refreshCatalogDirectories(file, catalog, capability, { signal, registry: options.providerRegistry });
36
43
  return catalog;
37
44
  }
38
45
 
@@ -48,7 +55,50 @@ function usage() {
48
55
  return [
49
56
  'Usage:',
50
57
  ' JSON input: --input <json>, --input-file <path>, or --input - (stdin).',
58
+ ' --provider-extension <absolute-js-or-mjs-entry> explicitly loads trusted Provider code; repeat for multiple extensions. Experimental v0, no installation or directory discovery.',
59
+ ' dsh-iris providers protocols [--provider-extension <absolute-entry>] lists protocol/configuration descriptions without credentials or model calls.',
60
+ ' dsh-iris plugins list',
61
+ ' dsh-iris plugins inspect --plugin-config <absolute-startup.json>',
62
+ ' Explicit trusted plugin entries, versions, contributions and selections; inspect imports/activates trusted code but does not start Core or grant connections.',
63
+ ' dsh-iris web serve ... --plugin-config <absolute-startup.json> [--auth-store <absolute-private-directory>]',
64
+ ' entries load plugins; operations enables exact business IDs; panel selects pages/settings/renderers. MCP grants remain separate. Do not combine with --provider-extension.',
65
+ ' dsh-iris skills <install|status|uninstall> --target kimi [--directory <absolute-skills-directory>]',
66
+ ' dsh-iris skills <install|status|uninstall> --directory <absolute-skills-directory>',
67
+ ' dsh-iris skills <install|status|uninstall> --bundle <absolute-connection-skill.json> --directory <absolute-skills-directory> [--dry-run]',
68
+ ' install/uninstall accept --dry-run. Copy only iris-core; preserve local changes and archive managed updates/removals outside the scan directory. status is read-only and does not verify host discovery.',
51
69
  ' dsh-iris doctor [--data-root <absolute-path>] [--json]',
70
+ ' dsh-iris api init --data-root <empty-existing-flow-root>',
71
+ ' dsh-iris execution init --data-root <empty-existing-flow-root> [--image-generation|--audio]',
72
+ ' Initialize a NEW v1 request container, v2 with --image-generation, or v3 with --audio (images + speech/transcription). Existing roots are never migrated.',
73
+ ' dsh-iris execution inspect --data-root <v1-flow-root>',
74
+ ' dsh-iris execution recover-lease --data-root <v1-flow-root> --confirm-stale-pid <pid>',
75
+ ' dsh-iris connection create --data-root <execution-flow-root> --directory <new-private-directory> --origin <http://127.0.0.1:port> --label <name> [--harness <generic|kimi|dsh>] [--scopes <read,create,execute,stop>] [--capabilities <image-edit,image-generate,tts,transcribe|none>] [--tools <comma-separated-names|none>]',
76
+ ' dsh-iris api serve ... --allow-execution --provider-config <absolute-path> [--connections-file <private-grants-file>]',
77
+ ' dsh-iris mcp serve --connection-file <private-client-json> [--scopes <read,create,execute,stop>] [--capabilities <image-edit,image-generate,tts,transcribe|none>] [--tools <comma-separated-names|none>]',
78
+ ' Official SDK stdio bridge to an already running Core service. Read-only by default; stdout contains MCP messages only.',
79
+ ' dsh-iris mcp capabilities',
80
+ ' dsh-iris mcp presets',
81
+ ' dsh-iris mcp config --connection-file <private-client-json> [--preset <generic|claude|codex|kimi>-<stdio|http> | --transport <stdio|http> --format <mcpServers|entry|claude-json|codex-toml>] [--output <new-absolute-path>]',
82
+ ' dsh-iris mcp check --connection-file <private-client-json> [--scopes <read,create,execute,stop>]',
83
+ ' stdio config also accepts scopes/capabilities/tools. HTTP config includes credentials and requires a new file inside the connection private directory.',
84
+ ' dsh-iris api serve ... --connections-file <private-grants-file> --allow-mcp',
85
+ ' dsh-iris web serve ... --connection-store <private-directory-outside-core> --allow-connection-management [--allow-mcp] [--allow-execution --provider-config <absolute-path>]',
86
+ ' dsh-iris web serve ... --provider-config <private-config> --allow-provider-management [--panel <trusted-panel-manifest.json>]',
87
+ ' Explicit standard Streamable HTTP endpoint at /mcp; default remains disabled. Uses the existing Core service and user-selected connection tools.',
88
+ ' dsh-iris operation list --data-root <execution-root> [--run-id <run-id>] [--limit <1..30>] [--cursor <operation-id>]',
89
+ ' dsh-iris operation inspect <operation-id> --data-root <execution-root>',
90
+ ' dsh-iris api serve --data-root <existing-flow-root> --token-file <new-absolute-path-outside-root> [--provider-config <absolute-path>] [--port <0..65535>] [--allow-local-writes] [--allow-selection]',
91
+ ' Explicit loopback-only API, read-only by default. Local writes import images/audio and create Runs within the root capabilities, without generation. The session token is saved privately; stdout contains no token.',
92
+ ' dsh-iris web serve --data-root <existing-flow-root> [--auth-store <absolute-private-directory>] [--provider-config <absolute-path>] [--port <0..65535>] [--allow-local-writes] [--allow-execution] [--allow-selection]',
93
+ ' Default: set one access password, then sign in at origin. Remembered browser sessions survive normal restarts. No Token file is required.',
94
+ ' Auth state defaults to $IRIS_HOME/web-auth/<root-key> ($IRIS_HOME defaults to ~/.iris). Open the initial loginUrl once to set your password.',
95
+ ' dsh-iris web login --data-root <existing-flow-root> [--auth-store <absolute-private-directory>]',
96
+ ' dsh-iris web password reset --data-root <existing-flow-root> [--auth-store <absolute-private-directory>]',
97
+ ' Reset only the workbench password/browser sessions; Core work and MCP grants remain intact. Prints a setup link while the service is running.',
98
+ ' Explicit --token-file or --token-dir keeps the legacy Token/link login mode. Do not combine them with --auth-store. api serve still requires one of these Token options.',
99
+ ' dsh-iris web login --origin <http://127.0.0.1:port> --token-file <current-session-token-file>',
100
+ ' Get a fresh one-time login link without restarting the workbench.',
101
+ ' Workbench media APIs are read-only by default; explicit write flags do not require connection management. --allow-selection permits owner-only, revision-checked image selection. Execution enables supported image/speech/transcription flows; Provider management uses its own owner-only flag.',
52
102
  ' dsh-iris runtime recover --data-root <absolute-path> --confirm-stale-pid <pid>',
53
103
  ' dsh-iris providers list --provider-config <absolute-path>',
54
104
  ' dsh-iris capabilities list --provider-config <absolute-path>',
@@ -87,16 +137,24 @@ function usage() {
87
137
  ' dsh-iris artifact export-many --data-root <absolute-path> --input <json> --output <absolute-directory>',
88
138
  ' dsh-iris artifact export <id> --data-root <absolute-path> --output <absolute-path>',
89
139
  ' dsh-iris artifact rebuild --data-root <absolute-path>',
140
+ ' dsh-iris flow init --data-root <new-absolute-path>',
141
+ ' dsh-iris flow create --data-root <absolute-path> --provider-config <absolute-path> --input <image-json>',
142
+ ' dsh-iris models workflow-check <provider::workflow> --provider-config <absolute-path> [--input <workflow_parameters-json>] (read-only)',
143
+ ' Use explicit model_ref and n=1. Edits require an imported source; text-to-image requires a new execution init --image-generation root. create saves private inputs but never submits.',
144
+ ' dsh-iris flow <list|inspect> [run-id] --data-root <absolute-path>',
145
+ ' dsh-iris flow continue <run-id> --data-root <absolute-path> [--provider-config <absolute-path>]',
146
+ ' dsh-iris flow select <run-id> --data-root <absolute-path> --input <json with artifact_id>',
147
+ ' dsh-iris flow export <run-id> --data-root <absolute-path> --output <absolute-path>',
90
148
  ' dsh-iris core <delete|cleanup> --data-root <absolute-path> [--input <json>] [--confirm-delete]',
91
149
  ' Default is a read-only preview. delete selects task_ids/artifact_ids; cleanup executes only explicitly selected preview paths.',
92
150
  ' dsh-iris core transactions --data-root <absolute-path>',
93
151
  ' dsh-iris core restore <transaction-id> --data-root <absolute-path>',
94
152
  '',
95
- 'Commands never start DSH. run image and the task observe/redeliver/cancel/retry verbs contact a provider only when explicitly requested; credentials come from a private file. observe polls once, redeliver only re-downloads, and cancel is recorded as canceled only when the provider explicitly confirms; none of them ever submits an existing task again. task retry creates a NEW task that may incur duplicate generation billing and requires --confirm-billing; the prompt must be supplied again because Core never persists prompts.'
153
+ 'Commands never start DSH. run image and the task observe/redeliver/cancel/retry verbs contact a provider only when explicitly requested; credentials come from a private file. observe polls once, redeliver only re-downloads, and cancel is recorded as canceled only when the provider explicitly confirms; none of them ever submits an existing task again. task retry creates a NEW task that may incur duplicate generation billing and requires --confirm-billing; the prompt must be supplied again because Task records never persist prompts. Flow inputs do retain the prompt privately. flow continue may perform the first submission or observe/redeliver the original task, but never resubmits a task with an Attempt.'
96
154
  ].join('\n');
97
155
  }
98
156
 
99
- function flags(args, allowed) {
157
+ function parseFlags(args, allowed) {
100
158
  const parsed = {};
101
159
  for (let index = 0; index < args.length; index += 2) {
102
160
  const flag = args[index];
@@ -154,6 +212,7 @@ function generationInput(capability, input, options) {
154
212
  /** C-8: catchable signals dispose active runtimes before the CLI exits. */
155
213
  const activeRuntimes = new Set();
156
214
  const cliAbortController = new AbortController();
215
+ let cliProviderRegistry;
157
216
  let shuttingDown = false;
158
217
 
159
218
  function registerRuntime(runtime) {
@@ -164,6 +223,7 @@ function registerRuntime(runtime) {
164
223
  async function disposeActiveRuntimes() {
165
224
  await Promise.allSettled([...activeRuntimes].map((runtime) => runtime.dispose().catch(() => {})));
166
225
  activeRuntimes.clear();
226
+ await cliProviderRegistry?.dispose();
167
227
  }
168
228
 
169
229
  for (const signal of ['SIGINT', 'SIGTERM']) {
@@ -193,12 +253,314 @@ async function withRuntime(mode, dataRoot, callback, ports = {}) {
193
253
  return withRawRuntime(mode, dataRoot, (runtime) => callback(createCommandService(runtime, ports)));
194
254
  }
195
255
 
196
- async function main(args) {
256
+ async function withApplication(dataRoot, callback, resolveImageCandidates, resolveTaskAdapter) {
257
+ const application = createApplicationRuntime({ dataRoot: required(dataRoot, 'data-root'), resolveImageCandidates, resolveTaskAdapter });
258
+ const unregister = registerRuntime(application);
259
+ try { return await callback(application); }
260
+ finally { try { await application.dispose(); } finally { unregister(); } }
261
+ }
262
+
263
+ /** Own the service and its plugin registry in order, including cancellation during import/start. */
264
+ async function withCliLaunch(pluginConfig, launchOptions, callback) {
265
+ let loading, launch, api, disposal, startupFailure;
266
+ const detail = error => ({ code: error.code || 'IRIS_CLI_FAILED', message: error.message || String(error),
267
+ ...(error instanceof AggregateError ? { errors: error.errors.map(detail) } : {}) });
268
+ const lifetime = { dispose() {
269
+ if (!disposal) disposal = (async () => {
270
+ if (loading) await Promise.allSettled([loading]); // main retains the original startup error.
271
+ const failures = [];
272
+ try { await api?.dispose(); } catch (error) { failures.push({ stage: 'core-stop', error }); }
273
+ try { await launch?.dispose(); } catch (error) { failures.push({ stage: 'plugin-stop', error }); }
274
+ if (failures.length) {
275
+ const error = Object.assign(new AggregateError(failures.map(item => item.error), 'Core 服务或插件释放失败;已尝试两阶段收尾'), { code: 'IRIS_CLI_SERVICE_CLEANUP_FAILED' });
276
+ error.diagnostic = { code: error.code, stage: 'shutdown', message: error.message, ...(pluginConfig ? { file: pluginConfig } : {}),
277
+ errors: failures.map(item => ({ stage: item.stage, ...detail(item.error) })) };
278
+ if (cliAbortController.signal.aborted) console.error(JSON.stringify(error.diagnostic));
279
+ throw error;
280
+ }
281
+ })();
282
+ return disposal;
283
+ } };
284
+ const unregister = registerRuntime(lifetime);
285
+ try {
286
+ loading = pluginConfig ? loadPluginLaunch(pluginConfig, launchOptions).then(value => (launch = value)) : Promise.resolve();
287
+ await loading; cliAbortController.signal.throwIfAborted();
288
+ return await callback(launch, value => { api = value; });
289
+ } catch (error) { startupFailure = error; throw error; }
290
+ finally {
291
+ try { await lifetime.dispose(); }
292
+ catch (error) { if (startupFailure) error.diagnostic.startup = detail(startupFailure); throw error; }
293
+ finally { unregister(); }
294
+ }
295
+ }
296
+
297
+ async function main(args, providerRegistry) {
298
+ const flags = (values, allowed) => Object.defineProperty(parseFlags(values, allowed), 'providerRegistry', { value: providerRegistry });
197
299
  if (!args.length || args.includes('--help') || args.includes('-h')) {
198
300
  console.log(usage());
199
301
  return args.length ? 0 : 2;
200
302
  }
201
303
 
304
+ if (args[0] === 'providers' && args[1] === 'protocols') {
305
+ flags(args.slice(2), []);
306
+ console.log(JSON.stringify(providerRegistry.snapshot(), null, 2)); return 0;
307
+ }
308
+
309
+ if (args[0] === 'plugins') {
310
+ if (args[1] === 'list') {
311
+ flags(args.slice(2), []);
312
+ console.log(JSON.stringify({ contractVersion: 0, builtins: builtinLaunchPlugins(), automaticDiscovery: false })); return 0;
313
+ }
314
+ if (args[1] !== 'inspect') throw new CliUsageError('plugins 仅支持 list/inspect');
315
+ const options = flags(args.slice(2), ['plugin-config']);
316
+ return withCliLaunch(required(options['plugin-config'], 'plugin-config'), {}, async launch => {
317
+ console.log(JSON.stringify(launch.summary, null, 2)); return 0;
318
+ });
319
+ }
320
+
321
+ if (args[0] === 'skills') {
322
+ const rest = args.slice(2), dryRunFlags = rest.filter(value => value === '--dry-run');
323
+ if (dryRunFlags.length > 1) throw new CliUsageError('参数重复:--dry-run');
324
+ const options = flags(rest.filter(value => value !== '--dry-run'), ['target', 'directory', 'bundle']);
325
+ let result;
326
+ try {
327
+ result = manageCoreSkill({ action: args[1], target: options.target, directory: options.directory, bundle: options.bundle, dryRun: dryRunFlags.length === 1 });
328
+ } catch (error) {
329
+ if (error.code === 'IRIS_SKILL_OPTIONS_INVALID') throw new CliUsageError(error.message);
330
+ throw error;
331
+ }
332
+ console.log(JSON.stringify(result, null, 2));
333
+ return args[1] === 'status' && result.status !== 'current' ? 1 : 0;
334
+ }
335
+
336
+ if (args[0] === 'mcp') {
337
+ if (args[1] === 'presets') {
338
+ flags(args.slice(2), []);
339
+ const { MCP_CONNECTION_PRESETS } = await import('../lib/mcp-delivery.js');
340
+ console.log(JSON.stringify({ contractVersion: 0, presets: MCP_CONNECTION_PRESETS, installationDetected: false }, null, 2)); return 0;
341
+ }
342
+ if (args[1] === 'capabilities') {
343
+ flags(args.slice(2), []);
344
+ const { BRIDGE_CAPABILITIES, BRIDGE_CONTROL_TOOLS } = await import('../lib/core-bridge-selection.js');
345
+ console.log(JSON.stringify({ contractVersion: 0, capabilities: BRIDGE_CAPABILITIES, controlTools: BRIDGE_CONTROL_TOOLS,
346
+ implementedMedia: BRIDGE_CAPABILITIES.map(group => group.recipe), selectionDoesNotGenerate: true }, null, 2)); return 0;
347
+ }
348
+ if (!['serve', 'config', 'check'].includes(args[1])) throw new CliUsageError('mcp 支持 serve/config/check/capabilities/presets');
349
+ const options = flags(args.slice(2), ['connection-file', 'scopes', 'capabilities', 'tools', ...(args[1] === 'config' ? ['transport', 'format', 'preset', 'output'] : [])]);
350
+ const list = value => value === 'none' ? [] : value.split(',');
351
+ const selected = { ...(options.capabilities !== undefined ? { capabilities: list(options.capabilities) } : {}),
352
+ ...(options.tools !== undefined ? { tools: list(options.tools) } : {}) };
353
+ if (args[1] === 'check') {
354
+ const { checkMcpConnection } = await import('../lib/mcp-check.js');
355
+ const result = await checkMcpConnection({ connectionFile: required(options['connection-file'], 'connection-file'),
356
+ ...(options.scopes !== undefined ? { scopes: options.scopes.split(',') } : {}), ...selected });
357
+ console.log(JSON.stringify(result, null, 2)); return result.ok ? 0 : 1;
358
+ }
359
+ if (args[1] === 'config') {
360
+ const { exportMcpConfiguration } = await import('../lib/mcp-config.js');
361
+ const result = exportMcpConfiguration({ connectionFile: required(options['connection-file'], 'connection-file'), ...selected,
362
+ ...(options.scopes !== undefined ? { scopes: options.scopes.split(',') } : {}), transport: options.transport, format: options.format, presetId: options.preset, output: options.output });
363
+ process.stdout.write(typeof result === 'string' ? result : JSON.stringify(result, null, 2) + '\n');
364
+ return 0;
365
+ }
366
+ const { createMcpBridge } = await import('../lib/mcp-bridge.js');
367
+ const manifest = JSON.parse(fs.readFileSync(new URL('../package.json', import.meta.url), 'utf8'));
368
+ const bridge = createMcpBridge({ connectionFile: required(options['connection-file'], 'connection-file'),
369
+ requestedScopes: (options.scopes === undefined ? 'read' : options.scopes).split(','), ...selected, version: manifest.version });
370
+ const unregister = registerRuntime(bridge);
371
+ try { await bridge.start(); await bridge.closed; return 0; }
372
+ finally { try { await bridge.dispose(); } finally { unregister(); } }
373
+ }
374
+ if (args[0] === 'connection') {
375
+ if (args[1] !== 'create') throw new CliUsageError('connection 只支持显式 create');
376
+ const options = flags(args.slice(2), ['data-root', 'directory', 'origin', 'harness', 'label', 'scopes', 'capabilities', 'tools']);
377
+ const result = createConnectionPair({ dataRoot: required(options['data-root'], 'data-root'), directory: required(options.directory, 'directory'), origin: required(options.origin, 'origin'),
378
+ harnessKind: options.harness === undefined ? 'generic' : options.harness, label: required(options.label, 'label'),
379
+ ...(options.scopes !== undefined ? { scopes: options.scopes.split(',') } : {}),
380
+ ...(options.capabilities !== undefined ? { capabilities: options.capabilities === 'none' ? [] : options.capabilities.split(',') } : {}),
381
+ ...(options.tools !== undefined ? { tools: options.tools === 'none' ? [] : options.tools.split(',') } : {}) });
382
+ console.log(JSON.stringify(result, null, 2)); return 0;
383
+ }
384
+ if (args[0] === 'execution' || args[0] === 'operation') {
385
+ if (args[0] === 'execution') {
386
+ if (['inspect', 'recover-lease'].includes(args[1])) {
387
+ const options = flags(args.slice(2), ['data-root', ...(args[1] === 'recover-lease' ? ['confirm-stale-pid'] : [])]);
388
+ const dataRoot = required(options['data-root'], 'data-root');
389
+ if (args[1] === 'recover-lease' && !/^[1-9][0-9]*$/.test(options['confirm-stale-pid'] || '')) throw new CliUsageError('需要明确回显 stale owner PID');
390
+ const result = args[1] === 'inspect' ? inspectExecutionLease(dataRoot)
391
+ : recoverExecutionLease(dataRoot, { confirmStalePid: Number(options['confirm-stale-pid']) });
392
+ console.log(JSON.stringify(result, null, 2)); return 0;
393
+ }
394
+ if (args[1] !== 'init') throw new CliUsageError('不支持的 execution 子命令');
395
+ const initArgs = args.slice(2), generationFlags = initArgs.filter(value => value === '--image-generation'), audioFlags = initArgs.filter(value => value === '--audio');
396
+ if (generationFlags.length > 1 || audioFlags.length > 1 || generationFlags.length && audioFlags.length) throw new CliUsageError('初始化开关重复或冲突;audio已包含文生图');
397
+ const options = flags(initArgs.filter(value => !['--image-generation', '--audio'].includes(value)), ['data-root']);
398
+ const suppliedRoot = required(options['data-root'], 'data-root');
399
+ if (!path.isAbsolute(suppliedRoot)) throw new CliUsageError('data-root 必须是已初始化的空 Flow 根绝对路径');
400
+ let dataRoot;
401
+ try { dataRoot = fs.realpathSync(suppliedRoot); }
402
+ catch (_) { throw new CliUsageError('请先在新的专用数据根执行 flow init'); }
403
+ const result = await withApplication(dataRoot, application =>
404
+ application.execute('execution.init', audioFlags.length ? { audio: true } : generationFlags.length ? { imageGeneration: true } : {}, { signal: cliAbortController.signal }));
405
+ console.log(JSON.stringify(result, null, 2)); return 0;
406
+ }
407
+ if (!['list', 'inspect'].includes(args[1])) throw new CliUsageError('operation 只支持只读 list/inspect');
408
+ const inspecting = args[1] === 'inspect';
409
+ if (inspecting && (!args[2] || args[2].startsWith('--'))) throw new CliUsageError('缺少 Operation ID');
410
+ const options = flags(args.slice(inspecting ? 3 : 2), ['data-root', ...(inspecting ? [] : ['run-id', 'limit', 'cursor'])]);
411
+ if (options.limit !== undefined && !/^(?:[1-9]|[12][0-9]|30)$/.test(options.limit)) throw new CliUsageError('limit 必须是 1–30 的整数');
412
+ const input = inspecting ? { id: args[2] } : { ...(options['run-id'] ? { runId: options['run-id'] } : {}),
413
+ ...(options.limit !== undefined ? { limit: Number(options.limit) } : {}), ...(options.cursor ? { cursor: options.cursor } : {}) };
414
+ const result = await withApplication(required(options['data-root'], 'data-root'), application =>
415
+ application.execute('operation.' + args[1], input, { signal: cliAbortController.signal }));
416
+ console.log(JSON.stringify(result, null, 2)); return 0;
417
+ }
418
+
419
+ if (args[0] === 'api' || args[0] === 'web') {
420
+ const serveWorkbench = args[0] === 'web';
421
+ if (serveWorkbench && args[1] === 'password') {
422
+ if (args[2] !== 'reset') throw new CliUsageError('web password 仅支持 reset');
423
+ const options = flags(args.slice(3), ['data-root', 'auth-store']);
424
+ const dataRoot = required(options['data-root'], 'data-root');
425
+ if (!path.isAbsolute(dataRoot)) throw new CliUsageError('data-root 必须是工作台数据根的绝对路径');
426
+ console.log(JSON.stringify(await manageWorkbenchPassword({ dataRoot, directory: options['auth-store'], reset: true, signal: cliAbortController.signal })));
427
+ return 0;
428
+ }
429
+ if (serveWorkbench && args[1] === 'login') {
430
+ const options = flags(args.slice(2), ['origin', 'token-file', 'data-root', 'auth-store']);
431
+ if (options['data-root'] !== undefined) {
432
+ if (options.origin !== undefined || options['token-file'] !== undefined) throw new CliUsageError('口令登录用 data-root,旧 Token 登录用 origin/token-file,不能混用');
433
+ if (!path.isAbsolute(options['data-root'])) throw new CliUsageError('data-root 必须是工作台数据根的绝对路径');
434
+ console.log(JSON.stringify(await manageWorkbenchPassword({ dataRoot: options['data-root'], directory: options['auth-store'], signal: cliAbortController.signal })));
435
+ return 0;
436
+ }
437
+ if (options['auth-store'] !== undefined) throw new CliUsageError('auth-store 需要同时明确 data-root');
438
+ const origin = required(options.origin, 'origin'), tokenFile = required(options['token-file'], 'token-file');
439
+ if (!/^http:\/\/127\.0\.0\.1:[1-9]\d{0,4}$/.test(origin) || Number(origin.split(':').at(-1)) > 65535) throw new CliUsageError('origin 必须是启动时输出的 http://127.0.0.1:端口');
440
+ if (!path.isAbsolute(tokenFile)) throw new CliUsageError('token-file 必须是当前启动文件的绝对路径');
441
+ let token;
442
+ const fd = fs.openSync(tokenFile, 'r');
443
+ try {
444
+ const info = fs.fstatSync(fd);
445
+ if (!info.isFile() || info.size > 256) throw new CliUsageError('请选择当前启动生成的小型 Token 文本文件');
446
+ token = fs.readFileSync(fd, 'utf8').trim();
447
+ } finally { fs.closeSync(fd); }
448
+ if (!/^[A-Za-z0-9_-]{43}$/.test(token)) throw new CliUsageError('Token 文件内容无效,请使用当前启动生成的文件');
449
+ const response = await fetch(origin + '/api/v0/workbench/login-link', { method: 'POST',
450
+ headers: { Authorization: 'Bearer ' + token, 'Content-Type': 'application/json' }, body: '{}',
451
+ redirect: 'error', signal: AbortSignal.any([cliAbortController.signal, AbortSignal.timeout(15000)]) });
452
+ if (!response.ok) throw new CliUsageError(response.status === 401 ? 'Token 已失效,请使用当前服务启动时生成的 Token 文件'
453
+ : response.status === 404 || response.status === 405 ? '此服务不支持登录链接,请确认地址属于新版 web serve 工作台' : '无法获取登录链接,请检查本机工作台状态');
454
+ const result = await response.json();
455
+ if (result.contractVersion !== 0 || typeof result.loginUrl !== 'string' || !result.loginUrl.startsWith(origin + '/#login=')
456
+ || !/^[A-Za-z0-9_-]{43}$/.test(result.loginUrl.slice((origin + '/#login=').length)) || !Number.isFinite(Date.parse(result.loginExpiresAt))) {
457
+ throw new CliUsageError('工作台返回的登录链接无效');
458
+ }
459
+ console.log(JSON.stringify({ ...result, message: '打开 loginUrl 即可进入;链接 10 分钟内有效、仅用一次,刷新无需重新登录。' }));
460
+ return 0;
461
+ }
462
+ if (!serveWorkbench && args[1] === 'init') {
463
+ const options = flags(args.slice(2), ['data-root']);
464
+ const suppliedRoot = required(options['data-root'], 'data-root');
465
+ if (!path.isAbsolute(suppliedRoot)) throw new CliUsageError('data-root 必须是已初始化的空 Flow 根绝对路径');
466
+ let dataRoot;
467
+ try { dataRoot = fs.realpathSync(suppliedRoot); }
468
+ catch (_) { throw new CliUsageError('请先在新的专用数据根执行 flow init'); }
469
+ const initialized = await withApplication(dataRoot, application => application.execute('creation.init', {}, { signal: cliAbortController.signal }));
470
+ console.log(JSON.stringify(initialized));
471
+ return 0;
472
+ }
473
+ if (args[1] !== 'serve') throw new CliUsageError(`不支持的 ${args[0]} 子命令`);
474
+ const rest = args.slice(2), localWriteFlags = rest.filter(value => value === '--allow-local-writes'), executionFlags = rest.filter(value => value === '--allow-execution'), selectionFlags = rest.filter(value => value === '--allow-selection'), mcpFlags = rest.filter(value => value === '--allow-mcp'), managementFlags = rest.filter(value => value === '--allow-connection-management'), providerFlags = rest.filter(value => value === '--allow-provider-management');
475
+ if (localWriteFlags.length > 1 || executionFlags.length > 1 || selectionFlags.length > 1 || mcpFlags.length > 1 || managementFlags.length > 1 || providerFlags.length > 1) throw new CliUsageError('写入/执行/选择/MCP/管理参数重复');
476
+ const allowExecution = executionFlags.length === 1, allowLocalWrites = localWriteFlags.length === 1 || allowExecution;
477
+ const allowSelection = selectionFlags.length === 1;
478
+ const allowConnectionManagement = managementFlags.length === 1;
479
+ const options = flags(rest.filter(value => !['--allow-local-writes', '--allow-execution', '--allow-selection', '--allow-mcp', '--allow-connection-management', '--allow-provider-management'].includes(value)), ['data-root', 'token-file', 'token-dir', 'auth-store', 'provider-config', 'port', 'connections-file', 'connection-store', 'panel', 'plugin-config']);
480
+ const suppliedRoot = required(options['data-root'], 'data-root');
481
+ const passwordMode = serveWorkbench && options['token-file'] === undefined && options['token-dir'] === undefined;
482
+ if (!passwordMode && Boolean(options['token-file']) === Boolean(options['token-dir'])) throw new CliUsageError('请选择且只选择 --token-file 或 --token-dir');
483
+ if (options['auth-store'] !== undefined && !passwordMode) throw new CliUsageError('auth-store 仅用于默认口令工作台;不能与 Token 参数或 api serve 混用');
484
+ if (!path.isAbsolute(suppliedRoot) || !passwordMode && !path.isAbsolute(options['token-file'] || options['token-dir'])) throw new CliUsageError('data-root 与 token-file/token-dir 必须是绝对路径');
485
+ let tokenFile = options['token-file'];
486
+ if (options.port !== undefined && (!/^\d+$/.test(options.port) || Number(options.port) > 65535)) throw new CliUsageError('port 必须是 0–65535 的整数');
487
+ // Pin the physical root for both the server and the outside-root credential check.
488
+ let dataRoot;
489
+ try { dataRoot = fs.realpathSync(suppliedRoot); }
490
+ catch (_) { throw new CliUsageError('data-root 必须是已初始化且可读取的 Flow 根'); }
491
+ const authDirectory = passwordMode ? resolveWorkbenchAuthDirectory(dataRoot, options['auth-store']) : undefined;
492
+ if (options['token-dir']) checkLocalApiTokenDirectory(options['token-dir'], dataRoot);
493
+ return withCliLaunch(options['plugin-config'], { serveWorkbench, panelManifestPath: options.panel }, async (launch, ownApi) => {
494
+ const api = createLocalReadApi({ dataRoot, providerConfigPath: options['provider-config'], allowLocalWrites, serveWorkbench,
495
+ ...(passwordMode ? { workbenchAuth: { directory: authDirectory } } : {}),
496
+ allowExecution, allowSelection, allowMcp: mcpFlags.length === 1, connectionsFile: options['connections-file'], allowConnectionManagement, connectionStore: options['connection-store'], providerRegistry,
497
+ ...(launch ? { pluginRegistry: launch.registry, providerRegistry: launch.registry.providers, allowedOperations: launch.allowedOperations,
498
+ panelContributions: launch.panelContributions } : {}),
499
+ allowProviderManagement: providerFlags.length === 1, panelManifestPath: options.panel });
500
+ ownApi(api);
501
+ const started = await api.start({ port: options.port === undefined ? 0 : Number(options.port) });
502
+ if (!passwordMode) {
503
+ if (options['token-dir']) tokenFile = writeLocalApiTokenDirectory(options['token-dir'], started.accessToken, dataRoot);
504
+ else writeLocalApiTokenFile(tokenFile, started.accessToken, dataRoot);
505
+ }
506
+ console.log(JSON.stringify({ contractVersion: 0, readOnly: !allowLocalWrites && !allowSelection, origin: started.origin,
507
+ ...(passwordMode ? { authentication: started.authentication, authStore: authDirectory } : { tokenFile }),
508
+ ...(launch ? { pluginLaunch: launch.summary } : {}),
509
+ ...(serveWorkbench ? { loginUrl: started.loginUrl, loginExpiresAt: started.loginExpiresAt } : {}),
510
+ ...(allowConnectionManagement ? { connectionManagement: true } : {}),
511
+ ...(providerFlags.length ? { providerManagement: true } : {}),
512
+ ...(started.mcpEndpoint ? { mcpEndpoint: started.mcpEndpoint } : {}),
513
+ message: passwordMode ? (started.authentication.configured ? '工作台已启动,打开 origin 用访问口令登录;记住设备后可跨正常重启继续使用。'
514
+ : '首次使用请打开 loginUrl 设置访问口令;之后打开普通地址即可登录,无需 Token 文件。')
515
+ : serveWorkbench ? (providerFlags.length ? 'Core 工作台与供应商管理已启动;打开 loginUrl 即可进入。配置保存不生成媒体。'
516
+ : allowConnectionManagement ? 'Core 工作台与连接管理已启动;打开 loginUrl 即可进入。配置连接不执行媒体。'
517
+ : allowLocalWrites || allowSelection ? 'Core 工作台已启动;打开 loginUrl 即可进入。媒体 API 按显式开关开放。'
518
+ : launch?.allowedOperations !== undefined ? 'Core 工作台已启动;打开 loginUrl 即可进入。插件业务按启动选择开放,媒体 API 保持只读。'
519
+ : '只读工作台已启动;打开 loginUrl 即可进入。')
520
+ : allowExecution ? '有限执行 API 已启动;显式继续才执行,停止只中止本地等待,重启不自动重提。'
521
+ : allowLocalWrites ? '本地创建 API 已启动;只上传/创建,不执行生成。会话 Token 在指定私有文件中,退出后失效。'
522
+ : allowSelection ? '本地选择 API 已启动;只更新已完成流程的选择,不执行生成。会话 Token 在指定私有文件中,退出后失效。'
523
+ : launch?.allowedOperations !== undefined ? '插件业务 API 已启动;按启动选择执行,媒体 API 保持只读。会话 Token 在指定私有文件中,退出后失效。'
524
+ : '只读本地 API 已启动;会话 Token 在指定私有文件中,退出后失效。' }));
525
+ await api.closed;
526
+ return 0;
527
+ });
528
+ }
529
+
530
+ if (args[0] === 'flow') {
531
+ const verb = args[1];
532
+ if (!['init', 'create', 'list', 'inspect', 'continue', 'select', 'export'].includes(verb)) throw new CliUsageError('不支持的 flow 子命令');
533
+ const needsId = ['inspect', 'continue', 'select', 'export'].includes(verb);
534
+ if (needsId && (!args[2] || args[2].startsWith('--'))) throw new CliUsageError('缺少 Run ID');
535
+ const allowed = ['data-root', ...(['create', 'continue'].includes(verb) ? ['provider-config'] : []),
536
+ ...(['create', 'select'].includes(verb) ? ['input'] : []), ...(verb === 'export' ? ['output'] : [])];
537
+ const options = flags(args.slice(needsId ? 3 : 2), allowed);
538
+ const input = ['create', 'select'].includes(verb) ? jsonInput(options.input) : {};
539
+ if (verb === 'select' && Object.keys(input).some(key => key !== 'artifact_id')) throw new CliUsageError('flow select 只接受 artifact_id');
540
+ if (needsId) input.run_id = args[2];
541
+ if (verb === 'export') input.output_path = required(options.output, 'output');
542
+ const resolveImageCandidates = options['provider-config'] ? async ({ modelRef, signal }) => {
543
+ const catalog = await generationCatalog(options, 'image-gen', modelRef, signal);
544
+ return imageCandidatesFromCatalog(catalog, modelRef, providerRegistry).map(route => ({ adapter: trackedProviderAdapter(route.provider, options),
545
+ model: route.modelRef, supportsImageEdit: supportsImageEdit(route.provider.mediaProtocol, route.model, providerRegistry),
546
+ selectionReason: route.selectionReason, providerBinding: providerTaskBinding(route.provider, providerRegistry) }));
547
+ } : undefined;
548
+ const resolveTaskAdapter = options['provider-config'] ? task => {
549
+ const catalog = loadProviderCatalog(options['provider-config']);
550
+ return trackedProviderAdapter(providerForTaskFromCatalog(catalog, task, providerRegistry), options, task);
551
+ } : undefined;
552
+ let result;
553
+ try {
554
+ result = await withApplication(options['data-root'], application =>
555
+ application.execute('flow.' + verb, input, { signal: cliAbortController.signal }), resolveImageCandidates, resolveTaskAdapter);
556
+ } catch (error) {
557
+ if (error instanceof GenerationInputError) throw new CliUsageError(error.message);
558
+ throw error;
559
+ }
560
+ console.log(JSON.stringify(result, null, 2));
561
+ return 0;
562
+ }
563
+
202
564
  if (args[0] === 'doctor') {
203
565
  const rest = args.slice(1);
204
566
  if (rest.filter((arg) => arg === '--json').length > 1) throw new CliUsageError('参数重复:--json');
@@ -235,14 +597,14 @@ async function main(args) {
235
597
  if (['providers', 'capabilities'].includes(args[0]) && args[1] === 'list') {
236
598
  const options = flags(args.slice(2), ['provider-config']);
237
599
  const catalog = loadProviderCatalog(required(options['provider-config'], 'provider-config'));
238
- const report = args[0] === 'providers' ? providerCatalogSnapshot(catalog) : catalogCapabilitySnapshot(catalog);
600
+ const report = args[0] === 'providers' ? providerCatalogSnapshot(catalog) : catalogCapabilitySnapshot(catalog, providerRegistry);
239
601
  console.log(JSON.stringify(report, null, 2));
240
602
  return 0;
241
603
  }
242
604
 
243
605
  if (['config', 'providers', 'models', 'assignments'].includes(args[0])) {
244
606
  const [group, verb] = args;
245
- const verbs = { config: ['init', 'show', 'check'], providers: ['add', 'set', 'remove'], models: ['list', 'add', 'remove', 'caps', 'protocol', 'music-protocol', 'vision-input', 'discover', 'test'], assignments: ['list', 'set', 'clear'] };
607
+ const verbs = { config: ['init', 'show', 'check'], providers: ['add', 'set', 'remove'], models: ['list', 'add', 'remove', 'caps', 'protocol', 'music-protocol', 'vision-input', 'discover', 'test', 'workflow-check'], assignments: ['list', 'set', 'clear'] };
246
608
  if (!verbs[group].includes(verb)) throw new CliUsageError('未知配置命令');
247
609
  const positional = (group === 'providers' && verb === 'remove') || (group === 'models' && verb !== 'list');
248
610
  const id = positional ? required(args[2], group === 'providers' || verb === 'discover' ? 'provider-id' : 'model-ref') : undefined;
@@ -259,7 +621,7 @@ async function main(args) {
259
621
  }
260
622
  }
261
623
  const result = await executeConfigCommand(required(options['provider-config'], 'provider-config'), group === 'config' ? verb : group + '.' + verb, input, {
262
- signal: cliAbortController.signal,
624
+ signal: cliAbortController.signal, registry: providerRegistry,
263
625
  probe: async ({ provider, modelId, capability, signal }) => {
264
626
  if (capability === 'music-gen') return { skipped: true, ok: false, reason: 'Music 不发起小样本探测;请使用 run music 提交创作要求' };
265
627
  if (capability === 'vision') return probeVisionModel(provider, modelId, { bytes: Buffer.from(RED_TEST_IMAGE.split(',')[1], 'base64'), mediaType: 'image/png' }, { signal });
@@ -272,14 +634,14 @@ async function main(args) {
272
634
  }
273
635
  return withRawRuntime('writer', options['data-root'], async runtime => {
274
636
  const mediaCapability = capability === 'image-gen' ? 'image' : capability === 'video-gen' ? 'video' : capability;
275
- if (mediaCapability === 'image') provider = providerForImageModel(provider, modelId);
276
- const adapter = createConfiguredProviderAdapter(provider, { allowRateLimited: true, modelId });
637
+ if (mediaCapability === 'image') provider = providerForImageModel(provider, modelId, providerRegistry);
638
+ const adapter = createConfiguredProviderAdapter(provider, { allowRateLimited: true, modelId, registry: providerRegistry });
277
639
  let providerInput = mediaCapability === 'image'
278
- ? { prompt: 'red circle', n: 1, ...(['openai-chat-images', 'openai-responses-images'].includes(adapter.protocol) ? {} : {
640
+ ? { prompt: 'red circle', n: 1, ...(['openai-chat-images', 'openai-responses-images', 'comfyui'].includes(adapter.protocol) ? {} : {
279
641
  size: adapter.protocol === 'openai-images' ? '256x256' : '512*512'
280
642
  }) }
281
643
  : { text: '你好', voice: probeInput.voice || provider.ttsVoice || 'Cherry' };
282
- let candidates = [{ adapter, model: qualifiedModelRef(provider.id, modelId), selectionReason: 'explicit', providerBinding: providerTaskBinding(provider) }];
644
+ let candidates = [{ adapter, model: qualifiedModelRef(provider.id, modelId), selectionReason: 'explicit', providerBinding: providerTaskBinding(provider, providerRegistry) }];
283
645
  if (['video', 'transcribe'].includes(mediaCapability)) {
284
646
  const normalized = generationInput(mediaCapability, { ...probeInput, model_ref: qualifiedModelRef(provider.id, modelId) },
285
647
  { allowVideoPaths: true, allowAudioPath: true });
@@ -320,8 +682,8 @@ async function main(args) {
320
682
  } catch (_) { throw new CliUsageError('输出文件已存在或父目录不可写'); }
321
683
  }
322
684
  const catalog = await generationCatalog(options, 'vision', options['model-ref'] || input.model_ref);
323
- if (input.transcribe && !input.transcribe_model_ref) await refreshCatalogDirectories(options['provider-config'], catalog, 'transcribe', { signal: cliAbortController.signal });
324
- const execute = runtime => executeVisionCommand({ command, input, catalog, runtime,
685
+ if (input.transcribe && !input.transcribe_model_ref) await refreshCatalogDirectories(options['provider-config'], catalog, 'transcribe', { signal: cliAbortController.signal, registry: providerRegistry });
686
+ const execute = runtime => executeVisionCommand({ command, input, catalog, runtime, registry: providerRegistry,
325
687
  onRateLimit: event => recordCatalogRateLimit(options['provider-config'], event),
326
688
  onModelAvailability: event => recordCatalogAvailability(options['provider-config'], event),
327
689
  modelRef: options['model-ref'] || '', signal: cliAbortController.signal,
@@ -358,8 +720,9 @@ async function main(args) {
358
720
  const options = flags(args.slice(2), ['data-root', 'input']);
359
721
  const input = jsonInput(options.input);
360
722
  const command = args[1] === 'diff' ? 'media.diff' : 'media.frames';
361
- const result = await withRuntime('writer', options['data-root'], (commands) =>
362
- commands.execute(command, input));
723
+ const result = args[1] === 'diff'
724
+ ? await withApplication(options['data-root'], application => application.execute(command, input, { signal: cliAbortController.signal }))
725
+ : await withRuntime('writer', options['data-root'], commands => commands.execute(command, input));
363
726
  console.log(JSON.stringify(result, null, 2));
364
727
  return 0;
365
728
  }
@@ -375,21 +738,25 @@ async function main(args) {
375
738
 
376
739
  if (args[0] === 'run' && args[1] === 'image') {
377
740
  const options = flags(args.slice(2), ['data-root', 'provider-config', 'input']);
378
- const { providerInput, modelRef, sourceArtifactId } = generationInput('image', jsonInput(options.input));
379
- const catalog = await generationCatalog(options, 'image-gen', modelRef);
380
- const routes = imageCandidatesFromCatalog(catalog, modelRef);
381
- await withRawRuntime('writer', options['data-root'], async (runtime) => {
382
- const candidates = routes.map((route) => ({
741
+ const input = jsonInput(options.input);
742
+ const resolveImageCandidates = async ({ modelRef, signal }) => {
743
+ const catalog = await generationCatalog(options, 'image-gen', modelRef, signal);
744
+ return imageCandidatesFromCatalog(catalog, modelRef, providerRegistry).map(route => ({
383
745
  adapter: trackedProviderAdapter(route.provider, options), model: route.modelRef,
746
+ supportsImageEdit: supportsImageEdit(route.provider.mediaProtocol, route.model, providerRegistry),
384
747
  selectionReason: route.selectionReason,
385
- providerBinding: providerTaskBinding(route.provider)
748
+ providerBinding: providerTaskBinding(route.provider, providerRegistry)
386
749
  }));
387
- const result = await createProviderTaskRunner(runtime).submit({
388
- capability: 'image', candidates,
389
- providerInput, sourceArtifactId
390
- });
391
- console.log(JSON.stringify(result, null, 2));
392
- });
750
+ };
751
+ let result;
752
+ try {
753
+ result = await withApplication(options['data-root'], application =>
754
+ application.generateImage(input, { signal: cliAbortController.signal }), resolveImageCandidates);
755
+ } catch (error) {
756
+ if (error instanceof GenerationInputError) throw new CliUsageError(error.message);
757
+ throw error;
758
+ }
759
+ console.log(JSON.stringify(result, null, 2));
393
760
  return 0;
394
761
  }
395
762
 
@@ -399,12 +766,12 @@ async function main(args) {
399
766
  const normalized = generationInput('video', jsonInput(options.input), { allowVideoPaths: true });
400
767
  const { providerInput, modelRef } = normalized;
401
768
  const catalog = await generationCatalog(options, 'video-gen', modelRef);
402
- const routes = videoCandidatesFromCatalog(catalog, modelRef);
769
+ const routes = videoCandidatesFromCatalog(catalog, modelRef, providerRegistry);
403
770
  await withRawRuntime('writer', options['data-root'], async (runtime) => {
404
771
  const candidates = videoTaskCandidates(routes.map((route) => ({
405
772
  adapter: trackedProviderAdapter(route.provider, options), model: route.modelRef,
406
773
  selectionReason: route.selectionReason,
407
- providerBinding: providerTaskBinding(route.provider)
774
+ providerBinding: providerTaskBinding(route.provider, providerRegistry)
408
775
  })), normalized);
409
776
  const result = await createProviderTaskRunner(runtime).submit({
410
777
  capability: 'video', candidates,
@@ -420,12 +787,12 @@ async function main(args) {
420
787
  const options = flags(args.slice(2), ['data-root', 'provider-config', 'input']);
421
788
  const normalized = generationInput('music', jsonInput(options.input));
422
789
  const catalog = await generationCatalog(options, 'music-gen', normalized.modelRef);
423
- const routes = musicCandidatesFromCatalog(catalog, normalized.modelRef);
790
+ const routes = musicCandidatesFromCatalog(catalog, normalized.modelRef, providerRegistry);
424
791
  let result;
425
792
  await withRawRuntime('writer', options['data-root'], async runtime => {
426
793
  result = await generateMusicTask({ runner: createProviderTaskRunner(runtime),
427
794
  candidates: routes.map(route => ({ adapter: trackedProviderAdapter(route.provider, options), model: route.modelRef,
428
- selectionReason: route.selectionReason, providerBinding: providerTaskBinding(route.provider) })),
795
+ selectionReason: route.selectionReason, providerBinding: providerTaskBinding(route.provider, providerRegistry) })),
429
796
  providerInput: normalized.providerInput, options: normalized.musicOptions, signal: cliAbortController.signal });
430
797
  console.log(JSON.stringify(result, null, 2));
431
798
  });
@@ -436,12 +803,12 @@ async function main(args) {
436
803
  const options = flags(args.slice(2), ['data-root', 'provider-config', 'input']);
437
804
  const { providerInput, modelRef } = generationInput('tts', jsonInput(options.input));
438
805
  const catalog = await generationCatalog(options, 'tts', modelRef);
439
- const routes = ttsCandidatesFromCatalog(catalog, modelRef);
806
+ const routes = ttsCandidatesFromCatalog(catalog, modelRef, providerRegistry);
440
807
  await withRawRuntime('writer', options['data-root'], async (runtime) => {
441
808
  const candidates = routes.map((route) => ({
442
809
  adapter: trackedProviderAdapter(route.provider, options), model: route.modelRef,
443
810
  selectionReason: route.selectionReason,
444
- providerBinding: providerTaskBinding(route.provider)
811
+ providerBinding: providerTaskBinding(route.provider, providerRegistry)
445
812
  }));
446
813
  const result = await createProviderTaskRunner(runtime).submit({
447
814
  capability: 'tts', candidates,
@@ -458,7 +825,7 @@ async function main(args) {
458
825
  const options = flags(args.slice(2), ['data-root', 'provider-config', 'input']);
459
826
  const normalized = generationInput('transcribe', jsonInput(options.input), { allowAudioPath: true });
460
827
  const catalog = await generationCatalog(options, 'transcribe', normalized.modelRef);
461
- const routes = transcribeCandidatesFromCatalog(catalog, normalized.modelRef);
828
+ const routes = transcribeCandidatesFromCatalog(catalog, normalized.modelRef, providerRegistry);
462
829
  let providerInput = normalized.providerInput;
463
830
  if (normalized.audioPath) {
464
831
  if (!fs.existsSync(normalized.audioPath)) throw new CliUsageError('音频文件不存在');
@@ -471,7 +838,7 @@ async function main(args) {
471
838
  const candidates = routes.map((route) => ({
472
839
  adapter: trackedProviderAdapter(route.provider, options), model: route.modelRef,
473
840
  selectionReason: route.selectionReason,
474
- providerBinding: providerTaskBinding(route.provider)
841
+ providerBinding: providerTaskBinding(route.provider, providerRegistry)
475
842
  }));
476
843
  const result = await createProviderTaskRunner(runtime).submit({
477
844
  capability: 'transcribe', candidates,
@@ -489,7 +856,7 @@ async function main(args) {
489
856
  const inspected = await withRuntime('reader', options['data-root'], commands => commands.execute('task.inspect', { task_id: args[2] }));
490
857
  const catalog = coreTaskWaitFinished(inspected.task) ? null : loadProviderCatalog(required(options['provider-config'], 'provider-config'));
491
858
  const result = await withRuntime(catalog ? 'writer' : 'reader', options['data-root'], commands => commands.execute('task.wait', input), {
492
- ...(catalog ? { resolveTaskAdapter: task => trackedProviderAdapter(providerForTaskFromCatalog(catalog, task), options, task) } : {}) });
859
+ ...(catalog ? { resolveTaskAdapter: task => trackedProviderAdapter(providerForTaskFromCatalog(catalog, task, providerRegistry), options, task) } : {}) });
493
860
  console.log(JSON.stringify(result, null, 2));
494
861
  return result.timedOut ? 3 : result.ready ? 0 : 1;
495
862
  }
@@ -499,7 +866,7 @@ async function main(args) {
499
866
  const catalog = loadProviderCatalog(required(options['provider-config'], 'provider-config'));
500
867
  await withRuntime('reader', options['data-root'], (commands) => commands.execute('task.inspect', { task_id: args[2] }));
501
868
  const result = await withRuntime('writer', options['data-root'], (commands) => commands.execute('task.observe', { task_id: args[2] }), {
502
- resolveTaskAdapter: (task) => trackedProviderAdapter(providerForTaskFromCatalog(catalog, task), options, task)
869
+ resolveTaskAdapter: (task) => trackedProviderAdapter(providerForTaskFromCatalog(catalog, task, providerRegistry), options, task)
503
870
  });
504
871
  console.log(JSON.stringify(result, null, 2));
505
872
  return 0;
@@ -510,7 +877,7 @@ async function main(args) {
510
877
  const catalog = options['provider-config'] ? loadProviderCatalog(options['provider-config']) : null;
511
878
  await withRuntime('reader', options['data-root'], (commands) => commands.execute('task.inspect', { task_id: args[2] }));
512
879
  const result = await withRuntime('writer', options['data-root'], (commands) => commands.execute('task.redeliver', { task_id: args[2] }), {
513
- ...(catalog ? { resolveTaskAdapter: (task) => trackedProviderAdapter(providerForTaskFromCatalog(catalog, task), options, task) } : {})
880
+ ...(catalog ? { resolveTaskAdapter: (task) => trackedProviderAdapter(providerForTaskFromCatalog(catalog, task, providerRegistry), options, task) } : {})
514
881
  });
515
882
  console.log(JSON.stringify(result, null, 2));
516
883
  return 0;
@@ -521,7 +888,7 @@ async function main(args) {
521
888
  const catalog = loadProviderCatalog(required(options['provider-config'], 'provider-config'));
522
889
  await withRuntime('reader', options['data-root'], (commands) => commands.execute('task.inspect', { task_id: args[2] }));
523
890
  const result = await withRuntime('writer', options['data-root'], (commands) => commands.execute('task.cancel', { task_id: args[2] }), {
524
- resolveTaskAdapter: (task) => trackedProviderAdapter(providerForTaskFromCatalog(catalog, task), options, task)
891
+ resolveTaskAdapter: (task) => trackedProviderAdapter(providerForTaskFromCatalog(catalog, task, providerRegistry), options, task)
525
892
  });
526
893
  console.log(JSON.stringify(result, null, 2));
527
894
  return 0;
@@ -545,19 +912,19 @@ async function main(args) {
545
912
  resolveTaskCandidates: async ({ capability, modelRef }) => {
546
913
  const catalog = await generationCatalog(options, { image: 'image-gen', video: 'video-gen', music: 'music-gen' }[capability] || capability, modelRef);
547
914
  return (capability === 'video'
548
- ? videoCandidatesFromCatalog(catalog, modelRef || undefined)
915
+ ? videoCandidatesFromCatalog(catalog, modelRef || undefined, providerRegistry)
549
916
  : capability === 'tts'
550
- ? ttsCandidatesFromCatalog(catalog, modelRef || undefined)
917
+ ? ttsCandidatesFromCatalog(catalog, modelRef || undefined, providerRegistry)
551
918
  : capability === 'music'
552
- ? musicCandidatesFromCatalog(catalog, modelRef || undefined)
919
+ ? musicCandidatesFromCatalog(catalog, modelRef || undefined, providerRegistry)
553
920
  : capability === 'transcribe'
554
- ? transcribeCandidatesFromCatalog(catalog, modelRef || undefined)
555
- : imageCandidatesFromCatalog(catalog, modelRef || undefined)
921
+ ? transcribeCandidatesFromCatalog(catalog, modelRef || undefined, providerRegistry)
922
+ : imageCandidatesFromCatalog(catalog, modelRef || undefined, providerRegistry)
556
923
  ).map((route) => ({
557
924
  adapter: trackedProviderAdapter(route.provider, options),
558
925
  model: route.modelRef,
559
926
  selectionReason: route.selectionReason,
560
- providerBinding: providerTaskBinding(route.provider)
927
+ providerBinding: providerTaskBinding(route.provider, providerRegistry)
561
928
  }));
562
929
  }
563
930
  });
@@ -567,8 +934,8 @@ async function main(args) {
567
934
 
568
935
  if (args[0] === 'task' && args[1] === 'inspect' && args[2]) {
569
936
  const options = flags(args.slice(3), ['data-root']);
570
- const result = await withRuntime('reader', options['data-root'], (commands) =>
571
- commands.execute('task.inspect', { task_id: args[2] }));
937
+ const result = await withApplication(options['data-root'], application =>
938
+ application.execute('task.inspect', { task_id: args[2] }, { signal: cliAbortController.signal }));
572
939
  console.log(JSON.stringify(result, null, 2));
573
940
  return 0;
574
941
  }
@@ -584,15 +951,16 @@ async function main(args) {
584
951
 
585
952
  if (args[0] === 'artifact' && args[1] === 'import') {
586
953
  const options = flags(args.slice(2), ['data-root', 'input']);
587
- const result = await withRuntime('writer', options['data-root'], commands => commands.execute('artifact.import', jsonInput(options.input)));
954
+ const result = await withApplication(options['data-root'], application =>
955
+ application.execute('artifact.import', jsonInput(options.input), { signal: cliAbortController.signal }));
588
956
  console.log(JSON.stringify(result, null, 2));
589
957
  return 0;
590
958
  }
591
959
 
592
960
  if (args[0] === 'artifact' && args[1] === 'inspect' && args[2]) {
593
961
  const options = flags(args.slice(3), ['data-root']);
594
- const result = await withRuntime('reader', options['data-root'], (commands) =>
595
- commands.execute('artifact.inspect', { artifact_id: args[2] }));
962
+ const result = await withApplication(options['data-root'], application =>
963
+ application.execute('artifact.inspect', { artifact_id: args[2] }, { signal: cliAbortController.signal }));
596
964
  console.log(JSON.stringify(result, null, 2));
597
965
  return 0;
598
966
  }
@@ -600,8 +968,8 @@ async function main(args) {
600
968
  if (args[0] === 'artifact' && args[1] === 'list') {
601
969
  const options = flags(args.slice(2), ['data-root', 'offset', 'limit', 'kind', 'media-type', 'task-id']);
602
970
  const input = Object.fromEntries(Object.entries(options).filter(([key]) => key !== 'data-root').map(([key, value]) => [key.replaceAll('-', '_'), ['offset', 'limit'].includes(key) ? Number(value) : value]));
603
- const result = await withRuntime('reader', options['data-root'], (commands) =>
604
- commands.execute('artifact.list', input));
971
+ const result = await withApplication(options['data-root'], application =>
972
+ application.execute('artifact.list', input, { signal: cliAbortController.signal }));
605
973
  console.log(JSON.stringify(result, null, 2));
606
974
  return 0;
607
975
  }
@@ -626,11 +994,11 @@ async function main(args) {
626
994
 
627
995
  if (args[0] === 'artifact' && args[1] === 'export' && args[2]) {
628
996
  const options = flags(args.slice(3), ['data-root', 'output']);
629
- const result = await withRuntime('writer', options['data-root'], (commands) =>
630
- commands.execute('artifact.export', {
997
+ const result = await withApplication(options['data-root'], application =>
998
+ application.execute('artifact.export', {
631
999
  artifact_id: args[2],
632
1000
  output_path: required(options.output, 'output')
633
- }));
1001
+ }, { signal: cliAbortController.signal }));
634
1002
  console.log(JSON.stringify(result, null, 2));
635
1003
  return 0;
636
1004
  }
@@ -639,11 +1007,25 @@ async function main(args) {
639
1007
  }
640
1008
 
641
1009
  try {
642
- process.exitCode = await main(process.argv.slice(2));
1010
+ const args = [], entries = [], raw = process.argv.slice(2);
1011
+ for (let index = 0; index < raw.length; index++) {
1012
+ if (raw[index] !== '--provider-extension') args.push(raw[index]);
1013
+ else {
1014
+ const entry = raw[++index];
1015
+ if (!entry || entry.startsWith('--')) throw new CliUsageError('--provider-extension 缺少入口路径');
1016
+ entries.push(entry);
1017
+ }
1018
+ }
1019
+ try {
1020
+ if (entries.length && args.includes('--plugin-config')) throw new CliUsageError('--plugin-config 与 --provider-extension 不能混用;Provider 插件应通过同一插件装配实例加载');
1021
+ if (args.length && !args.includes('--help') && !args.includes('-h')) cliProviderRegistry = await loadProviderRegistry(entries);
1022
+ process.exitCode = await main(args, cliProviderRegistry);
1023
+ } finally { await cliProviderRegistry?.dispose(); }
643
1024
  } catch (error) {
644
1025
  const code = error && error.code || 'IRIS_CLI_FAILED';
645
1026
  const message = error && error.message || '命令执行失败';
646
- console.error(code + ': ' + message);
1027
+ if (error.diagnostic) console.error(JSON.stringify(error.diagnostic));
1028
+ else console.error(code + ': ' + message);
647
1029
  if (error instanceof CliUsageError) console.error('\n' + usage());
648
1030
  process.exitCode = error instanceof CliUsageError ? 2 : 1;
649
1031
  }