@delexec/ops 0.1.0

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 (55) hide show
  1. package/README.md +3 -0
  2. package/README.zh-CN.md +6 -0
  3. package/node_modules/@delexec/caller-controller/README.md +3 -0
  4. package/node_modules/@delexec/caller-controller/README.zh-CN.md +6 -0
  5. package/node_modules/@delexec/caller-controller/package.json +53 -0
  6. package/node_modules/@delexec/caller-controller/src/server.js +127 -0
  7. package/node_modules/@delexec/caller-controller-core/README.md +3 -0
  8. package/node_modules/@delexec/caller-controller-core/README.zh-CN.md +6 -0
  9. package/node_modules/@delexec/caller-controller-core/package.json +26 -0
  10. package/node_modules/@delexec/caller-controller-core/src/index.js +1612 -0
  11. package/node_modules/@delexec/caller-skill-adapter/package.json +12 -0
  12. package/node_modules/@delexec/caller-skill-adapter/src/server.js +1042 -0
  13. package/node_modules/@delexec/caller-skill-mcp-adapter/README.md +65 -0
  14. package/node_modules/@delexec/caller-skill-mcp-adapter/package.json +16 -0
  15. package/node_modules/@delexec/caller-skill-mcp-adapter/src/server.js +527 -0
  16. package/node_modules/@delexec/responder-controller/README.md +3 -0
  17. package/node_modules/@delexec/responder-controller/README.zh-CN.md +6 -0
  18. package/node_modules/@delexec/responder-controller/package.json +53 -0
  19. package/node_modules/@delexec/responder-controller/src/server.js +254 -0
  20. package/node_modules/@delexec/responder-runtime-core/README.md +3 -0
  21. package/node_modules/@delexec/responder-runtime-core/README.zh-CN.md +6 -0
  22. package/node_modules/@delexec/responder-runtime-core/package.json +26 -0
  23. package/node_modules/@delexec/responder-runtime-core/src/executors.js +326 -0
  24. package/node_modules/@delexec/responder-runtime-core/src/index.js +1202 -0
  25. package/node_modules/@delexec/runtime-utils/README.md +3 -0
  26. package/node_modules/@delexec/runtime-utils/README.zh-CN.md +6 -0
  27. package/node_modules/@delexec/runtime-utils/package.json +23 -0
  28. package/node_modules/@delexec/runtime-utils/src/index.js +338 -0
  29. package/node_modules/@delexec/sqlite-store/README.md +3 -0
  30. package/node_modules/@delexec/sqlite-store/README.zh-CN.md +6 -0
  31. package/node_modules/@delexec/sqlite-store/package.json +26 -0
  32. package/node_modules/@delexec/sqlite-store/src/index.js +68 -0
  33. package/node_modules/@delexec/transport-email/README.md +3 -0
  34. package/node_modules/@delexec/transport-email/README.zh-CN.md +6 -0
  35. package/node_modules/@delexec/transport-email/package.json +23 -0
  36. package/node_modules/@delexec/transport-email/src/index.js +185 -0
  37. package/node_modules/@delexec/transport-emailengine/README.md +3 -0
  38. package/node_modules/@delexec/transport-emailengine/README.zh-CN.md +6 -0
  39. package/node_modules/@delexec/transport-emailengine/package.json +26 -0
  40. package/node_modules/@delexec/transport-emailengine/src/index.js +210 -0
  41. package/node_modules/@delexec/transport-gmail/README.md +3 -0
  42. package/node_modules/@delexec/transport-gmail/README.zh-CN.md +6 -0
  43. package/node_modules/@delexec/transport-gmail/package.json +26 -0
  44. package/node_modules/@delexec/transport-gmail/src/index.js +295 -0
  45. package/node_modules/@delexec/transport-relay-http/README.md +3 -0
  46. package/node_modules/@delexec/transport-relay-http/README.zh-CN.md +6 -0
  47. package/node_modules/@delexec/transport-relay-http/package.json +23 -0
  48. package/node_modules/@delexec/transport-relay-http/src/index.js +124 -0
  49. package/package.json +64 -0
  50. package/src/cli.js +1571 -0
  51. package/src/config.js +1180 -0
  52. package/src/example-hotline-worker.js +65 -0
  53. package/src/example-hotline.js +196 -0
  54. package/src/logging.js +56 -0
  55. package/src/supervisor.js +3070 -0
package/src/cli.js ADDED
@@ -0,0 +1,1571 @@
1
+ #!/usr/bin/env node
2
+ import { execFile, spawn } from "node:child_process";
3
+ import fs from "node:fs";
4
+ import path from "node:path";
5
+ import { promisify } from "node:util";
6
+ import { fileURLToPath } from "node:url";
7
+
8
+ import { ensureOpsDirectories, readJsonFile } from "@delexec/runtime-utils";
9
+ import { buildStructuredError } from "@delexec/contracts";
10
+ import { createOpsSupervisorServer } from "./supervisor.js";
11
+ import {
12
+ buildHotlineOnboardingBody,
13
+ ensureHotlineRegistrationDraft,
14
+ ensureOpsState,
15
+ ensureResponderIdentity,
16
+ loadHotlineRegistrationDraft,
17
+ removeHotline,
18
+ saveOpsState,
19
+ setHotlineEnabled,
20
+ upsertHotline
21
+ } from "./config.js";
22
+ import { buildExampleHotlineDefinition, isExampleHotlineDefinitionStale, LOCAL_EXAMPLE_HOTLINE_ID } from "./example-hotline.js";
23
+
24
+ const execFileAsync = promisify(execFile);
25
+ const CLI_PATH = fileURLToPath(import.meta.url);
26
+ const CLIENT_ROOT = path.resolve(path.dirname(CLI_PATH), "../../..");
27
+ const OPS_CONSOLE_DIR = path.join(CLIENT_ROOT, "apps/ops-console");
28
+ const DEFAULT_CONSOLE_HOST = "127.0.0.1";
29
+ const DEFAULT_CONSOLE_PORT = 4173;
30
+ const DEFAULT_UI_READY_TIMEOUT_MS = 120000;
31
+ const OPS_SESSION_HEADER = "X-Ops-Session";
32
+
33
+ function getOpsSessionFile() {
34
+ return path.join(ensureOpsDirectories(), "run", "session.json");
35
+ }
36
+
37
+ function usage() {
38
+ console.log(`Usage:
39
+ delexec-ops setup
40
+ delexec-ops start
41
+ delexec-ops status
42
+ delexec-ops bootstrap [--email <email>] [--platform <url>] [--text <text>] [--open-ui] [--ui-port <port>] [--ui-host <host>] [--no-browser]
43
+ delexec-ops ui start [--host <host>] [--port <port>] [--open] [--no-browser]
44
+ delexec-ops mcp spec
45
+ delexec-ops auth register --email <email> [--local] [--platform <url>]
46
+ delexec-ops enable-responder [--responder-id <id>] [--display-name <name>]
47
+ delexec-ops add-hotline --type <process|http> --hotline-id <id> [--cmd <command> | --url <url>] [--cwd <path>] [--env KEY=VALUE]
48
+ delexec-ops attach-project --project-path <path> [--project-name <name>] [--project-description <text>] [--hotline-id <id>] [--cmd <command> | --url <url>] [--cwd <path>] [--env KEY=VALUE] [--task-type <type>] [--capability <capability>]
49
+ delexec-ops add-example-hotline
50
+ delexec-ops remove-hotline --hotline-id <id>
51
+ delexec-ops enable-hotline --hotline-id <id>
52
+ delexec-ops disable-hotline --hotline-id <id>
53
+ delexec-ops submit-review [--hotline-id <id>]
54
+ delexec-ops responder show-draft --hotline-id <id>
55
+ delexec-ops responder submit-draft --hotline-id <id>
56
+ delexec-ops run-example [--text <text>]
57
+ delexec-ops doctor
58
+ delexec-ops debug-snapshot
59
+
60
+ Product terms:
61
+ Caller = Caller
62
+ Responder = Responder
63
+ Hotline = catalog-facing service entry backed by a responder/hotline pair
64
+ Platform Control = web UI for operator review and oversight
65
+
66
+ Compatibility:
67
+ delexec-ops responder init
68
+ delexec-ops responder register
69
+ delexec-ops responder add-hotline ...
70
+ delexec-ops responder start
71
+ delexec-ops responder status
72
+ delexec-ops responder doctor
73
+ delexec-ops responder init
74
+ delexec-ops responder register
75
+ delexec-ops responder add-hotline ...
76
+ delexec-ops responder start
77
+ delexec-ops responder status
78
+ delexec-ops responder doctor`);
79
+ }
80
+
81
+ function parseArgs(argv) {
82
+ const args = { _: [] };
83
+ for (let index = 0; index < argv.length; index += 1) {
84
+ const token = argv[index];
85
+ if (!token.startsWith("--")) {
86
+ args._.push(token);
87
+ continue;
88
+ }
89
+ const key = token.slice(2);
90
+ const next = argv[index + 1];
91
+ const value = !next || next.startsWith("--") ? true : next;
92
+ if (value !== true) {
93
+ index += 1;
94
+ }
95
+ if (args[key] === undefined) {
96
+ args[key] = value;
97
+ } else if (Array.isArray(args[key])) {
98
+ args[key].push(value);
99
+ } else {
100
+ args[key] = [args[key], value];
101
+ }
102
+ }
103
+ return args;
104
+ }
105
+
106
+ function emit(value) {
107
+ console.log(JSON.stringify(value, null, 2));
108
+ }
109
+
110
+ function logBootstrapStep(steps, step, ok, detail = {}) {
111
+ steps.push({ step, ok, ...detail });
112
+ }
113
+
114
+ function getValues(value) {
115
+ if (value === undefined || value === null || value === false) {
116
+ return [];
117
+ }
118
+ return Array.isArray(value) ? value.map(String) : [String(value)];
119
+ }
120
+
121
+ function parseEnvAssignments(value) {
122
+ const entries = getValues(value);
123
+ const env = {};
124
+ for (const entry of entries) {
125
+ const normalized = String(entry || "").trim();
126
+ const separatorIndex = normalized.indexOf("=");
127
+ if (separatorIndex <= 0) {
128
+ throw new Error(`invalid_env_assignment:${normalized}`);
129
+ }
130
+ const key = normalized.slice(0, separatorIndex).trim();
131
+ const envValue = normalized.slice(separatorIndex + 1);
132
+ if (!key) {
133
+ throw new Error(`invalid_env_assignment:${normalized}`);
134
+ }
135
+ env[key] = envValue;
136
+ }
137
+ return env;
138
+ }
139
+
140
+ function sanitizeIdSegment(value) {
141
+ return (
142
+ String(value || "")
143
+ .toLowerCase()
144
+ .replace(/[^a-z0-9]+/g, ".")
145
+ .replace(/^\.+|\.+$/g, "") || "project"
146
+ );
147
+ }
148
+
149
+ async function requestJson(baseUrl, pathname, { method = "GET", headers = {}, body } = {}) {
150
+ const response = await fetch(new URL(pathname, baseUrl), {
151
+ method,
152
+ headers: {
153
+ ...headers,
154
+ ...(body === undefined ? {} : { "content-type": "application/json; charset=utf-8" })
155
+ },
156
+ body: body === undefined ? undefined : JSON.stringify(body)
157
+ });
158
+ const text = await response.text();
159
+ return {
160
+ status: response.status,
161
+ body: text ? JSON.parse(text) : null
162
+ };
163
+ }
164
+
165
+ function readSupervisorSessionToken() {
166
+ const session = readJsonFile(getOpsSessionFile(), null);
167
+ if (!session?.token || !session?.expires_at) {
168
+ return null;
169
+ }
170
+ const expiresAt = Date.parse(session.expires_at);
171
+ if (!Number.isFinite(expiresAt) || expiresAt <= Date.now()) {
172
+ return null;
173
+ }
174
+ return String(session.token);
175
+ }
176
+
177
+ function writeSupervisorSessionToken(session) {
178
+ if (!session?.token || !session?.expires_at) {
179
+ return false;
180
+ }
181
+ ensureOpsDirectories();
182
+ const sessionFile = getOpsSessionFile();
183
+ fs.writeFileSync(
184
+ sessionFile,
185
+ `${JSON.stringify({ token: String(session.token), expires_at: String(session.expires_at) }, null, 2)}\n`,
186
+ "utf8"
187
+ );
188
+ return true;
189
+ }
190
+
191
+ function withSupervisorSessionHeaders(headers = {}) {
192
+ const token = readSupervisorSessionToken();
193
+ return token ? { ...headers, [OPS_SESSION_HEADER]: token } : headers;
194
+ }
195
+
196
+ async function recoverSupervisorSession(state) {
197
+ const response = await requestJson(supervisorUrlFromState(state), "/auth/session");
198
+ const session = response.body?.recoverable_session;
199
+ if (!session?.token || !session?.expires_at) {
200
+ return false;
201
+ }
202
+ return writeSupervisorSessionToken(session);
203
+ }
204
+
205
+ async function requestSupervisorJson(state, pathname, options = {}) {
206
+ let response = await requestJson(supervisorUrlFromState(state), pathname, {
207
+ ...options,
208
+ headers: withSupervisorSessionHeaders(options.headers || {})
209
+ });
210
+ if (response.status === 401 && response.body?.error?.code === "AUTH_SESSION_REQUIRED") {
211
+ const sessionFile = getOpsSessionFile();
212
+ if (fs.existsSync(sessionFile)) {
213
+ try {
214
+ fs.rmSync(sessionFile, { force: true });
215
+ } catch {}
216
+ }
217
+ const recovered = await recoverSupervisorSession(state).catch(() => false);
218
+ if (recovered) {
219
+ response = await requestJson(supervisorUrlFromState(state), pathname, {
220
+ ...options,
221
+ headers: withSupervisorSessionHeaders(options.headers || {})
222
+ });
223
+ }
224
+ }
225
+ return response;
226
+ }
227
+
228
+ async function runCliSubcommand(args, env) {
229
+ const result = await execFileAsync(process.execPath, [CLI_PATH, ...args], { env });
230
+ return result.stdout.trim() ? JSON.parse(result.stdout) : {};
231
+ }
232
+
233
+ async function waitFor(check, { timeoutMs = 15000, intervalMs = 250 } = {}) {
234
+ const started = Date.now();
235
+ while (Date.now() - started < timeoutMs) {
236
+ try {
237
+ return await check();
238
+ } catch {}
239
+ await new Promise((resolve) => setTimeout(resolve, intervalMs));
240
+ }
241
+ throw new Error("timeout");
242
+ }
243
+
244
+ function parsePort(value, fallback) {
245
+ const parsed = Number(value);
246
+ if (!Number.isFinite(parsed) || parsed <= 0) {
247
+ return fallback;
248
+ }
249
+ return Math.trunc(parsed);
250
+ }
251
+
252
+ function resolveUiConfig(args = {}) {
253
+ return {
254
+ host: String(args["ui-host"] || args.host || process.env.DELEXEC_OPS_UI_HOST || DEFAULT_CONSOLE_HOST).trim() || DEFAULT_CONSOLE_HOST,
255
+ port: parsePort(args["ui-port"] || args.port || process.env.DELEXEC_OPS_UI_PORT, DEFAULT_CONSOLE_PORT),
256
+ openBrowser: args["no-browser"] ? false : args["open-ui"] === true || args.open === true
257
+ };
258
+ }
259
+
260
+ function uiUrl({ host, port }) {
261
+ return `http://${host}:${port}`;
262
+ }
263
+
264
+ function npmExecutable() {
265
+ return process.platform === "win32" ? "npm.cmd" : "npm";
266
+ }
267
+
268
+ function canLaunchOpsConsoleWorkspace() {
269
+ return fs.existsSync(path.join(OPS_CONSOLE_DIR, "package.json"));
270
+ }
271
+
272
+ function buildUiLaunchCommand({ host, port }) {
273
+ const bin = process.env.DELEXEC_OPS_UI_BIN;
274
+ const customArgs = process.env.DELEXEC_OPS_UI_ARGS ? JSON.parse(process.env.DELEXEC_OPS_UI_ARGS) : null;
275
+ if (bin) {
276
+ return {
277
+ command: bin,
278
+ args: Array.isArray(customArgs) ? customArgs : [],
279
+ cwd: CLIENT_ROOT,
280
+ launch_mode: "configured_command"
281
+ };
282
+ }
283
+ if (!canLaunchOpsConsoleWorkspace()) {
284
+ throw new Error(
285
+ "Ops Console UI requires a source checkout. Run from the delegated-execution-client workspace, or use delexec-ops status/debug-snapshot from the global CLI. See the README source install section."
286
+ );
287
+ }
288
+ return {
289
+ command: npmExecutable(),
290
+ args: [
291
+ "exec",
292
+ "--workspace",
293
+ "@delexec/ops-console",
294
+ "--",
295
+ "vite",
296
+ "--host",
297
+ host,
298
+ "--port",
299
+ String(port),
300
+ "--strictPort"
301
+ ],
302
+ cwd: CLIENT_ROOT,
303
+ launch_mode: "workspace_vite"
304
+ };
305
+ }
306
+
307
+ async function waitForUi(url) {
308
+ return waitFor(async () => {
309
+ const response = await fetch(url, { method: "GET" });
310
+ if (!response.ok) {
311
+ throw new Error("ui_not_ready");
312
+ }
313
+ return true;
314
+ }, { timeoutMs: DEFAULT_UI_READY_TIMEOUT_MS, intervalMs: 500 });
315
+ }
316
+
317
+ function openBrowser(url) {
318
+ const configuredBin = process.env.DELEXEC_OPS_BROWSER_BIN;
319
+ const configuredArgs = process.env.DELEXEC_OPS_BROWSER_ARGS ? JSON.parse(process.env.DELEXEC_OPS_BROWSER_ARGS) : null;
320
+ if (configuredBin) {
321
+ const child = spawn(configuredBin, [...(Array.isArray(configuredArgs) ? configuredArgs : []), url], {
322
+ detached: true,
323
+ stdio: "ignore"
324
+ });
325
+ child.unref();
326
+ return { opened: true, command: configuredBin };
327
+ }
328
+
329
+ const browserCommand =
330
+ process.platform === "darwin"
331
+ ? { command: "open", args: [url] }
332
+ : process.platform === "win32"
333
+ ? { command: "cmd", args: ["/c", "start", "", url] }
334
+ : { command: "xdg-open", args: [url] };
335
+
336
+ const child = spawn(browserCommand.command, browserCommand.args, {
337
+ detached: true,
338
+ stdio: "ignore"
339
+ });
340
+ child.unref();
341
+ return { opened: true, command: browserCommand.command };
342
+ }
343
+
344
+ async function ensureUiAvailable(args = {}, env = process.env) {
345
+ const ui = resolveUiConfig(args);
346
+ const consoleUrl = uiUrl(ui);
347
+ let alreadyRunning = false;
348
+ try {
349
+ const probe = await fetch(consoleUrl, { method: "GET", signal: AbortSignal.timeout(2000) });
350
+ alreadyRunning = probe.ok;
351
+ } catch {}
352
+
353
+ let pid = null;
354
+ let launchMode = "existing";
355
+ if (!alreadyRunning) {
356
+ const launch = buildUiLaunchCommand(ui);
357
+ const child = spawn(launch.command, launch.args, {
358
+ cwd: launch.cwd,
359
+ env: {
360
+ ...env,
361
+ OPS_PORT_SUPERVISOR: String(env.OPS_PORT_SUPERVISOR || ensureOpsState().config.runtime.ports.supervisor),
362
+ DELEXEC_OPS_UI_HOST: ui.host,
363
+ DELEXEC_OPS_UI_PORT: String(ui.port)
364
+ },
365
+ detached: true,
366
+ stdio: "ignore"
367
+ });
368
+ child.unref();
369
+ pid = child.pid || null;
370
+ launchMode = launch.launch_mode;
371
+ await waitForUi(consoleUrl);
372
+ }
373
+
374
+ let browser = { opened: false };
375
+ if (ui.openBrowser) {
376
+ browser = openBrowser(consoleUrl);
377
+ }
378
+
379
+ return {
380
+ ok: true,
381
+ url: consoleUrl,
382
+ host: ui.host,
383
+ port: ui.port,
384
+ started: !alreadyRunning,
385
+ pid,
386
+ launch_mode: launchMode,
387
+ browser
388
+ };
389
+ }
390
+
391
+ function buildResponderRegisterHeaders(state) {
392
+ const apiKey =
393
+ state.config.caller.api_key ||
394
+ state.env.CALLER_PLATFORM_API_KEY ||
395
+ state.env.PLATFORM_API_KEY ||
396
+ process.env.CALLER_PLATFORM_API_KEY ||
397
+ process.env.PLATFORM_API_KEY ||
398
+ state.env.RESPONDER_PLATFORM_API_KEY;
399
+ if (!apiKey) {
400
+ throw new Error("caller_platform_api_key_required");
401
+ }
402
+ return { Authorization: `Bearer ${apiKey}` };
403
+ }
404
+
405
+ async function verifyRegisteredHotline(state, { hotlineId, expectedTemplateRef }) {
406
+ let detail;
407
+ let bundle;
408
+ try {
409
+ detail = await requestJson(state.config.platform.base_url, `/v1/catalog/hotlines/${encodeURIComponent(hotlineId)}`, {
410
+ headers: buildResponderRegisterHeaders(state)
411
+ });
412
+ } catch (error) {
413
+ return {
414
+ ok: false,
415
+ catalog_visible: false,
416
+ template_ref_matches: false,
417
+ template_bundle_available: false,
418
+ catalog_status: null,
419
+ template_bundle_status: null,
420
+ error: error instanceof Error ? error.message : "catalog_verification_failed"
421
+ };
422
+ }
423
+
424
+ const actualTemplateRef = detail.body?.template_ref || null;
425
+ const templateRefMatches = Boolean(detail.status === 200 && actualTemplateRef && actualTemplateRef === expectedTemplateRef);
426
+ if (detail.status === 200 && actualTemplateRef) {
427
+ try {
428
+ bundle = await requestJson(
429
+ state.config.platform.base_url,
430
+ `/v1/catalog/hotlines/${encodeURIComponent(hotlineId)}/template-bundle?template_ref=${encodeURIComponent(actualTemplateRef)}`,
431
+ {
432
+ headers: buildResponderRegisterHeaders(state)
433
+ }
434
+ );
435
+ } catch (error) {
436
+ bundle = {
437
+ status: null,
438
+ body: {
439
+ ok: false,
440
+ error: error instanceof Error ? error.message : "template_bundle_verification_failed"
441
+ }
442
+ };
443
+ }
444
+ }
445
+
446
+ const templateBundleAvailable = Boolean(bundle?.status === 200);
447
+ return {
448
+ ok: Boolean(detail.status === 200 && templateRefMatches && templateBundleAvailable),
449
+ catalog_visible: detail.status === 200,
450
+ template_ref_matches: templateRefMatches,
451
+ template_bundle_available: templateBundleAvailable,
452
+ catalog_status: detail.status,
453
+ template_bundle_status: bundle?.status ?? null,
454
+ template_ref: actualTemplateRef || expectedTemplateRef || null
455
+ };
456
+ }
457
+
458
+ function parseHotlineDefinition(args) {
459
+ const type = String(args.type || "process");
460
+ const hotlineId = String(args["hotline-id"] || "").trim();
461
+ if (!hotlineId) {
462
+ throw new Error("hotline_id_required");
463
+ }
464
+ const definition = {
465
+ hotline_id: hotlineId,
466
+ display_name: String(args["display-name"] || hotlineId),
467
+ enabled: true,
468
+ task_types: getValues(args["task-type"]),
469
+ capabilities: getValues(args.capability),
470
+ tags: getValues(args.tag),
471
+ adapter_type: type,
472
+ timeouts: {
473
+ soft_timeout_s: Number(args["soft-timeout-s"] || 60),
474
+ hard_timeout_s: Number(args["hard-timeout-s"] || 180)
475
+ },
476
+ review_status: "local_only",
477
+ submitted_for_review: false
478
+ };
479
+ if (type === "http") {
480
+ const url = String(args.url || "").trim();
481
+ if (!url) {
482
+ throw new Error("http_adapter_url_required");
483
+ }
484
+ definition.adapter = {
485
+ url,
486
+ method: String(args.method || "POST").toUpperCase()
487
+ };
488
+ return definition;
489
+ }
490
+ const cmd = String(args.cmd || "").trim();
491
+ if (!cmd) {
492
+ throw new Error("process_adapter_cmd_required");
493
+ }
494
+ definition.adapter = {
495
+ cmd,
496
+ cwd: args.cwd ? String(args.cwd) : undefined,
497
+ env: parseEnvAssignments(args.env)
498
+ };
499
+ return definition;
500
+ }
501
+
502
+ function buildProjectHotlineDefinition(args) {
503
+ const rawProjectPath = String(args["project-path"] || "").trim();
504
+ if (!rawProjectPath) {
505
+ throw new Error("project_path_required");
506
+ }
507
+ const projectPath = path.resolve(rawProjectPath);
508
+ if (!fs.existsSync(projectPath)) {
509
+ throw new Error("project_path_not_found");
510
+ }
511
+
512
+ const projectName = String(args["project-name"] || path.basename(projectPath) || "Local Project").trim();
513
+ const projectDescription = String(args["project-description"] || args.description || "").trim();
514
+ const adapterType = String(args.type || (args.url ? "http" : "process")).trim();
515
+ const hotlineId = String(args["hotline-id"] || `local.${sanitizeIdSegment(projectName)}.v1`).trim();
516
+ const tags = Array.from(new Set(["local", "project", ...getValues(args.tag)]));
517
+ const taskTypes = getValues(args["task-type"]);
518
+ const capabilities = getValues(args.capability);
519
+
520
+ const definition = {
521
+ hotline_id: hotlineId,
522
+ display_name: String(args["display-name"] || projectName).trim(),
523
+ enabled: true,
524
+ task_types: taskTypes.length > 0 ? taskTypes : ["project_task"],
525
+ capabilities: capabilities.length > 0 ? capabilities : [`project.${sanitizeIdSegment(projectName)}`],
526
+ tags,
527
+ adapter_type: adapterType,
528
+ timeouts: {
529
+ soft_timeout_s: Number(args["soft-timeout-s"] || 60),
530
+ hard_timeout_s: Number(args["hard-timeout-s"] || 180)
531
+ },
532
+ review_status: "local_only",
533
+ submitted_for_review: false,
534
+ metadata: {
535
+ project: {
536
+ path: projectPath,
537
+ name: projectName,
538
+ description: projectDescription || null,
539
+ mount_kind: "local_project"
540
+ }
541
+ }
542
+ };
543
+
544
+ if (adapterType === "http") {
545
+ const url = String(args.url || "").trim();
546
+ if (!url) {
547
+ throw new Error("http_adapter_url_required");
548
+ }
549
+ definition.adapter = {
550
+ url,
551
+ method: String(args.method || "POST").toUpperCase()
552
+ };
553
+ return definition;
554
+ }
555
+
556
+ const cmd = String(args.cmd || "").trim();
557
+ if (!cmd) {
558
+ throw new Error("process_adapter_cmd_required");
559
+ }
560
+ definition.adapter = {
561
+ cmd,
562
+ cwd: args.cwd ? String(args.cwd) : projectPath,
563
+ env: parseEnvAssignments(args.env)
564
+ };
565
+ return definition;
566
+ }
567
+
568
+ function supervisorUrlFromState(state) {
569
+ return `http://127.0.0.1:${state.config.runtime.ports.supervisor}`;
570
+ }
571
+
572
+ async function ensureSupervisorAvailable(baseUrl, env) {
573
+ try {
574
+ const health = await requestJson(baseUrl, "/healthz");
575
+ if (health.status === 200) {
576
+ return { started: false };
577
+ }
578
+ } catch {}
579
+
580
+ const child = spawn(process.execPath, [CLI_PATH, "start"], {
581
+ env,
582
+ detached: true,
583
+ stdio: "ignore"
584
+ });
585
+ child.unref();
586
+
587
+ await waitFor(async () => {
588
+ const health = await requestJson(baseUrl, "/healthz");
589
+ if (health.status !== 200) {
590
+ throw new Error("supervisor_not_ready");
591
+ }
592
+ return health;
593
+ });
594
+ return { started: true };
595
+ }
596
+
597
+ async function maybeApproveExample({ platformUrl, adminApiKey, responderId }) {
598
+ if (!adminApiKey) {
599
+ return { ok: false, reason: "admin_api_key_missing" };
600
+ }
601
+ const headers = { Authorization: `Bearer ${adminApiKey}` };
602
+ const responder = await requestJson(platformUrl, `/v2/admin/responders/${encodeURIComponent(responderId)}/approve`, {
603
+ method: "POST",
604
+ headers,
605
+ body: { reason: "ops bootstrap local demo approval" }
606
+ });
607
+ const hotline = await requestJson(platformUrl, `/v2/admin/hotlines/${encodeURIComponent(LOCAL_EXAMPLE_HOTLINE_ID)}/approve`, {
608
+ method: "POST",
609
+ headers,
610
+ body: { reason: "ops bootstrap local demo approval" }
611
+ });
612
+ return {
613
+ ok: responder.status === 200 && hotline.status === 200,
614
+ responder,
615
+ hotline
616
+ };
617
+ }
618
+
619
+ async function waitForCatalogVisibility(supervisorUrl, responderId, options) {
620
+ return waitFor(async () => {
621
+ const catalog = await requestJson(
622
+ supervisorUrl,
623
+ `/catalog/hotlines?hotline_id=${encodeURIComponent(LOCAL_EXAMPLE_HOTLINE_ID)}&responder_id=${encodeURIComponent(responderId)}`
624
+ );
625
+ const item = catalog.body?.items?.find(
626
+ (entry) => entry.hotline_id === LOCAL_EXAMPLE_HOTLINE_ID && entry.responder_id === responderId
627
+ );
628
+ if (!item) {
629
+ throw new Error("catalog_not_ready");
630
+ }
631
+ return item;
632
+ }, options);
633
+ }
634
+
635
+ async function commandSetup(args = {}) {
636
+ const state = ensureOpsState();
637
+ ensureResponderIdentity(state, {
638
+ responderId: args["responder-id"] ? String(args["responder-id"]) : null,
639
+ displayName: args["display-name"] ? String(args["display-name"]) : null
640
+ });
641
+ state.env = saveOpsState(state);
642
+ emit({
643
+ ok: true,
644
+ ops_home: path.dirname(state.envFile),
645
+ env_file: state.envFile,
646
+ config_file: state.opsConfigFile,
647
+ config: state.config
648
+ });
649
+ }
650
+
651
+ async function commandStart() {
652
+ const state = ensureOpsState();
653
+ ensureResponderIdentity(state);
654
+ state.env = saveOpsState(state);
655
+ const server = createOpsSupervisorServer();
656
+ await new Promise((resolve) => server.listen(state.config.runtime.ports.supervisor, "127.0.0.1", resolve));
657
+ await server.startManagedServices();
658
+ console.log(`[ops-supervisor] listening on ${state.config.runtime.ports.supervisor}`);
659
+ server.on("close", () => {
660
+ void server.stopManagedServices();
661
+ });
662
+ }
663
+
664
+ async function commandStatus() {
665
+ const state = ensureOpsState();
666
+ try {
667
+ const response = await requestJson(supervisorUrlFromState(state), "/status");
668
+ emit(response.body);
669
+ } catch {
670
+ emit({
671
+ ok: false,
672
+ running: false,
673
+ config: state.config
674
+ });
675
+ }
676
+ }
677
+
678
+ async function commandMcpSpec() {
679
+ const state = ensureOpsState();
680
+ const response = await requestJson(supervisorUrlFromState(state), "/mcp-adapter/spec");
681
+ emit(response.body);
682
+ }
683
+
684
+ async function commandAuthRegister(args) {
685
+ const state = ensureOpsState();
686
+ const localOnly = args.local === true;
687
+ if (args.platform) {
688
+ state.config.platform.base_url = String(args.platform).trim();
689
+ state.config.platform.enabled = true;
690
+ state.config.platform_console ||= {};
691
+ state.config.platform_console.base_url = state.config.platform.base_url;
692
+ state.env = saveOpsState(state);
693
+ }
694
+ const email = String(args.email || "").trim();
695
+ if (!email) {
696
+ throw new Error("email_required");
697
+ }
698
+ const fallbackRegister = async () => {
699
+ const local = ensureOpsState();
700
+ local.config.platform.base_url = String(args.platform || local.config.platform.base_url).trim();
701
+ local.config.platform.enabled = true;
702
+ local.config.platform_console ||= {};
703
+ local.config.platform_console.base_url = local.config.platform.base_url;
704
+ const direct = await requestJson(local.config.platform.base_url, "/v1/users/register", {
705
+ method: "POST",
706
+ body: { contact_email: email }
707
+ });
708
+ if (direct.status === 201) {
709
+ local.config.caller.api_key = direct.body.api_key;
710
+ local.config.caller.contact_email = direct.body.contact_email || email;
711
+ local.env = saveOpsState(local);
712
+ }
713
+ return direct;
714
+ };
715
+ let response;
716
+ try {
717
+ response = await requestSupervisorJson(state, "/auth/register-caller", {
718
+ method: "POST",
719
+ body: {
720
+ contact_email: email,
721
+ mode: localOnly ? "local_only" : "platform"
722
+ }
723
+ });
724
+ if (!localOnly && response.status === 401 && response.body?.error?.code === "AUTH_SESSION_REQUIRED") {
725
+ response = await fallbackRegister();
726
+ }
727
+ } catch {
728
+ response = localOnly
729
+ ? (() => {
730
+ state.config.caller.contact_email = email;
731
+ state.config.caller.registration_mode = "local_only";
732
+ state.config.caller.api_key = null;
733
+ state.config.caller.api_key_configured = false;
734
+ state.env = saveOpsState(state);
735
+ return {
736
+ status: 201,
737
+ body: {
738
+ ok: true,
739
+ registered: true,
740
+ mode: "local_only",
741
+ contact_email: email
742
+ }
743
+ };
744
+ })()
745
+ : await fallbackRegister();
746
+ }
747
+ emit({
748
+ ok: response.status === 201,
749
+ ...response.body
750
+ });
751
+ }
752
+
753
+ async function commandEnableResponder(args) {
754
+ const state = ensureOpsState();
755
+ state.config.responder.enabled = true;
756
+ ensureResponderIdentity(state, {
757
+ responderId: args["responder-id"] ? String(args["responder-id"]) : null,
758
+ displayName: args["display-name"] ? String(args["display-name"]) : null
759
+ });
760
+ state.env = saveOpsState(state);
761
+ try {
762
+ const response = await requestSupervisorJson(state, "/responder/enable", {
763
+ method: "POST",
764
+ body: {
765
+ responder_id: state.config.responder.responder_id,
766
+ display_name: state.config.responder.display_name
767
+ }
768
+ });
769
+ emit(response.body);
770
+ } catch {
771
+ emit({
772
+ ok: true,
773
+ responder: state.config.responder,
774
+ submitted: 0,
775
+ review: null
776
+ });
777
+ }
778
+ }
779
+
780
+ async function commandAddHotline(args) {
781
+ const state = ensureOpsState();
782
+ const definition = parseHotlineDefinition(args);
783
+ const registrationDraft = ensureHotlineRegistrationDraft(state, definition);
784
+ upsertHotline(state, definition);
785
+ state.env = saveOpsState(state);
786
+ let runtime = { synced: false, auth_required: false };
787
+ try {
788
+ const response = await requestSupervisorJson(state, "/responder/hotlines", {
789
+ method: "POST",
790
+ body: definition
791
+ });
792
+ if (response.status === 201) {
793
+ runtime = {
794
+ synced: true,
795
+ auth_required: false,
796
+ ...(response.body?.runtime || {})
797
+ };
798
+ } else if (response.status === 401 && response.body?.error?.code === "AUTH_SESSION_REQUIRED") {
799
+ runtime = { synced: false, auth_required: true };
800
+ }
801
+ } catch {}
802
+ emit({
803
+ ok: true,
804
+ hotline_id: definition.hotline_id,
805
+ adapter_type: definition.adapter_type,
806
+ runtime,
807
+ local_integration_file: registrationDraft.integration_file,
808
+ local_hook_file: registrationDraft.hook_file,
809
+ registration_draft_file: registrationDraft.draft_file,
810
+ registration_draft: registrationDraft.draft
811
+ });
812
+ }
813
+
814
+ async function commandAttachProject(args) {
815
+ const state = ensureOpsState();
816
+ const definition = buildProjectHotlineDefinition(args);
817
+ const registrationDraft = ensureHotlineRegistrationDraft(state, definition);
818
+ upsertHotline(state, definition);
819
+ state.env = saveOpsState(state);
820
+ let runtime = { synced: false, auth_required: false };
821
+ try {
822
+ const response = await requestSupervisorJson(state, "/responder/hotlines", {
823
+ method: "POST",
824
+ body: definition
825
+ });
826
+ if (response.status === 201) {
827
+ runtime = {
828
+ synced: true,
829
+ auth_required: false,
830
+ ...(response.body?.runtime || {})
831
+ };
832
+ } else if (response.status === 401 && response.body?.error?.code === "AUTH_SESSION_REQUIRED") {
833
+ runtime = { synced: false, auth_required: true };
834
+ }
835
+ } catch {}
836
+ emit({
837
+ ok: true,
838
+ hotline_id: definition.hotline_id,
839
+ adapter_type: definition.adapter_type,
840
+ project: definition.metadata.project,
841
+ runtime,
842
+ local_integration_file: registrationDraft.integration_file,
843
+ local_hook_file: registrationDraft.hook_file,
844
+ registration_draft_file: registrationDraft.draft_file,
845
+ registration_draft: registrationDraft.draft
846
+ });
847
+ }
848
+
849
+ async function commandAddExampleHotline() {
850
+ const state = ensureOpsState();
851
+ const existing = (state.config.responder.hotlines || []).find((item) => item.hotline_id === LOCAL_EXAMPLE_HOTLINE_ID);
852
+ const stale = isExampleHotlineDefinitionStale(existing);
853
+ const definition = buildExampleHotlineDefinition(existing);
854
+ if (stale) {
855
+ definition.submitted_for_review = false;
856
+ definition.review_status = "local_only";
857
+ }
858
+ const registrationDraft = ensureHotlineRegistrationDraft(state, definition);
859
+ upsertHotline(state, definition);
860
+ state.env = saveOpsState(state);
861
+ try {
862
+ const response = await requestJson(supervisorUrlFromState(state), "/responder/hotlines/example", {
863
+ method: "POST",
864
+ body: {}
865
+ });
866
+ emit(response.body);
867
+ return;
868
+ } catch {}
869
+ emit({
870
+ ok: true,
871
+ example: true,
872
+ hotline_id: definition.hotline_id,
873
+ adapter_type: definition.adapter_type,
874
+ local_integration_file: registrationDraft.integration_file,
875
+ local_hook_file: registrationDraft.hook_file,
876
+ registration_draft_file: registrationDraft.draft_file,
877
+ registration_draft: registrationDraft.draft
878
+ });
879
+ }
880
+
881
+ async function commandSetHotlineEnabled(args, enabled) {
882
+ const state = ensureOpsState();
883
+ const hotlineId = String(args["hotline-id"] || "").trim();
884
+ if (!hotlineId) {
885
+ throw new Error("hotline_id_required");
886
+ }
887
+ const item = setHotlineEnabled(state, hotlineId, enabled);
888
+ if (!item) {
889
+ throw new Error("hotline_not_found");
890
+ }
891
+ state.env = saveOpsState(state);
892
+ try {
893
+ const response = await requestSupervisorJson(
894
+ state,
895
+ `/responder/hotlines/${encodeURIComponent(hotlineId)}/${enabled ? "enable" : "disable"}`,
896
+ {
897
+ method: "POST",
898
+ body: {}
899
+ }
900
+ );
901
+ emit(response.body);
902
+ return;
903
+ } catch {}
904
+ emit({
905
+ ok: true,
906
+ hotline_id: item.hotline_id,
907
+ enabled: item.enabled !== false
908
+ });
909
+ }
910
+
911
+ async function commandRemoveHotline(args) {
912
+ const state = ensureOpsState();
913
+ const hotlineId = String(args["hotline-id"] || "").trim();
914
+ if (!hotlineId) {
915
+ throw new Error("hotline_id_required");
916
+ }
917
+ const item = removeHotline(state, hotlineId);
918
+ if (!item) {
919
+ throw new Error("hotline_not_found");
920
+ }
921
+ state.env = saveOpsState(state);
922
+ try {
923
+ const response = await requestSupervisorJson(state, `/responder/hotlines/${encodeURIComponent(hotlineId)}`, {
924
+ method: "DELETE"
925
+ });
926
+ emit(response.body);
927
+ return;
928
+ } catch {}
929
+ emit({
930
+ ok: true,
931
+ removed: {
932
+ hotline_id: item.hotline_id
933
+ }
934
+ });
935
+ }
936
+
937
+ function requireHotlineIdArg(args) {
938
+ const hotlineId = String(args["hotline-id"] || "").trim();
939
+ if (!hotlineId) {
940
+ throw new Error("hotline_id_required");
941
+ }
942
+ return hotlineId;
943
+ }
944
+
945
+ async function commandShowDraft(args = {}) {
946
+ const state = ensureOpsState();
947
+ const hotlineId = requireHotlineIdArg(args);
948
+ const hotline = (state.config.responder.hotlines || []).find((item) => item.hotline_id === hotlineId);
949
+ if (!hotline) {
950
+ throw new Error("hotline_not_found");
951
+ }
952
+ try {
953
+ const response = await requestSupervisorJson(state, `/responder/hotlines/${encodeURIComponent(hotlineId)}/draft`);
954
+ if (response.status === 200) {
955
+ emit(response.body);
956
+ return;
957
+ }
958
+ } catch {}
959
+
960
+ const registrationDraft = loadHotlineRegistrationDraft(state, hotline);
961
+ emit({
962
+ ok: Boolean(registrationDraft.draft),
963
+ hotline_id: hotline.hotline_id,
964
+ review_status: hotline.review_status || "local_only",
965
+ submitted_for_review: hotline.submitted_for_review === true,
966
+ local_integration_file: hotline?.metadata?.local?.integration_file || null,
967
+ local_hook_file: hotline?.metadata?.local?.hook_file || null,
968
+ draft_file: registrationDraft.draft_file,
969
+ draft: registrationDraft.draft
970
+ });
971
+ }
972
+
973
+ async function commandSubmitReview(args = {}) {
974
+ const state = ensureOpsState();
975
+ const hotlineId = args["hotline-id"] ? requireHotlineIdArg(args) : null;
976
+ if (hotlineId) {
977
+ const hotline = (state.config.responder.hotlines || []).find((item) => item.hotline_id === hotlineId);
978
+ if (!hotline) {
979
+ throw new Error("hotline_not_found");
980
+ }
981
+ }
982
+ const responderIdentity = ensureResponderIdentity(state, {
983
+ responderId: args["responder-id"] ? String(args["responder-id"]) : null,
984
+ displayName: args["display-name"] ? String(args["display-name"]) : null
985
+ });
986
+ state.env = saveOpsState(state);
987
+ const pending = (state.config.responder.hotlines || []).filter(
988
+ (item) => item.submitted_for_review !== true && (!hotlineId || item.hotline_id === hotlineId)
989
+ );
990
+ for (const item of pending) {
991
+ try {
992
+ buildHotlineOnboardingBody(state, item, responderIdentity);
993
+ } catch (error) {
994
+ emit(buildStructuredError(
995
+ error?.code || "HOTLINE_DRAFT_INVALID",
996
+ error instanceof Error ? error.message : "hotline registration draft is invalid",
997
+ { fields: Array.isArray(error?.fields) ? error.fields : [] }
998
+ ));
999
+ return;
1000
+ }
1001
+ }
1002
+ try {
1003
+ const response = await requestSupervisorJson(state, "/responder/submit-review", {
1004
+ method: "POST",
1005
+ body: {
1006
+ responder_id: state.config.responder.responder_id,
1007
+ display_name: state.config.responder.display_name,
1008
+ hotline_id: hotlineId
1009
+ }
1010
+ });
1011
+ if (response.status === 201) {
1012
+ emit(response.body);
1013
+ return;
1014
+ }
1015
+ } catch {}
1016
+ const results = [];
1017
+ for (const item of pending) {
1018
+ let onboarding;
1019
+ try {
1020
+ onboarding = buildHotlineOnboardingBody(state, item, responderIdentity);
1021
+ } catch (error) {
1022
+ emit(buildStructuredError(
1023
+ error?.code || "HOTLINE_DRAFT_INVALID",
1024
+ error instanceof Error ? error.message : "hotline registration draft is invalid",
1025
+ { fields: Array.isArray(error?.fields) ? error.fields : [] }
1026
+ ));
1027
+ return;
1028
+ }
1029
+ const response = await requestJson(state.config.platform.base_url, "/v2/hotlines", {
1030
+ method: "POST",
1031
+ headers: buildResponderRegisterHeaders(state),
1032
+ body: onboarding.body
1033
+ });
1034
+ if (response.status !== 201) {
1035
+ emit(response.body);
1036
+ return;
1037
+ }
1038
+ state.env.RESPONDER_PLATFORM_API_KEY = response.body.responder_api_key || response.body.api_key;
1039
+ item.submitted_for_review = true;
1040
+ item.review_status = response.body.hotline_review_status || response.body.review_status || "pending";
1041
+ const verification = await verifyRegisteredHotline(state, {
1042
+ hotlineId: item.hotline_id,
1043
+ expectedTemplateRef: onboarding.body.template_ref
1044
+ });
1045
+ results.push({
1046
+ ...response.body,
1047
+ draft_file: onboarding.draft_file,
1048
+ used_draft: onboarding.used_draft,
1049
+ verification
1050
+ });
1051
+ }
1052
+ state.env = saveOpsState(state);
1053
+ emit({
1054
+ ok: true,
1055
+ responder_id: state.config.responder.responder_id,
1056
+ submitted: results.length,
1057
+ results
1058
+ });
1059
+ }
1060
+
1061
+ async function commandDoctor() {
1062
+ const state = ensureOpsState();
1063
+ const adapterChecks = (state.config.responder.hotlines || []).map((item) => {
1064
+ if (item.adapter_type === "http") {
1065
+ const valid = typeof item.adapter?.url === "string" && item.adapter.url.startsWith("http");
1066
+ return {
1067
+ hotline_id: item.hotline_id,
1068
+ adapter_type: item.adapter_type,
1069
+ ok: valid,
1070
+ detail: valid ? item.adapter.url : "invalid_http_url"
1071
+ };
1072
+ }
1073
+ const cmd = String(item.adapter?.cmd || "").trim();
1074
+ const firstToken = cmd.split(/\s+/).filter(Boolean)[0] || "";
1075
+ const isAbsolute = firstToken.startsWith("/");
1076
+ const valid = Boolean(cmd) && (!isAbsolute || fs.existsSync(firstToken));
1077
+ return {
1078
+ hotline_id: item.hotline_id,
1079
+ adapter_type: item.adapter_type || "process",
1080
+ ok: valid,
1081
+ detail: valid ? cmd : "process_command_missing_or_not_found"
1082
+ };
1083
+ });
1084
+ try {
1085
+ const response = await requestJson(supervisorUrlFromState(state), "/status");
1086
+ emit({
1087
+ ok: true,
1088
+ checks: response.body.runtime,
1089
+ debug: response.body.debug,
1090
+ adapters: adapterChecks
1091
+ });
1092
+ } catch (error) {
1093
+ emit({
1094
+ ok: false,
1095
+ message: error instanceof Error ? error.message : "unknown_error",
1096
+ config: state.config,
1097
+ adapters: adapterChecks
1098
+ });
1099
+ }
1100
+ }
1101
+
1102
+ async function commandDebugSnapshot() {
1103
+ const state = ensureOpsState();
1104
+ const response = await requestSupervisorJson(state, "/debug/snapshot");
1105
+ emit(response.body);
1106
+ }
1107
+
1108
+ async function commandRunExample(args) {
1109
+ const state = ensureOpsState();
1110
+ const text = String(args.text || "Summarize this local example request.").trim();
1111
+ const response = await requestSupervisorJson(state, "/requests/example", {
1112
+ method: "POST",
1113
+ body: { text }
1114
+ });
1115
+ if (response.status !== 201 || !response.body?.request_id) {
1116
+ emit({
1117
+ ok: false,
1118
+ ...response.body
1119
+ });
1120
+ return;
1121
+ }
1122
+ const requestId = response.body.request_id;
1123
+ const final = await waitFor(async () => {
1124
+ const current = await requestSupervisorJson(state, `/requests/${encodeURIComponent(requestId)}`);
1125
+ if (!["SUCCEEDED", "FAILED", "UNVERIFIED", "TIMED_OUT"].includes(current.body?.status)) {
1126
+ throw new Error("request_not_ready");
1127
+ }
1128
+ return current.body;
1129
+ });
1130
+ emit({
1131
+ ok: final.status === "SUCCEEDED",
1132
+ ...response.body,
1133
+ status: final.status,
1134
+ request: final,
1135
+ result_package: final.result_package || null
1136
+ });
1137
+ }
1138
+
1139
+ async function commandBootstrap(args) {
1140
+ const steps = [];
1141
+ const initialState = ensureOpsState();
1142
+ const setupArgs = ["setup"];
1143
+ if (args["responder-id"]) {
1144
+ setupArgs.push("--responder-id", String(args["responder-id"]));
1145
+ }
1146
+ if (args["display-name"]) {
1147
+ setupArgs.push("--display-name", String(args["display-name"]));
1148
+ }
1149
+
1150
+ const explicitPlatformUrl = typeof args.platform === "string" ? args.platform.trim() : "";
1151
+ const platformUrl = String(explicitPlatformUrl || initialState.config.platform.base_url || "http://127.0.0.1:8080").trim();
1152
+ const bootstrapUsesPlatform = Boolean(explicitPlatformUrl);
1153
+ const warnings = [];
1154
+ const env = { ...process.env };
1155
+ if (bootstrapUsesPlatform) {
1156
+ env.PLATFORM_API_BASE_URL = platformUrl;
1157
+ } else {
1158
+ if (process.env.PLATFORM_API_BASE_URL) {
1159
+ warnings.push("PLATFORM_API_BASE_URL ignored for bootstrap; pass --platform to enable platform mode.");
1160
+ }
1161
+ delete env.PLATFORM_API_BASE_URL;
1162
+ }
1163
+
1164
+ try {
1165
+ const setup = await runCliSubcommand(setupArgs, env);
1166
+ logBootstrapStep(steps, "setup_ok", true, { ops_home: setup.ops_home });
1167
+
1168
+ let state = ensureOpsState();
1169
+ if (bootstrapUsesPlatform) {
1170
+ state.config.platform.enabled = true;
1171
+ state.config.platform.base_url = platformUrl;
1172
+ state.config.platform_console ||= {};
1173
+ state.config.platform_console.base_url = platformUrl;
1174
+ state.env = saveOpsState(state);
1175
+ logBootstrapStep(steps, "platform_enabled", true, { platform_url: platformUrl });
1176
+ }
1177
+ const email =
1178
+ String(args.email || state.config.caller.contact_email || process.env.BOOTSTRAP_CALLER_EMAIL || "").trim() ||
1179
+ `ops-user-${Date.now()}@local.test`;
1180
+ if (state.config.caller.api_key && state.config.caller.contact_email) {
1181
+ logBootstrapStep(steps, "caller_registered", true, {
1182
+ caller_email: state.config.caller.contact_email,
1183
+ existing: true
1184
+ });
1185
+ } else {
1186
+ const registerArgs = bootstrapUsesPlatform
1187
+ ? ["auth", "register", "--email", email, "--platform", platformUrl]
1188
+ : ["auth", "register", "--email", email, "--local"];
1189
+ const register = await runCliSubcommand(registerArgs, env);
1190
+ logBootstrapStep(steps, "caller_registered", register.ok === true, {
1191
+ caller_email: register.contact_email || email,
1192
+ mode: register.mode || (bootstrapUsesPlatform ? "platform" : "local_only")
1193
+ });
1194
+ if (register.ok !== true) {
1195
+ emit({
1196
+ ok: false,
1197
+ stage: "caller_register_failed",
1198
+ steps,
1199
+ response: register
1200
+ });
1201
+ return;
1202
+ }
1203
+ }
1204
+
1205
+ state = ensureOpsState();
1206
+ const hasExample = (state.config.responder.hotlines || []).some((item) => item.hotline_id === LOCAL_EXAMPLE_HOTLINE_ID);
1207
+ const added = await runCliSubcommand(["add-example-hotline"], env);
1208
+ logBootstrapStep(steps, "example_hotline_added", added.ok !== false, {
1209
+ hotline_id: added.hotline_id || LOCAL_EXAMPLE_HOTLINE_ID,
1210
+ existing: hasExample,
1211
+ refreshed: hasExample
1212
+ });
1213
+ if (added.ok === false) {
1214
+ emit({
1215
+ ok: false,
1216
+ stage: "example_hotline_add_failed",
1217
+ steps,
1218
+ response: added
1219
+ });
1220
+ return;
1221
+ }
1222
+
1223
+ state = ensureOpsState();
1224
+ const example = (state.config.responder.hotlines || []).find((item) => item.hotline_id === LOCAL_EXAMPLE_HOTLINE_ID);
1225
+ if (bootstrapUsesPlatform) {
1226
+ if (example?.submitted_for_review === true) {
1227
+ logBootstrapStep(steps, "review_submitted", true, {
1228
+ submitted: 0,
1229
+ existing: true
1230
+ });
1231
+ } else {
1232
+ const review = await runCliSubcommand(["submit-review"], env);
1233
+ const reviewOk = review.ok === true || typeof review.submitted === "number";
1234
+ logBootstrapStep(steps, "review_submitted", reviewOk, {
1235
+ submitted: review.submitted || 0
1236
+ });
1237
+ if (!reviewOk) {
1238
+ emit({
1239
+ ok: false,
1240
+ stage: "submit_review_failed",
1241
+ steps,
1242
+ response: review
1243
+ });
1244
+ return;
1245
+ }
1246
+ }
1247
+ } else {
1248
+ logBootstrapStep(steps, "review_skipped", true, {
1249
+ mode: "local_only"
1250
+ });
1251
+ }
1252
+
1253
+ const enabled = await runCliSubcommand(["enable-responder"], env);
1254
+ const responderId = enabled.responder?.responder_id || enabled.responder_id || ensureOpsState().config.responder.responder_id;
1255
+ logBootstrapStep(steps, "responder_enabled", enabled.ok === true, { responder_id: responderId });
1256
+ if (enabled.ok !== true) {
1257
+ emit({
1258
+ ok: false,
1259
+ stage: "enable_responder_failed",
1260
+ steps,
1261
+ response: enabled
1262
+ });
1263
+ return;
1264
+ }
1265
+
1266
+ const supervisorUrl = supervisorUrlFromState(ensureOpsState());
1267
+ const supervisor = await ensureSupervisorAvailable(supervisorUrl, env);
1268
+ logBootstrapStep(steps, "supervisor_started", true, supervisor);
1269
+ const supervisorSetup = await requestJson(supervisorUrl, "/setup", {
1270
+ method: "POST",
1271
+ body: {}
1272
+ });
1273
+ logBootstrapStep(steps, "supervisor_config_synced", supervisorSetup.status === 200);
1274
+ if (supervisorSetup.status !== 200) {
1275
+ emit({
1276
+ ok: false,
1277
+ stage: "supervisor_config_sync_failed",
1278
+ steps,
1279
+ response: supervisorSetup.body || supervisorSetup
1280
+ });
1281
+ return;
1282
+ }
1283
+
1284
+ const adminApiKey = process.env.PLATFORM_ADMIN_API_KEY || process.env.ADMIN_API_KEY || null;
1285
+ if (bootstrapUsesPlatform) {
1286
+ let catalogVisible = null;
1287
+ try {
1288
+ catalogVisible = await waitForCatalogVisibility(supervisorUrl, responderId, {
1289
+ timeoutMs: 750,
1290
+ intervalMs: 150
1291
+ });
1292
+ } catch {}
1293
+
1294
+ if (adminApiKey) {
1295
+ const approved = await maybeApproveExample({
1296
+ platformUrl,
1297
+ adminApiKey,
1298
+ responderId
1299
+ });
1300
+ if (!approved.ok && !catalogVisible) {
1301
+ emit({
1302
+ ok: false,
1303
+ stage: "awaiting_admin_approval",
1304
+ steps,
1305
+ responder_id: responderId,
1306
+ hotline_id: LOCAL_EXAMPLE_HOTLINE_ID,
1307
+ next_action: "Approve the responder and hotline runtime, then rerun delexec-ops bootstrap or delexec-ops run-example.",
1308
+ reason: approved.reason || "approval_failed"
1309
+ });
1310
+ return;
1311
+ }
1312
+ if (approved.ok) {
1313
+ logBootstrapStep(steps, "responder_approved", true);
1314
+ logBootstrapStep(steps, "hotline_approved", true);
1315
+ }
1316
+ catalogVisible = await waitForCatalogVisibility(supervisorUrl, responderId, {
1317
+ timeoutMs: 15000,
1318
+ intervalMs: 250
1319
+ });
1320
+ } else if (!catalogVisible) {
1321
+ emit({
1322
+ ok: false,
1323
+ stage: "awaiting_admin_approval",
1324
+ steps,
1325
+ warnings,
1326
+ responder_id: responderId,
1327
+ hotline_id: LOCAL_EXAMPLE_HOTLINE_ID,
1328
+ next_action: "Approve the responder and hotline runtime, then rerun delexec-ops bootstrap or delexec-ops run-example.",
1329
+ reason: "admin_api_key_missing"
1330
+ });
1331
+ return;
1332
+ }
1333
+ logBootstrapStep(steps, "catalog_visible", true, { hotline_id: LOCAL_EXAMPLE_HOTLINE_ID });
1334
+ } else {
1335
+ logBootstrapStep(steps, "local_hotline_ready", true, { hotline_id: LOCAL_EXAMPLE_HOTLINE_ID });
1336
+ }
1337
+
1338
+ const started = await requestJson(supervisorUrl, "/requests/example", {
1339
+ method: "POST",
1340
+ body: {
1341
+ text: String(args.text || process.env.BOOTSTRAP_EXAMPLE_TEXT || "Summarize this bootstrap request.").trim()
1342
+ }
1343
+ });
1344
+ if (started.status !== 201 || !started.body?.request_id) {
1345
+ if (bootstrapUsesPlatform && !adminApiKey) {
1346
+ emit({
1347
+ ok: false,
1348
+ stage: "awaiting_admin_approval",
1349
+ steps,
1350
+ responder_id: responderId,
1351
+ hotline_id: LOCAL_EXAMPLE_HOTLINE_ID,
1352
+ next_action: "Approve the responder and hotline runtime, then rerun delexec-ops bootstrap or delexec-ops run-example.",
1353
+ reason: started.body?.error?.code || "request_not_callable"
1354
+ });
1355
+ return;
1356
+ }
1357
+ emit({
1358
+ ok: false,
1359
+ stage: "request_start_failed",
1360
+ steps,
1361
+ warnings,
1362
+ response: started.body || started
1363
+ });
1364
+ return;
1365
+ }
1366
+
1367
+ const requestId = started.body.request_id;
1368
+ const final = await waitFor(async () => {
1369
+ const current = await requestJson(supervisorUrl, `/requests/${encodeURIComponent(requestId)}`);
1370
+ if (!["SUCCEEDED", "FAILED", "UNVERIFIED", "TIMED_OUT"].includes(current.body?.status)) {
1371
+ throw new Error("request_not_ready");
1372
+ }
1373
+ return current.body;
1374
+ });
1375
+ logBootstrapStep(steps, "request_succeeded", final.status === "SUCCEEDED", {
1376
+ request_id: requestId,
1377
+ status: final.status
1378
+ });
1379
+
1380
+ emit({
1381
+ ok: final.status === "SUCCEEDED",
1382
+ request_id: requestId,
1383
+ status: final.status,
1384
+ responder_id: responderId,
1385
+ hotline_id: LOCAL_EXAMPLE_HOTLINE_ID,
1386
+ supervisor_url: supervisorUrl,
1387
+ ui: args["open-ui"] ? await ensureUiAvailable(args, env) : null,
1388
+ warnings,
1389
+ next_steps: {
1390
+ one_click_start: "delexec-ops bootstrap --open-ui",
1391
+ reopen_web_ui: "delexec-ops ui start --open",
1392
+ local_services: "delexec-ops start",
1393
+ health_check: "delexec-ops status"
1394
+ },
1395
+ steps
1396
+ });
1397
+ } catch (error) {
1398
+ emit({
1399
+ ok: false,
1400
+ stage: "bootstrap_failed",
1401
+ steps,
1402
+ warnings,
1403
+ error: error instanceof Error ? error.message : "unknown_error"
1404
+ });
1405
+ }
1406
+ }
1407
+
1408
+ async function commandUiStart(args) {
1409
+ const state = ensureOpsState();
1410
+ const supervisorUrl = supervisorUrlFromState(state);
1411
+ const supervisor = await ensureSupervisorAvailable(supervisorUrl, process.env);
1412
+ const ui = await ensureUiAvailable(args, process.env);
1413
+ emit({
1414
+ ok: true,
1415
+ supervisor_url: supervisorUrl,
1416
+ supervisor_started: supervisor.started,
1417
+ ui,
1418
+ next_steps: {
1419
+ reopen_web_ui: "delexec-ops ui start --open",
1420
+ refresh_status: "delexec-ops status"
1421
+ }
1422
+ });
1423
+ }
1424
+
1425
+ async function main() {
1426
+ const args = parseArgs(process.argv.slice(2));
1427
+ if (args.help || args.h) {
1428
+ usage();
1429
+ process.exit(0);
1430
+ }
1431
+ if (args._.length === 0) {
1432
+ usage();
1433
+ process.exit(1);
1434
+ }
1435
+
1436
+ const [group, command] = args._;
1437
+
1438
+ if (group === "setup") {
1439
+ await commandSetup(args);
1440
+ return;
1441
+ }
1442
+ if (group === "start") {
1443
+ await commandStart();
1444
+ return;
1445
+ }
1446
+ if (group === "status") {
1447
+ await commandStatus();
1448
+ return;
1449
+ }
1450
+ if (group === "bootstrap") {
1451
+ await commandBootstrap(args);
1452
+ return;
1453
+ }
1454
+ if (group === "ui" && command === "start") {
1455
+ await commandUiStart(args);
1456
+ return;
1457
+ }
1458
+ if (group === "mcp" && command === "spec") {
1459
+ await commandMcpSpec();
1460
+ return;
1461
+ }
1462
+ if (group === "enable-responder") {
1463
+ await commandEnableResponder(args);
1464
+ return;
1465
+ }
1466
+ if (group === "add-hotline") {
1467
+ await commandAddHotline(args);
1468
+ return;
1469
+ }
1470
+ if (group === "attach-project") {
1471
+ await commandAttachProject(args);
1472
+ return;
1473
+ }
1474
+ if (group === "add-example-hotline") {
1475
+ await commandAddExampleHotline();
1476
+ return;
1477
+ }
1478
+ if (group === "enable-hotline") {
1479
+ await commandSetHotlineEnabled(args, true);
1480
+ return;
1481
+ }
1482
+ if (group === "remove-hotline") {
1483
+ await commandRemoveHotline(args);
1484
+ return;
1485
+ }
1486
+ if (group === "disable-hotline") {
1487
+ await commandSetHotlineEnabled(args, false);
1488
+ return;
1489
+ }
1490
+ if (group === "doctor") {
1491
+ await commandDoctor();
1492
+ return;
1493
+ }
1494
+ if (group === "debug-snapshot") {
1495
+ await commandDebugSnapshot();
1496
+ return;
1497
+ }
1498
+ if (group === "submit-review") {
1499
+ await commandSubmitReview(args);
1500
+ return;
1501
+ }
1502
+ if (group === "run-example") {
1503
+ await commandRunExample(args);
1504
+ return;
1505
+ }
1506
+ if (group === "auth" && command === "register") {
1507
+ await commandAuthRegister(args);
1508
+ return;
1509
+ }
1510
+
1511
+ if ((group === "responder" || group === "responder") && command === "init") {
1512
+ await commandSetup(args);
1513
+ return;
1514
+ }
1515
+ if ((group === "responder" || group === "responder") && command === "register") {
1516
+ await commandSubmitReview(args);
1517
+ return;
1518
+ }
1519
+ if ((group === "responder" || group === "responder") && command === "show-draft") {
1520
+ await commandShowDraft(args);
1521
+ return;
1522
+ }
1523
+ if ((group === "responder" || group === "responder") && command === "submit-draft") {
1524
+ await commandSubmitReview(args);
1525
+ return;
1526
+ }
1527
+ if ((group === "responder" || group === "responder") && command === "add-hotline") {
1528
+ await commandAddHotline(args);
1529
+ return;
1530
+ }
1531
+ if ((group === "responder" || group === "responder") && command === "attach-project") {
1532
+ await commandAttachProject(args);
1533
+ return;
1534
+ }
1535
+ if ((group === "responder" || group === "responder") && command === "enable-hotline") {
1536
+ await commandSetHotlineEnabled(args, true);
1537
+ return;
1538
+ }
1539
+ if ((group === "responder" || group === "responder") && command === "remove-hotline") {
1540
+ await commandRemoveHotline(args);
1541
+ return;
1542
+ }
1543
+ if ((group === "responder" || group === "responder") && command === "disable-hotline") {
1544
+ await commandSetHotlineEnabled(args, false);
1545
+ return;
1546
+ }
1547
+ if ((group === "responder" || group === "responder") && command === "start") {
1548
+ await commandStart();
1549
+ return;
1550
+ }
1551
+ if ((group === "responder" || group === "responder") && command === "status") {
1552
+ await commandStatus();
1553
+ return;
1554
+ }
1555
+ if ((group === "responder" || group === "responder") && command === "doctor") {
1556
+ await commandDoctor();
1557
+ return;
1558
+ }
1559
+ if ((group === "responder" || group === "responder") && command === "debug-snapshot") {
1560
+ await commandDebugSnapshot();
1561
+ return;
1562
+ }
1563
+
1564
+ usage();
1565
+ throw new Error(`unsupported_command:${group || ""}:${command || ""}`);
1566
+ }
1567
+
1568
+ main().catch((error) => {
1569
+ console.error(`[delexec-ops] ${error instanceof Error ? error.message : "unknown_error"}`);
1570
+ process.exit(1);
1571
+ });