omo-memory 0.1.13 → 0.1.14

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 CHANGED
@@ -106,6 +106,7 @@ The response contains a new `sessionId` and project metadata only. It deliberate
106
106
  ```
107
107
 
108
108
  This is local routing, not transcript scraping. OMO Memory does not automatically read full Codex or Grok transcripts. Hooks should record concise user actions, decisions, QA evidence, and handoffs; they should retrieve memory only when the user explicitly asks for OMO Memory or when the current user input can be matched to recorded intent.
109
+ The packaged `scripts/omo-memory-user-prompt.mjs` helper is the supported UserPromptSubmit hook target for adapters that can invoke a command with the hook payload on stdin. It records only the current user prompt as a redacted `user_prompt` event, ignores assistant output, and exits successfully without blocking the host when OMO Memory is unavailable.
109
110
 
110
111
  Use explicit retrieval for memory reads:
111
112
 
@@ -6,6 +6,22 @@ import { migrate } from "./memoryDb.js";
6
6
  import { resolveProjectContext } from "./projectContext.js";
7
7
  const STATE_DB_SUFFIX = join(".omo", "memory", "state.sqlite");
8
8
  const REQUIRED_TABLES = ["schema_meta", "projects", "events"];
9
+ const PRUNED_DIR_NAMES = new Set([
10
+ ".cache",
11
+ ".git",
12
+ ".hg",
13
+ ".next",
14
+ ".pnpm-store",
15
+ ".turbo",
16
+ ".venv",
17
+ ".yarn",
18
+ "Library",
19
+ "build",
20
+ "dist",
21
+ "node_modules",
22
+ "target",
23
+ "vendor",
24
+ ]);
9
25
  export function initGlobalMemory(globalDbPath) {
10
26
  mkdirSync(dirname(globalDbPath), { recursive: true });
11
27
  const db = new Database(globalDbPath);
@@ -105,7 +121,7 @@ function findStateDbs(rootPath) {
105
121
  }
106
122
  for (const entry of entries) {
107
123
  const entryPath = join(path, entry.name);
108
- if (entry.isDirectory())
124
+ if (entry.isDirectory() && shouldVisitDirectory(entry.name, entryPath))
109
125
  visit(entryPath);
110
126
  else if (entry.isFile() && entryPath.endsWith(STATE_DB_SUFFIX))
111
127
  dbPaths.push(entryPath);
@@ -114,6 +130,11 @@ function findStateDbs(rootPath) {
114
130
  visit(rootPath);
115
131
  return dbPaths.sort();
116
132
  }
133
+ function shouldVisitDirectory(name, path) {
134
+ if (path.endsWith(join(".omo", "memory")))
135
+ return true;
136
+ return !PRUNED_DIR_NAMES.has(name);
137
+ }
117
138
  function scanSourceDb(dbPath) {
118
139
  let db;
119
140
  try {
package/dist/graphTui.js CHANGED
@@ -1,5 +1,6 @@
1
1
  import { spawn } from "node:child_process";
2
2
  import { BoxRenderable, createCliRenderer, TextRenderable } from "@opentui/core";
3
+ import { classCode, graphContent } from "./graphTuiCanvas.js";
3
4
  const SNAPSHOT_ENV = "OMO_MEMORY_GRAPH_TUI_SNAPSHOT";
4
5
  const COLORS = {
5
6
  background: "#101419",
@@ -76,7 +77,7 @@ export async function runGraphTui(options) {
76
77
  graph = loadGraph(state);
77
78
  state = { ...state, selectedId: graph.detail?.id ?? null };
78
79
  title.content = titleText(graph, state.options.query);
79
- nodesText.content = nodesContent(graph);
80
+ nodesText.content = graphContent(graph);
80
81
  detailText.content = detailContent(graph);
81
82
  footer.content = "q quit | ArrowUp/ArrowDown/Tab select | Legend: D durable, W working, T temporary, E ephemeral";
82
83
  renderer.requestRender();
@@ -133,13 +134,7 @@ async function createGraphSnapshot(options) {
133
134
  dbPath: options.dbPath,
134
135
  ...(options.query === undefined ? {} : { query: options.query }),
135
136
  });
136
- const details = graph.nodes
137
- .map((node) => projectOntologyGraph({
138
- dbPath: options.dbPath,
139
- ...(options.query === undefined ? {} : { query: options.query }),
140
- selectedId: node.id,
141
- }).detail)
142
- .filter((detail) => detail !== null);
137
+ const details = graph.detail === null ? [] : [graph.detail];
143
138
  return { graph, details };
144
139
  }
145
140
  function parseSnapshot(raw) {
@@ -160,23 +155,37 @@ function isGraphSnapshot(value) {
160
155
  }
161
156
  function loadGraph(state) {
162
157
  const selectedId = state.selectedId ?? state.snapshot.graph.detail?.id ?? state.snapshot.graph.nodes[0]?.id ?? null;
163
- const detail = selectedId === null ? state.snapshot.graph.detail : (state.snapshot.details.find((item) => item.id === selectedId) ?? null);
158
+ const selectedNode = selectedId === null ? undefined : state.snapshot.graph.nodes.find((node) => node.id === selectedId);
159
+ const detail = selectedId === null ? state.snapshot.graph.detail : (state.snapshot.details.find((item) => item.id === selectedId) ?? nodeDetail(selectedNode) ?? null);
164
160
  return {
165
161
  ...state.snapshot.graph,
166
162
  nodes: state.snapshot.graph.nodes.map((node) => ({ ...node, selected: node.id === selectedId })),
167
163
  detail,
168
164
  };
169
165
  }
166
+ function nodeDetail(node) {
167
+ if (node === undefined)
168
+ return null;
169
+ return {
170
+ id: node.id,
171
+ label: node.label,
172
+ kind: node.kind,
173
+ description: node.description,
174
+ aliases: node.aliases,
175
+ retentionClass: node.retentionClass,
176
+ score: node.score,
177
+ scoreLabel: node.scoreLabel,
178
+ refCount: node.refCount,
179
+ projectSpread: node.projectSpread,
180
+ firstSeen: node.firstSeen,
181
+ lastSeen: node.lastSeen,
182
+ project: node.project,
183
+ };
184
+ }
170
185
  function titleText(graph, query) {
171
186
  const queryLabel = query === undefined ? "all concepts" : `query "${query}"`;
172
187
  return `OMO Ontology Graph - ${queryLabel} - ${graph.nodes.length} nodes / ${graph.edges.length} edges`;
173
188
  }
174
- function nodesContent(graph) {
175
- if (graph.nodes.length === 0)
176
- return graph.message ?? "No ontology graph data is available yet.";
177
- const edges = graph.edges.map((edge) => ` ${edge.sourceId} -> ${edge.targetId} [${edge.label}]`);
178
- return [...graph.nodes.map((node) => nodeLine(node)), "", "Relations", ...(edges.length === 0 ? [" none in current filter"] : edges)].join("\n");
179
- }
180
189
  function detailContent(graph) {
181
190
  if (graph.detail === null)
182
191
  return graph.message ?? "No ontology graph data is available yet.";
@@ -203,8 +212,8 @@ function writeCaptureFrame(graph, query) {
203
212
  process.stdout.write([
204
213
  titleText(graph, query),
205
214
  "",
206
- "Nodes",
207
- nodesContent(graph),
215
+ "Graph",
216
+ graphContent(graph),
208
217
  "",
209
218
  "Detail",
210
219
  detailContent(graph),
@@ -213,20 +222,6 @@ function writeCaptureFrame(graph, query) {
213
222
  "",
214
223
  ].join("\n"));
215
224
  }
216
- function nodeLine(node) {
217
- const marker = node.selected ? ">" : " ";
218
- return `${marker} ${classCode(node.retentionClass)} ${node.label} (${node.kind}, ${node.scoreLabel}, refs ${node.refCount})`;
219
- }
220
- function classCode(retentionClass) {
221
- const normalized = retentionClass.toLowerCase();
222
- if (normalized === "durable")
223
- return "D";
224
- if (normalized === "temporary")
225
- return "T";
226
- if (normalized === "ephemeral")
227
- return "E";
228
- return "W";
229
- }
230
225
  function nextSelectedId(nodes, selectedId, keyName) {
231
226
  if (nodes.length === 0)
232
227
  return null;
@@ -0,0 +1,104 @@
1
+ const GRAPH_WIDTH = 66;
2
+ const GRAPH_HEIGHT = 24;
3
+ const NODE_LIMIT = 28;
4
+ export function graphContent(graph) {
5
+ if (graph.nodes.length === 0)
6
+ return graph.message ?? "No ontology graph data is available yet.";
7
+ const drawnNodes = graph.nodes.slice(0, NODE_LIMIT);
8
+ const positions = layoutNodes(drawnNodes);
9
+ const rows = Array.from({ length: GRAPH_HEIGHT }, () => Array.from({ length: GRAPH_WIDTH }, () => " "));
10
+ for (const edge of graph.edges) {
11
+ const source = positions.get(edge.sourceId);
12
+ const target = positions.get(edge.targetId);
13
+ if (source === undefined || target === undefined)
14
+ continue;
15
+ drawEdge(rows, source, target);
16
+ }
17
+ for (const node of drawnNodes) {
18
+ const position = positions.get(node.id);
19
+ if (position === undefined)
20
+ continue;
21
+ drawNode(rows, node, position);
22
+ }
23
+ const graphLines = rows.map((row) => row.join("").trimEnd());
24
+ const legend = drawnNodes.map((node) => nodeLine(node));
25
+ return [...graphLines, "", "Nodes", ...legend, "", "Relations", ...relationLines(graph, positions)].join("\n");
26
+ }
27
+ export function nodeLine(node) {
28
+ const marker = node.selected ? ">" : " ";
29
+ return `${marker} ${classCode(node.retentionClass)} ${node.label} (${node.kind}, ${node.scoreLabel}, refs ${node.refCount})`;
30
+ }
31
+ export function classCode(retentionClass) {
32
+ const normalized = retentionClass.toLowerCase();
33
+ if (normalized === "durable")
34
+ return "D";
35
+ if (normalized === "temporary")
36
+ return "T";
37
+ if (normalized === "ephemeral")
38
+ return "E";
39
+ return "W";
40
+ }
41
+ function layoutNodes(nodes) {
42
+ const centerX = Math.floor(GRAPH_WIDTH / 2);
43
+ const centerY = Math.floor(GRAPH_HEIGHT / 2);
44
+ const radiusX = Math.max(8, Math.floor(GRAPH_WIDTH / 2) - 8);
45
+ const radiusY = Math.max(4, Math.floor(GRAPH_HEIGHT / 2) - 3);
46
+ const positions = new Map();
47
+ nodes.forEach((node, index) => {
48
+ const angle = (2 * Math.PI * index) / Math.max(1, nodes.length);
49
+ const scorePull = Math.max(0.55, 1 - Math.min(90, node.score) / 220);
50
+ positions.set(node.id, {
51
+ x: clamp(Math.round(centerX + Math.cos(angle) * radiusX * scorePull), 2, GRAPH_WIDTH - 3),
52
+ y: clamp(Math.round(centerY + Math.sin(angle) * radiusY * scorePull), 1, GRAPH_HEIGHT - 2),
53
+ });
54
+ });
55
+ return positions;
56
+ }
57
+ function drawEdge(rows, source, target) {
58
+ const steps = Math.max(Math.abs(target.x - source.x), Math.abs(target.y - source.y), 1);
59
+ for (let index = 1; index < steps; index += 1) {
60
+ const x = Math.round(source.x + ((target.x - source.x) * index) / steps);
61
+ const y = Math.round(source.y + ((target.y - source.y) * index) / steps);
62
+ const row = rows[y];
63
+ const current = row?.[x];
64
+ if (row === undefined || current === undefined || current !== " ")
65
+ continue;
66
+ row[x] = edgeGlyph(source, target);
67
+ }
68
+ }
69
+ function edgeGlyph(source, target) {
70
+ const dx = target.x - source.x;
71
+ const dy = target.y - source.y;
72
+ if (Math.abs(dx) > Math.abs(dy) * 2)
73
+ return "-";
74
+ if (Math.abs(dy) > Math.abs(dx) * 2)
75
+ return "|";
76
+ return dx * dy > 0 ? "\\" : "/";
77
+ }
78
+ function drawNode(rows, node, point) {
79
+ const glyph = node.selected ? "●" : classCode(node.retentionClass);
80
+ const label = `${glyph}${shortLabel(node.label)}`;
81
+ const startX = clamp(point.x - Math.floor(label.length / 2), 0, Math.max(0, GRAPH_WIDTH - label.length));
82
+ for (let index = 0; index < label.length; index += 1) {
83
+ const row = rows[point.y];
84
+ if (row === undefined)
85
+ continue;
86
+ row[startX + index] = label[index] ?? " ";
87
+ }
88
+ }
89
+ function shortLabel(label) {
90
+ const compact = label.replace(/\s+/g, " ").trim();
91
+ return compact.length <= 11 ? compact : compact.slice(0, 10);
92
+ }
93
+ function relationLines(graph, positions) {
94
+ const visible = graph.edges.filter((edge) => positions.has(edge.sourceId) && positions.has(edge.targetId)).slice(0, 12);
95
+ if (visible.length === 0)
96
+ return [" none in current filter"];
97
+ return visible.map((edge) => ` ${nodeLabel(graph, edge.sourceId)} -> ${nodeLabel(graph, edge.targetId)} [${edge.label}]`);
98
+ }
99
+ function nodeLabel(graph, nodeId) {
100
+ return graph.nodes.find((node) => node.id === nodeId)?.label ?? nodeId;
101
+ }
102
+ function clamp(value, min, max) {
103
+ return Math.min(max, Math.max(min, value));
104
+ }
@@ -1,4 +1,5 @@
1
1
  import { migrate, openMemoryDb } from "./memoryDb.js";
2
+ import { readGraphEdges } from "./ontologyGraphEdges.js";
2
3
  import { redactSecrets, sanitizeGitRemote } from "./privacy.js";
3
4
  function isRecord(value) {
4
5
  return value !== null && typeof value === "object";
@@ -55,21 +56,6 @@ function parseConceptRow(value) {
55
56
  gitRemote: nullableText(value["gitRemote"]),
56
57
  };
57
58
  }
58
- function parseRelationRow(value) {
59
- if (!isRecord(value)) {
60
- throw new Error("invalid relation row");
61
- }
62
- return {
63
- id: text(value["id"]),
64
- projectId: text(value["projectId"]),
65
- sourceId: text(value["sourceId"]),
66
- targetId: text(value["targetId"]),
67
- relation: text(value["relation"]),
68
- weight: numberValue(value["weight"]),
69
- repoRoot: text(value["repoRoot"]),
70
- gitRemote: nullableText(value["gitRemote"]),
71
- };
72
- }
73
59
  function matchesQuery(row, query) {
74
60
  const haystack = `${row.label}\n${row.description ?? ""}\n${row.aliases.join("\n")}`.toLowerCase();
75
61
  return haystack.includes(query);
@@ -88,6 +74,8 @@ function toNode(row, selectedId) {
88
74
  scoreLabel: `${score} ${retentionClass}`,
89
75
  refCount: Math.round(row.refCount),
90
76
  projectSpread: Math.round(row.projectSpread),
77
+ firstSeen: row.firstSeen,
78
+ lastSeen: row.lastSeen,
91
79
  project: projectFrom(row),
92
80
  selected: selectedId === row.id,
93
81
  };
@@ -111,19 +99,6 @@ function toDetail(row) {
111
99
  project: projectFrom(row),
112
100
  };
113
101
  }
114
- function toEdge(row) {
115
- const weight = Number(row.weight.toFixed(2));
116
- const relation = redactSecrets(row.relation);
117
- return {
118
- id: row.id,
119
- sourceId: row.sourceId,
120
- targetId: row.targetId,
121
- relation,
122
- label: `${relation} ${weight.toFixed(2)}`,
123
- weight,
124
- project: projectFrom(row),
125
- };
126
- }
127
102
  export function projectOntologyGraph(options) {
128
103
  const db = openMemoryDb(options.dbPath);
129
104
  try {
@@ -148,21 +123,9 @@ export function projectOntologyGraph(options) {
148
123
  const selectedId = options.selectedId && conceptIds.has(options.selectedId) ? options.selectedId : (conceptRows[0]?.id ?? null);
149
124
  const nodes = conceptRows.map((row) => toNode(row, selectedId));
150
125
  const selectedRow = selectedId === null ? undefined : conceptRows.find((row) => row.id === selectedId);
151
- const relationRows = db
152
- .prepare(`
153
- SELECT r.id, r.project_id AS projectId, r.source_id AS sourceId, r.target_id AS targetId,
154
- r.relation, COALESCE(r.weight, 1) AS weight, p.repo_root AS repoRoot, p.git_remote AS gitRemote
155
- FROM relations r
156
- JOIN projects p ON p.id = r.project_id
157
- WHERE r.source_type = 'concept' AND r.target_type = 'concept' AND r.valid_to IS NULL
158
- ORDER BY lower(r.relation) ASC, r.id ASC
159
- `)
160
- .all()
161
- .map(parseRelationRow)
162
- .filter((row) => conceptIds.has(row.sourceId) && conceptIds.has(row.targetId));
163
126
  return {
164
127
  nodes,
165
- edges: relationRows.map(toEdge),
128
+ edges: readGraphEdges(db, conceptIds),
166
129
  detail: selectedRow === undefined ? null : toDetail(selectedRow),
167
130
  message: nodes.length === 0 ? "No ontology graph data is available yet." : null,
168
131
  };
@@ -0,0 +1,86 @@
1
+ import { redactSecrets, sanitizeGitRemote } from "./privacy.js";
2
+ export function readGraphEdges(db, conceptIds) {
3
+ const relationRows = db
4
+ .prepare(`
5
+ SELECT r.id, r.project_id AS projectId, r.source_id AS sourceId, r.target_id AS targetId,
6
+ r.relation, COALESCE(r.weight, 1) AS weight, p.repo_root AS repoRoot, p.git_remote AS gitRemote
7
+ FROM relations r
8
+ JOIN projects p ON p.id = r.project_id
9
+ WHERE r.source_type = 'concept' AND r.target_type = 'concept' AND r.valid_to IS NULL
10
+ ORDER BY lower(r.relation) ASC, r.id ASC
11
+ `)
12
+ .all()
13
+ .map(parseRelationRow)
14
+ .filter((row) => conceptIds.has(row.sourceId) && conceptIds.has(row.targetId));
15
+ const edgeRows = relationRows.length === 0 ? readCoOccurrenceRows(db, conceptIds) : relationRows;
16
+ return edgeRows.map(toEdge);
17
+ }
18
+ function readCoOccurrenceRows(db, conceptIds) {
19
+ return db
20
+ .prepare(`
21
+ SELECT 'co:' || a.target_id || ':' || b.target_id AS id,
22
+ a.project_id AS projectId, a.target_id AS sourceId, b.target_id AS targetId,
23
+ 'co_occurs' AS relation, COUNT(*) AS weight, p.repo_root AS repoRoot, p.git_remote AS gitRemote
24
+ FROM memory_references a
25
+ JOIN memory_references b
26
+ ON b.project_id = a.project_id
27
+ AND b.source_type = a.source_type
28
+ AND b.source_id = a.source_id
29
+ AND b.target_type = 'concept'
30
+ AND a.target_id < b.target_id
31
+ JOIN projects p ON p.id = a.project_id
32
+ WHERE a.source_type = 'event' AND a.target_type = 'concept'
33
+ GROUP BY a.project_id, a.target_id, b.target_id
34
+ ORDER BY COUNT(*) DESC, a.target_id ASC, b.target_id ASC
35
+ LIMIT 160
36
+ `)
37
+ .all()
38
+ .map(parseRelationRow)
39
+ .filter((row) => conceptIds.has(row.sourceId) && conceptIds.has(row.targetId));
40
+ }
41
+ function parseRelationRow(value) {
42
+ if (!isRecord(value))
43
+ throw new Error("invalid relation row");
44
+ return {
45
+ id: text(value["id"]),
46
+ projectId: text(value["projectId"]),
47
+ sourceId: text(value["sourceId"]),
48
+ targetId: text(value["targetId"]),
49
+ relation: text(value["relation"]),
50
+ weight: numberValue(value["weight"]),
51
+ repoRoot: text(value["repoRoot"]),
52
+ gitRemote: nullableText(value["gitRemote"]),
53
+ };
54
+ }
55
+ function toEdge(row) {
56
+ const weight = Number(row.weight.toFixed(2));
57
+ const relation = redactSecrets(row.relation);
58
+ return {
59
+ id: row.id,
60
+ sourceId: row.sourceId,
61
+ targetId: row.targetId,
62
+ relation,
63
+ label: `${relation} ${weight.toFixed(2)}`,
64
+ weight,
65
+ project: projectFrom(row),
66
+ };
67
+ }
68
+ function projectFrom(row) {
69
+ return {
70
+ id: row.projectId,
71
+ repoRoot: redactSecrets(row.repoRoot),
72
+ gitRemote: sanitizeGitRemote(row.gitRemote),
73
+ };
74
+ }
75
+ function isRecord(value) {
76
+ return value !== null && typeof value === "object";
77
+ }
78
+ function text(value) {
79
+ return typeof value === "string" ? value : String(value ?? "");
80
+ }
81
+ function nullableText(value) {
82
+ return value === null || value === undefined ? null : text(value);
83
+ }
84
+ function numberValue(value) {
85
+ return typeof value === "number" && Number.isFinite(value) ? value : Number(value ?? 0);
86
+ }
@@ -118,14 +118,14 @@ During the session, hooks should write concise user-action summaries, task state
118
118
  }
119
119
  ```
120
120
 
121
- This package is the local MCP-to-SQLite router. It does not scrape host transcripts or centralize cloud state. Hosts and adapters must call the MCP tools at their own lifecycle points.
121
+ This package is the local MCP-to-SQLite router. It does not scrape host transcripts, store assistant output, or centralize cloud state. Hosts and adapters must call the MCP tools at their own lifecycle points.
122
122
 
123
123
  Retrieval is opt-in or intent-gated:
124
124
 
125
125
  - Use `memory_recent_events` only when the user explicitly asks for recent OMO Memory context.
126
126
  - Use `memory_recall_events` when the current user input has a concrete query that can be matched to recorded summaries, decisions, or evidence.
127
127
  - Do not automatically attach the last session to every user prompt.
128
- - Do not store raw user prompts by default; record concise, redacted action summaries.
128
+ - To preserve user intent across sessions, adapters may invoke the packaged `scripts/omo-memory-user-prompt.mjs` UserPromptSubmit helper with the hook payload on stdin. The helper records only the current user prompt as a redacted `user_prompt` event and ignores assistant output.
129
129
 
130
130
  Use these tools:
131
131
 
package/package.json CHANGED
@@ -1,11 +1,12 @@
1
1
  {
2
2
  "name": "omo-memory",
3
- "version": "0.1.13",
3
+ "version": "0.1.14",
4
4
  "description": "Host-neutral local SQLite memory and session ledger for OMO adapters, exposed through CLI and MCP.",
5
5
  "type": "module",
6
6
  "files": [
7
7
  "dist",
8
8
  "docs",
9
+ "scripts/omo-memory-user-prompt.mjs",
9
10
  "README.md"
10
11
  ],
11
12
  "bin": {
@@ -32,9 +33,10 @@
32
33
  "start": "node dist/cli.js",
33
34
  "prepack": "npm run build",
34
35
  "presmoke": "npm run build",
35
- "smoke": "npm run smoke:cli && npm run smoke:mcp",
36
+ "smoke": "npm run smoke:cli && npm run smoke:mcp && npm run smoke:hook",
36
37
  "smoke:cli": "node scripts/smoke-cli.mjs",
37
38
  "smoke:mcp": "node scripts/smoke-mcp.mjs",
39
+ "smoke:hook": "node scripts/verify-user-prompt-hook.mjs",
38
40
  "mcp": "node dist/cli.js mcp",
39
41
  "issue:epic": "node scripts/create-epic-issue.mjs"
40
42
  },
@@ -0,0 +1,107 @@
1
+ #!/usr/bin/env node
2
+ import { spawnSync } from "node:child_process";
3
+ import { readFileSync } from "node:fs";
4
+
5
+ const MAX_SUMMARY_CHARS = 4000;
6
+
7
+ const host = process.env["OMO_MEMORY_HOST"] ?? "unknown";
8
+ const adapter = process.env["OMO_MEMORY_ADAPTER"] ?? "unknown";
9
+
10
+ const input = readStdin();
11
+ const payload = parseJson(input);
12
+ const prompt = extractPrompt(payload);
13
+
14
+ if (prompt === null) process.exit(0);
15
+
16
+ const summary = truncate(prompt.replace(/\s+/g, " ").trim(), MAX_SUMMARY_CHARS);
17
+ if (summary.length === 0) process.exit(0);
18
+
19
+ const hookSessionId = readString(payload, "sessionId") ?? readString(payload, "session_id");
20
+ const workspaceRoot = readString(payload, "workspaceRoot") ?? readString(payload, "cwd");
21
+ const metadata = {
22
+ source: "hook",
23
+ hookEventName: readString(payload, "hookEventName") ?? process.env["GROK_HOOK_EVENT"] ?? process.env["CODEX_HOOK_EVENT"] ?? "UserPromptSubmit",
24
+ host,
25
+ adapter,
26
+ ...(hookSessionId === null ? {} : { hookSessionId }),
27
+ ...(workspaceRoot === null ? {} : { workspaceRoot }),
28
+ };
29
+
30
+ const args = ["event", "record", "--type", "user_prompt", "--summary", summary, "--payload-json", JSON.stringify(metadata)];
31
+
32
+ const result = runOmoMemory(args) ?? runNpx(args);
33
+ if (result === undefined || result.status !== 0) process.exit(0);
34
+
35
+ function readStdin() {
36
+ try {
37
+ return readFileSync(0, "utf8");
38
+ } catch (error) {
39
+ if (error instanceof Error) process.exit(0);
40
+ throw error;
41
+ }
42
+ }
43
+
44
+ function parseJson(raw) {
45
+ if (raw.trim().length === 0) return {};
46
+ try {
47
+ const parsed = JSON.parse(raw);
48
+ return isRecord(parsed) ? parsed : {};
49
+ } catch (error) {
50
+ if (error instanceof SyntaxError) return {};
51
+ throw error;
52
+ }
53
+ }
54
+
55
+ function extractPrompt(value) {
56
+ if (!isRecord(value)) return null;
57
+ for (const key of ["prompt", "userPrompt", "user_prompt", "message", "text", "input"]) {
58
+ const direct = readString(value, key);
59
+ if (direct !== null) return direct;
60
+ }
61
+ const nestedPrompt =
62
+ readNestedString(value, ["toolInput", "prompt"]) ?? readNestedString(value, ["payload", "prompt"]) ?? readNestedString(value, ["data", "prompt"]);
63
+ if (nestedPrompt !== null) return nestedPrompt;
64
+ return null;
65
+ }
66
+
67
+ function readNestedString(value, path) {
68
+ let cursor = value;
69
+ for (const key of path) {
70
+ if (!isRecord(cursor)) return null;
71
+ cursor = cursor[key];
72
+ }
73
+ return typeof cursor === "string" ? cursor : null;
74
+ }
75
+
76
+ function readString(value, key) {
77
+ const item = value[key];
78
+ return typeof item === "string" ? item : null;
79
+ }
80
+
81
+ function truncate(value, maxLength) {
82
+ if (value.length <= maxLength) return value;
83
+ return `${value.slice(0, maxLength - 15)} [TRUNCATED]`;
84
+ }
85
+
86
+ function runOmoMemory(args) {
87
+ const command = process.env["OMO_MEMORY_CLI"] ?? "omo-memory";
88
+ return run(command, args);
89
+ }
90
+
91
+ function runNpx(args) {
92
+ return run("npx", ["-y", "omo-memory", ...args]);
93
+ }
94
+
95
+ function run(command, args) {
96
+ const result = spawnSync(command, args, {
97
+ encoding: "utf8",
98
+ stdio: ["ignore", "pipe", "pipe"],
99
+ timeout: 5000,
100
+ });
101
+ if (result.error?.code === "ENOENT") return undefined;
102
+ return result;
103
+ }
104
+
105
+ function isRecord(value) {
106
+ return value !== null && typeof value === "object" && !Array.isArray(value);
107
+ }