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.
@@ -1,13 +1,22 @@
1
1
  import { filterFor } from '@oortmem/claude-code';
2
2
  import { formatForInjection, recall } from '@oortmem/core';
3
3
  import { uiFor } from '../ui.js';
4
- import { Refusal, degradedHint, scopeLabel, shapeLinks, withoutEmbedding, withWired, } from '../util.js';
4
+ import { Refusal, cloudOf, degradedHint, scopeLabel, shapeLinks, withoutEmbedding, withWired, } from '../util.js';
5
5
  /** Recall for the cwd's project plus global memories; lexical-only without a Voyage key. */
6
6
  export async function runRecall(w, text, asOf, now) {
7
7
  if (asOf !== undefined && Number.isNaN(Date.parse(asOf))) {
8
8
  // The value is not echoed: it is free text the user typed.
9
9
  throw new Refusal('--as-of is not an ISO date or timestamp.');
10
10
  }
11
+ const cloud = cloudOf(w);
12
+ if (cloud !== undefined) {
13
+ return cloud.recall({
14
+ text,
15
+ ...(w.scope.projectId !== undefined && { project: w.scope.projectId }),
16
+ ...(asOf !== undefined && { asOf }),
17
+ k: 10,
18
+ });
19
+ }
11
20
  return recall({ text, scope: filterFor(w.scope), ...(asOf !== undefined && { asOf }) }, {
12
21
  store: w.store,
13
22
  config: w.config,
@@ -24,7 +33,10 @@ export async function recallCommand(text, opts, ctx) {
24
33
  degradedHint(ctx.io, w, result.degraded);
25
34
  if (opts.json === true) {
26
35
  const filter = filterFor(w.scope);
27
- const shaped = await Promise.all(result.memories.map((m) => shapeLinks(w.store, filter, m)));
36
+ // Cloud memories arrive already shaped for this account.
37
+ const shaped = w.mode === 'cloud'
38
+ ? result.memories
39
+ : await Promise.all(result.memories.map((m) => shapeLinks(w.store, filter, m)));
28
40
  const plain = { ...result, memories: shaped.map(withoutEmbedding) };
29
41
  ctx.io.out(`${JSON.stringify(plain, null, 2)}\n`);
30
42
  }
@@ -3,7 +3,7 @@ import * as path from 'node:path';
3
3
  import { filterFor, queueDir } from '@oortmem/claude-code';
4
4
  import { isCurrent, localGrant } from '@oortmem/core';
5
5
  import { uiFor } from '../ui.js';
6
- import { scopeLabel, withWired } from '../util.js';
6
+ import { cloudOf, scopeLabel, withWired } from '../util.js';
7
7
  const size = (file) => {
8
8
  try {
9
9
  return fs.statSync(file).size;
@@ -58,6 +58,36 @@ const mb = (bytes) => `${(bytes / (1024 * 1024)).toFixed(2)} MB`;
58
58
  /** `oort stats [--json]`: counts for the memories visible from the cwd, and on-disk sizes. */
59
59
  export async function statsCommand(opts, ctx) {
60
60
  return withWired(ctx, async (w) => {
61
+ const cloud = cloudOf(w);
62
+ if (cloud !== undefined) {
63
+ const c = await cloud.stats();
64
+ const queue = queueDir(w.config.home);
65
+ const out = {
66
+ store: 'cloud',
67
+ url: cloud.url,
68
+ ...c,
69
+ queue: { pending: jsonlLines(queue), deadLetters: jsonlLines(path.join(queue, 'dead')) },
70
+ };
71
+ if (opts.json === true) {
72
+ ctx.io.out(`${JSON.stringify(out, null, 2)}\n`);
73
+ return 0;
74
+ }
75
+ const ui = uiFor(ctx.io);
76
+ const t = (tier) => `${ui.tierName(tier)} ${c.tiers[tier]}`;
77
+ ctx.io.out(ui.header('stats', 'Oort Cloud'));
78
+ ctx.io.out(ui.kv([
79
+ ['store', `Oort Cloud (${cloud.url})`],
80
+ ['memories', String(c.memories)],
81
+ ['tiers', `${t('hot')}, ${t('warm')}, ${t('cold')}`],
82
+ ['database', mb(c.databaseBytes)],
83
+ ['consolidated', c.lastConsolidatedAt ?? 'not yet'],
84
+ [
85
+ 'queue',
86
+ `${out.queue.pending} pending upload, ${out.queue.deadLetters} dead-lettered (this machine)`,
87
+ ],
88
+ ]));
89
+ return 0;
90
+ }
61
91
  const now = new Date();
62
92
  const all = await w.store.list(localGrant(filterFor(w.scope)), { validOnly: false });
63
93
  const tiers = { hot: 0, warm: 0, cold: 0 };
package/dist/mcp.d.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  import type { McpServer } from '@modelcontextprotocol/server';
2
2
  import { type MemoryBackend } from '@oortmem/mcp';
3
+ import { type CloudClient } from './cloud.js';
3
4
  import type { Wired } from './wire.js';
4
5
  /**
5
6
  * `oort mcp`: the shared Oort tool set (`@oortmem/mcp`) over a local store. One server
@@ -13,5 +14,13 @@ export interface OortServerDeps {
13
14
  }
14
15
  /** The local store as an MCP memory backend. */
15
16
  export declare function localBackend(deps: OortServerDeps): MemoryBackend;
17
+ /**
18
+ * Oort Cloud as an MCP memory backend: the same seven tools, answered by the API for this
19
+ * machine's project (plus account-wide memories). Recall goes through `runRecall`, which already
20
+ * calls the cloud in cloud mode.
21
+ */
22
+ export declare function cloudBackend(deps: OortServerDeps & {
23
+ client: CloudClient;
24
+ }): MemoryBackend;
16
25
  /** A new server instance with the seven tools; `serveStdio` calls this once per connection. */
17
26
  export declare function createOortServer(deps: OortServerDeps): McpServer;
package/dist/mcp.js CHANGED
@@ -4,6 +4,7 @@ import { VERSION, activation, isCurrent, scrub, localGrant } from '@oortmem/core
4
4
  import { supersedeChain } from './commands/inspect.js';
5
5
  import { runRecall } from './commands/recall.js';
6
6
  import { rememberFacts } from './remember.js';
7
+ import { OortCloudError, findCloudMemory } from './cloud.js';
7
8
  import { Refusal, findMemory, fixed } from './util.js';
8
9
  /**
9
10
  * `oort mcp`: the shared Oort tool set (`@oortmem/mcp`) over a local store. One server
@@ -54,7 +55,7 @@ export function localBackend(deps) {
54
55
  why: why.join(', ') || 'activation',
55
56
  };
56
57
  });
57
- const lexicalOnly = w.embed === undefined || w.rerank === undefined;
58
+ const lexicalOnly = w.mode !== 'cloud' && (w.embed === undefined || w.rerank === undefined);
58
59
  const refused = w.relayErrors?.at(-1);
59
60
  return {
60
61
  memories,
@@ -157,10 +158,142 @@ export function localBackend(deps) {
157
158
  },
158
159
  };
159
160
  }
161
+ /**
162
+ * Oort Cloud as an MCP memory backend: the same seven tools, answered by the API for this
163
+ * machine's project (plus account-wide memories). Recall goes through `runRecall`, which already
164
+ * calls the cloud in cloud mode.
165
+ */
166
+ export function cloudBackend(deps) {
167
+ const w = deps.wired;
168
+ const client = deps.client;
169
+ const now = deps.now ?? (() => new Date());
170
+ const project = w.scope.projectId;
171
+ const local = localBackend(deps);
172
+ return {
173
+ recall: local.recall,
174
+ async remember({ facts, type }) {
175
+ // The same turn the local engine observes for a fact, sent to the cloud's write path.
176
+ const hint = type === undefined ? '' : ` (a ${type} fact)`;
177
+ const results = [];
178
+ for (const [i, fact] of facts.entries()) {
179
+ const r = await client.observe({
180
+ ...(project !== undefined && { project }),
181
+ observation: {
182
+ user: `Remember this${hint}: ${fact}`,
183
+ assistant: `Noted: ${fact}`,
184
+ at: now().toISOString(),
185
+ },
186
+ });
187
+ const first = r.memories[0];
188
+ results.push({
189
+ fact: i,
190
+ outcome: r.outcome,
191
+ ...(first?.id !== undefined && { id: first.id }),
192
+ ...(r.outcome === 'skipped' && { reason: 'not durable' }),
193
+ });
194
+ }
195
+ return { results };
196
+ },
197
+ async forget({ id }) {
198
+ const m = await findCloudMemory(client, id);
199
+ await client.forget(m.id);
200
+ return { forgotten: m.id };
201
+ },
202
+ async pin({ id, pinned }) {
203
+ const m = await findCloudMemory(client, id);
204
+ if (m.pinned === pinned)
205
+ return { id: m.id, pinned, changed: false };
206
+ await client.pin(m.id, pinned);
207
+ return { id: m.id, pinned, changed: true };
208
+ },
209
+ async explain({ id }) {
210
+ const at = now();
211
+ const m = (await findCloudMemory(client, id));
212
+ const chain = [];
213
+ let hidden = 0;
214
+ for (const linkId of [m.supersedes, m.supersededBy]) {
215
+ if (linkId === null)
216
+ continue;
217
+ try {
218
+ const l = await client.get(linkId);
219
+ chain.push({
220
+ id: l.id,
221
+ text: clean(l.text),
222
+ valid_from: l.validFrom,
223
+ valid_to: l.validTo,
224
+ end_reason: l.endReason,
225
+ });
226
+ }
227
+ catch (e) {
228
+ if (e instanceof OortCloudError && e.kind === 'not-found')
229
+ hidden++;
230
+ else
231
+ throw e;
232
+ }
233
+ }
234
+ return {
235
+ memory: {
236
+ id: m.id,
237
+ text: clean(m.text),
238
+ type: m.type,
239
+ tier: m.tier,
240
+ pinned: m.pinned,
241
+ status: status(m, at),
242
+ valid_from: m.validFrom,
243
+ valid_to: m.validTo,
244
+ salience: m.salience,
245
+ activation: Number(fixed(activation(m.accessLog, m.salience, at, w.config.activation))),
246
+ source: m.source.harness,
247
+ created_at: m.createdAt,
248
+ },
249
+ decisions: m.source.decisions,
250
+ chain,
251
+ ...(hidden > 0 && { hidden_links: hidden }),
252
+ };
253
+ },
254
+ async listRecent({ since, limit }) {
255
+ const at = now();
256
+ const from = since === undefined ? -Infinity : Date.parse(since);
257
+ const r = await client.list({ ...(project !== undefined && { project }), limit: 100 });
258
+ const memories = r.memories
259
+ .filter((m) => Date.parse(m.updatedAt) >= from)
260
+ .sort((a, b) => Date.parse(b.updatedAt) - Date.parse(a.updatedAt))
261
+ .slice(0, limit)
262
+ .map((m) => ({
263
+ id: m.id,
264
+ text: clean(m.text),
265
+ type: m.type,
266
+ tier: m.tier,
267
+ status: status(m, at),
268
+ valid_from: m.validFrom,
269
+ updated_at: m.updatedAt,
270
+ }));
271
+ return { memories };
272
+ },
273
+ async whoami() {
274
+ const me = await client.whoami();
275
+ return {
276
+ mode: 'cloud',
277
+ account: me.account,
278
+ plan: me.plan,
279
+ note: 'Memories live in Oort Cloud (EU) for this account.',
280
+ project: project ?? null,
281
+ scope: project === undefined ? 'global' : 'project, plus global memories',
282
+ version: VERSION,
283
+ };
284
+ },
285
+ };
286
+ }
160
287
  /** A new server instance with the seven tools; `serveStdio` calls this once per connection. */
161
288
  export function createOortServer(deps) {
289
+ const w = deps.wired;
290
+ if (w.mode === 'cloud' && w.cloud === undefined) {
291
+ throw new Refusal(w.cloudProblem ?? 'Oort Cloud is not set up.');
292
+ }
162
293
  return createMcpServer({
163
- backend: localBackend(deps),
294
+ backend: w.mode === 'cloud' && w.cloud !== undefined
295
+ ? cloudBackend({ ...deps, client: w.cloud })
296
+ : localBackend(deps),
164
297
  version: VERSION,
165
298
  isRefusal: (e) => e instanceof Refusal,
166
299
  });
package/dist/program.js CHANGED
@@ -21,6 +21,7 @@ import { recallCommand } from './commands/recall.js';
21
21
  import { setupCommand } from './commands/setup.js';
22
22
  import { statsCommand } from './commands/stats.js';
23
23
  import { keychainFor } from './keychain.js';
24
+ import { cloudExportCommand, cloudImportCommand, cloudStatusCommand, cloudUseCommand, } from './commands/cloud.js';
24
25
  import { Refusal, echo } from './util.js';
25
26
  import { wire as defaultWire } from './wire.js';
26
27
  function program(ctx, setCode) {
@@ -138,6 +139,31 @@ function program(ctx, setCode) {
138
139
  .command('unlink')
139
140
  .description("remove this repository's binding (back to the derived id)")
140
141
  .action(run(() => projectUnlinkCommand(ctx)));
142
+ const cloud = p
143
+ .command('cloud')
144
+ .description('Oort Cloud (Oort Pro): keep memories in your account instead of on this machine');
145
+ cloud
146
+ .command('use')
147
+ .description('use Oort Cloud for hooks, MCP and every command (needs oort login and Pro)')
148
+ .option('--local', 'switch back to the local store on this machine')
149
+ .action(run((o) => cloudUseCommand(o, ctx)));
150
+ cloud
151
+ .command('status')
152
+ .description('which store this machine uses, the account, and import progress')
153
+ .option('--json', 'print JSON')
154
+ .action(run((o) => cloudStatusCommand(o, ctx)));
155
+ cloud
156
+ .command('import')
157
+ .description('upload the local store to Oort Cloud (resumable; safe to run again)')
158
+ .option('--dry-run', 'show what would be uploaded and stop')
159
+ .option('--yes', 'do not ask before uploading')
160
+ .action(run((o) => cloudImportCommand(o, ctx)));
161
+ cloud
162
+ .command('export')
163
+ .description('download every memory in your Oort Cloud account (v1 export format)')
164
+ .option('--out <file>', 'write to a new file (0600) instead of stdout')
165
+ .option('--force', 'with --out, overwrite an existing file (never a symlink)')
166
+ .action(run((o) => cloudExportCommand(o, ctx)));
141
167
  p.command('doctor')
142
168
  .description('check keys, hooks, MCP connectors, the store and the relay, with a fix per problem')
143
169
  .option('--json', 'print every check as JSON')
package/dist/util.d.ts CHANGED
@@ -2,6 +2,7 @@ import { type DecisionController, type Embedder, type Memory, type MemoryStore,
2
2
  import type { LoginDeps } from './commands/login.js';
3
3
  import type { Io } from './io.js';
4
4
  import type { Keychain } from './keychain.js';
5
+ import type { CloudClient } from './cloud.js';
5
6
  import type { WireFn, Wired } from './wire.js';
6
7
  /**
7
8
  * A user argument as echoed in a message: scrubbed, clipped to 24 characters,
@@ -24,6 +25,11 @@ export interface Ctx {
24
25
  }
25
26
  /** The scope a command reads, for header lines: `project <id>` or `global`. */
26
27
  export declare function scopeLabel(w: Pick<Wired, 'scope'>): string;
28
+ /**
29
+ * The Oort Cloud client when this machine uses the cloud store, undefined in local mode. In cloud
30
+ * mode without a client (no Oort key, a bad URL) it refuses with the reason.
31
+ */
32
+ export declare function cloudOf(w: Wired): CloudClient | undefined;
27
33
  /** Wires for the cwd, runs `fn`, and always closes the store. */
28
34
  export declare function withWired<T>(ctx: Ctx, fn: (w: Wired) => Promise<T>): Promise<T>;
29
35
  /**
package/dist/util.js CHANGED
@@ -21,6 +21,17 @@ export class Refusal extends Error {
21
21
  export function scopeLabel(w) {
22
22
  return w.scope.projectId === undefined ? 'global' : `project ${w.scope.projectId}`;
23
23
  }
24
+ /**
25
+ * The Oort Cloud client when this machine uses the cloud store, undefined in local mode. In cloud
26
+ * mode without a client (no Oort key, a bad URL) it refuses with the reason.
27
+ */
28
+ export function cloudOf(w) {
29
+ if (w.mode !== 'cloud')
30
+ return undefined;
31
+ if (w.cloud === undefined)
32
+ throw new Refusal(w.cloudProblem ?? 'Oort Cloud is not set up.');
33
+ return w.cloud;
34
+ }
24
35
  /** Wires for the cwd, runs `fn`, and always closes the store. */
25
36
  export async function withWired(ctx, fn) {
26
37
  const w = await ctx.wire(ctx.io.cwd, { env: ctx.io.env });
package/dist/wire.d.ts CHANGED
@@ -4,9 +4,19 @@ import type { Env } from './io.js';
4
4
  import { type AdapterRole, type RouteSummary } from './credentials.js';
5
5
  import { type KeychainReader } from './keychain.js';
6
6
  import { type OortRelayError } from './relay.js';
7
+ import { type CloudClient } from './cloud.js';
7
8
  export type { AdapterRole } from './credentials.js';
8
9
  export interface Wired {
9
10
  config: ResolvedConfig;
11
+ /**
12
+ * Which store this machine uses (`~/.oort/cloud.json`). In `cloud` mode memories live in Oort
13
+ * Cloud: `cloud` is the API client and no vendor adapter is loaded (the cloud calls the models).
14
+ * The local store still holds the hooks' cursors and queue.
15
+ */
16
+ mode?: 'local' | 'cloud';
17
+ cloud?: CloudClient;
18
+ /** Why cloud mode has no client (no Oort key, a bad OORT_CLOUD_URL). */
19
+ cloudProblem?: string;
10
20
  scope: Scope;
11
21
  store: MemoryStore;
12
22
  embed?: Embedder;
@@ -32,7 +42,7 @@ export interface Wired {
32
42
  close(): Promise<void>;
33
43
  }
34
44
  export interface WireWarning {
35
- code: 'embed-dimension-ignored' | 'project-id-untrusted' | 'keychain-unreadable';
45
+ code: 'embed-dimension-ignored' | 'project-id-untrusted' | 'keychain-unreadable' | 'cloud-unavailable';
36
46
  /** One line for stderr; holds config values only. */
37
47
  message: string;
38
48
  }
@@ -54,6 +64,8 @@ export interface WireOptions {
54
64
  * so it neither reads the TypeSafe and Anthropic keys nor imports their SDKs (CON-57).
55
65
  */
56
66
  roles?: readonly AdapterRole[];
67
+ /** Per-request timeout for the Oort Cloud client (the hooks pass their budget). */
68
+ cloudTimeoutMs?: number;
57
69
  }
58
70
  export type WireFn = (cwd: string, opts?: WireOptions) => Promise<Wired>;
59
71
  /**
package/dist/wire.js CHANGED
@@ -8,6 +8,7 @@ import { diskEmbeddingCache } from './embed-cache-disk.js';
8
8
  import { createKeychain } from './keychain.js';
9
9
  import { RELAY_MODELS, RELAY_PATHS, relayUrl, viaRelay } from './relay.js';
10
10
  import { echo } from './util.js';
11
+ import { cloudUrl, createCloudClient, readCloudSettings } from './cloud.js';
11
12
  const EMBED = {
12
13
  role: 'embed',
13
14
  configKey: 'embed',
@@ -179,6 +180,29 @@ export const wire = async (cwd, opts = {}) => {
179
180
  warnings,
180
181
  close: () => store.close(),
181
182
  };
183
+ const settings = readCloudSettings(config.home);
184
+ if (settings.store === 'cloud') {
185
+ out.mode = 'cloud';
186
+ const url = cloudUrl(env);
187
+ const key = await lookup('oort');
188
+ if ('error' in url)
189
+ out.cloudProblem = url.error;
190
+ else if (key === undefined)
191
+ out.cloudProblem =
192
+ 'cloud mode needs the Oort key: run `oort login` (or `oort cloud use --local`).';
193
+ else
194
+ out.cloud = createCloudClient({
195
+ url: url.url,
196
+ key: key.key,
197
+ ...fetchOpt,
198
+ ...(opts.cloudTimeoutMs !== undefined && { timeoutMs: opts.cloudTimeoutMs }),
199
+ });
200
+ if (out.cloudProblem !== undefined)
201
+ warnings.push({ code: 'cloud-unavailable', message: out.cloudProblem });
202
+ // The cloud runs the models with Oort's keys: no vendor adapter, no vendor key read.
203
+ return out;
204
+ }
205
+ out.mode = 'local';
182
206
  const want = new Set(opts.roles ?? ROLES);
183
207
  // The selected adapters load in parallel; key reads are cached per provider, so a shared key
184
208
  // (Voyage for embed and rerank) is still read once.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "oortmem",
3
- "version": "0.1.0",
3
+ "version": "0.1.1",
4
4
  "description": "Oort: memory for AI coding agents. The oort command line, MCP server and agent connectors.",
5
5
  "keywords": [
6
6
  "oort",
@@ -42,14 +42,14 @@
42
42
  "dependencies": {
43
43
  "@clack/prompts": "1.8.1",
44
44
  "@modelcontextprotocol/server": "2.4.0",
45
- "@oortmem/claude-code": "0.1.0",
46
- "@oortmem/core": "0.1.0",
47
- "@oortmem/decide-jev": "0.1.0",
48
- "@oortmem/embed-voyage": "0.1.0",
49
- "@oortmem/mcp": "0.1.0",
50
- "@oortmem/rerank-voyage": "0.1.0",
51
- "@oortmem/store-sqlite": "0.1.0",
52
- "@oortmem/writer": "0.1.0",
45
+ "@oortmem/claude-code": "0.1.1",
46
+ "@oortmem/core": "0.1.1",
47
+ "@oortmem/decide-jev": "0.1.1",
48
+ "@oortmem/embed-voyage": "0.1.1",
49
+ "@oortmem/mcp": "0.1.1",
50
+ "@oortmem/rerank-voyage": "0.1.1",
51
+ "@oortmem/store-sqlite": "0.1.1",
52
+ "@oortmem/writer": "0.1.1",
53
53
  "commander": "15.0.0",
54
54
  "jsonc-parser": "3.3.1",
55
55
  "picocolors": "1.1.1",