Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
6 changes: 6 additions & 0 deletions .claude-plugin/marketplace.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"name": "agent-lcm",
"description": "Shared local context memory for agent harnesses.",
"owner": { "name": "Team Volt" },
"plugins": [{ "name": "agent-lcm", "source": "." }]
}
10 changes: 10 additions & 0 deletions .claude-plugin/plugin.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
{
"name": "agent-lcm",
"version": "0.0.7",
"description": "Shared local context memory for agent harnesses.",
"author": { "name": "Team Volt" },
"homepage": "https://github.com/Team-Volt/agent-lcm",
"keywords": ["agent-memory", "context", "recall", "sessions"],
"skills": "./skills/",
"mcpServers": "./mcp.claude.json"
}
3 changes: 2 additions & 1 deletion .mcp.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,8 @@
"agent-lcm": {
"type": "stdio",
"command": "node",
"args": ["${PLUGIN_ROOT}/bin/agent-lcm", "mcp"]
"args": ["./bin/agent-lcm", "mcp"],
"cwd": "."
}
}
}
13 changes: 9 additions & 4 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,10 +33,11 @@ docs/ architecture and troubleshooting

## 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.
- `agent-lcm setup <harness>` uses native lifecycle commands for Codex, Claude
Code, 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, except Claude Code, whose native removal uninstalls
only the user-scoped plugin and leaves its marketplace configured.
- 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
Expand All @@ -48,6 +49,10 @@ docs/ architecture and troubleshooting
executable.
- Successful native Codex setup must not create `~/.codex/hooks.json`; it may
remove only exact Agent LCM fallback entries from an existing file.
- Claude Code uses `.claude-plugin/plugin.json`, a local marketplace source `.`,
`hooks/hooks.json`, and `mcp.claude.json` with `${CLAUDE_PLUGIN_ROOT}`. Its
setup status reports `hooksConfigured: false` and does not inspect or mutate
`settings.json`; native plugin health remains unknown to `doctor`.
- 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
Expand Down
39 changes: 29 additions & 10 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# Agent LCM

Agent LCM gives coding agents one shared, local memory. It captures sessions from
Codex, Cursor, VS Code, GitHub Copilot, and Kiro, then makes that history
Codex, Cursor, VS Code, GitHub Copilot, Kiro, and Claude Code, then makes that history
searchable from any of those harnesses through MCP.

LCM stands for lossless context memory. The sanitized event archive is the
Expand All @@ -19,7 +19,10 @@ hosted memory service.
combination. Cross-harness search is the default.
- Keep one private store per user and machine instead of one database per
harness or repository.
- Import sessions that existed before Agent LCM was installed.
- Import sessions that existed before Agent LCM was installed when that harness
has a supported importer.
- Capture Claude Code sessions through its live hooks. Claude Code has no
historical importer in Agent LCM.
- Rebuild the SQLite index from the raw archive if the derived data is damaged.
- Run without embeddings, external APIs, or cloud storage.

Expand Down Expand Up @@ -82,23 +85,26 @@ removal:
| 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) |
| Claude Code | `agent-lcm setup claude` | [Claude Code guide](docs/install/claude.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/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.
[Cursor Marketplace](https://cursor.com/marketplace),
[Kiro Powers](https://kiro.dev/docs/powers/), and
[Claude Code plugins](https://code.claude.com/docs/en/plugins) documentation. If
setup cannot run a supported native command, it reports the guide and uses the
manual hook path when that harness needs one.

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`

The published npm package omits that root manifest so Codex and Cursor select
their native compatibility manifests, which include hooks. Copilot and VS Code
The published npm package omits that root manifest so Codex, Cursor, and Claude
Code 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:

Expand Down Expand Up @@ -128,6 +134,7 @@ agent-lcm setup cursor
agent-lcm setup vscode
agent-lcm setup copilot
agent-lcm setup kiro
agent-lcm setup claude
```

Run only the commands for the harnesses you use. A legacy VS Code and GitHub
Expand All @@ -140,15 +147,17 @@ timestamped `-pre-agent-lcm-` backup beside it.

Legacy or setup-managed user hook locations are:

| Harness | Hook file |
| Harness | Legacy or setup path |
| --- | --- |
| Codex | `~/.codex/hooks.json` |
| Cursor | `~/.cursor/hooks.json` |
| VS Code | `~/.copilot/hooks/agent-lcm.json` |
| GitHub Copilot | `~/.copilot/hooks/agent-lcm.json` |
| Kiro | `~/.kiro/hooks/agent-lcm.json` |
| Claude Code | No managed hook file; status path is `~/.claude/settings.json` |

Codex, Cursor, Copilot, and VS Code native plugins carry their own hooks. Setup
Codex, Cursor, Copilot, VS Code, and Claude 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.
Expand Down Expand Up @@ -187,6 +196,13 @@ Native lifecycle support is limited to the commands that each client documents:
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.
- Claude Code probes `claude plugin marketplace list --json` and `claude plugin
list --json`. Setup adds the installed package root with `claude plugin
marketplace add <package-root> --scope user` when needed, then runs either
`claude plugin install agent-lcm@agent-lcm --scope user` or
`claude plugin update agent-lcm@agent-lcm --scope user`. Removal uninstalls
only that user plugin and retains the marketplace. Pass `--home PATH` to use
a Claude config directory through `CLAUDE_CONFIG_DIR`.

Setup validates an existing hook file before invoking a native CLI, preserves
unrelated entries, and changes only exact Agent LCM-owned registrations. It
Expand Down Expand Up @@ -262,6 +278,9 @@ Codex-only command remains available during initial migration work:
agent-lcm import-codex-sessions --dry-run --json
```

Claude Code is live-only in Agent LCM. It is not included in the historical
import scan.

## Local storage

The default store is `~/.agent-lcm`. Set `AGENT_LCM_HOME` to use another one.
Expand Down
67 changes: 67 additions & 0 deletions dist/claude-lifecycle.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
import path from "node:path";
export class ClaudeLifecycleOutputError extends Error {
name = "ClaudeLifecycleOutputError";
argv;
constructor(argv) {
super("Claude CLI returned malformed lifecycle JSON.");
this.argv = argv;
}
}
export function runClaudeLifecycle(action, packageRoot, run) {
if (action === "remove") {
const argv = ["plugin", "list", "--json"];
const plugins = parseRecords(run(argv), argv, isClaudePlugin);
if (hasUserPlugin(plugins))
run(["plugin", "uninstall", "agent-lcm@agent-lcm", "--scope", "user"]);
return;
}
const marketplaceArgv = ["plugin", "marketplace", "list", "--json"];
const marketplaces = parseRecords(run(marketplaceArgv), marketplaceArgv, isRecord);
const ownedMarketplaces = marketplaces.filter((entry) => entry.name === "agent-lcm");
if (ownedMarketplaces.length > 1)
throw new ClaudeLifecycleOutputError(marketplaceArgv);
const marketplace = ownedMarketplaces[0];
if (marketplace !== undefined) {
if (!isClaudeMarketplace(marketplace) || path.resolve(marketplace.path) !== packageRoot) {
throw new ClaudeLifecycleOutputError(marketplaceArgv);
}
}
if (marketplace === undefined)
run(["plugin", "marketplace", "add", packageRoot, "--scope", "user"]);
const pluginArgv = ["plugin", "list", "--json"];
const plugins = parseRecords(run(pluginArgv), pluginArgv, isClaudePlugin);
run(["plugin", hasUserPlugin(plugins) ? "update" : "install", "agent-lcm@agent-lcm", "--scope", "user"]);
}
function parseRecords(stdout, argv, isRecordType) {
let value;
try {
value = JSON.parse(stdout);
}
catch {
throw new ClaudeLifecycleOutputError(argv);
}
if (!Array.isArray(value) || !value.every(isRecordType))
throw new ClaudeLifecycleOutputError(argv);
return value;
}
function hasUserPlugin(plugins) {
return plugins.some((plugin) => plugin.id === "agent-lcm@agent-lcm" && plugin.scope === "user");
}
function isClaudeMarketplace(value) {
return isRecord(value)
&& typeof value.name === "string"
&& typeof value.path === "string";
}
function isClaudePlugin(value) {
return isRecord(value)
&& typeof value.id === "string"
&& typeof value.version === "string"
&& typeof value.scope === "string"
&& typeof value.enabled === "boolean"
&& typeof value.installPath === "string"
&& typeof value.installedAt === "string"
&& typeof value.lastUpdated === "string";
}
function isRecord(value) {
return typeof value === "object" && value !== null && !Array.isArray(value);
}
18 changes: 10 additions & 8 deletions dist/cli.js
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ export async function main(argv) {
printSetupReports(setupHarness(harness, {
home,
command: commandPath,
...(home ? { env: lifecycleEnvironment(home) } : {}),
...(home ? { env: lifecycleEnvironment(harness, home) } : {}),
}), rest.includes("--json"));
return;
}
Expand All @@ -59,7 +59,7 @@ export async function main(argv) {
const home = optionValue(rest, "--home");
printSetupReports(removeHarness(harness, {
home,
...(home ? { env: lifecycleEnvironment(home) } : {}),
...(home ? { env: lifecycleEnvironment(harness, home) } : {}),
}), rest.includes("--json"));
return;
}
Expand Down Expand Up @@ -265,11 +265,11 @@ Commands:
agent-lcm daemon run|start|restart|status|stop
agent-lcm mcp
agent-lcm hook <event>
agent-lcm capture --harness codex|cursor|vscode|copilot|kiro|auto [event]
agent-lcm capture --harness codex|cursor|vscode|copilot|kiro|claude|auto [event]
agent-lcm setup all
agent-lcm setup <codex|cursor|vscode|copilot|kiro> [--home PATH]
agent-lcm setup <codex|cursor|vscode|copilot|kiro|claude> [--home PATH]
agent-lcm setup status
agent-lcm remove <codex|cursor|vscode|copilot|kiro> [--home PATH]
agent-lcm remove <codex|cursor|vscode|copilot|kiro|claude> [--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 @@ -286,9 +286,9 @@ Commands:
`);
}
function captureHarness(value, action = "setup") {
if (value === "codex" || value === "cursor" || value === "vscode" || value === "copilot" || value === "kiro")
if (value === "codex" || value === "cursor" || value === "vscode" || value === "copilot" || value === "kiro" || value === "claude")
return value;
throw new Error(`Usage: agent-lcm ${action} <codex|cursor|vscode|copilot|kiro> [--home PATH]`);
throw new Error(`Usage: agent-lcm ${action} <codex|cursor|vscode|copilot|kiro|claude> [--home PATH]`);
}
function importHarness(value) {
if (value === "codex" || value === "cursor" || value === "vscode" || value === "copilot" || value === "kiro")
Expand Down Expand Up @@ -342,7 +342,9 @@ function printSetupReports(value, json) {
if (reports.some((report) => report.status !== "complete"))
process.exitCode = 2;
}
function lifecycleEnvironment(home) {
function lifecycleEnvironment(harness, home) {
if (harness === "claude")
return { ...process.env, CLAUDE_CONFIG_DIR: home };
return {
...process.env,
HOME: home,
Expand Down
5 changes: 5 additions & 0 deletions dist/doctor.js
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,11 @@ function adapterStatus(status) {
vscode: nativePluginAdapter("VS Code", setups.vscode.hooksConfigured),
copilot: nativePluginAdapter("Copilot", setups.copilot.hooksConfigured),
kiro: setupAdapter("kiro", setups.kiro.hooksConfigured),
claude: {
configured: null,
state: "unknown",
detail: "Claude native plugin health is not checked by doctor. Run `claude plugin list --json` or use the client's installed-plugin view.",
},
};
}
function nativePluginAdapter(harness, legacyHooksConfigured) {
Expand Down
2 changes: 1 addition & 1 deletion dist/events.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { DEFAULT_LIMITS } from "./config.js";
import { sanitizeForStorage, sha256 } from "./redact.js";
export const HARNESS_NAMES = ["codex", "cursor", "vscode", "copilot", "kiro", "mcp", "import"];
export const HARNESS_NAMES = ["codex", "cursor", "vscode", "copilot", "kiro", "claude", "mcp", "import"];
export function harnessSessionId(harness, nativeId) {
const id = nativeId.trim();
if (!id)
Expand Down
3 changes: 2 additions & 1 deletion dist/harnesses.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ const EVENT_MAP = {
vscode: { SessionStart: "SessionStart", UserPromptSubmit: "UserPromptSubmit", PostToolUse: "PostToolUse", Stop: "Stop" },
copilot: { sessionStart: "SessionStart", userPromptSubmitted: "UserPromptSubmit", postToolUse: "PostToolUse", sessionEnd: "Stop" },
kiro: { SessionStart: "SessionStart", UserPromptSubmit: "UserPromptSubmit", PostToolUse: "PostToolUse", Stop: "Stop" },
claude: { SessionStart: "SessionStart", UserPromptSubmit: "UserPromptSubmit", PostToolUse: "PostToolUse", Stop: "Stop" },
};
const KIRO_ALIASES = {
sessionStart: "SessionStart",
Expand Down Expand Up @@ -109,7 +110,7 @@ function stripHarnessPrefix(sessionId) {
return isHarness(prefix[1]) ? prefix[2] : sessionId;
}
function isHarness(value) {
return value === "codex" || value === "cursor" || value === "vscode" || value === "copilot" || value === "kiro" || value === "mcp" || value === "import";
return value === "codex" || value === "cursor" || value === "vscode" || value === "copilot" || value === "kiro" || value === "claude" || value === "mcp" || value === "import";
}
function isRecord(value) {
return typeof value === "object" && value !== null && !Array.isArray(value);
Expand Down
2 changes: 1 addition & 1 deletion dist/hook.js
Original file line number Diff line number Diff line change
Expand Up @@ -109,7 +109,7 @@ function captureArguments(args) {
if (index < 0 || !args[index + 1])
throw new Error("Usage: agent-lcm capture --harness <harness> [event]");
const requested = args[index + 1];
if (requested !== "auto" && requested !== "codex" && requested !== "cursor" && requested !== "vscode" && requested !== "copilot" && requested !== "kiro") {
if (requested !== "auto" && requested !== "codex" && requested !== "cursor" && requested !== "vscode" && requested !== "copilot" && requested !== "kiro" && requested !== "claude") {
throw new Error(`Unknown capture harness: ${requested}`);
}
const remaining = args.filter((_, position) => position !== index && position !== index + 1);
Expand Down
Loading