@workflow-code/cli 0.2.4-20261010002.codex-cli-web → 0.2.4-20261010004.codex-cli-web

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -24,7 +24,7 @@ async function main(argv = process.argv.slice(2)) {
24
24
  const prepared = prepareCliInvocation(argv);
25
25
  argv = prepared.argv;
26
26
  if (argv[0] === "web") {
27
- const webCli = await import("./web-KTQSEVQT.js");
27
+ const webCli = await import("./web-KUC6BL6E.js");
28
28
  return webCli.main(argv.slice(1));
29
29
  }
30
30
  if (argv[0] === "workspace") {
@@ -1,11 +1,12 @@
1
1
  import { readFileSync } from 'node:fs';
2
- import { readFile, writeFile, readdir, rm, stat, mkdir, link } from 'node:fs/promises';
2
+ import { readFile, writeFile, readdir, rm, stat, mkdir, link, mkdtemp } from 'node:fs/promises';
3
3
  import path from 'node:path';
4
4
  import { createHash } from 'node:crypto';
5
5
  import { register } from 'tsx/esm/api';
6
6
  import { resolve } from 'import-meta-resolve';
7
7
  import { pathToFileURL, fileURLToPath } from 'node:url';
8
8
  let ts;
9
+ let assistantScopeDirectory;
9
10
 
10
11
 
11
12
  const write = process.stdout.write.bind(process.stdout);
@@ -93,8 +94,21 @@ try {
93
94
  }
94
95
  send('result', { projects, errors });
95
96
  } else if (request.project?.folderPath) {
96
- const projectKey = createHash('sha256').update(request.project.folderPath).digest('hex').slice(0, 24);
97
+ const projectKey = createHash('sha256').update(request.project.assistant ? `assistant:${request.project.assistant.workdir}` : request.project.folderPath).digest('hex').slice(0, 24);
97
98
  const dataDir = path.join(request.storageDir, projectKey);
99
+ if (request.project.assistant) {
100
+ const assistant = request.project.assistant;
101
+ const primary = assistant.roots.find(root => root.folderPath === assistant.workdir);
102
+ if (!primary) { throw new Error('Assistant working directory is not an opened workspace folder.'); }
103
+ await mkdir(dataDir, { recursive: true });
104
+ assistantScopeDirectory = await mkdtemp(path.join(dataDir, 'scope-'));
105
+ const manifestPath = path.join(assistantScopeDirectory, 'workspace.json');
106
+ await writeFile(manifestPath, JSON.stringify({ schemaVersion: 2, primaryTarget: { kind: 'folder', folderId: primary.uri }, warnings: [],
107
+ roots: assistant.roots.map(root => ({ target: { kind: 'folder', folderId: root.uri }, name: root.name, rootPath: root.folderPath, role: root === primary ? 'primary' : 'linked', depth: root === primary ? 0 : 1 })) }));
108
+ process.env.WORKFLOW_DEVELOPER_ASSISTANT_HOST = 'vscode';
109
+ process.env.WORKFLOW_DEVELOPER_ASSISTANT_WORKDIR = assistant.workdir;
110
+ process.env.WORKFLOW_DEVELOPER_ASSISTANT_WORKSPACE_MANIFEST = manifestPath;
111
+ }
98
112
  const storage = { host: 'local', local: { kvStoreDir: path.join(dataDir, 'kv'), persistentValueStoreDir: path.join(dataDir, 'values') } };
99
113
  const filesDir = path.join(dataDir, 'files');
100
114
  const createdFiles = [];
@@ -205,4 +219,6 @@ try {
205
219
  } catch (error) {
206
220
  write(`__WORKFLOW_WEB__${JSON.stringify({ kind: 'error', error: error.message, status: error.metadata?.status })}\n`);
207
221
  process.exitCode = 1;
222
+ } finally {
223
+ if (assistantScopeDirectory) { await rm(assistantScopeDirectory, { recursive: true, force: true }); }
208
224
  }
@@ -4102,6 +4102,7 @@ ${state.user?.avatarUrl ?? ""}`;
4102
4102
  constructor(emit) {
4103
4103
  this.emit = emit;
4104
4104
  if (this.token) {
4105
+ sessionStorage.setItem("workflow-web-session", this.token);
4105
4106
  history.replaceState(null, "", `${location.pathname}${location.search}`);
4106
4107
  }
4107
4108
  queueMicrotask(() => {
@@ -4139,7 +4140,7 @@ ${state.user?.avatarUrl ?? ""}`;
4139
4140
  status = "connecting";
4140
4141
  attempts = 0;
4141
4142
  closed = false;
4142
- token = new URLSearchParams(location.hash.slice(1)).get("session");
4143
+ token = new URLSearchParams(location.hash.slice(1)).get("session") ?? sessionStorage.getItem("workflow-web-session");
4143
4144
  update(status) {
4144
4145
  this.status = status;
4145
4146
  this.emit({ type: "connection", status });
@@ -4186,9 +4187,8 @@ ${state.user?.avatarUrl ?? ""}`;
4186
4187
  this.update("expired");
4187
4188
  return;
4188
4189
  }
4189
- this.token = null;
4190
4190
  }
4191
- const session = await fetch("/api/session", { signal: AbortSignal.timeout(1e4) });
4191
+ const session = await fetch("/api/session", { headers: this.headers(), signal: AbortSignal.timeout(1e4) });
4192
4192
  if (session.status === 401) {
4193
4193
  this.update("expired");
4194
4194
  return;
@@ -4196,13 +4196,13 @@ ${state.user?.avatarUrl ?? ""}`;
4196
4196
  if (!session.ok) {
4197
4197
  throw new Error("Cannot connect to the project.");
4198
4198
  }
4199
- const source = this.source = new EventSource("/api/events");
4199
+ const source = this.source = new EventSource(this.fileUrl("/api/events"));
4200
4200
  source.onopen = () => {
4201
4201
  this.attempts = 0;
4202
4202
  this.update("connected");
4203
4203
  };
4204
4204
  source.onmessage = (event) => {
4205
- this.emit(JSON.parse(event.data));
4205
+ this.emit(this.publicFiles(JSON.parse(event.data)));
4206
4206
  };
4207
4207
  source.onerror = () => {
4208
4208
  if (this.source === source && !this.closed) {
@@ -4229,7 +4229,7 @@ ${state.user?.avatarUrl ?? ""}`;
4229
4229
  }
4230
4230
  let response;
4231
4231
  try {
4232
- response = await fetch("/api/message", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(message) });
4232
+ response = await fetch("/api/message", { method: "POST", headers: { ...this.headers(), "Content-Type": "application/json" }, body: JSON.stringify(message) });
4233
4233
  } catch {
4234
4234
  this.interrupted();
4235
4235
  throw new ConnectionError("\u8FDE\u63A5\u5DF2\u4E2D\u65AD\u3002");
@@ -4249,7 +4249,25 @@ ${state.user?.avatarUrl ?? ""}`;
4249
4249
  if (!response.ok) {
4250
4250
  throw new Error(reply.message || "Request failed.");
4251
4251
  }
4252
- return reply.messages ?? [];
4252
+ return (reply.messages ?? []).map((message2) => this.publicFiles(message2));
4253
+ }
4254
+ headers() {
4255
+ return this.token ? { Authorization: `Bearer ${this.token}` } : {};
4256
+ }
4257
+ fileUrl(path) {
4258
+ return this.token ? `${path}?session=${encodeURIComponent(this.token)}` : path;
4259
+ }
4260
+ publicFiles(value) {
4261
+ if (Array.isArray(value)) {
4262
+ return value.map((entry) => this.publicFiles(entry));
4263
+ }
4264
+ if (!value || typeof value !== "object") {
4265
+ return value;
4266
+ }
4267
+ return Object.fromEntries(Object.entries(value).map(([key, entry]) => [
4268
+ key,
4269
+ key === "previewUrl" && typeof entry === "string" && entry.startsWith("/api/file/") ? this.fileUrl(entry) : this.publicFiles(entry)
4270
+ ]));
4253
4271
  }
4254
4272
  };
4255
4273
 
@@ -4280,6 +4298,17 @@ ${state.user?.avatarUrl ?? ""}`;
4280
4298
  handleReply(entry);
4281
4299
  }
4282
4300
  };
4301
+ addEventListener("message", (event) => {
4302
+ if (window.parent === window || event.source !== window.parent || event.data?.type !== "workflow-host") {
4303
+ return;
4304
+ }
4305
+ if (event.data.theme === "dark" || event.data.theme === "light") {
4306
+ document.documentElement.dataset.theme = event.data.theme;
4307
+ }
4308
+ if (event.data.command === "new" || event.data.command === "history") {
4309
+ emit({ type: "assistant-command", command: event.data.command });
4310
+ }
4311
+ });
4283
4312
  const handleReply = (reply) => {
4284
4313
  if (reply.type === "download") {
4285
4314
  const url = URL.createObjectURL(new Blob([reply.content], { type: reply.mimeType ?? "application/json" }));
@@ -4364,7 +4393,7 @@ ${state.user?.avatarUrl ?? ""}`;
4364
4393
  return;
4365
4394
  }
4366
4395
  if (message.type === "open-file") {
4367
- window.open(`/api/file/${encodeURIComponent(message.id)}`, "_blank", "noopener");
4396
+ window.open(connection.fileUrl(`/api/file/${encodeURIComponent(message.id)}`), "_blank", "noopener");
4368
4397
  return;
4369
4398
  }
4370
4399
  if (message.type === "login") {
@@ -17,7 +17,7 @@ import "./chunk-NSPRIPOP.js";
17
17
 
18
18
  // src/web.ts
19
19
  import { spawn as spawn3 } from "child_process";
20
- import path3 from "path";
20
+ import path4 from "path";
21
21
 
22
22
  // src/web/server.ts
23
23
  import { createServer } from "http";
@@ -965,7 +965,7 @@ async function loginWebAccount(serverUrl, email, password) {
965
965
  // src/web/server.ts
966
966
  async function startWebServer(options) {
967
967
  const projectDir = await realpath(path2.resolve(options.projectDir));
968
- const key = createHash("sha256").update(projectDir).digest("hex").slice(0, 24);
968
+ const key = createHash("sha256").update(options.host?.assistant ? `assistant:${options.host.assistant.workdir}` : projectDir).digest("hex").slice(0, 24);
969
969
  const storageDir = path2.resolve(options.dataDir ?? path2.join(os.homedir(), ".workflow-code", "web"));
970
970
  const historyPath = path2.join(storageDir, key, "conversations.json");
971
971
  const clients = /* @__PURE__ */ new Set();
@@ -975,6 +975,10 @@ async function startWebServer(options) {
975
975
  let authIdentity = "";
976
976
  let login;
977
977
  let passwordLogin = false;
978
+ let hostConnection = options.host?.connection;
979
+ let hostManaged = !!hostConnection;
980
+ let authEpoch = 0;
981
+ const sensitiveTokens = /* @__PURE__ */ new Set();
978
982
  const runtime = options.runtime ?? createWebRuntime(projectDir, storageDir, () => connection);
979
983
  let inspected;
980
984
  try {
@@ -996,6 +1000,14 @@ async function startWebServer(options) {
996
1000
  throw new Error("workflow-code web currently supports Conversation and Workflow projects.");
997
1001
  }
998
1002
  const project = { ...descriptor, uri: pathToFileURL(projectDir).href };
1003
+ if (options.host?.assistant) {
1004
+ project.assistant = options.host.assistant;
1005
+ project.uri = `studio-assistant:${encodeURIComponent(project.assistant.workdir)}`;
1006
+ project.requiresLogin = true;
1007
+ project.fields = project.fields.map((field) => field.name === "permissionPolicy" ? { ...field, defaultValue: "workspace" } : field);
1008
+ const supported = /* @__PURE__ */ new Set(["read", "write", "edit", "bash", "grep", "find", "ls", "skill", "workflow-memory_global_memory", "workflow-ui_present_card"]);
1009
+ project.tools = project.tools?.filter((tool) => supported.has(tool.name));
1010
+ }
999
1011
  let saved = {};
1000
1012
  try {
1001
1013
  saved = JSON.parse(await readFile(historyPath, "utf8"));
@@ -1024,7 +1036,7 @@ async function startWebServer(options) {
1024
1036
  }
1025
1037
  return value;
1026
1038
  };
1027
- const state = () => publicValue(redactWorkflowData({ type: "state", project, sessions: controller.sessions, activeId: controller.activeId, running: controller.running, parameterValues: controller.current.parameterValues ?? {}, account, authenticationRevision: authRevision }, [connection?.accessToken ?? ""]));
1039
+ const state = () => publicValue(redactWorkflowData({ type: "state", project, sessions: controller.sessions, activeId: controller.activeId, running: controller.running, parameterValues: controller.current.parameterValues ?? {}, account, authenticationRevision: authRevision }, [...sensitiveTokens]));
1028
1040
  const broadcast = () => {
1029
1041
  if (broadcastTimer) {
1030
1042
  return;
@@ -1058,9 +1070,16 @@ async function startWebServer(options) {
1058
1070
  if (passwordLogin || login) {
1059
1071
  return;
1060
1072
  }
1073
+ const epoch = ++authEpoch;
1061
1074
  const config = await readCliAuthConfig();
1062
- const serverUrl = process.env.WORKFLOW_SERVER_URL ?? config.serverUrl;
1063
- const token = process.env.WORKFLOW_SERVER_ADMIN_KEY ?? config.apiKey;
1075
+ if (epoch !== authEpoch) {
1076
+ return;
1077
+ }
1078
+ const serverUrl = hostManaged ? hostConnection?.serverUrl : process.env.WORKFLOW_SERVER_URL ?? config.serverUrl;
1079
+ const token = hostManaged ? hostConnection?.accessToken : process.env.WORKFLOW_SERVER_ADMIN_KEY ?? config.apiKey;
1080
+ if (token) {
1081
+ sensitiveTokens.add(token);
1082
+ }
1064
1083
  const identity = signedOut ? "" : `${serverUrl}
1065
1084
  ${token}`;
1066
1085
  if (!force && identity === authIdentity && account.status !== "checking") {
@@ -1081,11 +1100,17 @@ ${token}`;
1081
1100
  try {
1082
1101
  const response = await fetch(`${connection.serverUrl}/api/auth/me`, { headers: { Authorization: `Bearer ${connection.accessToken}` }, signal: AbortSignal.timeout(1e4), redirect: "error" });
1083
1102
  const result = await response.json();
1103
+ if (epoch !== authEpoch) {
1104
+ return;
1105
+ }
1084
1106
  if (!response.ok || result.errCode !== 0 || !result.data?.profile) {
1085
1107
  throw new Error("Workflow login is invalid or expired.");
1086
1108
  }
1087
1109
  account = { ...account, status: "authenticated", user: result.data.profile, expiresAt: result.data.principal?.expiresAt ?? config.expiresAt };
1088
1110
  } catch (error) {
1111
+ if (epoch !== authEpoch) {
1112
+ return;
1113
+ }
1089
1114
  connection = void 0;
1090
1115
  if (!changed) {
1091
1116
  authRevision++;
@@ -1180,20 +1205,22 @@ ${token}`;
1180
1205
  const states = parameterStates.get(stateKey) ?? {};
1181
1206
  const trigger = message.trigger?.kind === "param" && parameters.fields.some((field) => field.name === message.trigger.paramName) ? { kind: "param", paramName: message.trigger.paramName } : { kind: message.trigger?.kind === "input" ? "input" : "init" };
1182
1207
  const result = parameters.paramResolver?.enabled ? await runtime.resolveParams(project, { draftText: String(message.draftText ?? "").slice(0, 2e4), args: workflowParamArgs(parameters.fields, values, states), inputMode: "form", trigger }, entrypointId) : { params: {} };
1183
- if (parameterEpochs.get(stateKey) !== epoch || revision !== authRevision) {
1208
+ if (revision !== authRevision) {
1184
1209
  break;
1185
1210
  }
1186
- for (const [name, patch] of Object.entries(result.params ?? {})) {
1187
- states[name] = { ...states[name], ...patch, options: { ...states[name]?.options, ...patch.options } };
1188
- if (patch.value === null) {
1189
- delete values[name];
1190
- } else if (patch.value !== void 0) {
1191
- values[name] = patch.value;
1211
+ if (parameterEpochs.get(stateKey) === epoch) {
1212
+ for (const [name, patch] of Object.entries(result.params ?? {})) {
1213
+ states[name] = { ...states[name], ...patch, options: { ...states[name]?.options, ...patch.options } };
1214
+ if (patch.value === null) {
1215
+ delete values[name];
1216
+ } else if (patch.value !== void 0) {
1217
+ values[name] = patch.value;
1218
+ }
1192
1219
  }
1220
+ parameterStates.set(stateKey, states);
1221
+ (current.parameterValues ??= {})[entrypointId ?? ""] = values;
1222
+ await controller.persist();
1193
1223
  }
1194
- parameterStates.set(stateKey, states);
1195
- (current.parameterValues ??= {})[entrypointId ?? ""] = values;
1196
- await controller.persist();
1197
1224
  replies.push({ type: "params", projectUri: project.uri, sessionId: current.id, requestId: message.requestId, params: result.params });
1198
1225
  } catch (error) {
1199
1226
  replies.push({ type: "params", projectUri: project.uri, sessionId: current.id, requestId: message.requestId, error: String(error.message) });
@@ -1339,7 +1366,7 @@ ${token}`;
1339
1366
  break;
1340
1367
  }
1341
1368
  case "login": {
1342
- if (process.env.WORKFLOW_SERVER_ADMIN_KEY !== void 0) {
1369
+ if (!options.host && process.env.WORKFLOW_SERVER_ADMIN_KEY !== void 0) {
1343
1370
  throw new Error("This session is supplied by the host. Sign in through the host.");
1344
1371
  }
1345
1372
  if (login || passwordLogin) {
@@ -1359,6 +1386,8 @@ ${token}`;
1359
1386
  throw new Error("Invalid login credentials.");
1360
1387
  }
1361
1388
  await loginWebAccount(url.toString().replace(/\/+$/, ""), message.email, message.password);
1389
+ hostManaged = false;
1390
+ hostConnection = void 0;
1362
1391
  } catch (error) {
1363
1392
  account = { ...account, status: "error", error: String(error.message) };
1364
1393
  broadcast();
@@ -1389,7 +1418,7 @@ ${token}`;
1389
1418
  case "logout":
1390
1419
  signedOut = true;
1391
1420
  login?.kill();
1392
- if (process.env.WORKFLOW_SERVER_ADMIN_KEY === void 0) {
1421
+ if (!hostManaged && process.env.WORKFLOW_SERVER_ADMIN_KEY === void 0) {
1393
1422
  await clearCliAuthConfig();
1394
1423
  }
1395
1424
  authIdentity = "signed-out";
@@ -1405,7 +1434,7 @@ ${token}`;
1405
1434
  void (async () => {
1406
1435
  response.setHeader("X-Content-Type-Options", "nosniff");
1407
1436
  response.setHeader("Referrer-Policy", "no-referrer");
1408
- if (request.headers.host !== new URL(origin).host || request.headers.origin && request.headers.origin !== origin || request.headers["sec-fetch-site"] === "cross-site") {
1437
+ if (request.headers.host !== new URL(origin).host || request.headers.origin && request.headers.origin !== origin) {
1409
1438
  json(response, 403, { message: "Invalid request origin." });
1410
1439
  return;
1411
1440
  }
@@ -1420,8 +1449,14 @@ ${token}`;
1420
1449
  return;
1421
1450
  }
1422
1451
  if (url.pathname.startsWith("/api/")) {
1452
+ if (request.headers["sec-fetch-site"] === "cross-site") {
1453
+ json(response, 403, { message: "Invalid request origin." });
1454
+ return;
1455
+ }
1423
1456
  const value = String(request.headers.cookie ?? "").split(";").map((value2) => value2.trim()).find((value2) => value2.startsWith(`${cookie}=`))?.slice(cookie.length + 1) ?? "";
1424
- if (!authMatches(value)) {
1457
+ const bearer = String(request.headers.authorization ?? "").replace(/^Bearer /, "");
1458
+ const readSession = request.method === "GET" && (url.pathname === "/api/events" || url.pathname.startsWith("/api/file/")) ? url.searchParams.get("session") ?? "" : "";
1459
+ if (!authMatches(value) && !authMatches(bearer) && !authMatches(readSession)) {
1425
1460
  json(response, 401, { message: "Open the URL printed by workflow-code web." });
1426
1461
  return;
1427
1462
  }
@@ -1453,7 +1488,7 @@ ${token}`;
1453
1488
  mutations = task;
1454
1489
  }
1455
1490
  await task;
1456
- json(response, 200, { messages: redactWorkflowData(messages, [connection?.accessToken ?? ""]) });
1491
+ json(response, 200, { messages: redactWorkflowData(messages, [...sensitiveTokens]) });
1457
1492
  return;
1458
1493
  }
1459
1494
  if (url.pathname.startsWith("/api/file/") && request.method === "GET") {
@@ -1477,8 +1512,9 @@ ${token}`;
1477
1512
  return;
1478
1513
  }
1479
1514
  if (url.pathname === "/") {
1480
- response.writeHead(200, { "Content-Type": "text/html; charset=utf-8", "Cache-Control": "no-store", "Content-Security-Policy": "default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data: https: http:; font-src 'self'; connect-src 'self'; media-src 'self' blob:; object-src 'none'; base-uri 'none'; frame-ancestors 'self' vscode-webview:; form-action 'self'" });
1481
- response.end(`<!doctype html><html lang="zh-CN" data-theme="${options.theme ?? "auto"}"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1"><meta name="workflow-project" content="${key}"><title>Workflow</title><link rel="stylesheet" href="/assets/codicons/codicon.css"><link rel="stylesheet" href="/assets/workflow-ui.css"><link rel="stylesheet" href="/assets/theme.css"></head><body data-surface="editor"><div id="app"></div><script src="/assets/workflow-ui.js" defer></script></body></html>`);
1515
+ const ancestors = options.host ? "'self' vscode-webview: vscode-file: https://*.vscode-cdn.net https://*.vscode-webview.net http://127.0.0.1:* http://localhost:*" : "'self' vscode-webview:";
1516
+ response.writeHead(200, { "Content-Type": "text/html; charset=utf-8", "Cache-Control": "no-store", "Content-Security-Policy": `default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data: https: http:; font-src 'self'; connect-src 'self'; media-src 'self' blob:; object-src 'none'; base-uri 'none'; frame-ancestors ${ancestors}; form-action 'self'` });
1517
+ response.end(`<!doctype html><html lang="zh-CN" data-theme="${options.theme ?? "auto"}"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1"><meta name="workflow-project" content="${key}"><title>Workflow</title><link rel="stylesheet" href="/assets/codicons/codicon.css"><link rel="stylesheet" href="/assets/workflow-ui.css"><link rel="stylesheet" href="/assets/theme.css"></head><body data-surface="${options.host?.surface ?? "editor"}"><div id="app"></div><script src="/assets/workflow-ui.js" defer></script></body></html>`);
1482
1518
  return;
1483
1519
  }
1484
1520
  const assets = { "/assets/workflow-ui.js": "application/javascript", "/assets/workflow-ui.css": "text/css", "/assets/theme.css": "text/css", "/assets/codicons/codicon.css": "text/css", "/assets/codicons/codicon.ttf": "font/ttf" };
@@ -1491,7 +1527,7 @@ ${token}`;
1491
1527
  response.end(content);
1492
1528
  })().catch((error) => {
1493
1529
  if (!response.headersSent) {
1494
- json(response, 400, { message: String(redactWorkflowData(error.message, [connection?.accessToken ?? ""])) });
1530
+ json(response, 400, { message: String(redactWorkflowData(error.message, [...sensitiveTokens])) });
1495
1531
  } else {
1496
1532
  response.end();
1497
1533
  }
@@ -1518,6 +1554,12 @@ ${token}`;
1518
1554
  url: `${origin}/#session=${capability}`,
1519
1555
  project,
1520
1556
  controller,
1557
+ setHostConnection: async (value) => {
1558
+ hostConnection = value;
1559
+ hostManaged = true;
1560
+ signedOut = !value;
1561
+ await refreshAuth(true);
1562
+ },
1521
1563
  close: () => closing ??= (async () => {
1522
1564
  clearInterval(authPoll);
1523
1565
  if (broadcastTimer) {
@@ -1537,9 +1579,43 @@ ${token}`;
1537
1579
  };
1538
1580
  }
1539
1581
 
1582
+ // src/web.ts
1583
+ import { createInterface } from "readline";
1584
+
1585
+ // src/web/host-config.ts
1586
+ import path3 from "path";
1587
+ function parseHostConnection(value) {
1588
+ if (value === null || value === void 0) {
1589
+ return;
1590
+ }
1591
+ const connection = value;
1592
+ if (typeof connection.serverUrl !== "string" || typeof connection.accessToken !== "string" || !connection.accessToken || connection.accessToken.length > 16384) {
1593
+ throw new Error("Invalid host login.");
1594
+ }
1595
+ const url = new URL(connection.serverUrl);
1596
+ if (!["http:", "https:"].includes(url.protocol) || url.username || url.password || url.search || url.hash) {
1597
+ throw new Error("Invalid host server URL.");
1598
+ }
1599
+ return { serverUrl: url.toString().replace(/\/+$/, ""), accessToken: connection.accessToken };
1600
+ }
1601
+ function parseHostConfiguration(value) {
1602
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
1603
+ throw new Error("Invalid web host configuration.");
1604
+ }
1605
+ const configuration = value;
1606
+ if (configuration.type !== "configure" || !["editor", "assistant"].includes(configuration.surface)) {
1607
+ throw new Error("Invalid web host configuration.");
1608
+ }
1609
+ const assistant = configuration.assistant;
1610
+ if (assistant && (typeof assistant.workdir !== "string" || !path3.isAbsolute(assistant.workdir) || !Array.isArray(assistant.roots) || !assistant.roots.length || assistant.roots.length > 100 || assistant.roots.some((root) => typeof root.uri !== "string" || typeof root.name !== "string" || typeof root.folderPath !== "string" || !path3.isAbsolute(root.folderPath)) || !assistant.roots.some((root) => root.folderPath === assistant.workdir))) {
1611
+ throw new Error("Invalid assistant workspace.");
1612
+ }
1613
+ return { surface: configuration.surface, assistant, connection: parseHostConnection(configuration.connection) };
1614
+ }
1615
+
1540
1616
  // src/web.ts
1541
1617
  function parseWebArgs(argv) {
1542
- const options = { projectDir: process.cwd(), port: 0, theme: "auto", open: true, json: false, help: false };
1618
+ const options = { projectDir: process.cwd(), port: 0, theme: "auto", dataDir: void 0, open: true, json: false, help: false, inspect: false, hostStdio: false };
1543
1619
  let projectSet = false;
1544
1620
  for (let index = 0; index < argv.length; index++) {
1545
1621
  const arg = argv[index];
@@ -1547,6 +1623,10 @@ function parseWebArgs(argv) {
1547
1623
  options.open = false;
1548
1624
  } else if (arg === "--json") {
1549
1625
  options.json = true;
1626
+ } else if (arg === "--inspect") {
1627
+ options.inspect = true;
1628
+ } else if (arg === "--host-stdio") {
1629
+ options.hostStdio = true;
1550
1630
  } else if (["--help", "-h"].includes(arg)) {
1551
1631
  options.help = true;
1552
1632
  } else if (["--port", "--data-dir", "--theme"].includes(arg)) {
@@ -1565,12 +1645,12 @@ function parseWebArgs(argv) {
1565
1645
  }
1566
1646
  options.theme = value;
1567
1647
  } else {
1568
- options.dataDir = path3.resolve(value);
1648
+ options.dataDir = path4.resolve(value);
1569
1649
  }
1570
1650
  } else if (arg.startsWith("-") || projectSet) {
1571
1651
  throw new Error(`Unknown web argument: ${arg}`);
1572
1652
  } else {
1573
- options.projectDir = path3.resolve(arg);
1653
+ options.projectDir = path4.resolve(arg);
1574
1654
  projectSet = true;
1575
1655
  }
1576
1656
  }
@@ -1582,10 +1662,39 @@ async function main(argv) {
1582
1662
  console.log(i18n.language.startsWith("zh") ? "workflow-code web [\u9879\u76EE\u76EE\u5F55]\n --port <0-65535> \u76D1\u542C\u7AEF\u53E3\uFF0C0 \u81EA\u52A8\u5206\u914D\n --no-open \u4E0D\u81EA\u52A8\u6253\u5F00\u6D4F\u89C8\u5668\n --theme <auto|light|dark>\n --data-dir <\u76EE\u5F55> \u5BF9\u8BDD\u548C\u6587\u4EF6\u5B58\u50A8\u4F4D\u7F6E\n --json \u8F93\u51FA\u7ED3\u6784\u5316\u542F\u52A8\u4FE1\u606F" : "workflow-code web [project-directory]\n --port <0-65535> Listen port; 0 selects an available port\n --no-open Do not open a browser\n --theme <auto|light|dark>\n --data-dir <path> Conversation and file storage directory\n --json Print structured startup information");
1583
1663
  return 0;
1584
1664
  }
1585
- const app = await startWebServer(options);
1665
+ if (options.inspect) {
1666
+ const runtime = createWebRuntime(options.projectDir, options.dataDir ?? "", () => void 0);
1667
+ try {
1668
+ console.log(JSON.stringify(await runtime.inspect([options.projectDir])));
1669
+ } finally {
1670
+ runtime.dispose();
1671
+ }
1672
+ return 0;
1673
+ }
1674
+ const input = options.hostStdio ? createInterface({ input: process.stdin, crlfDelay: Infinity }) : void 0;
1675
+ const lines = input?.[Symbol.asyncIterator]();
1676
+ let host;
1677
+ try {
1678
+ if (lines) {
1679
+ const initial = await lines.next();
1680
+ if (initial.done || initial.value.length > 256e3) {
1681
+ throw new Error("Web host configuration is missing or too large.");
1682
+ }
1683
+ host = parseHostConfiguration(JSON.parse(initial.value));
1684
+ }
1685
+ } catch (error) {
1686
+ input?.close();
1687
+ throw error;
1688
+ }
1689
+ const app = await startWebServer({ ...options, host }).catch((error) => {
1690
+ input?.close();
1691
+ throw error;
1692
+ });
1586
1693
  const close = () => {
1587
1694
  process.off("SIGINT", close);
1588
1695
  process.off("SIGTERM", close);
1696
+ input?.close();
1697
+ process.stdin.pause();
1589
1698
  void app.close().catch((error) => {
1590
1699
  console.error(error);
1591
1700
  process.exitCode = 1;
@@ -1593,7 +1702,31 @@ async function main(argv) {
1593
1702
  };
1594
1703
  process.once("SIGINT", close);
1595
1704
  process.once("SIGTERM", close);
1596
- console.log(options.json ? JSON.stringify({ type: "ready", protocolVersion: 1, url: app.url, project: { id: app.project.id, name: app.project.name, projectType: app.project.projectType }, runtime: app.project.runtime }) : `Workflow Web: ${app.url}`);
1705
+ console.log(options.json ? JSON.stringify({ type: "ready", protocolVersion: 1, hostProtocolVersion: 1, url: app.url, project: { id: app.project.id, name: app.project.name, projectType: app.project.projectType }, runtime: app.project.runtime }) : `Workflow Web: ${app.url}`);
1706
+ if (lines) {
1707
+ void (async () => {
1708
+ try {
1709
+ for await (const line of { [Symbol.asyncIterator]: () => lines }) {
1710
+ if (line.length > 256e3) {
1711
+ throw new Error("Web host message is too large.");
1712
+ }
1713
+ const message = JSON.parse(line);
1714
+ if (message.type === "auth") {
1715
+ await app.setHostConnection(parseHostConnection(message.connection));
1716
+ } else if (message.type === "close") {
1717
+ break;
1718
+ } else {
1719
+ throw new Error("Unknown web host message.");
1720
+ }
1721
+ }
1722
+ } catch {
1723
+ console.error("Workflow web host connection failed.");
1724
+ process.exitCode = 1;
1725
+ } finally {
1726
+ close();
1727
+ }
1728
+ })();
1729
+ }
1597
1730
  if (options.open) {
1598
1731
  const invocation = process.platform === "darwin" ? { command: "open", args: [app.url] } : process.platform === "win32" ? { command: "rundll32.exe", args: ["url.dll,FileProtocolHandler", app.url] } : { command: "xdg-open", args: [app.url] };
1599
1732
  const browser = spawn3(invocation.command, invocation.args, { detached: true, shell: false, stdio: "ignore" });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@workflow-code/cli",
3
- "version": "0.2.4-20261010002.codex-cli-web",
3
+ "version": "0.2.4-20261010004.codex-cli-web",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "workflow-code": "./dist/index.js"
@@ -37,8 +37,8 @@
37
37
  "workflow-code": "0.2.4-20260929001",
38
38
  "@workflow-code/auth-config": "0.1.0",
39
39
  "@workflow-code/execution-targets": "0.1.0",
40
- "@workflow-code/i18n": "0.1.0",
41
40
  "@workflow-code/openai-responses": "0.1.0",
41
+ "@workflow-code/i18n": "0.1.0",
42
42
  "@workflow-code/project-dependencies": "0.1.0",
43
43
  "@workflow-code/project-info": "0.1.0",
44
44
  "@workflow-code/project-upload": "0.1.0",