local-commander-agent 0.3.17-journal.1 → 0.3.22-fullaccess.1

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,11 +1,17 @@
1
- # Change history
2
-
3
- ## 0.3.17-journal.1 - 2026-10-08
4
-
5
- - Apply existing retention maintenance when a new request reaches the journal entry limit on a long-lived connection.
6
- - Remove only expired, acknowledged terminal records under the existing integrity and retention checks.
7
- - Keep active, unknown, unacknowledged and recent records protected; admission still fails closed when no record is eligible.
8
-
1
+ # Change history
2
+
3
+ ## 0.3.22-fullaccess.1 - 2026-10-10
4
+
5
+ - Owner-approved Full Access no longer requires per-executable registration for Windows shells or administration tools. The Windows account rights and explicit deny-command restrictions remain enforced.
6
+ - Scoped grants and native authentication, target binding, journal/recovery and concurrency safeguards are unchanged.
7
+ - Local direct MCP and remote startup improvements remain separate issue #104 tasks; this build still uses authenticated hosted relay for ChatGPT web.
8
+
9
+ ## 0.3.17-journal.1 - 2026-10-08
10
+
11
+ - Apply existing retention maintenance when a new request reaches the journal entry limit on a long-lived connection.
12
+ - Remove only expired, acknowledged terminal records under the existing integrity and retention checks.
13
+ - Keep active, unknown, unacknowledged and recent records protected; admission still fails closed when no record is eligible.
14
+
9
15
  Includes the aligned monitor and expanded names from 0.3.16. This does not release unknown cloud reservations or replay work.
10
16
 
11
17
  ## 0.3.16-monitor.1 — 2026-10-08
@@ -24,3 +30,10 @@ Names are display hints. IDs and permissions are unchanged. Token figures remain
24
30
  - Release cloud reservations only after a rejection receipt passes the existing execution and device checks.
25
31
  - Retain fencing for unknown executions without rejection proof.
26
32
  - Prune acknowledged terminal journal records using the existing retention rules.
33
+ # 0.3.19-admission.1
34
+
35
+ New execution admission pauses when protected journal capacity is exhausted; authenticated relay status carries the pause while status, output and cancellation remain available. Retains the verified native18 default of 2,000 entries, the existing byte/retention limits and unknown-execution fencing.
36
+
37
+ # 0.3.21-project-targets.3
38
+
39
+ Explicit project targets use on-demand repository identity and constrain session paths to the selected repository. Retains installed target20 behavior and adds accepted journal admission guards and authenticated readiness reporting; existing journal retention, permission checks and unknown-execution fencing remain.
package/README.md CHANGED
@@ -74,7 +74,7 @@ npx local-commander-agent@latest workspaces
74
74
  npx local-commander-agent@latest revoke-workspace <workspaceGrantId>
75
75
  ```
76
76
 
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.
77
+ `configure-access` reopens the full-access/selected-folder choice. **It replaces the current set of 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.
78
78
 
79
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:
80
80
 
@@ -90,13 +90,13 @@ npx local-commander-agent@latest deny-localhost <workspaceGrantId> 3000
90
90
 
91
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
92
 
93
- ### Approve Git from CMD or PowerShell
93
+ ### Scoped access: approve Git from CMD or PowerShell
94
94
 
95
95
  When giving an owner Windows commands, identify the shell before using shell-specific syntax and label the example. Prefer explicit workspace IDs and quoted absolute executable paths that work in both CMD and PowerShell; do not assume a standard installation path.
96
96
 
97
97
  Use the example for the shell you are running. A prompt such as `C:\project>` is normally **Command Prompt (CMD)**; `PS C:\project>` is normally **PowerShell**. Windows Terminal can host either shell.
98
98
 
99
- First run `npx local-commander-agent@latest workspaces` and copy the intended workspace's grant ID.
99
+ For selected-folders/scoped mode, first run `npx local-commander-agent@latest workspaces` and copy the intended workspace's grant ID. Full Access does not require this setup for Git.
100
100
 
101
101
  **CMD:** run `where git.exe`. If it lists several paths, choose the Git executable you intend to approve. Replace `WORKSPACE_ID` and the example path below with those actual values; keep the path in double quotes.
102
102
 
@@ -118,9 +118,10 @@ Do not paste `(Get-Command git).Source` into CMD: CMD passes it literally instea
118
118
 
119
119
  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.
120
120
 
121
- Full access does not include Windows shells, script hosts or administration binaries (`cmd`, `powershell`, `pwsh`, `wscript`, `cscript`, `mshta`, `rundll32`, `regsvr32`, `certutil`, `bitsadmin`, `msiexec`, `wmic`, `reg`, `schtasks`, `sc`, `net`, `netsh`, `icacls`, `takeown`, `runas`, `psexec`, `wsl`, `bash`, `sh`, `dism`, `winrm` and similar). An assistant request for one of these, by bare name or absolute path, fails with `COMMAND_NOT_ALLOWED` until you register that exact executable yourself with `allow-command <workspace-id> <name> <absolute .exe path>` and confirm the native dialog; it then runs only under the registered name or that exact path. Ordinary developer tools (`node`, `git`, `npm`, `python`, `dotnet`, ...) continue to resolve from your absolute `PATH` entries.
121
+ 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`.
122
122
 
123
- 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.
123
+
124
+ 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.
124
125
 
125
126
  ## Troubleshooting
126
127
 
@@ -207,4 +208,4 @@ Reload the personal MCP server once to load this source change, then reopen `npm
207
208
 
208
209
  The view shows measured per-agent calls, mean tool-plus-queue duration, stdout bytes, sent response bytes and errors when an identified reporter has those counters. Native measured calls, status/output polls and session records are separate figures; sessions are not execution capacity. Current savings totals exclude stale native snapshots. Missing measurements remain unavailable.
209
210
 
210
- See [CHANGELOG.md](CHANGELOG.md) for release changes.
211
+ See [CHANGELOG.md](https://github.com/netbka/local-commander-mcp/blob/main/packages/agent-cli/CHANGELOG.md) for release changes.
@@ -7,7 +7,7 @@ const SHIM_MARKER = "@rem Local Commander managed command\r\n";
7
7
  const WRAPPER_MARKER = "# Local Commander managed command\r\n";
8
8
  const MONITOR_WRAPPER = "localcommander-monitor.ps1";
9
9
  const SHORTCUT_NAME = "Local Commander Monitor";
10
- const CLI_FILES = ["package.json", "bin/localcommander.mjs", "bin/monitor.mjs", "bin/monitor-agents.mjs", "bin/monitor-context.mjs", "bin/monitor-mcp.mjs", "bin/command-path.mjs"];
10
+ const CLI_FILES = ["package.json", "bin/localcommander.mjs", "bin/monitor.mjs", "bin/monitor-agents.mjs", "bin/monitor-context.mjs", "bin/monitor-mcp.mjs", "bin/monitor-savings.mjs", "bin/command-path.mjs"];
11
11
 
12
12
  function validVersion(version) {
13
13
  if (!/^\d+\.\d+\.\d+(?:[-+][0-9A-Za-z.-]+)?$/.test(version)) throw Error("PACKAGE_VERSION_INVALID");
@@ -1,7 +1,7 @@
1
1
  import { execFile } from "node:child_process";
2
2
  import { createHash } from "node:crypto";
3
- import { lstat, open } from "node:fs/promises";
4
- import { lstatSync, readFileSync } from "node:fs";
3
+ import { lstat, open } from "node:fs/promises";
4
+ import { lstatSync, readFileSync } from "node:fs";
5
5
  import os from "node:os";
6
6
  import path from "node:path";
7
7
  import { promisify } from "node:util";
@@ -120,16 +120,16 @@ $rows = @(Get-Process -Name codex,claude,cursor-agent -ErrorAction SilentlyConti
120
120
  })
121
121
  ConvertTo-Json -InputObject $rows -Compress`;
122
122
 
123
- export function presenceDirectory(projectRoot = process.cwd()) {
124
- let state = process.env.LCMCP_OAUTH_STATE_PATH;
125
- if (!state) try {
126
- const file = path.resolve(projectRoot, process.env.LCMCP_CONFIG?.trim() || "local-commander.config.json");
127
- const stat = lstatSync(file);
128
- if (stat.isFile() && !stat.isSymbolicLink() && stat.size <= 65536)
129
- state = JSON.parse(readFileSync(file, "utf8").replace(/^\uFEFF/, "")).auth?.oauthStatePath;
130
- } catch { /* Missing/unreadable local configuration leaves observations unavailable. */ }
131
- return typeof state === "string" && state.trim() ? path.join(path.dirname(path.resolve(projectRoot, state)), ".local-commander") : path.join(projectRoot, ".local-commander");
132
- }
123
+ export function presenceDirectory(projectRoot = process.cwd()) {
124
+ let state = process.env.LCMCP_OAUTH_STATE_PATH;
125
+ if (!state) try {
126
+ const file = path.resolve(projectRoot, process.env.LCMCP_CONFIG?.trim() || "local-commander.config.json");
127
+ const stat = lstatSync(file);
128
+ if (stat.isFile() && !stat.isSymbolicLink() && stat.size <= 65536)
129
+ state = JSON.parse(readFileSync(file, "utf8").replace(/^\uFEFF/, "")).auth?.oauthStatePath;
130
+ } catch { /* Missing/unreadable local configuration leaves observations unavailable. */ }
131
+ return typeof state === "string" && state.trim() ? path.join(path.dirname(path.resolve(projectRoot, state)), ".local-commander") : path.join(projectRoot, ".local-commander");
132
+ }
133
133
 
134
134
  export async function readAgentActivity(directory, { previousHosts = new Map(), signal, projectRoot = process.cwd() } = {}) {
135
135
  const computer = os.hostname().replace(/[^A-Za-z0-9._-]/g, "_").slice(0, 64) || "PC";
@@ -0,0 +1,20 @@
1
+ export interface LifetimeSavings {
2
+ scope: "all_time";
3
+ coverage: "recorded_native_results";
4
+ since: string | null;
5
+ asOf: string | null;
6
+ calls: number;
7
+ originalBytes: number;
8
+ sentBytes: number;
9
+ savedBytes: number;
10
+ }
11
+ export function lifetimeSavingsView(raw: unknown): LifetimeSavings | null;
12
+ export function summarizeLifetimeSavings(result: {
13
+ instances?: Array<{ lifetimeSavings?: unknown; savingsSourceId?: string }>;
14
+ unavailable?: number;
15
+ }): { available: boolean; partial: boolean; unavailable: number; sources?: number;
16
+ calls?: number; originalBytes?: number; sentBytes?: number; savedBytes?: number;
17
+ since?: string | null; asOf?: string | null; estimatedSavedTokens?: number };
18
+ export function formatLifetimeSavings(result: Parameters<typeof summarizeLifetimeSavings>[0] & {
19
+ mcp?: { instances: Array<{ instanceId: string; metrics?: Array<{ savedBytes: number }> }>; unavailable?: number };
20
+ }): string[];
@@ -0,0 +1,71 @@
1
+ // Scalar projections only. Never read a ledger, private state or provider data here.
2
+ const count = value => Number.isSafeInteger(value) && value >= 0 && value <= 1e15;
3
+ const date = value => typeof value === "string" && /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$/.test(value) && Number.isFinite(Date.parse(value)) && new Date(value).toISOString() === value;
4
+
5
+ /** Invalid or older optional telemetry must not hide the rest of a computer. */
6
+ export function lifetimeSavingsView(raw) {
7
+ if (!raw || raw.scope !== "all_time" || raw.coverage !== "recorded_native_results" ||
8
+ ![raw.calls, raw.originalBytes, raw.sentBytes, raw.savedBytes].every(count) ||
9
+ raw.sentBytes > raw.originalBytes || raw.savedBytes !== raw.originalBytes - raw.sentBytes ||
10
+ (raw.calls === 0 ? raw.since !== null || raw.asOf !== null || raw.originalBytes !== 0 :
11
+ !date(raw.since) || !date(raw.asOf) || raw.since > raw.asOf)) return null;
12
+ return { scope: "all_time", coverage: "recorded_native_results", since: raw.since, asOf: raw.asOf,
13
+ calls: raw.calls, originalBytes: raw.originalBytes, sentBytes: raw.sentBytes, savedBytes: raw.savedBytes };
14
+ }
15
+
16
+ /** Aggregate complete scalar native histories, even when their producers have stopped. */
17
+ export function summarizeLifetimeSavings(result) {
18
+ const sources = new Map(), rejected = new Set();
19
+ let unavailable = result.unavailable ?? 0;
20
+ // Ascending counts expose old conflicts too, regardless of the reader's file order.
21
+ const snapshots = (result.instances ?? []).map(instance => ({
22
+ id: instance.savingsSourceId, value: lifetimeSavingsView(instance.lifetimeSavings)
23
+ })).sort((a, b) => (a.value?.calls ?? 0) - (b.value?.calls ?? 0));
24
+ for (const { id, value } of snapshots) {
25
+ if (typeof id !== "string" || !/^[a-f0-9]{64}$/.test(id)) { unavailable++; continue; }
26
+ if (rejected.has(id)) continue;
27
+ const old = sources.get(id);
28
+ const conflict = old && (value?.calls === old.calls ?
29
+ Object.keys(old).some(key => value[key] !== old[key]) :
30
+ value && (["originalBytes", "sentBytes", "savedBytes"].some(key => value[key] < old[key]) ||
31
+ (old.since !== null && value.since > old.since) || (old.asOf !== null && value.asOf < old.asOf)));
32
+ if (!value || conflict) {
33
+ sources.delete(id); rejected.add(id); unavailable++; continue;
34
+ }
35
+ sources.set(id, value);
36
+ }
37
+ const values = [...sources.values()];
38
+ const totals = { calls: 0, originalBytes: 0, sentBytes: 0, savedBytes: 0 };
39
+ for (const value of values) for (const key of Object.keys(totals)) totals[key] += value[key];
40
+ if (!Object.values(totals).every(count)) return { available: false, partial: true, unavailable: unavailable + 1 };
41
+ const dates = values.map(value => value.since).filter(Boolean).sort();
42
+ const updates = values.map(value => value.asOf).filter(Boolean).sort();
43
+ return { available: values.length > 0, partial: unavailable > 0, unavailable, sources: values.length,
44
+ ...totals, since: dates[0] ?? null, asOf: updates.at(-1) ?? null,
45
+ estimatedSavedTokens: Math.round(totals.savedBytes / 4) };
46
+ }
47
+
48
+ /** Native all-time and retained personal MCP have different coverage and byte units. */
49
+ export function formatLifetimeSavings(result) {
50
+ const total = summarizeLifetimeSavings(result);
51
+ const tokens = bytes => "~" + Math.round(bytes / 4).toLocaleString("en-US");
52
+ const personal = new Map();
53
+ for (const instance of result.mcp?.instances ?? []) personal.set(instance.instanceId, instance);
54
+ const personalBytes = [...personal.values()].flatMap(instance => instance.metrics ?? [])
55
+ .reduce((sum, metric) => sum + metric.savedBytes, 0);
56
+ const personalKnown = [...personal.values()].some(instance => instance.metrics?.length) && count(personalBytes);
57
+ const knownBytes = (total.available ? total.savedBytes : 0) + (personalKnown ? personalBytes : 0);
58
+ const available = (total.available || personalKnown) && count(knownBytes);
59
+ const partial = total.partial || !total.available || personal.size > 0 || Boolean(result.mcp?.unavailable);
60
+ const lines = [available ?
61
+ "All-time saved" + (partial ? " (partial)" : " (native)") + ": " + tokens(knownBytes) + " tokens" :
62
+ "All-time saved: unavailable | historical measurements not reported"];
63
+ if (total.available) lines.push("Native history: Raw " + tokens(total.originalBytes) + " | Sent " + tokens(total.sentBytes) +
64
+ " | " + (total.originalBytes ? (100 * total.savedBytes / total.originalBytes).toFixed(1) : "0.0") + "% | " +
65
+ (total.since ? "since " + total.since.slice(0, 10) + " | as of " + total.asOf.slice(0, 16).replace("T", " ") + "Z" : "no recorded results"));
66
+ lines.push(personalKnown ?
67
+ "Personal MCP (partial): " + tokens(personalBytes) + " tok retained" :
68
+ "Personal MCP saved: unavailable (historical total not reported)");
69
+ lines.push("Est: bytes/4; provider tokens unknown");
70
+ return lines;
71
+ }
package/bin/monitor.mjs CHANGED
@@ -1,4 +1,5 @@
1
1
  import { lstat, open, readdir } from "node:fs/promises";
2
+ import { lifetimeSavingsView, formatLifetimeSavings } from "./monitor-savings.mjs";
2
3
  import path from "node:path";
3
4
  import { setTimeout as delay } from "node:timers/promises";
4
5
  import { readAgentActivity } from "./monitor-agents.mjs";
@@ -99,7 +100,9 @@ export function monitorView(raw, now = Date.now(), alive = pidAlive) {
99
100
  });
100
101
  return { instanceId: raw.instanceId, pid: raw.pid, agentVersion: raw.agentVersion, updatedAt: raw.updatedAt,
101
102
  running, connection, origin, sessions, events, computer: raw.computer ?? null, processName: "lc-agent", executions,
102
- efficiency: efficiencyView(raw.efficiency) };
103
+ efficiency: efficiencyView(raw.efficiency),
104
+ savingsSourceId: typeof raw.savingsSourceId === "string" && /^[a-f0-9]{64}$/.test(raw.savingsSourceId) ? raw.savingsSourceId : null,
105
+ lifetimeSavings: lifetimeSavingsView(raw.lifetimeSavings) };
103
106
  }
104
107
 
105
108
  export async function readMonitors(directory) {
@@ -247,9 +250,10 @@ export function formatMonitor(result, columns = process.stdout.columns ?? 120, h
247
250
  let shown = 0, shownPcs = 0, shownAgents = 0;
248
251
  const footer = [
249
252
  "-".repeat(width),
250
- stats.length ? "Raw: " + tokens(saved + sent) + " tokens Sent: " + tokens(sent) + " Saved: " + tokens(saved) +
253
+ ...formatLifetimeSavings(result),
254
+ stats.length ? "Native this run - Raw: " + tokens(saved + sent) + " tokens Sent: " + tokens(sent) + " Saved: " + tokens(saved) +
251
255
  " " + (saved + sent ? Math.round(100 * saved / (saved + sent)) : 0) + "%" + (stats.length < live.length ? " (partial)" : "") :
252
- "Raw: n/a Sent: n/a Saved: n/a",
256
+ "Native this run - Raw: n/a Sent: n/a Saved: n/a",
253
257
  mcp.length ? "MCP this run: " + usageSummary(mcpMetrics) + " | saved stdout " + tokens(mcpMetrics.reduce((sum, row) => sum + row.savedBytes, 0)) + " tok" :
254
258
  "~ Tokens: bytes / 4, this run | model: declared or not reported",
255
259
  mcpMetrics.length ? "Review: " + [...mcpMetrics].sort((a, b) => b.sentBytes - a.sentBytes).slice(0, 2)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "local-commander-agent",
3
- "version": "0.3.17-journal.1",
3
+ "version": "0.3.22-fullaccess.1",
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