Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
bf1bb8c
test(setup): characterize existing hook configuration
davidrobertson Aug 11, 2026
d833c70
feat(setup): add native harness lifecycle adapters
davidrobertson Aug 11, 2026
215194b
fix(setup): serialize safe configuration updates
davidrobertson Aug 11, 2026
53f0721
fix(plugin): validate the native Codex package
davidrobertson Aug 11, 2026
f93e934
docs(setup): add per-harness install guides
davidrobertson Aug 11, 2026
2e2c325
feat(setup): install native harness plugins when supported
davidrobertson Aug 11, 2026
470dae1
feat(setup): remove only owned harness hooks
davidrobertson Aug 11, 2026
2ce1ac0
feat(cli): add safe harness removal
davidrobertson Aug 11, 2026
1c31ce9
fix(setup): report native client capability accurately
davidrobertson Aug 11, 2026
b008f73
docs(setup): document native lifecycle safety
davidrobertson Aug 11, 2026
76f67da
build: refresh setup distribution
davidrobertson Aug 11, 2026
a787681
fix(setup): prepare new native client homes
davidrobertson Aug 11, 2026
41f869f
fix(setup): harden native installation boundary
davidrobertson Aug 11, 2026
557cffd
test(distribution): compare canonical package path
davidrobertson Aug 11, 2026
9fef60a
fix(setup): close file races and status gaps
davidrobertson Aug 11, 2026
21abe15
fix(setup): replace mutable lock files
davidrobertson Aug 11, 2026
af60658
fix(setup): anchor configuration publication
davidrobertson Aug 11, 2026
7fbc5a6
fix(setup): materialize native Copilot package
davidrobertson Aug 11, 2026
139e999
docs(setup): explain portable hook boundary
davidrobertson Aug 11, 2026
84d9b10
fix(setup): load native Codex and Cursor hooks
davidrobertson Aug 11, 2026
e53e772
fix(setup): report concurrent config changes
davidrobertson Aug 11, 2026
0e5b7f1
fix(setup): preserve concurrent config writes
davidrobertson Aug 11, 2026
05a5e55
fix(setup): detect hooks created during removal
davidrobertson Aug 11, 2026
d48f43e
fix(setup): leave manual hook rewrites untouched
davidrobertson Aug 11, 2026
3fd9695
fix(kiro): satisfy Power manifest requirements
davidrobertson Aug 11, 2026
fe5e6d5
fix(setup): run Windows CLI shims safely
davidrobertson Aug 11, 2026
deb2f22
fix(setup): pass Windows shim argv directly
davidrobertson Aug 11, 2026
dd3f800
fix(setup): stop on shim lookup errors
davidrobertson Aug 11, 2026
e16d17f
test(setup): stop Windows shim lookup fallthrough
davidrobertson Aug 11, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 10 additions & 1 deletion .codex-plugin/plugin.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,15 @@
"homepage": "https://github.com/Team-Volt/agent-lcm",
"license": "MIT",
"skills": "./skills/",
"mcpServers": "./mcp.json",
"interface": {
"displayName": "Agent LCM",
"shortDescription": "Use shared local context memory in Codex.",
"longDescription": "Agent LCM captures and recalls coding-agent sessions from one local store.",
"developerName": "Team Volt",
"category": "Developer Tools",
"capabilities": [],
"defaultPrompt": "Recall relevant work from earlier coding sessions."
},
"mcpServers": "./.mcp.json",
"hooks": "./hooks/codex.json"
}
2 changes: 1 addition & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -45,5 +45,5 @@ jobs:
cache: npm
cache-dependency-path: package-lock.json
- run: npm ci
- run: node --test tests/distribution.test.ts tests/setup.test.ts
- run: node --test tests/distribution.test.ts tests/setup-adapters.test.ts tests/setup.test.ts
- run: node --test --test-name-pattern="raw-log workers with the same PID|hook recovers after its lock-owning worker terminates|concurrent single ingest writers append" tests/storage.test.ts tests/hook-cli.test.ts
9 changes: 9 additions & 0 deletions .mcp.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
{
"mcpServers": {
"agent-lcm": {
"type": "stdio",
"command": "node",
"args": ["${PLUGIN_ROOT}/bin/agent-lcm", "mcp"]
}
}
}
30 changes: 30 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,36 @@ docs/ architecture and troubleshooting
provenance in every result.
- Do not add `lcm_record_note`; Agent LCM has no note-writing MCP tool.

## Harness setup and removal

- `agent-lcm setup <harness>` uses native lifecycle commands only for Codex and
the shared Copilot/VS Code store; Cursor Marketplace and Kiro Powers remain
manual. `agent-lcm remove <harness>` removes only exact Agent LCM-owned hook
entries.
- Setup reports `complete` with exit `0`; `manual-required` and
`shared-retained` use exit `2`; command errors use exit `1`.
- Copilot and VS Code share the native plugin store. Single-harness removal
must retain that plugin and leave any legacy fallback hook file unchanged.
- The repository root manifest declares Agent Plugins 1.0 for Kiro and portable
skills/MCP clients. The npm artifact must omit it so Codex and Cursor select
their native hook manifests. Copilot/VS Code setup installs the generated
native package whose hook and MCP commands use the absolute Agent LCM
executable.
- Successful native Codex setup must not create `~/.codex/hooks.json`; it may
remove only exact Agent LCM fallback entries from an existing file.
- Setup-file mutation runs through the directory-anchored helper. Do not
replace it with path checks followed by later path-based writes.
- Validate existing setup JSON before native work. Preserve unrelated and
near-matching hooks, reject symlinked or non-regular targets, and publish
changes under an atomic `<target>.lock` directory through a unique fsynced
temporary file and rename.
- Keep native CLI argv shell-free. On Windows, resolve npm `.cmd` or `.bat`
shims from `PATH`, reject command-shell metacharacters, and invoke only that
resolved shim through `cmd.exe`.
- If a hook file changes during native work, preserve the new bytes and report
whether the native action completed or setup stopped; never hide it behind a
generic file error.

See `src/AGENTS.md` and `tests/AGENTS.md` for more specific rules.

## Commands
Expand Down
116 changes: 86 additions & 30 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -71,30 +71,36 @@ you never need to find or reference a harness cache path.

## Install in each harness

Agent Plugins 1.0 defines the package, not one shared installer. Use the native
flow for each harness:

| Harness | Install |
| --- | --- |
| Codex | `codex plugin marketplace add Team-Volt/agent-lcm`, then `codex plugin add agent-lcm@agent-lcm` |
| GitHub Copilot CLI | `copilot plugin install Team-Volt/agent-lcm` |
| VS Code | Run `Chat: Install Plugin From Source` and enter `https://github.com/Team-Volt/agent-lcm`; VS Code also discovers the Copilot CLI install |
| Cursor | Use `/add-plugin` after Agent LCM is listed in the Cursor Marketplace, or ask an admin to add the repository to your Team Marketplace |
| Kiro IDE | Open Powers, choose the GitHub import option, and enter `https://github.com/Team-Volt/agent-lcm` |

These flows follow the current [Codex plugin](https://help.openai.com/en/articles/20001256-plugins-in-codex/),
[Copilot CLI plugin](https://docs.github.com/en/copilot/how-tos/copilot-cli/customize-copilot/plugins-finding-installing),
Each harness selects its own native package format. Run the setup command for
each harness you use, then follow its guide for native installation, trust, and
removal:

| Harness | Setup command | Guide |
| --- | --- | --- |
| Codex | `agent-lcm setup codex` | [Codex guide](docs/install/codex.md) |
| Cursor | `agent-lcm setup cursor` | [Cursor guide](docs/install/cursor.md) |
| VS Code | `agent-lcm setup vscode` | [VS Code guide](docs/install/vscode.md) |
| GitHub Copilot CLI | `agent-lcm setup copilot` | [Copilot guide](docs/install/copilot.md) |
| Kiro IDE | `agent-lcm setup kiro` | [Kiro guide](docs/install/kiro.md) |

The guides follow the current [Codex plugin](https://github.com/openai/codex/blob/main/codex-rs/skills/src/assets/samples/plugin-creator/references/installing-and-updating.md),
[Copilot CLI plugin](https://docs.github.com/en/copilot/reference/copilot-cli-reference/cli-plugin-reference),
[VS Code agent plugin](https://code.visualstudio.com/docs/agent-customization/agent-plugins),
[Cursor marketplace](https://cursor.com/blog/marketplace), and
[Kiro Powers](https://kiro.dev/docs/powers/) documentation.
[Cursor Marketplace](https://cursor.com/marketplace), and
[Kiro Powers](https://kiro.dev/docs/powers/) documentation. If setup cannot run a
supported native command, it reports the guide and uses the manual hook path
when that harness needs one.

Compatible clients discover the same portable components:
The repository root remains an Agent Plugins package for Kiro and other
skills/MCP-only clients:

- `skills/lcm-recall/SKILL.md`
- the `agent-lcm` stdio server in `mcp.json`

Codex and Cursor compatibility manifests are included for their native plugin
layouts. If a client cannot install the plugin, add this stdio MCP server:
The published npm package omits that root manifest so Codex and Cursor select
their native compatibility manifests, which include hooks. Copilot and VS Code
use the native package generated by setup. If a client cannot install the
plugin, add this stdio MCP server:

```json
{
Expand All @@ -105,14 +111,16 @@ layouts. If a client cannot install the plugin, add this stdio MCP server:

The harness must inherit a `PATH` that contains the npm global binary. Native
plugin installation is more reliable for GUI apps because it uses the bundled
command. Restart the harness after installation.
command. Use the relevant guide's trust or refresh note after installation; a
restart is not a general requirement documented by every harness.

## Enable automatic capture

`agent-lcm setup all` detects the harnesses installed under your home directory
and installs or repairs hooks only for those harnesses. It does not create
configuration directories for clients you do not use. To configure a harness
that setup cannot detect, run its command directly:
and completes native setup where supported, with manual hook wiring only where
that harness needs it. It does not create configuration directories for clients
you do not use. To configure a harness that setup cannot detect, run its
command directly:

```sh
agent-lcm setup codex
Expand All @@ -122,14 +130,15 @@ agent-lcm setup copilot
agent-lcm setup kiro
```

Run only the commands for the harnesses you use. VS Code and GitHub Copilot
share `~/.copilot/hooks/agent-lcm.json`; either setup command installs the same
auto-detecting hooks. Setup preserves unrelated hook entries, is safe to run
again, and writes private files containing the absolute Agent LCM command. If a
target file already exists and needs changes, setup first saves a timestamped
`-pre-agent-lcm-` backup beside it.
Run only the commands for the harnesses you use. A legacy VS Code and GitHub
Copilot fallback may share `~/.copilot/hooks/agent-lcm.json`; native plugin hooks
are loaded from the plugin store instead of being duplicated there. Setup
preserves unrelated hook entries, is safe to run again, and writes private
files containing the absolute Agent LCM command when manual wiring is needed.
If a target file already exists and needs changes, setup first saves a
timestamped `-pre-agent-lcm-` backup beside it.

The user hook locations are:
Legacy or setup-managed user hook locations are:

| Harness | Hook file |
| --- | --- |
Expand All @@ -139,13 +148,60 @@ The user hook locations are:
| GitHub Copilot | `~/.copilot/hooks/agent-lcm.json` |
| Kiro | `~/.kiro/hooks/agent-lcm.json` |

Check the result, then restart each harness:
Codex, Cursor, Copilot, and VS Code native plugins carry their own hooks. Setup
does not add a second user-level copy after native installation. The Codex path
above exists only for older fallback entries, which setup removes after native
installation succeeds.

Check setup-managed and legacy hook files, then run the broader doctor checks:

```sh
agent-lcm setup status
agent-lcm doctor --json
```

Setup and removal print one report per harness. Exit status `0` means the
requested native work and hook work completed. Exit status `2` means a manual
native step remains (`manual-required`) or a shared Copilot resource was
deliberately retained (`shared-retained`). Exit status `1` means the command
failed; inspect stderr before retrying. If stderr says the native action
completed but the hook file could not be updated safely, repair that file and
rerun the same command. A reported concurrent change is left untouched; for
other file errors, inspect the file because publication may have completed.
Add `--json` when a script needs the report fields.

Native lifecycle support is limited to the commands that each client documents:

- Codex probes with `codex plugin list`, adds the installed npm package as a
local marketplace, then runs `codex plugin add agent-lcm@agent-lcm`. The npm
artifact omits the portable root manifest so Codex loads the native manifest,
including hooks. Removal runs `codex plugin remove agent-lcm@agent-lcm`.
- GitHub Copilot CLI and VS Code share the Copilot plugin store. Setup probes
with `copilot plugin list`, builds a private native package with absolute
Agent LCM hook and MCP commands, and installs it with `copilot plugin
install`. `agent-lcm remove copilot` and `agent-lcm remove vscode`
return `shared-retained` without uninstalling that shared plugin; use the
documented Copilot uninstall command only after reviewing both clients.
- Cursor and Kiro are probed with `cursor-agent --version` and `kiro-cli
--version`. Neither CLI documents a noninteractive plugin install or removal
command. Cursor must load the native npm package, not the repository-root
Agent Plugin, to get hooks. Kiro uses the repository-root Power and the
separate Kiro hook file.

Setup validates an existing hook file before invoking a native CLI, preserves
unrelated entries, and changes only exact Agent LCM-owned registrations. It
backs up a changed file as `*-pre-agent-lcm-*.json` and holds an atomic lock
directory at `<target>.lock` for at most ten seconds. A helper process anchors
its working directory to the checked target directory before it reads, backs
up, or publishes through a unique `wx` temporary file, `fsync`, and rename.
Symlinked directory components, lock paths, targets, and non-regular files are
refused. Hook commands must be absolute paths without shell metacharacters. These rules make
repeated setup and removal safe while avoiding a second user-level hook copy
after native installation. A native client and a hook file cannot share one
transaction. If another process changes the hook file during native work,
Agent LCM reports whether the native action completed or setup stopped, then
leaves the changed bytes untouched.

Hooks start the daemon on demand. You can also manage it directly:

```sh
Expand Down
59 changes: 46 additions & 13 deletions dist/cli.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import { runCapture, runHook } from "./hook.js";
import { readStatus } from "./installer.js";
import { startMcpServer } from "./mcp.js";
import { packageVersion } from "./release.js";
import { setupHarness, setupStatus } from "./setup.js";
import { removeHarness, setupHarness, setupStatus } from "./setup.js";
import { detectedHarnesses } from "./setup-targets.js";
export async function main(argv) {
const [command, ...rest] = argv;
Expand Down Expand Up @@ -46,9 +46,20 @@ export async function main(argv) {
return;
}
const harness = captureHarness(rest[0]);
const home = optionValue(rest, "--home");
printSetupReports(setupHarness(harness, {
home: optionValue(rest, "--home"),
home,
command: commandPath,
...(home ? { env: lifecycleEnvironment(home) } : {}),
}), rest.includes("--json"));
return;
}
if (command === "remove") {
const harness = captureHarness(rest[0], "remove");
const home = optionValue(rest, "--home");
printSetupReports(removeHarness(harness, {
home,
...(home ? { env: lifecycleEnvironment(home) } : {}),
}), rest.includes("--json"));
return;
}
Expand Down Expand Up @@ -258,6 +269,7 @@ Commands:
agent-lcm setup all
agent-lcm setup <codex|cursor|vscode|copilot|kiro> [--home PATH]
agent-lcm setup status
agent-lcm remove <codex|cursor|vscode|copilot|kiro> [--home PATH]
agent-lcm status [--codex-home PATH] [--json]
agent-lcm doctor [--codex-home PATH] [--json] Diagnose install, storage, and capture state
agent-lcm health [--json]
Expand All @@ -273,10 +285,10 @@ Commands:
agent-lcm import-codex-sessions [--from PATH] [--dry-run] [--progress] [--json]
`);
}
function captureHarness(value) {
function captureHarness(value, action = "setup") {
if (value === "codex" || value === "cursor" || value === "vscode" || value === "copilot" || value === "kiro")
return value;
throw new Error("Usage: agent-lcm setup <codex|cursor|vscode|copilot|kiro> [--home PATH]");
throw new Error(`Usage: agent-lcm ${action} <codex|cursor|vscode|copilot|kiro> [--home PATH]`);
}
function importHarness(value) {
if (value === "codex" || value === "cursor" || value === "vscode" || value === "copilot" || value === "kiro")
Expand Down Expand Up @@ -307,15 +319,36 @@ function printObjectOrText(value) {
function printSetupReports(value, json) {
if (json) {
printObjectOrText(value);
return;
}
const reports = Array.isArray(value) ? value : [value];
if (reports.length === 0) {
process.stdout.write("No supported harnesses were detected. Configure one with agent-lcm setup <harness>.\n");
return;
}
for (const report of reports) {
const state = report.changed ? "have been configured" : "are already configured";
process.stdout.write(`${report.harness} hooks ${state}: ${report.path}\n`);
else {
const reports = Array.isArray(value) ? value : [value];
if (reports.length === 0) {
process.stdout.write("No supported harnesses were detected. Configure one with agent-lcm setup <harness>.\n");
return;
}
for (const report of reports) {
process.stdout.write(`${report.harness} ${report.action}: ${report.status}\n`);
process.stdout.write(`Hooks ${report.hooks.changed ? "changed" : "unchanged"}: ${report.hooks.path}\n`);
if (report.status === "manual-required") {
process.stdout.write(report.nativeCli === null
? "Native CLI unavailable.\n"
: `${report.nativeCli} is installed, but it has no supported noninteractive plugin ${report.action} command.\n`);
}
if (report.status !== "complete")
process.stdout.write(`Manual steps: ${report.guide}\n`);
}
}
const reports = Array.isArray(value) ? value : [value];
if (reports.some((report) => report.status !== "complete"))
process.exitCode = 2;
}
function lifecycleEnvironment(home) {
return {
...process.env,
HOME: home,
USERPROFILE: home,
CODEX_HOME: home,
COPILOT_HOME: home,
AGENT_LCM_HOME: path.join(home, "agent-lcm"),
};
}
62 changes: 62 additions & 0 deletions dist/copilot-plugin.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
import fs from "node:fs";
import os from "node:os";
import path from "node:path";
import { fileURLToPath } from "node:url";
import { assertSafeSetupCommand } from "./setup-hook-status.js";
const PACKAGE_ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
export function withCopilotPluginSource(command, callback) {
assertSafeSetupCommand(command);
const temporary = fs.mkdtempSync(path.join(os.tmpdir(), "agent-lcm-copilot-plugin-"));
const source = path.join(temporary, "agent-lcm");
try {
fs.mkdirSync(source, { mode: 0o700 });
const packageJson = readPackageJson();
writeJson(path.join(source, "plugin.json"), {
name: "agent-lcm",
version: packageJson.version,
description: packageJson.description,
author: { name: "Team Volt" },
homepage: "https://github.com/Team-Volt/agent-lcm",
license: "MIT",
skills: "skills/",
hooks: "hooks.json",
mcpServers: ".mcp.json",
});
writeJson(path.join(source, "hooks.json"), copilotHooks(command));
writeJson(path.join(source, ".mcp.json"), {
mcpServers: {
"agent-lcm": { type: "stdio", command: "node", args: [command, "mcp"] },
},
});
fs.cpSync(path.join(PACKAGE_ROOT, "skills"), path.join(source, "skills"), { recursive: true });
return callback(source);
}
finally {
fs.rmSync(temporary, { recursive: true, force: true });
}
}
function copilotHooks(command) {
const capture = `node "${command}" capture --harness auto`;
return {
version: 1,
hooks: {
sessionStart: [{ type: "command", command: capture }],
userPromptSubmitted: [{ type: "command", command: capture }],
postToolUse: [{ type: "command", command: capture }],
sessionEnd: [{ type: "command", command: capture }],
},
};
}
function readPackageJson() {
const value = JSON.parse(fs.readFileSync(path.join(PACKAGE_ROOT, "package.json"), "utf8"));
if (!isRecord(value) || typeof value.version !== "string" || typeof value.description !== "string") {
throw new Error("Agent LCM package metadata is invalid.");
}
return { version: value.version, description: value.description };
}
function writeJson(target, value) {
fs.writeFileSync(target, `${JSON.stringify(value, null, 2)}\n`, { mode: 0o600 });
}
function isRecord(value) {
return typeof value === "object" && value !== null && !Array.isArray(value);
}
Loading