om-memory-system 3.5.0 → 3.6.0-next.38
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 +24 -16
- package/dist/adapters/claude-code/hook-client.d.ts +73 -0
- package/dist/adapters/claude-code/hook-client.js +285 -0
- package/dist/adapters/claude-code/hook-command.d.ts +6 -0
- package/dist/adapters/claude-code/hook-command.js +25 -0
- package/dist/cli/index.js +19 -3
- package/dist/cli/memory-command.d.ts +24 -0
- package/dist/cli/memory-command.js +180 -0
- package/dist/core/retrieval.d.ts +21 -0
- package/dist/core/retrieval.js +35 -0
- package/dist/importer/backfill-controls.d.ts +10 -0
- package/dist/importer/backfill-controls.js +40 -11
- package/dist/importer/backfill-model.d.ts +1 -1
- package/dist/importer/backfill-model.js +3 -0
- package/dist/importer/claude-conversation.d.ts +74 -0
- package/dist/importer/claude-conversation.js +216 -0
- package/dist/importer/claude-hook-api.d.ts +50 -0
- package/dist/importer/claude-hook-api.js +441 -0
- package/dist/importer/claude-import.d.ts +15 -0
- package/dist/importer/claude-import.js +62 -0
- package/dist/importer/claude-reader.d.ts +99 -0
- package/dist/importer/claude-reader.js +292 -0
- package/dist/importer/external-backfill-models.js +2 -2
- package/dist/importer/import-args.d.ts +1 -1
- package/dist/importer/import-args.js +11 -4
- package/dist/importer/import-readiness.d.ts +13 -1
- package/dist/importer/import-readiness.js +19 -0
- package/dist/importer/import-runs.js +1 -1
- package/dist/importer/import-sessions.d.ts +4 -1
- package/dist/importer/import-sessions.js +14 -2
- package/dist/importer/import-sources.d.ts +8 -2
- package/dist/importer/import-sources.js +21 -4
- package/dist/importer/map-suggestions.d.ts +1 -0
- package/dist/importer/map-suggestions.js +6 -1
- package/dist/importer/run-import.js +14 -10
- package/dist/importer/web-import-jobs.js +10 -3
- package/dist/index.js +9 -23
- package/dist/services/ai/live-model-choice.d.ts +5 -0
- package/dist/services/ai/live-model-choice.js +7 -0
- package/dist/services/backfill-state.d.ts +1 -1
- package/dist/services/capture-retry-queue.js +3 -2
- package/dist/services/claude-capture-cursor.d.ts +10 -0
- package/dist/services/claude-capture-cursor.js +44 -0
- package/dist/services/settings-snapshot.d.ts +1 -0
- package/dist/services/settings-snapshot.js +3 -1
- package/dist/services/web-server.d.ts +2 -0
- package/dist/services/web-server.js +61 -28
- package/dist/types/host-label.d.ts +3 -0
- package/dist/types/host-label.js +9 -0
- package/dist/types/index.d.ts +1 -1
- package/dist/web/assets/index-BFiVixOK.js +124 -0
- package/dist/web/index.html +1 -1
- package/package.json +1 -1
- package/dist/web/assets/index-DIy--9g_.js +0 -124
package/README.md
CHANGED
|
@@ -17,6 +17,10 @@ It runs inside [OpenCode](https://opencode.ai) and the
|
|
|
17
17
|
Both share one memory per project, so a note written in one is available in
|
|
18
18
|
the other. Everything is stored locally on your machine.
|
|
19
19
|
|
|
20
|
+
Claude Code is also supported, through a plugin of hooks. It needs a global
|
|
21
|
+
install and your own external API for capture. See the
|
|
22
|
+
[Claude Code adapter](docs/claude-code-adapter.md) guide.
|
|
23
|
+
|
|
20
24
|
## What it does
|
|
21
25
|
|
|
22
26
|
- **Remembers automatically.** After each piece of work, a background model
|
|
@@ -43,6 +47,8 @@ You need one of:
|
|
|
43
47
|
|
|
44
48
|
- **OpenCode** 1.18.29 or later (v1 plugin API) or OpenCode v2
|
|
45
49
|
- **Pi coding agent**
|
|
50
|
+
- **Claude Code**, through a plugin of hooks. Capture needs your own external API. See
|
|
51
|
+
the [Claude Code adapter](docs/claude-code-adapter.md) guide for setup.
|
|
46
52
|
|
|
47
53
|
Nothing else is required. OMMS brings its own database. On first use it
|
|
48
54
|
downloads a small embedding model (the part that makes memories searchable),
|
|
@@ -142,22 +148,24 @@ backup first. See [Updating and upgrading](docs/upgrading.md) and
|
|
|
142
148
|
|
|
143
149
|
## Documentation
|
|
144
150
|
|
|
145
|
-
| Read this
|
|
146
|
-
|
|
|
147
|
-
| [Using memory day to day](docs/using-memory.md)
|
|
148
|
-
| [Configuration](docs/configuration.md)
|
|
149
|
-
| [Web UI](docs/web-ui.md)
|
|
150
|
-
| [Settings page](docs/web-ui-settings.md)
|
|
151
|
-
| [Moving projects](docs/moving-projects.md)
|
|
152
|
-
| [Updating and upgrading](docs/upgrading.md)
|
|
153
|
-
| [OpenCode adapter](docs/opencode-adapter.md)
|
|
154
|
-
| [Pi adapter](docs/pi-adapter.md)
|
|
155
|
-
| [
|
|
156
|
-
| [
|
|
157
|
-
| [
|
|
158
|
-
| [
|
|
159
|
-
| [
|
|
160
|
-
| [
|
|
151
|
+
| Read this | To learn about |
|
|
152
|
+
| ---------------------------------------------------------------- | ---------------------------------------------------------------- |
|
|
153
|
+
| [Using memory day to day](docs/using-memory.md) | How capture and recall work, the `memory` tool, the user profile |
|
|
154
|
+
| [Configuration](docs/configuration.md) | Settings, choosing the model, embeddings, troubleshooting |
|
|
155
|
+
| [Web UI](docs/web-ui.md) | The memory explorer, opening it on a network safely |
|
|
156
|
+
| [Settings page](docs/web-ui-settings.md) | Every card and control on the web Settings page |
|
|
157
|
+
| [Moving projects](docs/moving-projects.md) | Nested repositories, moved folders, backup and restore |
|
|
158
|
+
| [Updating and upgrading](docs/upgrading.md) | Updates, pinning a version, older stores |
|
|
159
|
+
| [OpenCode adapter](docs/opencode-adapter.md) | How the OpenCode plugin hooks in |
|
|
160
|
+
| [Pi adapter](docs/pi-adapter.md) | How the Pi extension hooks in |
|
|
161
|
+
| [Claude Code adapter](docs/claude-code-adapter.md) | Installing the Claude Code plugin, hooks, the `memory` command |
|
|
162
|
+
| [OpenCode history import](docs/opencode-history-import.md) | Importing past OpenCode sessions |
|
|
163
|
+
| [Pi history import](docs/pi-history-import.md) | Importing past Pi sessions, moving machines |
|
|
164
|
+
| [Claude Code history import](docs/claude-code-history-import.md) | Importing past Claude Code transcripts |
|
|
165
|
+
| [CLI reference](docs/cli.md) | The `om-memory-system` terminal command |
|
|
166
|
+
| [Migrating from opencode-mem](docs/omms-migration.md) | What changes when upgrading from the original plugin |
|
|
167
|
+
| [For developers](docs/developers.md) | Building, testing, the public `tags` export, architecture |
|
|
168
|
+
| [Contributing](CONTRIBUTING.md) | How to set up, test, and send a pull request |
|
|
161
169
|
|
|
162
170
|
## About this fork
|
|
163
171
|
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
export declare const CLAUDE_HOOK_EVENTS: readonly ["session-start", "user-prompt-submit", "stop"];
|
|
2
|
+
export type ClaudeHookEvent = (typeof CLAUDE_HOOK_EVENTS)[number];
|
|
3
|
+
/** Claude Code caps added context at 10,000 characters; keep a margin. */
|
|
4
|
+
export declare const CLAUDE_CONTEXT_LIMIT = 9500;
|
|
5
|
+
/**
|
|
6
|
+
* Copy of `RETRIEVAL_SECTION_TAG` in `src/core/retrieval.ts`. That module loads
|
|
7
|
+
* the store, so the hook keeps its own copy; a test checks the two match.
|
|
8
|
+
*/
|
|
9
|
+
export declare const RETRIEVAL_TAG = "omms-retrieval";
|
|
10
|
+
export interface HookServerSettings {
|
|
11
|
+
enabled: boolean;
|
|
12
|
+
baseUrl: string;
|
|
13
|
+
basicAuth?: {
|
|
14
|
+
username: string;
|
|
15
|
+
password: string;
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
interface SpawnedChild {
|
|
19
|
+
unref(): void;
|
|
20
|
+
on(event: "error", listener: (error: Error) => void): unknown;
|
|
21
|
+
}
|
|
22
|
+
type SpawnFn = (command: string, args: string[], options: {
|
|
23
|
+
detached: true;
|
|
24
|
+
stdio: "ignore";
|
|
25
|
+
cwd: string;
|
|
26
|
+
windowsHide: true;
|
|
27
|
+
}) => SpawnedChild;
|
|
28
|
+
/** A readable byte stream such as `process.stdin`. */
|
|
29
|
+
export interface HookInputStream {
|
|
30
|
+
on(event: "data", listener: (chunk: Buffer | string) => void): unknown;
|
|
31
|
+
on(event: "end" | "error", listener: (arg?: unknown) => void): unknown;
|
|
32
|
+
removeAllListeners(): unknown;
|
|
33
|
+
pause(): unknown;
|
|
34
|
+
destroy?(): unknown;
|
|
35
|
+
}
|
|
36
|
+
export interface ClaudeHookOptions {
|
|
37
|
+
stdin?: HookInputStream;
|
|
38
|
+
writeStdout?: (text: string) => void;
|
|
39
|
+
fetch?: typeof fetch;
|
|
40
|
+
spawn?: SpawnFn;
|
|
41
|
+
now?: () => number;
|
|
42
|
+
sleep?: (ms: number) => Promise<void>;
|
|
43
|
+
log?: (message: string, data: Record<string, unknown>) => void | Promise<void>;
|
|
44
|
+
readToken?: () => Promise<string>;
|
|
45
|
+
resolveRuntime?: () => Promise<string | null>;
|
|
46
|
+
loadSettings?: () => Promise<HookServerSettings>;
|
|
47
|
+
/** The `om-memory-system` script that `web` is run from. */
|
|
48
|
+
cliScript?: string;
|
|
49
|
+
inputTimeoutMs?: number;
|
|
50
|
+
maxInputBytes?: number;
|
|
51
|
+
/** Overrides the event's request budget. */
|
|
52
|
+
requestTimeoutMs?: number;
|
|
53
|
+
}
|
|
54
|
+
export type ClaudeHookCode = "ok" | "bad-event" | "no-input" | "bad-input" | "server-disabled" | "server-unreachable" | "start-timeout" | "timeout" | "network-error" | "bad-response" | "error" | `http-${number}`;
|
|
55
|
+
export interface ClaudeHookResult {
|
|
56
|
+
code: ClaudeHookCode;
|
|
57
|
+
spawned: boolean;
|
|
58
|
+
inputBytes: number;
|
|
59
|
+
outputBytes: number;
|
|
60
|
+
}
|
|
61
|
+
/** Read stdin up to a time and size limit. Late, oversized, or broken input is no input. */
|
|
62
|
+
export declare function readHookInput(stream: HookInputStream, limits: {
|
|
63
|
+
timeoutMs: number;
|
|
64
|
+
maxBytes: number;
|
|
65
|
+
}): Promise<{
|
|
66
|
+
text: string | null;
|
|
67
|
+
bytes: number;
|
|
68
|
+
}>;
|
|
69
|
+
/** Cut text to the limit. When it ends with the retrieval tag, keep that closing tag. */
|
|
70
|
+
export declare function truncateContext(text: string, limit?: number): string;
|
|
71
|
+
/** Run one hook event. Never throws; the result says what happened. */
|
|
72
|
+
export declare function runClaudeHook(event: ClaudeHookEvent, options?: ClaudeHookOptions): Promise<ClaudeHookResult>;
|
|
73
|
+
export {};
|
|
@@ -0,0 +1,285 @@
|
|
|
1
|
+
import { spawn as nodeSpawn } from "node:child_process";
|
|
2
|
+
import { homedir } from "node:os";
|
|
3
|
+
import { fileURLToPath } from "node:url";
|
|
4
|
+
import { AUTH_HEADER } from "../../services/auth-token.js";
|
|
5
|
+
// Claude Code hook client: read the hook input, find or start the OMMS web
|
|
6
|
+
// app, send one request, print the added context, and log one metadata line.
|
|
7
|
+
// It loads no store, embedding model, or engine; the web app does that work.
|
|
8
|
+
export const CLAUDE_HOOK_EVENTS = ["session-start", "user-prompt-submit", "stop"];
|
|
9
|
+
/** Claude Code caps added context at 10,000 characters; keep a margin. */
|
|
10
|
+
export const CLAUDE_CONTEXT_LIMIT = 9_500;
|
|
11
|
+
/**
|
|
12
|
+
* Copy of `RETRIEVAL_SECTION_TAG` in `src/core/retrieval.ts`. That module loads
|
|
13
|
+
* the store, so the hook keeps its own copy; a test checks the two match.
|
|
14
|
+
*/
|
|
15
|
+
export const RETRIEVAL_TAG = "omms-retrieval";
|
|
16
|
+
const INPUT_TIMEOUT_MS = 2_000;
|
|
17
|
+
const MAX_INPUT_BYTES = 1024 * 1024;
|
|
18
|
+
const HEALTH_PROBE_MS = 1_000;
|
|
19
|
+
const POLL_INTERVAL_MS = 250;
|
|
20
|
+
const BUDGETS = {
|
|
21
|
+
"session-start": { startMs: 15_000, requestMs: 3_000 },
|
|
22
|
+
"user-prompt-submit": { startMs: 5_000, requestMs: 3_000 },
|
|
23
|
+
stop: { startMs: 30_000, requestMs: 5_000 },
|
|
24
|
+
};
|
|
25
|
+
const HOOK_EVENT_NAMES = {
|
|
26
|
+
"session-start": "SessionStart",
|
|
27
|
+
"user-prompt-submit": "UserPromptSubmit",
|
|
28
|
+
stop: "Stop",
|
|
29
|
+
};
|
|
30
|
+
/** Read stdin up to a time and size limit. Late, oversized, or broken input is no input. */
|
|
31
|
+
export function readHookInput(stream, limits) {
|
|
32
|
+
return new Promise((resolve) => {
|
|
33
|
+
const chunks = [];
|
|
34
|
+
let bytes = 0;
|
|
35
|
+
let settled = false;
|
|
36
|
+
const finish = (complete) => {
|
|
37
|
+
if (settled)
|
|
38
|
+
return;
|
|
39
|
+
settled = true;
|
|
40
|
+
clearTimeout(timer);
|
|
41
|
+
stream.removeAllListeners();
|
|
42
|
+
stream.pause();
|
|
43
|
+
// Let the process exit even though the writer has not closed stdin.
|
|
44
|
+
if (!complete)
|
|
45
|
+
stream.destroy?.();
|
|
46
|
+
const text = complete && bytes <= limits.maxBytes ? Buffer.concat(chunks).toString() : null;
|
|
47
|
+
resolve({ text, bytes });
|
|
48
|
+
};
|
|
49
|
+
const timer = setTimeout(() => finish(false), limits.timeoutMs);
|
|
50
|
+
stream.on("data", (chunk) => {
|
|
51
|
+
const buffer = typeof chunk === "string" ? Buffer.from(chunk) : chunk;
|
|
52
|
+
bytes += buffer.length;
|
|
53
|
+
if (bytes > limits.maxBytes)
|
|
54
|
+
return finish(false);
|
|
55
|
+
chunks.push(buffer);
|
|
56
|
+
});
|
|
57
|
+
stream.on("end", () => finish(true));
|
|
58
|
+
stream.on("error", () => finish(false));
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
/** Cut text to the limit. When it ends with the retrieval tag, keep that closing tag. */
|
|
62
|
+
export function truncateContext(text, limit = CLAUDE_CONTEXT_LIMIT) {
|
|
63
|
+
if (text.length <= limit)
|
|
64
|
+
return text;
|
|
65
|
+
const close = `</${RETRIEVAL_TAG}>`;
|
|
66
|
+
const suffix = text.trimEnd().endsWith(close) ? `\n${close}` : "";
|
|
67
|
+
let head = text.slice(0, limit - suffix.length);
|
|
68
|
+
// Do not leave half of a surrogate pair at the cut.
|
|
69
|
+
if (/[\uD800-\uDBFF]$/.test(head))
|
|
70
|
+
head = head.slice(0, -1);
|
|
71
|
+
return head + suffix;
|
|
72
|
+
}
|
|
73
|
+
function parseInput(text, event) {
|
|
74
|
+
let value;
|
|
75
|
+
try {
|
|
76
|
+
value = JSON.parse(text);
|
|
77
|
+
}
|
|
78
|
+
catch {
|
|
79
|
+
return null;
|
|
80
|
+
}
|
|
81
|
+
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
82
|
+
return null;
|
|
83
|
+
const input = value;
|
|
84
|
+
if (typeof input.session_id !== "string" || typeof input.cwd !== "string")
|
|
85
|
+
return null;
|
|
86
|
+
if (event === "stop" && typeof input.transcript_path !== "string")
|
|
87
|
+
return null;
|
|
88
|
+
return input;
|
|
89
|
+
}
|
|
90
|
+
function requestFor(event, input) {
|
|
91
|
+
const optional = (key) => (input[key] === undefined ? {} : { [key]: input[key] });
|
|
92
|
+
if (event === "stop")
|
|
93
|
+
return {
|
|
94
|
+
path: "/api/claude/capture",
|
|
95
|
+
body: {
|
|
96
|
+
session_id: input.session_id,
|
|
97
|
+
transcript_path: input.transcript_path,
|
|
98
|
+
cwd: input.cwd,
|
|
99
|
+
...optional("last_assistant_message"),
|
|
100
|
+
...optional("stop_hook_active"),
|
|
101
|
+
},
|
|
102
|
+
};
|
|
103
|
+
return {
|
|
104
|
+
path: "/api/claude/retrieve",
|
|
105
|
+
body: {
|
|
106
|
+
event,
|
|
107
|
+
session_id: input.session_id,
|
|
108
|
+
cwd: input.cwd,
|
|
109
|
+
...optional("transcript_path"),
|
|
110
|
+
...optional(event === "session-start" ? "source" : "prompt"),
|
|
111
|
+
},
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
function productionDefaults() {
|
|
115
|
+
return {
|
|
116
|
+
stdin: process.stdin,
|
|
117
|
+
writeStdout: (text) => process.stdout.write(text),
|
|
118
|
+
fetch: globalThis.fetch,
|
|
119
|
+
spawn: nodeSpawn,
|
|
120
|
+
now: () => Date.now(),
|
|
121
|
+
sleep: (ms) => new Promise((resolve) => setTimeout(resolve, ms)),
|
|
122
|
+
// The shared logger only uses the file system; load it lazily anyway.
|
|
123
|
+
log: async (message, data) => (await import("../../services/logger.js")).log(message, data),
|
|
124
|
+
readToken: async () => (await import("../../services/auth-token.js")).getOrCreateAuthToken(),
|
|
125
|
+
resolveRuntime: async () => (await import("../../services/web-autostart.js")).resolveWebRuntime(),
|
|
126
|
+
loadSettings: loadWebSettings,
|
|
127
|
+
// From dist/adapters/claude-code/ to the CLI entry point.
|
|
128
|
+
cliScript: fileURLToPath(new URL("../../cli/index.js", import.meta.url)),
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
/** Port, bind address, and auth as `om-memory-system web` reads them (global config from home). */
|
|
132
|
+
async function loadWebSettings() {
|
|
133
|
+
const config = await import("../../config.js");
|
|
134
|
+
config.initConfig(homedir());
|
|
135
|
+
const { CONFIG } = config;
|
|
136
|
+
const { webServerUrl } = await import("../../services/web-api-auth.js");
|
|
137
|
+
const settings = {
|
|
138
|
+
enabled: CONFIG.webServerEnabled,
|
|
139
|
+
baseUrl: webServerUrl(CONFIG.webServerHost, CONFIG.webServerPort),
|
|
140
|
+
};
|
|
141
|
+
const password = (CONFIG.webServerAuthPassword ?? "").trim();
|
|
142
|
+
if (password) {
|
|
143
|
+
const { WebAuth } = await import("../../services/web-auth.js");
|
|
144
|
+
const { username } = new WebAuth({
|
|
145
|
+
password,
|
|
146
|
+
username: CONFIG.webServerAuthUsername,
|
|
147
|
+
}).getConfig();
|
|
148
|
+
settings.basicAuth = { username, password };
|
|
149
|
+
}
|
|
150
|
+
return settings;
|
|
151
|
+
}
|
|
152
|
+
/** Run one hook event. Never throws; the result says what happened. */
|
|
153
|
+
export async function runClaudeHook(event, options = {}) {
|
|
154
|
+
const deps = { ...productionDefaults(), ...options };
|
|
155
|
+
const started = deps.now();
|
|
156
|
+
const result = { code: "ok", spawned: false, inputBytes: 0, outputBytes: 0 };
|
|
157
|
+
try {
|
|
158
|
+
result.code = await runSteps(event, deps, result);
|
|
159
|
+
}
|
|
160
|
+
catch {
|
|
161
|
+
result.code = "error";
|
|
162
|
+
}
|
|
163
|
+
try {
|
|
164
|
+
await deps.log("Claude Code hook", {
|
|
165
|
+
event,
|
|
166
|
+
code: result.code,
|
|
167
|
+
elapsedMs: deps.now() - started,
|
|
168
|
+
spawned: result.spawned,
|
|
169
|
+
inputBytes: result.inputBytes,
|
|
170
|
+
outputBytes: result.outputBytes,
|
|
171
|
+
});
|
|
172
|
+
}
|
|
173
|
+
catch {
|
|
174
|
+
/* A failed log line must not break the hook. */
|
|
175
|
+
}
|
|
176
|
+
return result;
|
|
177
|
+
}
|
|
178
|
+
async function runSteps(event, deps, result) {
|
|
179
|
+
const raw = await readHookInput(deps.stdin, {
|
|
180
|
+
timeoutMs: deps.inputTimeoutMs ?? INPUT_TIMEOUT_MS,
|
|
181
|
+
maxBytes: deps.maxInputBytes ?? MAX_INPUT_BYTES,
|
|
182
|
+
});
|
|
183
|
+
result.inputBytes = raw.bytes;
|
|
184
|
+
if (!raw.text?.trim())
|
|
185
|
+
return "no-input";
|
|
186
|
+
const input = parseInput(raw.text, event);
|
|
187
|
+
if (!input)
|
|
188
|
+
return "bad-input";
|
|
189
|
+
const settings = await deps.loadSettings();
|
|
190
|
+
if (!settings.enabled)
|
|
191
|
+
return "server-disabled";
|
|
192
|
+
const budget = BUDGETS[event];
|
|
193
|
+
const ready = await ensureServer(settings.baseUrl, budget.startMs, deps, result);
|
|
194
|
+
if (ready !== "ok")
|
|
195
|
+
return ready;
|
|
196
|
+
const headers = {
|
|
197
|
+
"content-type": "application/json",
|
|
198
|
+
[AUTH_HEADER]: await deps.readToken(),
|
|
199
|
+
};
|
|
200
|
+
if (settings.basicAuth) {
|
|
201
|
+
const { username, password } = settings.basicAuth;
|
|
202
|
+
headers.authorization = `Basic ${Buffer.from(`${username}:${password}`).toString("base64")}`;
|
|
203
|
+
}
|
|
204
|
+
const { path, body } = requestFor(event, input);
|
|
205
|
+
const controller = new AbortController();
|
|
206
|
+
const timer = setTimeout(() => controller.abort(), deps.requestTimeoutMs ?? budget.requestMs);
|
|
207
|
+
let response;
|
|
208
|
+
try {
|
|
209
|
+
response = await deps.fetch(`${settings.baseUrl}${path}`, {
|
|
210
|
+
method: "POST",
|
|
211
|
+
headers,
|
|
212
|
+
body: JSON.stringify(body),
|
|
213
|
+
signal: controller.signal,
|
|
214
|
+
});
|
|
215
|
+
}
|
|
216
|
+
catch {
|
|
217
|
+
return controller.signal.aborted ? "timeout" : "network-error";
|
|
218
|
+
}
|
|
219
|
+
finally {
|
|
220
|
+
clearTimeout(timer);
|
|
221
|
+
}
|
|
222
|
+
if (!response.ok)
|
|
223
|
+
return `http-${response.status}`;
|
|
224
|
+
if (event === "stop")
|
|
225
|
+
return "ok";
|
|
226
|
+
const reply = (await response.json().catch(() => null));
|
|
227
|
+
if (typeof reply?.additionalContext !== "string")
|
|
228
|
+
return "bad-response";
|
|
229
|
+
if (!reply.additionalContext)
|
|
230
|
+
return "ok";
|
|
231
|
+
const output = `${JSON.stringify({
|
|
232
|
+
hookSpecificOutput: {
|
|
233
|
+
hookEventName: HOOK_EVENT_NAMES[event],
|
|
234
|
+
additionalContext: truncateContext(reply.additionalContext),
|
|
235
|
+
},
|
|
236
|
+
})}\n`;
|
|
237
|
+
deps.writeStdout(output);
|
|
238
|
+
result.outputBytes = Buffer.byteLength(output);
|
|
239
|
+
return "ok";
|
|
240
|
+
}
|
|
241
|
+
async function isHealthy(baseUrl, timeoutMs, deps) {
|
|
242
|
+
try {
|
|
243
|
+
const response = await deps.fetch(`${baseUrl}/api/health`, {
|
|
244
|
+
method: "GET",
|
|
245
|
+
signal: AbortSignal.timeout(Math.max(1, timeoutMs)),
|
|
246
|
+
});
|
|
247
|
+
if (!response.ok)
|
|
248
|
+
return false;
|
|
249
|
+
// Same envelope check as WebServer.checkServerAvailable: another local
|
|
250
|
+
// service on the port must not count as OMMS.
|
|
251
|
+
const body = (await response.json());
|
|
252
|
+
return body.success === true && body.status === "ok";
|
|
253
|
+
}
|
|
254
|
+
catch {
|
|
255
|
+
return false;
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
/** Use a running web app, or start `om-memory-system web` and wait for it within the budget. */
|
|
259
|
+
async function ensureServer(baseUrl, budgetMs, deps, result) {
|
|
260
|
+
const started = deps.now();
|
|
261
|
+
const remaining = () => budgetMs - (deps.now() - started);
|
|
262
|
+
if (await isHealthy(baseUrl, Math.min(HEALTH_PROBE_MS, remaining()), deps))
|
|
263
|
+
return "ok";
|
|
264
|
+
const runtime = await deps.resolveRuntime();
|
|
265
|
+
if (!runtime)
|
|
266
|
+
return "server-unreachable";
|
|
267
|
+
const child = deps.spawn(runtime, [deps.cliScript, "web"], {
|
|
268
|
+
detached: true,
|
|
269
|
+
stdio: "ignore",
|
|
270
|
+
cwd: homedir(),
|
|
271
|
+
windowsHide: true,
|
|
272
|
+
});
|
|
273
|
+
// A missing runtime reports through this event; polling then runs out.
|
|
274
|
+
child.on("error", () => undefined);
|
|
275
|
+
child.unref();
|
|
276
|
+
result.spawned = true;
|
|
277
|
+
while (remaining() > 0) {
|
|
278
|
+
await deps.sleep(Math.min(POLL_INTERVAL_MS, remaining()));
|
|
279
|
+
if (remaining() <= 0)
|
|
280
|
+
break;
|
|
281
|
+
if (await isHealthy(baseUrl, Math.min(HEALTH_PROBE_MS, remaining()), deps))
|
|
282
|
+
return "ok";
|
|
283
|
+
}
|
|
284
|
+
return "start-timeout";
|
|
285
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { type ClaudeHookOptions } from "./hook-client.js";
|
|
2
|
+
/**
|
|
3
|
+
* `om-memory-system claude-hook <event>`. Always returns 0: a hook must never
|
|
4
|
+
* block or break a Claude Code session, whatever goes wrong.
|
|
5
|
+
*/
|
|
6
|
+
export declare function runClaudeHookCommand(argv: string[], io?: ClaudeHookOptions): Promise<number>;
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { CLAUDE_HOOK_EVENTS, runClaudeHook, } from "./hook-client.js";
|
|
2
|
+
function isHookEvent(value) {
|
|
3
|
+
return CLAUDE_HOOK_EVENTS.includes(value ?? "");
|
|
4
|
+
}
|
|
5
|
+
/**
|
|
6
|
+
* `om-memory-system claude-hook <event>`. Always returns 0: a hook must never
|
|
7
|
+
* block or break a Claude Code session, whatever goes wrong.
|
|
8
|
+
*/
|
|
9
|
+
export async function runClaudeHookCommand(argv, io = {}) {
|
|
10
|
+
try {
|
|
11
|
+
const [event] = argv;
|
|
12
|
+
if (isHookEvent(event)) {
|
|
13
|
+
await runClaudeHook(event, io);
|
|
14
|
+
return 0;
|
|
15
|
+
}
|
|
16
|
+
// Only a known name reaches the log; argv is not trusted text.
|
|
17
|
+
const log = io.log ??
|
|
18
|
+
(async (message, data) => (await import("../../services/logger.js")).log(message, data));
|
|
19
|
+
await log("Claude Code hook", { event: "unknown", code: "bad-event" });
|
|
20
|
+
}
|
|
21
|
+
catch {
|
|
22
|
+
/* Exit 0 on every failure. */
|
|
23
|
+
}
|
|
24
|
+
return 0;
|
|
25
|
+
}
|
package/dist/cli/index.js
CHANGED
|
@@ -5,26 +5,32 @@ import { historyImportUsage, importNeedsModel, parseHistoryImportArgs, } from ".
|
|
|
5
5
|
const COMMANDS = {
|
|
6
6
|
"import-opencode-history": "opencode",
|
|
7
7
|
"import-pi-history": "pi",
|
|
8
|
+
"import-claude-history": "claude-code",
|
|
8
9
|
};
|
|
9
10
|
const usage = `Usage: om-memory-system <command> [options]
|
|
10
11
|
|
|
11
12
|
Commands:
|
|
12
13
|
import-opencode-history Import OpenCode history into omms
|
|
13
14
|
import-pi-history Import Pi history into omms
|
|
15
|
+
import-claude-history Import Claude Code history into omms
|
|
14
16
|
web Start the web app in the foreground
|
|
15
17
|
web install|uninstall|status Manage the web app login item
|
|
18
|
+
memory <mode> [options] Search, add, list, or forget memories (run with --help)
|
|
19
|
+
claude-hook <event> Run a Claude Code hook (used by the Claude Code plugin)
|
|
16
20
|
|
|
17
21
|
Options:
|
|
18
22
|
--version, -v Print the installed version
|
|
19
23
|
|
|
20
24
|
Import commands take the same options; run one with --help to see them.
|
|
21
25
|
Inside a session, /memory-import-opencode-history and /memory-import-pi-history
|
|
22
|
-
use that session's model instead of an API key
|
|
26
|
+
use that session's model instead of an API key. import-claude-history always
|
|
27
|
+
uses the external API or --provider, --api-url, and --api-key-env.`;
|
|
23
28
|
export function parseImportArgs(argv) {
|
|
24
29
|
const [command, ...rest] = argv;
|
|
25
30
|
const host = command ? COMMANDS[command] : undefined;
|
|
26
|
-
if (!host)
|
|
27
|
-
throw new Error("Expected import-opencode-history or import-
|
|
31
|
+
if (!host) {
|
|
32
|
+
throw new Error("Expected import-opencode-history, import-pi-history, or import-claude-history; use --help");
|
|
33
|
+
}
|
|
28
34
|
const args = parseHistoryImportArgs(rest, { host, surface: "cli" });
|
|
29
35
|
if (!args.help && args.errors.length > 0)
|
|
30
36
|
throw new Error(args.errors.join("; "));
|
|
@@ -40,6 +46,16 @@ export async function runCli(argv) {
|
|
|
40
46
|
console.log(usage);
|
|
41
47
|
return 0;
|
|
42
48
|
}
|
|
49
|
+
if (argv[0] === "claude-hook") {
|
|
50
|
+
const { runClaudeHookCommand } = await import("../adapters/claude-code/hook-command.js");
|
|
51
|
+
await runClaudeHookCommand(argv.slice(1));
|
|
52
|
+
// An idle fetch connection could keep the hook alive past its Claude Code timeout.
|
|
53
|
+
process.exit(0);
|
|
54
|
+
}
|
|
55
|
+
if (argv[0] === "memory") {
|
|
56
|
+
const { runMemoryCommand } = await import("./memory-command.js");
|
|
57
|
+
return await runMemoryCommand(argv.slice(1));
|
|
58
|
+
}
|
|
43
59
|
if (argv[0] === "web") {
|
|
44
60
|
try {
|
|
45
61
|
const { runWebCommand } = await import("./web-command.js");
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import type { MemoryOperationArgs } from "../core/memory-operations.js";
|
|
2
|
+
/**
|
|
3
|
+
* `om-memory-system memory <mode>`: the shared `memory` tool operations from a
|
|
4
|
+
* terminal, for hosts without an in-process tool (Claude Code). Parsing and
|
|
5
|
+
* usage stay free of engine imports so `--help` is fast; config, storage and
|
|
6
|
+
* embeddings load with dynamic `import()` only when an operation runs.
|
|
7
|
+
*/
|
|
8
|
+
export interface MemoryCommandIO {
|
|
9
|
+
/** Working directory used when `--directory` is not given. */
|
|
10
|
+
cwd?: string;
|
|
11
|
+
stdout?: (text: string) => void;
|
|
12
|
+
stderr?: (text: string) => void;
|
|
13
|
+
}
|
|
14
|
+
export declare const memoryCommandUsage: string;
|
|
15
|
+
interface ParsedMemoryCommand {
|
|
16
|
+
help: boolean;
|
|
17
|
+
args: MemoryOperationArgs;
|
|
18
|
+
directory?: string;
|
|
19
|
+
}
|
|
20
|
+
/** Parse `<mode> [options]` into memory tool arguments. Throws on bad input. */
|
|
21
|
+
export declare function parseMemoryCommandArgs(argv: string[]): ParsedMemoryCommand;
|
|
22
|
+
/** Run `om-memory-system memory ...` and return the process exit code. */
|
|
23
|
+
export declare function runMemoryCommand(argv: string[], io?: MemoryCommandIO): Promise<number>;
|
|
24
|
+
export {};
|