local-commander-agent 0.3.6 → 0.3.7

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
@@ -19,8 +19,8 @@ Use **Windows 11 x64** and a currently supported Node.js LTS version meeting the
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 filesystem access in the local Windows dialog:
23
- - **All connected drives — read and write:** approve all currently available drive letters shown in the dialog at once. There is no folder picker or separate approval for each drive.
22
+ 3. Choose access in the local Windows dialog:
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
 
26
26
  Cancel leaves existing permissions unchanged. On first setup, cancellation stops before enabling autostart or starting the agent. Existing grants are preserved when repeating installation or updating: an update never opts you into all drives.
@@ -34,11 +34,13 @@ Use **Windows 11 x64** and a currently supported Node.js LTS version meeting the
34
34
 
35
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.
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.
38
+
37
39
  ## What “all connected drives” means
38
40
 
39
41
  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.
40
42
 
41
- This is not administrator access or an unrestricted shell. Windows permissions, protected Local Commander state/runtime and secret-file filters still apply to file tools. Direct command execution and localhost ports remain separately controlled. Read/write access is powerful: an assistant may read personal documents or change accessible files, and writing startup items, scripts or Git hooks can make programs run later as you. Treat drive-wide write access like permission to run programs. Folder controls are not an OS sandbox for an executable you separately allow. ChatGPT/Claude or your workspace may still ask for their own action confirmations.
43
+ 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.
42
44
 
43
45
  ## Manage the installation
44
46
 
@@ -72,7 +74,7 @@ npx local-commander-agent@latest workspaces
72
74
  npx local-commander-agent@latest revoke-workspace <workspaceGrantId>
73
75
  ```
74
76
 
75
- `configure-access` reopens the all-drives/selected-folder choice. **It replaces the current set of filesystem grants and clears their command and localhost permissions.** Choosing a restricted folder actually removes the old broader grants. `approve-workspace` adds another read/write folder without replacing the existing set. `workspaces` shows the grant IDs and permissions to use with the other commands. A successful change restarts an already-running agent to synchronize grants; cancellation does not restart it. A revoked drive is not silently re-added in the background.
77
+ `configure-access` reopens the full-access/selected-folder choice. **It replaces current grants and their restrictions.** Full access enables commands and localhost by default; selected folders start read/write with separate command/port allowlists. Choosing a restricted folder removes old broader grants. `approve-workspace` adds another read/write folder without replacing the existing set. `workspaces` shows grant IDs, full/scoped mode and explicit denials. A successful change restarts an already-running agent to synchronize grants; cancellation does not restart it. A revoked drive is not silently re-added in the background.
76
78
 
77
79
  To enable or remove individual capabilities, replace the placeholders with a grant ID from `workspaces` and an executable path that actually exists on your PC:
78
80
 
@@ -86,7 +88,11 @@ npx local-commander-agent@latest deny-command <workspaceGrantId> node
86
88
  npx local-commander-agent@latest deny-localhost <workspaceGrantId> 3000
87
89
  ```
88
90
 
89
- `allow-command` associates an exact executable with the name the assistant uses; it does not accept a shell command string. Capability changes are local owner actions, not MCP settings the assistant can silently widen. The all-drives decision always requires the local dialog; there is no `--yes` or environment flag to approve it.
91
+ `allow-command` associates an exact executable with the name the assistant uses; it does not accept a shell command string. Capability changes are local owner actions, not MCP settings the assistant can silently widen. The all-drives decision always requires the local dialog; there is no `--yes` or environment flag to approve it.
92
+
93
+ 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.
94
+
95
+ The MCP discovery sequence is `workspace_list`, `diagnostics`, `execution_session_open`, requested work, output/status, then session close. 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.
90
96
 
91
97
  ## Troubleshooting
92
98
 
@@ -114,3 +120,21 @@ npx local-commander-agent@latest
114
120
  ```
115
121
 
116
122
  Enrollment retains the chosen origin. Changing this variable does not move an already paired device or turn the package into a standalone MCP server. Account/device enrollment, OAuth, the control plane and relay are still required.
123
+
124
+ ## Monitor the production connection from CMD
125
+
126
+ After installing an agent release with monitoring support, run these commands from any CMD window:
127
+
128
+ ```cmd
129
+ npx local-commander-agent@latest monitor
130
+ npx local-commander-agent@latest monitor --watch
131
+ npx local-commander-agent@latest logs --follow
132
+ ```
133
+
134
+ If the CLI is already on PATH, use `localcommander monitor --watch` or `localcommander logs --follow`. `monitor --json` returns a machine-readable snapshot; `logs` prints recent events once. Ctrl+C exits a live view without stopping the agent. These commands only read local monitoring files; they do not install, pair, restart, dispatch cloud work or consume execution quota.
135
+
136
+ The view lists this Windows user's reporting production-agent instances, process IDs, agent versions, enrolled server origin, relay connection state and recently observed execution sessions. A live process with a snapshot older than ten seconds is **stale**, not connected. An execution session is not an independently identified AI agent. **Model names are unavailable** because the current production relay does not transmit them.
137
+
138
+ Each instance retains its last 100 connection/tool events and 32 recently observed sessions in `%LOCALAPPDATA%\LocalCommander\Monitor`. Logs show timestamps, tool names, result codes and session/execution IDs. They exclude credentials, command arguments, file paths, request bodies and tool output. The monitor refreshes every two seconds. `logs --follow` reports a gap if the bounded history is overwritten between reads. Last snapshots remain available after a stop; a new run replaces that state directory's snapshot.
139
+
140
+ An older installed agent produces no monitoring snapshot even if it is running; the CLI reports that monitoring is unavailable. Publishing/installing the updated agent is required before these views can report its connection. The existing `status` command remains available for installation and process state.
@@ -0,0 +1,102 @@
1
+ import fs from "node:fs/promises";
2
+ import path from "node:path";
3
+ import { spawnSync } from "node:child_process";
4
+
5
+ const SHIM_MARKER = "@rem Local Commander managed command\r\n";
6
+ const CLI_FILES = ["package.json", "bin/localcommander.mjs", "bin/monitor.mjs", "bin/command-path.mjs"];
7
+
8
+ export function commandShim(version) {
9
+ if (!/^\d+\.\d+\.\d+(?:[-+][0-9A-Za-z.-]+)?$/.test(version)) throw Error("PACKAGE_VERSION_INVALID");
10
+ return SHIM_MARKER + '@echo off\r\nsetlocal DisableDelayedExpansion\r\nnode "%~dp0..\\Versions\\' + version +
11
+ '\\bin\\localcommander.mjs" %*\r\nexit /b %errorlevel%\r\n';
12
+ }
13
+
14
+ async function plainDirectory(directory) {
15
+ await fs.mkdir(directory, { recursive: true, mode: 0o700 });
16
+ const stat = await fs.lstat(directory);
17
+ if (!stat.isDirectory() || stat.isSymbolicLink()) throw Error("INSTALL_DIRECTORY_INVALID");
18
+ }
19
+ async function regularOrMissing(file) {
20
+ const stat = await fs.lstat(file).catch(error => { if (error.code !== "ENOENT") throw error; });
21
+ if (stat && (!stat.isFile() || stat.isSymbolicLink())) throw Error("INSTALL_STATE_INVALID");
22
+ }
23
+
24
+ export async function installCommandFiles(packageRoot, installRoot, version) {
25
+ const shim = commandShim(version);
26
+ const runtime = path.join(installRoot, "Versions", version);
27
+ const bin = path.join(installRoot, "bin");
28
+ for (const directory of [installRoot, path.join(installRoot, "Versions"), runtime, path.join(runtime, "bin"), bin]) await plainDirectory(directory);
29
+ for (const file of CLI_FILES) {
30
+ const source = path.join(packageRoot, file), target = path.join(runtime, file);
31
+ await regularOrMissing(source);
32
+ await regularOrMissing(target);
33
+ if (path.resolve(source).toLowerCase() !== path.resolve(target).toLowerCase()) await fs.copyFile(source, target);
34
+ }
35
+ const target = path.join(bin, "localcommander.cmd");
36
+ await regularOrMissing(target);
37
+ const previous = await fs.readFile(target, "utf8").catch(error => { if (error.code !== "ENOENT") throw error; });
38
+ if (previous !== undefined && !previous.startsWith(SHIM_MARKER)) throw Error("COMMAND_SHIM_CONFLICT");
39
+ await fs.writeFile(target, shim, { mode: 0o600 });
40
+ return bin;
41
+ }
42
+
43
+ export async function removeCommandFile(installRoot) {
44
+ for (const directory of [installRoot, path.join(installRoot, "bin")]) {
45
+ const stat = await fs.lstat(directory).catch(error => { if (error.code !== "ENOENT") throw error; });
46
+ if (!stat) return;
47
+ if (!stat.isDirectory() || stat.isSymbolicLink()) throw Error("INSTALL_DIRECTORY_INVALID");
48
+ }
49
+ const file = path.join(installRoot, "bin", "localcommander.cmd");
50
+ await regularOrMissing(file);
51
+ const content = await fs.readFile(file, "utf8").catch(error => { if (error.code !== "ENOENT") throw error; });
52
+ if (content?.startsWith(SHIM_MARKER)) await fs.unlink(file);
53
+ }
54
+
55
+ // Read the registry's raw value so %VARIABLE% entries and its value type survive.
56
+ export const USER_PATH_SCRIPT = String.raw`
57
+ $ErrorActionPreference = 'Stop'
58
+ $target = $env:LC_COMMAND_BIN
59
+ $remove = $env:LC_COMMAND_PATH_REMOVE -eq '1'
60
+ $key = [Microsoft.Win32.Registry]::CurrentUser.CreateSubKey('Environment')
61
+ try {
62
+ $raw = $key.GetValue('Path', '', [Microsoft.Win32.RegistryValueOptions]::DoNotExpandEnvironmentNames)
63
+ if ($raw -isnot [string]) { throw 'USER_PATH_INVALID' }
64
+ $kind = [Microsoft.Win32.RegistryValueKind]::ExpandString
65
+ if ($key.GetValueNames() -contains 'Path') { $kind = $key.GetValueKind('Path') }
66
+ if ($kind -ne [Microsoft.Win32.RegistryValueKind]::String -and $kind -ne [Microsoft.Win32.RegistryValueKind]::ExpandString) { throw 'USER_PATH_INVALID' }
67
+ $entries = $raw.Split(';')
68
+ $kept = @($entries | Where-Object {
69
+ $expanded = [Environment]::ExpandEnvironmentVariables($_.Trim().Trim('"')).Replace('/', '\').TrimEnd('\')
70
+ -not [string]::Equals($expanded, $target.TrimEnd('\'), [StringComparison]::OrdinalIgnoreCase)
71
+ })
72
+ $next = $raw
73
+ if ($remove) { $next = [string]::Join(';', $kept) }
74
+ elseif ($kept.Count -eq $entries.Count) {
75
+ $separator = if ($raw.Length -gt 0 -and -not $raw.EndsWith(';')) { ';' } else { '' }
76
+ $next = $raw + $separator + $target
77
+ }
78
+ if ($next.Length -gt 32766) { throw 'USER_PATH_TOO_LONG' }
79
+ if ($next -cne $raw) { $key.SetValue('Path', $next, $kind) }
80
+ } finally { $key.Dispose() }
81
+ Add-Type -TypeDefinition @'
82
+ using System;
83
+ using System.Runtime.InteropServices;
84
+ public static class LocalCommanderEnvironment {
85
+ [DllImport("user32.dll", CharSet=CharSet.Unicode, SetLastError=true)]
86
+ public static extern IntPtr SendMessageTimeout(IntPtr window, uint message, IntPtr word, string value, uint flags, uint timeout, out UIntPtr result);
87
+ }
88
+ '@
89
+ $result = [UIntPtr]::Zero
90
+ [void][LocalCommanderEnvironment]::SendMessageTimeout([IntPtr]0xffff, 0x1a, [IntPtr]::Zero, 'Environment', 2, 1000, [ref]$result)
91
+ Write-Output 'USER_PATH_READY'
92
+ `;
93
+
94
+ export function updateUserPath(bin, remove = false) {
95
+ if (!path.isAbsolute(bin) || /[;"\r\n\0]/.test(bin)) throw Error("COMMAND_PATH_INVALID");
96
+ const result = spawnSync(path.join(process.env.SystemRoot || "C:\\Windows", "System32/WindowsPowerShell/v1.0/powershell.exe"),
97
+ ["-NoLogo", "-NoProfile", "-NonInteractive", "-EncodedCommand", Buffer.from(USER_PATH_SCRIPT, "utf16le").toString("base64")], {
98
+ windowsHide: true, encoding: "utf8", timeout: 15_000,
99
+ env: { ...process.env, LC_COMMAND_BIN: bin, LC_COMMAND_PATH_REMOVE: remove ? "1" : "0" }
100
+ });
101
+ if (result.error || result.status !== 0 || result.stdout.trim() !== "USER_PATH_READY") throw Error("USER_PATH_UPDATE_FAILED");
102
+ }
@@ -5,6 +5,8 @@ import path from "node:path";
5
5
  import { randomBytes } from "node:crypto";
6
6
  import { spawn, spawnSync } from "node:child_process";
7
7
  import { fileURLToPath } from "node:url";
8
+ import { runMonitor } from "./monitor.mjs";
9
+ import { installCommandFiles, removeCommandFile, updateUserPath } from "./command-path.mjs";
8
10
 
9
11
  const PACKAGE_ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
10
12
  const PACKAGE_META = JSON.parse(await fs.readFile(path.join(PACKAGE_ROOT, "package.json"), "utf8"));
@@ -43,6 +45,7 @@ function currentPath() {
43
45
  return path.join(appDataRoot(), "current.json");
44
46
  }
45
47
  function runtimeSource() {
48
+ if (PACKAGE_ROOT.toLowerCase() === path.join(versionsDirectory(), VERSION).toLowerCase()) return PACKAGE_ROOT;
46
49
  return path.join(PACKAGE_ROOT, "runtime");
47
50
  }
48
51
  function processFile() {
@@ -364,12 +367,17 @@ async function bootstrap({ chooseWorkspace = true } = {}) {
364
367
  }
365
368
  registerAutostart(runtimeDir);
366
369
  await startAgent();
370
+ const commandBin = await installCommandFiles(PACKAGE_ROOT, appDataRoot(), VERSION);
371
+ updateUserPath(commandBin);
367
372
  if (previous && previous.version !== VERSION) process.stdout.write("Updated Local Commander from " + previous.version + " to " + VERSION + ".\n");
368
373
  process.stdout.write("Local Commander " + VERSION + " is installed and running.\n");
374
+ process.stdout.write("The localcommander command is on your user PATH. Open a new CMD or terminal window to use it.\n");
369
375
  }
370
376
  async function uninstall(purgeState) {
371
377
  await stopAgent();
372
378
  removeAutostart();
379
+ updateUserPath(path.join(appDataRoot(), "bin"), true);
380
+ await removeCommandFile(appDataRoot());
373
381
  const versions = versionsDirectory();
374
382
  if (under(appDataRoot(), versions)) await fs.rm(versions, { recursive: true, force: true });
375
383
  await fs.rm(currentPath(), { force: true });
@@ -388,6 +396,9 @@ function help() {
388
396
  "Usage:",
389
397
  " npx local-commander-agent@latest install/pair/start",
390
398
  " npx local-commander-agent@latest status show local state",
399
+ " npx local-commander-agent@latest monitor --watch production connection and sessions",
400
+ " npx local-commander-agent@latest monitor --json monitoring snapshot as JSON",
401
+ " npx local-commander-agent@latest logs --follow recent production relay events",
391
402
  " npx local-commander-agent@latest start start background agent",
392
403
  " npx local-commander-agent@latest stop stop background agent",
393
404
  " npx local-commander-agent@latest restart restart background agent",
@@ -447,6 +458,10 @@ async function main() {
447
458
  process.stdout.write(JSON.stringify(await statusObject(), null, 2) + "\n");
448
459
  return;
449
460
  }
461
+ if (command === "monitor" || command === "logs") {
462
+ await runMonitor(path.join(appDataRoot(), "Monitor"), command, args);
463
+ return;
464
+ }
450
465
  if (command === "configure-access") {
451
466
  if (args.length) fail("ARGUMENT_INVALID");
452
467
  const current = await readCurrent();
@@ -0,0 +1,131 @@
1
+ import { lstat, open, readdir } from "node:fs/promises";
2
+ import path from "node:path";
3
+ import { setTimeout as delay } from "node:timers/promises";
4
+
5
+ const UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
6
+ const STATES = new Set(["starting", "connecting", "connected", "disconnected", "stopped", "failed"]);
7
+ const EVENTS = new Set([...STATES, "execute", "prepare", "status", "output", "cancel", "finished", "cancelled", "unknown", "result", "request_failed", "rejected", "connection_error"]);
8
+ const iso = value => typeof value === "string" && /^\d{4}-\d\d-\d\dT[\d:.]+Z$/.test(value) && Number.isFinite(Date.parse(value));
9
+ const invalid = () => { throw Error("MONITOR_DATA_INVALID"); };
10
+ function pidAlive(pid) {
11
+ try { process.kill(pid, 0); return true; }
12
+ catch (error) { return error?.code === "EPERM"; }
13
+ }
14
+
15
+ // Explicit allowlist projection: never print extra fields from a local file.
16
+ export function monitorView(raw, now = Date.now(), alive = pidAlive) {
17
+ if (!raw || raw.schemaVersion !== 1 || !UUID.test(raw.instanceId) || !Number.isSafeInteger(raw.pid) || raw.pid <= 0 ||
18
+ typeof raw.agentVersion !== "string" || !/^[A-Za-z0-9._+-]{1,64}$/.test(raw.agentVersion) || !iso(raw.updatedAt) ||
19
+ !STATES.has(raw.connection?.state) || !Array.isArray(raw.sessions) || raw.sessions.length > 32 ||
20
+ !Array.isArray(raw.events) || raw.events.length > 100) invalid();
21
+ let origin = null;
22
+ if (raw.connection.origin !== null) {
23
+ const url = new URL(raw.connection.origin);
24
+ if (url.origin !== raw.connection.origin || url.username || url.password ||
25
+ !(url.protocol === "https:" || (url.protocol === "http:" && ["127.0.0.1", "localhost", "[::1]"].includes(url.hostname)))) invalid();
26
+ origin = url.origin;
27
+ }
28
+ const sessions = raw.sessions.map(session => {
29
+ if (!session || !UUID.test(session.sessionId) || !UUID.test(session.workspaceGrantId) || !iso(session.lastSeenAt)) invalid();
30
+ return { sessionId: session.sessionId, workspaceGrantId: session.workspaceGrantId, lastSeenAt: session.lastSeenAt, model: null };
31
+ });
32
+ const events = raw.events.map(event => {
33
+ if (!event || !Number.isSafeInteger(event.seq) || event.seq < 1 || !iso(event.at) || !EVENTS.has(event.event) ||
34
+ (event.sessionId !== undefined && !UUID.test(event.sessionId)) || (event.executionId !== undefined && !UUID.test(event.executionId)) ||
35
+ (event.tool !== undefined && (typeof event.tool !== "string" || !/^[a-z_]{1,64}$/.test(event.tool))) ||
36
+ (event.code !== undefined && (typeof event.code !== "string" || !/^[A-Z0-9_]{1,64}$/.test(event.code)))) invalid();
37
+ return { seq: event.seq, at: event.at, event: event.event,
38
+ ...(event.sessionId ? { sessionId: event.sessionId } : {}), ...(event.executionId ? { executionId: event.executionId } : {}),
39
+ ...(event.tool ? { tool: event.tool } : {}), ...(event.code ? { code: event.code } : {}) };
40
+ });
41
+ const running = alive(raw.pid);
42
+ const age = now - Date.parse(raw.updatedAt);
43
+ const connection = !running ? "stopped" : age < -5000 || age > 10_000 ? "stale" : raw.connection.state;
44
+ return { instanceId: raw.instanceId, pid: raw.pid, agentVersion: raw.agentVersion, updatedAt: raw.updatedAt,
45
+ running, connection, origin, sessions, events };
46
+ }
47
+
48
+ export async function readMonitors(directory) {
49
+ let entries;
50
+ try {
51
+ const stat = await lstat(directory);
52
+ if (!stat.isDirectory() || stat.isSymbolicLink()) invalid();
53
+ entries = (await readdir(directory)).filter(name => /^[a-f0-9]{64}\.json$/.test(name));
54
+ } catch (error) {
55
+ if (error?.code === "ENOENT") return { instances: [], unavailable: 0 };
56
+ return { instances: [], unavailable: 1 };
57
+ }
58
+ const instances = [];
59
+ let unavailable = Math.max(0, entries.length - 32);
60
+ for (const name of entries.slice(0, 32)) {
61
+ try {
62
+ const file = path.join(directory, name);
63
+ const stat = await lstat(file);
64
+ if (!stat.isFile() || stat.isSymbolicLink() || stat.size > 128 * 1024) invalid();
65
+ const handle = await open(file, "r");
66
+ let contents;
67
+ try {
68
+ const buffer = Buffer.alloc(128 * 1024 + 1);
69
+ const { bytesRead } = await handle.read(buffer, 0, buffer.length, 0);
70
+ if (bytesRead > 128 * 1024) invalid();
71
+ contents = buffer.subarray(0, bytesRead).toString("utf8");
72
+ } finally { await handle.close(); }
73
+ instances.push(monitorView(JSON.parse(contents)));
74
+ } catch { unavailable++; }
75
+ }
76
+ instances.sort((a, b) => b.updatedAt.localeCompare(a.updatedAt));
77
+ return { instances, unavailable };
78
+ }
79
+
80
+ function logLine(instance, event) {
81
+ return [event.at, "pid=" + instance.pid, event.event, event.tool, event.code,
82
+ event.sessionId && "session=" + event.sessionId, event.executionId && "execution=" + event.executionId].filter(Boolean).join(" ");
83
+ }
84
+
85
+ export function formatMonitor(result) {
86
+ const lines = ["Local Commander production monitor", ""];
87
+ if (!result.instances.length) lines.push("No monitoring snapshots. Start an agent version that supports monitoring; older agents need an update.");
88
+ for (const instance of result.instances) {
89
+ lines.push("PID " + instance.pid + " | v" + instance.agentVersion + " | " + instance.connection.toUpperCase(),
90
+ "Server: " + (instance.origin ?? "Not connected yet") + " | Updated: " + instance.updatedAt,
91
+ "Observed execution sessions (not AI instance identities): " + instance.sessions.length);
92
+ for (const session of instance.sessions) lines.push(" " + session.sessionId + " | model: unavailable | last seen: " + session.lastSeenAt);
93
+ lines.push("Recent events:", ...instance.events.slice(-10).map(event => " " + logLine(instance, event)), "");
94
+ }
95
+ if (result.unavailable) lines.push("Unavailable or omitted snapshots: " + result.unavailable);
96
+ lines.push("Models are unavailable: production relay does not report AI model names.");
97
+ return lines.join("\n") + "\n";
98
+ }
99
+
100
+ export async function runMonitor(directory, command, args) {
101
+ const allowed = command === "logs" ? ["--follow"] : ["--watch", "--json"];
102
+ if (new Set(args).size !== args.length || args.some(arg => !allowed.includes(arg))) throw Error("ARGUMENT_INVALID");
103
+ const repeat = args.includes("--watch") || args.includes("--follow");
104
+ const stop = new AbortController();
105
+ const abort = () => stop.abort();
106
+ process.once("SIGINT", abort);
107
+ process.once("SIGTERM", abort);
108
+ const seen = new Map();
109
+ try {
110
+ do {
111
+ const result = await readMonitors(directory);
112
+ if (command === "logs" && result.instances.length) {
113
+ for (const instance of result.instances) {
114
+ const after = seen.get(instance.instanceId) ?? 0;
115
+ if (after && instance.events[0]?.seq > after + 1) process.stdout.write("Earlier events were overwritten in the 100-event buffer.\n");
116
+ for (const event of instance.events) if (event.seq > after) process.stdout.write(logLine(instance, event) + "\n");
117
+ seen.set(instance.instanceId, instance.events.at(-1)?.seq ?? after);
118
+ }
119
+ for (const key of seen.keys()) if (!result.instances.some(instance => instance.instanceId === key)) seen.delete(key);
120
+ if (result.unavailable) process.stderr.write("Some monitoring snapshots are unavailable.\n");
121
+ } else {
122
+ if (repeat && process.stdout.isTTY && !args.includes("--json")) console.clear();
123
+ process.stdout.write(args.includes("--json") ? JSON.stringify(result) + "\n" : formatMonitor(result));
124
+ }
125
+ if (repeat) await delay(2000, undefined, { signal: stop.signal }).catch(error => { if (error.name !== "AbortError") throw error; });
126
+ } while (repeat && !stop.signal.aborted);
127
+ } finally {
128
+ process.removeListener("SIGINT", abort);
129
+ process.removeListener("SIGTERM", abort);
130
+ }
131
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "local-commander-agent",
3
- "version": "0.3.6",
3
+ "version": "0.3.7",
4
4
  "description": "Windows companion for Local Commander: connect AI clients to local files, approved commands and localhost. Requires https://localcmd.app.",
5
5
  "type": "module",
6
6
  "bin": {
Binary file
Binary file
Binary file