local-commander-agent 0.3.47 → 0.3.49
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 +218 -207
- package/README.md +216 -216
- package/bin/command-path.mjs +223 -223
- package/bin/localcommander.mjs +872 -872
- package/bin/monitor-agents.mjs +162 -162
- package/bin/monitor-context.d.mts +7 -7
- package/bin/monitor-context.mjs +65 -65
- package/bin/monitor-mcp.mjs +136 -136
- package/bin/monitor-savings.d.mts +20 -20
- package/bin/monitor-savings.mjs +71 -71
- package/bin/monitor.mjs +530 -530
- package/bin/network-package.mjs +38 -38
- package/bin/posix.mjs +84 -84
- package/package.json +57 -57
- package/runtime/JobHost.exe +0 -0
- package/runtime/LocalCommanderAgent.exe +0 -0
- package/runtime/OwnerBridge.exe +0 -0
- package/runtime/agent.mjs +35 -6
package/bin/network-package.mjs
CHANGED
|
@@ -1,38 +1,38 @@
|
|
|
1
|
-
import fs from "node:fs/promises";
|
|
2
|
-
import path from "node:path";
|
|
3
|
-
import { createHash } from "node:crypto";
|
|
4
|
-
|
|
5
|
-
const HASH=/^[a-f0-9]{64}$/;
|
|
6
|
-
/** Signed bundles may be set up during install; an unsigned pilot only by an explicit owner command. */
|
|
7
|
-
export function implicitSetupAllowed(manifest){return manifest?.signed===true;}
|
|
8
|
-
const FILES=["JobHost.exe","libcronet.dll","sing-box.exe","wintun.dll","sing-box.LICENSE","wintun.LICENSE"];
|
|
9
|
-
async function plainDirectory(directory) {
|
|
10
|
-
const stat=await fs.lstat(directory);
|
|
11
|
-
if(!stat.isDirectory()||stat.isSymbolicLink()||(await fs.realpath(directory)).toLowerCase()!==path.resolve(directory).toLowerCase())throw Error("NETWORK_BUNDLE_DIRECTORY_INVALID");
|
|
12
|
-
}
|
|
13
|
-
async function bytes(file,max) {
|
|
14
|
-
const stat=await fs.lstat(file);
|
|
15
|
-
if(!stat.isFile()||stat.isSymbolicLink()||stat.size<1||stat.size>max)throw Error("NETWORK_BUNDLE_FILE_INVALID");
|
|
16
|
-
return fs.readFile(file);
|
|
17
|
-
}
|
|
18
|
-
export async function inspectNetworkBundle(directory) {
|
|
19
|
-
try {await plainDirectory(directory);}catch(error){if(error.code==="ENOENT")return null;throw error;}
|
|
20
|
-
const manifest=JSON.parse((await bytes(path.join(directory,"manifest.json"),8192)).toString("utf8"));
|
|
21
|
-
if(manifest.schemaVersion!==1||manifest.configured!==true||!(manifest.signed===true||manifest.signing==="unsigned-pilot")||!HASH.test(manifest.bundleId)||!HASH.test(manifest.helperSha256)||
|
|
22
|
-
!manifest.files||typeof manifest.files!=="object"||Array.isArray(manifest.files)||Object.keys(manifest.files).sort().join()!==[...FILES].sort().join())throw Error("NETWORK_BUNDLE_NOT_DEPLOYABLE");
|
|
23
|
-
const names=[...FILES,"NetworkService.exe","manifest.json"];
|
|
24
|
-
if((await fs.readdir(directory)).sort().join()!==names.sort().join())throw Error("NETWORK_BUNDLE_ALLOWLIST_FAILED");
|
|
25
|
-
for(const name of [...FILES,"NetworkService.exe"]){const expected=name==="NetworkService.exe"?manifest.helperSha256:manifest.files[name];
|
|
26
|
-
if(!HASH.test(expected)||createHash("sha256").update(await bytes(path.join(directory,name),128*1024*1024)).digest("hex")!==expected)throw Error("NETWORK_BUNDLE_DIGEST_MISMATCH");}
|
|
27
|
-
return {bundleId:manifest.bundleId,files:names,manifest};
|
|
28
|
-
}
|
|
29
|
-
export async function copyNetworkBundle(source,destination) {
|
|
30
|
-
const bundle=await inspectNetworkBundle(source);if(!bundle)return false;
|
|
31
|
-
if(path.resolve(source).toLowerCase()===path.resolve(destination).toLowerCase())return true;
|
|
32
|
-
const exists=await fs.lstat(destination).catch(e=>{if(e.code!=="ENOENT")throw e;return null;});
|
|
33
|
-
if(exists){const previous=await inspectNetworkBundle(destination);if(previous?.bundleId===bundle.bundleId&&previous.manifest.helperSha256===bundle.manifest.helperSha256)return true;throw Error("NETWORK_BUNDLE_DESTINATION_CONFLICT");}
|
|
34
|
-
await fs.mkdir(destination,{recursive:false,mode:0o700});
|
|
35
|
-
try{for(const name of bundle.files)await fs.copyFile(path.join(source,name),path.join(destination,name),fs.constants.COPYFILE_EXCL);await inspectNetworkBundle(destination);}
|
|
36
|
-
catch(error){for(const name of bundle.files)await fs.rm(path.join(destination,name),{force:true}).catch(()=>{});await fs.rmdir(destination).catch(()=>{});throw error;}
|
|
37
|
-
return true;
|
|
38
|
-
}
|
|
1
|
+
import fs from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { createHash } from "node:crypto";
|
|
4
|
+
|
|
5
|
+
const HASH=/^[a-f0-9]{64}$/;
|
|
6
|
+
/** Signed bundles may be set up during install; an unsigned pilot only by an explicit owner command. */
|
|
7
|
+
export function implicitSetupAllowed(manifest){return manifest?.signed===true;}
|
|
8
|
+
const FILES=["JobHost.exe","libcronet.dll","sing-box.exe","wintun.dll","sing-box.LICENSE","wintun.LICENSE"];
|
|
9
|
+
async function plainDirectory(directory) {
|
|
10
|
+
const stat=await fs.lstat(directory);
|
|
11
|
+
if(!stat.isDirectory()||stat.isSymbolicLink()||(await fs.realpath(directory)).toLowerCase()!==path.resolve(directory).toLowerCase())throw Error("NETWORK_BUNDLE_DIRECTORY_INVALID");
|
|
12
|
+
}
|
|
13
|
+
async function bytes(file,max) {
|
|
14
|
+
const stat=await fs.lstat(file);
|
|
15
|
+
if(!stat.isFile()||stat.isSymbolicLink()||stat.size<1||stat.size>max)throw Error("NETWORK_BUNDLE_FILE_INVALID");
|
|
16
|
+
return fs.readFile(file);
|
|
17
|
+
}
|
|
18
|
+
export async function inspectNetworkBundle(directory) {
|
|
19
|
+
try {await plainDirectory(directory);}catch(error){if(error.code==="ENOENT")return null;throw error;}
|
|
20
|
+
const manifest=JSON.parse((await bytes(path.join(directory,"manifest.json"),8192)).toString("utf8"));
|
|
21
|
+
if(manifest.schemaVersion!==1||manifest.configured!==true||!(manifest.signed===true||manifest.signing==="unsigned-pilot")||!HASH.test(manifest.bundleId)||!HASH.test(manifest.helperSha256)||
|
|
22
|
+
!manifest.files||typeof manifest.files!=="object"||Array.isArray(manifest.files)||Object.keys(manifest.files).sort().join()!==[...FILES].sort().join())throw Error("NETWORK_BUNDLE_NOT_DEPLOYABLE");
|
|
23
|
+
const names=[...FILES,"NetworkService.exe","manifest.json"];
|
|
24
|
+
if((await fs.readdir(directory)).sort().join()!==names.sort().join())throw Error("NETWORK_BUNDLE_ALLOWLIST_FAILED");
|
|
25
|
+
for(const name of [...FILES,"NetworkService.exe"]){const expected=name==="NetworkService.exe"?manifest.helperSha256:manifest.files[name];
|
|
26
|
+
if(!HASH.test(expected)||createHash("sha256").update(await bytes(path.join(directory,name),128*1024*1024)).digest("hex")!==expected)throw Error("NETWORK_BUNDLE_DIGEST_MISMATCH");}
|
|
27
|
+
return {bundleId:manifest.bundleId,files:names,manifest};
|
|
28
|
+
}
|
|
29
|
+
export async function copyNetworkBundle(source,destination) {
|
|
30
|
+
const bundle=await inspectNetworkBundle(source);if(!bundle)return false;
|
|
31
|
+
if(path.resolve(source).toLowerCase()===path.resolve(destination).toLowerCase())return true;
|
|
32
|
+
const exists=await fs.lstat(destination).catch(e=>{if(e.code!=="ENOENT")throw e;return null;});
|
|
33
|
+
if(exists){const previous=await inspectNetworkBundle(destination);if(previous?.bundleId===bundle.bundleId&&previous.manifest.helperSha256===bundle.manifest.helperSha256)return true;throw Error("NETWORK_BUNDLE_DESTINATION_CONFLICT");}
|
|
34
|
+
await fs.mkdir(destination,{recursive:false,mode:0o700});
|
|
35
|
+
try{for(const name of bundle.files)await fs.copyFile(path.join(source,name),path.join(destination,name),fs.constants.COPYFILE_EXCL);await inspectNetworkBundle(destination);}
|
|
36
|
+
catch(error){for(const name of bundle.files)await fs.rm(path.join(destination,name),{force:true}).catch(()=>{});await fs.rmdir(destination).catch(()=>{});throw error;}
|
|
37
|
+
return true;
|
|
38
|
+
}
|
package/bin/posix.mjs
CHANGED
|
@@ -1,84 +1,84 @@
|
|
|
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
|
-
// Owner opt-out for take_screenshot under Full Access (#486): a marker in the installed service agent's state folder
|
|
32
|
-
// (the service runs without XDG_DATA_HOME). No runtime staging is needed; the agent checks it on each capture.
|
|
33
|
-
async function screenshotCommand(args) {
|
|
34
|
-
const action = args[0] || "status";
|
|
35
|
-
if (args.length > 1 || !["status", "on", "off"].includes(action)) fail("ARGUMENT_INVALID");
|
|
36
|
-
if (typeof process.getuid === "function" && process.getuid() === 0) fail("POSIX_OWNER_REQUIRED");
|
|
37
|
-
const state = path.join(os.homedir(), process.platform === "darwin" ? "Library/Application Support" : ".local/share", "LocalCommander", "Agent");
|
|
38
|
-
const folder = await fs.lstat(state).catch(() => null);
|
|
39
|
-
if (!folder?.isDirectory() || folder.uid !== process.getuid()) fail("AGENT_NOT_INSTALLED");
|
|
40
|
-
const file = path.join(state, "screenshot-disabled-by-owner");
|
|
41
|
-
if (action === "off") await fs.open(file, constants.O_WRONLY | constants.O_CREAT | constants.O_EXCL | constants.O_NOFOLLOW, 0o600)
|
|
42
|
-
.then(handle => handle.close(), error => { if (error?.code !== "EEXIST") throw error; });
|
|
43
|
-
if (action === "on") await fs.rm(file, { force: true });
|
|
44
|
-
const off = await fs.lstat(file).then(() => true, error => error?.code !== "ENOENT");
|
|
45
|
-
process.stdout.write(JSON.stringify({ screenshot: off ? "off" : "on" }) + "\n");
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
export async function runPosix(argv, version) {
|
|
49
|
-
const name = PACKAGES[process.platform + "-" + process.arch];
|
|
50
|
-
if (!name) fail(process.platform === "darwin" ? "MACOS_APPLE_SILICON_REQUIRED" : process.platform === "linux" ? "LINUX_X64_OR_ARM64_REQUIRED" : "PLATFORM_UNSUPPORTED");
|
|
51
|
-
const command = argv[0] || "bootstrap", args = argv.slice(1);
|
|
52
|
-
if (command === "-h" || command === "--help" || command === "help") { process.stdout.write(posixHelp()); return; }
|
|
53
|
-
if (command === "screenshot") { await screenshotCommand(args); return; }
|
|
54
|
-
const lifecycle = COMMANDS[command];
|
|
55
|
-
if (!lifecycle) fail("ARGUMENT_INVALID");
|
|
56
|
-
if (lifecycle === "uninstall" ? args.length > 1 || (args.length === 1 && args[0] !== "--purge-state") : args.length) fail("ARGUMENT_INVALID");
|
|
57
|
-
if (typeof process.getuid === "function" && process.getuid() === 0) fail("POSIX_OWNER_REQUIRED");
|
|
58
|
-
|
|
59
|
-
let packageJson;
|
|
60
|
-
try { packageJson = createRequire(import.meta.url).resolve(name + "/package.json"); } catch { fail("PLATFORM_PACKAGE_MISSING"); }
|
|
61
|
-
const meta = JSON.parse(await fs.readFile(packageJson, "utf8"));
|
|
62
|
-
if (meta.name !== name || meta.version !== version) fail("PLATFORM_PACKAGE_MISMATCH");
|
|
63
|
-
const source = path.join(path.dirname(packageJson), "runtime");
|
|
64
|
-
const manifest = JSON.parse(await fs.readFile(path.join(source, "manifest.json"), "utf8"));
|
|
65
|
-
const files = Object.keys(manifest?.files ?? {});
|
|
66
|
-
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");
|
|
67
|
-
|
|
68
|
-
// Canonical parent: on macOS os.tmpdir() is under /var, a symlink to /private/var, and the lifecycle rejects aliases.
|
|
69
|
-
const stage = await fs.mkdtemp(path.join(await fs.realpath(os.tmpdir()), "local-commander-"));
|
|
70
|
-
try {
|
|
71
|
-
await fs.chmod(stage, 0o700);
|
|
72
|
-
for (const file of [...files, "manifest.json"]) {
|
|
73
|
-
const target = path.join(stage, file);
|
|
74
|
-
await fs.copyFile(path.join(source, file), target, constants.COPYFILE_EXCL);
|
|
75
|
-
await fs.chmod(target, EXECUTABLES.has(file) ? 0o700 : 0o600);
|
|
76
|
-
}
|
|
77
|
-
// The lifecycle verifies every file against the manifest, then copies them into its own versions directory.
|
|
78
|
-
const result = spawnSync(path.join(stage, "node"), [path.join(stage, "lifecycle.mjs"), lifecycle, ...args], { stdio: "inherit" });
|
|
79
|
-
if (result.error) throw result.error;
|
|
80
|
-
process.exitCode = result.status ?? 1;
|
|
81
|
-
} finally {
|
|
82
|
-
await fs.rm(stage, { recursive: true, force: true });
|
|
83
|
-
}
|
|
84
|
-
}
|
|
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
|
+
// Owner opt-out for take_screenshot under Full Access (#486): a marker in the installed service agent's state folder
|
|
32
|
+
// (the service runs without XDG_DATA_HOME). No runtime staging is needed; the agent checks it on each capture.
|
|
33
|
+
async function screenshotCommand(args) {
|
|
34
|
+
const action = args[0] || "status";
|
|
35
|
+
if (args.length > 1 || !["status", "on", "off"].includes(action)) fail("ARGUMENT_INVALID");
|
|
36
|
+
if (typeof process.getuid === "function" && process.getuid() === 0) fail("POSIX_OWNER_REQUIRED");
|
|
37
|
+
const state = path.join(os.homedir(), process.platform === "darwin" ? "Library/Application Support" : ".local/share", "LocalCommander", "Agent");
|
|
38
|
+
const folder = await fs.lstat(state).catch(() => null);
|
|
39
|
+
if (!folder?.isDirectory() || folder.uid !== process.getuid()) fail("AGENT_NOT_INSTALLED");
|
|
40
|
+
const file = path.join(state, "screenshot-disabled-by-owner");
|
|
41
|
+
if (action === "off") await fs.open(file, constants.O_WRONLY | constants.O_CREAT | constants.O_EXCL | constants.O_NOFOLLOW, 0o600)
|
|
42
|
+
.then(handle => handle.close(), error => { if (error?.code !== "EEXIST") throw error; });
|
|
43
|
+
if (action === "on") await fs.rm(file, { force: true });
|
|
44
|
+
const off = await fs.lstat(file).then(() => true, error => error?.code !== "ENOENT");
|
|
45
|
+
process.stdout.write(JSON.stringify({ screenshot: off ? "off" : "on" }) + "\n");
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export async function runPosix(argv, version) {
|
|
49
|
+
const name = PACKAGES[process.platform + "-" + process.arch];
|
|
50
|
+
if (!name) fail(process.platform === "darwin" ? "MACOS_APPLE_SILICON_REQUIRED" : process.platform === "linux" ? "LINUX_X64_OR_ARM64_REQUIRED" : "PLATFORM_UNSUPPORTED");
|
|
51
|
+
const command = argv[0] || "bootstrap", args = argv.slice(1);
|
|
52
|
+
if (command === "-h" || command === "--help" || command === "help") { process.stdout.write(posixHelp()); return; }
|
|
53
|
+
if (command === "screenshot") { await screenshotCommand(args); return; }
|
|
54
|
+
const lifecycle = COMMANDS[command];
|
|
55
|
+
if (!lifecycle) fail("ARGUMENT_INVALID");
|
|
56
|
+
if (lifecycle === "uninstall" ? args.length > 1 || (args.length === 1 && args[0] !== "--purge-state") : args.length) fail("ARGUMENT_INVALID");
|
|
57
|
+
if (typeof process.getuid === "function" && process.getuid() === 0) fail("POSIX_OWNER_REQUIRED");
|
|
58
|
+
|
|
59
|
+
let packageJson;
|
|
60
|
+
try { packageJson = createRequire(import.meta.url).resolve(name + "/package.json"); } catch { fail("PLATFORM_PACKAGE_MISSING"); }
|
|
61
|
+
const meta = JSON.parse(await fs.readFile(packageJson, "utf8"));
|
|
62
|
+
if (meta.name !== name || meta.version !== version) fail("PLATFORM_PACKAGE_MISMATCH");
|
|
63
|
+
const source = path.join(path.dirname(packageJson), "runtime");
|
|
64
|
+
const manifest = JSON.parse(await fs.readFile(path.join(source, "manifest.json"), "utf8"));
|
|
65
|
+
const files = Object.keys(manifest?.files ?? {});
|
|
66
|
+
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");
|
|
67
|
+
|
|
68
|
+
// Canonical parent: on macOS os.tmpdir() is under /var, a symlink to /private/var, and the lifecycle rejects aliases.
|
|
69
|
+
const stage = await fs.mkdtemp(path.join(await fs.realpath(os.tmpdir()), "local-commander-"));
|
|
70
|
+
try {
|
|
71
|
+
await fs.chmod(stage, 0o700);
|
|
72
|
+
for (const file of [...files, "manifest.json"]) {
|
|
73
|
+
const target = path.join(stage, file);
|
|
74
|
+
await fs.copyFile(path.join(source, file), target, constants.COPYFILE_EXCL);
|
|
75
|
+
await fs.chmod(target, EXECUTABLES.has(file) ? 0o700 : 0o600);
|
|
76
|
+
}
|
|
77
|
+
// The lifecycle verifies every file against the manifest, then copies them into its own versions directory.
|
|
78
|
+
const result = spawnSync(path.join(stage, "node"), [path.join(stage, "lifecycle.mjs"), lifecycle, ...args], { stdio: "inherit" });
|
|
79
|
+
if (result.error) throw result.error;
|
|
80
|
+
process.exitCode = result.status ?? 1;
|
|
81
|
+
} finally {
|
|
82
|
+
await fs.rm(stage, { recursive: true, force: true });
|
|
83
|
+
}
|
|
84
|
+
}
|
package/package.json
CHANGED
|
@@ -1,57 +1,57 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "local-commander-agent",
|
|
3
|
-
"version": "0.3.
|
|
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.
|
|
20
|
-
"local-commander-agent-linux-arm64": "0.3.
|
|
21
|
-
"local-commander-agent-linux-x64": "0.3.
|
|
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
|
-
}
|
|
1
|
+
{
|
|
2
|
+
"name": "local-commander-agent",
|
|
3
|
+
"version": "0.3.49",
|
|
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.49",
|
|
20
|
+
"local-commander-agent-linux-arm64": "0.3.49",
|
|
21
|
+
"local-commander-agent-linux-x64": "0.3.49"
|
|
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
|
+
}
|
package/runtime/JobHost.exe
CHANGED
|
Binary file
|
|
Binary file
|
package/runtime/OwnerBridge.exe
CHANGED
|
Binary file
|
package/runtime/agent.mjs
CHANGED
|
@@ -27932,11 +27932,16 @@ var WindowsPrivateStore = class _WindowsPrivateStore {
|
|
|
27932
27932
|
if (process.platform !== "win32") for (const name2 of ["owner-key.json", "owner-key.dat", "owner-key.lock"]) approved.add(name2);
|
|
27933
27933
|
if (names.length && !names.includes(MARKER)) throw fail2("STORE_DIRECTORY_UNKNOWN");
|
|
27934
27934
|
const memoryFile = (name2) => name2 === "host-memory.json" || /^host-memory\.json\.[1-9][0-9]{0,9}\.tmp$/.test(name2);
|
|
27935
|
-
|
|
27935
|
+
const switchFile = (name2) => name2 === "screenshot-acceptance-enabled" || name2 === "screenshot-disabled-by-owner";
|
|
27936
|
+
if (names.some((name2) => !approved.has(name2) && !memoryFile(name2) && !switchFile(name2))) throw fail2("STORE_DIRECTORY_UNKNOWN");
|
|
27936
27937
|
for (const name2 of names.filter(memoryFile)) {
|
|
27937
27938
|
const stat2 = await lstat4(path10.join(absolute2, name2));
|
|
27938
27939
|
if (!stat2.isFile() || stat2.isSymbolicLink() || stat2.size > 32768) throw fail2("STORE_DIRECTORY_INVALID");
|
|
27939
27940
|
}
|
|
27941
|
+
for (const name2 of names.filter(switchFile)) {
|
|
27942
|
+
const stat2 = await lstat4(path10.join(absolute2, name2));
|
|
27943
|
+
if (!stat2.isFile() || stat2.isSymbolicLink() || stat2.size > 4096) throw fail2("STORE_DIRECTORY_INVALID");
|
|
27944
|
+
}
|
|
27940
27945
|
for (const name2 of ["agent-process.json", "optimization", ".local-commander"]) {
|
|
27941
27946
|
if (!names.includes(name2)) continue;
|
|
27942
27947
|
const filename = path10.join(absolute2, name2), stat2 = await lstat4(filename);
|
|
@@ -31100,7 +31105,7 @@ var NativeTools = class {
|
|
|
31100
31105
|
if (call.name === "command_run") {
|
|
31101
31106
|
if (!["win32", "linux", "darwin"].includes(process.platform)) fail3("COMMAND_NOT_ALLOWED");
|
|
31102
31107
|
const cwd2 = await resolve(call.args.cwd, true), program = await this.resolveProgram(call.args.command, workspace2, call.args.args);
|
|
31103
|
-
const claims2 = writeClaim(cwd2);
|
|
31108
|
+
const claims2 = call.args.cwd === void 0 ? [{ path: cwd2, namespace: "process", mode: "read" }] : writeClaim(cwd2);
|
|
31104
31109
|
return { call, claims: claims2, execution: { ...program, cwd: cwd2 }, run: async (signal, onStarted, onOutput, onSupervisor) => {
|
|
31105
31110
|
let r;
|
|
31106
31111
|
try {
|
|
@@ -31368,7 +31373,10 @@ var NativeScheduler = class {
|
|
|
31368
31373
|
} catch {
|
|
31369
31374
|
}
|
|
31370
31375
|
}
|
|
31371
|
-
|
|
31376
|
+
/** lockWaitMs: how long a job may wait behind a conflicting claim before it is refused with
|
|
31377
|
+
* RESOURCE_LOCKED (proved not started), so the caller hears back inside its relay window
|
|
31378
|
+
* instead of timing out (#504). Waiting only for a free slot still lasts until the deadline. */
|
|
31379
|
+
schedule(id2, claims, deadlineAt, run2, signal, lockWaitMs) {
|
|
31372
31380
|
if (!this.accepting) return Promise.reject(Error("AGENT_PAUSED"));
|
|
31373
31381
|
if (!id2 || id2.length > 100 || this.active.has(id2) || this.waiting.some((j) => j.id === id2)) return Promise.reject(Error("DUPLICATE_SCHEDULED_EXECUTION"));
|
|
31374
31382
|
if (!Array.isArray(claims) || !claims.length || claims.length > 32 || claims.some((c) => !c || !path19.isAbsolute(c.path) || !["read", "write"].includes(c.mode))) return Promise.reject(Error("INVALID_RESOURCE_CLAIM"));
|
|
@@ -31389,6 +31397,12 @@ var NativeScheduler = class {
|
|
|
31389
31397
|
};
|
|
31390
31398
|
job.aborted = () => remove("CANCELLED_BEFORE_ACCEPTANCE");
|
|
31391
31399
|
job.timer = setTimeout(() => remove("ADMISSION_DEADLINE_EXPIRED"), Math.min(2147483647, Math.max(1, deadline - Date.now())));
|
|
31400
|
+
if (lockWaitMs !== void 0 && Number.isFinite(lockWaitMs) && lockWaitMs >= 0) {
|
|
31401
|
+
const lockIn = Math.min(lockWaitMs, deadline - Date.now() - 1e3);
|
|
31402
|
+
if (lockIn > 0) job.lockTimer = setTimeout(() => {
|
|
31403
|
+
if (this.lockBlocked(job)) remove("RESOURCE_LOCKED");
|
|
31404
|
+
}, lockIn);
|
|
31405
|
+
}
|
|
31392
31406
|
signal?.addEventListener("abort", job.aborted, { once: true });
|
|
31393
31407
|
this.waiting.push(job);
|
|
31394
31408
|
this.observe({ kind: "queued", executionId: id2, deadlineAt: deadline });
|
|
@@ -31397,8 +31411,21 @@ var NativeScheduler = class {
|
|
|
31397
31411
|
}
|
|
31398
31412
|
cleanup(job) {
|
|
31399
31413
|
clearTimeout(job.timer);
|
|
31414
|
+
clearTimeout(job.lockTimer);
|
|
31400
31415
|
if (job.aborted) job.signal?.removeEventListener("abort", job.aborted);
|
|
31401
31416
|
}
|
|
31417
|
+
/** True when a waiting job is held by a conflicting claim: a running job, or an earlier conflicting
|
|
31418
|
+
* waiter that is itself lock-blocked. A wait that is only for a free slot is never a lock. */
|
|
31419
|
+
lockBlocked(job) {
|
|
31420
|
+
const index = this.waiting.indexOf(job);
|
|
31421
|
+
if (index < 0) return false;
|
|
31422
|
+
const blocked2 = [];
|
|
31423
|
+
for (let i = 0; i <= index; i++) {
|
|
31424
|
+
const current2 = this.waiting[i];
|
|
31425
|
+
blocked2[i] = [...this.active.values()].some((claims) => claimsConflict(claims, current2.claims)) || this.waiting.slice(0, i).some((older, j) => blocked2[j] === true && claimsConflict(older.claims, current2.claims));
|
|
31426
|
+
}
|
|
31427
|
+
return blocked2[index] === true;
|
|
31428
|
+
}
|
|
31402
31429
|
pump() {
|
|
31403
31430
|
for (let i = 0; i < this.waiting.length && this.active.size < this.safeSlots; ) {
|
|
31404
31431
|
const job = this.waiting[i];
|
|
@@ -32101,6 +32128,7 @@ var knownCodes = /* @__PURE__ */ new Set([
|
|
|
32101
32128
|
"WORKSPACE_CHANGED",
|
|
32102
32129
|
"WORKSPACE_UNAVAILABLE",
|
|
32103
32130
|
"RESOURCE_BUSY",
|
|
32131
|
+
"RESOURCE_LOCKED",
|
|
32104
32132
|
"AGENT_PAUSED",
|
|
32105
32133
|
"CANCELLED_BEFORE_ACCEPTANCE",
|
|
32106
32134
|
"ADMISSION_DEADLINE_EXPIRED",
|
|
@@ -32115,6 +32143,7 @@ var knownCodes = /* @__PURE__ */ new Set([
|
|
|
32115
32143
|
"SCREEN_ENCODER_UNAVAILABLE",
|
|
32116
32144
|
"SCREEN_RECORDING_PERMISSION_REQUIRED"
|
|
32117
32145
|
]);
|
|
32146
|
+
var LOCK_WAIT_MS = 2e4;
|
|
32118
32147
|
function safeCode(error62) {
|
|
32119
32148
|
const code = error62 instanceof NativeToolError ? error62.code : error62 instanceof Error ? error62.message : "";
|
|
32120
32149
|
return knownCodes.has(code) || /^GUARD_[A-Z_]{1,30}$/.test(code) ? code : "TOOL_FAILED";
|
|
@@ -32326,7 +32355,7 @@ var NativeRuntime = class {
|
|
|
32326
32355
|
const final = await this.options.journal.finish(binding2.executionId, image === void 0 ? journalOutput : { ...journalOutput, stdout: screenshotMetadata(image) }, resultCode);
|
|
32327
32356
|
const outbound = await outboundResult(runnable.call, journalResult(final.result, resultCode), binding2, this.optionalTelemetry, this.options.efficiencyLedger);
|
|
32328
32357
|
this.send({ type: "result", version: 1, binding: binding2, result: image === void 0 ? outbound : { ...outbound, stdout: image } });
|
|
32329
|
-
}, abort.signal);
|
|
32358
|
+
}, abort.signal, LOCK_WAIT_MS);
|
|
32330
32359
|
context.task = work.catch(async (error62) => {
|
|
32331
32360
|
if (!context.accepted) {
|
|
32332
32361
|
if (context.guardPermit) await this.options.guard.rejected(context.guardPermit, safeCode(error62)).catch(() => {
|
|
@@ -48979,7 +49008,7 @@ var NativeEventBuffer = class {
|
|
|
48979
49008
|
|
|
48980
49009
|
// agent/src/native-event-supervisor.ts
|
|
48981
49010
|
var eventKinds = /* @__PURE__ */ new Set(["queued", "admitted", "released", "queue_rejected", "prepared", "accepted", "running", "result", "not_accepted", "connected", "disconnected", "mailbox_expired"]);
|
|
48982
|
-
var codes = /* @__PURE__ */ new Set(["SUCCESS", "EXECUTION_UNKNOWN", "AGENT_RESTARTED", "ADMISSION_DEADLINE_EXPIRED", "CANCELLED_BEFORE_ACCEPTANCE", "RESOURCE_BUSY", "AGENT_PAUSED", "PREPARATION_EXPIRED", "CANCELLED", "TIMEOUT", "USER_PROGRAM_FAILED", "TOOL_FAILED", "MAILBOX_EXPIRED"]);
|
|
49011
|
+
var codes = /* @__PURE__ */ new Set(["SUCCESS", "EXECUTION_UNKNOWN", "AGENT_RESTARTED", "ADMISSION_DEADLINE_EXPIRED", "CANCELLED_BEFORE_ACCEPTANCE", "RESOURCE_BUSY", "RESOURCE_LOCKED", "AGENT_PAUSED", "PREPARATION_EXPIRED", "CANCELLED", "TIMEOUT", "USER_PROGRAM_FAILED", "TOOL_FAILED", "MAILBOX_EXPIRED"]);
|
|
48983
49012
|
var NativeEventSupervisor = class {
|
|
48984
49013
|
constructor(onChange) {
|
|
48985
49014
|
this.onChange = onChange;
|
|
@@ -50811,7 +50840,7 @@ async function restoreContentAudit(input2, output2, key) {
|
|
|
50811
50840
|
}
|
|
50812
50841
|
|
|
50813
50842
|
// agent/src/main.ts
|
|
50814
|
-
var AGENT_VERSION2 = true ? "0.3.
|
|
50843
|
+
var AGENT_VERSION2 = true ? "0.3.49" : "0.3.2";
|
|
50815
50844
|
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" : "";
|
|
50816
50845
|
var AI_CONNECTION_SHIPPED = false;
|
|
50817
50846
|
function processAlive(pid) {
|