local-commander-agent 0.3.43 → 0.3.45

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/CHANGELOG.md CHANGED
@@ -1,5 +1,17 @@
1
1
  # Change history
2
2
 
3
+ ## 0.3.45 - 2026-10-22
4
+
5
+ - New: macOS 14 or later on Apple Silicon, and Linux x64 or arm64 with a systemd user session. Run the same `npx local-commander-agent@latest` in Terminal; npm downloads only the runtime for your computer. Setup pairs the computer, asks what may be used in the terminal, and runs the agent as a background service that starts when you sign in.
6
+ - Fixed: a computer that lost its connection without noticing (after sleep, a network change or a server restart) now reconnects within about three minutes instead of staying offline.
7
+ - Fixed: a request that never reached a sleeping computer no longer keeps one of its execution slots busy; the computer frees it when it reconnects.
8
+ - Includes everything in 0.3.44.
9
+
10
+ ## 0.3.44 - 2026-10-22
11
+
12
+ - New: when you set up a computer, the window that asks what may be used now shows the Local Commander account the computer joined. Someone helping you can approve your computer from their own phone with the code you read to them. Continue only if you know that account; Cancel shares nothing.
13
+ - Includes everything in 0.3.43.
14
+
3
15
  ## 0.3.43 - 2026-10-21
4
16
 
5
17
  - Faster: file search on Windows reads files in batches through one helper process instead of starting one per file. A search over 300 files now takes well under a second. Results are unchanged.
package/README.md CHANGED
@@ -1,25 +1,25 @@
1
1
  # Local Commander Agent
2
2
 
3
- **Connect your AI assistant to your Windows computer through [Local Commander](https://localcmd.app).** The agent lets an authorized MCP client work with local files, run explicitly approved commands, and access approved localhost services on your paired computer.
3
+ **Connect your AI assistant to your computer through [Local Commander](https://localcmd.app).** The agent lets an authorized MCP client work with local files, run explicitly approved commands, and access approved localhost services on your paired computer.
4
4
 
5
- > **Requires the Local Commander service at https://localcmd.app.** This npm package is its Windows companion, not a standalone MCP server, an offline AI assistant, or a JavaScript SDK. Installing the package alone is not enough: you need a Local Commander account, an approved device pairing, a running agent, an internet connection, and an authorized MCP connection from your AI client. Local management commands such as status and stop do not require the service to be online.
5
+ > **Requires the Local Commander service at https://localcmd.app.** This npm package is its computer companion, not a standalone MCP server, an offline AI assistant, or a JavaScript SDK. Installing the package alone is not enough: you need a Local Commander account, an approved device pairing, a running agent, an internet connection, and an authorized MCP connection from your AI client. Local management commands such as status and stop do not require the service to be online.
6
6
 
7
7
  [Website and sign-in](https://localcmd.app) ┬╖ [Support](https://localcmd.app/support) ┬╖ [Privacy](https://localcmd.app/privacy)
8
8
 
9
9
  ## Before you start
10
10
 
11
- Use **Windows 11 x64** and a currently supported Node.js LTS version meeting the package's Node.js 20+ minimum. Node.js/npm are needed to launch `npx`; the installed background agent includes its own Node runtime. You do not need Docker, WSL, a repository checkout, a database, or administrator rights for normal setup. Your AI client must support authenticated remote MCP connections; availability depends on the client and workspace policy.
11
+ Use **Windows 11 x64**, **macOS 14 or later on Apple Silicon**, or **Linux with a systemd user session** (Ubuntu 24.04 x64 or arm64), and a currently supported Node.js LTS version meeting the package's Node.js 20+ minimum. Node.js/npm are needed to launch `npx`; the installed background agent includes its own Node runtime. You do not need Docker, WSL, a repository checkout, a database, or administrator rights for normal setup. Your AI client must support authenticated remote MCP connections; availability depends on the client and workspace policy.
12
12
 
13
13
  ## Install and connect
14
14
 
15
- 1. Open PowerShell or Windows Terminal as your normal Windows user and run:
15
+ 1. Open a terminal as your normal user (PowerShell or Windows Terminal on Windows, Terminal on macOS or Linux) and run:
16
16
 
17
17
  ```powershell
18
18
  npx local-commander-agent@latest
19
19
  ```
20
20
 
21
21
  2. The installer opens a browser. Sign in to [Local Commander](https://localcmd.app), check the displayed pairing code, and approve this computer. Approve only a pairing you just started yourself.
22
- 3. Choose access in the local Windows dialog:
22
+ 3. Choose access in the local dialog (a window on Windows, a terminal prompt on macOS and Linux):
23
23
  - **Full access - all connected drives (default):** approve the available drive letters, file reads/writes, installed commands and localhost services at once. There is no folder picker or separate approval for Node.
24
24
  - **Selected folders:** choose one folder in the folder picker. It starts read/write; more folders and write permission can be added later.
25
25
 
@@ -32,15 +32,15 @@ Use **Windows 11 x64** and a currently supported Node.js LTS version meeting the
32
32
 
33
33
  Complete the browser authorization with the same Local Commander account. Device pairing and AI-client authorization are separate steps. For an AI client that runs on this same computer, you can instead use direct local MCP (see "Direct local MCP" below). Do not configure `npx local-commander-agent` itself as a stdio server.
34
34
 
35
- You can close the installation terminal. The agent runs in the background and starts when this Windows user signs in. The PC must be awake and online for remote AI tasks. As a first test, ask the connected assistant to list the files in a folder you approved.
35
+ You can close the installation terminal. The agent runs in the background and starts when you sign in to this computer. The computer must be awake and online for remote AI tasks. As a first test, ask the connected assistant to list the files in a folder you approved.
36
36
 
37
- Installation and update also add `localcommander` to your Windows **user PATH**. Open a new CMD/terminal window, then run `localcommander status`, `localcommander monitor --watch`, or `localcommander logs --follow` from any folder. No global npm installation is needed. The launcher uses your required Node.js installation and an installed copy of the CLI, so clearing npm's cache does not remove the command. Fetch newer releases with `npx local-commander-agent@latest update`. Uninstall removes Local Commander's PATH entry and launcher while preserving your other entries.
37
+ On Windows, installation and update also add `localcommander` to your **user PATH**. Open a new CMD/terminal window, then run `localcommander status`, `localcommander monitor --watch`, or `localcommander logs --follow` from any folder. No global npm installation is needed. The launcher uses your required Node.js installation and an installed copy of the CLI, so clearing npm's cache does not remove the command. Fetch newer releases with `npx local-commander-agent@latest update`. Uninstall removes Local Commander's PATH entry and launcher while preserving your other entries.
38
38
 
39
39
  ## Direct local MCP (AI clients on this computer)
40
40
 
41
- An AI client that runs on the same Windows computer, such as Claude Code, Codex or Cursor, can use your approved workspaces directly through the running agent. No cloud round trip happens per call. It keeps working while the internet or localcmd.app is unavailable, and calls take milliseconds instead of a cloud admission. Hosted assistants such as ChatGPT or claude.ai on the web still use `https://localcmd.app/mcp`.
41
+ An AI client that runs on the same computer, such as Claude Code, Codex or Cursor, can use your approved workspaces directly through the running agent. No cloud round trip happens per call. It keeps working while the internet or localcmd.app is unavailable, and calls take milliseconds instead of a cloud admission. Hosted assistants such as ChatGPT or claude.ai on the web still use `https://localcmd.app/mcp`.
42
42
 
43
- 1. Turn it on once. This is an owner choice for this Windows user, and `off` revokes it immediately:
43
+ 1. Turn it on once. This is an owner choice for this user account, and `off` revokes it immediately:
44
44
 
45
45
  ```powershell
46
46
  localcommander mcp on
@@ -57,15 +57,15 @@ An AI client that runs on the same Windows computer, such as Claude Code, Codex
57
57
  Other clients: { "command": "cmd", "args": ["/c", "localcommander", "mcp"] }
58
58
  ```
59
59
 
60
- The tools are the same as the remote ones: `workspace_list`, file reads and writes, search, Git, `project_snapshot`, `command_run` and `localhost_request`. Each takes an optional `workspace` (a grant id, a root such as `D:\`, or a display name). The same workspace grants, Full Access or scoped mode and owner denials apply, and commands run hidden with your Windows account's rights.
60
+ The tools are the same as the remote ones: `workspace_list`, file reads and writes, search, Git, `project_snapshot`, `command_run` and `localhost_request`. Each takes an optional `workspace` (a grant id, a root such as `D:\`, or a display name). The same workspace grants, Full Access or scoped mode and owner denials apply, and commands run hidden with your user account's rights.
61
61
 
62
- The agent listens only on `127.0.0.1`, and only while direct local MCP is on. Every connection must first prove a key that only this Windows user's protected Local Commander state can derive, so other Windows users and other computers cannot connect. Check the state with `localcommander mcp status`; turn it off with `localcommander mcp off`. While an enterprise Guard policy is configured, direct local calls are refused so that Guard evidence cannot be bypassed.
62
+ The agent listens only on `127.0.0.1`, and only while direct local MCP is on. Every connection must first prove a key that only this user's protected Local Commander state can derive, so other users and other computers cannot connect. Check the state with `localcommander mcp status`; turn it off with `localcommander mcp off`. While an enterprise Guard policy is configured, direct local calls are refused so that Guard evidence cannot be bypassed.
63
63
 
64
64
  ## What тАЬall connected drivesтАЭ means
65
65
 
66
66
  It expands the **filesystem scope** to the drive letters shown at the moment you consent. It includes available local fixed and removable drives. Mapped network drives and `subst` letters are not included. New drive letters, unavailable drives and unmounted volumes are not automatically added; run `configure-access` again after making the desired drive available. Grants are path/drive-letter based, not tied to a particular removable disk's hardware identity: reusing an approved letter gives access under that letter.
67
67
 
68
- Full access allows installed commands resolved from absolute owner PATH entries or an absolute executable path, plus localhost services. Explicit command/port denials and write restrictions still apply. It does not elevate Windows privileges. Protected Local Commander state/runtime and secret-file filters apply to file tools; programs run with your Windows account's rights, so these controls are not an OS sandbox. ChatGPT/Claude or your workspace may still ask for their own action confirmations. Existing installations keep their current permissions until the owner runs `configure-access` and selects full access using a release that supports it.
68
+ Full access allows installed commands resolved from absolute owner PATH entries or an absolute executable path, plus localhost services. Explicit command/port denials and write restrictions still apply. It does not elevate privileges. Protected Local Commander state/runtime and secret-file filters apply to file tools; programs run with your user account's rights, so these controls are not an OS sandbox. ChatGPT/Claude or your workspace may still ask for their own action confirmations. Existing installations keep their current permissions until the owner runs `configure-access` and selects full access using a release that supports it.
69
69
 
70
70
  ## Manage the installation
71
71
 
@@ -143,7 +143,7 @@ Do not paste `(Get-Command git).Source` into CMD: CMD passes it literally instea
143
143
 
144
144
  Under full access, `deny-command` and `deny-localhost` exclude the named command or port while other commands/ports remain available. Native `.exe`/`.com` commands receive argument arrays directly. `.cmd`/`.bat` launchers use a hidden PowerShell wrapper and reject cmd metacharacters in arguments; use an explicit interpreter for more complex scripts. Allowing a general interpreter is not containment against it launching a denied program.
145
145
 
146
- Under **owner-approved Full Access**, installed Windows shells, script hosts and administration executables (including PowerShell and CMD) work without per-command registration. They execute under the Windows account's own rights, not elevated privileges. Owner-defined `deny-command` exclusions and explicit write/localhost restrictions still apply. A general interpreter can access resources beyond file-tool path checks; Full Access is not an OS sandbox. Existing scoped grants stay scoped until the owner deliberately changes access with `configure-access`.
146
+ Under **owner-approved Full Access**, installed Windows shells, script hosts and administration executables (including PowerShell and CMD) work without per-command registration. They execute under the user account's own rights, not elevated privileges. Owner-defined `deny-command` exclusions and explicit write/localhost restrictions still apply. A general interpreter can access resources beyond file-tool path checks; Full Access is not an OS sandbox. Existing scoped grants stay scoped until the owner deliberately changes access with `configure-access`.
147
147
 
148
148
 
149
149
  For a remote MCP client with an unknown device/workspace, use `workspace_list`, then `execution_session_open`, perform the requested work and read results. Reuse valid, already authorized session and target context where supported. `diagnostics` is for faults, not a mandatory startup roundtrip. Diagnostics reports quota separately from the device's execution slot limit. Its optional `repairSafe` mode releases only expired reservations from this OAuth connection that never acquired a dispatch lease. It never retries work, changes permissions or clears unknown executions.
@@ -153,7 +153,7 @@ For a remote MCP client with an unknown device/workspace, use `workspace_list`,
153
153
  | Symptom | What to check |
154
154
  | --- | --- |
155
155
  | `npx` is not recognized | Install supported Node.js LTS, then open a new terminal. |
156
- | Unsupported OS or architecture | This package targets Windows x64, not macOS, Linux or Windows ARM64. |
156
+ | Unsupported OS or architecture | Supported: Windows 11 x64, macOS 14 or later on Apple Silicon, Linux x64 or arm64 with systemd. Windows ARM64 and Intel Macs are not supported. |
157
157
  | Pairing does not complete | Open https://localcmd.app, verify your account and internet access, then rerun the install command to resume. Do not share pairing codes. |
158
158
  | `ACCESS_SETUP_CANCELLED` | No filesystem grant was saved by that dialog. Rerun setup and choose the intended scope. |
159
159
  | `STATE_CHANGED` | Local account/policy/permissions changed while the dialog was open. Review current state and reopen the dialog. |
@@ -723,8 +723,10 @@ function help() {
723
723
  ].join("\n");
724
724
  }
725
725
  async function main() {
726
- requireWindows();
727
726
  if (!/^\d+\.\d+\.\d+(?:[-+][0-9A-Za-z.-]+)?$/u.test(VERSION)) fail("PACKAGE_VERSION_INVALID");
727
+ // macOS and Linux run the per-platform runtime package through its own lifecycle (#253).
728
+ if (process.platform !== "win32") return (await import("./posix.mjs")).runPosix(process.argv.slice(2), VERSION);
729
+ requireWindows();
728
730
  const command = process.argv[2] || "bootstrap";
729
731
  const args = process.argv.slice(3);
730
732
  if (command === "-h" || command === "--help" || command === "help") {
package/bin/posix.mjs ADDED
@@ -0,0 +1,66 @@
1
+ // macOS and Linux: the runtime ships in a per-platform package (an optional dependency, so npm installs only the
2
+ // matching one). Its lifecycle accepts only a private, owner-only artifact directory, so the files are copied from the
3
+ // npx cache into a fresh private directory and the lifecycle installs from there into its own versions directory.
4
+ import { constants } from "node:fs";
5
+ import fs from "node:fs/promises";
6
+ import os from "node:os";
7
+ import path from "node:path";
8
+ import { spawnSync } from "node:child_process";
9
+ import { createRequire } from "node:module";
10
+
11
+ const PACKAGES = { "darwin-arm64": "local-commander-agent-darwin-arm64", "linux-x64": "local-commander-agent-linux-x64", "linux-arm64": "local-commander-agent-linux-arm64" };
12
+ // npm command -> lifecycle command. The default (`npx local-commander-agent@latest`) is setup, as on Windows.
13
+ const COMMANDS = { bootstrap: "setup", install: "setup", setup: "setup", update: "update", pair: "pair", access: "access", "configure-access": "access",
14
+ start: "start", stop: "stop", status: "status", rollback: "rollback", uninstall: "uninstall" };
15
+ const EXECUTABLES = new Set(["node", "posix-host", "owner-store"]);
16
+
17
+ function fail(code) { const error = new Error(code); error.code = code; throw error; }
18
+
19
+ export function posixHelp() {
20
+ return [
21
+ "Local Commander",
22
+ "",
23
+ " npx local-commander-agent@latest set up this computer, pair it and choose access",
24
+ " npx local-commander-agent@latest update install this version, keeping pairing and access",
25
+ " npx local-commander-agent@latest status|start|stop|pair|access|rollback",
26
+ " npx local-commander-agent@latest uninstall [--purge-state]",
27
+ ""
28
+ ].join("\n");
29
+ }
30
+
31
+ export async function runPosix(argv, version) {
32
+ const name = PACKAGES[process.platform + "-" + process.arch];
33
+ if (!name) fail(process.platform === "darwin" ? "MACOS_APPLE_SILICON_REQUIRED" : process.platform === "linux" ? "LINUX_X64_OR_ARM64_REQUIRED" : "PLATFORM_UNSUPPORTED");
34
+ const command = argv[0] || "bootstrap", args = argv.slice(1);
35
+ if (command === "-h" || command === "--help" || command === "help") { process.stdout.write(posixHelp()); return; }
36
+ const lifecycle = COMMANDS[command];
37
+ if (!lifecycle) fail("ARGUMENT_INVALID");
38
+ if (lifecycle === "uninstall" ? args.length > 1 || (args.length === 1 && args[0] !== "--purge-state") : args.length) fail("ARGUMENT_INVALID");
39
+ if (typeof process.getuid === "function" && process.getuid() === 0) fail("POSIX_OWNER_REQUIRED");
40
+
41
+ let packageJson;
42
+ try { packageJson = createRequire(import.meta.url).resolve(name + "/package.json"); } catch { fail("PLATFORM_PACKAGE_MISSING"); }
43
+ const meta = JSON.parse(await fs.readFile(packageJson, "utf8"));
44
+ if (meta.name !== name || meta.version !== version) fail("PLATFORM_PACKAGE_MISMATCH");
45
+ const source = path.join(path.dirname(packageJson), "runtime");
46
+ const manifest = JSON.parse(await fs.readFile(path.join(source, "manifest.json"), "utf8"));
47
+ const files = Object.keys(manifest?.files ?? {});
48
+ if (manifest?.platform !== process.platform || manifest?.arch !== process.arch || !files.length || files.some(file => !/^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$/u.test(file))) fail("PLATFORM_PACKAGE_INVALID");
49
+
50
+ // Canonical parent: on macOS os.tmpdir() is under /var, a symlink to /private/var, and the lifecycle rejects aliases.
51
+ const stage = await fs.mkdtemp(path.join(await fs.realpath(os.tmpdir()), "local-commander-"));
52
+ try {
53
+ await fs.chmod(stage, 0o700);
54
+ for (const file of [...files, "manifest.json"]) {
55
+ const target = path.join(stage, file);
56
+ await fs.copyFile(path.join(source, file), target, constants.COPYFILE_EXCL);
57
+ await fs.chmod(target, EXECUTABLES.has(file) ? 0o700 : 0o600);
58
+ }
59
+ // The lifecycle verifies every file against the manifest, then copies them into its own versions directory.
60
+ const result = spawnSync(path.join(stage, "node"), [path.join(stage, "lifecycle.mjs"), lifecycle, ...args], { stdio: "inherit" });
61
+ if (result.error) throw result.error;
62
+ process.exitCode = result.status ?? 1;
63
+ } finally {
64
+ await fs.rm(stage, { recursive: true, force: true });
65
+ }
66
+ }
package/package.json CHANGED
@@ -1,47 +1,57 @@
1
- {
2
- "name": "local-commander-agent",
3
- "version": "0.3.43",
4
- "description": "Windows companion for Local Commander: connect AI clients to local files, approved commands and localhost. Requires https://localcmd.app.",
5
- "type": "module",
6
- "bin": {
7
- "localcommander": "bin/localcommander.mjs"
8
- },
9
- "os": [
10
- "win32"
11
- ],
12
- "cpu": [
13
- "x64"
14
- ],
15
- "engines": {
16
- "node": ">=20.0.0"
17
- },
18
- "files": [
19
- "bin/",
20
- "runtime/",
21
- "README.md",
22
- "CHANGELOG.md"
23
- ],
24
- "scripts": {
25
- "prepack": "node ../../scripts/build-npm-agent.mjs",
26
- "prepublishOnly": "node ../../scripts/check-npm-publish-auth.mjs && node ../../scripts/verify-npm-agent.mjs"
27
- },
28
- "publishConfig": {
29
- "access": "public"
30
- },
31
- "keywords": [
32
- "mcp",
33
- "local",
34
- "agent",
35
- "chatgpt",
36
- "claude",
37
- "windows"
38
- ],
39
- "localCommander": {
40
- "controlPlaneOrigin": "https://localcmd.app"
41
- },
42
- "homepage": "https://localcmd.app",
43
- "bugs": {
44
- "url": "https://localcmd.app/support",
45
- "email": "peter.mamrukov@localcmd.app"
46
- }
47
- }
1
+ {
2
+ "name": "local-commander-agent",
3
+ "version": "0.3.45",
4
+ "description": "Computer companion for Local Commander: connect AI clients to local files, approved commands and localhost. Requires https://localcmd.app.",
5
+ "type": "module",
6
+ "bin": {
7
+ "localcommander": "bin/localcommander.mjs"
8
+ },
9
+ "os": [
10
+ "win32",
11
+ "darwin",
12
+ "linux"
13
+ ],
14
+ "cpu": [
15
+ "x64",
16
+ "arm64"
17
+ ],
18
+ "optionalDependencies": {
19
+ "local-commander-agent-darwin-arm64": "0.3.45",
20
+ "local-commander-agent-linux-arm64": "0.3.45",
21
+ "local-commander-agent-linux-x64": "0.3.45"
22
+ },
23
+ "engines": {
24
+ "node": ">=20.0.0"
25
+ },
26
+ "files": [
27
+ "bin/",
28
+ "runtime/",
29
+ "README.md",
30
+ "CHANGELOG.md"
31
+ ],
32
+ "scripts": {
33
+ "prepack": "node ../../scripts/build-npm-agent.mjs",
34
+ "prepublishOnly": "node ../../scripts/check-npm-publish-auth.mjs && node ../../scripts/verify-npm-agent.mjs"
35
+ },
36
+ "publishConfig": {
37
+ "access": "public"
38
+ },
39
+ "keywords": [
40
+ "mcp",
41
+ "local",
42
+ "agent",
43
+ "chatgpt",
44
+ "claude",
45
+ "windows",
46
+ "macos",
47
+ "linux"
48
+ ],
49
+ "localCommander": {
50
+ "controlPlaneOrigin": "https://localcmd.app"
51
+ },
52
+ "homepage": "https://localcmd.app",
53
+ "bugs": {
54
+ "url": "https://localcmd.app/support",
55
+ "email": "peter.mamrukov@localcmd.app"
56
+ }
57
+ }
Binary file
Binary file
Binary file
package/runtime/agent.mjs CHANGED
@@ -25388,6 +25388,7 @@ async function secretCommand(executable, args, input2) {
25388
25388
  async function secretServicePresent() {
25389
25389
  const bus = path3.join("/run/user", String(process.getuid()), "bus");
25390
25390
  if (!process.env.DBUS_SESSION_BUS_ADDRESS && await absent(bus)) return false;
25391
+ if (await absent("/usr/bin/gdbus")) return false;
25391
25392
  const args = ["call", "--session", "--dest", "org.freedesktop.DBus", "--object-path", "/org/freedesktop/DBus", "--method"];
25392
25393
  for (const method of ["org.freedesktop.DBus.ListNames", "org.freedesktop.DBus.ListActivatableNames"]) {
25393
25394
  const output2 = (await secretCommand("/usr/bin/gdbus", [...args, method])).toString("utf8");
@@ -26484,6 +26485,7 @@ var NativeCapacitySchema = external_exports.object({ type: external_exports.lite
26484
26485
  var NATIVE_STATUS_CAPABILITY = "nativeStatusV1";
26485
26486
  var NativeStatusSchema = NativeCapacitySchema.extend({ type: external_exports.literal("nativeStatus"), active: external_exports.number().int().min(0).max(32), queued: external_exports.number().int().min(0).max(256), paused: external_exports.boolean() }).strict().refine((v) => v.active <= v.safeSlots);
26486
26487
  var TerminalEventSchema = external_exports.object({ type: external_exports.literal("executionResult"), protocolVersion: external_exports.literal(1), requestId: uuid11, tenantId: uuid11, deviceId: uuid11, connectionEpoch: epoch2, binding: ExecutionBindingSchema, state: external_exports.enum(["finished", "failed", "cancelled", "unknown", "rejected"]), resultCode: external_exports.string().regex(/^[A-Z0-9_]{1,40}$/), metrics: TerminalMetricsSchema.optional() }).strict();
26488
+ var HeldLeasesFrameSchema = external_exports.object({ type: external_exports.literal("heldLeases"), protocolVersion: external_exports.literal(1), connectionEpoch: epoch2, bindings: external_exports.array(ExecutionBindingSchema).min(1).max(64) }).strict();
26487
26489
  var TerminalEventAckSchema = external_exports.object({ type: external_exports.literal("executionResultAck"), protocolVersion: external_exports.literal(1), requestId: uuid11, ok: external_exports.boolean() }).strict();
26488
26490
  function canonicalJson(value) {
26489
26491
  function canonical2(v) {
@@ -26763,9 +26765,19 @@ var NativeJournal = class _NativeJournal {
26763
26765
  sizes = /* @__PURE__ */ new Map();
26764
26766
  digests = /* @__PURE__ */ new Map();
26765
26767
  totalBytes = 0;
26768
+ /** First connection epoch this journal saw (#448): from then on every received request has an entry. "0" for a journal
26769
+ * an older agent created (continuous since pairing); null until a new journal's first connection. */
26770
+ firstEpoch = null;
26766
26771
  static async open(directory, box, options = {}) {
26767
26772
  const maxEntries = options.maxEntries ?? 2e3, maxBytes = options.maxBytes ?? 64 * 1048576, retentionMs = options.retentionMs ?? 864e5;
26768
26773
  if (!Number.isInteger(maxEntries) || maxEntries < 1 || maxEntries > 1e4 || !Number.isInteger(maxBytes) || maxBytes < 1024 || maxBytes > 64 * 1048576 || !Number.isInteger(retentionMs) || retentionMs < 0 || retentionMs > 30 * 864e5) throw failure2("JOURNAL_LIMIT_INVALID");
26774
+ let existed = true;
26775
+ try {
26776
+ await lstat2(path6.resolve(directory));
26777
+ } catch (e) {
26778
+ if (e.code !== "ENOENT") throw e;
26779
+ existed = false;
26780
+ }
26769
26781
  const dir = await safeDirectory(directory), lockPath = path6.join(dir, "journal.lock"), nonce2 = randomBytes4(16).toString("hex");
26770
26782
  const guard = options.lockHelper ? await acquirePosixJournalGuard(dir, options.lockHelper) : void 0;
26771
26783
  let file2;
@@ -26781,6 +26793,7 @@ var NativeJournal = class _NativeJournal {
26781
26793
  await file2.writeFile(JSON.stringify({ pid: process.pid, nonce: nonce2, createdAt: journal.now().toISOString() }));
26782
26794
  await file2.sync();
26783
26795
  await journal.load();
26796
+ await journal.loadStart(existed);
26784
26797
  return journal;
26785
26798
  } catch (e) {
26786
26799
  await file2.close().catch(() => {
@@ -26862,6 +26875,65 @@ var NativeJournal = class _NativeJournal {
26862
26875
  this.digests.set(id2, digest2(envelope));
26863
26876
  }
26864
26877
  }
26878
+ async loadStart(existed) {
26879
+ const filename = path6.join(this.dir, "journal.start");
26880
+ let envelope;
26881
+ try {
26882
+ const stat2 = await lstat2(filename);
26883
+ if (!stat2.isFile() || stat2.isSymbolicLink() || stat2.size > 4096) throw failure2("JOURNAL_CORRUPT");
26884
+ envelope = await readFile3(filename);
26885
+ } catch (e) {
26886
+ if (e.code !== "ENOENT") throw e;
26887
+ return this.writeStart(existed ? "0" : null);
26888
+ }
26889
+ try {
26890
+ const value = JSON.parse(this.box.open(envelope, "journal-start").toString("utf8"));
26891
+ if (value?.version !== 1 || Object.keys(value).length !== 2 || value.firstEpoch !== null && (typeof value.firstEpoch !== "string" || !/^(0|[1-9]\d{0,18})$/.test(value.firstEpoch))) throw Error();
26892
+ this.firstEpoch = value.firstEpoch;
26893
+ } catch {
26894
+ throw failure2("JOURNAL_CORRUPT");
26895
+ }
26896
+ }
26897
+ async writeStart(firstEpoch) {
26898
+ const temporary = path6.join(this.dir, "journal-start." + this.lock.nonce + "." + randomBytes4(8).toString("hex") + ".tmp");
26899
+ const handle = await open3(temporary, constants2.O_CREAT | constants2.O_EXCL | constants2.O_WRONLY, 384);
26900
+ try {
26901
+ await handle.writeFile(this.box.seal(Buffer.from(JSON.stringify({ version: 1, firstEpoch })), "journal-start"));
26902
+ await handle.sync();
26903
+ await handle.close();
26904
+ await rename(temporary, path6.join(this.dir, "journal.start"));
26905
+ if (process.platform !== "win32") {
26906
+ const d = await open3(this.dir, constants2.O_RDONLY);
26907
+ try {
26908
+ await d.sync();
26909
+ } finally {
26910
+ await d.close();
26911
+ }
26912
+ }
26913
+ } catch (e) {
26914
+ await handle.close().catch(() => {
26915
+ });
26916
+ await rm2(temporary, { force: true }).catch(() => {
26917
+ });
26918
+ throw e;
26919
+ }
26920
+ this.firstEpoch = firstEpoch;
26921
+ }
26922
+ /** Called on every connection before any request of it is served; only the first one is recorded. */
26923
+ recordConnection(connectionEpoch) {
26924
+ return this.enqueue(async () => {
26925
+ if (!/^[1-9]\d{0,18}$/.test(connectionEpoch)) throw failure2("JOURNAL_INPUT_INVALID");
26926
+ if (this.firstEpoch === null) await this.writeStart(connectionEpoch);
26927
+ });
26928
+ }
26929
+ /** True only when this journal existed for the whole connection that leased the request and has no entry for it:
26930
+ * every received request is journaled synchronously on arrival, before it can start (#448). */
26931
+ neverReceived(executionId, connectionEpoch) {
26932
+ return this.enqueue(async () => {
26933
+ if (!uuid14.test(executionId) || !/^[1-9]\d{0,18}$/.test(connectionEpoch)) throw failure2("JOURNAL_INPUT_INVALID");
26934
+ return this.firstEpoch !== null && BigInt(connectionEpoch) >= BigInt(this.firstEpoch) && !this.entries.has(executionId);
26935
+ });
26936
+ }
26865
26937
  async persist(entry) {
26866
26938
  const st = await lstat2(this.dir);
26867
26939
  if (!st.isDirectory() || st.isSymbolicLink()) throw failure2("JOURNAL_DIRECTORY_INVALID");
@@ -27330,7 +27402,7 @@ async function choosePosixFolder() {
27330
27402
  }
27331
27403
  async function choosePosixAccess() {
27332
27404
  return terminal2(async (ask) => {
27333
- if (process.platform === "darwin") await ask("This experimental macOS runtime runs each command as its own launchd job and stops that job's processes when the command ends; work handed to other services is not stopped. Press Enter to review access: ");
27405
+ if (process.platform === "darwin") await ask("Local Commander runs each command as its own launchd job and stops that job's processes when the command ends; work handed to other services is not stopped. Press Enter to review access: ");
27334
27406
  const choice = await ask("Local Commander access\n1. One folder: read and write files\n2. Full Access: account capabilities across this computer, including commands and localhost\nEnter 1 or 2 (empty cancels): ");
27335
27407
  if (choice !== "1" && choice !== "2") return null;
27336
27408
  const folder = choice === "2" ? "/" : await ask("Absolute folder path: ");
@@ -28370,11 +28442,14 @@ var WindowsPrivateStore = class _WindowsPrivateStore {
28370
28442
  }
28371
28443
  /** Replace scope only after an explicit local dialog; updates never call this implicitly. */
28372
28444
  async configureAccess() {
28445
+ let account = "";
28373
28446
  const expected = await this.run(() => this.withLock(async () => {
28374
- if (!(await this.readState())?.enrollment) throw fail2("ENROLLMENT_REQUIRED");
28447
+ const enrollment = (await this.readState())?.enrollment;
28448
+ if (!enrollment) throw fail2("ENROLLMENT_REQUIRED");
28449
+ account = enrollment.accountEmail;
28375
28450
  return this.rawHash();
28376
28451
  }));
28377
- const decision = await this.chooseAccess();
28452
+ const decision = await this.chooseAccess(account);
28378
28453
  if (decision === null) return null;
28379
28454
  const selected = parseAccessSelection(decision);
28380
28455
  if (!selected) return null;
@@ -28417,9 +28492,9 @@ var WindowsPrivateStore = class _WindowsPrivateStore {
28417
28492
  return structuredClone(workspaces);
28418
28493
  }));
28419
28494
  }
28420
- chooseAccess() {
28495
+ chooseAccess(account) {
28421
28496
  if (this.posix) return this.run(() => choosePosixAccess());
28422
- return this.run(async () => parseAccessSelection(await _WindowsPrivateStore.invoke(this.bridge, "choose-access", {}, 24e4, true)));
28497
+ return this.run(async () => parseAccessSelection(await _WindowsPrivateStore.invoke(this.bridge, "choose-access", { data: Buffer.from(account, "utf8").toString("base64") }, 24e4, true)));
28423
28498
  }
28424
28499
  loadPermissionHistory() {
28425
28500
  return this.run(async () => structuredClone((await this.readState())?.permissionHistory ?? []));
@@ -31006,6 +31081,7 @@ var NativeRelayBridge = class {
31006
31081
  async connected(connection) {
31007
31082
  if (this.connection) await this.disconnected();
31008
31083
  this.connection = { ...connection };
31084
+ await this.journal.recordConnection?.(connection.connectionEpoch);
31009
31085
  this.runtime.connected(connection, (frame) => {
31010
31086
  if (frame.type === "control_result") this.control.get(frame.requestId)?.resolve(frame);
31011
31087
  else if ("binding" in frame) this.execution.get(frame.binding.executionId)?.resolve(frame);
@@ -31033,6 +31109,22 @@ var NativeRelayBridge = class {
31033
31109
  }
31034
31110
  }
31035
31111
  }
31112
+ /** Leases the cloud holds from earlier connections that this device never accepted (#448). Each one the journal
31113
+ * never received gets the same never-started receipt as #143; the cloud checks it against its own rows. */
31114
+ async heldLeases(bindings) {
31115
+ for (const binding2 of bindings) {
31116
+ const connection = this.connection, id2 = binding2.executionId;
31117
+ if (!connection || binding2.tenantId !== connection.tenantId || binding2.deviceId !== connection.deviceId || BigInt(binding2.connectionEpoch) >= BigInt(connection.connectionEpoch)) continue;
31118
+ if (this.terminalSending.has(id2)) continue;
31119
+ this.terminalSending.add(id2);
31120
+ try {
31121
+ if (await this.journal.neverReceived?.(id2, binding2.connectionEpoch)) await this.sendTerminal(binding2, "rejected", "PRE_ACCEPTANCE_REJECTED");
31122
+ } catch {
31123
+ } finally {
31124
+ this.terminalSending.delete(id2);
31125
+ }
31126
+ }
31127
+ }
31036
31128
  async disconnected() {
31037
31129
  this.connection = null;
31038
31130
  for (const pending of [...this.execution.values(), ...this.control.values()]) pending.resolve(null);
@@ -46032,7 +46124,8 @@ async function connectOnce(options) {
46032
46124
  tenantId,
46033
46125
  deviceId,
46034
46126
  guardIdentity: true,
46035
- ...options.aiConnectivity ? { aiConnectivity: true } : {}
46127
+ ...options.aiConnectivity ? { aiConnectivity: true } : {},
46128
+ ...options.onHeldLeases ? { heldLeases: true } : {}
46036
46129
  }, maxFrameBytes);
46037
46130
  const challenge = validateChallenge(await waitForMessage(socket, maxFrameBytes, connectTimeoutMs, "RELAY_HANDSHAKE_TIMEOUT"), protocolVersion2);
46038
46131
  const machine = challenge.machineIdentity ? options.machineIdentity?.(deviceId) ?? null : null;
@@ -46066,8 +46159,22 @@ async function connectOnce(options) {
46066
46159
  const { safeSlots, active, queued, paused } = nativeStatus();
46067
46160
  sendJson(socket, NativeStatusSchema.parse({ type: "nativeStatus", protocolVersion: 1, connectionEpoch, agentVersion: options.agentVersion, safeSlots, active, queued, paused }), maxFrameBytes);
46068
46161
  };
46162
+ let watchdog;
46163
+ const armWatchdog = () => {
46164
+ clearTimeout(watchdog);
46165
+ watchdog = setTimeout(() => {
46166
+ process.stderr.write("RELAY_HEARTBEAT_LOST\n");
46167
+ socket.terminate();
46168
+ }, monitorHeartbeat * 3);
46169
+ watchdog.unref();
46170
+ };
46171
+ if (monitorHeartbeat) {
46172
+ armWatchdog();
46173
+ socket.once("close", () => clearTimeout(watchdog));
46174
+ }
46069
46175
  if (monitorHeartbeat) socket.on("ping", (data) => {
46070
46176
  if (data.toString() !== "lcmcp-monitor:" + connectionEpoch) return;
46177
+ armWatchdog();
46071
46178
  try {
46072
46179
  void Promise.resolve(options.onMonitorHeartbeat?.()).catch(() => {
46073
46180
  });
@@ -46155,6 +46262,15 @@ async function connectOnce(options) {
46155
46262
  ack.data.ok ? pending.resolve() : pending.reject(new RelayClientError("RELAY_RESULT_REJECTED"));
46156
46263
  return;
46157
46264
  }
46265
+ if (frame.type === "heldLeases") {
46266
+ const held = HeldLeasesFrameSchema.safeParse(frame);
46267
+ if (!held.success || !options.onHeldLeases || held.data.connectionEpoch !== connectionEpoch) fail5("RELAY_FRAME_INVALID");
46268
+ try {
46269
+ options.onHeldLeases(held.data.bindings);
46270
+ } catch {
46271
+ }
46272
+ return;
46273
+ }
46158
46274
  if (frame.type === "workspace_ack") {
46159
46275
  const ack = ServerFrameSchema.safeParse(frame);
46160
46276
  if (!ack.success || ack.data.type !== "workspace_ack" || !pendingWorkspace.has(ack.data.requestId)) fail5("RELAY_FRAME_INVALID");
@@ -46364,6 +46480,51 @@ async function readMachineGuid(timeoutMs2 = 5e3) {
46364
46480
  child.on("close", (code) => finish(code === 0 ? pattern.exec(output2)?.[1]?.toLowerCase() ?? null : null));
46365
46481
  });
46366
46482
  }
46483
+ async function readComputerName(timeoutMs2 = 5e3) {
46484
+ const valid = (name2) => !!name2 && name2.length <= 120 && name2.trim() === name2 && !/[\u0000-\u001f\u007f]/.test(name2);
46485
+ if (process.platform === "linux") {
46486
+ let handle;
46487
+ try {
46488
+ handle = await open7("/etc/hostname", constants8.O_RDONLY | constants8.O_NOFOLLOW);
46489
+ const data = Buffer.alloc(257), { bytesRead } = await handle.read(data, 0, data.length, 0);
46490
+ const name2 = data.subarray(0, bytesRead).toString("utf8").trim();
46491
+ if (bytesRead <= 256 && valid(name2)) return name2;
46492
+ } catch {
46493
+ } finally {
46494
+ await handle?.close().catch(() => {
46495
+ });
46496
+ }
46497
+ }
46498
+ if (process.platform === "darwin") {
46499
+ const name2 = await new Promise((resolve) => {
46500
+ let output2 = "", settled = false;
46501
+ const finish = (value) => {
46502
+ if (!settled) {
46503
+ settled = true;
46504
+ clearTimeout(timer);
46505
+ resolve(value);
46506
+ }
46507
+ };
46508
+ const child = spawn5("/usr/sbin/scutil", ["--get", "ComputerName"], { shell: false, stdio: ["ignore", "pipe", "ignore"], env: buildChildEnvironment(process.env) });
46509
+ const timer = setTimeout(() => {
46510
+ child.kill();
46511
+ finish("");
46512
+ }, timeoutMs2);
46513
+ child.stdout.setEncoding("utf8");
46514
+ child.stdout.on("data", (chunk) => {
46515
+ if (output2.length + chunk.length <= 1024) output2 += chunk;
46516
+ else {
46517
+ child.kill();
46518
+ finish("");
46519
+ }
46520
+ });
46521
+ child.on("error", () => finish(""));
46522
+ child.on("close", (code) => finish(code === 0 ? output2.replace(/\n$/, "") : ""));
46523
+ });
46524
+ if (valid(name2)) return name2;
46525
+ }
46526
+ return os5.hostname();
46527
+ }
46367
46528
  function machineProof(deviceId, machineGuid, computerName = os5.hostname()) {
46368
46529
  const name2 = computerName.slice(0, 120);
46369
46530
  if (!name2 || name2.trim() !== name2 || /[\u0000-\u001f\u007f]/.test(name2)) return null;
@@ -49273,7 +49434,7 @@ async function restoreContentAudit(input2, output2, key) {
49273
49434
  }
49274
49435
 
49275
49436
  // agent/src/main.ts
49276
- var AGENT_VERSION2 = true ? "0.3.43" : "0.3.2";
49437
+ var AGENT_VERSION2 = true ? "0.3.45" : "0.3.2";
49277
49438
  var NETWORK_PUBLIC_KEY = true ? "-----BEGIN PUBLIC KEY-----\nMIIBojANBgkqhkiG9w0BAQEFAAOCAY8AMIIBigKCAYEAvmu+81vWtJBQDZNHcAIW\nzCSxpG+R9RBsum2df5mP2cOW+wyUDTpnYquFge1qgI5YYTF96j0xCyXL8B9FeNWs\n939opWNmv53j+CQlS4cEIQhBbZSCu0eC4icsVEzBhvf68MB+E668SOtZBcVTEnyh\n96PTcupxS10rQJBWDinbKBXRC2gvGYeu01rWa/nsrndy4vAYP5t3yQtdOC8e8mr0\n+2Mg2Qhiq+KiTCtDd+L9oXvoLqqJYbzUIveNGha25AWxF1Weblp0gOM87hell5gV\nK7q4ZLUq/wBRGYNmQRdLyV7/WRE1FFPSW21/SBZQ/5aU4UqJU/MjMcrJUrnRKngz\nYKmo0czkxS3N7Q2KVxomgR1AQSuQFKMrboZYKjShADp9RIYUxzs7cYg51Of5PnHh\n6Ax+VevDGfzjMD+yHuyxYZqCtX4OwgolJxz63UQbzKlris+8xb5lLYTbIuz90Pss\nn6HK/F5May25c6oMmS3nh/NsiZopL1OvdtWKTt1L4WlRAgMBAAE=\n-----END PUBLIC KEY-----\n" : "";
49278
49439
  var AI_CONNECTION_SHIPPED = false;
49279
49440
  function processAlive(pid) {
@@ -49352,7 +49513,8 @@ async function runAgent(options) {
49352
49513
  throw error62;
49353
49514
  }
49354
49515
  const machineGuid = await readMachineGuid();
49355
- const machineIdentity = machineGuid ? (deviceId) => machineProof(deviceId, machineGuid) : void 0;
49516
+ let computerName = "";
49517
+ const machineIdentity = machineGuid ? (deviceId) => machineProof(deviceId, machineGuid, computerName) : void 0;
49356
49518
  const ledger = options.efficiencyLedger;
49357
49519
  monitor(() => options.monitor?.setLifetimeSavings(ledger?.savingsSnapshot?.()));
49358
49520
  const efficiencyLedger = ledger && {
@@ -49419,6 +49581,7 @@ async function runAgent(options) {
49419
49581
  });
49420
49582
  try {
49421
49583
  while (!options.signal.aborted) {
49584
+ computerName = await readComputerName();
49422
49585
  let ready;
49423
49586
  const readyPromise = new Promise((resolve) => {
49424
49587
  ready = resolve;
@@ -49433,6 +49596,11 @@ async function runAgent(options) {
49433
49596
  safeSlots: runtime.status().safeSlots,
49434
49597
  status: () => runtime.status(),
49435
49598
  onIncidentAck: (ack) => incidentProducer.ack(ack),
49599
+ // After bridge.connected, like requests: the journal has recorded this connection first.
49600
+ onHeldLeases: (bindings) => {
49601
+ void readyPromise.then(() => bridge.heldLeases(bindings)).catch(() => {
49602
+ });
49603
+ },
49436
49604
  handleRequest: async (request) => {
49437
49605
  await readyPromise;
49438
49606
  monitor(() => {