@skylandnpm/octopus-proxy 0.1.34 → 0.1.38

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
@@ -18,7 +18,7 @@ octopus-proxy identity
18
18
 
19
19
  Proxy 主动连接 Hub 的 `/ws/proxy-chat`,无需开放入站端口或安装 sshd。Claw 经 `/ws/claw-chat` 建立到 Proxy 的内层 TLS 1.3 会话;Hub 只转发加密字节。首次打开工作区显示主机公钥指纹,点击 Trust 后按 Hub 地址和机器人 ID 固定;之后指纹变化会拒绝连接。每次连接都要输入运行 Proxy 服务账号的主机密码,由主机上的 PAM(Linux)或 Open Directory(macOS)验证。断线或连续 60 分钟没有用户操作后需要重新认证。服务安装时生成主机身份,`octopus-proxy identity` 可在主机上查看指纹。安装主机需提供 OpenSSL 和 C 编译器;Linux 还需 PAM 开发头文件。机器人 Secret 仅用于 Hub 路由,不能单独授权工作区。
20
20
 
21
- macOS 12.3 及以上运行 `octopus-proxy onboard` 时,配置保存后、服务启动前会准备截屏辅助程序并申请系统屏幕录制权限;已授权时不会重复弹窗。交互模式和 `--non-interactive` 均执行此步骤,`--no-install-service` 也会申请。请在系统弹窗中允许访问;拒绝、超时或辅助程序编译失败时,向导提示原因与授权路径,并继续初始化。之后可在“系统设置 → 隐私与安全性 → 屏幕录制(或屏幕与系统音频录制)”中授权 `octopus-desktop-capture` 或系统列出的启动程序,再重启 Proxy。Linux 没有通用的截屏授权弹窗,需使用桌面用户的图形会话环境;平台依赖与设置见[桌面快照说明](../../docs/claw-desktop-snapshots.md)。
21
+ macOS 12.3 及以上运行 `octopus-proxy onboard` 时,配置保存后、服务启动前会准备截屏辅助程序并申请系统屏幕录制权限;已授权时不会重复弹窗。交互模式和 `--non-interactive` 均执行此步骤,`--no-install-service` 也会申请。请在系统弹窗中允许访问;拒绝、超时或辅助程序编译失败时,向导提示原因与授权路径,并继续初始化。在“系统设置 → 隐私与安全性 → 屏幕录制(或屏幕与系统音频录制)”中应授权弹窗显示的实际程序,可能是 Terminal、iTerm、编辑器或 node,不保证出现 `octopus-desktop-capture`。onboard 和 doctor 会显示辅助程序路径,文件名不等于系统授权名称。终端授权不代表后台服务已授权,需在 Claw 点击截屏,让实际服务申请权限,按实际名称授权后重启 Proxy。Linux 没有通用的截屏授权弹窗,需使用桌面用户的图形会话环境;平台依赖与设置见[桌面快照说明](../../docs/claw-desktop-snapshots.md)。
22
22
 
23
23
  添加授权项目时,填写项目 ID 后会询问是否继续添加;直接回车即可结束添加并继续保存配置。
24
24
 
@@ -11714,8 +11714,8 @@ async function ensureHostIdentity(stateDirectory) {
11714
11714
  }
11715
11715
 
11716
11716
  // src/daemon.ts
11717
- import { mkdir as mkdir10 } from "node:fs/promises";
11718
- import { join as join13 } from "node:path";
11717
+ import { mkdir as mkdir11 } from "node:fs/promises";
11718
+ import { join as join14 } from "node:path";
11719
11719
 
11720
11720
  // src/workspace-diff.ts
11721
11721
  import { execFile as execFile2 } from "node:child_process";
@@ -12186,7 +12186,7 @@ function isAuthentication(operation) {
12186
12186
  }
12187
12187
 
12188
12188
  // src/journal.ts
12189
- import { join as join4 } from "node:path";
12189
+ import { join as join6 } from "node:path";
12190
12190
  import { createHash as createHash4 } from "node:crypto";
12191
12191
 
12192
12192
  // ../../packages/protocol/src/index.ts
@@ -12250,6 +12250,8 @@ var Operations = [
12250
12250
  "desktop.list",
12251
12251
  "desktop.capture",
12252
12252
  "desktop.read",
12253
+ "desktop.lockStatus",
12254
+ "desktop.unlock",
12253
12255
  "artifacts.list",
12254
12256
  "artifacts.history",
12255
12257
  "artifacts.read",
@@ -12271,6 +12273,252 @@ var CommandSchema = z.object({
12271
12273
  }).strict();
12272
12274
  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);
12273
12275
 
12276
+ // src/screen-lock.ts
12277
+ import { spawn as spawn2 } from "node:child_process";
12278
+ import { chmod as chmod3, mkdir as mkdir5, rename as rename4, rm, stat as stat6 } from "node:fs/promises";
12279
+ import { randomUUID as randomUUID7 } from "node:crypto";
12280
+ import { userInfo as userInfo2 } from "node:os";
12281
+ import { join as join5 } from "node:path";
12282
+ import { fileURLToPath as fileURLToPath3 } from "node:url";
12283
+ import { setTimeout as delay } from "node:timers/promises";
12284
+
12285
+ // src/host-auth.ts
12286
+ import { spawn } from "node:child_process";
12287
+ import { chmod as chmod2, mkdir as mkdir4, rename as rename3, stat as stat5, unlink as unlink4 } from "node:fs/promises";
12288
+ import { userInfo, platform as platform2 } from "node:os";
12289
+ import { randomUUID as randomUUID6 } from "node:crypto";
12290
+ import { join as join4 } from "node:path";
12291
+ import { fileURLToPath as fileURLToPath2 } from "node:url";
12292
+ function helperPath(stateDirectory) {
12293
+ return join4(stateDirectory, "octopus-host-auth");
12294
+ }
12295
+ async function ensureHostAuth(stateDirectory) {
12296
+ await mkdir4(stateDirectory, { recursive: true, mode: 448 });
12297
+ const target = helperPath(stateDirectory);
12298
+ const system = platform2();
12299
+ if (system !== "darwin" && system !== "linux") throw new Error("Host authentication is supported only on macOS and Linux");
12300
+ const mac = system === "darwin";
12301
+ const source = fileURLToPath2(new URL(mac ? "../native/auth-macos.m" : "../native/auth-linux.c", import.meta.url));
12302
+ const sourceInfo = await stat5(source);
12303
+ const existing = await stat5(target).catch(() => null);
12304
+ if (existing?.isFile() && existing.mtimeMs >= sourceInfo.mtimeMs && (existing.mode & 63) === 0) return target;
12305
+ const temporary = `${target}.${randomUUID6()}`;
12306
+ const args = mac ? ["-O2", "-framework", "Foundation", "-framework", "OpenDirectory", source, "-o", target] : ["-O2", source, "-o", temporary, "-lpam"];
12307
+ if (mac) args[args.length - 1] = temporary;
12308
+ const compiler = spawn(mac ? "clang" : "cc", args, { stdio: ["ignore", "ignore", "pipe"] });
12309
+ let diagnostics = "";
12310
+ compiler.stderr.on("data", (chunk) => {
12311
+ diagnostics += String(chunk).slice(0, 4096);
12312
+ });
12313
+ const exit = await new Promise((resolve4, reject) => {
12314
+ compiler.on("error", reject);
12315
+ compiler.on("exit", (code) => resolve4(code ?? 1));
12316
+ });
12317
+ if (exit !== 0) {
12318
+ await unlink4(temporary).catch(() => {
12319
+ });
12320
+ throw new Error(`Host authentication helper build failed: ${diagnostics.trim()}`);
12321
+ }
12322
+ try {
12323
+ await chmod2(temporary, 448);
12324
+ await rename3(temporary, target);
12325
+ return target;
12326
+ } finally {
12327
+ await unlink4(temporary).catch(() => {
12328
+ });
12329
+ }
12330
+ }
12331
+ async function verifyHostPassword(stateDirectory, username, password) {
12332
+ if (username !== userInfo().username || !password || Buffer.byteLength(password) > 1024) return false;
12333
+ const path = await ensureHostAuth(stateDirectory);
12334
+ const child = spawn(path, [], { stdio: ["pipe", "pipe", "ignore"] });
12335
+ const bytes = Buffer.from(password, "utf8");
12336
+ const header2 = Buffer.alloc(2);
12337
+ header2.writeUInt16BE(bytes.length);
12338
+ child.stdin.end(Buffer.concat([header2, bytes]));
12339
+ bytes.fill(0);
12340
+ let output = "";
12341
+ child.stdout.on("data", (chunk) => {
12342
+ output += String(chunk).slice(0, 16);
12343
+ });
12344
+ const timer = setTimeout(() => child.kill("SIGKILL"), 1e4);
12345
+ try {
12346
+ const code = await new Promise((resolve4, reject) => {
12347
+ child.on("error", reject);
12348
+ child.on("exit", (value) => resolve4(value ?? 1));
12349
+ });
12350
+ return code === 0 && output.trim() === "OK";
12351
+ } finally {
12352
+ clearTimeout(timer);
12353
+ }
12354
+ }
12355
+
12356
+ // src/screen-lock.ts
12357
+ var ScreenLockError = class extends Error {
12358
+ };
12359
+ var nativeErrors = {
12360
+ permission: "\u8BF7\u5728 Proxy \u4E3B\u673A\u201C\u7CFB\u7EDF\u8BBE\u7F6E \u2192 \u9690\u79C1\u4E0E\u5B89\u5168\u6027 \u2192 \u8F85\u52A9\u529F\u80FD\u201D\u4E2D\u6388\u6743\u5B9E\u9645\u8FD0\u884C Proxy \u7684\u7A0B\u5E8F\uFF0C\u5E76\u91CD\u542F Proxy\u3002",
12361
+ session: "\u65E0\u6CD5\u8BBF\u95EE Proxy \u7528\u6237\u7684\u6D3B\u52A8\u56FE\u5F62\u4F1A\u8BDD\uFF1B\u4E0D\u652F\u6301\u767B\u5F55\u524D\u3001FileVault \u542F\u52A8\u89E3\u9501\u6216\u5207\u6362\u5230\u5176\u4ED6\u7528\u6237\u7684\u5C4F\u5E55\u3002",
12362
+ focus: "\u672A\u627E\u5230 loginwindow \u7684\u5BC6\u7801\u8F93\u5165\u6846\uFF0C\u8BF7\u5728\u4E3B\u673A\u663E\u793A\u5F53\u524D\u7528\u6237\u7684\u5BC6\u7801\u89E3\u9501\u754C\u9762\u540E\u91CD\u8BD5\u3002",
12363
+ changed: "\u4E3B\u673A\u9501\u5C4F\u72B6\u6001\u6216\u5BC6\u7801\u8F93\u5165\u7126\u70B9\u5DF2\u53D8\u5316\uFF0C\u5DF2\u505C\u6B62\u8F93\u5165\uFF0C\u8BF7\u91CD\u8BD5\u3002",
12364
+ timeout: "\u4E3B\u673A\u5C4F\u5E55\u4ECD\u9501\u5B9A\uFF0C\u7CFB\u7EDF\u672A\u63A5\u53D7\u89E3\u9501\u8F93\u5165\uFF1B\u8BF7\u68C0\u67E5\u8F85\u52A9\u529F\u80FD\u6743\u9650\u548C\u9501\u5C4F\u754C\u9762\u540E\u91CD\u8BD5\u3002",
12365
+ password: "\u4E3B\u673A\u5BC6\u7801\u683C\u5F0F\u65E0\u6548\u3002"
12366
+ };
12367
+ var executeScreenLock = (file, args, input, signal) => new Promise((resolve4, reject) => {
12368
+ if (signal?.aborted) {
12369
+ reject(new ScreenLockError("\u8FDE\u63A5\u5DF2\u5173\u95ED\uFF0C\u5C4F\u5E55\u89E3\u9501\u5DF2\u53D6\u6D88\u3002"));
12370
+ return;
12371
+ }
12372
+ const child = spawn2(file, args, { stdio: ["pipe", "pipe", "pipe"], signal });
12373
+ let output = "", diagnostic = "", timedOut = false;
12374
+ const timer = setTimeout(() => {
12375
+ timedOut = true;
12376
+ child.kill("SIGKILL");
12377
+ }, args[0] === "unlock" ? 45e3 : 15e3);
12378
+ child.stdout.on("data", (chunk) => {
12379
+ output += String(chunk);
12380
+ if (output.length > 4096) child.kill("SIGKILL");
12381
+ });
12382
+ child.stderr.on("data", (chunk) => {
12383
+ diagnostic = (diagnostic + String(chunk)).slice(0, 4096);
12384
+ });
12385
+ child.stdin.on("error", () => {
12386
+ });
12387
+ child.on("error", () => {
12388
+ clearTimeout(timer);
12389
+ reject(new ScreenLockError(signal?.aborted ? "\u8FDE\u63A5\u5DF2\u5173\u95ED\uFF0C\u5C4F\u5E55\u89E3\u9501\u5DF2\u53D6\u6D88\u3002" : "\u5C4F\u5E55\u89E3\u9501\u4F9D\u8D56\u4E0D\u53EF\u7528\uFF1BmacOS \u9700\u8981 Xcode Command Line Tools\uFF0CLinux \u9700\u8981 systemd loginctl\u3002"));
12390
+ });
12391
+ child.on("close", (code) => {
12392
+ clearTimeout(timer);
12393
+ if (signal?.aborted) reject(new ScreenLockError("\u8FDE\u63A5\u5DF2\u5173\u95ED\uFF0C\u5C4F\u5E55\u89E3\u9501\u5DF2\u53D6\u6D88\u3002"));
12394
+ else if (code !== 0 || output.length > 4096) reject(new ScreenLockError(timedOut ? "\u5C4F\u5E55\u89E3\u9501\u64CD\u4F5C\u8D85\u65F6\uFF0C\u8BF7\u68C0\u67E5\u4E3B\u673A\u72B6\u6001\u540E\u91CD\u8BD5\u3002" : nativeErrors[diagnostic.trim()] ?? "\u5C4F\u5E55\u89E3\u9501\u64CD\u4F5C\u5931\u8D25\uFF0C\u8BF7\u68C0\u67E5\u4E3B\u673A\u7684\u56FE\u5F62\u4F1A\u8BDD\u3001\u7CFB\u7EDF\u6743\u9650\u53CA\u4F9D\u8D56\u3002"));
12395
+ else resolve4(output);
12396
+ });
12397
+ child.stdin.end(input);
12398
+ });
12399
+ var PlatformScreenLock = class {
12400
+ constructor(stateDirectory, system = process.platform, environment = process.env, execute3 = executeScreenLock, authenticate = verifyHostPassword, account = userInfo2()) {
12401
+ this.stateDirectory = stateDirectory;
12402
+ this.system = system;
12403
+ this.environment = environment;
12404
+ this.execute = execute3;
12405
+ this.authenticate = authenticate;
12406
+ this.account = account;
12407
+ }
12408
+ helper;
12409
+ failures = 0;
12410
+ blockedUntil = 0;
12411
+ macHelper() {
12412
+ if (this.helper) return this.helper;
12413
+ this.helper = (async () => {
12414
+ await mkdir5(this.stateDirectory, { recursive: true, mode: 448 });
12415
+ const target = join5(this.stateDirectory, "octopus-screen-unlock");
12416
+ const source = fileURLToPath3(new URL("../native/screen-unlock-macos.m", import.meta.url));
12417
+ const sourceInfo = await stat6(source), existing = await stat6(target).catch(() => null);
12418
+ if (existing?.isFile() && existing.mtimeMs >= sourceInfo.mtimeMs && (existing.mode & 63) === 0) return target;
12419
+ const temporary = `${target}.${randomUUID7()}`;
12420
+ try {
12421
+ await this.execute("/usr/bin/clang", ["-O2", "-fobjc-arc", "-mmacosx-version-min=12.3", "-framework", "Foundation", "-framework", "AppKit", "-framework", "ApplicationServices", source, "-o", temporary]);
12422
+ await chmod3(temporary, 448);
12423
+ await rename4(temporary, target);
12424
+ return target;
12425
+ } finally {
12426
+ await rm(temporary, { force: true });
12427
+ }
12428
+ })();
12429
+ this.helper.catch(() => {
12430
+ this.helper = void 0;
12431
+ });
12432
+ return this.helper;
12433
+ }
12434
+ async linuxSession(signal) {
12435
+ let ids;
12436
+ if (this.environment.XDG_SESSION_ID) ids = [this.environment.XDG_SESSION_ID];
12437
+ else ids = (await this.execute("loginctl", ["--no-ask-password", "list-sessions", "--no-legend"], void 0, signal)).trim().split("\n").map((line) => line.trim().split(/\s+/)[0]).filter(Boolean);
12438
+ const sessions = [];
12439
+ for (const id of ids) {
12440
+ if (!/^[a-zA-Z0-9_-]+$/.test(id)) continue;
12441
+ const text = await this.execute("loginctl", ["--no-ask-password", "show-session", id, "-p", "User", "-p", "Type", "-p", "Active", "-p", "LockedHint"], void 0, signal);
12442
+ const properties = Object.fromEntries(text.trim().split("\n").map((line) => line.split("=")));
12443
+ if (properties.User !== String(this.account.uid) || !["x11", "wayland"].includes(properties.Type) || properties.Active !== "yes") continue;
12444
+ if (!["yes", "no"].includes(properties.LockedHint)) throw new ScreenLockError("\u684C\u9762\u672A\u62A5\u544A\u6709\u6548\u7684\u9501\u5C4F\u72B6\u6001\uFF0C\u65E0\u6CD5\u786E\u8BA4\u89E3\u9501\u3002");
12445
+ sessions.push({ id, locked: properties.LockedHint === "yes" });
12446
+ }
12447
+ if (sessions.length !== 1) throw new ScreenLockError("\u65E0\u6CD5\u786E\u5B9A Proxy \u7528\u6237\u7684\u552F\u4E00\u6D3B\u52A8\u56FE\u5F62\u4F1A\u8BDD\uFF0C\u8BF7\u8BBE\u7F6E\u6B63\u786E\u7684 XDG_SESSION_ID\u3002");
12448
+ return sessions[0];
12449
+ }
12450
+ parseStatus(text) {
12451
+ let data;
12452
+ try {
12453
+ data = JSON.parse(text);
12454
+ } catch {
12455
+ throw new ScreenLockError("\u4E3B\u673A\u8FD4\u56DE\u7684\u9501\u5C4F\u72B6\u6001\u65E0\u6548\u3002");
12456
+ }
12457
+ if (!data || typeof data.locked !== "boolean" || typeof data.canUnlock !== "boolean") throw new ScreenLockError("\u4E3B\u673A\u8FD4\u56DE\u7684\u9501\u5C4F\u72B6\u6001\u65E0\u6548\u3002");
12458
+ return { locked: data.locked, canUnlock: data.canUnlock, account: this.account.username, ...!data.canUnlock ? { message: nativeErrors.permission } : {} };
12459
+ }
12460
+ async status() {
12461
+ if (this.system === "darwin") return this.parseStatus(await this.execute(await this.macHelper(), ["status"]));
12462
+ if (this.system === "linux") return { locked: (await this.linuxSession()).locked, canUnlock: true, account: this.account.username };
12463
+ throw new ScreenLockError("\u5C4F\u5E55\u89E3\u9501\u4EC5\u652F\u6301 macOS 12.3 \u53CA\u4EE5\u4E0A\u548C\u4F7F\u7528 systemd \u7684 Linux \u56FE\u5F62\u4F1A\u8BDD\u3002");
12464
+ }
12465
+ async unlock(password, signal) {
12466
+ const check = () => {
12467
+ if (signal.aborted) throw new ScreenLockError("\u8FDE\u63A5\u5DF2\u5173\u95ED\uFF0C\u5C4F\u5E55\u89E3\u9501\u5DF2\u53D6\u6D88\u3002");
12468
+ };
12469
+ check();
12470
+ if (!password || Buffer.byteLength(password) > 1024 || /[\x00-\x1f\x7f]/.test(password)) throw new ScreenLockError("\u4E3B\u673A\u5BC6\u7801\u683C\u5F0F\u65E0\u6548\u3002");
12471
+ if (Date.now() < this.blockedUntil) throw new ScreenLockError("\u5BC6\u7801\u9A8C\u8BC1\u5931\u8D25\u6B21\u6570\u8FC7\u591A\uFF0C\u8BF7\u7B49\u5F85 30 \u79D2\u540E\u91CD\u8BD5\u3002");
12472
+ const status = await this.status();
12473
+ check();
12474
+ if (!status.locked) return status;
12475
+ if (!status.canUnlock) throw new ScreenLockError(status.message ?? "\u4E3B\u673A\u4E0D\u652F\u6301\u5C4F\u5E55\u89E3\u9501\u3002");
12476
+ let valid;
12477
+ try {
12478
+ valid = await this.authenticate(this.stateDirectory, this.account.username, password);
12479
+ } catch {
12480
+ throw new ScreenLockError("\u65E0\u6CD5\u9A8C\u8BC1\u4E3B\u673A\u5BC6\u7801\uFF0C\u8BF7\u68C0\u67E5\u4E3B\u673A\u8BA4\u8BC1\u8F85\u52A9\u7A0B\u5E8F\u3002");
12481
+ }
12482
+ check();
12483
+ if (!valid) {
12484
+ if (++this.failures >= 3) {
12485
+ this.blockedUntil = Date.now() + 3e4;
12486
+ this.failures = 0;
12487
+ }
12488
+ throw new ScreenLockError("\u4E3B\u673A\u767B\u5F55\u5BC6\u7801\u9519\u8BEF\uFF0C\u8BF7\u91CD\u65B0\u8F93\u5165\u3002");
12489
+ }
12490
+ this.failures = 0;
12491
+ if (this.system === "darwin") {
12492
+ const helper = await this.macHelper();
12493
+ check();
12494
+ const bytes = Buffer.from(password, "utf8"), input = Buffer.alloc(bytes.length + 2);
12495
+ input.writeUInt16BE(bytes.length);
12496
+ bytes.copy(input, 2);
12497
+ bytes.fill(0);
12498
+ let result;
12499
+ try {
12500
+ result = this.parseStatus(await this.execute(helper, ["unlock"], input, signal));
12501
+ } finally {
12502
+ input.fill(0);
12503
+ }
12504
+ check();
12505
+ if (result.locked) throw new ScreenLockError(nativeErrors.timeout);
12506
+ return result;
12507
+ }
12508
+ const session = await this.linuxSession(signal);
12509
+ check();
12510
+ if (session.locked) await this.execute("loginctl", ["--no-ask-password", "unlock-session", session.id], void 0, signal);
12511
+ for (let i = 0; i < 20; i++) {
12512
+ check();
12513
+ const current = await this.linuxSession(signal);
12514
+ if (current.id !== session.id) throw new ScreenLockError("\u4E3B\u673A\u6D3B\u52A8\u4F1A\u8BDD\u5DF2\u53D8\u5316\uFF0C\u5DF2\u505C\u6B62\u89E3\u9501\u3002");
12515
+ if (!current.locked) return { ...status, locked: false };
12516
+ await delay(250, void 0, { signal }).catch(() => check());
12517
+ }
12518
+ throw new ScreenLockError("\u684C\u9762\u9501\u5C4F\u7A0B\u5E8F\u672A\u54CD\u5E94\u89E3\u9501\u8BF7\u6C42\uFF0C\u8BF7\u786E\u8BA4\u5B83\u652F\u6301 systemd-logind \u7684 Unlock \u4FE1\u53F7\u3002");
12519
+ }
12520
+ };
12521
+
12274
12522
  // src/journal.ts
12275
12523
  var Journal = class {
12276
12524
  constructor(directory) {
@@ -12286,8 +12534,8 @@ var Journal = class {
12286
12534
  return active.result;
12287
12535
  }
12288
12536
  const run4 = async () => {
12289
- const path = join4(this.directory, `${command.id}.json`);
12290
- const readOnly = isReadOperation(command.operation) || isAuthentication(command.operation);
12537
+ const path = join6(this.directory, `${command.id}.json`);
12538
+ const readOnly = command.operation.startsWith("desktop.") || isReadOperation(command.operation) || isAuthentication(command.operation);
12291
12539
  if (!readOnly) {
12292
12540
  const old = await readJson(path);
12293
12541
  if (old) {
@@ -12300,7 +12548,7 @@ var Journal = class {
12300
12548
  try {
12301
12549
  response = { type: "response", id: command.id, botId: command.botId, status: "completed", data: await action() };
12302
12550
  } catch (error) {
12303
- response = this.failure(command, isAuthentication(command.operation) ? "\u8BA4\u8BC1\u64CD\u4F5C\u5931\u8D25\uFF0C\u8BF7\u68C0\u67E5\u7F16\u7801\u5DE5\u5177\u7684\u767B\u5F55\u72B6\u6001\u540E\u91CD\u8BD5" : error instanceof Error ? error.message : "Operation failed");
12551
+ response = this.failure(command, command.operation === "desktop.unlock" ? error instanceof ScreenLockError ? error.message : "\u5C4F\u5E55\u89E3\u9501\u5931\u8D25\uFF0C\u8BF7\u68C0\u67E5\u4E3B\u673A\u72B6\u6001\u4E0E\u6743\u9650\u540E\u91CD\u8BD5\u3002" : isAuthentication(command.operation) ? "\u8BA4\u8BC1\u64CD\u4F5C\u5931\u8D25\uFF0C\u8BF7\u68C0\u67E5\u7F16\u7801\u5DE5\u5177\u7684\u767B\u5F55\u72B6\u6001\u540E\u91CD\u8BD5" : error instanceof Error ? error.message : "Operation failed");
12304
12552
  if (!readOnly && error instanceof Error && ["TimeoutError", "AbortError", "UnknownResultError"].includes(error.name)) response.status = "unknown";
12305
12553
  }
12306
12554
  if (!readOnly) await atomicJson(path, { hash, response, finishedAt: (/* @__PURE__ */ new Date()).toISOString() });
@@ -12322,6 +12570,7 @@ var Journal = class {
12322
12570
  return result;
12323
12571
  }
12324
12572
  queueKey(c) {
12573
+ if (c.operation.startsWith("desktop.")) return void 0;
12325
12574
  if (isReadOperation(c.operation) || isAuthentication(c.operation)) return void 0;
12326
12575
  const [group] = c.operation.split(".");
12327
12576
  if (c.operation === "sessions.abort") return JSON.stringify(["abort", c.projectId, c.sessionId]);
@@ -12406,8 +12655,8 @@ function pageHistory(input, payload) {
12406
12655
 
12407
12656
  // src/artifacts.ts
12408
12657
  import { createHash as createHash5 } from "node:crypto";
12409
- import { join as join5 } from "node:path";
12410
- import { readFile as readFile4, writeFile as writeFile2, mkdir as mkdir4, stat as stat5 } from "node:fs/promises";
12658
+ import { join as join7 } from "node:path";
12659
+ import { readFile as readFile4, writeFile as writeFile2, mkdir as mkdir6, stat as stat7 } from "node:fs/promises";
12411
12660
  var Artifacts = class {
12412
12661
  constructor(workspace, directory, emit) {
12413
12662
  this.workspace = workspace;
@@ -12423,7 +12672,7 @@ var Artifacts = class {
12423
12672
  }
12424
12673
  index(project) {
12425
12674
  this.workspace.project(project);
12426
- return join5(this.directory, project, "index.json");
12675
+ return join7(this.directory, project, "index.json");
12427
12676
  }
12428
12677
  async list(project) {
12429
12678
  return Object.values(await readJson(this.index(project)) ?? {});
@@ -12461,9 +12710,9 @@ var Artifacts = class {
12461
12710
  changed = true;
12462
12711
  }
12463
12712
  if (previous?.hash !== hash || previous.deleted) {
12464
- const dir = join5(this.directory, project, "blobs");
12465
- await mkdir4(dir, { recursive: true, mode: 448 });
12466
- await writeFile2(join5(dir, hash), data, { mode: 384 });
12713
+ const dir = join7(this.directory, project, "blobs");
12714
+ await mkdir6(dir, { recursive: true, mode: 448 });
12715
+ await writeFile2(join7(dir, hash), data, { mode: 384 });
12467
12716
  record2.revisions.push({ hash, timestamp: (/* @__PURE__ */ new Date()).toISOString() });
12468
12717
  changed = true;
12469
12718
  }
@@ -12479,7 +12728,7 @@ var Artifacts = class {
12479
12728
  }
12480
12729
  for (const item of Object.values(index)) if (!seen.has(item.path) && !item.revisions.at(-1)?.deleted) {
12481
12730
  try {
12482
- await stat5(await this.workspace.path(project, item.path));
12731
+ await stat7(await this.workspace.path(project, item.path));
12483
12732
  } catch (e) {
12484
12733
  if (e.code === "ENOENT") {
12485
12734
  item.revisions.push({ hash: "", timestamp: (/* @__PURE__ */ new Date()).toISOString(), deleted: true });
@@ -12499,7 +12748,7 @@ var Artifacts = class {
12499
12748
  }
12500
12749
  async read(project, path, hash) {
12501
12750
  if (!/^[a-f0-9]{64}$/.test(hash) || !(await this.history(project, path)).some((r) => r.hash === hash)) throw new Error("Unknown revision");
12502
- return readFile4(join5(this.directory, project, "blobs", hash), "utf8");
12751
+ return readFile4(join7(this.directory, project, "blobs", hash), "utf8");
12503
12752
  }
12504
12753
  associate(project, path, sessionId) {
12505
12754
  return this.serial(project, () => this.associateUnlocked(project, path, sessionId));
@@ -12516,82 +12765,9 @@ var Artifacts = class {
12516
12765
  // src/secure-chat.ts
12517
12766
  import WebSocket from "ws";
12518
12767
  import { Duplex } from "node:stream";
12519
- import { createPublicKey, randomUUID as randomUUID7, verify } from "node:crypto";
12520
- import { userInfo as userInfo2 } from "node:os";
12768
+ import { createPublicKey, randomUUID as randomUUID8, verify } from "node:crypto";
12769
+ import { userInfo as userInfo3 } from "node:os";
12521
12770
  import { TLSSocket, createSecureContext } from "node:tls";
12522
-
12523
- // src/host-auth.ts
12524
- import { spawn } from "node:child_process";
12525
- import { chmod as chmod2, mkdir as mkdir5, rename as rename3, stat as stat6, unlink as unlink4 } from "node:fs/promises";
12526
- import { userInfo, platform as platform2 } from "node:os";
12527
- import { randomUUID as randomUUID6 } from "node:crypto";
12528
- import { join as join6 } from "node:path";
12529
- import { fileURLToPath as fileURLToPath2 } from "node:url";
12530
- function helperPath(stateDirectory) {
12531
- return join6(stateDirectory, "octopus-host-auth");
12532
- }
12533
- async function ensureHostAuth(stateDirectory) {
12534
- await mkdir5(stateDirectory, { recursive: true, mode: 448 });
12535
- const target = helperPath(stateDirectory);
12536
- const system = platform2();
12537
- if (system !== "darwin" && system !== "linux") throw new Error("Host authentication is supported only on macOS and Linux");
12538
- const mac = system === "darwin";
12539
- const source = fileURLToPath2(new URL(mac ? "../native/auth-macos.m" : "../native/auth-linux.c", import.meta.url));
12540
- const sourceInfo = await stat6(source);
12541
- const existing = await stat6(target).catch(() => null);
12542
- if (existing?.isFile() && existing.mtimeMs >= sourceInfo.mtimeMs && (existing.mode & 63) === 0) return target;
12543
- const temporary = `${target}.${randomUUID6()}`;
12544
- const args = mac ? ["-O2", "-framework", "Foundation", "-framework", "OpenDirectory", source, "-o", target] : ["-O2", source, "-o", temporary, "-lpam"];
12545
- if (mac) args[args.length - 1] = temporary;
12546
- const compiler = spawn(mac ? "clang" : "cc", args, { stdio: ["ignore", "ignore", "pipe"] });
12547
- let diagnostics = "";
12548
- compiler.stderr.on("data", (chunk) => {
12549
- diagnostics += String(chunk).slice(0, 4096);
12550
- });
12551
- const exit = await new Promise((resolve4, reject) => {
12552
- compiler.on("error", reject);
12553
- compiler.on("exit", (code) => resolve4(code ?? 1));
12554
- });
12555
- if (exit !== 0) {
12556
- await unlink4(temporary).catch(() => {
12557
- });
12558
- throw new Error(`Host authentication helper build failed: ${diagnostics.trim()}`);
12559
- }
12560
- try {
12561
- await chmod2(temporary, 448);
12562
- await rename3(temporary, target);
12563
- return target;
12564
- } finally {
12565
- await unlink4(temporary).catch(() => {
12566
- });
12567
- }
12568
- }
12569
- async function verifyHostPassword(stateDirectory, username, password) {
12570
- if (username !== userInfo().username || !password || Buffer.byteLength(password) > 1024) return false;
12571
- const path = await ensureHostAuth(stateDirectory);
12572
- const child = spawn(path, [], { stdio: ["pipe", "pipe", "ignore"] });
12573
- const bytes = Buffer.from(password, "utf8");
12574
- const header2 = Buffer.alloc(2);
12575
- header2.writeUInt16BE(bytes.length);
12576
- child.stdin.end(Buffer.concat([header2, bytes]));
12577
- bytes.fill(0);
12578
- let output = "";
12579
- child.stdout.on("data", (chunk) => {
12580
- output += String(chunk).slice(0, 16);
12581
- });
12582
- const timer = setTimeout(() => child.kill("SIGKILL"), 1e4);
12583
- try {
12584
- const code = await new Promise((resolve4, reject) => {
12585
- child.on("error", reject);
12586
- child.on("exit", (value) => resolve4(value ?? 1));
12587
- });
12588
- return code === 0 && output.trim() === "OK";
12589
- } finally {
12590
- clearTimeout(timer);
12591
- }
12592
- }
12593
-
12594
- // src/secure-chat.ts
12595
12771
  var OPEN = 1;
12596
12772
  var DATA = 2;
12597
12773
  var CLOSE = 3;
@@ -12649,13 +12825,13 @@ var SecureChat = class {
12649
12825
  }
12650
12826
  outer;
12651
12827
  sessions = /* @__PURE__ */ new Map();
12652
- epoch = randomUUID7();
12828
+ epoch = randomUUID8();
12653
12829
  sequence = 0;
12654
12830
  events = [];
12655
12831
  eventBytes = 0;
12656
12832
  stopping = false;
12657
12833
  context;
12658
- serviceAccount = userInfo2().username;
12834
+ serviceAccount = userInfo3().username;
12659
12835
  timer;
12660
12836
  start() {
12661
12837
  this.connect();
@@ -12861,7 +13037,7 @@ var SecureChat = class {
12861
13037
  };
12862
13038
 
12863
13039
  // src/proxy-snapshot.ts
12864
- import { randomUUID as randomUUID8 } from "node:crypto";
13040
+ import { randomUUID as randomUUID9 } from "node:crypto";
12865
13041
  import { execFile as execFile3 } from "node:child_process";
12866
13042
  import { promisify as promisify3 } from "node:util";
12867
13043
  var execute2 = promisify3(execFile3);
@@ -12915,7 +13091,7 @@ async function buildProxySnapshot(config, runtime, adapter, requestedProjectId)
12915
13091
  let modelCatalogStatus = capabilities.includes("configuration.models") ? "unavailable" : "unsupported";
12916
13092
  if (status.healthy === true && projectId && capabilities.includes("configuration.models")) {
12917
13093
  try {
12918
- const command = { type: "command", id: randomUUID8(), botId: config.botId, operation: "configuration.models", projectId, payload: {} };
13094
+ const command = { type: "command", id: randomUUID9(), botId: config.botId, operation: "configuration.models", projectId, payload: {} };
12919
13095
  models.push(...publicModels(await adapter.dispatch(command)));
12920
13096
  modelCatalogStatus = models.length ? "available" : "unavailable";
12921
13097
  } catch {
@@ -12942,11 +13118,11 @@ async function buildProxySnapshot(config, runtime, adapter, requestedProjectId)
12942
13118
  }
12943
13119
 
12944
13120
  // src/media.ts
12945
- import { createHash as createHash6, randomUUID as randomUUID9 } from "node:crypto";
13121
+ import { createHash as createHash6, randomUUID as randomUUID10 } from "node:crypto";
12946
13122
  import { constants as constants3 } from "node:fs";
12947
13123
  import { open as open4, unlink as unlink5, realpath as realpath3 } from "node:fs/promises";
12948
13124
  import { basename as basename3, isAbsolute as isAbsolute3, relative as relative3, resolve as resolve3, sep as sep2 } from "node:path";
12949
- import { fileURLToPath as fileURLToPath3 } from "node:url";
13125
+ import { fileURLToPath as fileURLToPath4 } from "node:url";
12950
13126
 
12951
13127
  // src/screenshot-delivery.ts
12952
13128
  var instructions = "\n\n[Octopus screenshot delivery]\nFor requested UI tests/screenshots, run the real application with the available test tools. Save each requested key-step or failure screenshot under the authorized project (prefer test-results/screenshots/ with a unique filename). After saving, immediately report it as ![step and result](relative/project/path.png) in your progress or final response. Octopus Proxy will attach the actual file to Claw. Never claim a screenshot or test succeeded without producing it; explain environment/tool blockers. Do not substitute generated mockups for test screenshots.\n[/Octopus screenshot delivery]";
@@ -13063,7 +13239,7 @@ var RemoteMedia = class {
13063
13239
  } else {
13064
13240
  let path = value.path ?? value.localPath ?? value.artifactUri ?? source.path ?? source.localPath ?? url;
13065
13241
  if (typeof path !== "string") throw new Error("Image has no readable data or project path");
13066
- if (path.startsWith("file:")) path = fileURLToPath3(path);
13242
+ if (path.startsWith("file:")) path = fileURLToPath4(path);
13067
13243
  if (/^[a-z][a-z0-9+.-]*:/i.test(path)) throw new Error("Only images inside the authorized project can be returned");
13068
13244
  if (isAbsolute3(path)) {
13069
13245
  const root = await this.workspace.root(project);
@@ -13159,7 +13335,7 @@ var RemoteMedia = class {
13159
13335
  if (bytes.length === 0 || bytes.length > this.imageLimit()) throw new Error("Image is empty or exceeds size limit");
13160
13336
  const extension = { "image/png": "png", "image/jpeg": "jpg", "image/gif": "gif", "image/webp": "webp", "application/pdf": "pdf" }[mime] ?? "bin";
13161
13337
  const directory = await this.workspace.attachmentDirectory(project, "media", sha256), path = directory + "/asset." + extension;
13162
- const temp = await this.workspace.path(project, directory + "/.octopus-media-" + randomUUID9(), true);
13338
+ const temp = await this.workspace.path(project, directory + "/.octopus-media-" + randomUUID10(), true);
13163
13339
  const file = await open4(temp, constants3.O_WRONLY | constants3.O_CREAT | constants3.O_EXCL | constants3.O_NOFOLLOW, 384);
13164
13340
  try {
13165
13341
  await file.writeFile(bytes);
@@ -14577,7 +14753,7 @@ function applyEdits(text, edits) {
14577
14753
 
14578
14754
  // src/project-config.ts
14579
14755
  import { readFile as readFile5, writeFile as writeFile3, unlink as unlink6 } from "node:fs/promises";
14580
- import { createHash as createHash7, randomUUID as randomUUID10 } from "node:crypto";
14756
+ import { createHash as createHash7, randomUUID as randomUUID11 } from "node:crypto";
14581
14757
  var ProjectConfig = class {
14582
14758
  constructor(workspace, request) {
14583
14759
  this.workspace = workspace;
@@ -14628,7 +14804,7 @@ var ProjectConfig = class {
14628
14804
  if (!this.validator(parse2(updated))) throw new Error("Merged OpenCode configuration is invalid: " + JSON.stringify(this.validator.errors));
14629
14805
  if (exists) await this.workspace.write(project, name, updated, createHash7("sha256").update(original).digest("hex"));
14630
14806
  else {
14631
- const temporary = await this.workspace.path(project, `.octopus-config-${randomUUID10()}.tmp`, true);
14807
+ const temporary = await this.workspace.path(project, `.octopus-config-${randomUUID11()}.tmp`, true);
14632
14808
  try {
14633
14809
  await writeFile3(temporary, updated, { mode: 384, flag: "wx" });
14634
14810
  await this.workspace.publish(project, temporary, name);
@@ -14799,13 +14975,16 @@ var OpenCodeAdapter = class {
14799
14975
  }
14800
14976
  async models(project) {
14801
14977
  const models = await this.data(project, "GET", "/api/model");
14978
+ const config = await this.data(project, "GET", "/api/config").catch(() => void 0);
14979
+ const layers = Array.isArray(config) ? config : [config];
14980
+ const configured = layers.map((layer) => layer?.config?.model ?? layer?.model).filter((value) => typeof value === "string").at(-1);
14802
14981
  const providers = /* @__PURE__ */ new Map();
14803
14982
  for (const model of Array.isArray(models) ? models : []) {
14804
14983
  if (model.enabled === false || model.status === "deprecated") continue;
14805
14984
  const modelId = model.id ?? model.modelID;
14806
14985
  if (typeof model.providerID !== "string" || typeof modelId !== "string") continue;
14807
14986
  const provider = providers.get(model.providerID) ?? { id: model.providerID, models: {} };
14808
- provider.models[modelId] = { ...model, id: modelId };
14987
+ provider.models[modelId] = { ...model, id: modelId, isDefault: configured ? configured === `${model.providerID}/${modelId}` : model.isDefault === true };
14809
14988
  providers.set(model.providerID, provider);
14810
14989
  }
14811
14990
  return redact({ providers: [...providers.values()] });
@@ -15083,11 +15262,11 @@ function redact(value) {
15083
15262
  }
15084
15263
 
15085
15264
  // src/runtime.ts
15086
- import { spawn as spawn2 } from "node:child_process";
15265
+ import { spawn as spawn3 } from "node:child_process";
15087
15266
  import { createHash as createHash8 } from "node:crypto";
15088
- import { mkdir as mkdir6, open as open5, readFile as readFile6, rm, writeFile as writeFile4 } from "node:fs/promises";
15267
+ import { mkdir as mkdir7, open as open5, readFile as readFile6, rm as rm2, writeFile as writeFile4 } from "node:fs/promises";
15089
15268
  import { createServer } from "node:net";
15090
- import { join as join7 } from "node:path";
15269
+ import { join as join8 } from "node:path";
15091
15270
  var Runtime = class {
15092
15271
  constructor(config) {
15093
15272
  this.config = config;
@@ -15103,7 +15282,7 @@ var Runtime = class {
15103
15282
  return this.endpoint;
15104
15283
  }
15105
15284
  get endpointStatePath() {
15106
- return join7(this.config.stateDirectory, "opencode-endpoint.json");
15285
+ return join8(this.config.stateDirectory, "opencode-endpoint.json");
15107
15286
  }
15108
15287
  get passwordHash() {
15109
15288
  return createHash8("sha256").update(this.config.tool.password).digest("hex");
@@ -15165,13 +15344,13 @@ var Runtime = class {
15165
15344
  throw new Error(`OpenCode \u5730\u5740 ${url.origin} \u5DF2\u6709 HTTP \u670D\u52A1\uFF0C\u4F46\u5065\u5EB7\u68C0\u67E5\u5931\u8D25\uFF08HTTP ${response.status}\uFF09\uFF1B\u8BF7\u68C0\u67E5\u670D\u52A1\u5BC6\u7801\u6216\u7AEF\u53E3`);
15166
15345
  }
15167
15346
  }
15168
- await mkdir6(this.config.stateDirectory, { recursive: true, mode: 448 });
15169
- const logPath = join7(this.config.stateDirectory, "opencode-serve.log");
15347
+ await mkdir7(this.config.stateDirectory, { recursive: true, mode: 448 });
15348
+ const logPath = join8(this.config.stateDirectory, "opencode-serve.log");
15170
15349
  const log = await open5(logPath, "a", 384);
15171
15350
  const env = { ...process.env, OPENCODE_SERVER_PASSWORD: this.config.tool.password, OPENCODE_SERVER_USERNAME: "opencode" };
15172
15351
  let child;
15173
15352
  try {
15174
- child = spawn2(this.config.tool.command, ["serve", "--hostname", url.hostname === "[::1]" ? "::1" : url.hostname, "--port", url.port || "80"], {
15353
+ child = spawn3(this.config.tool.command, ["serve", "--hostname", url.hostname === "[::1]" ? "::1" : url.hostname, "--port", url.port || "80"], {
15175
15354
  cwd: this.config.projects[0].directory,
15176
15355
  env,
15177
15356
  detached: true,
@@ -15209,7 +15388,7 @@ var Runtime = class {
15209
15388
  await new Promise((resolve4) => setTimeout(resolve4, 250));
15210
15389
  }
15211
15390
  if (child.exitCode === null && child.signalCode === null) child.kill("SIGTERM");
15212
- await rm(this.endpointStatePath, { force: true }).catch(() => {
15391
+ await rm2(this.endpointStatePath, { force: true }).catch(() => {
15213
15392
  });
15214
15393
  this.endpoint = this.config.tool.baseUrl;
15215
15394
  throw new Error(`OpenCode \u542F\u52A8\u5931\u8D25\u6216\u5065\u5EB7\u68C0\u67E5\u8D85\u65F6\uFF1B\u8BF7\u68C0\u67E5 ${logPath}\u3001\u670D\u52A1\u5730\u5740\u548C\u5BC6\u7801`);
@@ -15258,24 +15437,24 @@ var Runtime = class {
15258
15437
 
15259
15438
  // src/tools/zcode-env.ts
15260
15439
  import { existsSync } from "node:fs";
15261
- import { dirname as dirname4, join as join8 } from "node:path";
15440
+ import { dirname as dirname4, join as join9 } from "node:path";
15262
15441
  function zcodeEnvironment(command) {
15263
15442
  const env = { ...process.env };
15264
15443
  if (!env.ZCODE_BUILTIN_PROVIDER_CONFIG_FILE) {
15265
- const bundled = join8(dirname4(command), "..", "config", "provider", "zcode-builtin.json");
15444
+ const bundled = join9(dirname4(command), "..", "config", "provider", "zcode-builtin.json");
15266
15445
  if (existsSync(bundled)) env.ZCODE_BUILTIN_PROVIDER_CONFIG_FILE = bundled;
15267
15446
  }
15268
15447
  return env;
15269
15448
  }
15270
15449
 
15271
15450
  // src/tools/stdio-runtime.ts
15272
- import { spawn as spawn3, execFile as execFile4 } from "node:child_process";
15451
+ import { spawn as spawn4, execFile as execFile4 } from "node:child_process";
15273
15452
  import { promisify as promisify4 } from "node:util";
15274
- import { mkdir as mkdir7, open as open6 } from "node:fs/promises";
15275
- import { join as join9 } from "node:path";
15453
+ import { mkdir as mkdir8, open as open6 } from "node:fs/promises";
15454
+ import { join as join10 } from "node:path";
15276
15455
 
15277
15456
  // src/tools/line-rpc.ts
15278
- import { randomUUID as randomUUID11 } from "node:crypto";
15457
+ import { randomUUID as randomUUID12 } from "node:crypto";
15279
15458
  import { crc32 } from "node:zlib";
15280
15459
  import { StringDecoder } from "node:string_decoder";
15281
15460
  function interrupted(message) {
@@ -15460,7 +15639,7 @@ var LineRpc = class {
15460
15639
  }
15461
15640
  };
15462
15641
  function wrapEnvelope(frame) {
15463
- return { wireVersion: 3, kind: "complete", deliveryKind: "desktop-continuous", logicalFrameId: randomUUID11(), logicalFrameOrdinal: 1, topic: "outbound", subscriptionId: "outbound", frame };
15642
+ return { wireVersion: 3, kind: "complete", deliveryKind: "desktop-continuous", logicalFrameId: randomUUID12(), logicalFrameOrdinal: 1, topic: "outbound", subscriptionId: "outbound", frame };
15464
15643
  }
15465
15644
 
15466
15645
  // src/tools/stdio-runtime.ts
@@ -15636,12 +15815,12 @@ ${bytes.toString("utf8")}`);
15636
15815
  if (inFlight) return inFlight;
15637
15816
  if (this.children.has(projectId)) return;
15638
15817
  const spawnOnce = (async () => {
15639
- const logPath = join9(this.config.stateDirectory, `${this.kind}-${projectId}.log`);
15640
- await mkdir7(this.config.stateDirectory, { recursive: true, mode: 448 });
15818
+ const logPath = join10(this.config.stateDirectory, `${this.kind}-${projectId}.log`);
15819
+ await mkdir8(this.config.stateDirectory, { recursive: true, mode: 448 });
15641
15820
  const log = await open6(logPath, "a", 384);
15642
15821
  let child;
15643
15822
  try {
15644
- child = spawn3(this.config.tool.command, [...this.descriptor.serverArgs], { cwd: directory, env: this.kind === "zcode" ? zcodeEnvironment(this.config.tool.command) : { ...process.env }, stdio: ["pipe", "pipe", log.fd] });
15823
+ child = spawn4(this.config.tool.command, [...this.descriptor.serverArgs], { cwd: directory, env: this.kind === "zcode" ? zcodeEnvironment(this.config.tool.command) : { ...process.env }, stdio: ["pipe", "pipe", log.fd] });
15645
15824
  } catch (error) {
15646
15825
  await log.close();
15647
15826
  throw error;
@@ -15690,14 +15869,14 @@ ${bytes.toString("utf8")}`);
15690
15869
  return;
15691
15870
  }
15692
15871
  this.attempts.set(projectId, attempts);
15693
- const delay = Math.min(3e4, 1e3 * 2 ** attempts);
15872
+ const delay2 = Math.min(3e4, 1e3 * 2 ** attempts);
15694
15873
  this.timers.set(projectId, setTimeout(() => {
15695
15874
  this.timers.delete(projectId);
15696
15875
  if (!this.wanted || this.detached) return;
15697
15876
  const project = this.config.projects.find((entry) => entry.id === projectId);
15698
15877
  if (project) void this.track(projectId, project.directory).catch(() => {
15699
15878
  });
15700
- }, delay));
15879
+ }, delay2));
15701
15880
  }
15702
15881
  kill(projectId) {
15703
15882
  clearTimeout(this.timers.get(projectId));
@@ -15716,7 +15895,7 @@ ${bytes.toString("utf8")}`);
15716
15895
  };
15717
15896
 
15718
15897
  // src/tools/codex.ts
15719
- import { join as join10 } from "node:path";
15898
+ import { join as join11 } from "node:path";
15720
15899
 
15721
15900
  // src/adapter.ts
15722
15901
  var DAEMON_OPERATIONS = [
@@ -15752,14 +15931,16 @@ var DAEMON_OPERATIONS = [
15752
15931
  "terminal.attach",
15753
15932
  "desktop.list",
15754
15933
  "desktop.capture",
15755
- "desktop.read"
15934
+ "desktop.read",
15935
+ "desktop.lockStatus",
15936
+ "desktop.unlock"
15756
15937
  ];
15757
15938
  function unsupportedOperation(kind, operation) {
15758
15939
  throw new Error(`Unsupported ${kind} operation: ${operation}`);
15759
15940
  }
15760
15941
 
15761
15942
  // src/tools/codex.ts
15762
- import { randomUUID as randomUUID12 } from "node:crypto";
15943
+ import { randomUUID as randomUUID13 } from "node:crypto";
15763
15944
  var OPERATIONS = [
15764
15945
  ...DAEMON_OPERATIONS,
15765
15946
  ...AUTH_OPERATIONS,
@@ -15854,7 +16035,7 @@ var CodexAdapter = class {
15854
16035
  }
15855
16036
  onServerRequest(project, id, method, params, respond) {
15856
16037
  const detail = params ?? {};
15857
- const requestId = randomUUID12();
16038
+ const requestId = randomUUID13();
15858
16039
  const threadId = String(detail.threadId ?? "");
15859
16040
  if (["item/permissions/requestApproval", "item/commandExecution/requestApproval", "item/fileChange/requestApproval"].includes(method)) {
15860
16041
  this.approvals.set(requestId, { project, method, requestId, threadId, detail, respond });
@@ -15869,21 +16050,21 @@ var CodexAdapter = class {
15869
16050
  respond({ decision: "decline" });
15870
16051
  }
15871
16052
  async index(project) {
15872
- const stored = await readJson(join10(this.runtime.config.stateDirectory, `codex-${project}.json`));
16053
+ const stored = await readJson(join11(this.runtime.config.stateDirectory, `codex-${project}.json`));
15873
16054
  return { threadIds: stored?.threadIds ?? [] };
15874
16055
  }
15875
16056
  async remember(project, threadId) {
15876
16057
  const index = await this.index(project);
15877
16058
  if (!index.threadIds.includes(threadId)) {
15878
16059
  index.threadIds.push(threadId);
15879
- await atomicJson(join10(this.runtime.config.stateDirectory, `codex-${project}.json`), index);
16060
+ await atomicJson(join11(this.runtime.config.stateDirectory, `codex-${project}.json`), index);
15880
16061
  }
15881
16062
  }
15882
16063
  async forget(project, threadId) {
15883
16064
  const index = await this.index(project);
15884
16065
  if (index.threadIds.includes(threadId)) {
15885
16066
  index.threadIds = index.threadIds.filter((entry) => entry !== threadId);
15886
- await atomicJson(join10(this.runtime.config.stateDirectory, `codex-${project}.json`), index);
16067
+ await atomicJson(join11(this.runtime.config.stateDirectory, `codex-${project}.json`), index);
15887
16068
  }
15888
16069
  }
15889
16070
  async resume(project, threadId, rpc) {
@@ -16094,7 +16275,10 @@ var CodexAdapter = class {
16094
16275
  if (cursor && seenCursors.has(cursor)) break;
16095
16276
  if (cursor) seenCursors.add(cursor);
16096
16277
  } while (cursor && models.length < 500);
16097
- return { providers: [{ id: "codex", models: Object.fromEntries(models.map((entry) => [entry.model ?? entry.id, { id: entry.model ?? entry.id, name: entry.displayName ?? entry.id, isDefault: entry.isDefault === true }])) }] };
16278
+ const config = await rpc.request("config/read", { cwd: await this.workspace.root(project), includeLayers: false }).catch(() => void 0);
16279
+ const configured = typeof config?.config?.model === "string" ? config.config.model.trim() : void 0;
16280
+ if (configured && !models.some((entry) => (entry.model ?? entry.id) === configured)) models.push({ id: configured, model: configured, displayName: configured });
16281
+ return { providers: [{ id: "codex", models: Object.fromEntries(models.map((entry) => [entry.model ?? entry.id, { id: entry.model ?? entry.id, name: entry.displayName ?? entry.id, isDefault: configured ? (entry.model ?? entry.id) === configured : entry.isDefault === true }])) }] };
16098
16282
  }
16099
16283
  if (op === "sessions.list") {
16100
16284
  const index = await this.index(project);
@@ -16223,8 +16407,8 @@ var CodexAdapter = class {
16223
16407
  };
16224
16408
 
16225
16409
  // src/tools/zcode.ts
16226
- import { spawn as spawn4 } from "node:child_process";
16227
- import { randomUUID as randomUUID13 } from "node:crypto";
16410
+ import { spawn as spawn5 } from "node:child_process";
16411
+ import { randomUUID as randomUUID14 } from "node:crypto";
16228
16412
  import { realpath as realpath5 } from "node:fs/promises";
16229
16413
  var OPERATIONS2 = [
16230
16414
  ...DAEMON_OPERATIONS,
@@ -16279,7 +16463,7 @@ var ZcodeAdapter = class {
16279
16463
  }
16280
16464
  onServerRequest(project, id, method, params, respond, respondError) {
16281
16465
  const detail = params ?? {};
16282
- const requestId = randomUUID13();
16466
+ const requestId = randomUUID14();
16283
16467
  const sessionId = String(detail.sessionId ?? "");
16284
16468
  if (method === "session/requestRuntimePreferences") {
16285
16469
  respond({ nativeSearchEnhancementsEnabled: false });
@@ -16416,7 +16600,7 @@ var ZcodeAdapter = class {
16416
16600
  const flow = this.auth.create(project, () => {
16417
16601
  child?.kill("SIGTERM");
16418
16602
  });
16419
- child = spawn4(this.runtime.config.tool.command, ["login", "--no-browser", "--json"], { cwd: await this.workspace.root(project), env: zcodeEnvironment(this.runtime.config.tool.command), stdio: ["ignore", "pipe", "pipe"] });
16603
+ child = spawn5(this.runtime.config.tool.command, ["login", "--no-browser", "--json"], { cwd: await this.workspace.root(project), env: zcodeEnvironment(this.runtime.config.tool.command), stdio: ["ignore", "pipe", "pipe"] });
16420
16604
  let output = "", diagnostic = "";
16421
16605
  child.stdout?.on("data", (data) => {
16422
16606
  output = (output + data.toString()).slice(-65536);
@@ -16554,8 +16738,8 @@ var ZcodeAdapter = class {
16554
16738
  };
16555
16739
 
16556
16740
  // src/tools/pi.ts
16557
- import { randomUUID as randomUUID14 } from "node:crypto";
16558
- import { readFile as readFile7, readdir as readdir3, rm as rm2 } from "node:fs/promises";
16741
+ import { randomUUID as randomUUID15 } from "node:crypto";
16742
+ import { readFile as readFile7, readdir as readdir3, rm as rm3 } from "node:fs/promises";
16559
16743
  import { dirname as dirname5 } from "node:path";
16560
16744
  var OPERATIONS3 = [
16561
16745
  ...DAEMON_OPERATIONS,
@@ -16649,7 +16833,7 @@ var PiAdapter = class {
16649
16833
  }
16650
16834
  onQuestion(project, sessionId, detail) {
16651
16835
  if (!["select", "confirm", "input", "editor"].includes(String(detail.method))) return;
16652
- const id = randomUUID14();
16836
+ const id = randomUUID15();
16653
16837
  void this.rpc(project, sessionId).then((rpc) => {
16654
16838
  if (rpc.exited) return;
16655
16839
  const question = { id, project, sessionId, detail, rpc };
@@ -16736,10 +16920,11 @@ var PiAdapter = class {
16736
16920
  if (op === "configuration.models") {
16737
16921
  const rpc2 = await this.runtime.connect(project);
16738
16922
  const { models } = await rpc2.request("get_available_models");
16923
+ const state = await rpc2.request("get_state").catch(() => void 0);
16739
16924
  const providers = /* @__PURE__ */ new Map();
16740
16925
  for (const model of models) {
16741
16926
  const provider = providers.get(model.provider) ?? { id: model.provider, models: /* @__PURE__ */ Object.create(null) };
16742
- provider.models[model.id] = { id: model.id, name: model.name ?? model.id };
16927
+ provider.models[model.id] = { id: model.id, name: model.name ?? model.id, isDefault: state?.model?.provider === model.provider && state.model.id === model.id };
16743
16928
  providers.set(model.provider, provider);
16744
16929
  }
16745
16930
  return { providers: [...providers.values()] };
@@ -16754,7 +16939,7 @@ var PiAdapter = class {
16754
16939
  return sessions.sort((a, b) => b.time.updated - a.time.updated);
16755
16940
  }
16756
16941
  if (op === "sessions.create") {
16757
- const session2 = { id: randomUUID14(), title: typeof p.title === "string" && p.title.trim() ? p.title : "\u65B0\u4EFB\u52A1", directory: await this.workspace.root(project), time: { created: Date.now(), updated: Date.now() } };
16942
+ const session2 = { id: randomUUID15(), title: typeof p.title === "string" && p.title.trim() ? p.title : "\u65B0\u4EFB\u52A1", directory: await this.workspace.root(project), time: { created: Date.now(), updated: Date.now() } };
16758
16943
  try {
16759
16944
  const rpc2 = await this.rpc(project, session2.id);
16760
16945
  await rpc2.request("set_session_name", { name: session2.title });
@@ -16801,8 +16986,8 @@ var PiAdapter = class {
16801
16986
  this.deleting.add(this.key(project, id));
16802
16987
  try {
16803
16988
  await this.runtime.closeSession(project, id);
16804
- await rm2(this.runtime.sessionFile(project, id), { force: true });
16805
- await rm2(this.metadataPath(project, id));
16989
+ await rm3(this.runtime.sessionFile(project, id), { force: true });
16990
+ await rm3(this.metadataPath(project, id));
16806
16991
  this.clearQuestions(project, id);
16807
16992
  } finally {
16808
16993
  this.deleting.delete(this.key(project, id));
@@ -16852,9 +17037,9 @@ var PiAdapter = class {
16852
17037
  };
16853
17038
 
16854
17039
  // src/tools/pi-runtime.ts
16855
- import { execFile as execFile5, spawn as spawn5 } from "node:child_process";
16856
- import { mkdir as mkdir8, open as open7 } from "node:fs/promises";
16857
- import { join as join11 } from "node:path";
17040
+ import { execFile as execFile5, spawn as spawn6 } from "node:child_process";
17041
+ import { mkdir as mkdir9, open as open7 } from "node:fs/promises";
17042
+ import { join as join12 } from "node:path";
16858
17043
  import { promisify as promisify5 } from "node:util";
16859
17044
  var run3 = promisify5(execFile5);
16860
17045
  var PiRuntime = class {
@@ -16940,7 +17125,7 @@ ${bytes.toString("utf8")}`);
16940
17125
  sessionFile(projectId, sessionId) {
16941
17126
  this.project(projectId);
16942
17127
  if (!describeTool("pi").sessionPattern.test(sessionId)) throw new Error("Invalid session id");
16943
- return join11(this.config.stateDirectory, "pi-sessions", projectId, `${sessionId}.jsonl`);
17128
+ return join12(this.config.stateDirectory, "pi-sessions", projectId, `${sessionId}.jsonl`);
16944
17129
  }
16945
17130
  project(id) {
16946
17131
  const project = this.config.projects.find((entry) => entry.id === id);
@@ -16971,15 +17156,15 @@ ${bytes.toString("utf8")}`);
16971
17156
  async launch(key, projectId, sessionId, handlers, generation) {
16972
17157
  await this.kill(key);
16973
17158
  const directory = this.project(projectId).directory;
16974
- await mkdir8(join11(this.config.stateDirectory, "pi-sessions", projectId), { recursive: true, mode: 448 });
16975
- const logPath = join11(this.config.stateDirectory, `pi-${key}.log`);
17159
+ await mkdir9(join12(this.config.stateDirectory, "pi-sessions", projectId), { recursive: true, mode: 448 });
17160
+ const logPath = join12(this.config.stateDirectory, `pi-${key}.log`);
16976
17161
  const log = await open7(logPath, "a", 384);
16977
17162
  if (generation !== this.generation) {
16978
17163
  await log.close();
16979
17164
  throw new Error("Pi runtime stopped during startup");
16980
17165
  }
16981
17166
  const args = [...describeTool("pi").serverArgs, ...sessionId ? ["--session", this.sessionFile(projectId, sessionId)] : ["--no-session"]];
16982
- const child = spawn5(this.config.tool.command, args, { cwd: directory, env: { ...process.env }, stdio: ["pipe", "pipe", log.fd] });
17167
+ const child = spawn6(this.config.tool.command, args, { cwd: directory, env: { ...process.env }, stdio: ["pipe", "pipe", log.fd] });
16983
17168
  child.once("close", () => {
16984
17169
  void log.close();
16985
17170
  });
@@ -17034,7 +17219,7 @@ function createAdapter(runtime, workspace, uploads, emit) {
17034
17219
  }
17035
17220
 
17036
17221
  // src/project-registry.ts
17037
- import { realpath as realpath6, readFile as readFile8, stat as stat7 } from "node:fs/promises";
17222
+ import { realpath as realpath6, readFile as readFile8, stat as stat8 } from "node:fs/promises";
17038
17223
  import { isAbsolute as isAbsolute4 } from "node:path";
17039
17224
  var ProjectMutationSchema = ProjectSchema.extend({
17040
17225
  name: ProjectSchema.shape.name.trim().max(120),
@@ -17094,7 +17279,7 @@ var ProjectRegistry = class {
17094
17279
  const parsed = ProjectMutationSchema.parse(payload);
17095
17280
  const directory = expandHome(parsed.directory);
17096
17281
  if (!isAbsolute4(directory)) throw new Error("\u9879\u76EE\u76EE\u5F55\u5FC5\u987B\u662F Proxy \u4E3B\u673A\u4E0A\u7684\u7EDD\u5BF9\u8DEF\u5F84");
17097
- const info = await stat7(directory).catch(() => void 0);
17282
+ const info = await stat8(directory).catch(() => void 0);
17098
17283
  if (!info?.isDirectory()) throw new Error(`\u9879\u76EE\u76EE\u5F55\u4E0D\u5B58\u5728\u6216\u4E0D\u662F\u76EE\u5F55\uFF1A${directory}`);
17099
17284
  const canonical = await realpath6(directory);
17100
17285
  if (this.config.projects.some((project2) => project2.id === parsed.id)) throw new Error(`\u9879\u76EE ID \u5DF2\u5B58\u5728\uFF1A${parsed.id}`);
@@ -17114,7 +17299,7 @@ var ProjectRegistry = class {
17114
17299
  if (index < 0) throw new Error(`\u672A\u77E5\u6388\u6743\u9879\u76EE\uFF1A${parsed.id}`);
17115
17300
  const directory = expandHome(parsed.directory);
17116
17301
  if (!isAbsolute4(directory)) throw new Error("\u9879\u76EE\u76EE\u5F55\u5FC5\u987B\u662F Proxy \u4E3B\u673A\u4E0A\u7684\u7EDD\u5BF9\u8DEF\u5F84");
17117
- const info = await stat7(directory).catch(() => void 0);
17302
+ const info = await stat8(directory).catch(() => void 0);
17118
17303
  if (!info?.isDirectory()) throw new Error(`\u9879\u76EE\u76EE\u5F55\u4E0D\u5B58\u5728\u6216\u4E0D\u662F\u76EE\u5F55\uFF1A${directory}`);
17119
17304
  const canonical = await realpath6(directory);
17120
17305
  for (const project of this.config.projects) {
@@ -17161,10 +17346,10 @@ var ProjectRegistry = class {
17161
17346
  };
17162
17347
 
17163
17348
  // src/terminal.ts
17164
- import { randomUUID as randomUUID15 } from "node:crypto";
17349
+ import { randomUUID as randomUUID16 } from "node:crypto";
17165
17350
  import { access } from "node:fs/promises";
17166
17351
  import { constants as constants4 } from "node:fs";
17167
- import { platform as platform3, userInfo as userInfo3 } from "node:os";
17352
+ import { platform as platform3, userInfo as userInfo4 } from "node:os";
17168
17353
  import * as pty from "node-pty";
17169
17354
  var TerminalManager = class {
17170
17355
  constructor(workspace, emit) {
@@ -17189,13 +17374,13 @@ var TerminalManager = class {
17189
17374
  const cwd = await this.workspace.path(project, typeof p.cwd === "string" ? p.cwd : ".");
17190
17375
  const shell = process.env.SHELL || (platform3() === "darwin" ? "/bin/zsh" : "/bin/sh");
17191
17376
  await access(shell, constants4.X_OK);
17192
- const id = randomUUID15();
17377
+ const id = randomUUID16();
17193
17378
  const terminalProcess = pty.spawn(shell, [], {
17194
17379
  name: "xterm-256color",
17195
17380
  cols: 80,
17196
17381
  rows: 24,
17197
17382
  cwd,
17198
- env: { PATH: process.env.PATH || "/usr/bin:/bin", HOME: userInfo3().homedir, TERM: "xterm-256color", USER: userInfo3().username }
17383
+ env: { PATH: process.env.PATH || "/usr/bin:/bin", HOME: userInfo4().homedir, TERM: "xterm-256color", USER: userInfo4().username }
17199
17384
  });
17200
17385
  const terminal2 = { id, project, owner, process: terminalProcess, history: "" };
17201
17386
  this.terminals.set(id, terminal2);
@@ -17248,10 +17433,10 @@ var TerminalManager = class {
17248
17433
  // src/desktop.ts
17249
17434
  import { execFile as execFile6 } from "node:child_process";
17250
17435
  import { promisify as promisify6 } from "node:util";
17251
- import { createHash as createHash9, randomUUID as randomUUID16 } from "node:crypto";
17252
- import { chmod as chmod3, mkdir as mkdir9, mkdtemp, readFile as readFile9, rename as rename4, rm as rm3, stat as stat8 } from "node:fs/promises";
17253
- import { join as join12 } from "node:path";
17254
- import { fileURLToPath as fileURLToPath4 } from "node:url";
17436
+ import { createHash as createHash9, randomUUID as randomUUID17 } from "node:crypto";
17437
+ import { chmod as chmod4, mkdir as mkdir10, mkdtemp, readFile as readFile9, rename as rename5, rm as rm4, stat as stat9 } from "node:fs/promises";
17438
+ import { join as join13 } from "node:path";
17439
+ import { fileURLToPath as fileURLToPath5 } from "node:url";
17255
17440
  var DESKTOP_IMAGE_LIMIT = 20 * 1024 * 1024;
17256
17441
  var DESKTOP_CHUNK_LIMIT = 512 * 1024;
17257
17442
  var PNG_SIGNATURE = Buffer.from("89504e470d0a1a0a", "hex");
@@ -17266,7 +17451,7 @@ var executeDesktop = async (file, args) => {
17266
17451
  throw new Error(`\u684C\u9762\u622A\u5C4F\u5931\u8D25\uFF1A${detail.stderr?.trim().slice(0, 2048) || detail.message}`);
17267
17452
  }
17268
17453
  };
17269
- var DESKTOP_PERMISSION_GUIDANCE = "\u8BF7\u5728\u201C\u7CFB\u7EDF\u8BBE\u7F6E \u2192 \u9690\u79C1\u4E0E\u5B89\u5168\u6027 \u2192 \u5C4F\u5E55\u5F55\u5236\uFF08\u6216\u5C4F\u5E55\u4E0E\u7CFB\u7EDF\u97F3\u9891\u5F55\u5236\uFF09\u201D\u4E2D\u6388\u6743 octopus-desktop-capture \u6216\u7CFB\u7EDF\u5217\u51FA\u7684\u542F\u52A8\u7A0B\u5E8F\uFF0C\u7136\u540E\u91CD\u542F Proxy\u3002";
17454
+ var DESKTOP_PERMISSION_GUIDANCE = "\u8BF7\u5728\u201C\u7CFB\u7EDF\u8BBE\u7F6E \u2192 \u9690\u79C1\u4E0E\u5B89\u5168\u6027 \u2192 \u5C4F\u5E55\u5F55\u5236\uFF08\u6216\u5C4F\u5E55\u4E0E\u7CFB\u7EDF\u97F3\u9891\u5F55\u5236\uFF09\u201D\u4E2D\u542F\u7528\u7CFB\u7EDF\u6388\u6743\u5F39\u7A97\u663E\u793A\u7684\u5B9E\u9645\u7A0B\u5E8F\u3002\u7EC8\u7AEF\u8FD0\u884C\u65F6\u53EF\u80FD\u663E\u793A Terminal\u3001iTerm \u6216\u7F16\u8F91\u5668\uFF0C\u540E\u53F0\u670D\u52A1\u53EF\u80FD\u663E\u793A node\uFF1B\u4E0D\u4E00\u5B9A\u4F1A\u51FA\u73B0 octopus-desktop-capture\u3002\u7EC8\u7AEF\u6388\u6743\u4E0D\u4EE3\u8868\u540E\u53F0\u670D\u52A1\u5DF2\u6388\u6743\uFF1B\u8BF7\u5728 Claw \u70B9\u51FB\u201C\u622A\u5C4F\u201D\u89E6\u53D1\u670D\u52A1\u7684\u6743\u9650\u7533\u8BF7\uFF0C\u6309\u5B9E\u9645\u540D\u79F0\u6388\u6743\u540E\u91CD\u542F Proxy\u3002";
17270
17455
  async function requestDesktopPermission(stateDirectory) {
17271
17456
  return new PlatformDesktop(stateDirectory).requestScreenCapturePermission();
17272
17457
  }
@@ -17316,19 +17501,19 @@ var PlatformDesktop = class {
17316
17501
  macHelper() {
17317
17502
  if (this.helper) return this.helper;
17318
17503
  this.helper = (async () => {
17319
- await mkdir9(this.stateDirectory, { recursive: true, mode: 448 });
17320
- const target = join12(this.stateDirectory, "octopus-desktop-capture");
17321
- const source = fileURLToPath4(new URL("../native/desktop-macos.m", import.meta.url));
17322
- const sourceInfo = await stat8(source), existing = await stat8(target).catch(() => null);
17504
+ await mkdir10(this.stateDirectory, { recursive: true, mode: 448 });
17505
+ const target = join13(this.stateDirectory, "octopus-desktop-capture");
17506
+ const source = fileURLToPath5(new URL("../native/desktop-macos.m", import.meta.url));
17507
+ const sourceInfo = await stat9(source), existing = await stat9(target).catch(() => null);
17323
17508
  if (existing?.isFile() && existing.mtimeMs >= sourceInfo.mtimeMs && (existing.mode & 63) === 0) return target;
17324
- const temporary = `${target}.${randomUUID16()}`;
17509
+ const temporary = `${target}.${randomUUID17()}`;
17325
17510
  try {
17326
17511
  await this.execute("/usr/bin/clang", ["-O2", "-fobjc-arc", "-mmacosx-version-min=12.3", "-framework", "Foundation", "-framework", "AppKit", "-framework", "ScreenCaptureKit", "-framework", "CoreMedia", "-framework", "CoreVideo", "-framework", "VideoToolbox", "-framework", "ImageIO", source, "-o", temporary]);
17327
- await chmod3(temporary, 448);
17328
- await rename4(temporary, target);
17512
+ await chmod4(temporary, 448);
17513
+ await rename5(temporary, target);
17329
17514
  return target;
17330
17515
  } finally {
17331
- await rm3(temporary, { force: true });
17516
+ await rm4(temporary, { force: true });
17332
17517
  }
17333
17518
  })();
17334
17519
  this.helper.catch(() => {
@@ -17364,14 +17549,14 @@ var PlatformDesktop = class {
17364
17549
  const helper = await this.macHelper();
17365
17550
  try {
17366
17551
  const displays = await this.list();
17367
- record2("\u684C\u9762\u622A\u5C4F\u652F\u6301", "ok", `macOS ${version} \u652F\u6301 ScreenCaptureKit\uFF1B${displays.length} \u4E2A\u6D3B\u52A8\u663E\u793A\u5668\uFF1B\u8F85\u52A9\u7A0B\u5E8F\u53EF\u7528`);
17552
+ record2("\u684C\u9762\u622A\u5C4F\u652F\u6301", "ok", `macOS ${version} \u652F\u6301 ScreenCaptureKit\uFF1B${displays.length} \u4E2A\u6D3B\u52A8\u663E\u793A\u5668\uFF1B\u8F85\u52A9\u7A0B\u5E8F\uFF1A${helper}\uFF08\u6587\u4EF6\u540D\u4E0D\u4E00\u5B9A\u662F\u7CFB\u7EDF\u6388\u6743\u540D\u79F0\uFF09`);
17368
17553
  } catch (error) {
17369
- record2("\u684C\u9762\u622A\u5C4F\u652F\u6301", "warn", error instanceof Error ? error.message : String(error));
17554
+ record2("\u684C\u9762\u622A\u5C4F\u652F\u6301", "warn", `${error instanceof Error ? error.message : error} \u8F85\u52A9\u7A0B\u5E8F\uFF1A${helper}\uFF08\u6587\u4EF6\u540D\u4E0D\u4E00\u5B9A\u662F\u7CFB\u7EDF\u6388\u6743\u540D\u79F0\uFF09`);
17370
17555
  }
17371
17556
  try {
17372
17557
  const permission = (await this.execute(helper, ["permission-status"])).trim();
17373
17558
  if (permission !== "granted" && permission !== "denied") throw new Error("\u5C4F\u5E55\u5F55\u5236\u6388\u6743\u68C0\u67E5\u8FD4\u56DE\u4E86\u65E0\u6548\u7ED3\u679C\u3002");
17374
- record2("\u5C4F\u5E55\u5F55\u5236\u6743\u9650", permission === "granted" ? "ok" : "warn", permission === "granted" ? "\u5DF2\u6388\u6743\uFF08\u5F53\u524D\u7528\u6237\u53CA\u542F\u52A8\u7A0B\u5E8F\uFF09\uFF1B\u68C0\u67E5\u672A\u8BFB\u53D6\u684C\u9762\u56FE\u7247\u3002" : `\u672A\u6388\u6743\u6216\u6388\u6743\u5C1A\u672A\u751F\u6548\uFF1B\u8FD0\u884C octopus-proxy onboard \u7533\u8BF7\u3002${DESKTOP_PERMISSION_GUIDANCE}`);
17559
+ record2("\u5C4F\u5E55\u5F55\u5236\u6743\u9650", permission === "granted" ? "ok" : "warn", permission === "granted" ? "\u5F53\u524D\u542F\u52A8\u7A0B\u5E8F\u5DF2\u6388\u6743\uFF1B\u540E\u53F0\u670D\u52A1\u7684\u6388\u6743\u9700\u5728 Claw \u5B9E\u9645\u622A\u5C4F\u9A8C\u8BC1\u3002" : `\u672A\u6388\u6743\u6216\u6388\u6743\u5C1A\u672A\u751F\u6548\uFF1B\u8FD0\u884C octopus-proxy onboard \u7533\u8BF7\u3002${DESKTOP_PERMISSION_GUIDANCE}`);
17375
17560
  } catch (error) {
17376
17561
  record2("\u5C4F\u5E55\u5F55\u5236\u6743\u9650", "warn", `\u65E0\u6CD5\u68C0\u67E5\uFF1A${error instanceof Error ? error.message : error} ${DESKTOP_PERMISSION_GUIDANCE}`);
17377
17562
  }
@@ -17412,9 +17597,9 @@ var PlatformDesktop = class {
17412
17597
  async capture(displayId) {
17413
17598
  const display = (await this.list()).find((item) => item.id === displayId);
17414
17599
  if (!display) throw new Error("\u6240\u9009\u663E\u793A\u5668\u5DF2\u65AD\u5F00\uFF0C\u8BF7\u5237\u65B0\u663E\u793A\u5668\u5217\u8868\u540E\u91CD\u8BD5\u3002");
17415
- await mkdir9(this.stateDirectory, { recursive: true, mode: 448 });
17416
- const directory = await mkdtemp(join12(this.stateDirectory, "desktop-capture-"));
17417
- const output = join12(directory, "screen.png");
17600
+ await mkdir10(this.stateDirectory, { recursive: true, mode: 448 });
17601
+ const directory = await mkdtemp(join13(this.stateDirectory, "desktop-capture-"));
17602
+ const output = join13(directory, "screen.png");
17418
17603
  try {
17419
17604
  if (this.system === "darwin") {
17420
17605
  await this.execute(await this.macHelper(), ["capture", display.id, output]);
@@ -17432,24 +17617,39 @@ var PlatformDesktop = class {
17432
17617
  await this.execute("import", args);
17433
17618
  }
17434
17619
  }
17435
- const info = await stat8(output);
17620
+ const info = await stat9(output);
17436
17621
  if (!info.isFile() || !info.size || info.size > DESKTOP_IMAGE_LIMIT) throw new Error("\u684C\u9762\u622A\u56FE\u4E3A\u7A7A\u6216\u8D85\u8FC7 20 MiB \u4E0A\u9650\u3002");
17437
17622
  return { display, bytes: await readFile9(output) };
17438
17623
  } finally {
17439
- await rm3(directory, { recursive: true, force: true });
17624
+ await rm4(directory, { recursive: true, force: true });
17440
17625
  }
17441
17626
  }
17442
17627
  };
17443
17628
  var DesktopManager = class {
17444
- constructor(backend) {
17629
+ constructor(backend, screenLock) {
17445
17630
  this.backend = backend;
17631
+ this.screenLock = screenLock;
17446
17632
  }
17447
17633
  captures = /* @__PURE__ */ new Map();
17448
17634
  pending = /* @__PURE__ */ new Map();
17635
+ unlocking;
17449
17636
  closed = false;
17450
17637
  async dispatch(command, owner) {
17451
17638
  if (this.closed) throw new Error("\u684C\u9762\u622A\u5C4F\u670D\u52A1\u5DF2\u505C\u6B62\u3002");
17452
17639
  const p = command.payload;
17640
+ if (command.operation === "desktop.lockStatus" || command.operation === "desktop.unlock") {
17641
+ if (!this.screenLock) throw new ScreenLockError("\u6B64 Proxy \u4E0D\u652F\u6301\u5C4F\u5E55\u89E3\u9501\uFF0C\u8BF7\u5347\u7EA7 Proxy\u3002");
17642
+ if (command.operation === "desktop.lockStatus") return this.screenLock.status();
17643
+ if (typeof p.password !== "string" || !p.password || Buffer.byteLength(p.password) > 1024 || /[\x00-\x1f\x7f]/.test(p.password)) throw new ScreenLockError("\u8BF7\u8F93\u5165\u6709\u6548\u7684\u4E3B\u673A\u767B\u5F55\u5BC6\u7801\uFF08\u6700\u591A 1024 \u5B57\u8282\uFF0C\u4E0D\u80FD\u5305\u542B\u63A7\u5236\u5B57\u7B26\uFF09\u3002");
17644
+ if (this.unlocking) throw new ScreenLockError("\u6B63\u5728\u89E3\u9501\u4E3B\u673A\u5C4F\u5E55\uFF0C\u8BF7\u7A0D\u540E\u91CD\u8BD5\u3002");
17645
+ const pending = { owner, abort: new AbortController() };
17646
+ this.unlocking = pending;
17647
+ try {
17648
+ return await this.screenLock.unlock(p.password, pending.abort.signal);
17649
+ } finally {
17650
+ if (this.unlocking === pending) this.unlocking = void 0;
17651
+ }
17652
+ }
17453
17653
  if (command.operation === "desktop.list") return this.backend.list();
17454
17654
  if (command.operation === "desktop.capture") {
17455
17655
  if (typeof p.displayId !== "string" || !p.displayId) throw new Error("\u8BF7\u9009\u62E9\u663E\u793A\u5668\u3002");
@@ -17463,7 +17663,7 @@ var DesktopManager = class {
17463
17663
  if (bytes.length < 24 || !bytes.subarray(0, 8).equals(PNG_SIGNATURE) || bytes.toString("ascii", 12, 16) !== "IHDR") throw new Error("\u622A\u5C4F\u672A\u8FD4\u56DE\u6709\u6548\u7684 PNG \u56FE\u7247\u3002");
17464
17664
  const width = bytes.readUInt32BE(16), height = bytes.readUInt32BE(20);
17465
17665
  if (!width || !height) throw new Error("\u684C\u9762\u622A\u56FE\u5C3A\u5BF8\u65E0\u6548\u3002");
17466
- const info = { id: randomUUID16(), display: publicDisplay(display), capturedAt: (/* @__PURE__ */ new Date()).toISOString(), mime: "image/png", width, height, size: bytes.length, sha256: createHash9("sha256").update(bytes).digest("hex") };
17666
+ const info = { id: randomUUID17(), display: publicDisplay(display), capturedAt: (/* @__PURE__ */ new Date()).toISOString(), mime: "image/png", width, height, size: bytes.length, sha256: createHash9("sha256").update(bytes).digest("hex") };
17467
17667
  this.clearCapture(owner);
17468
17668
  const timer = setTimeout(() => this.clearCapture(owner), 6e4);
17469
17669
  timer.unref();
@@ -17493,9 +17693,11 @@ var DesktopManager = class {
17493
17693
  closeOwned(owner) {
17494
17694
  this.pending.delete(owner);
17495
17695
  this.clearCapture(owner);
17696
+ if (this.unlocking?.owner === owner) this.unlocking.abort.abort();
17496
17697
  }
17497
17698
  close() {
17498
17699
  this.closed = true;
17700
+ this.unlocking?.abort.abort();
17499
17701
  this.pending.clear();
17500
17702
  for (const owner of this.captures.keys()) this.clearCapture(owner);
17501
17703
  }
@@ -17504,15 +17706,15 @@ var DesktopManager = class {
17504
17706
  // src/daemon.ts
17505
17707
  async function runDaemon(configPath, authenticate = verifyHostPassword) {
17506
17708
  const config = await loadConfig(configPath);
17507
- await mkdir10(config.stateDirectory, { recursive: true, mode: 448 });
17508
- const workspace = new Workspace(config.projects), runtime = createRuntime(config), uploads = new Uploads(workspace, join13(config.stateDirectory, "uploads")), journal = new Journal(join13(config.stateDirectory, "journal"));
17709
+ await mkdir11(config.stateDirectory, { recursive: true, mode: 448 });
17710
+ const workspace = new Workspace(config.projects), runtime = createRuntime(config), uploads = new Uploads(workspace, join14(config.stateDirectory, "uploads")), journal = new Journal(join14(config.stateDirectory, "journal"));
17509
17711
  let secureChat;
17510
17712
  function emit(projectId, event, data) {
17511
17713
  secureChat?.publish(projectId, event, data);
17512
17714
  }
17513
17715
  const terminals = new TerminalManager(workspace, (owner, project, event, data) => secureChat?.sendTo(owner, project, event, data));
17514
- const desktop = new DesktopManager(new PlatformDesktop(config.stateDirectory));
17515
- const adapter = createAdapter(runtime, workspace, uploads, emit), artifacts = new Artifacts(workspace, join13(config.stateDirectory, "artifacts"), emit);
17716
+ const desktop = new DesktopManager(new PlatformDesktop(config.stateDirectory), new PlatformScreenLock(config.stateDirectory));
17717
+ const adapter = createAdapter(runtime, workspace, uploads, emit), artifacts = new Artifacts(workspace, join14(config.stateDirectory, "artifacts"), emit);
17516
17718
  const projectRegistry = new ProjectRegistry(configPath, config, runtime, adapter);
17517
17719
  async function dispatch(c, secureSessionId) {
17518
17720
  if (c.operation.startsWith("desktop.")) return desktop.dispatch(c, secureSessionId);
package/dist/cli.js CHANGED
@@ -27,7 +27,7 @@ import {
27
27
  runDaemon,
28
28
  supportedPlatform,
29
29
  systemdUnitPath
30
- } from "./chunk-BIL5ROSQ.js";
30
+ } from "./chunk-6DTK4JVV.js";
31
31
 
32
32
  // src/cli.ts
33
33
  import { spawn } from "node:child_process";
@@ -456,7 +456,7 @@ import { stdin, stdout } from "node:process";
456
456
  import { access as access3, constants as constants2, mkdir as mkdir4, stat as stat3 } from "node:fs/promises";
457
457
  import { execFile as execFile5 } from "node:child_process";
458
458
  import { platform as platform2 } from "node:os";
459
- import { isAbsolute as isAbsolute3 } from "node:path";
459
+ import { isAbsolute as isAbsolute3, join as join3 } from "node:path";
460
460
  import { promisify as promisify5 } from "node:util";
461
461
  var DEFAULT_HUB_URL = "https://octopus.wuyehao.com";
462
462
  async function ask(rl, question, options = {}) {
@@ -645,9 +645,12 @@ async function runOnboard(opts) {
645
645
  stateDirectory = expandHome(stateDirectory);
646
646
  await mkdir4(stateDirectory, { recursive: true, mode: 448 });
647
647
  try {
648
- if (platform2() === "darwin") console.log("\u6B63\u5728\u68C0\u67E5\u5C4F\u5E55\u5F55\u5236\u6743\u9650\uFF1B\u5982\u51FA\u73B0\u7CFB\u7EDF\u6388\u6743\u63D0\u793A\uFF0C\u8BF7\u5141\u8BB8 Proxy \u8BFB\u53D6\u684C\u9762\u3002");
648
+ if (platform2() === "darwin") {
649
+ console.log(`\u622A\u5C4F\u8F85\u52A9\u7A0B\u5E8F\u8DEF\u5F84\uFF1A${join3(stateDirectory, "octopus-desktop-capture")}\uFF08\u6587\u4EF6\u540D\u4E0D\u4E00\u5B9A\u662F\u7CFB\u7EDF\u6388\u6743\u540D\u79F0\uFF09`);
650
+ console.log("\u6B63\u5728\u68C0\u67E5\u5C4F\u5E55\u5F55\u5236\u6743\u9650\uFF1B\u5982\u51FA\u73B0\u7CFB\u7EDF\u6388\u6743\u63D0\u793A\uFF0C\u8BF7\u6309\u5F39\u7A97\u4E2D\u7684\u5B9E\u9645\u7A0B\u5E8F\u540D\u79F0\u6388\u6743\uFF0C\u53EF\u80FD\u663E\u793A Terminal\u3001iTerm\u3001\u7F16\u8F91\u5668\u6216 node\u3002");
651
+ }
649
652
  const permission = await requestDesktopPermission(stateDirectory);
650
- if (permission === "granted") console.log("\u5C4F\u5E55\u5F55\u5236\u6743\u9650\uFF1A\u5DF2\u6388\u6743\uFF0C\u53EF\u5728 Claw \u7684\u201C\u5FEB\u7167\u201D\u4E2D\u622A\u5C4F\u3002");
653
+ if (permission === "granted") console.log("\u5C4F\u5E55\u5F55\u5236\u6743\u9650\uFF1A\u5F53\u524D\u542F\u52A8\u7A0B\u5E8F\u5DF2\u6388\u6743\uFF1B\u540E\u53F0\u670D\u52A1\u7684\u6388\u6743\u9700\u5728 Claw \u7684\u201C\u5FEB\u7167\u201D\u4E2D\u5B9E\u9645\u622A\u5C4F\u9A8C\u8BC1\u3002");
651
654
  else if (permission === "denied") console.log(`\u8B66\u544A\uFF1A\u5C1A\u672A\u83B7\u5F97\u5C4F\u5E55\u5F55\u5236\u6743\u9650\uFF0C\u521D\u59CB\u5316\u5C06\u7EE7\u7EED\u3002${DESKTOP_PERMISSION_GUIDANCE}`);
652
655
  } catch (error) {
653
656
  console.log(`\u8B66\u544A\uFF1A\u65E0\u6CD5\u7533\u8BF7\u5C4F\u5E55\u5F55\u5236\u6743\u9650\uFF0C\u521D\u59CB\u5316\u5C06\u7EE7\u7EED\uFF1A${error instanceof Error ? error.message : error} ${DESKTOP_PERMISSION_GUIDANCE}`);
package/dist/main.js CHANGED
@@ -2,7 +2,7 @@
2
2
  import {
3
3
  ConfigError,
4
4
  runDaemon
5
- } from "./chunk-BIL5ROSQ.js";
5
+ } from "./chunk-6DTK4JVV.js";
6
6
 
7
7
  // src/main.ts
8
8
  runDaemon(process.env.OCTOPUS_PROXY_CONFIG ?? "proxy.json").catch((error) => {
@@ -13,7 +13,7 @@ static void fail(NSString *message) {
13
13
  }
14
14
 
15
15
  static NSString *permissionMessage(void) {
16
- return @"无法读取桌面。请在 Proxy 主机的“系统设置 → 隐私与安全性 → 屏幕录制(或屏幕与系统音频录制)”中授权 octopus-desktop-capture 或其启动程序,并重启 Proxy 后重试。";
16
+ return @"无法读取桌面。请在 Proxy 主机的“系统设置 → 隐私与安全性 → 屏幕录制(或屏幕与系统音频录制)”中启用系统弹窗显示的实际程序。终端运行时可能显示 Terminal、iTerm 或编辑器,后台服务可能显示 node;不一定会出现 octopus-desktop-capture。请按此次截屏请求的实际程序名称授权,重启 Proxy 后重试。";
17
17
  }
18
18
 
19
19
  static NSDictionary *displayInfo(CGDirectDisplayID displayId) {
@@ -0,0 +1,127 @@
1
+ #import <AppKit/AppKit.h>
2
+ #import <ApplicationServices/ApplicationServices.h>
3
+ #include <unistd.h>
4
+ #include <string.h>
5
+
6
+ static void wipe(void *buffer, size_t length) {
7
+ volatile unsigned char *bytes = buffer;
8
+ while (length--) *bytes++ = 0;
9
+ }
10
+
11
+ static int fail(const char *code) { fprintf(stderr, "%s\n", code); return 1; }
12
+
13
+ // This WindowServer property is not a documented public lock-state API. Fail
14
+ // closed when the session is unavailable or is not this user's console desktop.
15
+ static BOOL lockState(BOOL *locked) {
16
+ NSDictionary *session = CFBridgingRelease(CGSessionCopyCurrentDictionary());
17
+ if (!session || ![session[(__bridge NSString *)kCGSessionOnConsoleKey] boolValue] ||
18
+ ![session[(__bridge NSString *)kCGSessionLoginDoneKey] boolValue] ||
19
+ ![session[(__bridge NSString *)kCGSessionUserIDKey] isKindOfClass:NSNumber.class] ||
20
+ [session[(__bridge NSString *)kCGSessionUserIDKey] unsignedIntValue] != getuid()) return NO;
21
+ id value = session[@"CGSSessionScreenIsLocked"];
22
+ if (value && ![value isKindOfClass:NSNumber.class]) return NO;
23
+ *locked = [value boolValue];
24
+ return YES;
25
+ }
26
+
27
+ static void printStatus(BOOL locked) {
28
+ printf("{\"locked\":%s,\"canUnlock\":%s}", locked ? "true" : "false",
29
+ AXIsProcessTrusted() && CGPreflightPostEventAccess() ? "true" : "false");
30
+ }
31
+
32
+ static AXUIElementRef passwordFocus(void) {
33
+ AXUIElementRef system = AXUIElementCreateSystemWide();
34
+ AXUIElementSetMessagingTimeout(system, 0.5);
35
+ CFTypeRef focused = NULL;
36
+ AXError result = AXUIElementCopyAttributeValue(system, kAXFocusedUIElementAttribute, &focused);
37
+ CFRelease(system);
38
+ if (result != kAXErrorSuccess || !focused || CFGetTypeID(focused) != AXUIElementGetTypeID()) {
39
+ if (focused) CFRelease(focused);
40
+ return NULL;
41
+ }
42
+ pid_t pid = 0;
43
+ AXUIElementRef field = (AXUIElementRef)focused;
44
+ AXUIElementSetMessagingTimeout(field, 0.5);
45
+ NSRunningApplication *app = nil;
46
+ if (AXUIElementGetPid(field, &pid) == kAXErrorSuccess) app = [NSRunningApplication runningApplicationWithProcessIdentifier:pid];
47
+ CFTypeRef subrole = NULL;
48
+ AXUIElementCopyAttributeValue(field, kAXSubroleAttribute, &subrole);
49
+ BOOL secure = subrole && CFEqual(subrole, kAXSecureTextFieldSubrole);
50
+ if (subrole) CFRelease(subrole);
51
+ if (!secure || ![app.bundleIdentifier isEqualToString:@"com.apple.loginwindow"] ||
52
+ ![app.executableURL.path isEqualToString:@"/System/Library/CoreServices/loginwindow.app/Contents/MacOS/loginwindow"]) {
53
+ CFRelease(field); return NULL;
54
+ }
55
+ return field;
56
+ }
57
+
58
+ static BOOL stillFocused(AXUIElementRef field) {
59
+ BOOL locked = NO;
60
+ if (!lockState(&locked) || !locked) return NO;
61
+ AXUIElementRef current = passwordFocus();
62
+ BOOL same = current && CFEqual(current, field);
63
+ if (current) CFRelease(current);
64
+ return same;
65
+ }
66
+
67
+ static BOOL key(CGKeyCode code, CGEventFlags flags, const UniChar *text, size_t length) {
68
+ CGEventRef down = CGEventCreateKeyboardEvent(NULL, code, true);
69
+ CGEventRef up = CGEventCreateKeyboardEvent(NULL, code, false);
70
+ if (!down || !up) { if (down) CFRelease(down); if (up) CFRelease(up); return NO; }
71
+ CGEventSetFlags(down, flags); CGEventSetFlags(up, flags);
72
+ if (text) { CGEventKeyboardSetUnicodeString(down, length, text); CGEventKeyboardSetUnicodeString(up, length, text); }
73
+ CGEventPost(kCGHIDEventTap, down); CGEventPost(kCGHIDEventTap, up);
74
+ CFRelease(down); CFRelease(up);
75
+ usleep(20000);
76
+ return YES;
77
+ }
78
+
79
+ int main(int argc, const char *argv[]) {
80
+ @autoreleasepool {
81
+ if (getuid() == 0 || argc != 2) return fail("session");
82
+ BOOL locked = NO;
83
+ if (!lockState(&locked)) return fail("session");
84
+ if (strcmp(argv[1], "status") == 0) { printStatus(locked); return 0; }
85
+ if (strcmp(argv[1], "unlock") != 0) return fail("session");
86
+ if (!locked) { printStatus(NO); return 0; }
87
+ if (!AXIsProcessTrusted() || !CGPreflightPostEventAccess()) return fail("permission");
88
+ unsigned char header[2], bytes[1025] = {0};
89
+ if (fread(header, 1, 2, stdin) != 2) return fail("password");
90
+ size_t count = ((size_t)header[0] << 8) | header[1];
91
+ if (!count || count > 1024 || fread(bytes, 1, count, stdin) != count) { wipe(bytes, sizeof(bytes)); return fail("password"); }
92
+ NSString *password = [[NSString alloc] initWithBytes:bytes length:count encoding:NSUTF8StringEncoding];
93
+ wipe(bytes, sizeof(bytes));
94
+ if (!password || [password rangeOfCharacterFromSet:NSCharacterSet.controlCharacterSet].location != NSNotFound) return fail("password");
95
+ // A non-text key wakes the password prompt. Never type a secret into an
96
+ // arbitrary app, username field, clipboard, or a blind screen coordinate.
97
+ if (!lockState(&locked) || !locked) return fail("changed");
98
+ if (!key(56, 0, NULL, 0)) return fail("focus");
99
+ AXUIElementRef field = NULL;
100
+ for (int i = 0; i < 20 && !field; i++) { usleep(100000); field = passwordFocus(); }
101
+ if (!field) return fail("focus");
102
+ if (!stillFocused(field) || !key(0, kCGEventFlagMaskCommand, NULL, 0) ||
103
+ !stillFocused(field) || !key(51, 0, NULL, 0)) { CFRelease(field); return fail("changed"); }
104
+ UniChar characters[1024] = {0};
105
+ NSUInteger length = password.length;
106
+ if (length > 1024) { CFRelease(field); return fail("password"); }
107
+ [password getCharacters:characters range:NSMakeRange(0, length)];
108
+ password = nil;
109
+ BOOL sent = YES;
110
+ // One Unicode scalar per event avoids event-string truncation. Check the
111
+ // secure loginwindow focus and console lock immediately before each event.
112
+ for (NSUInteger i = 0; i < length;) {
113
+ NSUInteger units = CFStringIsSurrogateHighCharacter(characters[i]) && i + 1 < length && CFStringIsSurrogateLowCharacter(characters[i + 1]) ? 2 : 1;
114
+ if (!stillFocused(field) || !key(0, 0, &characters[i], units)) { sent = NO; break; }
115
+ i += units;
116
+ }
117
+ wipe(characters, sizeof(characters));
118
+ if (!sent || !stillFocused(field) || !key(36, 0, NULL, 0)) { CFRelease(field); return fail("changed"); }
119
+ CFRelease(field);
120
+ for (int i = 0; i < 50; i++) {
121
+ usleep(100000);
122
+ if (!lockState(&locked)) return fail("session");
123
+ if (!locked) { printStatus(NO); return 0; }
124
+ }
125
+ return fail("timeout");
126
+ }
127
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@skylandnpm/octopus-proxy",
3
- "version": "0.1.34",
3
+ "version": "0.1.38",
4
4
  "type": "module",
5
5
  "description": "Octopus host proxy: OpenCode, Codex, ZCode and Pi adapters with service installer CLI",
6
6
  "bin": {