@skylandnpm/octopus-proxy 0.1.63 → 0.1.65

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -60,7 +60,7 @@ octopus-proxy restart
60
60
 
61
61
  Claw 完成主机信任和账号认证后,通过端到端会话向 Proxy 发送只读 `runtime.snapshot` 命令。响应包含当前运行状态、Hub 和工具设置、授权项目路径,以及工具可提供的模型信息;机器人 Secret、工具密码和提供商凭据不会返回。工作区的“Proxy 信息”按钮可查看这些内容。
62
62
 
63
- Codex 和 ZCode 由 Proxy 按授权项目启动各自的 app-server,默认随 Proxy 自动启动;服务未就绪时也可在 Claw 点击“启动服务”。若命令不可执行或某个项目启动失败,工作区会显示失败项目及原因。修改命令路径或授权项目后,运行 `octopus-proxy restart` 使配置生效。
63
+ Codex 和 ZCode 由 Proxy 按授权项目启动各自的 app-server,默认随 Proxy 自动启动;服务未就绪时也可在 Claw 点击“启动服务”。若命令不可执行或某个项目启动失败,工作区会显示失败项目及原因。通过 `onboard` 或配置文件修改授权项目后,Claw 打开、切换或刷新工作区项目时会读取最新项目配置;目录变化会释放该项目的旧运行资源和终端,并刷新文件面板。Proxy 不轮询配置文件,也不因主机配置变化主动推送刷新通知。Claw 也可在授权项目菜单中点击“修改项目”,修改“Proxy 主机上的目录”并保存;Proxy 校验目录存在且未重复授权后写入主机配置,Claw 随即刷新工作区,无需重启 Proxy。修改工具命令、机器人凭据等其他设置后,仍需运行 `octopus-proxy restart`。
64
64
 
65
65
  ## Claw 设备配对与撤销
66
66
 
@@ -123,6 +123,30 @@ octopus-proxy update
123
123
 
124
124
  安装或版本校验失败时不会触发服务重启;包安装失败不保证旧版本文件完整保留。已安装新版本但重启失败时会单独提示,可运行 `octopus-proxy doctor` 和 `octopus-proxy logs` 排查。此命令用于 npm 全局安装,覆盖安装前会检查 npm 安装目录是否与当前命令一致;源码、本地安装或 Node.js 环境不一致时会中止并给出提示。
125
125
 
126
+ ## Linux 服务在线,但终端显示“未运行、未启用”
127
+
128
+ 后台服务创建的终端可能没有登录会话中的 `XDG_RUNTIME_DIR`。此时 `systemctl --user` 会报 `Failed to connect to bus: No medium found`,不能据此判断服务已停止。CLI 会在未显式配置用户会话环境时,检查 `/run/user/<当前 UID>` 是否存在且属于当前用户,并用于服务查询和控制;查询失败会报告错误。此检查也支持只有 `systemd/private`、没有会话 `bus` socket 的环境。
129
+
130
+ 旧版可先验证:
131
+
132
+ ```sh
133
+ XDG_RUNTIME_DIR=/run/user/$(id -u) octopus-proxy status
134
+ ```
135
+
136
+ 如果显示正常,可在该服务账号的 `~/.bashrc` 中补上以下设置,使 Claw 新建的 Bash 终端也能查询用户服务:
137
+
138
+ ```sh
139
+ if [ -z "${XDG_RUNTIME_DIR:-}" ] && [ -z "${DBUS_SESSION_BUS_ADDRESS:-}" ]; then
140
+ octopus_runtime_dir="/run/user/$(id -u)"
141
+ if [ -d "$octopus_runtime_dir" ] && [ -O "$octopus_runtime_dir" ]; then
142
+ export XDG_RUNTIME_DIR="$octopus_runtime_dir"
143
+ fi
144
+ unset octopus_runtime_dir
145
+ fi
146
+ ```
147
+
148
+ 重新打开终端或运行 `source ~/.bashrc` 后验证 `octopus-proxy status`。如果对应用户运行目录或用户服务管理器不存在,仍需检查主机的登录会话和 linger 配置。
149
+
126
150
  ## 服务启动时报 `Unexpected token '??='`
127
151
 
128
152
  这通常表示服务实际使用了旧版 Node.js。错误栈中 `.nvm/versions/node/v22.../lib/node_modules` 是包的安装位置,不能说明运行它的 Node.js 版本。安装器只会选择 Node.js 22 或更高版本;旧版安装器可能优先选中能执行 `--version` 的旧系统 Node。
@@ -8334,6 +8334,7 @@ var Operations = [
8334
8334
  "configuration.get",
8335
8335
  "configuration.patch",
8336
8336
  "configuration.models",
8337
+ "configuration.permissionModes",
8337
8338
  "configuration.agents",
8338
8339
  "configuration.commands",
8339
8340
  "configuration.providers",
@@ -8373,7 +8374,7 @@ var CommandSchema = z.object({
8373
8374
  sessionId: z.string().optional(),
8374
8375
  payload: z.record(z.unknown()).default({})
8375
8376
  }).strict();
8376
- var isReadOperation = (operation) => operation === "desktop.capture" || /\.(status|snapshot|list|get|messages|diff|todos|children|logs|projects|read|search|history|models|agents|commands|providers|mcp)$/.test(operation);
8377
+ var isReadOperation = (operation) => operation === "desktop.capture" || /\.(status|snapshot|list|get|messages|diff|todos|children|logs|projects|read|search|history|models|permissionModes|agents|commands|providers|mcp)$/.test(operation);
8377
8378
 
8378
8379
  // src/journal.ts
8379
8380
  var Journal = class {
@@ -11031,6 +11032,10 @@ var OpenCodeAdapter = class {
11031
11032
  await this.session(project, id);
11032
11033
  }
11033
11034
  const base = `/api/session/${encodeURIComponent(id ?? "")}`;
11035
+ if (op === "configuration.permissionModes") return { defaultMode: "ask", modes: [
11036
+ { id: "ask", label: "\u8BF7\u6C42\u6279\u51C6", description: "\u6BCF\u6B21\u4F7F\u7528\u5DE5\u5177\u524D\u8BF7\u6C42\u6279\u51C6" },
11037
+ { id: "full", label: "\u5B8C\u5168\u8BBF\u95EE", description: "\u81EA\u52A8\u5141\u8BB8\u5DE5\u5177\u64CD\u4F5C\uFF1B\u4ECD\u9075\u5B88\u4E3B\u673A\u548C\u5DE5\u5177\u81EA\u8EAB\u7684\u8BBF\u95EE\u9650\u5236" }
11038
+ ] };
11034
11039
  if (op === "sessions.prompt") return this.prompt(project, id, p, base);
11035
11040
  if (op === "sessions.command" || op === "sessions.shell") {
11036
11041
  await this.switchSelection(project, base, p);
@@ -11098,6 +11103,13 @@ var OpenCodeAdapter = class {
11098
11103
  return this.data(project, "POST", `${base}/prompt`, { text, files, delivery: "queue", ...p.messageID ? { id: p.messageID } : {} });
11099
11104
  }
11100
11105
  async switchSelection(project, base, payload) {
11106
+ if (payload.permissionMode !== void 0) {
11107
+ if (!["ask", "full"].includes(String(payload.permissionMode))) throw new Error("Unsupported permission mode");
11108
+ const states = await this.data(project, "GET", "/api/session/active");
11109
+ const state = states?.[decodeURIComponent(base.split("/").at(-1))];
11110
+ if (state && !["idle", "completed", "failed", "interrupted"].includes(state.type)) throw new Error("Wait for the active task before changing permission mode");
11111
+ await this.data(project, "PATCH", base, { permissions: [{ action: "*", resource: "*", effect: payload.permissionMode === "full" ? "allow" : "ask" }] });
11112
+ }
11101
11113
  if (payload.agent) await this.data(project, "POST", `${base}/agent`, { agent: payload.agent });
11102
11114
  if (!payload.model) return;
11103
11115
  let model;
@@ -11604,7 +11616,13 @@ function standaloneZcodeSource(source) {
11604
11616
  throw new Error("Unsupported ZCode account bootstrap; native compatibility check failed");
11605
11617
  }
11606
11618
  body = body.replace(registryCall, `${registry}($1,{standalone:{}})`).replace(nativePorts, "$&providerRuntimeHeadersPort:$1.providerRuntimeHeadersPort,");
11607
- return source.slice(0, start) + body + source.slice(end);
11619
+ source = source.slice(0, start) + body + source.slice(end);
11620
+ const close = /\br\(([$\w]+),"closeSession"\)/.exec(source)?.[1];
11621
+ if (!close) throw new Error("Unsupported ZCode bundle: session close is unavailable");
11622
+ const escapedClose = close.replace(/[$]/g, "\\$&");
11623
+ const closeBody = new RegExp(`async function ${escapedClose}\\(([$\\w]+),([$\\w]+)\\)\\{[^}]*?await ([$\\w]+)\\.eventStore\\.deleteSession\\(([$\\w]+)\\.sessionId\\),\\{closed:!0\\}`, "g");
11624
+ if ([...source.matchAll(closeBody)].length !== 1) throw new Error("Unsupported ZCode session close; native compatibility check failed");
11625
+ return source.replace(closeBody, (match, context, _params, record2, parsed) => match.replace(",{closed:!0}", `,${record2}.persistence!=="deferred"&&await ${context}.deps.sessionStore.updateSession({id:${parsed}.sessionId,timeArchived:Date.now()}),{closed:!0}`));
11608
11626
  }
11609
11627
  function zcodeServerLaunch(command, args) {
11610
11628
  if (basename4(command) !== "zcode.cjs") return { command, args: [...args] };
@@ -12129,6 +12147,7 @@ var OPERATIONS = [
12129
12147
  "sessions.abort",
12130
12148
  "sessions.delete",
12131
12149
  "configuration.models",
12150
+ "configuration.permissionModes",
12132
12151
  "permissions.list",
12133
12152
  "permissions.reply",
12134
12153
  "questions.list",
@@ -12403,6 +12422,31 @@ var CodexAdapter = class {
12403
12422
  }
12404
12423
  return { decision: accept ? reply === "always" ? "acceptForSession" : "accept" : "decline" };
12405
12424
  }
12425
+ async permissionModes(project, rpc) {
12426
+ const result = await rpc.request("config/read", { cwd: await this.workspace.root(project), includeLayers: false }).catch(() => void 0);
12427
+ const autoReview = !!result?.config && Object.hasOwn(result.config, "approvals_reviewer");
12428
+ return {
12429
+ defaultMode: "ask",
12430
+ modes: [
12431
+ { id: "ask", label: "\u8BF7\u6C42\u6279\u51C6", description: "\u7F16\u8F91\u9879\u76EE\u5916\u7684\u6587\u4EF6\u548C\u4F7F\u7528\u4E92\u8054\u7F51\u65F6\u8BF7\u6C42\u6279\u51C6" },
12432
+ ...autoReview ? [{ id: "auto", label: "\u5E2E\u6211\u6279\u51C6", description: "\u7531 Codex \u81EA\u52A8\u5BA1\u6838\u64CD\u4F5C\u98CE\u9669\uFF0C\u9700\u8981\u4F60\u786E\u8BA4\u65F6\u518D\u8BF7\u6C42\u6279\u51C6" }] : [],
12433
+ { id: "full", label: "\u5B8C\u5168\u8BBF\u95EE", description: "\u5141\u8BB8\u8BBF\u95EE\u4E92\u8054\u7F51\u548C Proxy \u4E3B\u673A\u4E0A\u7684\u4EFB\u4F55\u6587\u4EF6\uFF0C\u65E0\u9700\u64CD\u4F5C\u6279\u51C6" }
12434
+ ],
12435
+ autoReview
12436
+ };
12437
+ }
12438
+ async permissionOverrides(project, rpc, mode) {
12439
+ if (mode === void 0) return {};
12440
+ const options = await this.permissionModes(project, rpc);
12441
+ if (!options.modes.some((entry) => entry.id === mode)) throw new Error("Unsupported permission mode");
12442
+ const reviewer = options.autoReview ? { approvalsReviewer: mode === "auto" ? "guardian_subagent" : "user" } : {};
12443
+ if (mode === "full") return { approvalPolicy: "never", sandboxPolicy: { type: "dangerFullAccess" }, ...reviewer };
12444
+ return {
12445
+ approvalPolicy: "on-request",
12446
+ sandboxPolicy: { type: "workspaceWrite", writableRoots: [await this.workspace.root(project)], networkAccess: false },
12447
+ ...reviewer
12448
+ };
12449
+ }
12406
12450
  async session(project, id) {
12407
12451
  if (!describeTool("codex").sessionPattern.test(id)) throw new Error("Invalid session id");
12408
12452
  const rpc = await this.rpc(project);
@@ -12431,6 +12475,10 @@ var CodexAdapter = class {
12431
12475
  if (op === "auth.poll") return this.auth.poll(project, p);
12432
12476
  if (op === "auth.cancel") return this.auth.cancel(project, p.flowId);
12433
12477
  const rpc = await this.rpc(project);
12478
+ if (op === "configuration.permissionModes") {
12479
+ const { autoReview: _, ...options } = await this.permissionModes(project, rpc);
12480
+ return options;
12481
+ }
12434
12482
  if (op === "auth.status") {
12435
12483
  const result = await rpc.request("account/read", { refreshToken: false });
12436
12484
  return {
@@ -12530,6 +12578,7 @@ var CodexAdapter = class {
12530
12578
  if (op === "sessions.prompt") {
12531
12579
  if (!id) throw new Error("Session required");
12532
12580
  await this.session(project, id);
12581
+ const permissions = await this.permissionOverrides(project, rpc, p.permissionMode);
12533
12582
  await this.resume(project, id, rpc);
12534
12583
  const input = [];
12535
12584
  const lines = [];
@@ -12544,7 +12593,7 @@ var CodexAdapter = class {
12544
12593
  const selectedModel = p.model;
12545
12594
  const model = selectedModel?.providerID === "codex" && typeof selectedModel.modelID === "string" ? selectedModel.modelID : void 0;
12546
12595
  const effort = selectedReasoningLevel(p, "effort");
12547
- const result = await rpc.request("turn/start", { threadId: id, input, ...model ? { model } : {}, ...effort ? { effort } : {} });
12596
+ const result = await rpc.request("turn/start", { threadId: id, input, ...model ? { model } : {}, ...effort ? { effort } : {}, ...permissions });
12548
12597
  this.activeTurns.set(id, result.turn.id);
12549
12598
  return result;
12550
12599
  }
@@ -12619,6 +12668,7 @@ var OPERATIONS2 = [
12619
12668
  ...DAEMON_OPERATIONS,
12620
12669
  ...AUTH_OPERATIONS,
12621
12670
  "configuration.models",
12671
+ "configuration.permissionModes",
12622
12672
  "sessions.list",
12623
12673
  "sessions.create",
12624
12674
  "sessions.get",
@@ -12811,6 +12861,12 @@ var ZcodeAdapter = class {
12811
12861
  if (op === "auth.poll") return this.auth.poll(project, p);
12812
12862
  if (op === "auth.cancel") return this.auth.cancel(project, p.flowId);
12813
12863
  const rpc = await this.rpc(project);
12864
+ if (op === "configuration.permissionModes") return { defaultMode: "ask", modes: [
12865
+ { id: "ask", label: "\u8BF7\u6C42\u6279\u51C6", description: "\u4F7F\u7528 ZCode build \u6A21\u5F0F\uFF0C\u4FEE\u6539\u6587\u4EF6\u524D\u8BF7\u6C42\u6279\u51C6" },
12866
+ { id: "edit", label: "\u81EA\u52A8\u7F16\u8F91", description: "\u81EA\u52A8\u7F16\u8F91\u76F8\u5173\u6587\u4EF6\uFF0C\u5176\u4ED6\u64CD\u4F5C\u6309 ZCode \u539F\u751F\u89C4\u5219\u5BA1\u6279" },
12867
+ { id: "plan", label: "\u53EA\u8BFB\u89C4\u5212", description: "\u4F7F\u7528 ZCode plan \u6A21\u5F0F\uFF0C\u68C0\u67E5\u9879\u76EE\u5E76\u5236\u5B9A\u8BA1\u5212" },
12868
+ { id: "full", label: "\u5B8C\u5168\u8BBF\u95EE", description: "\u4F7F\u7528 ZCode yolo \u6A21\u5F0F\uFF0C\u51CF\u5C11\u5DE5\u5177\u64CD\u4F5C\u786E\u8BA4" }
12869
+ ] };
12814
12870
  if (op === "configuration.models" || op === "auth.status") {
12815
12871
  const snapshot = await rpc.request("session/create", { workspace: await this.workspaceRef(project), persistence: "deferred", titleGenerationEnabled: false, toolAllowlist: [] });
12816
12872
  try {
@@ -12886,7 +12942,8 @@ var ZcodeAdapter = class {
12886
12942
  }
12887
12943
  if (op === "sessions.prompt") {
12888
12944
  if (!id) throw new Error("Session required");
12889
- await this.session(project, id);
12945
+ const session = await this.session(project, id);
12946
+ if (p.permissionMode !== void 0 && typeof session.status === "string" && !["idle", "completed", "stopped", "closed", "failed", "error"].includes(session.status.toLowerCase())) throw new Error("Wait for the active task before changing permission mode");
12890
12947
  await this.watch(project, rpc, id);
12891
12948
  const lines = [];
12892
12949
  if (typeof p.text === "string" && p.text.trim()) lines.push(screenshotPrompt(p.text));
@@ -12895,6 +12952,12 @@ var ZcodeAdapter = class {
12895
12952
  lines.push(`Attached file: ${upload.destination} (${upload.name}, ${upload.mime}). Read this file if relevant to the task.`);
12896
12953
  }
12897
12954
  if (!lines.length) throw new Error("Message or attachments required");
12955
+ if (p.permissionMode !== void 0) {
12956
+ const modes = { ask: "build", edit: "edit", plan: "plan", full: "yolo" };
12957
+ const mode = typeof p.permissionMode === "string" && Object.hasOwn(modes, p.permissionMode) ? modes[p.permissionMode] : void 0;
12958
+ if (!mode) throw new Error("Unsupported permission mode");
12959
+ await rpc.request("session/setMode", { sessionId: id, mode });
12960
+ }
12898
12961
  const selected = p.model;
12899
12962
  if (selected?.providerID && selected.modelID) {
12900
12963
  let level = selectedReasoningLevel(p, "model.options.reasoningLevel");
@@ -12986,6 +13049,22 @@ var ZcodeAdapter = class {
12986
13049
  import { randomUUID as randomUUID15 } from "node:crypto";
12987
13050
  import { readFile as readFile8, readdir as readdir3, rm as rm2 } from "node:fs/promises";
12988
13051
  import { dirname as dirname5 } from "node:path";
13052
+
13053
+ // src/tools/pi-permissions.ts
13054
+ var PI_PERMISSION_TITLE = "Octopus \u5DE5\u5177\u6388\u6743";
13055
+ var PI_PERMISSION_EXTENSION = `
13056
+ export default function (pi) {
13057
+ const ready = process.env.OCTOPUS_PI_PERMISSION_READY;
13058
+ pi.on('session_start', (_event, ctx) => ctx.ui.notify(ready, 'info'));
13059
+ pi.on('tool_call', async (event, ctx) => {
13060
+ if (!ctx.hasUI) return { block: true, reason: '\u5DE5\u5177\u64CD\u4F5C\u9700\u8981\u7528\u6237\u6279\u51C6' };
13061
+ const allowed = await ctx.ui.confirm(${JSON.stringify(PI_PERMISSION_TITLE)}, JSON.stringify({ toolName: event.toolName, input: event.input }));
13062
+ if (allowed !== true) return { block: true, reason: '\u7528\u6237\u62D2\u7EDD\u4E86\u5DE5\u5177\u64CD\u4F5C' };
13063
+ });
13064
+ }
13065
+ `;
13066
+
13067
+ // src/tools/pi.ts
12989
13068
  var OPERATIONS3 = [
12990
13069
  ...DAEMON_OPERATIONS,
12991
13070
  "sessions.list",
@@ -12998,7 +13077,9 @@ var OPERATIONS3 = [
12998
13077
  "sessions.delete",
12999
13078
  "auth.status",
13000
13079
  "configuration.models",
13080
+ "configuration.permissionModes",
13001
13081
  "permissions.list",
13082
+ "permissions.reply",
13002
13083
  "questions.list",
13003
13084
  "questions.reply",
13004
13085
  "questions.reject"
@@ -13018,6 +13099,7 @@ var PiAdapter = class {
13018
13099
  questions = /* @__PURE__ */ new Map();
13019
13100
  mutations = /* @__PURE__ */ new Map();
13020
13101
  deleting = /* @__PURE__ */ new Set();
13102
+ reconfiguring = /* @__PURE__ */ new Set();
13021
13103
  supportedOperations() {
13022
13104
  return OPERATIONS3;
13023
13105
  }
@@ -13072,7 +13154,7 @@ var PiAdapter = class {
13072
13154
  this.partials.delete(this.key(project, id));
13073
13155
  this.active.delete(this.key(project, id));
13074
13156
  this.clearQuestions(project, id);
13075
- this.emit(project, "session.error", { sessionID: id, message: "Pi connection closed; reconnect to resume the saved session" });
13157
+ if (!this.reconfiguring.has(this.key(project, id))) this.emit(project, "session.error", { sessionID: id, message: "Pi connection closed; reconnect to resume the saved session" });
13076
13158
  }
13077
13159
  });
13078
13160
  }
@@ -13090,16 +13172,20 @@ var PiAdapter = class {
13090
13172
  question.timer.unref();
13091
13173
  }
13092
13174
  this.questions.set(id, question);
13093
- this.emit(project, "question.request", { sessionID: sessionId, requestId: id });
13175
+ this.emit(project, this.isPermission(question) ? "permission.request" : "question.request", { sessionID: sessionId, requestId: id });
13094
13176
  }).catch(() => {
13095
13177
  });
13096
13178
  }
13097
13179
  clearQuestions(project, id) {
13098
13180
  for (const [key, question] of this.questions) if (question.project === project && question.sessionId === id) {
13099
13181
  clearTimeout(question.timer);
13182
+ if (!question.rpc.exited) question.rpc.notify("extension_ui_response", { id: question.detail.id, cancelled: true });
13100
13183
  this.questions.delete(key);
13101
13184
  }
13102
13185
  }
13186
+ isPermission(question) {
13187
+ return question.detail.method === "confirm" && question.detail.title === PI_PERMISSION_TITLE;
13188
+ }
13103
13189
  close() {
13104
13190
  for (const question of this.questions.values()) {
13105
13191
  clearTimeout(question.timer);
@@ -13157,6 +13243,10 @@ var PiAdapter = class {
13157
13243
  async execute(command, project) {
13158
13244
  const op = command.operation, p = command.payload, id = command.sessionId;
13159
13245
  if (!OPERATIONS3.includes(op)) unsupportedOperation("pi", op);
13246
+ if (op === "configuration.permissionModes") return { defaultMode: "ask", modes: [
13247
+ { id: "ask", label: "\u8BF7\u6C42\u6279\u51C6", description: "\u901A\u8FC7 Pi \u5DE5\u5177\u6388\u6743\u6269\u5C55\uFF0C\u5728\u6BCF\u6B21\u5DE5\u5177\u6267\u884C\u524D\u8BF7\u6C42\u6279\u51C6" },
13248
+ { id: "full", label: "\u5B8C\u5168\u8BBF\u95EE", description: "\u6309 Pi \u539F\u751F\u6743\u9650\u6267\u884C\u5DE5\u5177\uFF0C\u4E0D\u542F\u7528\u9010\u6B21\u5BA1\u6279\uFF1B\u4E0D\u63D0\u4F9B\u7CFB\u7EDF\u6C99\u7BB1" }
13249
+ ] };
13160
13250
  if (op === "auth.status") {
13161
13251
  const rpc2 = await this.runtime.connect(project);
13162
13252
  const result = await rpc2.request("get_available_models");
@@ -13198,11 +13288,26 @@ var PiAdapter = class {
13198
13288
  }
13199
13289
  if (op === "permissions.list") {
13200
13290
  if (id) await this.session(project, id);
13201
- return [];
13291
+ return [...this.questions.values()].filter((q) => q.project === project && (!id || q.sessionId === id) && this.isPermission(q)).map((q) => ({
13292
+ id: q.id,
13293
+ sessionID: q.sessionId,
13294
+ permission: "Pi \u5DE5\u5177\u64CD\u4F5C",
13295
+ patterns: [q.detail.message ?? ""],
13296
+ replies: ["once", "reject"]
13297
+ }));
13298
+ }
13299
+ if (op === "permissions.reply") {
13300
+ const question = this.questions.get(String(p.requestId));
13301
+ if (!question || !this.isPermission(question) || question.project !== project || id && id !== question.sessionId) throw new Error("Request is no longer pending in this session");
13302
+ if (p.reply !== "once" && p.reply !== "reject") throw new Error("Invalid permission reply");
13303
+ question.rpc.notify("extension_ui_response", { id: question.detail.id, confirmed: p.reply === "once" });
13304
+ clearTimeout(question.timer);
13305
+ this.questions.delete(question.id);
13306
+ return { ok: true };
13202
13307
  }
13203
13308
  if (op === "questions.list") {
13204
13309
  if (id) await this.session(project, id);
13205
- return [...this.questions.values()].filter((q) => q.project === project && (!id || q.sessionId === id)).map((q) => ({
13310
+ return [...this.questions.values()].filter((q) => q.project === project && (!id || q.sessionId === id) && !this.isPermission(q)).map((q) => ({
13206
13311
  id: q.id,
13207
13312
  sessionID: q.sessionId,
13208
13313
  questions: [{
@@ -13216,7 +13321,7 @@ var PiAdapter = class {
13216
13321
  }
13217
13322
  if (op === "questions.reply" || op === "questions.reject") {
13218
13323
  const question = this.questions.get(String(p.requestId));
13219
- if (!question || question.project !== project || id && id !== question.sessionId) throw new Error("Request is no longer pending in this session");
13324
+ if (!question || this.isPermission(question) || question.project !== project || id && id !== question.sessionId) throw new Error("Request is no longer pending in this session");
13220
13325
  const answers = Array.isArray(p.answers) ? p.answers : [];
13221
13326
  const first = Array.isArray(answers[0]) ? answers[0][0] : answers[0];
13222
13327
  const value = typeof first === "string" ? first : "";
@@ -13234,13 +13339,14 @@ var PiAdapter = class {
13234
13339
  await this.runtime.closeSession(project, id);
13235
13340
  await rm2(this.runtime.sessionFile(project, id), { force: true });
13236
13341
  await rm2(this.metadataPath(project, id));
13342
+ await rm2(this.runtime.sessionFile(project, id).replace(/\.jsonl$/, ".permission.ts"), { force: true });
13237
13343
  this.clearQuestions(project, id);
13238
13344
  } finally {
13239
13345
  this.deleting.delete(this.key(project, id));
13240
13346
  }
13241
13347
  return { ok: true };
13242
13348
  }
13243
- const rpc = await this.rpc(project, id);
13349
+ let rpc = await this.rpc(project, id);
13244
13350
  if (op === "sessions.status") {
13245
13351
  const state = await rpc.request("get_state");
13246
13352
  return { [id]: { type: this.active.has(this.key(project, id)) || state.isStreaming || state.isCompacting || (state.pendingMessageCount ?? 0) > 0 ? "busy" : "idle" } };
@@ -13256,6 +13362,7 @@ var PiAdapter = class {
13256
13362
  return this.media.messages(project, id, this.messages(messages, this.key(project, id)));
13257
13363
  }
13258
13364
  if (op === "sessions.abort") {
13365
+ this.clearQuestions(project, id);
13259
13366
  await rpc.request("clear_queue");
13260
13367
  await rpc.request("abort");
13261
13368
  return { ok: true };
@@ -13270,6 +13377,22 @@ var PiAdapter = class {
13270
13377
  } else lines.push(`Attached file: ${upload.destination} (${upload.name}, ${upload.mime}). Read this file if relevant to the task.`);
13271
13378
  }
13272
13379
  if (!lines.length && !images.length) throw new Error("Message or attachments required");
13380
+ if (p.permissionMode !== void 0) {
13381
+ if (p.permissionMode !== "ask" && p.permissionMode !== "full") throw new Error("Unsupported permission mode");
13382
+ if (p.permissionMode !== (session.permissionMode ?? "full")) {
13383
+ const state = await rpc.request("get_state");
13384
+ if (this.active.has(this.key(project, id)) || state.isStreaming || state.isCompacting || (state.pendingMessageCount ?? 0) > 0) throw new Error("Wait for the active task before changing permission mode");
13385
+ session.permissionMode = p.permissionMode;
13386
+ await atomicJson(this.metadataPath(project, id), session);
13387
+ this.reconfiguring.add(this.key(project, id));
13388
+ try {
13389
+ await this.runtime.closeSession(project, id);
13390
+ rpc = await this.rpc(project, id);
13391
+ } finally {
13392
+ this.reconfiguring.delete(this.key(project, id));
13393
+ }
13394
+ }
13395
+ }
13273
13396
  const model = p.model;
13274
13397
  const level = selectedReasoningLevel(p, "level");
13275
13398
  if (model && typeof model.providerID === "string" && typeof model.modelID === "string") await rpc.request("set_model", { provider: model.providerID, modelId: model.modelID });
@@ -13286,7 +13409,8 @@ var PiAdapter = class {
13286
13409
 
13287
13410
  // src/tools/pi-runtime.ts
13288
13411
  import { execFile as execFile5, spawn as spawn5 } from "node:child_process";
13289
- import { mkdir as mkdir8, open as open8 } from "node:fs/promises";
13412
+ import { mkdir as mkdir8, open as open8, writeFile as writeFile6 } from "node:fs/promises";
13413
+ import { randomUUID as randomUUID16 } from "node:crypto";
13290
13414
  import { join as join13 } from "node:path";
13291
13415
  import { promisify as promisify5 } from "node:util";
13292
13416
  var run3 = promisify5(execFile5);
@@ -13405,32 +13529,58 @@ ${bytes.toString("utf8")}`);
13405
13529
  await this.kill(key);
13406
13530
  const directory = this.project(projectId).directory;
13407
13531
  await mkdir8(join13(this.config.stateDirectory, "pi-sessions", projectId), { recursive: true, mode: 448 });
13532
+ const sessionFile = sessionId ? this.sessionFile(projectId, sessionId) : void 0;
13533
+ const policy = sessionFile ? await readJson(sessionFile.replace(/\.jsonl$/, ".json")) : void 0;
13534
+ const gated = policy?.permissionMode === "ask";
13535
+ const extension = sessionFile?.replace(/\.jsonl$/, ".permission.ts");
13536
+ if (gated) await writeFile6(extension, PI_PERMISSION_EXTENSION, { mode: 384 });
13537
+ const readyToken = gated ? `octopus-permissions-ready:${randomUUID16()}` : "";
13408
13538
  const logPath = join13(this.config.stateDirectory, `pi-${key}.log`);
13409
13539
  const log = await open8(logPath, "a", 384);
13410
13540
  if (generation !== this.generation) {
13411
13541
  await log.close();
13412
13542
  throw new Error("Pi runtime stopped during startup");
13413
13543
  }
13414
- const args = [...describeTool("pi").serverArgs, ...sessionId ? ["--session", this.sessionFile(projectId, sessionId)] : ["--no-session"]];
13415
- const child = spawn5(this.config.tool.command, args, { cwd: directory, env: { ...process.env }, stdio: ["pipe", "pipe", log.fd] });
13544
+ const args = [...describeTool("pi").serverArgs, ...sessionFile ? ["--session", sessionFile] : ["--no-session"], ...gated ? ["--extension", extension] : []];
13545
+ const child = spawn5(this.config.tool.command, args, { cwd: directory, env: { ...process.env, OCTOPUS_PI_PERMISSION_READY: readyToken }, stdio: ["pipe", "pipe", log.fd] });
13416
13546
  child.once("close", () => {
13417
13547
  void log.close();
13418
13548
  });
13419
13549
  this.logPaths.add(logPath);
13550
+ let resolveReady = () => {
13551
+ }, rejectReady = (_error) => {
13552
+ };
13553
+ let readyTimeout;
13554
+ const ready = gated ? new Promise((resolve5, reject) => {
13555
+ resolveReady = resolve5;
13556
+ rejectReady = reject;
13557
+ readyTimeout = setTimeout(() => reject(new Error("Pi \u5DE5\u5177\u6388\u6743\u6269\u5C55\u672A\u5C31\u7EEA\uFF0C\u5DF2\u505C\u6B62\u4F1A\u8BDD\u4EE5\u907F\u514D\u8DF3\u8FC7\u5BA1\u6279")), 1e4);
13558
+ }) : Promise.resolve();
13420
13559
  const rpc = new LineRpc(child, {
13421
13560
  ...handlers,
13561
+ onNotification: (type, data) => {
13562
+ const detail = data;
13563
+ if (gated && type === "extension_ui_request" && detail.method === "notify" && detail.message === readyToken) {
13564
+ resolveReady();
13565
+ return;
13566
+ }
13567
+ handlers.onNotification?.(type, data);
13568
+ },
13422
13569
  onClose: (reason) => {
13570
+ rejectReady(new Error("Pi \u5DE5\u5177\u6388\u6743\u8FDE\u63A5\u5DF2\u5173\u95ED"));
13423
13571
  handlers.onClose?.(reason);
13424
13572
  }
13425
13573
  }, "pi");
13426
13574
  this.processes.set(key, { child, rpc, projectId, sessionId });
13427
13575
  try {
13428
- await rpc.request("get_state");
13576
+ await Promise.all([rpc.request("get_state"), ready]);
13429
13577
  if (generation !== this.generation) throw new Error("Pi runtime stopped during startup");
13430
13578
  return rpc;
13431
13579
  } catch (error) {
13432
13580
  await this.kill(key);
13433
13581
  throw error;
13582
+ } finally {
13583
+ clearTimeout(readyTimeout);
13434
13584
  }
13435
13585
  }
13436
13586
  async kill(key) {
@@ -13469,6 +13619,7 @@ function createAdapter(runtime, workspace, uploads, emit) {
13469
13619
  // src/project-registry.ts
13470
13620
  import { realpath as realpath6, readFile as readFile9, stat as stat7 } from "node:fs/promises";
13471
13621
  import { isAbsolute as isAbsolute4 } from "node:path";
13622
+ import { isDeepStrictEqual } from "node:util";
13472
13623
  var ProjectMutationSchema = ProjectSchema.extend({
13473
13624
  name: ProjectSchema.shape.name.trim().max(120),
13474
13625
  specDirectories: ProjectSchema.shape.specDirectories.removeDefault().element.min(1).array().max(20).default(["specs", "docs"])
@@ -13484,13 +13635,21 @@ function rawProjectId(value) {
13484
13635
  return typeof value === "object" && value !== null && !Array.isArray(value) && typeof value.id === "string" ? value.id : void 0;
13485
13636
  }
13486
13637
  var ProjectRegistry = class {
13487
- constructor(configPath, config, runtime, adapter) {
13638
+ constructor(configPath, config, runtime, adapter, onChanged = () => {
13639
+ }) {
13488
13640
  this.configPath = configPath;
13489
13641
  this.config = config;
13490
13642
  this.runtime = runtime;
13491
13643
  this.adapter = adapter;
13644
+ this.onChanged = onChanged;
13492
13645
  }
13493
13646
  pending = Promise.resolve();
13647
+ // The daemon runs this under the same lock as tool selection and project mutations.
13648
+ reload() {
13649
+ return this.serial(async () => {
13650
+ await this.readCurrent();
13651
+ });
13652
+ }
13494
13653
  add(payload) {
13495
13654
  return this.serial(() => this.addUnlocked(payload));
13496
13655
  }
@@ -13514,17 +13673,36 @@ var ProjectRegistry = class {
13514
13673
  }
13515
13674
  async readCurrent() {
13516
13675
  const current = rawConfig(JSON.parse(await readFile9(this.configPath, "utf8")));
13517
- const diskProjects = current.projects.map((value) => {
13518
- const project = ProjectSchema.parse(value);
13519
- return { ...project, directory: expandHome(project.directory) };
13520
- });
13521
- if (JSON.stringify(diskProjects) !== JSON.stringify(this.config.projects)) {
13522
- throw new Error("Proxy \u914D\u7F6E\u5DF2\u5728\u4E3B\u673A\u4E0A\u88AB\u4FEE\u6539\uFF1B\u8BF7\u91CD\u542F Proxy \u540E\u518D\u7BA1\u7406\u9879\u76EE");
13523
- }
13676
+ const diskProjects = ConfigSchema.shape.projects.parse(current.projects).map((project) => ({ ...project, directory: expandHome(project.directory) }));
13677
+ if (new Set(diskProjects.map((project) => project.id)).size !== diskProjects.length) throw new Error("\u9879\u76EE ID \u91CD\u590D");
13678
+ if (diskProjects.some((project) => !isAbsolute4(project.directory))) throw new Error("\u9879\u76EE\u76EE\u5F55\u5FC5\u987B\u662F Proxy \u4E3B\u673A\u4E0A\u7684\u7EDD\u5BF9\u8DEF\u5F84");
13679
+ await this.applyProjects(diskProjects);
13524
13680
  return current;
13525
13681
  }
13682
+ async persist(expected, projects) {
13683
+ const latest = rawConfig(JSON.parse(await readFile9(this.configPath, "utf8")));
13684
+ if (!isDeepStrictEqual(latest.projects, expected.projects)) {
13685
+ throw new Error("\u4E3B\u673A\u4E0A\u7684\u9879\u76EE\u914D\u7F6E\u5728\u4FDD\u5B58\u671F\u95F4\u53D1\u751F\u53D8\u5316\uFF0C\u8BF7\u5237\u65B0\u540E\u91CD\u8BD5");
13686
+ }
13687
+ await atomicJson(this.configPath, { ...latest, projects });
13688
+ }
13689
+ async applyProjects(projects) {
13690
+ if (isDeepStrictEqual(projects, this.config.projects)) return;
13691
+ const reset = this.config.projects.filter((previous) => !projects.some((project) => project.id === previous.id && project.directory === previous.directory));
13692
+ const added = projects.filter((project) => !this.config.projects.some((previous) => previous.id === project.id && previous.directory === project.directory));
13693
+ for (const project of reset) {
13694
+ await Promise.all([
13695
+ this.bestEffort(() => this.adapter.removeProject?.(project.id)),
13696
+ this.bestEffort(() => this.runtime.removeProject?.(project.id))
13697
+ ]);
13698
+ }
13699
+ this.config.projects.splice(0, this.config.projects.length, ...projects);
13700
+ for (const project of added) await this.bestEffort(() => this.runtime.addProject?.(project.id));
13701
+ this.onChanged(reset.map((project) => project.id));
13702
+ }
13526
13703
  async addUnlocked(payload) {
13527
13704
  const parsed = ProjectMutationSchema.parse(payload);
13705
+ const raw = await this.readCurrent();
13528
13706
  const directory = expandHome(parsed.directory);
13529
13707
  if (!isAbsolute4(directory)) throw new Error("\u9879\u76EE\u76EE\u5F55\u5FC5\u987B\u662F Proxy \u4E3B\u673A\u4E0A\u7684\u7EDD\u5BF9\u8DEF\u5F84");
13530
13708
  const info = await stat7(directory).catch(() => void 0);
@@ -13535,14 +13713,13 @@ var ProjectRegistry = class {
13535
13713
  if (await realpath6(project2.directory).catch(() => project2.directory) === canonical) throw new Error("\u8BE5\u76EE\u5F55\u5DF2\u7ECF\u662F\u6388\u6743\u9879\u76EE");
13536
13714
  }
13537
13715
  const project = { ...parsed, directory: canonical };
13538
- const raw = await this.readCurrent();
13539
- await atomicJson(this.configPath, { ...raw, projects: [...raw.projects, project] });
13540
- this.config.projects.push(project);
13541
- await this.bestEffort(() => this.runtime.addProject?.(project.id));
13716
+ await this.persist(raw, [...raw.projects, project]);
13717
+ await this.applyProjects([...this.config.projects, project]);
13542
13718
  return publicProjects(this.config);
13543
13719
  }
13544
13720
  async updateUnlocked(payload) {
13545
13721
  const parsed = ProjectUpdateSchema.parse(payload);
13722
+ const raw = await this.readCurrent();
13546
13723
  const index = this.config.projects.findIndex((project) => project.id === parsed.id);
13547
13724
  if (index < 0) throw new Error(`\u672A\u77E5\u6388\u6743\u9879\u76EE\uFF1A${parsed.id}`);
13548
13725
  const directory = expandHome(parsed.directory);
@@ -13556,21 +13733,8 @@ var ProjectRegistry = class {
13556
13733
  }
13557
13734
  }
13558
13735
  const current = this.config.projects[index];
13559
- const oldDirectory = await realpath6(current.directory).catch(() => current.directory);
13560
- const directoryChanged = oldDirectory !== canonical;
13561
- const raw = await this.readCurrent();
13562
- await atomicJson(this.configPath, {
13563
- ...raw,
13564
- projects: raw.projects.map((project) => rawProjectId(project) === parsed.id ? { ...project, name: parsed.name, directory: canonical } : project)
13565
- });
13566
- if (directoryChanged) {
13567
- await Promise.all([
13568
- this.bestEffort(() => this.adapter.removeProject?.(parsed.id)),
13569
- this.bestEffort(() => this.runtime.removeProject?.(parsed.id))
13570
- ]);
13571
- }
13572
- this.config.projects[index] = { ...current, name: parsed.name, directory: canonical };
13573
- if (directoryChanged) await this.bestEffort(() => this.runtime.addProject?.(parsed.id));
13736
+ await this.persist(raw, raw.projects.map((project) => rawProjectId(project) === parsed.id ? { ...project, name: parsed.name, directory: canonical } : project));
13737
+ await this.applyProjects(this.config.projects.map((project) => project.id === parsed.id ? { ...current, name: parsed.name, directory: canonical } : project));
13574
13738
  return publicProjects(this.config);
13575
13739
  }
13576
13740
  async removeUnlocked(payload) {
@@ -13579,22 +13743,18 @@ var ProjectRegistry = class {
13579
13743
  throw new Error("\u5220\u9664\u9879\u76EE\u9700\u8981\u5408\u6CD5\u7684\u9879\u76EE ID");
13580
13744
  }
13581
13745
  const id = payload.id;
13746
+ const raw = await this.readCurrent();
13582
13747
  if (this.config.projects.length === 1) throw new Error("\u81F3\u5C11\u9700\u8981\u4FDD\u7559\u4E00\u4E2A\u6388\u6743\u9879\u76EE");
13583
13748
  const index = this.config.projects.findIndex((project) => project.id === id);
13584
13749
  if (index < 0) throw new Error(`\u672A\u77E5\u6388\u6743\u9879\u76EE\uFF1A${id}`);
13585
- const raw = await this.readCurrent();
13586
- await atomicJson(this.configPath, { ...raw, projects: raw.projects.filter((project) => rawProjectId(project) !== id) });
13587
- await Promise.all([
13588
- this.bestEffort(() => this.adapter.removeProject?.(id)),
13589
- this.bestEffort(() => this.runtime.removeProject?.(id))
13590
- ]);
13591
- this.config.projects.splice(index, 1);
13750
+ await this.persist(raw, raw.projects.filter((project) => rawProjectId(project) !== id));
13751
+ await this.applyProjects(this.config.projects.filter((project) => project.id !== id));
13592
13752
  return publicProjects(this.config);
13593
13753
  }
13594
13754
  };
13595
13755
 
13596
13756
  // src/terminal.ts
13597
- import { randomUUID as randomUUID16 } from "node:crypto";
13757
+ import { randomUUID as randomUUID17 } from "node:crypto";
13598
13758
  import { access as access2, stat as stat9 } from "node:fs/promises";
13599
13759
  import { constants as constants5 } from "node:fs";
13600
13760
  import { platform as platform3, userInfo as userInfo3 } from "node:os";
@@ -13680,7 +13840,7 @@ var TerminalManager = class {
13680
13840
  throw new Error(`\u7EC8\u7AEF Shell \u4E0D\u5B58\u5728\u6216\u4E0D\u53EF\u6267\u884C\uFF1A${shell}\uFF1B\u8BF7\u4FEE\u6B63 Proxy \u670D\u52A1\u7684 SHELL \u73AF\u5883\u53D8\u91CF`);
13681
13841
  }
13682
13842
  const { helper } = await ensureTerminalSupport();
13683
- const id = randomUUID16();
13843
+ const id = randomUUID17();
13684
13844
  const path = [
13685
13845
  process.env.PATH,
13686
13846
  "/usr/bin",
@@ -13745,6 +13905,13 @@ var TerminalManager = class {
13745
13905
  terminal.process.kill();
13746
13906
  }
13747
13907
  }
13908
+ closeProject(project) {
13909
+ for (const terminal of this.terminals.values()) if (terminal.project === project) {
13910
+ this.terminals.delete(terminal.id);
13911
+ terminal.process.kill();
13912
+ this.emit(terminal.owner, project, "terminal.closed", { id: terminal.id });
13913
+ }
13914
+ }
13748
13915
  close() {
13749
13916
  for (const owner of new Set([...this.terminals.values()].map((t) => t.owner))) this.closeOwned(owner);
13750
13917
  }
@@ -13779,7 +13946,7 @@ var PlatformDesktopSession = class {
13779
13946
 
13780
13947
  // src/remote-manager.ts
13781
13948
  import { fork } from "node:child_process";
13782
- import { randomBytes as randomBytes2, randomUUID as randomUUID17 } from "node:crypto";
13949
+ import { randomBytes as randomBytes2, randomUUID as randomUUID18 } from "node:crypto";
13783
13950
  import { fileURLToPath as fileURLToPath4 } from "node:url";
13784
13951
 
13785
13952
  // src/remote-stun.ts
@@ -13913,7 +14080,7 @@ var RemoteManager = class {
13913
14080
  if (this.current) throw new Error("\u4E3B\u673A\u6B63\u5728\u88AB\u63A7\u5236");
13914
14081
  const quality = RemoteQualitySchema.parse(payload.quality ?? "hd");
13915
14082
  const budget = remoteSessionBudget(this.config, quality, payload.quality === void 0);
13916
- const session = { remoteId: randomUUID17(), owner, closed: false, signaling: Promise.resolve(), signals: 0, signalWindow: Date.now(), candidates: 0 };
14083
+ const session = { remoteId: randomUUID18(), owner, closed: false, signaling: Promise.resolve(), signals: 0, signalWindow: Date.now(), candidates: 0 };
13917
14084
  this.current = session;
13918
14085
  try {
13919
14086
  const [permissions, stunUrls] = await Promise.all([
@@ -13992,10 +14159,10 @@ var RemoteManager = class {
13992
14159
  };
13993
14160
 
13994
14161
  // src/tool-selection.ts
13995
- import { randomBytes as randomBytes3, randomUUID as randomUUID18 } from "node:crypto";
14162
+ import { randomBytes as randomBytes3, randomUUID as randomUUID19 } from "node:crypto";
13996
14163
  import { access as access3, constants as constants6, readFile as readFile10 } from "node:fs/promises";
13997
14164
  import { platform as platform5 } from "node:os";
13998
- import { isDeepStrictEqual } from "node:util";
14165
+ import { isDeepStrictEqual as isDeepStrictEqual2 } from "node:util";
13999
14166
 
14000
14167
  // src/node-resolution.ts
14001
14168
  import { realpath as realpath7, stat as stat10 } from "node:fs/promises";
@@ -14136,7 +14303,7 @@ var ToolSelection = class {
14136
14303
  });
14137
14304
  }
14138
14305
  modelCommand(projectId) {
14139
- return { type: "command", id: randomUUID18(), botId: this.config.botId, operation: "configuration.models", projectId, payload: {} };
14306
+ return { type: "command", id: randomUUID19(), botId: this.config.botId, operation: "configuration.models", projectId, payload: {} };
14140
14307
  }
14141
14308
  profiles(raw) {
14142
14309
  return raw.toolProfiles && typeof raw.toolProfiles === "object" && !Array.isArray(raw.toolProfiles) ? raw.toolProfiles : {};
@@ -14175,7 +14342,7 @@ var ToolSelection = class {
14175
14342
  }
14176
14343
  async readCurrent() {
14177
14344
  const disk = await loadConfig(this.configPath);
14178
- if (!isDeepStrictEqual(JSON.parse(JSON.stringify(disk.tool)), JSON.parse(JSON.stringify(this.config.tool))) || !isDeepStrictEqual(disk.projects, this.config.projects) || disk.botId !== this.config.botId || disk.secretKey !== this.config.secretKey) {
14345
+ if (!isDeepStrictEqual2(JSON.parse(JSON.stringify(disk.tool)), JSON.parse(JSON.stringify(this.config.tool))) || !isDeepStrictEqual2(disk.projects, this.config.projects) || disk.botId !== this.config.botId || disk.secretKey !== this.config.secretKey) {
14179
14346
  throw new Error("Proxy \u914D\u7F6E\u5DF2\u5728\u4E3B\u673A\u4E0A\u88AB\u4FEE\u6539\uFF1B\u8BF7\u91CD\u542F Proxy \u540E\u518D\u5207\u6362\u667A\u80FD\u4F53");
14180
14347
  }
14181
14348
  return JSON.parse(await readFile10(this.configPath, "utf8"));
@@ -14219,7 +14386,12 @@ async function runDaemon(configPath, authenticate = verifyHostPassword) {
14219
14386
  return { runtime, adapter };
14220
14387
  });
14221
14388
  const artifacts = new Artifacts(workspace, join15(config.stateDirectory, "artifacts"), emit);
14389
+ const projectRegistry = () => new ProjectRegistry(configPath, config, tools.current.runtime, tools.current.adapter, (resetProjects) => {
14390
+ for (const id of resetProjects) terminals.closeProject(id);
14391
+ });
14392
+ const reloadProjects = () => tools.exclusive(() => projectRegistry().reload());
14222
14393
  async function dispatch(c, secureSessionId) {
14394
+ if (["runtime.snapshot", "workspace.projects", "runtime.models", "runtime.switchTool"].includes(c.operation)) await reloadProjects();
14223
14395
  if (c.operation.startsWith("desktop.remote.")) return remote.dispatch(c, secureSessionId);
14224
14396
  if (c.operation.startsWith("desktop.")) return desktop.dispatch(c, secureSessionId);
14225
14397
  if (c.operation.startsWith("terminal.")) return terminals.dispatch(c, secureSessionId);
@@ -14239,17 +14411,17 @@ async function runDaemon(configPath, authenticate = verifyHostPassword) {
14239
14411
  if (c.operation === "runtime.restart") return runtime.restart();
14240
14412
  if (c.operation === "workspace.projects") return publicProjects(config);
14241
14413
  if (c.operation === "workspace.projects.add") {
14242
- const result2 = await tools.exclusive(() => new ProjectRegistry(configPath, config, tools.current.runtime, tools.current.adapter).add(p));
14414
+ const result2 = await tools.exclusive(() => projectRegistry().add(p));
14243
14415
  emit(String(p.id), "octopus.resync", {});
14244
14416
  return result2;
14245
14417
  }
14246
14418
  if (c.operation === "workspace.projects.update") {
14247
- const result2 = await tools.exclusive(() => new ProjectRegistry(configPath, config, tools.current.runtime, tools.current.adapter).update(p));
14419
+ const result2 = await tools.exclusive(() => projectRegistry().update(p));
14248
14420
  emit(String(p.id), "octopus.resync", {});
14249
14421
  return result2;
14250
14422
  }
14251
14423
  if (c.operation === "workspace.projects.remove") {
14252
- const result2 = await tools.exclusive(() => new ProjectRegistry(configPath, config, tools.current.runtime, tools.current.adapter).remove(p));
14424
+ const result2 = await tools.exclusive(() => projectRegistry().remove(p));
14253
14425
  emit(String(p.id), "octopus.resync", {});
14254
14426
  return result2;
14255
14427
  }
package/dist/cli.js CHANGED
@@ -35,7 +35,7 @@ import {
35
35
  supportedPlatform,
36
36
  systemdUnitPath,
37
37
  toolSearchDirs
38
- } from "./chunk-R73E3EFB.js";
38
+ } from "./chunk-6RJKEA3I.js";
39
39
  import {
40
40
  DESKTOP_PERMISSION_GUIDANCE,
41
41
  PlatformDesktop,
@@ -48,7 +48,7 @@ import { spawn as spawn2 } from "node:child_process";
48
48
  import { access as access4 } from "node:fs/promises";
49
49
 
50
50
  // src/doctor.ts
51
- import { access as access2, chmod, constants, mkdir as mkdir3, stat } from "node:fs/promises";
51
+ import { access as access2, chmod, constants, mkdir as mkdir3, stat as stat2 } from "node:fs/promises";
52
52
  import { isAbsolute as isAbsolute3, join } from "node:path";
53
53
  import { execFile as execFile3 } from "node:child_process";
54
54
  import { promisify as promisify3 } from "node:util";
@@ -130,8 +130,9 @@ async function launchdStatus() {
130
130
  }
131
131
 
132
132
  // src/systemd.ts
133
- import { mkdir as mkdir2, rm as rm2, writeFile as writeFile2 } from "node:fs/promises";
133
+ import { mkdir as mkdir2, rm as rm2, stat, writeFile as writeFile2 } from "node:fs/promises";
134
134
  import { dirname as dirname2 } from "node:path";
135
+ import { userInfo as userInfo2 } from "node:os";
135
136
  import { execFile as execFile2 } from "node:child_process";
136
137
  import { promisify as promisify2 } from "node:util";
137
138
  var run2 = promisify2(execFile2);
@@ -170,8 +171,15 @@ async function writeUnitFile(text) {
170
171
  return path;
171
172
  }
172
173
  async function systemctl(args) {
174
+ const env = { ...process.env };
175
+ if (!env.XDG_RUNTIME_DIR && !env.DBUS_SESSION_BUS_ADDRESS) {
176
+ const uid = userInfo2().uid;
177
+ const runtimeDirectory = `/run/user/${uid}`;
178
+ const info = await stat(runtimeDirectory).catch(() => void 0);
179
+ if (info?.isDirectory() && info.uid === uid) env.XDG_RUNTIME_DIR = runtimeDirectory;
180
+ }
173
181
  try {
174
- const { stdout: stdout3 } = await run2("systemctl", ["--user", ...args]);
182
+ const { stdout: stdout3 } = await run2("systemctl", ["--user", ...args], { env });
175
183
  return stdout3;
176
184
  } catch (error) {
177
185
  const stderr = error instanceof Error ? error.stderr ?? error.message : String(error);
@@ -214,9 +222,9 @@ async function systemdUninstall() {
214
222
  await systemdReload();
215
223
  }
216
224
  async function systemdStatus() {
217
- const active = await ignoreFailure(() => systemctl(["is-active", SYSTEMD_UNIT]));
218
- const enabled = await ignoreFailure(() => systemctl(["is-enabled", SYSTEMD_UNIT]));
219
- return { active: active?.trim() === "active", enabled: enabled?.trim() === "enabled" };
225
+ const output = await systemctl(["show", SYSTEMD_UNIT, "--property=ActiveState", "--property=UnitFileState"]);
226
+ const properties = Object.fromEntries(output.trim().split("\n").map((line) => line.split("=")));
227
+ return { active: properties.ActiveState === "active", enabled: properties.UnitFileState === "enabled" };
220
228
  }
221
229
 
222
230
  // src/service.ts
@@ -339,7 +347,7 @@ async function runDoctor(opts) {
339
347
  results.push(await checkTerminalSupport(opts.fix));
340
348
  let config;
341
349
  try {
342
- const info = await stat(opts.configPath).catch((error) => {
350
+ const info = await stat2(opts.configPath).catch((error) => {
343
351
  if (error.code === "ENOENT") throw new Error(`\u914D\u7F6E\u6587\u4EF6\u4E0D\u5B58\u5728\uFF1A${opts.configPath}\uFF1B\u5148\u8FD0\u884C octopus-proxy onboard`);
344
352
  throw error;
345
353
  });
@@ -457,7 +465,7 @@ async function runDoctor(opts) {
457
465
  import { createInterface } from "node:readline/promises";
458
466
  import { randomBytes } from "node:crypto";
459
467
  import { stdin, stdout } from "node:process";
460
- import { access as access3, constants as constants2, mkdir as mkdir4, stat as stat2 } from "node:fs/promises";
468
+ import { access as access3, constants as constants2, mkdir as mkdir4, stat as stat3 } from "node:fs/promises";
461
469
  import { execFile as execFile4 } from "node:child_process";
462
470
  import { platform } from "node:os";
463
471
  import { isAbsolute as isAbsolute4, join as join2 } from "node:path";
@@ -501,7 +509,7 @@ async function validateToolCommand(value, kind) {
501
509
  const command2 = expandHome(value);
502
510
  if (!isAbsolute4(command2)) return "\u670D\u52A1\u73AF\u5883\u9700\u8981\u7EDD\u5BF9\u8DEF\u5F84";
503
511
  try {
504
- if (!(await stat2(command2)).isFile()) return "\u547D\u4EE4\u8DEF\u5F84\u5FC5\u987B\u6307\u5411\u53EF\u6267\u884C\u6587\u4EF6";
512
+ if (!(await stat3(command2)).isFile()) return "\u547D\u4EE4\u8DEF\u5F84\u5FC5\u987B\u6307\u5411\u53EF\u6267\u884C\u6587\u4EF6";
505
513
  await access3(command2, constants2.X_OK);
506
514
  } catch {
507
515
  return `\u547D\u4EE4\u4E0D\u5B58\u5728\u6216\u4E0D\u53EF\u6267\u884C\uFF1A${command2}`;
@@ -514,7 +522,7 @@ async function validateToolCommand(value, kind) {
514
522
  return void 0;
515
523
  }
516
524
  async function runOnboard(opts) {
517
- const configExists = await stat2(opts.configPath).then(() => true, (error) => {
525
+ const configExists = await stat3(opts.configPath).then(() => true, (error) => {
518
526
  if (error.code === "ENOENT") return false;
519
527
  throw error;
520
528
  });
@@ -564,7 +572,7 @@ async function runOnboard(opts) {
564
572
  console.log(" \u5FC5\u987B\u662F\u7EDD\u5BF9\u8DEF\u5F84");
565
573
  continue;
566
574
  }
567
- if (!await stat2(project.directory).then(() => true, () => false)) {
575
+ if (!await stat3(project.directory).then(() => true, () => false)) {
568
576
  console.log(" \u76EE\u5F55\u4E0D\u5B58\u5728");
569
577
  continue;
570
578
  }
@@ -593,7 +601,7 @@ async function runOnboard(opts) {
593
601
  console.log(" \u5FC5\u987B\u662F\u7EDD\u5BF9\u8DEF\u5F84");
594
602
  continue;
595
603
  }
596
- if (!await stat2(directory).then(() => true, () => false)) {
604
+ if (!await stat3(directory).then(() => true, () => false)) {
597
605
  console.log(" \u76EE\u5F55\u4E0D\u5B58\u5728");
598
606
  continue;
599
607
  }
package/dist/main.js CHANGED
@@ -2,7 +2,7 @@
2
2
  import {
3
3
  ConfigError,
4
4
  runDaemon
5
- } from "./chunk-R73E3EFB.js";
5
+ } from "./chunk-6RJKEA3I.js";
6
6
  import "./chunk-GS3Q6EU5.js";
7
7
 
8
8
  // src/main.ts
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@skylandnpm/octopus-proxy",
3
- "version": "0.1.63",
3
+ "version": "0.1.65",
4
4
  "type": "module",
5
5
  "description": "Octopus host proxy: OpenCode, Codex, ZCode and Pi adapters with service installer CLI",
6
6
  "bin": {