@workflow-code/cli 0.2.4-20261010003.codex-cli-web → 0.2.4-20261011001.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-7QLJGSAQ.js");
27
+ const webCli = await import("./web-TJ2SME4G.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 });
@@ -4182,13 +4183,15 @@ ${state.user?.avatarUrl ?? ""}`;
4182
4183
  try {
4183
4184
  if (this.token) {
4184
4185
  const session2 = await fetch("/api/session", { method: "POST", headers: { Authorization: `Bearer ${this.token}` }, signal: AbortSignal.timeout(1e4) });
4185
- if (!session2.ok) {
4186
+ if (session2.status === 401) {
4186
4187
  this.update("expired");
4187
4188
  return;
4188
4189
  }
4189
- this.token = null;
4190
+ if (!session2.ok) {
4191
+ throw new Error("Cannot connect to the project.");
4192
+ }
4190
4193
  }
4191
- const session = await fetch("/api/session", { signal: AbortSignal.timeout(1e4) });
4194
+ const session = await fetch("/api/session", { headers: this.headers(), signal: AbortSignal.timeout(1e4) });
4192
4195
  if (session.status === 401) {
4193
4196
  this.update("expired");
4194
4197
  return;
@@ -4196,13 +4199,13 @@ ${state.user?.avatarUrl ?? ""}`;
4196
4199
  if (!session.ok) {
4197
4200
  throw new Error("Cannot connect to the project.");
4198
4201
  }
4199
- const source = this.source = new EventSource("/api/events");
4202
+ const source = this.source = new EventSource(this.fileUrl("/api/events"));
4200
4203
  source.onopen = () => {
4201
4204
  this.attempts = 0;
4202
4205
  this.update("connected");
4203
4206
  };
4204
4207
  source.onmessage = (event) => {
4205
- this.emit(JSON.parse(event.data));
4208
+ this.emit(this.publicFiles(JSON.parse(event.data)));
4206
4209
  };
4207
4210
  source.onerror = () => {
4208
4211
  if (this.source === source && !this.closed) {
@@ -4229,7 +4232,7 @@ ${state.user?.avatarUrl ?? ""}`;
4229
4232
  }
4230
4233
  let response;
4231
4234
  try {
4232
- response = await fetch("/api/message", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(message) });
4235
+ response = await fetch("/api/message", { method: "POST", headers: { ...this.headers(), "Content-Type": "application/json" }, body: JSON.stringify(message) });
4233
4236
  } catch {
4234
4237
  this.interrupted();
4235
4238
  throw new ConnectionError("\u8FDE\u63A5\u5DF2\u4E2D\u65AD\u3002");
@@ -4249,7 +4252,25 @@ ${state.user?.avatarUrl ?? ""}`;
4249
4252
  if (!response.ok) {
4250
4253
  throw new Error(reply.message || "Request failed.");
4251
4254
  }
4252
- return reply.messages ?? [];
4255
+ return (reply.messages ?? []).map((message2) => this.publicFiles(message2));
4256
+ }
4257
+ headers() {
4258
+ return this.token ? { Authorization: `Bearer ${this.token}` } : {};
4259
+ }
4260
+ fileUrl(path) {
4261
+ return this.token ? `${path}?session=${encodeURIComponent(this.token)}` : path;
4262
+ }
4263
+ publicFiles(value) {
4264
+ if (Array.isArray(value)) {
4265
+ return value.map((entry) => this.publicFiles(entry));
4266
+ }
4267
+ if (!value || typeof value !== "object") {
4268
+ return value;
4269
+ }
4270
+ return Object.fromEntries(Object.entries(value).map(([key, entry]) => [
4271
+ key,
4272
+ key === "previewUrl" && typeof entry === "string" && entry.startsWith("/api/file/") ? this.fileUrl(entry) : this.publicFiles(entry)
4273
+ ]));
4253
4274
  }
4254
4275
  };
4255
4276
 
@@ -4280,6 +4301,17 @@ ${state.user?.avatarUrl ?? ""}`;
4280
4301
  handleReply(entry);
4281
4302
  }
4282
4303
  };
4304
+ addEventListener("message", (event) => {
4305
+ if (window.parent === window || event.source !== window.parent || event.data?.type !== "workflow-host") {
4306
+ return;
4307
+ }
4308
+ if (event.data.theme === "dark" || event.data.theme === "light") {
4309
+ document.documentElement.dataset.theme = event.data.theme;
4310
+ }
4311
+ if (event.data.command === "new" || event.data.command === "history") {
4312
+ emit({ type: "assistant-command", command: event.data.command });
4313
+ }
4314
+ });
4283
4315
  const handleReply = (reply) => {
4284
4316
  if (reply.type === "download") {
4285
4317
  const url = URL.createObjectURL(new Blob([reply.content], { type: reply.mimeType ?? "application/json" }));
@@ -4364,7 +4396,7 @@ ${state.user?.avatarUrl ?? ""}`;
4364
4396
  return;
4365
4397
  }
4366
4398
  if (message.type === "open-file") {
4367
- window.open(`/api/file/${encodeURIComponent(message.id)}`, "_blank", "noopener");
4399
+ window.open(connection.fileUrl(`/api/file/${encodeURIComponent(message.id)}`), "_blank", "noopener");
4368
4400
  return;
4369
4401
  }
4370
4402
  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";
@@ -699,8 +699,29 @@ function createWebRuntime(projectDir, storageDir, connection) {
699
699
  return;
700
700
  }
701
701
  const auth = connection();
702
- const env = { ...process.env, WORKFLOW_RUNTIME_PLATFORM: "cli", WORKFLOW_SERVER_URL: auth?.serverUrl ?? "", WORKFLOW_SERVER_ADMIN_KEY: auth?.accessToken ?? "" };
703
- for (const key of ["TSX_TSCONFIG_PATH", "WORKFLOW_LOCAL_CORE_ROOT", "WORKFLOW_LOCAL_SHARED_ROOT", "WORKFLOW_LOCAL_SOURCE_MODE"]) {
702
+ if ((request.method === "run" || request.method === "resolve") && request.project?.requiresLogin && !auth) {
703
+ reject(new Error("Workflow Server login is not configured."));
704
+ return;
705
+ }
706
+ const env = {
707
+ ...process.env,
708
+ WORKFLOW_RUNTIME_PLATFORM: "cli",
709
+ WORKFLOW_SERVER_URL: auth?.serverUrl ?? "",
710
+ WORKFLOW_SERVER_ADMIN_KEY: auth?.accessToken ?? "",
711
+ WORKFLOW_SERVER_TOKEN: "",
712
+ WORKFLOW_LLM_SERVER_URL: auth?.serverUrl ?? "",
713
+ WORKFLOW_LLM_SERVER_TOKEN: auth?.accessToken ?? "",
714
+ WORKFLOW_CLI_AUTH_DIR: path.join(storageDir, "runtime-auth")
715
+ };
716
+ for (const key of [
717
+ "TSX_TSCONFIG_PATH",
718
+ "WORKFLOW_LOCAL_CORE_ROOT",
719
+ "WORKFLOW_LOCAL_SHARED_ROOT",
720
+ "WORKFLOW_LOCAL_SOURCE_MODE",
721
+ "WORKFLOW_DEVELOPER_ASSISTANT_HOST",
722
+ "WORKFLOW_DEVELOPER_ASSISTANT_WORKDIR",
723
+ "WORKFLOW_DEVELOPER_ASSISTANT_WORKSPACE_MANIFEST"
724
+ ]) {
704
725
  delete env[key];
705
726
  }
706
727
  if (env.NODE_OPTIONS) {
@@ -965,7 +986,7 @@ async function loginWebAccount(serverUrl, email, password) {
965
986
  // src/web/server.ts
966
987
  async function startWebServer(options) {
967
988
  const projectDir = await realpath(path2.resolve(options.projectDir));
968
- const key = createHash("sha256").update(projectDir).digest("hex").slice(0, 24);
989
+ const key = createHash("sha256").update(options.host?.assistant ? `assistant:${options.host.assistant.workdir}` : projectDir).digest("hex").slice(0, 24);
969
990
  const storageDir = path2.resolve(options.dataDir ?? path2.join(os.homedir(), ".workflow-code", "web"));
970
991
  const historyPath = path2.join(storageDir, key, "conversations.json");
971
992
  const clients = /* @__PURE__ */ new Set();
@@ -975,6 +996,11 @@ async function startWebServer(options) {
975
996
  let authIdentity = "";
976
997
  let login;
977
998
  let passwordLogin = false;
999
+ let hostConnection = options.host?.connection;
1000
+ let hostManaged = !!hostConnection;
1001
+ let authEpoch = 0;
1002
+ let authCheck;
1003
+ const sensitiveTokens = /* @__PURE__ */ new Set();
978
1004
  const runtime = options.runtime ?? createWebRuntime(projectDir, storageDir, () => connection);
979
1005
  let inspected;
980
1006
  try {
@@ -996,6 +1022,14 @@ async function startWebServer(options) {
996
1022
  throw new Error("workflow-code web currently supports Conversation and Workflow projects.");
997
1023
  }
998
1024
  const project = { ...descriptor, uri: pathToFileURL(projectDir).href };
1025
+ if (options.host?.assistant) {
1026
+ project.assistant = options.host.assistant;
1027
+ project.uri = `studio-assistant:${encodeURIComponent(project.assistant.workdir)}`;
1028
+ project.requiresLogin = true;
1029
+ project.fields = project.fields.map((field) => field.name === "permissionPolicy" ? { ...field, defaultValue: "workspace" } : field);
1030
+ const supported = /* @__PURE__ */ new Set(["read", "write", "edit", "bash", "grep", "find", "ls", "skill", "workflow-memory_global_memory", "workflow-ui_present_card"]);
1031
+ project.tools = project.tools?.filter((tool) => supported.has(tool.name));
1032
+ }
999
1033
  let saved = {};
1000
1034
  try {
1001
1035
  saved = JSON.parse(await readFile(historyPath, "utf8"));
@@ -1024,7 +1058,7 @@ async function startWebServer(options) {
1024
1058
  }
1025
1059
  return value;
1026
1060
  };
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 ?? ""]));
1061
+ 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
1062
  const broadcast = () => {
1029
1063
  if (broadcastTimer) {
1030
1064
  return;
@@ -1055,37 +1089,63 @@ async function startWebServer(options) {
1055
1089
  }
1056
1090
  });
1057
1091
  const refreshAuth = async (force = false) => {
1058
- if (passwordLogin || login) {
1092
+ if ((passwordLogin || login) && !force) {
1059
1093
  return;
1060
1094
  }
1095
+ const readingEpoch = authEpoch;
1061
1096
  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;
1097
+ if (readingEpoch !== authEpoch) {
1098
+ return;
1099
+ }
1100
+ const serverUrl = hostManaged ? hostConnection?.serverUrl : process.env.WORKFLOW_SERVER_URL ?? config.serverUrl;
1101
+ const token = hostManaged ? hostConnection?.accessToken : process.env.WORKFLOW_SERVER_ADMIN_KEY ?? config.apiKey;
1102
+ if (token) {
1103
+ sensitiveTokens.add(token);
1104
+ }
1064
1105
  const identity = signedOut ? "" : `${serverUrl}
1065
1106
  ${token}`;
1066
- if (!force && identity === authIdentity && account.status !== "checking") {
1107
+ const expired = account.expiresAt && Date.parse(account.expiresAt) <= Date.now();
1108
+ if (!force && identity === authIdentity && account.status !== "error" && !expired) {
1067
1109
  return;
1068
1110
  }
1111
+ const epoch = ++authEpoch;
1112
+ authCheck?.abort();
1113
+ const check = authCheck = new AbortController();
1069
1114
  const changed = identity !== authIdentity;
1070
1115
  authIdentity = identity;
1071
- connection = !signedOut && serverUrl && token ? { serverUrl: serverUrl.replace(/\/+$/, ""), accessToken: token } : void 0;
1116
+ const candidate = !signedOut && serverUrl && token ? { serverUrl: serverUrl.replace(/\/+$/, ""), accessToken: token } : void 0;
1072
1117
  if (changed) {
1118
+ connection = void 0;
1073
1119
  authRevision++;
1074
1120
  parameterStates.clear();
1075
1121
  for (const session of controller.sessions) {
1076
1122
  controller.stop(session);
1077
1123
  }
1078
1124
  }
1079
- account = { status: connection ? "checking" : "signed-out", serverUrl: serverUrl || "http://localhost:7126", user: connection ? config.user ?? account.user : void 0, revision: account.revision + 1 };
1080
- if (connection) {
1125
+ account = { status: candidate ? "checking" : "signed-out", serverUrl: serverUrl || "http://localhost:7126", user: candidate ? changed ? hostManaged ? void 0 : config.user : account.user : void 0, revision: account.revision + 1 };
1126
+ if (candidate) {
1127
+ let unauthorized = false;
1081
1128
  try {
1082
- const response = await fetch(`${connection.serverUrl}/api/auth/me`, { headers: { Authorization: `Bearer ${connection.accessToken}` }, signal: AbortSignal.timeout(1e4), redirect: "error" });
1129
+ const response = await fetch(`${candidate.serverUrl}/api/auth/me`, { headers: { Authorization: `Bearer ${candidate.accessToken}` }, signal: AbortSignal.any([check.signal, AbortSignal.timeout(1e4)]), redirect: "error" });
1130
+ unauthorized = response.status === 401;
1083
1131
  const result = await response.json();
1084
- if (!response.ok || result.errCode !== 0 || !result.data?.profile) {
1132
+ if (epoch !== authEpoch) {
1133
+ return;
1134
+ }
1135
+ const profile = result.data?.profile;
1136
+ if (!response.ok || result.errCode !== 0 || typeof profile?.userId !== "string" || !profile.userId || typeof profile.email !== "string" || typeof profile.displayName !== "string") {
1085
1137
  throw new Error("Workflow login is invalid or expired.");
1086
1138
  }
1087
- account = { ...account, status: "authenticated", user: result.data.profile, expiresAt: result.data.principal?.expiresAt ?? config.expiresAt };
1139
+ if (!connection) {
1140
+ authRevision++;
1141
+ parameterStates.clear();
1142
+ }
1143
+ connection = candidate;
1144
+ account = { ...account, status: "authenticated", user: profile, expiresAt: result.data.principal?.expiresAt ?? config.expiresAt };
1088
1145
  } catch (error) {
1146
+ if (epoch !== authEpoch) {
1147
+ return;
1148
+ }
1089
1149
  connection = void 0;
1090
1150
  if (!changed) {
1091
1151
  authRevision++;
@@ -1094,8 +1154,10 @@ ${token}`;
1094
1154
  controller.stop(session);
1095
1155
  }
1096
1156
  }
1097
- account = { ...account, status: "error", error: String(error.message) };
1157
+ account = { ...account, status: unauthorized ? "expired" : "error", error: String(error.message) };
1098
1158
  }
1159
+ } else {
1160
+ connection = void 0;
1099
1161
  }
1100
1162
  broadcast();
1101
1163
  };
@@ -1341,7 +1403,7 @@ ${token}`;
1341
1403
  break;
1342
1404
  }
1343
1405
  case "login": {
1344
- if (process.env.WORKFLOW_SERVER_ADMIN_KEY !== void 0) {
1406
+ if (!options.host && process.env.WORKFLOW_SERVER_ADMIN_KEY !== void 0) {
1345
1407
  throw new Error("This session is supplied by the host. Sign in through the host.");
1346
1408
  }
1347
1409
  if (login || passwordLogin) {
@@ -1355,15 +1417,23 @@ ${token}`;
1355
1417
  account = { ...account, status: "checking" };
1356
1418
  broadcast();
1357
1419
  if (message.email !== void 0 || message.password !== void 0) {
1420
+ const epoch = ++authEpoch;
1358
1421
  passwordLogin = true;
1359
1422
  try {
1360
1423
  if (typeof message.email !== "string" || typeof message.password !== "string") {
1361
1424
  throw new Error("Invalid login credentials.");
1362
1425
  }
1363
1426
  await loginWebAccount(url.toString().replace(/\/+$/, ""), message.email, message.password);
1427
+ if (epoch !== authEpoch) {
1428
+ break;
1429
+ }
1430
+ hostManaged = false;
1431
+ hostConnection = void 0;
1364
1432
  } catch (error) {
1365
- account = { ...account, status: "error", error: String(error.message) };
1366
- broadcast();
1433
+ if (epoch === authEpoch) {
1434
+ account = { ...account, status: "error", error: String(error.message) };
1435
+ broadcast();
1436
+ }
1367
1437
  throw error;
1368
1438
  } finally {
1369
1439
  passwordLogin = false;
@@ -1391,7 +1461,7 @@ ${token}`;
1391
1461
  case "logout":
1392
1462
  signedOut = true;
1393
1463
  login?.kill();
1394
- if (process.env.WORKFLOW_SERVER_ADMIN_KEY === void 0) {
1464
+ if (!hostManaged && process.env.WORKFLOW_SERVER_ADMIN_KEY === void 0) {
1395
1465
  await clearCliAuthConfig();
1396
1466
  }
1397
1467
  authIdentity = "signed-out";
@@ -1407,7 +1477,9 @@ ${token}`;
1407
1477
  void (async () => {
1408
1478
  response.setHeader("X-Content-Type-Options", "nosniff");
1409
1479
  response.setHeader("Referrer-Policy", "no-referrer");
1410
- if (request.headers.host !== new URL(origin).host || request.headers.origin && request.headers.origin !== origin || request.headers["sec-fetch-site"] === "cross-site") {
1480
+ const host = new URL(`http://${request.headers.host ?? ""}`);
1481
+ const forwarded = !!options.host && ["127.0.0.1", "localhost", "[::1]"].includes(host.hostname) && !host.username && !host.password && host.pathname === "/" && !host.search && !host.hash;
1482
+ if (request.headers.host !== new URL(origin).host && !forwarded || request.headers.origin && request.headers.origin !== (forwarded ? host.origin : origin)) {
1411
1483
  json(response, 403, { message: "Invalid request origin." });
1412
1484
  return;
1413
1485
  }
@@ -1422,8 +1494,14 @@ ${token}`;
1422
1494
  return;
1423
1495
  }
1424
1496
  if (url.pathname.startsWith("/api/")) {
1497
+ if (request.headers["sec-fetch-site"] === "cross-site") {
1498
+ json(response, 403, { message: "Invalid request origin." });
1499
+ return;
1500
+ }
1425
1501
  const value = String(request.headers.cookie ?? "").split(";").map((value2) => value2.trim()).find((value2) => value2.startsWith(`${cookie}=`))?.slice(cookie.length + 1) ?? "";
1426
- if (!authMatches(value)) {
1502
+ const bearer = String(request.headers.authorization ?? "").replace(/^Bearer /, "");
1503
+ const readSession = request.method === "GET" && (url.pathname === "/api/events" || url.pathname.startsWith("/api/file/")) ? url.searchParams.get("session") ?? "" : "";
1504
+ if (!authMatches(value) && !authMatches(bearer) && !authMatches(readSession)) {
1427
1505
  json(response, 401, { message: "Open the URL printed by workflow-code web." });
1428
1506
  return;
1429
1507
  }
@@ -1455,7 +1533,7 @@ ${token}`;
1455
1533
  mutations = task;
1456
1534
  }
1457
1535
  await task;
1458
- json(response, 200, { messages: redactWorkflowData(messages, [connection?.accessToken ?? ""]) });
1536
+ json(response, 200, { messages: redactWorkflowData(messages, [...sensitiveTokens]) });
1459
1537
  return;
1460
1538
  }
1461
1539
  if (url.pathname.startsWith("/api/file/") && request.method === "GET") {
@@ -1479,8 +1557,9 @@ ${token}`;
1479
1557
  return;
1480
1558
  }
1481
1559
  if (url.pathname === "/") {
1482
- 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'" });
1483
- 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>`);
1560
+ 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:";
1561
+ 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'` });
1562
+ 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>`);
1484
1563
  return;
1485
1564
  }
1486
1565
  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" };
@@ -1493,7 +1572,7 @@ ${token}`;
1493
1572
  response.end(content);
1494
1573
  })().catch((error) => {
1495
1574
  if (!response.headersSent) {
1496
- json(response, 400, { message: String(redactWorkflowData(error.message, [connection?.accessToken ?? ""])) });
1575
+ json(response, 400, { message: String(redactWorkflowData(error.message, [...sensitiveTokens])) });
1497
1576
  } else {
1498
1577
  response.end();
1499
1578
  }
@@ -1520,7 +1599,17 @@ ${token}`;
1520
1599
  url: `${origin}/#session=${capability}`,
1521
1600
  project,
1522
1601
  controller,
1602
+ setHostConnection: async (value) => {
1603
+ authEpoch++;
1604
+ authCheck?.abort();
1605
+ hostConnection = value;
1606
+ hostManaged = true;
1607
+ signedOut = !value;
1608
+ await refreshAuth(true);
1609
+ },
1523
1610
  close: () => closing ??= (async () => {
1611
+ authEpoch++;
1612
+ authCheck?.abort();
1524
1613
  clearInterval(authPoll);
1525
1614
  if (broadcastTimer) {
1526
1615
  clearTimeout(broadcastTimer);
@@ -1539,9 +1628,43 @@ ${token}`;
1539
1628
  };
1540
1629
  }
1541
1630
 
1631
+ // src/web.ts
1632
+ import { createInterface } from "readline";
1633
+
1634
+ // src/web/host-config.ts
1635
+ import path3 from "path";
1636
+ function parseHostConnection(value) {
1637
+ if (value === null || value === void 0) {
1638
+ return;
1639
+ }
1640
+ const connection = value;
1641
+ if (typeof connection.serverUrl !== "string" || typeof connection.accessToken !== "string" || !connection.accessToken || connection.accessToken.length > 16384) {
1642
+ throw new Error("Invalid host login.");
1643
+ }
1644
+ const url = new URL(connection.serverUrl);
1645
+ if (!["http:", "https:"].includes(url.protocol) || url.username || url.password || url.search || url.hash) {
1646
+ throw new Error("Invalid host server URL.");
1647
+ }
1648
+ return { serverUrl: url.toString().replace(/\/+$/, ""), accessToken: connection.accessToken };
1649
+ }
1650
+ function parseHostConfiguration(value) {
1651
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
1652
+ throw new Error("Invalid web host configuration.");
1653
+ }
1654
+ const configuration = value;
1655
+ if (configuration.type !== "configure" || !["editor", "assistant"].includes(configuration.surface)) {
1656
+ throw new Error("Invalid web host configuration.");
1657
+ }
1658
+ const assistant = configuration.assistant;
1659
+ 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))) {
1660
+ throw new Error("Invalid assistant workspace.");
1661
+ }
1662
+ return { surface: configuration.surface, assistant, connection: parseHostConnection(configuration.connection) };
1663
+ }
1664
+
1542
1665
  // src/web.ts
1543
1666
  function parseWebArgs(argv) {
1544
- const options = { projectDir: process.cwd(), port: 0, theme: "auto", open: true, json: false, help: false };
1667
+ const options = { projectDir: process.cwd(), port: 0, theme: "auto", dataDir: void 0, open: true, json: false, help: false, inspect: false, hostStdio: false };
1545
1668
  let projectSet = false;
1546
1669
  for (let index = 0; index < argv.length; index++) {
1547
1670
  const arg = argv[index];
@@ -1549,6 +1672,10 @@ function parseWebArgs(argv) {
1549
1672
  options.open = false;
1550
1673
  } else if (arg === "--json") {
1551
1674
  options.json = true;
1675
+ } else if (arg === "--inspect") {
1676
+ options.inspect = true;
1677
+ } else if (arg === "--host-stdio") {
1678
+ options.hostStdio = true;
1552
1679
  } else if (["--help", "-h"].includes(arg)) {
1553
1680
  options.help = true;
1554
1681
  } else if (["--port", "--data-dir", "--theme"].includes(arg)) {
@@ -1567,12 +1694,12 @@ function parseWebArgs(argv) {
1567
1694
  }
1568
1695
  options.theme = value;
1569
1696
  } else {
1570
- options.dataDir = path3.resolve(value);
1697
+ options.dataDir = path4.resolve(value);
1571
1698
  }
1572
1699
  } else if (arg.startsWith("-") || projectSet) {
1573
1700
  throw new Error(`Unknown web argument: ${arg}`);
1574
1701
  } else {
1575
- options.projectDir = path3.resolve(arg);
1702
+ options.projectDir = path4.resolve(arg);
1576
1703
  projectSet = true;
1577
1704
  }
1578
1705
  }
@@ -1584,10 +1711,39 @@ async function main(argv) {
1584
1711
  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");
1585
1712
  return 0;
1586
1713
  }
1587
- const app = await startWebServer(options);
1714
+ if (options.inspect) {
1715
+ const runtime = createWebRuntime(options.projectDir, options.dataDir ?? "", () => void 0);
1716
+ try {
1717
+ console.log(JSON.stringify(await runtime.inspect([options.projectDir])));
1718
+ } finally {
1719
+ runtime.dispose();
1720
+ }
1721
+ return 0;
1722
+ }
1723
+ const input = options.hostStdio ? createInterface({ input: process.stdin, crlfDelay: Infinity }) : void 0;
1724
+ const lines = input?.[Symbol.asyncIterator]();
1725
+ let host;
1726
+ try {
1727
+ if (lines) {
1728
+ const initial = await lines.next();
1729
+ if (initial.done || initial.value.length > 256e3) {
1730
+ throw new Error("Web host configuration is missing or too large.");
1731
+ }
1732
+ host = parseHostConfiguration(JSON.parse(initial.value));
1733
+ }
1734
+ } catch (error) {
1735
+ input?.close();
1736
+ throw error;
1737
+ }
1738
+ const app = await startWebServer({ ...options, host }).catch((error) => {
1739
+ input?.close();
1740
+ throw error;
1741
+ });
1588
1742
  const close = () => {
1589
1743
  process.off("SIGINT", close);
1590
1744
  process.off("SIGTERM", close);
1745
+ input?.close();
1746
+ process.stdin.pause();
1591
1747
  void app.close().catch((error) => {
1592
1748
  console.error(error);
1593
1749
  process.exitCode = 1;
@@ -1595,7 +1751,35 @@ async function main(argv) {
1595
1751
  };
1596
1752
  process.once("SIGINT", close);
1597
1753
  process.once("SIGTERM", close);
1598
- 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}`);
1754
+ 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}`);
1755
+ if (lines) {
1756
+ void (async () => {
1757
+ try {
1758
+ for await (const line of { [Symbol.asyncIterator]: () => lines }) {
1759
+ if (line.length > 256e3) {
1760
+ throw new Error("Web host message is too large.");
1761
+ }
1762
+ const message = JSON.parse(line);
1763
+ if (message.type === "auth") {
1764
+ void app.setHostConnection(parseHostConnection(message.connection)).catch(() => {
1765
+ console.error("Workflow web host connection failed.");
1766
+ process.exitCode = 1;
1767
+ close();
1768
+ });
1769
+ } else if (message.type === "close") {
1770
+ break;
1771
+ } else {
1772
+ throw new Error("Unknown web host message.");
1773
+ }
1774
+ }
1775
+ } catch {
1776
+ console.error("Workflow web host connection failed.");
1777
+ process.exitCode = 1;
1778
+ } finally {
1779
+ close();
1780
+ }
1781
+ })();
1782
+ }
1599
1783
  if (options.open) {
1600
1784
  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] };
1601
1785
  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-20261010003.codex-cli-web",
3
+ "version": "0.2.4-20261011001.codex-cli-web",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "workflow-code": "./dist/index.js"
@@ -39,12 +39,12 @@
39
39
  "@workflow-code/execution-targets": "0.1.0",
40
40
  "@workflow-code/i18n": "0.1.0",
41
41
  "@workflow-code/openai-responses": "0.1.0",
42
- "@workflow-code/project-dependencies": "0.1.0",
43
42
  "@workflow-code/project-info": "0.1.0",
43
+ "@workflow-code/project-dependencies": "0.1.0",
44
44
  "@workflow-code/project-upload": "0.1.0",
45
- "@workflow-code/runtime-compatibility": "0.1.0",
46
45
  "@workflow-code/workflow-ids": "0.1.0",
47
- "@workflow-code/workflow-mcp": "0.1.0"
46
+ "@workflow-code/workflow-mcp": "0.1.0",
47
+ "@workflow-code/runtime-compatibility": "0.1.0"
48
48
  },
49
49
  "scripts": {
50
50
  "bootstrap": "node scripts/bootstrap.mjs",