omo-memory 0.1.0
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 +84 -0
- package/dist/cli.js +84 -0
- package/dist/mcp.js +76 -0
- package/dist/memory.js +257 -0
- package/dist/privacy.js +21 -0
- package/dist/types.js +1 -0
- package/docs/adapter-integration.md +125 -0
- package/docs/epic-omo-memory.md +98 -0
- package/package.json +49 -0
package/README.md
ADDED
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
# OMO Memory
|
|
2
|
+
|
|
3
|
+
OMO Memory is a host-neutral local session/work memory for OMO adapters.
|
|
4
|
+
|
|
5
|
+
It gives lazycodex, omo-on-opencode, lfg, and future OMO adapters a shared local SQLite ledger that can be accessed through both:
|
|
6
|
+
|
|
7
|
+
- `omo-memory` CLI for install, inspection, search, and handoff workflows.
|
|
8
|
+
- `omo-memory mcp` stdio server for coding tools and agents.
|
|
9
|
+
|
|
10
|
+
## Product shape
|
|
11
|
+
|
|
12
|
+
- Global local DB: `~/.omo/memory/state.sqlite`
|
|
13
|
+
- Project namespacing: by git remote + project root hash
|
|
14
|
+
- Privacy default: local-only, no network sync, no secrets by design
|
|
15
|
+
- Intended adapters: Codex/lazycodex, OpenCode/OMO, GrokBuild/lfg
|
|
16
|
+
|
|
17
|
+
## MVP commands
|
|
18
|
+
|
|
19
|
+
```sh
|
|
20
|
+
npm install
|
|
21
|
+
npm run build
|
|
22
|
+
node dist/cli.js init
|
|
23
|
+
node dist/cli.js session start --host grok --adapter lfg
|
|
24
|
+
node dist/cli.js event record --type decision --summary "Chose SQLite + MCP + CLI for OMO shared memory"
|
|
25
|
+
node dist/cli.js recent
|
|
26
|
+
node dist/cli.js mcp
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
## Install
|
|
30
|
+
|
|
31
|
+
After the package is published to npm, use the same package for CLI and MCP:
|
|
32
|
+
|
|
33
|
+
```sh
|
|
34
|
+
npx -y omo-memory init
|
|
35
|
+
npx -y omo-memory recent --limit 5
|
|
36
|
+
npx -y omo-memory mcp
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
For local development before publish:
|
|
40
|
+
|
|
41
|
+
```sh
|
|
42
|
+
npm install
|
|
43
|
+
npm run build
|
|
44
|
+
npm link
|
|
45
|
+
omo-memory init
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
## MCP registration
|
|
49
|
+
|
|
50
|
+
Register the same MCP server in every host that should read/write the shared local memory DB.
|
|
51
|
+
|
|
52
|
+
Codex:
|
|
53
|
+
|
|
54
|
+
```sh
|
|
55
|
+
codex mcp add omo-memory -- npx -y omo-memory mcp
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
Grok:
|
|
59
|
+
|
|
60
|
+
```sh
|
|
61
|
+
grok mcp add omo-memory -- npx -y omo-memory mcp
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
Both hosts use `~/.omo/memory/state.sqlite` by default. The `host` value is recorded when an adapter calls `memory_start_session`, not by installing separate servers.
|
|
65
|
+
|
|
66
|
+
## MCP tools
|
|
67
|
+
|
|
68
|
+
Initial stdio MCP tools:
|
|
69
|
+
|
|
70
|
+
- `memory_init`
|
|
71
|
+
- `memory_project_context`
|
|
72
|
+
- `memory_start_session`
|
|
73
|
+
- `memory_record_event`
|
|
74
|
+
- `memory_recent_events`
|
|
75
|
+
- `memory_write_handoff`
|
|
76
|
+
- `memory_export`
|
|
77
|
+
- `memory_purge`
|
|
78
|
+
|
|
79
|
+
## Non-goals for MVP
|
|
80
|
+
|
|
81
|
+
- No cloud sync.
|
|
82
|
+
- No full transcript capture by default.
|
|
83
|
+
- No secret storage.
|
|
84
|
+
- No adapter-specific host lock-in.
|
package/dist/cli.js
ADDED
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { readFileSync } from "node:fs";
|
|
3
|
+
import { doctorReport, exportMemory, initMemory, purgeMemory, recentEvents, recordEvent, startSession, writeHandoff } from "./memory.js";
|
|
4
|
+
import { runMcpServer } from "./mcp.js";
|
|
5
|
+
async function main(argv) {
|
|
6
|
+
const [command, subcommand, ...rest] = argv;
|
|
7
|
+
if (command === undefined || command === "help" || command === "--help" || command === "-h") {
|
|
8
|
+
printHelp();
|
|
9
|
+
return;
|
|
10
|
+
}
|
|
11
|
+
if (command === "mcp") {
|
|
12
|
+
await runMcpServer();
|
|
13
|
+
return;
|
|
14
|
+
}
|
|
15
|
+
const result = runCommand(command, subcommand, rest);
|
|
16
|
+
process.stdout.write(`${JSON.stringify(result, null, 2)}\n`);
|
|
17
|
+
}
|
|
18
|
+
function runCommand(command, subcommand, rest) {
|
|
19
|
+
if (command === "init") {
|
|
20
|
+
return { ok: true, ...initMemory() };
|
|
21
|
+
}
|
|
22
|
+
if (command === "doctor") {
|
|
23
|
+
return { ok: true, ...doctorReport() };
|
|
24
|
+
}
|
|
25
|
+
if (command === "export") {
|
|
26
|
+
return { ok: true, ...exportMemory() };
|
|
27
|
+
}
|
|
28
|
+
if (command === "purge") {
|
|
29
|
+
const args = [subcommand, ...rest].filter((value) => value !== undefined);
|
|
30
|
+
return { ok: true, ...purgeMemory({ yes: args.includes("--yes") }) };
|
|
31
|
+
}
|
|
32
|
+
if (command === "session" && subcommand === "start") {
|
|
33
|
+
const host = parseHost(readFlag(rest, "--host") ?? "unknown");
|
|
34
|
+
const adapter = readFlag(rest, "--adapter") ?? "unknown";
|
|
35
|
+
return { ok: true, ...startSession({ host, adapter }) };
|
|
36
|
+
}
|
|
37
|
+
if (command === "event" && subcommand === "record") {
|
|
38
|
+
const type = readFlag(rest, "--type") ?? fail("event record requires --type");
|
|
39
|
+
const summary = readFlag(rest, "--summary") ?? fail("event record requires --summary");
|
|
40
|
+
const payloadJson = readFlag(rest, "--payload-json");
|
|
41
|
+
const sessionId = readFlag(rest, "--session-id");
|
|
42
|
+
return { ok: true, ...recordEvent({ type, summary, ...(payloadJson === undefined ? {} : { payloadJson }), ...(sessionId === undefined ? {} : { sessionId }) }) };
|
|
43
|
+
}
|
|
44
|
+
if (command === "recent") {
|
|
45
|
+
const limitRaw = readFlag([subcommand, ...rest].filter((value) => value !== undefined), "--limit");
|
|
46
|
+
const limit = limitRaw === undefined ? 10 : Number(limitRaw);
|
|
47
|
+
if (!Number.isInteger(limit) || limit <= 0)
|
|
48
|
+
fail("recent --limit must be a positive integer");
|
|
49
|
+
return { ok: true, events: recentEvents(limit) };
|
|
50
|
+
}
|
|
51
|
+
if (command === "handoff" && subcommand === "write") {
|
|
52
|
+
const summary = readFlag(rest, "--summary");
|
|
53
|
+
const summaryFile = readFlag(rest, "--summary-file");
|
|
54
|
+
const sessionId = readFlag(rest, "--session-id");
|
|
55
|
+
const summaryMd = summary ?? (summaryFile === undefined ? undefined : readFileSync(summaryFile, "utf8")) ?? fail("handoff write requires --summary or --summary-file");
|
|
56
|
+
return { ok: true, ...writeHandoff(summaryMd, sessionId) };
|
|
57
|
+
}
|
|
58
|
+
fail(`unknown command: ${[command, subcommand].filter(Boolean).join(" ")}`);
|
|
59
|
+
}
|
|
60
|
+
function readFlag(args, name) {
|
|
61
|
+
const index = args.indexOf(name);
|
|
62
|
+
if (index === -1)
|
|
63
|
+
return undefined;
|
|
64
|
+
const value = args[index + 1];
|
|
65
|
+
if (value === undefined || value.startsWith("--"))
|
|
66
|
+
fail(`${name} requires a value`);
|
|
67
|
+
return value;
|
|
68
|
+
}
|
|
69
|
+
function parseHost(value) {
|
|
70
|
+
if (value === "codex" || value === "opencode" || value === "grok" || value === "unknown")
|
|
71
|
+
return value;
|
|
72
|
+
fail("--host must be one of codex, opencode, grok, unknown");
|
|
73
|
+
}
|
|
74
|
+
function fail(message) {
|
|
75
|
+
throw new Error(message);
|
|
76
|
+
}
|
|
77
|
+
function printHelp() {
|
|
78
|
+
process.stdout.write(`OMO Memory\n\nCommands:\n omo-memory init\n omo-memory doctor\n omo-memory export\n omo-memory purge --yes\n omo-memory session start --host <codex|opencode|grok|unknown> --adapter <name>\n omo-memory event record --type <type> --summary <text> [--session-id <id>]\n omo-memory recent [--limit <n>]\n omo-memory handoff write (--summary <text> | --summary-file <path>) [--session-id <id>]\n omo-memory mcp\n`);
|
|
79
|
+
}
|
|
80
|
+
main(process.argv.slice(2)).catch((error) => {
|
|
81
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
82
|
+
process.stderr.write(`${message}\n`);
|
|
83
|
+
process.exitCode = 1;
|
|
84
|
+
});
|
package/dist/mcp.js
ADDED
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
2
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
3
|
+
import { z } from "zod";
|
|
4
|
+
import { exportMemory, initMemory, memoryPaths, purgeMemory, PurgeConfirmationError, recentEvents, recordEvent, resolveProjectContext, startSession, writeHandoff } from "./memory.js";
|
|
5
|
+
export async function runMcpServer() {
|
|
6
|
+
const server = new McpServer({ name: "omo-memory", version: "0.1.0" });
|
|
7
|
+
server.registerTool("memory_init", {
|
|
8
|
+
title: "Initialize OMO Memory",
|
|
9
|
+
description: "Create or migrate the local OMO memory SQLite database.",
|
|
10
|
+
inputSchema: {},
|
|
11
|
+
}, async () => jsonResult(initMemory()));
|
|
12
|
+
server.registerTool("memory_project_context", {
|
|
13
|
+
title: "Get OMO Project Context",
|
|
14
|
+
description: "Return the current project identity used by OMO Memory.",
|
|
15
|
+
inputSchema: {},
|
|
16
|
+
}, async () => jsonResult({ paths: memoryPaths(), project: resolveProjectContext() }));
|
|
17
|
+
server.registerTool("memory_export", {
|
|
18
|
+
title: "Export OMO Memory",
|
|
19
|
+
description: "Export the current project's OMO memory sessions, events, and handoffs.",
|
|
20
|
+
inputSchema: {},
|
|
21
|
+
}, async () => jsonResult(exportMemory()));
|
|
22
|
+
server.registerTool("memory_purge", {
|
|
23
|
+
title: "Purge OMO Memory",
|
|
24
|
+
description: "Delete the current project's OMO memory sessions, events, handoffs, and project row.",
|
|
25
|
+
inputSchema: {
|
|
26
|
+
confirm: z.boolean(),
|
|
27
|
+
},
|
|
28
|
+
}, async ({ confirm }) => {
|
|
29
|
+
try {
|
|
30
|
+
return jsonResult(purgeMemory({ yes: confirm }));
|
|
31
|
+
}
|
|
32
|
+
catch (error) {
|
|
33
|
+
if (error instanceof PurgeConfirmationError) {
|
|
34
|
+
return jsonResult({ ok: false, error: "memory_purge requires confirm: true" });
|
|
35
|
+
}
|
|
36
|
+
throw error;
|
|
37
|
+
}
|
|
38
|
+
});
|
|
39
|
+
server.registerTool("memory_start_session", {
|
|
40
|
+
title: "Start OMO Session",
|
|
41
|
+
description: "Record a new OMO adapter session for the current project.",
|
|
42
|
+
inputSchema: {
|
|
43
|
+
host: z.enum(["codex", "opencode", "grok", "unknown"]),
|
|
44
|
+
adapter: z.string().min(1),
|
|
45
|
+
},
|
|
46
|
+
}, async ({ host, adapter }) => jsonResult(startSession({ host, adapter })));
|
|
47
|
+
server.registerTool("memory_record_event", {
|
|
48
|
+
title: "Record OMO Memory Event",
|
|
49
|
+
description: "Append a summarized event to the current project's OMO memory ledger.",
|
|
50
|
+
inputSchema: {
|
|
51
|
+
type: z.string().min(1),
|
|
52
|
+
summary: z.string().min(1),
|
|
53
|
+
payloadJson: z.string().optional(),
|
|
54
|
+
sessionId: z.string().optional(),
|
|
55
|
+
},
|
|
56
|
+
}, async ({ type, summary, payloadJson, sessionId }) => jsonResult(recordEvent({ type, summary, ...(payloadJson === undefined ? {} : { payloadJson }), ...(sessionId === undefined ? {} : { sessionId }) })));
|
|
57
|
+
server.registerTool("memory_recent_events", {
|
|
58
|
+
title: "List Recent OMO Memory Events",
|
|
59
|
+
description: "List recent events for the current project.",
|
|
60
|
+
inputSchema: {
|
|
61
|
+
limit: z.number().int().positive().max(100).default(10),
|
|
62
|
+
},
|
|
63
|
+
}, async ({ limit }) => jsonResult({ events: recentEvents(limit) }));
|
|
64
|
+
server.registerTool("memory_write_handoff", {
|
|
65
|
+
title: "Write OMO Handoff",
|
|
66
|
+
description: "Store a handoff summary for the current project.",
|
|
67
|
+
inputSchema: {
|
|
68
|
+
summaryMd: z.string().min(1),
|
|
69
|
+
sessionId: z.string().optional(),
|
|
70
|
+
},
|
|
71
|
+
}, async ({ summaryMd, sessionId }) => jsonResult(writeHandoff(summaryMd, sessionId)));
|
|
72
|
+
await server.connect(new StdioServerTransport());
|
|
73
|
+
}
|
|
74
|
+
function jsonResult(value) {
|
|
75
|
+
return { content: [{ type: "text", text: JSON.stringify(value, null, 2) }] };
|
|
76
|
+
}
|
package/dist/memory.js
ADDED
|
@@ -0,0 +1,257 @@
|
|
|
1
|
+
import Database from "better-sqlite3";
|
|
2
|
+
import { createHash, randomUUID } from "node:crypto";
|
|
3
|
+
import { mkdirSync } from "node:fs";
|
|
4
|
+
import { homedir } from "node:os";
|
|
5
|
+
import { dirname, join, resolve } from "node:path";
|
|
6
|
+
import { execFileSync } from "node:child_process";
|
|
7
|
+
import { redactSecrets, sanitizeGitRemote } from "./privacy.js";
|
|
8
|
+
export class PurgeConfirmationError extends Error {
|
|
9
|
+
constructor() {
|
|
10
|
+
super("purge requires --yes");
|
|
11
|
+
this.name = "PurgeConfirmationError";
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
const SCHEMA_VERSION = 1;
|
|
15
|
+
export function defaultDbPath() {
|
|
16
|
+
return process.env["OMO_MEMORY_DB"] ?? join(homedir(), ".omo", "memory", "state.sqlite");
|
|
17
|
+
}
|
|
18
|
+
export function memoryPaths() {
|
|
19
|
+
return { dbPath: defaultDbPath() };
|
|
20
|
+
}
|
|
21
|
+
export function openMemoryDb(dbPath = defaultDbPath()) {
|
|
22
|
+
mkdirSync(dirname(dbPath), { recursive: true });
|
|
23
|
+
const db = new Database(dbPath);
|
|
24
|
+
db.pragma("journal_mode = WAL");
|
|
25
|
+
return db;
|
|
26
|
+
}
|
|
27
|
+
export function migrate(db) {
|
|
28
|
+
db.exec(`
|
|
29
|
+
CREATE TABLE IF NOT EXISTS schema_meta (
|
|
30
|
+
key TEXT PRIMARY KEY,
|
|
31
|
+
value TEXT NOT NULL
|
|
32
|
+
);
|
|
33
|
+
|
|
34
|
+
CREATE TABLE IF NOT EXISTS projects (
|
|
35
|
+
id TEXT PRIMARY KEY,
|
|
36
|
+
repo_root TEXT NOT NULL,
|
|
37
|
+
git_remote TEXT,
|
|
38
|
+
created_at TEXT NOT NULL,
|
|
39
|
+
last_seen_at TEXT NOT NULL
|
|
40
|
+
);
|
|
41
|
+
|
|
42
|
+
CREATE TABLE IF NOT EXISTS sessions (
|
|
43
|
+
id TEXT PRIMARY KEY,
|
|
44
|
+
project_id TEXT NOT NULL,
|
|
45
|
+
host TEXT NOT NULL,
|
|
46
|
+
adapter TEXT NOT NULL,
|
|
47
|
+
started_at TEXT NOT NULL,
|
|
48
|
+
ended_at TEXT,
|
|
49
|
+
git_branch TEXT,
|
|
50
|
+
git_head TEXT,
|
|
51
|
+
FOREIGN KEY(project_id) REFERENCES projects(id)
|
|
52
|
+
);
|
|
53
|
+
|
|
54
|
+
CREATE TABLE IF NOT EXISTS events (
|
|
55
|
+
id TEXT PRIMARY KEY,
|
|
56
|
+
session_id TEXT,
|
|
57
|
+
project_id TEXT NOT NULL,
|
|
58
|
+
type TEXT NOT NULL,
|
|
59
|
+
summary TEXT NOT NULL,
|
|
60
|
+
payload_json TEXT,
|
|
61
|
+
created_at TEXT NOT NULL,
|
|
62
|
+
FOREIGN KEY(project_id) REFERENCES projects(id),
|
|
63
|
+
FOREIGN KEY(session_id) REFERENCES sessions(id)
|
|
64
|
+
);
|
|
65
|
+
|
|
66
|
+
CREATE TABLE IF NOT EXISTS handoffs (
|
|
67
|
+
id TEXT PRIMARY KEY,
|
|
68
|
+
project_id TEXT NOT NULL,
|
|
69
|
+
session_id TEXT,
|
|
70
|
+
summary_md TEXT NOT NULL,
|
|
71
|
+
created_at TEXT NOT NULL,
|
|
72
|
+
FOREIGN KEY(project_id) REFERENCES projects(id),
|
|
73
|
+
FOREIGN KEY(session_id) REFERENCES sessions(id)
|
|
74
|
+
);
|
|
75
|
+
`);
|
|
76
|
+
db.prepare("INSERT OR REPLACE INTO schema_meta (key, value) VALUES ('schema_version', ?)").run(String(SCHEMA_VERSION));
|
|
77
|
+
}
|
|
78
|
+
export function initMemory(dbPath = defaultDbPath()) {
|
|
79
|
+
const db = openMemoryDb(dbPath);
|
|
80
|
+
try {
|
|
81
|
+
migrate(db);
|
|
82
|
+
return { dbPath, schemaVersion: SCHEMA_VERSION };
|
|
83
|
+
}
|
|
84
|
+
finally {
|
|
85
|
+
db.close();
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
export function doctorReport(dbPath = defaultDbPath()) {
|
|
89
|
+
const db = openMemoryDb(dbPath);
|
|
90
|
+
try {
|
|
91
|
+
migrate(db);
|
|
92
|
+
const project = resolveProjectContext();
|
|
93
|
+
const schemaVersion = Number(db.prepare("SELECT value FROM schema_meta WHERE key = 'schema_version'").pluck().get());
|
|
94
|
+
const count = (table) => Number(db.prepare(`SELECT COUNT(*) FROM ${table}`).pluck().get());
|
|
95
|
+
return {
|
|
96
|
+
paths: { dbPath },
|
|
97
|
+
schemaVersion,
|
|
98
|
+
project,
|
|
99
|
+
counts: {
|
|
100
|
+
projects: count("projects"),
|
|
101
|
+
sessions: count("sessions"),
|
|
102
|
+
events: count("events"),
|
|
103
|
+
handoffs: count("handoffs"),
|
|
104
|
+
},
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
finally {
|
|
108
|
+
db.close();
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
export function resolveProjectContext(cwd = process.cwd()) {
|
|
112
|
+
const repoRoot = gitValue(["rev-parse", "--show-toplevel"], cwd) ?? resolve(cwd);
|
|
113
|
+
const rawGitRemote = gitValue(["config", "--get", "remote.origin.url"], repoRoot);
|
|
114
|
+
const gitRemote = sanitizeGitRemote(rawGitRemote);
|
|
115
|
+
const gitBranch = gitValue(["rev-parse", "--abbrev-ref", "HEAD"], repoRoot);
|
|
116
|
+
const gitHead = gitValue(["rev-parse", "HEAD"], repoRoot);
|
|
117
|
+
const id = createHash("sha256").update(`${rawGitRemote ?? ""}\n${repoRoot}`).digest("hex").slice(0, 24);
|
|
118
|
+
return { id, repoRoot, gitRemote, gitBranch, gitHead };
|
|
119
|
+
}
|
|
120
|
+
export function upsertProject(db, project) {
|
|
121
|
+
const now = new Date().toISOString();
|
|
122
|
+
db.prepare(`
|
|
123
|
+
INSERT INTO projects (id, repo_root, git_remote, created_at, last_seen_at)
|
|
124
|
+
VALUES (?, ?, ?, ?, ?)
|
|
125
|
+
ON CONFLICT(id) DO UPDATE SET
|
|
126
|
+
repo_root = excluded.repo_root,
|
|
127
|
+
git_remote = excluded.git_remote,
|
|
128
|
+
last_seen_at = excluded.last_seen_at
|
|
129
|
+
`).run(project.id, project.repoRoot, project.gitRemote, now, now);
|
|
130
|
+
}
|
|
131
|
+
export function startSession(input, dbPath = defaultDbPath()) {
|
|
132
|
+
const db = openMemoryDb(dbPath);
|
|
133
|
+
try {
|
|
134
|
+
migrate(db);
|
|
135
|
+
const project = resolveProjectContext();
|
|
136
|
+
upsertProject(db, project);
|
|
137
|
+
const sessionId = randomUUID();
|
|
138
|
+
db.prepare(`
|
|
139
|
+
INSERT INTO sessions (id, project_id, host, adapter, started_at, git_branch, git_head)
|
|
140
|
+
VALUES (?, ?, ?, ?, ?, ?, ?)
|
|
141
|
+
`).run(sessionId, project.id, input.host, input.adapter, new Date().toISOString(), project.gitBranch, project.gitHead);
|
|
142
|
+
return { sessionId, project };
|
|
143
|
+
}
|
|
144
|
+
finally {
|
|
145
|
+
db.close();
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
export function recordEvent(input, dbPath = defaultDbPath()) {
|
|
149
|
+
const db = openMemoryDb(dbPath);
|
|
150
|
+
try {
|
|
151
|
+
migrate(db);
|
|
152
|
+
const project = resolveProjectContext();
|
|
153
|
+
upsertProject(db, project);
|
|
154
|
+
const eventId = randomUUID();
|
|
155
|
+
db.prepare(`
|
|
156
|
+
INSERT INTO events (id, session_id, project_id, type, summary, payload_json, created_at)
|
|
157
|
+
VALUES (?, ?, ?, ?, ?, ?, ?)
|
|
158
|
+
`).run(eventId, input.sessionId ?? null, project.id, input.type, redactSecrets(input.summary), input.payloadJson === undefined ? null : redactSecrets(input.payloadJson), new Date().toISOString());
|
|
159
|
+
return { eventId, project };
|
|
160
|
+
}
|
|
161
|
+
finally {
|
|
162
|
+
db.close();
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
export function recentEvents(limit, dbPath = defaultDbPath()) {
|
|
166
|
+
const db = openMemoryDb(dbPath);
|
|
167
|
+
try {
|
|
168
|
+
migrate(db);
|
|
169
|
+
const project = resolveProjectContext();
|
|
170
|
+
return db.prepare(`
|
|
171
|
+
SELECT id, type, summary, created_at AS createdAt, session_id AS sessionId
|
|
172
|
+
FROM events
|
|
173
|
+
WHERE project_id = ?
|
|
174
|
+
ORDER BY created_at DESC
|
|
175
|
+
LIMIT ?
|
|
176
|
+
`).all(project.id, limit);
|
|
177
|
+
}
|
|
178
|
+
finally {
|
|
179
|
+
db.close();
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
export function writeHandoff(summaryMd, sessionId, dbPath = defaultDbPath()) {
|
|
183
|
+
const db = openMemoryDb(dbPath);
|
|
184
|
+
try {
|
|
185
|
+
migrate(db);
|
|
186
|
+
const project = resolveProjectContext();
|
|
187
|
+
upsertProject(db, project);
|
|
188
|
+
const handoffId = randomUUID();
|
|
189
|
+
db.prepare(`
|
|
190
|
+
INSERT INTO handoffs (id, project_id, session_id, summary_md, created_at)
|
|
191
|
+
VALUES (?, ?, ?, ?, ?)
|
|
192
|
+
`).run(handoffId, project.id, sessionId ?? null, redactSecrets(summaryMd), new Date().toISOString());
|
|
193
|
+
return { handoffId, project };
|
|
194
|
+
}
|
|
195
|
+
finally {
|
|
196
|
+
db.close();
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
export function exportMemory(dbPath = defaultDbPath()) {
|
|
200
|
+
const db = openMemoryDb(dbPath);
|
|
201
|
+
try {
|
|
202
|
+
migrate(db);
|
|
203
|
+
const project = resolveProjectContext();
|
|
204
|
+
const sessions = db.prepare(`
|
|
205
|
+
SELECT id, host, adapter, started_at AS startedAt, ended_at AS endedAt, git_branch AS gitBranch, git_head AS gitHead FROM sessions
|
|
206
|
+
WHERE project_id = ? ORDER BY started_at ASC, id ASC
|
|
207
|
+
`).all(project.id);
|
|
208
|
+
const events = db.prepare(`
|
|
209
|
+
SELECT id, session_id AS sessionId, type, summary, payload_json AS payloadJson, created_at AS createdAt FROM events
|
|
210
|
+
WHERE project_id = ? ORDER BY created_at ASC, id ASC
|
|
211
|
+
`).all(project.id);
|
|
212
|
+
const handoffs = db.prepare(`
|
|
213
|
+
SELECT id, session_id AS sessionId, summary_md AS summaryMd, created_at AS createdAt FROM handoffs
|
|
214
|
+
WHERE project_id = ? ORDER BY created_at ASC, id ASC
|
|
215
|
+
`).all(project.id);
|
|
216
|
+
return {
|
|
217
|
+
schemaVersion: SCHEMA_VERSION,
|
|
218
|
+
exportedAt: new Date().toISOString(),
|
|
219
|
+
paths: { dbPath },
|
|
220
|
+
project,
|
|
221
|
+
sessions,
|
|
222
|
+
events,
|
|
223
|
+
handoffs,
|
|
224
|
+
};
|
|
225
|
+
}
|
|
226
|
+
finally {
|
|
227
|
+
db.close();
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
export function purgeMemory(input, dbPath = defaultDbPath()) {
|
|
231
|
+
if (!input.yes)
|
|
232
|
+
throw new PurgeConfirmationError();
|
|
233
|
+
const db = openMemoryDb(dbPath);
|
|
234
|
+
try {
|
|
235
|
+
migrate(db);
|
|
236
|
+
const project = resolveProjectContext();
|
|
237
|
+
const deleteProject = db.transaction(() => {
|
|
238
|
+
const events = db.prepare("DELETE FROM events WHERE project_id IN (SELECT id FROM projects WHERE id = ? OR repo_root = ?)").run(project.id, project.repoRoot).changes;
|
|
239
|
+
const handoffs = db.prepare("DELETE FROM handoffs WHERE project_id IN (SELECT id FROM projects WHERE id = ? OR repo_root = ?)").run(project.id, project.repoRoot).changes;
|
|
240
|
+
const sessions = db.prepare("DELETE FROM sessions WHERE project_id IN (SELECT id FROM projects WHERE id = ? OR repo_root = ?)").run(project.id, project.repoRoot).changes;
|
|
241
|
+
const projects = db.prepare("DELETE FROM projects WHERE id = ? OR repo_root = ?").run(project.id, project.repoRoot).changes;
|
|
242
|
+
return { events, handoffs, sessions, projects };
|
|
243
|
+
});
|
|
244
|
+
return { project, deleted: deleteProject() };
|
|
245
|
+
}
|
|
246
|
+
finally {
|
|
247
|
+
db.close();
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
function gitValue(args, cwd) {
|
|
251
|
+
try {
|
|
252
|
+
return execFileSync("git", args, { cwd, encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] }).trim() || null;
|
|
253
|
+
}
|
|
254
|
+
catch {
|
|
255
|
+
return null;
|
|
256
|
+
}
|
|
257
|
+
}
|
package/dist/privacy.js
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
const SECRET_PATTERNS = [
|
|
2
|
+
/\bBearer\s+[A-Za-z0-9._-]+/gi,
|
|
3
|
+
/\bgithub_pat_[A-Za-z0-9_]+/g,
|
|
4
|
+
/\bAKIA[0-9A-Z]{16}\b/g,
|
|
5
|
+
/\bxox[baprs]-[A-Za-z0-9-]+/g,
|
|
6
|
+
/\b(?:sk-|pk-)[A-Za-z0-9_-]{12,}\b/g,
|
|
7
|
+
/\b(password|passwd|secret|token|api[_-]?key|auth)(["']?\s*[:=]\s*["']?)[A-Za-z0-9_\-./+=]{8,}/gi,
|
|
8
|
+
];
|
|
9
|
+
export function redactSecrets(input) {
|
|
10
|
+
return SECRET_PATTERNS.reduce((value, pattern) => {
|
|
11
|
+
if (pattern.source.startsWith("\\b(password")) {
|
|
12
|
+
return value.replace(pattern, "$1$2[REDACTED]");
|
|
13
|
+
}
|
|
14
|
+
return value.replace(pattern, "[REDACTED]");
|
|
15
|
+
}, input);
|
|
16
|
+
}
|
|
17
|
+
export function sanitizeGitRemote(remote) {
|
|
18
|
+
if (remote === null)
|
|
19
|
+
return null;
|
|
20
|
+
return redactSecrets(remote).replace(/(https?:\/\/)([^/@\s]+)@/gi, "$1[REDACTED]@");
|
|
21
|
+
}
|
package/dist/types.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
# Adapter Integration
|
|
2
|
+
|
|
3
|
+
OMO Memory is the shared local work ledger for OMO adapters. It is host-neutral: lazycodex, omo-on-opencode, lfg, and future adapters all write summaries, decisions, QA evidence, task state, and handoffs to the same local SQLite database at `~/.omo/memory/state.sqlite` by default.
|
|
4
|
+
|
|
5
|
+
No full transcript capture by default. Do not store API keys, tokens, `.env` contents, auth files, raw tool logs, auth headers, cookies, or any other secret-bearing material.
|
|
6
|
+
|
|
7
|
+
## Contract
|
|
8
|
+
|
|
9
|
+
- Use the shared CLI or MCP surface; do not create adapter-specific tables, schemas, or side databases.
|
|
10
|
+
- Keep `host` and `adapter` as session metadata. Use `host` for `codex`, `opencode`, `grok`, or `unknown`; use `adapter` for names such as `lazycodex`, `omo-on-opencode`, or `lfg`.
|
|
11
|
+
- Record concise events with a stable `type`, a human-readable `summary`, and optional redacted JSON metadata in `payloadJson`.
|
|
12
|
+
- Store handoffs as summary markdown that another host can read without needing the originating transcript.
|
|
13
|
+
- Treat CLI and MCP as two entrypoints to the same core functions and schema.
|
|
14
|
+
- Use `OMO_MEMORY_DB` only when the caller explicitly chooses a different database path, such as an isolated smoke test.
|
|
15
|
+
|
|
16
|
+
## Adapter Metadata
|
|
17
|
+
|
|
18
|
+
| Adapter | host | adapter | Typical use |
|
|
19
|
+
| --- | --- | --- | --- |
|
|
20
|
+
| lazycodex | `codex` | `lazycodex` | Codex session memory, decisions, QA evidence, and handoffs. |
|
|
21
|
+
| omo-on-opencode | `opencode` | `omo-on-opencode` | OpenCode-hosted OMO session memory. |
|
|
22
|
+
| lfg | `grok` | `lfg` | Grok/GrokBuild-oriented harness memory. |
|
|
23
|
+
|
|
24
|
+
## CLI Examples
|
|
25
|
+
|
|
26
|
+
After npm publish, adapters and users can invoke the packaged CLI directly:
|
|
27
|
+
|
|
28
|
+
```sh
|
|
29
|
+
npx -y omo-memory init
|
|
30
|
+
npx -y omo-memory session start --host codex --adapter lazycodex
|
|
31
|
+
npx -y omo-memory session start --host grok --adapter lfg
|
|
32
|
+
npx -y omo-memory recent --limit 10
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
For a source checkout:
|
|
36
|
+
|
|
37
|
+
```sh
|
|
38
|
+
node dist/cli.js init
|
|
39
|
+
node dist/cli.js session start --host codex --adapter lazycodex
|
|
40
|
+
node dist/cli.js session start --host opencode --adapter omo-on-opencode
|
|
41
|
+
node dist/cli.js session start --host grok --adapter lfg
|
|
42
|
+
node dist/cli.js event record --type decision --summary "Use OMO Memory as the shared local ledger."
|
|
43
|
+
node dist/cli.js event record --type qa_evidence --summary "CLI smoke passed for init/session/event/recent."
|
|
44
|
+
node dist/cli.js handoff write --summary "Continue from recent decision and qa_evidence events."
|
|
45
|
+
node dist/cli.js recent --limit 10
|
|
46
|
+
node dist/cli.js export
|
|
47
|
+
node dist/cli.js purge --yes
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
For isolated adapter tests:
|
|
51
|
+
|
|
52
|
+
```sh
|
|
53
|
+
OMO_MEMORY_DB="$(mktemp -u /tmp/omo-memory.XXXXXX.sqlite)" node dist/cli.js init
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
## MCP Tools
|
|
57
|
+
|
|
58
|
+
Adapters that run through MCP should register the packaged command after npm publish:
|
|
59
|
+
|
|
60
|
+
```sh
|
|
61
|
+
npx -y omo-memory mcp
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
For a source checkout:
|
|
65
|
+
|
|
66
|
+
```sh
|
|
67
|
+
node dist/cli.js mcp
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
Codex registration:
|
|
71
|
+
|
|
72
|
+
```sh
|
|
73
|
+
codex mcp add omo-memory -- npx -y omo-memory mcp
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
Grok registration:
|
|
77
|
+
|
|
78
|
+
```sh
|
|
79
|
+
grok mcp add omo-memory -- npx -y omo-memory mcp
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
Register the same MCP server in every host that needs memory access. Do not create separate Codex/Grok schemas or databases; host identity belongs in `memory_start_session` metadata.
|
|
83
|
+
|
|
84
|
+
Use these tools:
|
|
85
|
+
|
|
86
|
+
- `memory_init`
|
|
87
|
+
- `memory_project_context`
|
|
88
|
+
- `memory_start_session`
|
|
89
|
+
- `memory_record_event`
|
|
90
|
+
- `memory_recent_events`
|
|
91
|
+
- `memory_write_handoff`
|
|
92
|
+
- `memory_export`
|
|
93
|
+
- `memory_purge`
|
|
94
|
+
|
|
95
|
+
Example session start:
|
|
96
|
+
|
|
97
|
+
```json
|
|
98
|
+
{
|
|
99
|
+
"tool": "memory_start_session",
|
|
100
|
+
"arguments": {
|
|
101
|
+
"host": "codex",
|
|
102
|
+
"adapter": "lazycodex"
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
Example QA evidence:
|
|
108
|
+
|
|
109
|
+
```json
|
|
110
|
+
{
|
|
111
|
+
"tool": "memory_record_event",
|
|
112
|
+
"arguments": {
|
|
113
|
+
"type": "qa_evidence",
|
|
114
|
+
"summary": "MCP tools/list included memory_export and memory_purge."
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
## Privacy Review
|
|
120
|
+
|
|
121
|
+
- No full transcript capture by default.
|
|
122
|
+
- Do not read or record `.env`, auth files, tokens, cookies, API keys, bearer headers, or raw secret-bearing logs.
|
|
123
|
+
- Store sanitized summaries and evidence references instead of full logs.
|
|
124
|
+
- Host-specific values may appear only in small redacted metadata payloads and must not require schema branches.
|
|
125
|
+
- Export and purge are explicit lifecycle commands; purge requires explicit confirmation.
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
# Epic: OMO Memory — shared local session DB for OMO adapters
|
|
2
|
+
|
|
3
|
+
## Goal
|
|
4
|
+
|
|
5
|
+
Build a host-neutral local memory layer for OMO so lazycodex, omo-on-opencode, lfg, and future adapters can share task context across coding-tool sessions.
|
|
6
|
+
|
|
7
|
+
The product ships as both:
|
|
8
|
+
|
|
9
|
+
- CLI: install, inspect, search, export, purge, and handoff operations.
|
|
10
|
+
- MCP server: standard stdio tools for agents and coding tools to read/write OMO memory.
|
|
11
|
+
|
|
12
|
+
## Problem
|
|
13
|
+
|
|
14
|
+
OMO currently runs through multiple host adapters. Each host session can know what happened in its own conversation, but session decisions, QA results, failure reasons, next actions, and agent verdicts are not available to the next host/tool session in a reliable local-first format.
|
|
15
|
+
|
|
16
|
+
Codegraph solves code intelligence, not work/session memory. OMO needs a separate shared memory ledger.
|
|
17
|
+
|
|
18
|
+
## Product direction
|
|
19
|
+
|
|
20
|
+
- Global local DB: `~/.omo/memory/state.sqlite`
|
|
21
|
+
- Project namespace: derive from git remote + repo root hash, with branch/head metadata
|
|
22
|
+
- Local-first privacy: no cloud sync and no secret storage by default
|
|
23
|
+
- Adapter-neutral schema: `host` and `adapter` are metadata, not separate products
|
|
24
|
+
- Shared API: CLI and MCP use the same core DB layer
|
|
25
|
+
|
|
26
|
+
## In scope
|
|
27
|
+
|
|
28
|
+
- SQLite schema and migrations
|
|
29
|
+
- CLI commands:
|
|
30
|
+
- `init`
|
|
31
|
+
- `session start`
|
|
32
|
+
- `event record`
|
|
33
|
+
- `recent`
|
|
34
|
+
- `handoff write`
|
|
35
|
+
- `doctor`
|
|
36
|
+
- MCP stdio server with tools:
|
|
37
|
+
- `memory_init`
|
|
38
|
+
- `memory_project_context`
|
|
39
|
+
- `memory_record_event`
|
|
40
|
+
- `memory_recent_events`
|
|
41
|
+
- `memory_write_handoff`
|
|
42
|
+
- Privacy guardrails:
|
|
43
|
+
- local-only default
|
|
44
|
+
- no API key/token/env capture
|
|
45
|
+
- explicit purge/export commands
|
|
46
|
+
- Adapter integration notes for lazycodex, omo-on-opencode, and lfg
|
|
47
|
+
|
|
48
|
+
## Out of scope
|
|
49
|
+
|
|
50
|
+
- Cloud sync
|
|
51
|
+
- Team sharing by default
|
|
52
|
+
- Full transcript capture by default
|
|
53
|
+
- Vector/embedding search in MVP
|
|
54
|
+
- Replacing codegraph
|
|
55
|
+
- Host-specific private APIs as required dependencies
|
|
56
|
+
|
|
57
|
+
## Acceptance criteria
|
|
58
|
+
|
|
59
|
+
### CLI
|
|
60
|
+
|
|
61
|
+
- `omo-memory init` creates or migrates `~/.omo/memory/state.sqlite`.
|
|
62
|
+
- `omo-memory session start --host grok --adapter lfg` records a session for the current project.
|
|
63
|
+
- `omo-memory event record --type decision --summary "..."` appends a project/session event.
|
|
64
|
+
- `omo-memory recent` lists recent project events.
|
|
65
|
+
- `omo-memory handoff write --summary-file path.md` stores a handoff summary.
|
|
66
|
+
|
|
67
|
+
### MCP
|
|
68
|
+
|
|
69
|
+
- `omo-memory mcp` starts a stdio MCP server.
|
|
70
|
+
- MCP tools can initialize the DB, read project context, record events, list recent events, and write handoffs.
|
|
71
|
+
- MCP tool responses are structured JSON text.
|
|
72
|
+
|
|
73
|
+
### Privacy and safety
|
|
74
|
+
|
|
75
|
+
- No secrets are read from `.env`, auth files, or host config by default.
|
|
76
|
+
- DB path can be overridden only by explicit `OMO_MEMORY_DB`.
|
|
77
|
+
- `doctor` reports DB path, schema version, and project identity without leaking sensitive values.
|
|
78
|
+
|
|
79
|
+
### QA evidence
|
|
80
|
+
|
|
81
|
+
- `npm run typecheck`
|
|
82
|
+
- `npm run build`
|
|
83
|
+
- CLI smoke:
|
|
84
|
+
- `node dist/cli.js init`
|
|
85
|
+
- `node dist/cli.js session start --host grok --adapter lfg`
|
|
86
|
+
- `node dist/cli.js event record --type decision --summary "smoke"`
|
|
87
|
+
- `node dist/cli.js recent`
|
|
88
|
+
- MCP smoke: start `node dist/cli.js mcp` and verify `tools/list` includes memory tools.
|
|
89
|
+
|
|
90
|
+
## Follow-up issue breakdown
|
|
91
|
+
|
|
92
|
+
1. Schema and migration core
|
|
93
|
+
2. CLI command surface
|
|
94
|
+
3. MCP stdio server
|
|
95
|
+
4. Project identity and git metadata
|
|
96
|
+
5. Privacy/redaction/purge/export
|
|
97
|
+
6. Adapter integration docs for lazycodex, omo-on-opencode, and lfg
|
|
98
|
+
7. QA harness for CLI and MCP smoke tests
|
package/package.json
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "omo-memory",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Host-neutral local SQLite memory and session ledger for OMO adapters, exposed through CLI and MCP.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"files": [
|
|
7
|
+
"dist",
|
|
8
|
+
"docs",
|
|
9
|
+
"README.md"
|
|
10
|
+
],
|
|
11
|
+
"bin": {
|
|
12
|
+
"omo-memory": "dist/cli.js"
|
|
13
|
+
},
|
|
14
|
+
"repository": {
|
|
15
|
+
"type": "git",
|
|
16
|
+
"url": "git+https://github.com/islee23520/omo-memory.git"
|
|
17
|
+
},
|
|
18
|
+
"bugs": {
|
|
19
|
+
"url": "https://github.com/islee23520/omo-memory/issues"
|
|
20
|
+
},
|
|
21
|
+
"homepage": "https://github.com/islee23520/omo-memory#readme",
|
|
22
|
+
"publishConfig": {
|
|
23
|
+
"access": "public"
|
|
24
|
+
},
|
|
25
|
+
"scripts": {
|
|
26
|
+
"build": "tsc -p tsconfig.json",
|
|
27
|
+
"typecheck": "tsc --noEmit",
|
|
28
|
+
"start": "node dist/cli.js",
|
|
29
|
+
"prepack": "npm run build",
|
|
30
|
+
"smoke": "npm run smoke:cli && npm run smoke:mcp",
|
|
31
|
+
"smoke:cli": "node scripts/smoke-cli.mjs",
|
|
32
|
+
"smoke:mcp": "node scripts/smoke-mcp.mjs",
|
|
33
|
+
"mcp": "node dist/cli.js mcp",
|
|
34
|
+
"issue:epic": "node scripts/create-epic-issue.mjs"
|
|
35
|
+
},
|
|
36
|
+
"dependencies": {
|
|
37
|
+
"@modelcontextprotocol/sdk": "^1.29.0",
|
|
38
|
+
"better-sqlite3": "^12.11.1",
|
|
39
|
+
"zod": "^4.4.3"
|
|
40
|
+
},
|
|
41
|
+
"devDependencies": {
|
|
42
|
+
"@types/better-sqlite3": "^7.6.13",
|
|
43
|
+
"@types/node": "^24.10.1",
|
|
44
|
+
"typescript": "^6.0.3"
|
|
45
|
+
},
|
|
46
|
+
"engines": {
|
|
47
|
+
"node": ">=20"
|
|
48
|
+
}
|
|
49
|
+
}
|