oortmem 0.0.0-stage → 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.
Files changed (71) hide show
  1. package/LICENSE +202 -0
  2. package/README.md +21 -2
  3. package/dist/commands/connect.d.ts +14 -0
  4. package/dist/commands/connect.js +99 -0
  5. package/dist/commands/consolidate.d.ts +3 -0
  6. package/dist/commands/consolidate.js +21 -0
  7. package/dist/commands/doctor.d.ts +15 -0
  8. package/dist/commands/doctor.js +410 -0
  9. package/dist/commands/explain.d.ts +5 -0
  10. package/dist/commands/explain.js +67 -0
  11. package/dist/commands/export.d.ts +19 -0
  12. package/dist/commands/export.js +69 -0
  13. package/dist/commands/forget.d.ts +5 -0
  14. package/dist/commands/forget.js +17 -0
  15. package/dist/commands/hook.d.ts +12 -0
  16. package/dist/commands/hook.js +165 -0
  17. package/dist/commands/import.d.ts +23 -0
  18. package/dist/commands/import.js +67 -0
  19. package/dist/commands/init.d.ts +9 -0
  20. package/dist/commands/init.js +86 -0
  21. package/dist/commands/inspect.d.ts +26 -0
  22. package/dist/commands/inspect.js +126 -0
  23. package/dist/commands/install.d.ts +28 -0
  24. package/dist/commands/install.js +122 -0
  25. package/dist/commands/keys.d.ts +16 -0
  26. package/dist/commands/keys.js +69 -0
  27. package/dist/commands/login.d.ts +74 -0
  28. package/dist/commands/login.js +326 -0
  29. package/dist/commands/mcp.d.ts +14 -0
  30. package/dist/commands/mcp.js +42 -0
  31. package/dist/commands/observe.d.ts +15 -0
  32. package/dist/commands/observe.js +73 -0
  33. package/dist/commands/pin.d.ts +3 -0
  34. package/dist/commands/pin.js +16 -0
  35. package/dist/commands/project.d.ts +15 -0
  36. package/dist/commands/project.js +60 -0
  37. package/dist/commands/recall.d.ts +11 -0
  38. package/dist/commands/recall.js +46 -0
  39. package/dist/commands/setup.d.ts +50 -0
  40. package/dist/commands/setup.js +532 -0
  41. package/dist/commands/stats.d.ts +25 -0
  42. package/dist/commands/stats.js +113 -0
  43. package/dist/connectors.d.ts +83 -0
  44. package/dist/connectors.js +615 -0
  45. package/dist/credentials-file.d.ts +37 -0
  46. package/dist/credentials-file.js +94 -0
  47. package/dist/credentials.d.ts +47 -0
  48. package/dist/credentials.js +46 -0
  49. package/dist/embed-cache-disk.d.ts +13 -0
  50. package/dist/embed-cache-disk.js +81 -0
  51. package/dist/io.d.ts +36 -0
  52. package/dist/io.js +117 -0
  53. package/dist/keychain.d.ts +70 -0
  54. package/dist/keychain.js +218 -0
  55. package/dist/main.d.ts +2 -0
  56. package/dist/main.js +28 -0
  57. package/dist/mcp.d.ts +17 -0
  58. package/dist/mcp.js +168 -0
  59. package/dist/program.d.ts +14 -0
  60. package/dist/program.js +224 -0
  61. package/dist/relay.d.ts +57 -0
  62. package/dist/relay.js +119 -0
  63. package/dist/remember.d.ts +27 -0
  64. package/dist/remember.js +42 -0
  65. package/dist/ui.d.ts +42 -0
  66. package/dist/ui.js +63 -0
  67. package/dist/util.d.ts +78 -0
  68. package/dist/util.js +202 -0
  69. package/dist/wire.d.ts +65 -0
  70. package/dist/wire.js +257 -0
  71. package/package.json +67 -3
@@ -0,0 +1,410 @@
1
+ import * as fs from 'node:fs';
2
+ import * as path from 'node:path';
3
+ import { filterFor, hookStatus } from '@oortmem/claude-code';
4
+ import { resolveConfig, resolveHome, localGrant } from '@oortmem/core';
5
+ import { HOSTS, HOST_LABEL, detectHosts, inspectHost } from '../connectors.js';
6
+ import { ROLES, ROLE_VENDOR, keyLookup, routeFor } from '../credentials.js';
7
+ import { describeProblem, inspectCredentialsFile } from '../credentials-file.js';
8
+ import { PROVIDER_ENV } from '../keychain.js';
9
+ import { RELAY_MODELS, relayUrl } from '../relay.js';
10
+ import { uiFor } from '../ui.js';
11
+ import { Refusal, echo, tilde, withWired } from '../util.js';
12
+ import { hostContext } from './connect.js';
13
+ import { hookCommandPath, settingsFile } from './install.js';
14
+ const ROLE_NEED = {
15
+ embed: 'recall is lexical-only',
16
+ rerank: 'recall is not reranked',
17
+ decide: 'nothing new is saved',
18
+ write: 'nothing new is saved',
19
+ };
20
+ const CONFIG_KEY = {
21
+ embed: 'embed',
22
+ rerank: 'rerank',
23
+ decide: 'decide',
24
+ write: 'writer',
25
+ };
26
+ /** Keys: which one serves each role (presence only), and whether the relay can serve it. */
27
+ async function keyChecks(ctx) {
28
+ const env = ctx.io.env;
29
+ const lookup = keyLookup(env, ctx.keychain.read);
30
+ let config;
31
+ try {
32
+ config = resolveConfig(ctx.io.cwd, env);
33
+ }
34
+ catch {
35
+ config = undefined; // the store check reports an invalid config
36
+ }
37
+ const checks = [];
38
+ let relayed = false;
39
+ for (const role of ROLES) {
40
+ const r = await routeFor(role, lookup);
41
+ const where = (source, provider) => source === 'env'
42
+ ? PROVIDER_ENV[provider]
43
+ : ctx.keychain.where(provider);
44
+ if (r.via === 'vendor') {
45
+ checks.push({
46
+ group: 'keys',
47
+ name: role,
48
+ status: 'ok',
49
+ detail: `own ${r.provider} key (${where(r.source, r.provider)})`,
50
+ });
51
+ continue;
52
+ }
53
+ if (r.via === 'none') {
54
+ checks.push({
55
+ group: 'keys',
56
+ name: role,
57
+ status: 'warn',
58
+ detail: `no ${r.provider} key and no Oort key, so ${ROLE_NEED[role]}`,
59
+ fix: ctx.keychain.supported
60
+ ? `oort keys set ${r.provider}, or oort login (Oort Pro)`
61
+ : `export ${PROVIDER_ENV[r.provider]}=..., or OORT_API_KEY=... from oortcloud.dev/account (Oort Pro)`,
62
+ });
63
+ continue;
64
+ }
65
+ relayed = true;
66
+ const allowed = RELAY_MODELS[role];
67
+ const model = config?.[CONFIG_KEY[role]];
68
+ if (allowed !== undefined && model?.model !== undefined && !allowed.includes(model.model)) {
69
+ checks.push({
70
+ group: 'keys',
71
+ name: role,
72
+ status: 'fail',
73
+ detail: `${CONFIG_KEY[role]}.model ${echo(model.model)} is not served by Oort Pro (it serves ${allowed.join(', ')})`,
74
+ fix: `use ${allowed[0]} in ~/.oort/config.json, or set ${PROVIDER_ENV[ROLE_VENDOR[role]]}`,
75
+ });
76
+ continue;
77
+ }
78
+ checks.push({
79
+ group: 'keys',
80
+ name: role,
81
+ status: 'ok',
82
+ detail: `Oort Pro (${where(r.source, 'oort')}), through the relay`,
83
+ });
84
+ }
85
+ if (ctx.keychain.kind === 'file') {
86
+ const file = ctx.keychain.where('oort');
87
+ const state = inspectCredentialsFile(file);
88
+ const problem = describeProblem(file, state);
89
+ if (problem !== undefined) {
90
+ checks.push({
91
+ group: 'keys',
92
+ name: 'credentials',
93
+ status: 'fail',
94
+ detail: problem.split(' Fix:')[0] ?? problem,
95
+ fix: state.state === 'wide' ? `chmod 600 ${file}` : `fix or delete ${file}`,
96
+ });
97
+ }
98
+ }
99
+ return { checks, relayed };
100
+ }
101
+ /** The Claude Code hooks in the user settings file. */
102
+ function hookChecks(ctx, cmd, home) {
103
+ const file = settingsFile(false, ctx);
104
+ const show = tilde(file, home);
105
+ if (!fs.existsSync(path.join(home, '.claude'))) {
106
+ return [
107
+ {
108
+ group: 'hooks',
109
+ name: 'claude-code',
110
+ status: 'info',
111
+ detail: 'Claude Code not found (~/.claude)',
112
+ },
113
+ ];
114
+ }
115
+ let status;
116
+ try {
117
+ status = hookStatus(file, cmd);
118
+ }
119
+ catch (e) {
120
+ return [
121
+ {
122
+ group: 'hooks',
123
+ name: 'claude-code',
124
+ status: 'fail',
125
+ detail: e.message.split('\n')[0] ?? 'unreadable settings',
126
+ fix: `fix the JSON in ${show}, then oort install claude-code`,
127
+ },
128
+ ];
129
+ }
130
+ const found = status.events.filter((e) => e.state !== 'missing');
131
+ if (found.length === 0) {
132
+ return [
133
+ {
134
+ group: 'hooks',
135
+ name: 'claude-code',
136
+ status: 'info',
137
+ detail: `not installed in ${show}`,
138
+ fix: 'oort install claude-code',
139
+ },
140
+ ];
141
+ }
142
+ const broken = found.filter((e) => e.command !== undefined && !isExecutable(e.command) && e.command !== 'oort');
143
+ const missing = status.events.filter((e) => e.state === 'missing').map((e) => e.event);
144
+ const stale = found.filter((e) => e.state === 'stale');
145
+ if (broken.length > 0) {
146
+ return [
147
+ {
148
+ group: 'hooks',
149
+ name: 'claude-code',
150
+ status: 'fail',
151
+ detail: `${broken.map((e) => e.event).join(', ')} run ${broken[0]?.command ?? ''}, which is not an executable file`,
152
+ fix: 'oort install claude-code',
153
+ },
154
+ ];
155
+ }
156
+ if (missing.length > 0 || stale.length > 0) {
157
+ return [
158
+ {
159
+ group: 'hooks',
160
+ name: 'claude-code',
161
+ status: 'warn',
162
+ detail: missing.length > 0
163
+ ? `${missing.join(', ')} missing in ${show}`
164
+ : `installed from another path (${stale[0]?.command ?? ''})`,
165
+ fix: 'oort install claude-code',
166
+ },
167
+ ];
168
+ }
169
+ return [
170
+ {
171
+ group: 'hooks',
172
+ name: 'claude-code',
173
+ status: 'ok',
174
+ detail: `${found.map((e) => e.event).join(', ')} in ${show}`,
175
+ },
176
+ ];
177
+ }
178
+ function isExecutable(file) {
179
+ try {
180
+ fs.accessSync(file, fs.constants.X_OK);
181
+ return fs.statSync(file).isFile();
182
+ }
183
+ catch {
184
+ return false;
185
+ }
186
+ }
187
+ /** The MCP entry in every host's config. */
188
+ function mcpChecks(ctx, cmd) {
189
+ const hc = hostContext(ctx);
190
+ const detected = new Set(detectHosts(hc));
191
+ return HOSTS.map((host) => {
192
+ const s = inspectHost(host, hc, cmd);
193
+ const show = tilde(s.file, hc.home);
194
+ const base = { group: 'mcp', name: host };
195
+ switch (s.state) {
196
+ case 'connected':
197
+ return { ...base, status: 'ok', detail: `${HOST_LABEL[host]} starts oort mcp (${show})` };
198
+ case 'outdated':
199
+ return {
200
+ ...base,
201
+ status: 'warn',
202
+ detail: `Oort's entry in ${show} is from another install path or an older version`,
203
+ fix: `oort connect ${host}`,
204
+ };
205
+ case 'broken':
206
+ return {
207
+ ...base,
208
+ status: 'fail',
209
+ detail: `Oort's entry in ${show} runs a command that is not an executable file`,
210
+ fix: `oort connect ${host}`,
211
+ };
212
+ case 'foreign':
213
+ return {
214
+ ...base,
215
+ status: 'info',
216
+ detail: `${show} has an "oort" server Oort did not write; left alone`,
217
+ };
218
+ case 'invalid':
219
+ return {
220
+ ...base,
221
+ status: 'fail',
222
+ detail: (s.message ?? `${show} cannot be read`).replace(s.file, show),
223
+ fix: `fix ${show} by hand (Oort will not edit it until it parses)`,
224
+ };
225
+ case 'absent':
226
+ return detected.has(host)
227
+ ? {
228
+ ...base,
229
+ status: 'info',
230
+ detail: host === 'claude-code'
231
+ ? 'MCP server not added (the hooks work without it)'
232
+ : `${HOST_LABEL[host]} found, not connected`,
233
+ fix: `oort connect ${host}`,
234
+ }
235
+ : { ...base, status: 'info', detail: `${HOST_LABEL[host]} not found` };
236
+ }
237
+ });
238
+ }
239
+ /** The local store: present, private, and readable. It is never created here. */
240
+ async function storeChecks(ctx) {
241
+ const home = resolveHome(ctx.io.env);
242
+ const userHome = ctx.io.env['HOME'] ?? '';
243
+ const show = tilde(home, userHome);
244
+ const db = path.join(home, 'oort.sqlite');
245
+ if (!fs.existsSync(db)) {
246
+ return [
247
+ {
248
+ group: 'store',
249
+ name: 'store',
250
+ status: 'info',
251
+ detail: `no store yet in ${show}; the first memory (or oort setup) creates it`,
252
+ },
253
+ ];
254
+ }
255
+ const checks = [];
256
+ try {
257
+ const count = await withWired(ctx, async (w) => {
258
+ const all = await w.store.list(localGrant(filterFor(w.scope)), { validOnly: false });
259
+ return all.length;
260
+ });
261
+ checks.push({
262
+ group: 'store',
263
+ name: 'store',
264
+ status: 'ok',
265
+ detail: `${tilde(db, userHome)} opens; ${count} ${count === 1 ? 'memory' : 'memories'} visible from here`,
266
+ });
267
+ }
268
+ catch (e) {
269
+ checks.push({
270
+ group: 'store',
271
+ name: 'store',
272
+ status: 'fail',
273
+ detail: `${tilde(db, userHome)} does not open: ${e.message.split('\n')[0] ?? ''}`,
274
+ fix: `close other oort processes and run oort stats; if it still fails, move ${show} aside`,
275
+ });
276
+ }
277
+ if (process.platform !== 'win32') {
278
+ const mode = fs.statSync(home).mode & 0o777;
279
+ if ((mode & 0o077) !== 0) {
280
+ checks.push({
281
+ group: 'store',
282
+ name: 'permissions',
283
+ status: 'warn',
284
+ detail: `${show} is mode ${mode.toString(8)}, so other users can read your memories`,
285
+ fix: `chmod 700 ${show}`,
286
+ });
287
+ }
288
+ }
289
+ return checks;
290
+ }
291
+ const PROBE_TIMEOUT_MS = 3000;
292
+ /** GET `<relay>/health` with no credentials; true only for 200 `{"ok":true}`. */
293
+ async function probeRelay(url, doFetch) {
294
+ const abort = new AbortController();
295
+ const timer = setTimeout(() => abort.abort(), PROBE_TIMEOUT_MS);
296
+ try {
297
+ const res = await doFetch(`${url}/health`, { signal: abort.signal, redirect: 'error' });
298
+ if (!res.ok) {
299
+ void res.body?.cancel().catch(() => undefined);
300
+ return false;
301
+ }
302
+ const body = await res.json();
303
+ return body?.ok === true;
304
+ }
305
+ catch {
306
+ return false;
307
+ }
308
+ finally {
309
+ clearTimeout(timer);
310
+ }
311
+ }
312
+ /** The relay: its URL, and, when a role goes through it, whether `/health` answers within 3 s. */
313
+ async function relayChecks(ctx, relayed) {
314
+ const r = relayUrl(ctx.io.env);
315
+ if ('error' in r) {
316
+ return [
317
+ {
318
+ group: 'relay',
319
+ name: 'relay',
320
+ status: relayed ? 'fail' : 'warn',
321
+ detail: r.error,
322
+ fix: 'unset OORT_RELAY_URL (or set it to an https URL, or http://localhost for wrangler dev)',
323
+ },
324
+ ];
325
+ }
326
+ if (!relayed) {
327
+ return [
328
+ {
329
+ group: 'relay',
330
+ name: 'relay',
331
+ status: 'info',
332
+ detail: `not used: every role has its own key or none (${r.url})`,
333
+ },
334
+ ];
335
+ }
336
+ const up = await probeRelay(r.url, ctx.fetch ?? globalThis.fetch);
337
+ return [
338
+ up
339
+ ? { group: 'relay', name: 'relay', status: 'ok', detail: `${r.url} reachable` }
340
+ : {
341
+ group: 'relay',
342
+ name: 'relay',
343
+ status: 'warn',
344
+ detail: `${r.url} unreachable (no healthy answer from /health within 3 s)`,
345
+ fix: 'check your network and proxy; if it persists, see status at oortcloud.dev, or set your own keys with oort keys set <provider>',
346
+ },
347
+ ];
348
+ }
349
+ const GLYPH = {
350
+ ok: '✓',
351
+ warn: '!',
352
+ fail: '✗',
353
+ info: '·',
354
+ unknown: '?',
355
+ };
356
+ function paint(ui, status, s) {
357
+ return status === 'ok'
358
+ ? ui.green(s)
359
+ : status === 'warn'
360
+ ? ui.yellow(s)
361
+ : status === 'fail'
362
+ ? ui.red(s)
363
+ : ui.dim(s);
364
+ }
365
+ /** `oort doctor [--json]`: every check, with a one-line fix per problem. Exits 1 if one fails. */
366
+ export async function doctorCommand(opts, ctx) {
367
+ const home = ctx.io.env['HOME'] ?? '';
368
+ let cmd;
369
+ try {
370
+ cmd = hookCommandPath(ctx.io.entry);
371
+ }
372
+ catch (e) {
373
+ if (!(e instanceof Refusal))
374
+ throw e;
375
+ cmd = undefined; // not the installed bin: entries are judged on their own paths
376
+ }
377
+ const keys = await keyChecks(ctx);
378
+ const checks = [
379
+ ...keys.checks,
380
+ ...hookChecks(ctx, cmd, home),
381
+ ...mcpChecks(ctx, cmd),
382
+ ...(await storeChecks(ctx)),
383
+ ...(await relayChecks(ctx, keys.relayed)),
384
+ ];
385
+ const ok = !checks.some((c) => c.status === 'fail');
386
+ if (opts.json === true) {
387
+ ctx.io.out(`${JSON.stringify({ ok, checks }, null, 2)}\n`);
388
+ return ok ? 0 : 1;
389
+ }
390
+ const ui = uiFor(ctx.io);
391
+ const problems = checks.filter((c) => c.status === 'warn' || c.status === 'fail').length;
392
+ ctx.io.out(ui.header('doctor', problems === 0 ? 'no problems' : `${problems} to fix`));
393
+ for (const group of ['keys', 'hooks', 'mcp', 'store', 'relay']) {
394
+ const rows = checks.filter((c) => c.group === group);
395
+ ctx.io.out(`${ui.bold(group)}\n`);
396
+ ctx.io.out(ui.kv(rows.flatMap((c) => [
397
+ [`${paint(ui, c.status, GLYPH[c.status])} ${c.name}`, c.detail],
398
+ ...(c.fix !== undefined
399
+ ? [
400
+ [
401
+ '',
402
+ ui.dim(`${c.status === 'warn' || c.status === 'fail' ? 'fix' : 'try'}: ${c.fix}`),
403
+ ],
404
+ ]
405
+ : []),
406
+ ]), { indent: 2 }));
407
+ }
408
+ return ok ? 0 : 1;
409
+ }
410
+ //# sourceMappingURL=doctor.js.map
@@ -0,0 +1,5 @@
1
+ import { type Ctx } from '../util.js';
2
+ /** `oort explain "<query>" [--json]`: recall, with each stage's rank and score per memory. */
3
+ export declare function explainCommand(query: string, opts: {
4
+ json?: boolean;
5
+ }, ctx: Ctx): Promise<number>;
@@ -0,0 +1,67 @@
1
+ import { uiFor } from '../ui.js';
2
+ import { age, clip, degradedHint, fixed, scopeLabel, short, withWired } from '../util.js';
3
+ import { runRecall } from './recall.js';
4
+ /** `oort explain "<query>" [--json]`: recall, with each stage's rank and score per memory. */
5
+ export async function explainCommand(query, opts, ctx) {
6
+ return withWired(ctx, async (w) => {
7
+ const now = new Date();
8
+ const result = await runRecall(w, query, undefined, now);
9
+ degradedHint(ctx.io, w, result.degraded);
10
+ if (opts.json === true) {
11
+ const memories = result.memories.map((m, i) => ({
12
+ rank: i + 1,
13
+ id: m.id,
14
+ type: m.type,
15
+ tier: m.tier,
16
+ validFrom: m.validFrom,
17
+ text: m.text,
18
+ explanation: m.explanation,
19
+ }));
20
+ const out = {
21
+ memories,
22
+ timings: result.timings,
23
+ ...(result.degraded !== undefined && { degraded: result.degraded }),
24
+ };
25
+ ctx.io.out(`${JSON.stringify(out, null, 2)}\n`);
26
+ return 0;
27
+ }
28
+ const ui = uiFor(ctx.io);
29
+ const rows = result.memories.map((m, i) => {
30
+ const e = m.explanation;
31
+ return [
32
+ String(i + 1),
33
+ short(m.id),
34
+ m.type,
35
+ ui.tier(m.tier),
36
+ age(m.validFrom, now),
37
+ e.lexicalRank === undefined ? '-' : String(e.lexicalRank),
38
+ e.denseRank === undefined ? '-' : String(e.denseRank),
39
+ fixed(e.rrf, 4),
40
+ fixed(e.fused, 4),
41
+ fixed(e.reranked, 3),
42
+ fixed(e.activation),
43
+ e.hydrated ? 'yes' : 'no',
44
+ clip(m.text, 80),
45
+ ];
46
+ });
47
+ const t = result.timings;
48
+ ctx.io.out(ui.header('explain', `${result.memories.length} memories in ${fixed(t['totalMs'], 1)} ms`, scopeLabel(w), ...(result.degraded !== undefined ? [`degraded: ${result.degraded.join(', ')}`] : [])));
49
+ ctx.io.out(ui.table([
50
+ 'rank',
51
+ 'id',
52
+ 'type',
53
+ 'tier',
54
+ 'age',
55
+ 'lexical',
56
+ 'dense',
57
+ 'rrf',
58
+ 'fused',
59
+ 'reranked',
60
+ 'activation',
61
+ 'hydrated',
62
+ 'text',
63
+ ], rows));
64
+ return 0;
65
+ });
66
+ }
67
+ //# sourceMappingURL=explain.js.map
@@ -0,0 +1,19 @@
1
+ import { type Scope } from '@oortmem/core';
2
+ import { type Ctx } from '../util.js';
3
+ /** Version of the export format documented in docs/export-format.md. */
4
+ export declare const EXPORT_VERSION = 1;
5
+ export interface ExportManifest {
6
+ oort: 'export';
7
+ version: typeof EXPORT_VERSION;
8
+ exportedAt: string;
9
+ memories: number;
10
+ scopes: Scope[];
11
+ }
12
+ /**
13
+ * `oort export [--out <file>]`: the v1 export format, a manifest line and one
14
+ * JSON line per memory (every tier and validity state) visible from the cwd.
15
+ */
16
+ export declare function exportCommand(opts: {
17
+ out?: string;
18
+ force?: boolean;
19
+ }, ctx: Ctx): Promise<number>;
@@ -0,0 +1,69 @@
1
+ import * as path from 'node:path';
2
+ import { filterFor } from '@oortmem/claude-code';
3
+ import { localGrant } from '@oortmem/core';
4
+ import { shapeLinks, toJson, withWired, writeOwnerOnly } from '../util.js';
5
+ /** Version of the export format documented in docs/export-format.md. */
6
+ export const EXPORT_VERSION = 1;
7
+ /** A cold memory's main record has only its stub indexed; the cold copy holds the full text and embedding. */
8
+ async function decoded(m, store, grant, ctx) {
9
+ if (m.tier !== 'cold')
10
+ return m;
11
+ let blob;
12
+ try {
13
+ blob = await store.coldGet(grant, m.id);
14
+ }
15
+ catch (e) {
16
+ // One unreadable blob must not block a backup: export what the main record holds.
17
+ ctx.io.err(`oort: cold copy of ${m.id} is unreadable (${e.name}); exported its stub only.\n`);
18
+ return m;
19
+ }
20
+ if (blob === null)
21
+ return m;
22
+ const { embedding: _none, ...live } = m;
23
+ return {
24
+ ...live,
25
+ text: blob.text,
26
+ stub: blob.stub,
27
+ entities: blob.entities,
28
+ ...(blob.embedding !== undefined && { embedding: blob.embedding }),
29
+ };
30
+ }
31
+ const scopeKey = (s) => JSON.stringify(Object.entries(s).sort());
32
+ /**
33
+ * `oort export [--out <file>]`: the v1 export format, a manifest line and one
34
+ * JSON line per memory (every tier and validity state) visible from the cwd.
35
+ */
36
+ export async function exportCommand(opts, ctx) {
37
+ return withWired(ctx, async (w) => {
38
+ const filter = filterFor(w.scope);
39
+ const grant = localGrant(filter);
40
+ const listed = await w.store.list(grant, { validOnly: false });
41
+ // Oldest first, so an importer replays supersede chains in order.
42
+ listed.sort((a, b) => Date.parse(a.createdAt) - Date.parse(b.createdAt) || (a.id < b.id ? -1 : 1));
43
+ const memories = [];
44
+ for (const m of listed) {
45
+ memories.push(await shapeLinks(w.store, filter, await decoded(m, w.store, grant, ctx)));
46
+ }
47
+ const scopes = new Map();
48
+ for (const m of memories)
49
+ scopes.set(scopeKey(m.scope), m.scope);
50
+ const manifest = {
51
+ oort: 'export',
52
+ version: EXPORT_VERSION,
53
+ exportedAt: new Date().toISOString(),
54
+ memories: memories.length,
55
+ scopes: [...scopes.values()],
56
+ };
57
+ const data = [manifest, ...memories.map(toJson)].map((x) => `${JSON.stringify(x)}\n`).join('');
58
+ if (opts.out === undefined) {
59
+ ctx.io.out(data);
60
+ }
61
+ else {
62
+ const file = path.resolve(ctx.io.cwd, opts.out);
63
+ writeOwnerOnly(file, data, opts.force === true);
64
+ ctx.io.out(`Exported ${memories.length} memories to ${file}\n`);
65
+ }
66
+ return 0;
67
+ });
68
+ }
69
+ //# sourceMappingURL=export.js.map
@@ -0,0 +1,5 @@
1
+ import { type Ctx } from '../util.js';
2
+ /** `oort forget <id> [--yes]`: deletes a memory everywhere (main record and cold copy). */
3
+ export declare function forgetCommand(id: string, opts: {
4
+ yes?: boolean;
5
+ }, ctx: Ctx): Promise<number>;
@@ -0,0 +1,17 @@
1
+ import { filterFor, grantFor } from '@oortmem/claude-code';
2
+ import { clip, confirm, findMemory, short, withWired } from '../util.js';
3
+ /** `oort forget <id> [--yes]`: deletes a memory everywhere (main record and cold copy). */
4
+ export async function forgetCommand(id, opts, ctx) {
5
+ return withWired(ctx, async (w) => {
6
+ const m = await findMemory(w.store, filterFor(w.scope), id);
7
+ const ok = await confirm(ctx.io, `Forget ${short(m.id)} "${clip(m.text, 60)}"?`, opts.yes === true, 'forget it');
8
+ if (!ok) {
9
+ ctx.io.out('Kept.\n');
10
+ return 0;
11
+ }
12
+ await w.store.forget(grantFor(w.scope), m.id);
13
+ ctx.io.out(`Forgot ${m.id}.\n`);
14
+ return 0;
15
+ });
16
+ }
17
+ //# sourceMappingURL=forget.js.map
@@ -0,0 +1,12 @@
1
+ import { type HookEvent } from '@oortmem/claude-code';
2
+ import { type Ctx } from '../util.js';
3
+ export declare const HOOK_COMMAND_EVENTS: readonly HookEvent[];
4
+ /** Stdin is read for at most this long; Claude Code writes it at once and closes it. */
5
+ export declare const HOOK_STDIN_CAP_MS = 1000;
6
+ /**
7
+ * A hook waits at most this long for the store's write lock (held, say, by a
8
+ * drain). SQLite's busy wait is synchronous: it freezes the event loop, the
9
+ * deadline timer included, so it must stay short. The drain keeps the default.
10
+ */
11
+ export declare const HOOK_BUSY_TIMEOUT_MS = 250;
12
+ export declare function hookCommand(event: string, ctx: Ctx): Promise<void>;