oortmem 0.1.0 → 0.1.1
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/dist/cloud-remote.d.ts +9 -0
- package/dist/cloud-remote.js +72 -0
- package/dist/cloud.d.ts +139 -0
- package/dist/cloud.js +223 -0
- package/dist/commands/cloud.d.ts +34 -0
- package/dist/commands/cloud.js +263 -0
- package/dist/commands/consolidate.js +4 -1
- package/dist/commands/doctor.d.ts +1 -1
- package/dist/commands/doctor.js +73 -1
- package/dist/commands/export.d.ts +4 -1
- package/dist/commands/export.js +17 -3
- package/dist/commands/forget.js +10 -3
- package/dist/commands/hook.js +5 -1
- package/dist/commands/import.js +3 -0
- package/dist/commands/inspect.js +46 -6
- package/dist/commands/observe.js +16 -1
- package/dist/commands/pin.js +13 -1
- package/dist/commands/recall.js +14 -2
- package/dist/commands/stats.js +31 -1
- package/dist/mcp.d.ts +9 -0
- package/dist/mcp.js +135 -2
- package/dist/program.js +26 -0
- package/dist/util.d.ts +6 -0
- package/dist/util.js +11 -0
- package/dist/wire.d.ts +13 -1
- package/dist/wire.js +24 -0
- package/package.json +9 -9
|
@@ -0,0 +1,263 @@
|
|
|
1
|
+
import * as path from 'node:path';
|
|
2
|
+
import { principalGrant } from '@oortmem/core';
|
|
3
|
+
import { OortCloudError, cloudUrl, createCloudClient, readCloudSettings, writeCloudSettings, } from '../cloud.js';
|
|
4
|
+
import { keyLookup } from '../credentials.js';
|
|
5
|
+
import { uiFor } from '../ui.js';
|
|
6
|
+
import { Refusal, confirm, toJson, withWired, writeOwnerOnly } from '../util.js';
|
|
7
|
+
import { EXPORT_VERSION, decoded, scopeKey } from './export.js';
|
|
8
|
+
/** One import request: at most this many memory lines and bytes (the API takes 1,000 and 10 MB). */
|
|
9
|
+
export const IMPORT_CHUNK_LINES = 500;
|
|
10
|
+
export const IMPORT_CHUNK_BYTES = 8 * 1024 * 1024;
|
|
11
|
+
/** A client for this machine's Oort key, whichever store mode is set. */
|
|
12
|
+
async function clientFor(ctx) {
|
|
13
|
+
const url = cloudUrl(ctx.io.env);
|
|
14
|
+
if ('error' in url)
|
|
15
|
+
throw new Refusal(url.error);
|
|
16
|
+
const key = await keyLookup(ctx.io.env, ctx.keychain.read)('oort');
|
|
17
|
+
if (key === undefined) {
|
|
18
|
+
throw new Refusal('Oort Cloud needs the Oort key: run `oort login` first.');
|
|
19
|
+
}
|
|
20
|
+
return createCloudClient({
|
|
21
|
+
url: url.url,
|
|
22
|
+
key: key.key,
|
|
23
|
+
...(ctx.fetch !== undefined && { fetch: ctx.fetch }),
|
|
24
|
+
});
|
|
25
|
+
}
|
|
26
|
+
/** Every memory in the local store (all projects of this machine's principal), oldest first. */
|
|
27
|
+
async function localExport(w, ctx) {
|
|
28
|
+
const s = w.scope;
|
|
29
|
+
const grant = principalGrant({ orgId: s.orgId, customerId: s.customerId, agentId: s.agentId });
|
|
30
|
+
const all = await w.store.list(grant, { validOnly: false });
|
|
31
|
+
all.sort((a, b) => Date.parse(a.createdAt) - Date.parse(b.createdAt) || (a.id < b.id ? -1 : 1));
|
|
32
|
+
const lines = [];
|
|
33
|
+
const scopes = new Map();
|
|
34
|
+
let withEmbedding = 0;
|
|
35
|
+
for (const m of all) {
|
|
36
|
+
const full = await decoded(m, w.store, grant, ctx);
|
|
37
|
+
if (full.embedding !== undefined && full.embedding.length === w.config.embed.dimension)
|
|
38
|
+
withEmbedding++;
|
|
39
|
+
scopes.set(scopeKey(full.scope), full.scope);
|
|
40
|
+
lines.push(JSON.stringify(toJson(full)));
|
|
41
|
+
}
|
|
42
|
+
const manifest = {
|
|
43
|
+
oort: 'export',
|
|
44
|
+
version: EXPORT_VERSION,
|
|
45
|
+
exportedAt: new Date().toISOString(),
|
|
46
|
+
memories: lines.length,
|
|
47
|
+
scopes: [...scopes.values()],
|
|
48
|
+
};
|
|
49
|
+
return { manifest, lines, withEmbedding };
|
|
50
|
+
}
|
|
51
|
+
/** Chunks of export lines, each within the line and byte caps. */
|
|
52
|
+
function chunks(lines, from) {
|
|
53
|
+
const out = [];
|
|
54
|
+
let cur = [];
|
|
55
|
+
let bytes = 0;
|
|
56
|
+
let start = from;
|
|
57
|
+
for (let i = from; i < lines.length; i++) {
|
|
58
|
+
const line = lines[i];
|
|
59
|
+
const b = Buffer.byteLength(line) + 1;
|
|
60
|
+
if (cur.length > 0 && (cur.length >= IMPORT_CHUNK_LINES || bytes + b > IMPORT_CHUNK_BYTES)) {
|
|
61
|
+
out.push({ start, lines: cur });
|
|
62
|
+
cur = [];
|
|
63
|
+
bytes = 0;
|
|
64
|
+
start = i;
|
|
65
|
+
}
|
|
66
|
+
cur.push(line);
|
|
67
|
+
bytes += b;
|
|
68
|
+
}
|
|
69
|
+
if (cur.length > 0)
|
|
70
|
+
out.push({ start, lines: cur });
|
|
71
|
+
return out;
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* Uploads one chunk. The API imports at most 1,000 lines a call and answers `next` (an index into
|
|
75
|
+
* the posted lines) when it stopped early; the same lines are posted again with `from = next`
|
|
76
|
+
* until it is done. `progress` gets how many of the chunk's lines are in after each call.
|
|
77
|
+
*/
|
|
78
|
+
export async function uploadChunk(client, header, lines, progress) {
|
|
79
|
+
const body = [header, ...lines];
|
|
80
|
+
const total = { imported: 0, skipped: 0, reembedded: 0, calls: 0 };
|
|
81
|
+
let from = 0;
|
|
82
|
+
for (;;) {
|
|
83
|
+
const r = await client.importLines(body, from);
|
|
84
|
+
total.calls++;
|
|
85
|
+
total.imported += r.imported;
|
|
86
|
+
total.skipped += r.skipped;
|
|
87
|
+
total.reembedded += r.reembedded;
|
|
88
|
+
if (r.next === undefined) {
|
|
89
|
+
progress(lines.length);
|
|
90
|
+
return total;
|
|
91
|
+
}
|
|
92
|
+
// Line 0 is the manifest, so line `next` of the body is line `next - 1` of the chunk.
|
|
93
|
+
if (!Number.isInteger(r.next) || r.next <= Math.max(from, 1) || r.next > lines.length) {
|
|
94
|
+
throw new Refusal('Oort Cloud stopped making progress on the import; run it again later.');
|
|
95
|
+
}
|
|
96
|
+
progress(r.next - 1);
|
|
97
|
+
from = r.next;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
const fingerprintOf = (lines) => {
|
|
101
|
+
const id = (l) => l === undefined ? '' : (JSON.parse(l).id ?? '');
|
|
102
|
+
return `${lines.length}:${id(lines[0])}:${id(lines.at(-1))}`;
|
|
103
|
+
};
|
|
104
|
+
/** `oort cloud use [--local]`: switch this machine's store. */
|
|
105
|
+
export async function cloudUseCommand(opts, ctx) {
|
|
106
|
+
const home = (await withWired(ctx, async (w) => w.config.home));
|
|
107
|
+
const settings = readCloudSettings(home);
|
|
108
|
+
const ui = uiFor(ctx.io);
|
|
109
|
+
if (opts.local === true) {
|
|
110
|
+
writeCloudSettings(home, { ...settings, store: 'local' });
|
|
111
|
+
ctx.io.out(ui.header('cloud', 'local store'));
|
|
112
|
+
ctx.io.out('This machine now uses the local store. Memories already in Oort Cloud stay there; `oort cloud export` downloads them.\n');
|
|
113
|
+
return 0;
|
|
114
|
+
}
|
|
115
|
+
const client = await clientFor(ctx);
|
|
116
|
+
const me = await client.whoami();
|
|
117
|
+
const local = await withWired(ctx, async (w) => (await localExport(w, ctx)).lines.length);
|
|
118
|
+
writeCloudSettings(home, { ...settings, store: 'cloud' });
|
|
119
|
+
ctx.io.out(ui.header('cloud', 'Oort Cloud'));
|
|
120
|
+
ctx.io.out(ui.kv([
|
|
121
|
+
['account', me.account],
|
|
122
|
+
['plan', me.plan],
|
|
123
|
+
['store', `Oort Cloud (${client.url}), stored in the EU`],
|
|
124
|
+
]));
|
|
125
|
+
ctx.io.out(`\nHooks, MCP and the oort commands now read and save in Oort Cloud.\n` +
|
|
126
|
+
(local > 0
|
|
127
|
+
? `${local} ${local === 1 ? 'memory stays' : 'memories stay'} in the local store until you run \`oort cloud import\`.\n`
|
|
128
|
+
: '') +
|
|
129
|
+
'Switch back any time with `oort cloud use --local`.\n');
|
|
130
|
+
return 0;
|
|
131
|
+
}
|
|
132
|
+
/** `oort cloud status [--json]` */
|
|
133
|
+
export async function cloudStatusCommand(opts, ctx) {
|
|
134
|
+
return withWired(ctx, async (w) => {
|
|
135
|
+
const settings = readCloudSettings(w.config.home);
|
|
136
|
+
let account;
|
|
137
|
+
let stats;
|
|
138
|
+
try {
|
|
139
|
+
const client = await clientFor(ctx);
|
|
140
|
+
account = await client.whoami();
|
|
141
|
+
stats = (await client.stats());
|
|
142
|
+
}
|
|
143
|
+
catch (e) {
|
|
144
|
+
if (!(e instanceof Refusal))
|
|
145
|
+
throw e;
|
|
146
|
+
account = { error: e.message };
|
|
147
|
+
}
|
|
148
|
+
const out = {
|
|
149
|
+
store: settings.store,
|
|
150
|
+
url: (() => {
|
|
151
|
+
const u = cloudUrl(ctx.io.env);
|
|
152
|
+
return 'url' in u ? u.url : u.error;
|
|
153
|
+
})(),
|
|
154
|
+
account,
|
|
155
|
+
...(stats !== undefined && { cloud: stats }),
|
|
156
|
+
...(settings.import !== undefined && { import: settings.import }),
|
|
157
|
+
};
|
|
158
|
+
if (opts.json === true) {
|
|
159
|
+
ctx.io.out(`${JSON.stringify(out, null, 2)}\n`);
|
|
160
|
+
return 0;
|
|
161
|
+
}
|
|
162
|
+
const ui = uiFor(ctx.io);
|
|
163
|
+
const acct = 'error' in account
|
|
164
|
+
? ui.yellow(String(account['error']))
|
|
165
|
+
: `${String(account['account'])} (${String(account['plan'])})`;
|
|
166
|
+
ctx.io.out(ui.header('cloud', settings.store === 'cloud' ? 'Oort Cloud' : 'local store'));
|
|
167
|
+
ctx.io.out(ui.kv([
|
|
168
|
+
['store', settings.store === 'cloud' ? `Oort Cloud (${out.url})` : 'local (this machine)'],
|
|
169
|
+
['account', acct],
|
|
170
|
+
...(stats !== undefined
|
|
171
|
+
? [['cloud memories', String(stats['memories'])]]
|
|
172
|
+
: []),
|
|
173
|
+
...(settings.import !== undefined
|
|
174
|
+
? [
|
|
175
|
+
[
|
|
176
|
+
'import',
|
|
177
|
+
`${settings.import.done}/${settings.import.total} uploaded${settings.import.done < settings.import.total ? ' (run `oort cloud import` to resume)' : ''}`,
|
|
178
|
+
],
|
|
179
|
+
]
|
|
180
|
+
: []),
|
|
181
|
+
]));
|
|
182
|
+
return 0;
|
|
183
|
+
});
|
|
184
|
+
}
|
|
185
|
+
/** `oort cloud import [--dry-run] [--yes]`: uploads the local store, resumable and idempotent. */
|
|
186
|
+
export async function cloudImportCommand(opts, ctx) {
|
|
187
|
+
return withWired(ctx, async (w) => {
|
|
188
|
+
const ui = uiFor(ctx.io);
|
|
189
|
+
const { manifest, lines, withEmbedding } = await localExport(w, ctx);
|
|
190
|
+
const settings = readCloudSettings(w.config.home);
|
|
191
|
+
const fingerprint = fingerprintOf(lines);
|
|
192
|
+
const resume = settings.import?.fingerprint === fingerprint
|
|
193
|
+
? Math.min(settings.import.done, lines.length)
|
|
194
|
+
: 0;
|
|
195
|
+
const plan = chunks(lines, resume);
|
|
196
|
+
const bytes = lines.slice(resume).reduce((n, l) => n + Buffer.byteLength(l) + 1, 0);
|
|
197
|
+
ctx.io.out(ui.header('cloud import', `${lines.length} local memories`));
|
|
198
|
+
ctx.io.out(ui.kv([
|
|
199
|
+
['to upload', `${lines.length - resume}${resume > 0 ? ` (resuming after ${resume})` : ''}`],
|
|
200
|
+
['size', `${(bytes / (1024 * 1024)).toFixed(2)} MB in ${plan.length} requests`],
|
|
201
|
+
[
|
|
202
|
+
'embeddings',
|
|
203
|
+
`${withEmbedding} reused; ${lines.length - withEmbedding} re-embedded in the cloud (counted toward your Pro embed use)`,
|
|
204
|
+
],
|
|
205
|
+
]));
|
|
206
|
+
if (opts.dryRun === true || lines.length === resume) {
|
|
207
|
+
if (lines.length === resume && lines.length > 0)
|
|
208
|
+
ctx.io.out('Already uploaded.\n');
|
|
209
|
+
return 0;
|
|
210
|
+
}
|
|
211
|
+
const go = await confirm(ctx.io, `Upload ${lines.length - resume} memories to Oort Cloud (EU)?`, opts.yes === true, 'upload them');
|
|
212
|
+
if (!go) {
|
|
213
|
+
ctx.io.out('Nothing uploaded.\n');
|
|
214
|
+
return 0;
|
|
215
|
+
}
|
|
216
|
+
const client = w.cloud ?? (await clientFor(ctx));
|
|
217
|
+
const header = JSON.stringify(manifest);
|
|
218
|
+
let done = resume;
|
|
219
|
+
let imported = 0;
|
|
220
|
+
let skipped = 0;
|
|
221
|
+
let reembedded = 0;
|
|
222
|
+
for (const c of plan) {
|
|
223
|
+
const r = await uploadChunk(client, header, c.lines, (inChunk) => {
|
|
224
|
+
done = c.start + inChunk;
|
|
225
|
+
writeCloudSettings(w.config.home, {
|
|
226
|
+
...readCloudSettings(w.config.home),
|
|
227
|
+
import: { fingerprint, done, total: lines.length, updatedAt: new Date().toISOString() },
|
|
228
|
+
});
|
|
229
|
+
});
|
|
230
|
+
imported += r.imported;
|
|
231
|
+
skipped += r.skipped;
|
|
232
|
+
reembedded += r.reembedded;
|
|
233
|
+
ctx.io.out(` uploaded ${done}/${lines.length}\n`);
|
|
234
|
+
}
|
|
235
|
+
ctx.io.out(`${ui.ok('done')}: ${imported} imported, ${skipped} already in the cloud, ${reembedded} re-embedded.\n`);
|
|
236
|
+
if (w.mode !== 'cloud')
|
|
237
|
+
ctx.io.out('Run `oort cloud use` to start using them.\n');
|
|
238
|
+
return 0;
|
|
239
|
+
});
|
|
240
|
+
}
|
|
241
|
+
/** `oort cloud export [--out <file>] [--force]`: the whole cloud account in the v1 format. */
|
|
242
|
+
export async function cloudExportCommand(opts, ctx) {
|
|
243
|
+
const client = await clientFor(ctx);
|
|
244
|
+
let data;
|
|
245
|
+
try {
|
|
246
|
+
data = await client.exportText();
|
|
247
|
+
}
|
|
248
|
+
catch (e) {
|
|
249
|
+
if (e instanceof OortCloudError)
|
|
250
|
+
throw e;
|
|
251
|
+
throw new Refusal('Oort Cloud export failed; try again.');
|
|
252
|
+
}
|
|
253
|
+
const memories = Math.max(0, data.split('\n').filter((l) => l.length > 0).length - 1);
|
|
254
|
+
if (opts.out === undefined)
|
|
255
|
+
ctx.io.out(data);
|
|
256
|
+
else {
|
|
257
|
+
const file = path.resolve(ctx.io.cwd, opts.out);
|
|
258
|
+
writeOwnerOnly(file, data, opts.force === true);
|
|
259
|
+
ctx.io.out(`Exported ${memories} memories from Oort Cloud to ${file}\n`);
|
|
260
|
+
}
|
|
261
|
+
return 0;
|
|
262
|
+
}
|
|
263
|
+
//# sourceMappingURL=cloud.js.map
|
|
@@ -1,10 +1,13 @@
|
|
|
1
1
|
import { filterFor } from '@oortmem/claude-code';
|
|
2
2
|
import { consolidate } from '@oortmem/core';
|
|
3
3
|
import { uiFor } from '../ui.js';
|
|
4
|
-
import { scopeLabel, withWired } from '../util.js';
|
|
4
|
+
import { Refusal, scopeLabel, withWired } from '../util.js';
|
|
5
5
|
/** `oort consolidate`: expire, re-tier and evict the memories visible from the cwd. */
|
|
6
6
|
export async function consolidateCommand(ctx) {
|
|
7
7
|
return withWired(ctx, async (w) => {
|
|
8
|
+
if (w.mode === 'cloud') {
|
|
9
|
+
throw new Refusal('In cloud mode Oort Cloud consolidates your memories itself (daily).');
|
|
10
|
+
}
|
|
8
11
|
const r = await consolidate(filterFor(w.scope), { store: w.store, config: w.config });
|
|
9
12
|
const ui = uiFor(ctx.io);
|
|
10
13
|
ctx.io.out(ui.header('consolidate', scopeLabel(w)));
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { type Ctx } from '../util.js';
|
|
2
2
|
export type CheckStatus = 'ok' | 'warn' | 'fail' | 'info' | 'unknown';
|
|
3
3
|
export interface Check {
|
|
4
|
-
group: 'keys' | 'hooks' | 'mcp' | 'store' | 'relay';
|
|
4
|
+
group: 'keys' | 'hooks' | 'mcp' | 'store' | 'relay' | 'cloud';
|
|
5
5
|
name: string;
|
|
6
6
|
status: CheckStatus;
|
|
7
7
|
/** One line; never a key or a memory. */
|
package/dist/commands/doctor.js
CHANGED
|
@@ -6,6 +6,7 @@ import { HOSTS, HOST_LABEL, detectHosts, inspectHost } from '../connectors.js';
|
|
|
6
6
|
import { ROLES, ROLE_VENDOR, keyLookup, routeFor } from '../credentials.js';
|
|
7
7
|
import { describeProblem, inspectCredentialsFile } from '../credentials-file.js';
|
|
8
8
|
import { PROVIDER_ENV } from '../keychain.js';
|
|
9
|
+
import { OortCloudError, cloudUrl, createCloudClient, readCloudSettings } from '../cloud.js';
|
|
9
10
|
import { RELAY_MODELS, relayUrl } from '../relay.js';
|
|
10
11
|
import { uiFor } from '../ui.js';
|
|
11
12
|
import { Refusal, echo, tilde, withWired } from '../util.js';
|
|
@@ -346,6 +347,76 @@ async function relayChecks(ctx, relayed) {
|
|
|
346
347
|
},
|
|
347
348
|
];
|
|
348
349
|
}
|
|
350
|
+
/** Oort Cloud: which store this machine uses, and in cloud mode whether the account answers. */
|
|
351
|
+
async function cloudChecks(ctx) {
|
|
352
|
+
const settings = readCloudSettings(resolveHome(ctx.io.env));
|
|
353
|
+
const u = cloudUrl(ctx.io.env);
|
|
354
|
+
if (settings.store !== 'cloud') {
|
|
355
|
+
return [
|
|
356
|
+
{
|
|
357
|
+
group: 'cloud',
|
|
358
|
+
name: 'store',
|
|
359
|
+
status: 'info',
|
|
360
|
+
detail: 'local store on this machine',
|
|
361
|
+
fix: 'oort cloud use, to keep memories in Oort Cloud (Oort Pro)',
|
|
362
|
+
},
|
|
363
|
+
];
|
|
364
|
+
}
|
|
365
|
+
if ('error' in u) {
|
|
366
|
+
return [
|
|
367
|
+
{
|
|
368
|
+
group: 'cloud',
|
|
369
|
+
name: 'store',
|
|
370
|
+
status: 'fail',
|
|
371
|
+
detail: u.error,
|
|
372
|
+
fix: 'unset OORT_CLOUD_URL (or set it to an https URL, or http://localhost for wrangler dev)',
|
|
373
|
+
},
|
|
374
|
+
];
|
|
375
|
+
}
|
|
376
|
+
const key = await keyLookup(ctx.io.env, ctx.keychain.read)('oort');
|
|
377
|
+
if (key === undefined) {
|
|
378
|
+
return [
|
|
379
|
+
{
|
|
380
|
+
group: 'cloud',
|
|
381
|
+
name: 'store',
|
|
382
|
+
status: 'fail',
|
|
383
|
+
detail: 'cloud mode is on, but there is no Oort key: hooks inject nothing and saves wait',
|
|
384
|
+
fix: 'oort login (or oort cloud use --local)',
|
|
385
|
+
},
|
|
386
|
+
];
|
|
387
|
+
}
|
|
388
|
+
const client = createCloudClient({
|
|
389
|
+
url: u.url,
|
|
390
|
+
key: key.key,
|
|
391
|
+
timeoutMs: PROBE_TIMEOUT_MS,
|
|
392
|
+
...(ctx.fetch !== undefined && { fetch: ctx.fetch }),
|
|
393
|
+
});
|
|
394
|
+
try {
|
|
395
|
+
const me = await client.whoami();
|
|
396
|
+
return [
|
|
397
|
+
{
|
|
398
|
+
group: 'cloud',
|
|
399
|
+
name: 'store',
|
|
400
|
+
status: 'ok',
|
|
401
|
+
detail: `Oort Cloud (${u.url}), account ${me.account}, plan ${me.plan}`,
|
|
402
|
+
},
|
|
403
|
+
];
|
|
404
|
+
}
|
|
405
|
+
catch (e) {
|
|
406
|
+
const err = e instanceof OortCloudError ? e : new OortCloudError('network', 0);
|
|
407
|
+
return [
|
|
408
|
+
{
|
|
409
|
+
group: 'cloud',
|
|
410
|
+
name: 'store',
|
|
411
|
+
status: err.kind === 'network' || err.kind === 'busy' ? 'warn' : 'fail',
|
|
412
|
+
detail: err.message,
|
|
413
|
+
fix: err.kind === 'network' || err.kind === 'busy'
|
|
414
|
+
? 'check your network; saves wait in the local queue meanwhile'
|
|
415
|
+
: 'oort login, or oort cloud use --local',
|
|
416
|
+
},
|
|
417
|
+
];
|
|
418
|
+
}
|
|
419
|
+
}
|
|
349
420
|
const GLYPH = {
|
|
350
421
|
ok: '✓',
|
|
351
422
|
warn: '!',
|
|
@@ -381,6 +452,7 @@ export async function doctorCommand(opts, ctx) {
|
|
|
381
452
|
...mcpChecks(ctx, cmd),
|
|
382
453
|
...(await storeChecks(ctx)),
|
|
383
454
|
...(await relayChecks(ctx, keys.relayed)),
|
|
455
|
+
...(await cloudChecks(ctx)),
|
|
384
456
|
];
|
|
385
457
|
const ok = !checks.some((c) => c.status === 'fail');
|
|
386
458
|
if (opts.json === true) {
|
|
@@ -390,7 +462,7 @@ export async function doctorCommand(opts, ctx) {
|
|
|
390
462
|
const ui = uiFor(ctx.io);
|
|
391
463
|
const problems = checks.filter((c) => c.status === 'warn' || c.status === 'fail').length;
|
|
392
464
|
ctx.io.out(ui.header('doctor', problems === 0 ? 'no problems' : `${problems} to fix`));
|
|
393
|
-
for (const group of ['keys', 'hooks', 'mcp', 'store', 'relay']) {
|
|
465
|
+
for (const group of ['keys', 'hooks', 'mcp', 'store', 'relay', 'cloud']) {
|
|
394
466
|
const rows = checks.filter((c) => c.group === group);
|
|
395
467
|
ctx.io.out(`${ui.bold(group)}\n`);
|
|
396
468
|
ctx.io.out(ui.kv(rows.flatMap((c) => [
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { type Scope } from '@oortmem/core';
|
|
1
|
+
import { type Grant, type Memory, type MemoryStore, type Scope } from '@oortmem/core';
|
|
2
2
|
import { type Ctx } from '../util.js';
|
|
3
3
|
/** Version of the export format documented in docs/export-format.md. */
|
|
4
4
|
export declare const EXPORT_VERSION = 1;
|
|
@@ -9,6 +9,9 @@ export interface ExportManifest {
|
|
|
9
9
|
memories: number;
|
|
10
10
|
scopes: Scope[];
|
|
11
11
|
}
|
|
12
|
+
/** A cold memory's main record has only its stub indexed; the cold copy holds the full text and embedding. */
|
|
13
|
+
export declare function decoded(m: Memory, store: MemoryStore, grant: Grant, ctx: Ctx): Promise<Memory>;
|
|
14
|
+
export declare const scopeKey: (s: Scope) => string;
|
|
12
15
|
/**
|
|
13
16
|
* `oort export [--out <file>]`: the v1 export format, a manifest line and one
|
|
14
17
|
* JSON line per memory (every tier and validity state) visible from the cwd.
|
package/dist/commands/export.js
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import * as path from 'node:path';
|
|
2
2
|
import { filterFor } from '@oortmem/claude-code';
|
|
3
3
|
import { localGrant } from '@oortmem/core';
|
|
4
|
-
import { shapeLinks, toJson, withWired, writeOwnerOnly } from '../util.js';
|
|
4
|
+
import { cloudOf, shapeLinks, toJson, withWired, writeOwnerOnly } from '../util.js';
|
|
5
5
|
/** Version of the export format documented in docs/export-format.md. */
|
|
6
6
|
export const EXPORT_VERSION = 1;
|
|
7
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) {
|
|
8
|
+
export async function decoded(m, store, grant, ctx) {
|
|
9
9
|
if (m.tier !== 'cold')
|
|
10
10
|
return m;
|
|
11
11
|
let blob;
|
|
@@ -28,13 +28,27 @@ async function decoded(m, store, grant, ctx) {
|
|
|
28
28
|
...(blob.embedding !== undefined && { embedding: blob.embedding }),
|
|
29
29
|
};
|
|
30
30
|
}
|
|
31
|
-
const scopeKey = (s) => JSON.stringify(Object.entries(s).sort());
|
|
31
|
+
export const scopeKey = (s) => JSON.stringify(Object.entries(s).sort());
|
|
32
32
|
/**
|
|
33
33
|
* `oort export [--out <file>]`: the v1 export format, a manifest line and one
|
|
34
34
|
* JSON line per memory (every tier and validity state) visible from the cwd.
|
|
35
35
|
*/
|
|
36
36
|
export async function exportCommand(opts, ctx) {
|
|
37
37
|
return withWired(ctx, async (w) => {
|
|
38
|
+
const cloud = cloudOf(w);
|
|
39
|
+
if (cloud !== undefined) {
|
|
40
|
+
// The whole account, in the same v1 format: `oort cloud export` is how you leave the cloud.
|
|
41
|
+
const data = await cloud.exportText();
|
|
42
|
+
const memories = Math.max(0, data.split('\n').filter((l) => l.length > 0).length - 1);
|
|
43
|
+
if (opts.out === undefined)
|
|
44
|
+
ctx.io.out(data);
|
|
45
|
+
else {
|
|
46
|
+
const file = path.resolve(ctx.io.cwd, opts.out);
|
|
47
|
+
writeOwnerOnly(file, data, opts.force === true);
|
|
48
|
+
ctx.io.out(`Exported ${memories} memories from Oort Cloud to ${file}\n`);
|
|
49
|
+
}
|
|
50
|
+
return 0;
|
|
51
|
+
}
|
|
38
52
|
const filter = filterFor(w.scope);
|
|
39
53
|
const grant = localGrant(filter);
|
|
40
54
|
const listed = await w.store.list(grant, { validOnly: false });
|
package/dist/commands/forget.js
CHANGED
|
@@ -1,15 +1,22 @@
|
|
|
1
1
|
import { filterFor, grantFor } from '@oortmem/claude-code';
|
|
2
|
-
import {
|
|
2
|
+
import { findCloudMemory } from '../cloud.js';
|
|
3
|
+
import { clip, cloudOf, confirm, findMemory, short, withWired } from '../util.js';
|
|
3
4
|
/** `oort forget <id> [--yes]`: deletes a memory everywhere (main record and cold copy). */
|
|
4
5
|
export async function forgetCommand(id, opts, ctx) {
|
|
5
6
|
return withWired(ctx, async (w) => {
|
|
6
|
-
const
|
|
7
|
+
const cloud = cloudOf(w);
|
|
8
|
+
const m = cloud !== undefined
|
|
9
|
+
? await findCloudMemory(cloud, id)
|
|
10
|
+
: await findMemory(w.store, filterFor(w.scope), id);
|
|
7
11
|
const ok = await confirm(ctx.io, `Forget ${short(m.id)} "${clip(m.text, 60)}"?`, opts.yes === true, 'forget it');
|
|
8
12
|
if (!ok) {
|
|
9
13
|
ctx.io.out('Kept.\n');
|
|
10
14
|
return 0;
|
|
11
15
|
}
|
|
12
|
-
|
|
16
|
+
if (cloud !== undefined)
|
|
17
|
+
await cloud.forget(m.id);
|
|
18
|
+
else
|
|
19
|
+
await w.store.forget(grantFor(w.scope), m.id);
|
|
13
20
|
ctx.io.out(`Forgot ${m.id}.\n`);
|
|
14
21
|
return 0;
|
|
15
22
|
});
|
package/dist/commands/hook.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { HOOK_TIMEOUTS, createHookLogger, runHook } from '@oortmem/claude-code';
|
|
2
2
|
import { resolveHome } from '@oortmem/core';
|
|
3
|
+
import { cloudRemote } from '../cloud-remote.js';
|
|
3
4
|
import { echo } from '../util.js';
|
|
4
5
|
export const HOOK_COMMAND_EVENTS = [
|
|
5
6
|
'UserPromptSubmit',
|
|
@@ -105,6 +106,8 @@ export async function hookCommand(event, ctx) {
|
|
|
105
106
|
const wireStart = performance.now();
|
|
106
107
|
w = await ctx.wire(inputCwd(stdin) ?? io.cwd, {
|
|
107
108
|
env: io.env,
|
|
109
|
+
// Oort Cloud: a recall must answer inside the hook's own budget.
|
|
110
|
+
cloudTimeoutMs: event === 'drain' ? 30_000 : 1400,
|
|
108
111
|
// The drain serves every project's queue: home config only, whatever lies above OORT_HOME.
|
|
109
112
|
...(event === 'drain' ? { projectConfig: false } : { busyTimeoutMs: HOOK_BUSY_TIMEOUT_MS }),
|
|
110
113
|
roles: HOOK_ROLES[event],
|
|
@@ -114,7 +117,7 @@ export async function hookCommand(event, ctx) {
|
|
|
114
117
|
for (const warning of w.warnings)
|
|
115
118
|
log(ctx, w, event, `config:${warning.code}`);
|
|
116
119
|
const home = w.config.home;
|
|
117
|
-
const { harnessLog, unavailable: _u, close: _c, scope: _s, warnings: _w, store, ...engine } = w;
|
|
120
|
+
const { harnessLog, unavailable: _u, close: _c, scope: _s, warnings: _w, store, mode: _m, cloud: _cl, cloudProblem: _cp, ...engine } = w;
|
|
118
121
|
// The scope is left out: each handler derives it from the session's cwd.
|
|
119
122
|
const out = await runHook(event, stdin, {
|
|
120
123
|
...engine,
|
|
@@ -125,6 +128,7 @@ export async function hookCommand(event, ctx) {
|
|
|
125
128
|
// The drain runs in the home directory, so it resolves the home config only:
|
|
126
129
|
// project config never affects the write path.
|
|
127
130
|
spawnDrain: () => io.spawnDrain(home),
|
|
131
|
+
...(w.mode === 'cloud' && { remote: cloudRemote(w.cloud, w.cloudProblem) }),
|
|
128
132
|
});
|
|
129
133
|
stdout = out.stdout;
|
|
130
134
|
// The drain is detached and nothing waits on it, so it closes the store cleanly.
|
package/dist/commands/import.js
CHANGED
|
@@ -20,6 +20,9 @@ export async function importCommand(opts, ctx) {
|
|
|
20
20
|
const home = ctx.io.env['HOME'] || os.homedir();
|
|
21
21
|
const projectsDir = path.resolve(ctx.io.cwd, opts.projectsDir ?? path.join(home, '.claude', 'projects'));
|
|
22
22
|
return withWired(ctx, async (w) => {
|
|
23
|
+
if (w.mode === 'cloud') {
|
|
24
|
+
throw new Refusal('transcript import writes to the local store: run it with `oort cloud use --local`, then `oort cloud import`.');
|
|
25
|
+
}
|
|
23
26
|
const deps = { ...w, log: w.harnessLog };
|
|
24
27
|
const base = {
|
|
25
28
|
projectsDir,
|
package/dist/commands/inspect.js
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { filterFor } from '@oortmem/claude-code';
|
|
2
2
|
import { activation, localGrant, } from '@oortmem/core';
|
|
3
|
+
import { OortCloudError, findCloudMemory } from '../cloud.js';
|
|
3
4
|
import { uiFor } from '../ui.js';
|
|
4
|
-
import { Refusal, echo, age, clip, findMemory, fixed, scopeLabel, shapeLinks, short, withoutEmbedding, withWired, } from '../util.js';
|
|
5
|
+
import { Refusal, cloudOf, echo, age, clip, findMemory, fixed, scopeLabel, shapeLinks, short, withoutEmbedding, withWired, } from '../util.js';
|
|
5
6
|
const link = (m) => ({
|
|
6
7
|
id: m.id,
|
|
7
8
|
validFrom: m.validFrom,
|
|
@@ -40,6 +41,34 @@ export async function supersedeChain(store, m, filter) {
|
|
|
40
41
|
await walk(m.supersededBy, (x) => x.supersededBy, (l) => forward.push(l));
|
|
41
42
|
return { links: [...back, link(m), ...forward], hidden };
|
|
42
43
|
}
|
|
44
|
+
/** The supersede chain through `m` in Oort Cloud: links the account cannot read count as hidden. */
|
|
45
|
+
async function cloudChain(client, m) {
|
|
46
|
+
const seen = new Set([m.id]);
|
|
47
|
+
let hidden = 0;
|
|
48
|
+
const walk = async (start, next, add) => {
|
|
49
|
+
for (let id = start; id !== null && !seen.has(id);) {
|
|
50
|
+
seen.add(id);
|
|
51
|
+
let x;
|
|
52
|
+
try {
|
|
53
|
+
x = (await client.get(id));
|
|
54
|
+
}
|
|
55
|
+
catch (e) {
|
|
56
|
+
if (e instanceof OortCloudError && e.kind === 'not-found') {
|
|
57
|
+
hidden++;
|
|
58
|
+
break;
|
|
59
|
+
}
|
|
60
|
+
throw e;
|
|
61
|
+
}
|
|
62
|
+
add(link(x));
|
|
63
|
+
id = next(x);
|
|
64
|
+
}
|
|
65
|
+
};
|
|
66
|
+
const back = [];
|
|
67
|
+
const forward = [];
|
|
68
|
+
await walk(m.supersedes, (x) => x.supersedes, (l) => back.unshift(l));
|
|
69
|
+
await walk(m.supersededBy, (x) => x.supersededBy, (l) => forward.push(l));
|
|
70
|
+
return { links: [...back, link(m), ...forward], hidden };
|
|
71
|
+
}
|
|
43
72
|
function limitOf(raw) {
|
|
44
73
|
if (raw === undefined)
|
|
45
74
|
return 50;
|
|
@@ -54,10 +83,19 @@ export async function inspectCommand(id, opts, ctx) {
|
|
|
54
83
|
return withWired(ctx, async (w) => {
|
|
55
84
|
const now = new Date();
|
|
56
85
|
const filter = filterFor(w.scope);
|
|
86
|
+
const cloud = cloudOf(w);
|
|
87
|
+
const project = w.scope.projectId;
|
|
57
88
|
if (id === undefined) {
|
|
58
|
-
const current =
|
|
89
|
+
const current = cloud !== undefined
|
|
90
|
+
? (await cloud.list({
|
|
91
|
+
...(project !== undefined && { project }),
|
|
92
|
+
limit: Math.min(limit, 100),
|
|
93
|
+
})).memories
|
|
94
|
+
: (await w.store.list(localGrant(filter))).slice(0, limit);
|
|
59
95
|
if (opts.json === true) {
|
|
60
|
-
const shaped =
|
|
96
|
+
const shaped = cloud !== undefined
|
|
97
|
+
? current
|
|
98
|
+
: await Promise.all(current.map((m) => shapeLinks(w.store, filter, m)));
|
|
61
99
|
ctx.io.out(`${JSON.stringify(shaped.map(withoutEmbedding), null, 2)}\n`);
|
|
62
100
|
return 0;
|
|
63
101
|
}
|
|
@@ -75,10 +113,12 @@ export async function inspectCommand(id, opts, ctx) {
|
|
|
75
113
|
ctx.io.out(ui.table(['id', 'type', 'tier', 'activation', 'age', 'text'], rows));
|
|
76
114
|
return 0;
|
|
77
115
|
}
|
|
78
|
-
const m =
|
|
79
|
-
|
|
116
|
+
const m = cloud !== undefined
|
|
117
|
+
? (await findCloudMemory(cloud, id))
|
|
118
|
+
: await findMemory(w.store, filter, id);
|
|
119
|
+
const { links: chain, hidden } = cloud !== undefined ? await cloudChain(cloud, m) : await supersedeChain(w.store, m, filter);
|
|
80
120
|
if (opts.json === true) {
|
|
81
|
-
const memory = withoutEmbedding(await shapeLinks(w.store, filter, m));
|
|
121
|
+
const memory = withoutEmbedding(cloud !== undefined ? m : await shapeLinks(w.store, filter, m));
|
|
82
122
|
const out = { memory, chain, hiddenLinks: hidden };
|
|
83
123
|
ctx.io.out(`${JSON.stringify(out, null, 2)}\n`);
|
|
84
124
|
return 0;
|
package/dist/commands/observe.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
2
|
import { observe, scrubObservation } from '@oortmem/core';
|
|
3
|
-
import { Refusal, writePath, withWired } from '../util.js';
|
|
3
|
+
import { Refusal, cloudOf, writePath, withWired } from '../util.js';
|
|
4
4
|
const ObservationInput = z.strictObject({
|
|
5
5
|
userText: z.string(),
|
|
6
6
|
assistantText: z.string(),
|
|
@@ -59,6 +59,21 @@ export async function observeCommand(opts, ctx) {
|
|
|
59
59
|
return 0;
|
|
60
60
|
}
|
|
61
61
|
return withWired(ctx, async (w) => {
|
|
62
|
+
const cloud = cloudOf(w);
|
|
63
|
+
if (cloud !== undefined) {
|
|
64
|
+
const result = await cloud.observe({
|
|
65
|
+
...(w.scope.projectId !== undefined && { project: w.scope.projectId }),
|
|
66
|
+
observation: {
|
|
67
|
+
user: obs.userText,
|
|
68
|
+
assistant: obs.assistantText,
|
|
69
|
+
at: obs.at,
|
|
70
|
+
...(obs.sessionId !== undefined && { session: obs.sessionId }),
|
|
71
|
+
...(obs.toolSummaries.length > 0 && { tools: obs.toolSummaries }),
|
|
72
|
+
},
|
|
73
|
+
});
|
|
74
|
+
ctx.io.out(`${JSON.stringify(result, null, 2)}\n`);
|
|
75
|
+
return 0;
|
|
76
|
+
}
|
|
62
77
|
const adapters = writePath(w, 'oort observe');
|
|
63
78
|
const result = await observe(obs, w.scope, {
|
|
64
79
|
store: w.store,
|
package/dist/commands/pin.js
CHANGED
|
@@ -1,8 +1,20 @@
|
|
|
1
1
|
import { filterFor, grantFor } from '@oortmem/claude-code';
|
|
2
|
-
import {
|
|
2
|
+
import { findCloudMemory } from '../cloud.js';
|
|
3
|
+
import { cloudOf, findMemory, withWired } from '../util.js';
|
|
3
4
|
/** `oort pin <id>` / `oort unpin <id>`: consolidation never sends a pinned memory to cold. */
|
|
4
5
|
export async function pinCommand(id, pinned, ctx) {
|
|
5
6
|
return withWired(ctx, async (w) => {
|
|
7
|
+
const cloud = cloudOf(w);
|
|
8
|
+
if (cloud !== undefined) {
|
|
9
|
+
const c = await findCloudMemory(cloud, id);
|
|
10
|
+
if (c.pinned === pinned) {
|
|
11
|
+
ctx.io.out(`${c.id} is already ${pinned ? 'pinned' : 'unpinned'}.\n`);
|
|
12
|
+
return 0;
|
|
13
|
+
}
|
|
14
|
+
await cloud.pin(c.id, pinned);
|
|
15
|
+
ctx.io.out(`${pinned ? 'Pinned' : 'Unpinned'} ${c.id}.\n`);
|
|
16
|
+
return 0;
|
|
17
|
+
}
|
|
6
18
|
const m = await findMemory(w.store, filterFor(w.scope), id);
|
|
7
19
|
if (m.pinned === pinned) {
|
|
8
20
|
ctx.io.out(`${m.id} is already ${pinned ? 'pinned' : 'unpinned'}.\n`);
|