feat(memory): persistent memory layer with remember/recall CodeAct API

Add a memories table (survives index rebuilds) for agent-written
  conclusions with provenance (session, message range, project). The agent
  writes markdown files via Write tool (user-approved), then registers
  them via a --remember CodeAct script with remember(). Recall via
  memories() in --query scripts, filtered by project/session/time.

  Separates query (read-only, assertReadOnlySql) from remember (write)
  execution contexts in runtime.mjs.
This commit is contained in:
tommy0103
2026-06-10 02:05:27 +08:00
parent dff88bbb37
commit 34f3a164ab
9 changed files with 364 additions and 29 deletions
+2 -1
View File
@@ -1,3 +1,4 @@
.DS_Store
plans/
.skillopt-backups
.skillopt-backups
tests/
+12 -5
View File
@@ -28,8 +28,9 @@ Every past session, subagent, and workflow -- queryable by your agent.
Most history tools help humans find old chats.
Obelisk is built for agents. It exposes past work as structured data: sessions,
messages, tool calls, subagents, workflows, file history, failures, and parent
chains. The agent writes the query, runs it locally, and answers in plain language.
messages, tool calls, subagents, workflows, file history, failures, parent
chains, and human-approved markdown memories. The agent writes the query, runs
it locally, and answers in plain language.
You don't manage history. You ask questions about past work.
@@ -88,6 +89,10 @@ Runs it via node runtime.mjs --query <script>
Reads the JSON result, answers you in natural language
```
When a retrieval produces a memory worth keeping, the agent proposes a markdown
memory file. After user approval, it registers that file with the narrow
`runtime.mjs --remember <script>` runtime, which exposes only `remember()`.
**The core idea: don't make humans browse, tag, or organize sessions.**
Don't invent a rigid query DSL either.
@@ -101,10 +106,11 @@ references only when the question needs them:
- `search(text)` — FTS5 full-text search, returns matches with surrounding context
- `context(uuid)` — full story around a message (parent chain, subagent/workflow metadata)
- `sql(query, ...params)` — raw SQL for anything else
- `sql(query, ...params)` — read-only SQL for structured queries
**Structured shortcuts** — session, summary, subagent, workflow, file-history,
failure, raw-window, and parent-chain helpers over the same SQLite data.
**Structured shortcuts** — session, memory, summary, subagent, workflow,
file-history, failure, raw-window, and parent-chain helpers over the same
SQLite data.
**References** — agent reads on demand when the task needs deeper structure:
@@ -126,6 +132,7 @@ schema stay out of the first prompt until the agent needs them.
| **Subagents** | `subagents/agent-<id>.jsonl` | Agent type, description, full conversation |
| **Workflows** | `workflows/wf_<runId>.json` | Script, structured result, agent count |
| **Workflow agents** | `subagents/workflows/wf_<runId>/` | Per-agent transcripts linked to workflow |
| **Memories** | markdown files registered by the agent after user approval | Prior conclusions linked to source sessions/messages |
Full-text search via FTS5 covers message text across every layer, while the SQLite tables preserve the structure agents need for investigation.
+61 -2
View File
@@ -4,6 +4,7 @@ description: >
Search and query past Claude Code session history.
Reactive: when the user asks "how did I fix X", "what did we do last time", "find the session where", "上次怎么修的", "之前的session", "历史记录".
Proactive: when the user references past work you lack context for, when you're about to modify a file with complex edit history, when the user says "继续之前的" or "continue where we left off", or when understanding prior decisions would improve your current response.
Memory: when the user says "记住这个", "remember this", "写入记忆", "save this conclusion", or when you determine a retrieval result contains a conclusion worth persisting.
allowed-tools:
- Read
- Bash(node:*)
@@ -43,6 +44,8 @@ Custom query:
3. Parse JSON stdout and answer with concise evidence.
The query file runs inside `(async () => { ... })()`. Use `return` to emit JSON.
Query scripts are read-only: `remember()` is not available, and `sql()` only
accepts read-only SELECT/WITH queries.
## Query Routing
@@ -97,7 +100,7 @@ expand vertically from one evidence point without dumping the whole session.
### `sql(query, ...params)`
Raw SQL with `?` placeholders. Returns array rows.
Read-only SQL SELECT/WITH with `?` placeholders. Returns array rows.
Before writing non-trivial SQL, read `references/schema.md`. Common safe joins:
@@ -107,7 +110,7 @@ Before writing non-trivial SQL, read `references/schema.md`. Common safe joins:
- Prefer SQL-side `GROUP BY`, `COUNT`, `MAX`, `ORDER BY`, and `LIMIT` over hand-counting in the final answer.
Tables: `sessions`, `messages`, `tool_calls`, `tool_results`, `summaries`,
`subagents`, `workflows`, `workflow_agents`, `messages_fts`.
`memories`, `subagents`, `workflows`, `workflow_agents`, `messages_fts`.
## Structured Helpers
@@ -130,6 +133,7 @@ tiny sample before relying on less common filters.
- `trace(uuid)` -- parent chain from root to message.
- `thread(sessionId)` -- full session messages; last resort only.
- `raw(uuid, opts?)` -- windowed access to the original JSONL line.
- `memories(opts?)` -- recall memory layer, newest first. opts: `{ query, project, sessionId, sessions, after, before, branch, limit }`. `query` filters summary/path by terms. Returns registered memory records (id, path, summary, project, session_id, created_at). Read the file at `path` for full content.
## Retrieval Contract
@@ -144,6 +148,61 @@ If field, context, ordering, FTS, or helper semantics affect the query, read
`references/retrieval-semantics.md` before coding. If a query errors, read
`references/pitfalls.md` before retrying.
## Memory Layer
Obelisk has a persistent memory layer alongside raw session data. Every
retrieval queries both layers: `memories()` for prior conclusions, `search()`
and helpers for raw session evidence. Use memory as prior notes, not final
authority. If a memory record influences your answer, say naturally that it was
previously recorded, and compare it with raw session evidence when correctness
depends on it. Raw session data is the evidence layer, but one hit is not a
complete truth; query and cite it compactly.
**Recall:** query `memories({ query: 'topic terms', project: '...' })` to find
prior conclusions relevant to the current task. Like other list helpers,
passing a string is treated as `sessionId`, and passing a number is treated as
`limit`. Read the file at `path` for full content.
**Writing memories:** after a retrieval produces a conclusion worth persisting,
propose writing a memory file. The user must approve. Flow:
1. Write a markdown file using the `Write` tool (user approves).
2. Register it via `remember()` in a narrow memory-registration script:
```js
return remember({
path: '.obelisk/memories/design-decision-x.md',
session_id: 'current-session-id',
message_start: 'uuid-of-first-relevant-msg',
message_end: 'uuid-of-last-relevant-msg',
summary: 'Detailed summary: what was decided, why, what alternatives were considered, and what constraints drove the choice.'
})
```
Run the registration script with:
```bash
node $SKILL_DIR/scripts/runtime.mjs --remember /tmp/register-memory.mjs
```
`--remember` exposes only `remember()`. It does not expose `search()`, `sql()`,
`memories()`, or other retrieval helpers. If you need source IDs, find them
first with a normal `--query` script.
`remember()` validates that `path` already exists and points to a file. Relative
paths are resolved against the source session's `project_path` when
`session_id` is provided, then stored as normalized absolute paths. Prefer
project-relative paths such as `.obelisk/memories/...` plus `session_id`.
`summary` should be detailed enough that `memories()` results alone can judge
relevance without reading the file. Include the decision, the reasoning, and
the key constraints — not just a title.
The `message_start`/`message_end` range marks where in the conversation this
conclusion was drawn. Use it later to trace back to the original evidence.
Memory records survive index rebuilds. They are never auto-deleted.
## Minimal Patterns
Search, then expand one promising hit:
+73 -2
View File
@@ -1,7 +1,8 @@
# Obelisk Query Patterns
These are copyable CodeAct patterns for `runtime.mjs --query` scripts. They are
not new APIs. Adapt them to the user's scope and return compact evidence.
These are copyable CodeAct patterns for `runtime.mjs --query` scripts, plus one
`--remember` registration pattern. They are not new APIs. Adapt them to the
user's scope and return compact evidence.
## Bounded Search To Context
@@ -26,6 +27,76 @@ return hits.slice(0, 5).map(h => {
});
```
## Memory Plus Session Evidence
Use this when prior conclusions may exist but the answer still depends on raw
session evidence. Keep memory as prior notes, not final authority; compare it
with session evidence in your final answer when correctness matters.
```js
const project = '%quiet-zero%';
const topic = 'markdown memory layer';
const ftsTopic = topic.replace(/[-_]/g, ' ');
const prior_memories = memories({
project,
query: topic,
limit: 5,
}).map(m => ({
id: m.id,
path: m.path,
session_id: m.session_id,
message_start: m.message_start,
message_end: m.message_end,
created_at: m.created_at,
summary: m.summary?.slice(0, 260),
}));
const session_evidence = search(ftsTopic, { project, limit: 8 })
.slice(0, 6)
.map(h => ({
session_id: h.session.id,
session_title: h.session.title,
uuid: h.message.uuid,
timestamp: h.message.timestamp,
snippet: h.message.text?.slice(0, 220),
}));
return {
query_plan: {
project,
topic,
memory_limit: 5,
session_limit: 8,
},
prior_memories,
session_evidence,
};
```
## Register Approved Memory
Use this only after the user approves writing memory and the markdown file
already exists. `remember()` validates the file and stores a normalized absolute
path, so keep the script small and return the registered record.
Run this script with `runtime.mjs --remember <script>`. The `--remember` runtime
exposes only `remember()`, not retrieval helpers.
```js
return remember({
path: '.obelisk/memories/memory-layer-design.md',
session_id: 'source-session-id',
message_start: 'first-message-uuid',
message_end: 'last-message-uuid',
summary: [
'Decision: Obelisk uses one user-facing entry that queries both memory and raw sessions.',
'Memory records are prior notes and must be identified naturally when they influence an answer.',
'New memory writes require human confirmation before the markdown file is written and registered.',
].join(' '),
});
```
## One-Shot Retrieval For Synthesis
Use this for conclusion, broad history, failure investigation, or file evolution
+13 -8
View File
@@ -13,7 +13,7 @@ Classify the user's request before choosing tools.
|-------------|--------------|------------|-------------|
| project name/path, session, cwd, file, time range | scope | `sessions()`, exact SQL on `project_path`, `sessionId`, `fileHistory()` | broad FTS |
| workflow, subagent, tool call, summary, edit | artifact | `workflows()`, `subagents()`, `summaries()`, `tool_calls`, `tool_results` | all-session search |
| concept, conclusion, design history, vague memory | semantic | `search()`, summaries, bounded facet sweep | session dumps |
| concept, conclusion, design history, vague memory | semantic | `memories({ query })`, `search()`, summaries, bounded facet sweep | session dumps |
One-shot retrieval is not all-shot retrieval. A query script may perform
multiple steps, but the first locator should be the narrowest semantic fit. If a
@@ -23,6 +23,7 @@ unless scoped evidence is insufficient and `query_plan` says why.
Project-like fields are distinct:
- `sessions.project`: stored Claude Code project slug.
- `memories.project`: stored project slug copied onto registered memory records.
- `sessions.project_path`: reconstructed absolute project path.
- `messages.cwd`: working directory at message time.
- helper `project`: SQL `LIKE` over `sessions.project`, not exact membership.
@@ -62,23 +63,27 @@ Use the database shape before asking the model to read text.
Ordering and context are semantic:
- `sessions()`, `summaries()`, `workflows()`, and `failures()` are newest first.
- `sessions()`, `memories()`, `summaries()`, `workflows()`, and `failures()` are newest first.
- `fileHistory()` is oldest first.
- `search().context` is temporal neighbors in one session, not causal context.
- `context(uuid)` and `trace(uuid)` are for parent-chain/causal expansion.
### Evidence Before Conclusion
Obelisk stores original structure, not precompiled claims. It has sessions,
messages, summaries, tool calls/results, files, subagents, workflows, parent
chains, and raw JSONL windows. It does not store "claim", "stance",
"contradiction", or "conclusion" entities.
Obelisk's raw session layer stores original structure, not precompiled claims:
sessions, messages, summaries, tool calls/results, files, subagents, workflows,
parent chains, and raw JSONL windows. The memory layer can store
human-approved markdown conclusions, but treat them as prior notes to compare
against raw evidence when correctness matters.
For semantic questions, build a task-local evidence view:
```js
{
query_plan: { mode, scope, facets, limits },
prior_memories: [
{ id, path, session_id, created_at, summary }
],
evidence: [
{ type, id, session_id, timestamp, facet, snippet }
],
@@ -86,8 +91,8 @@ For semantic questions, build a task-local evidence view:
}
```
Then synthesize the conclusion in the final answer. Do not pretend the evidence
view is a stored Obelisk entity.
Then synthesize the conclusion in the final answer. Do not pretend the raw
evidence view is itself a stored Obelisk entity.
## Text Search Semantics
+102 -4
View File
@@ -177,6 +177,28 @@ CREATE TABLE index_state (
);
```
### memories
Human-approved markdown memory records registered in Obelisk. The markdown
file at `path` is the durable memory content; `summary` is the compact retrieval
surface.
```sql
CREATE TABLE memories (
id TEXT PRIMARY KEY, -- memory record ID
session_id TEXT, -- FK -> sessions.id where the memory was drawn, if known
project TEXT, -- project slug used for scoped recall
message_start TEXT, -- first relevant message UUID, if known
message_end TEXT, -- last relevant message UUID, if known
path TEXT, -- normalized absolute markdown memory file path
summary TEXT, -- retrieval summary of the memory
created_at TEXT -- ISO 8601 registration time
);
```
Indexes: `idx_memories_project(project)`,
`idx_memories_session(session_id)`, `idx_memories_created(created_at)`.
### Key Relationships
```
@@ -185,8 +207,10 @@ sessions.id <-- tool_calls.session_id
sessions.id <-- tool_results.session_id
sessions.id <-- subagents.session_id
sessions.id <-- workflows.session_id
sessions.id <-- memories.session_id
messages.uuid <-- tool_calls.message_uuid
messages.uuid <-- tool_results.message_uuid
messages.uuid <-- memories.message_start / memories.message_end
messages.agent_id --> subagents.agent_id (for subagent messages)
messages.agent_id --> workflow_agents.agent_id (for workflow agent messages)
tool_calls.id <-- tool_results.tool_use_id
@@ -197,8 +221,9 @@ workflows.run_id <-- workflow_agents.run_id
## 2. Query API Reference
All functions are available as globals inside `--query` scripts.
Scripts run in an async IIFE with a 30-second timeout.
Read helpers are available as globals inside `--query` scripts. Memory write
helpers are available only inside `--remember` scripts. Scripts run in an async
IIFE with a 30-second timeout.
### Simple Layer
@@ -250,15 +275,18 @@ return { chain_length: c.parentChain.length, session_title: c.session?.title };
#### `sql(query, ...params)`
Raw SQL with parameterized bindings. Returns an array of row objects.
Read-only SQL with parameterized bindings. Returns an array of row objects.
| Param | Type | Description |
|-------|------|-------------|
| `query` | `string` | SQL SELECT statement |
| `query` | `string` | SQL SELECT/WITH statement |
| `...params` | `any` | Bind parameters (positional `?`) |
**Returns:** `Array<Object>` -- each row as `{ column: value }`.
Write statements are rejected. Use `--remember` and `remember()` for memory
registration after user approval.
```js
const rows = sql('SELECT id, title FROM sessions WHERE project = ? ORDER BY ended_at DESC LIMIT 5', 'Users-tomiya-Code-quiet-zero');
return rows;
@@ -412,6 +440,76 @@ const qz = sessions({ project: '%quiet-zero%', limit: 5 });
return qz.map(s => ({ title: s.title, branch: s.git_branch, ended: s.ended_at }));
```
#### `memories(opts?)`
Registered markdown memory records. Like other list helpers, passing a string
is treated as `sessionId`, and passing a number is treated as `limit`.
| Param | Type | Description |
|-------|------|-------------|
| `opts.query` | `string` | Term filter over `summary` and `path`; hyphens/underscores are treated as spaces |
| `opts.project` | `string` | SQL `LIKE` pattern over `memories.project` |
| `opts.sessionId` | `string` | Restrict to one source session |
| `opts.sessions` | `string[]` | Restrict to a set of source session IDs |
| `opts.after` | `string` | ISO 8601 lower bound on `created_at` |
| `opts.before` | `string` | ISO 8601 upper bound on `created_at` |
| `opts.branch` | `string` | Filter by source session git branch (exact match) |
| `opts.limit` | `number` | Max results (default 50) |
**Returns:** `Array<memory_row>` ordered by `created_at` descending.
`query` is a lightweight term filter, not FTS5 ranking. Use it to avoid pulling
all recent memories, then read the markdown file at `path` when a memory looks
relevant.
```js
const prior = memories({
project: '%quiet-zero%',
query: 'memory layer markdown',
limit: 5,
});
return prior.map(m => ({
id: m.id,
path: m.path,
session_id: m.session_id,
summary: m.summary?.slice(0, 240),
}));
```
#### `remember(record)`
Register a human-approved markdown memory file. This is a write helper, not a
recall helper; use it only after the user has approved writing memory. It is
available only in scripts run with `runtime.mjs --remember`.
`--remember` exposes only `remember()`, not `search()`, `sql()`, `memories()`,
or other retrieval helpers. If source IDs are unknown, find them first with a
normal `--query` script.
| Param | Type | Description |
|-------|------|-------------|
| `record.path` | `string` | Existing markdown file path. Relative paths resolve against the source session `project_path` when `session_id` is provided, otherwise against the runtime cwd |
| `record.summary` | `string` | Required retrieval summary: decision, reasoning, constraints |
| `record.session_id` | `string` | Source session ID, if known |
| `record.message_start` | `string` | First relevant source message UUID, if known |
| `record.message_end` | `string` | Last relevant source message UUID, if known |
| `record.project` | `string` | Project slug override. Defaults from `sessions.project` for `session_id` |
`remember()` validates that `path` exists and is a regular file. It stores the
normalized absolute path in `memories.path`.
**Returns:** `{ id, path, project, created_at }`.
```js
return remember({
path: '.obelisk/memories/memory-layer-design.md',
session_id: 'source-session-id',
message_start: 'first-message-uuid',
message_end: 'last-message-uuid',
summary: 'Decision: keep Obelisk as one user-facing entry that queries both memory and raw session evidence. Memory is prior notes, not final authority.',
});
```
---
## 3. Common Query Patterns
+7
View File
@@ -54,6 +54,13 @@ CREATE INDEX IF NOT EXISTS idx_sa_session ON subagents(session_id);
CREATE INDEX IF NOT EXISTS idx_wf_session ON workflows(session_id);
CREATE INDEX IF NOT EXISTS idx_wa_run ON workflow_agents(run_id);
CREATE INDEX IF NOT EXISTS idx_summaries_session ON summaries(session_id);
CREATE TABLE IF NOT EXISTS memories (
id TEXT PRIMARY KEY, session_id TEXT, project TEXT,
message_start TEXT, message_end TEXT,
path TEXT, summary TEXT, created_at TEXT);
CREATE INDEX IF NOT EXISTS idx_memories_project ON memories(project);
CREATE INDEX IF NOT EXISTS idx_memories_session ON memories(session_id);
CREATE INDEX IF NOT EXISTS idx_memories_created ON memories(created_at);
`;
function openDb() {
+74 -3
View File
@@ -24,8 +24,21 @@ function buildWhere(opts, aliases) {
const BASH_EXIT_PAT = 'Exit code %';
function assertReadOnlySql(sql) {
const text = String(sql || '').trim();
if (!/^(SELECT|WITH)\b/i.test(text)) {
throw new Error('sql() only supports read-only SELECT/WITH queries');
}
if (/\b(INSERT|UPDATE|DELETE|REPLACE|CREATE|DROP|ALTER|PRAGMA|VACUUM|ATTACH|DETACH)\b/i.test(text)) {
throw new Error('sql() only supports read-only SELECT/WITH queries');
}
}
function createQueryApi(db) {
const q = (sql, ...p) => db.prepare(sql).all(...p);
const q = (sql, ...p) => {
assertReadOnlySql(sql);
return db.prepare(sql).all(...p);
};
const search = (text, opts = {}) => {
const { limit = 20, sessionId, project, after, before, cwd } = opts;
@@ -213,7 +226,65 @@ function createQueryApi(db) {
};
};
return { sql: q, search, context, trace, thread, subagents, workflows, workflowTree, fileHistory, failures, sessions, recent, summaries, raw };
const memories = (optsOrSid) => {
const opts = normalizeOpts(optsOrSid);
const { limit = 50, query } = opts;
const needsJoin = opts.branch;
const { where: baseWhere, params } = buildWhere(opts, {
sessionId: 'mem.session_id',
project: 'mem.project',
timestamp: 'mem.created_at',
branch: 's.git_branch',
});
const terms = String(query || '')
.trim()
.replace(/[-_]/g, ' ')
.split(/\s+/)
.filter(Boolean);
let where = baseWhere;
for (const term of terms) {
where += " AND lower(coalesce(mem.summary,'') || ' ' || coalesce(mem.path,'')) LIKE ?";
params.push(`%${term.toLowerCase()}%`);
}
params.push(limit);
const join = needsJoin ? 'LEFT JOIN sessions s ON s.id=mem.session_id' : '';
return db.prepare(`SELECT mem.* FROM memories mem ${join} WHERE ${where} ORDER BY mem.created_at DESC LIMIT ?`).all(...params);
};
return { sql: q, search, context, trace, thread, subagents, workflows, workflowTree, fileHistory, failures, sessions, recent, summaries, raw, memories };
}
export { createQueryApi };
function createRememberApi(db) {
const resolveMemoryPath = (memoryPath, sessionId) => {
let base = null;
if (sessionId) {
base = db.prepare('SELECT project_path FROM sessions WHERE id=?').get(sessionId)?.project_path || null;
}
const resolved = path.isAbsolute(memoryPath)
? path.normalize(memoryPath)
: path.resolve(base || process.cwd(), memoryPath);
let stat;
try {
stat = fs.statSync(resolved);
} catch {
throw new Error(`remember() memory file does not exist: ${resolved}`);
}
if (!stat.isFile()) throw new Error(`remember() memory path is not a file: ${resolved}`);
return resolved;
};
const remember = ({ path: memoryPath, session_id, message_start, message_end, summary, project }) => {
if (!memoryPath || !summary) throw new Error('remember() requires path and summary');
const normalizedPath = resolveMemoryPath(memoryPath, session_id);
const id = `mem-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
const proj = project || db.prepare('SELECT project FROM sessions WHERE id=?').get(session_id)?.project || null;
const created_at = new Date().toISOString();
db.prepare('INSERT OR REPLACE INTO memories (id, session_id, project, message_start, message_end, path, summary, created_at) VALUES (?,?,?,?,?,?,?,?)').run(
id, session_id || null, proj, message_start || null, message_end || null, normalizedPath, summary, created_at);
return { id, path: normalizedPath, project: proj, created_at };
};
return { remember };
}
export { createQueryApi, createRememberApi };
+20 -4
View File
@@ -7,10 +7,9 @@ const vm = require('node:vm');
import { DB_PATH, openDb } from './db.mjs';
import { buildIndex } from './indexer.mjs';
import { createQueryApi } from './query.mjs';
import { createQueryApi, createRememberApi } from './query.mjs';
function executeQuery(db, scriptContent) {
const api = createQueryApi(db);
function executeScript(api, scriptContent) {
const sandbox = {
...api, JSON, Math, Array, Object, Set, Map, Date, RegExp,
parseInt, parseFloat, String, Number, Boolean, Error, Promise, console, setTimeout,
@@ -19,6 +18,14 @@ function executeQuery(db, scriptContent) {
return vm.runInNewContext(`(async()=>{${scriptContent}})()`, ctx, { timeout: 30000 });
}
function executeQuery(db, scriptContent) {
return executeScript(createQueryApi(db), scriptContent);
}
function executeRemember(db, scriptContent) {
return executeScript(createRememberApi(db), scriptContent);
}
function main() {
const args = process.argv.slice(2);
if (args[0] === '--build') {
@@ -42,7 +49,16 @@ function main() {
.catch(e => { process.stdout.write(JSON.stringify({ error: e.message, stack: e.stack }) + '\n'); db.close(); process.exitCode = 1; });
return;
}
process.stderr.write('Usage:\n node runtime.mjs --build\n node runtime.mjs --search "text"\n node runtime.mjs --query <file.js>\n');
if (args[0] === '--remember' && args[1]) {
buildIndex();
const db = openDb();
const script = fs.readFileSync(path.resolve(args[1]), 'utf8');
executeRemember(db, script)
.then(r => { process.stdout.write(JSON.stringify(r, null, 2) + '\n'); db.close(); })
.catch(e => { process.stdout.write(JSON.stringify({ error: e.message, stack: e.stack }) + '\n'); db.close(); process.exitCode = 1; });
return;
}
process.stderr.write('Usage:\n node runtime.mjs --build\n node runtime.mjs --search "text"\n node runtime.mjs --query <file.js>\n node runtime.mjs --remember <file.js>\n');
process.exitCode = 1;
}