atlas-mcp 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 (54) hide show
  1. package/.env.example +32 -0
  2. package/README.md +282 -0
  3. package/package.json +72 -0
  4. package/public/app/assets/app-CxbS1w9p.js +3981 -0
  5. package/public/app/assets/index-BA6nxCuI.css +1 -0
  6. package/public/app/assets/index-BXmIRrQH.js +177 -0
  7. package/public/app/index.html +27 -0
  8. package/public/assets/brain-atlas.LICENSE.txt +16 -0
  9. package/public/assets/brain-atlas.glb +0 -0
  10. package/public/assets/brain.obj +27282 -0
  11. package/public/fonts/DepartureMono-Regular.woff +0 -0
  12. package/public/fonts/DepartureMono-Regular.woff2 +0 -0
  13. package/scripts/sync-memory-vectors.js +46 -0
  14. package/src/audit.js +9 -0
  15. package/src/cli/args.js +87 -0
  16. package/src/cli/commands/add.js +103 -0
  17. package/src/cli/commands/config.js +228 -0
  18. package/src/cli/commands/delete.js +75 -0
  19. package/src/cli/commands/entities.js +39 -0
  20. package/src/cli/commands/entity.js +47 -0
  21. package/src/cli/commands/get.js +46 -0
  22. package/src/cli/commands/list.js +53 -0
  23. package/src/cli/commands/related.js +56 -0
  24. package/src/cli/commands/search.js +68 -0
  25. package/src/cli/commands/update.js +58 -0
  26. package/src/cli/deps.js +114 -0
  27. package/src/cli/env-file.js +44 -0
  28. package/src/cli/format.js +246 -0
  29. package/src/cli.js +187 -0
  30. package/src/cognitive-worker.js +381 -0
  31. package/src/db.js +2674 -0
  32. package/src/extraction-context.js +31 -0
  33. package/src/ingestion-service.js +387 -0
  34. package/src/ingestion-worker.js +225 -0
  35. package/src/llm-config.js +31 -0
  36. package/src/llm.js +789 -0
  37. package/src/logger.js +51 -0
  38. package/src/mcp-server.js +577 -0
  39. package/src/memory-comparison.js +421 -0
  40. package/src/related-memories.js +232 -0
  41. package/src/run-cognitive-worker.js +12 -0
  42. package/src/run-ingestion-worker.js +13 -0
  43. package/src/run-vector-worker.js +12 -0
  44. package/src/schemas.js +413 -0
  45. package/src/semantic-validation.js +430 -0
  46. package/src/server.js +827 -0
  47. package/src/shared/brain-regions.js +61 -0
  48. package/src/shared/entity-lens.js +249 -0
  49. package/src/shared/memory-placement.js +171 -0
  50. package/src/shared/memory-search.js +55 -0
  51. package/src/shared/region-anchors.js +112 -0
  52. package/src/shared/region-mapper.js +247 -0
  53. package/src/vector-store.js +546 -0
  54. package/src/vector-worker.js +71 -0
package/src/cli.js ADDED
@@ -0,0 +1,187 @@
1
+ #!/usr/bin/env node
2
+
3
+ import "dotenv/config";
4
+ import { pathToFileURL } from "node:url";
5
+ import { readFileSync, realpathSync } from "node:fs";
6
+ import { fileURLToPath } from "node:url";
7
+ import { dirname, resolve } from "node:path";
8
+ import { parseArgs } from "./cli/args.js";
9
+ import { defaultDependencies } from "./cli/deps.js";
10
+ import { assertAtlasModeSupported } from "./vector-store.js";
11
+
12
+ process.env.LOG_STREAM = "stderr";
13
+
14
+ import * as add from "./cli/commands/add.js";
15
+ import * as list from "./cli/commands/list.js";
16
+ import * as getCmd from "./cli/commands/get.js";
17
+ import * as search from "./cli/commands/search.js";
18
+ import * as related from "./cli/commands/related.js";
19
+ import * as entities from "./cli/commands/entities.js";
20
+ import * as entityCmd from "./cli/commands/entity.js";
21
+ import * as update from "./cli/commands/update.js";
22
+ import * as del from "./cli/commands/delete.js";
23
+ import * as config from "./cli/commands/config.js";
24
+
25
+ const COMMANDS = {
26
+ add,
27
+ list,
28
+ get: getCmd,
29
+ search,
30
+ related,
31
+ entities,
32
+ entity: entityCmd,
33
+ update,
34
+ delete: del,
35
+ config,
36
+ };
37
+
38
+ function loadVersion() {
39
+ try {
40
+ const here = dirname(realpathSync(fileURLToPath(import.meta.url)));
41
+ let dir = here;
42
+ for (let i = 0; i < 5; i += 1) {
43
+ try {
44
+ const pkg = JSON.parse(
45
+ readFileSync(resolve(dir, "package.json"), "utf8"),
46
+ );
47
+ if (pkg.name === "atlas-mcp") return pkg.version;
48
+ } catch {
49
+ // keep climbing
50
+ }
51
+ const parent = dirname(dir);
52
+ if (parent === dir) break;
53
+ dir = parent;
54
+ }
55
+ } catch {
56
+ // fall through
57
+ }
58
+ return "0.0.0";
59
+ }
60
+
61
+ const VERSION = loadVersion();
62
+
63
+ const TOP_HELP = `atlas ${VERSION}
64
+
65
+ Store, recall, and inspect memories from the terminal. Mirrors the Atlas
66
+ MCP tools; uses the same data store and LLM pipeline as the web app.
67
+
68
+ Usage: atlas <command> [args] [flags]
69
+
70
+ Commands:
71
+ add <text> Save a new memory (runs the LLM extraction pipeline)
72
+ list Browse recently stored memories
73
+ get <id> Fetch one memory with its full extraction
74
+ search <query> Find memories via hybrid search
75
+ related <id> Find memories connected to <id>
76
+ entities <query> Look up canonical entities by name
77
+ entity <id> List memories for one entity
78
+ update <id> Replace a memory's summary (reindexes the vector)
79
+ delete <id> Permanently delete a memory
80
+ config View and edit configuration
81
+
82
+ Global flags:
83
+ --help, -h Show this help (or per-command help: atlas <cmd> --help)
84
+ --version, -v Print the version
85
+
86
+ Examples:
87
+ atlas add "I prefer dark roast coffee" --type preference --title "Coffee" --json
88
+ atlas search "coffee" --strategy hybrid --json
89
+ atlas get mem_12ab34cd --json | jq .extraction
90
+ atlas delete mem_12ab34cd --yes
91
+ `;
92
+
93
+ function printUsageError(message) {
94
+ console.error(`Error: ${message}\n`);
95
+ console.error(`Run 'atlas --help' for a list of commands.`);
96
+ }
97
+
98
+ export async function runCli(argv = process.argv.slice(2), deps = defaultDependencies()) {
99
+ const { positional, flags } = parseArgs(argv);
100
+ const version = flags.version === true || flags.v === true;
101
+
102
+ if (version) {
103
+ console.log(VERSION);
104
+ return { exitCode: 0 };
105
+ }
106
+
107
+ const [commandName, ...rest] = positional;
108
+
109
+ if (!commandName) {
110
+ console.log(TOP_HELP);
111
+ return { exitCode: 0 };
112
+ }
113
+
114
+ const command = COMMANDS[commandName];
115
+ if (!command) {
116
+ printUsageError(`unknown command: ${commandName}`);
117
+ return { exitCode: 2 };
118
+ }
119
+
120
+ // The top-level parse already separated positional from flags, so the
121
+ // command receives the same flag set. We only re-parse `rest` (positional
122
+ // args after the command name) to keep `sub.positional` available and to
123
+ // detect per-command `--help` placed after the command name.
124
+ const sub = parseArgs(rest);
125
+ const helpFlag = sub.flags.help === true
126
+ || sub.flags.h === true
127
+ || flags.help === true
128
+ || flags.h === true;
129
+ if (helpFlag) {
130
+ console.log(command.meta.help);
131
+ return { exitCode: 0 };
132
+ }
133
+
134
+ const mergedFlags = { ...flags, ...sub.flags };
135
+ const json = mergedFlags.json === true || mergedFlags.json === "true";
136
+
137
+ try {
138
+ const result = await command.run({
139
+ positional: sub.positional,
140
+ flags: mergedFlags,
141
+ deps,
142
+ json,
143
+ });
144
+ return result || { exitCode: 0 };
145
+ } catch (error) {
146
+ console.error(`Error: ${error.message}`);
147
+ return { exitCode: 1 };
148
+ }
149
+ }
150
+
151
+ function isInvokedAsMain() {
152
+ if (!process.argv[1]) return false;
153
+ try {
154
+ const real = realpathSync(process.argv[1]);
155
+ return import.meta.url === pathToFileURL(real).href;
156
+ } catch {
157
+ return false;
158
+ }
159
+ }
160
+
161
+ const isMain = isInvokedAsMain();
162
+
163
+ if (isMain) {
164
+ let deps;
165
+ Promise.resolve()
166
+ .then(() => {
167
+ assertAtlasModeSupported();
168
+ deps = defaultDependencies();
169
+ return runCli(process.argv.slice(2), deps);
170
+ })
171
+ .then((result) => {
172
+ if (result && Number.isInteger(result.exitCode)) {
173
+ process.exitCode = result.exitCode;
174
+ }
175
+ })
176
+ .catch((error) => {
177
+ console.error(`Error: ${error.message}`);
178
+ process.exitCode = 1;
179
+ })
180
+ .finally(() => {
181
+ try {
182
+ deps?.closeDb();
183
+ } catch {
184
+ // Ignore: closeDb is idempotent and may not exist in tests.
185
+ }
186
+ });
187
+ }
@@ -0,0 +1,381 @@
1
+ import {
2
+ REGION_MAPPING_VERSION,
3
+ mapExtractionToRegions,
4
+ } from "./shared/region-mapper.js";
5
+
6
+ const DEFAULT_POLL_INTERVAL_MS = 1_000;
7
+ const DEFAULT_STALE_AFTER_MS = 5 * 60_000;
8
+ const DEFAULT_BASE_RETRY_MS = 1_000;
9
+ const DEFAULT_MAX_RETRY_MS = 60_000;
10
+ const DEFAULT_MAX_ATTEMPTS = 5;
11
+
12
+ function requiredFunction(value, name) {
13
+ if (typeof value !== "function") {
14
+ throw new TypeError(`cognitive worker requires ${name}()`);
15
+ }
16
+ return value;
17
+ }
18
+
19
+ function optionalFunction(value) {
20
+ return typeof value === "function" ? value : null;
21
+ }
22
+
23
+ function errorMessage(error) {
24
+ if (error instanceof Error) return error.message;
25
+ return String(error);
26
+ }
27
+
28
+ function jobId(job) {
29
+ return job.id ?? job.jobId ?? job.job_id;
30
+ }
31
+
32
+ function memoryId(job) {
33
+ return job.memoryId ?? job.memory_id ?? job.memory?.id;
34
+ }
35
+
36
+ function attemptCount(job) {
37
+ const value = Number(job.attempts ?? job.attemptCount ?? job.attempt_count);
38
+ return Number.isInteger(value) && value > 0 ? value : 1;
39
+ }
40
+
41
+ function maximumAttempts(job, fallback) {
42
+ const value = Number(
43
+ job.maxAttempts ?? job.max_attempts ?? job.maximumAttempts,
44
+ );
45
+ return Number.isInteger(value) && value > 0
46
+ ? Math.min(value, fallback)
47
+ : fallback;
48
+ }
49
+
50
+ function semanticFrom(value) {
51
+ if (!value) return null;
52
+ return (
53
+ value.semanticExtraction ??
54
+ value.semantic_extraction ??
55
+ value.extractionJson ??
56
+ value.extraction_json ??
57
+ value.extraction ??
58
+ null
59
+ );
60
+ }
61
+
62
+ function parseObject(value, label) {
63
+ if (typeof value !== "string") return value;
64
+ try {
65
+ return JSON.parse(value);
66
+ } catch {
67
+ throw new TypeError(`${label} must contain valid JSON`);
68
+ }
69
+ }
70
+
71
+ function validateOptions(options) {
72
+ for (const [name, value] of [
73
+ ["pollIntervalMs", options.pollIntervalMs],
74
+ ["staleAfterMs", options.staleAfterMs],
75
+ ["baseRetryMs", options.baseRetryMs],
76
+ ["maxRetryMs", options.maxRetryMs],
77
+ ]) {
78
+ if (!Number.isFinite(value) || value < 0) {
79
+ throw new RangeError(`${name} must be a non-negative finite number`);
80
+ }
81
+ }
82
+ if (!Number.isInteger(options.maxAttempts) || options.maxAttempts < 1) {
83
+ throw new RangeError("maxAttempts must be a positive integer");
84
+ }
85
+ if (options.maxRetryMs < options.baseRetryMs) {
86
+ throw new RangeError("maxRetryMs must be at least baseRetryMs");
87
+ }
88
+ }
89
+
90
+ export function calculateRetryDelay(
91
+ attempt,
92
+ {
93
+ baseRetryMs = DEFAULT_BASE_RETRY_MS,
94
+ maxRetryMs = DEFAULT_MAX_RETRY_MS,
95
+ } = {},
96
+ ) {
97
+ const exponent = Math.max(0, Number(attempt) - 1);
98
+ return Math.min(maxRetryMs, baseRetryMs * 2 ** exponent);
99
+ }
100
+
101
+ export function createCognitiveWorker({
102
+ db,
103
+ annotateMemory,
104
+ mapRegions = mapExtractionToRegions,
105
+ now = () => new Date(),
106
+ setTimer = setTimeout,
107
+ clearTimer = clearTimeout,
108
+ pollIntervalMs = DEFAULT_POLL_INTERVAL_MS,
109
+ staleAfterMs = DEFAULT_STALE_AFTER_MS,
110
+ baseRetryMs = DEFAULT_BASE_RETRY_MS,
111
+ maxRetryMs = DEFAULT_MAX_RETRY_MS,
112
+ maxAttempts = DEFAULT_MAX_ATTEMPTS,
113
+ mappingVersion = REGION_MAPPING_VERSION,
114
+ onError = () => {},
115
+ } = {}) {
116
+ if (!db || typeof db !== "object") {
117
+ throw new TypeError("cognitive worker requires a db adapter");
118
+ }
119
+ requiredFunction(annotateMemory, "annotateMemory");
120
+ requiredFunction(mapRegions, "mapRegions");
121
+
122
+ const options = {
123
+ pollIntervalMs,
124
+ staleAfterMs,
125
+ baseRetryMs,
126
+ maxRetryMs,
127
+ maxAttempts,
128
+ };
129
+ validateOptions(options);
130
+
131
+ const claimJob = requiredFunction(
132
+ db.claimAnnotationJob ?? db.claimPendingAnnotationJob,
133
+ "db.claimAnnotationJob",
134
+ );
135
+ const recoverJobs = requiredFunction(
136
+ db.recoverAnnotationJobs ?? db.recoverStaleAnnotationJobs ??
137
+ db.recoverStaleProcessingJobs,
138
+ "db.recoverAnnotationJobs",
139
+ );
140
+ const getMemory = optionalFunction(
141
+ db.getMemory ?? db.getMemoryForAnnotation,
142
+ );
143
+ const getSemantic = optionalFunction(
144
+ db.getLatestExtraction ?? db.getSemanticExtraction ??
145
+ db.getLatestSemanticExtraction,
146
+ );
147
+ const saveAnnotation = requiredFunction(
148
+ db.saveCognitiveAnnotation ?? db.persistCognitiveAnnotation,
149
+ "db.saveCognitiveAnnotation",
150
+ );
151
+ const saveActivations = requiredFunction(
152
+ db.saveRegionActivations ?? db.replaceRegionActivations,
153
+ "db.saveRegionActivations",
154
+ );
155
+ const completeJob = requiredFunction(
156
+ db.completeAnnotationJob ?? db.markAnnotationJobCompleted,
157
+ "db.completeAnnotationJob",
158
+ );
159
+ const retryJob = requiredFunction(
160
+ db.retryAnnotationJob ?? db.rescheduleAnnotationJob,
161
+ "db.retryAnnotationJob",
162
+ );
163
+ const failJob = optionalFunction(
164
+ db.failAnnotationJob ?? db.markAnnotationJobFailed,
165
+ );
166
+ const completeAnnotation = optionalFunction(db.completeCognitiveAnnotation);
167
+
168
+ let timer = null;
169
+ let resolvePoll = null;
170
+ let loopPromise = null;
171
+ let stopped = true;
172
+
173
+ const currentDate = () => {
174
+ const value = now();
175
+ const date = value instanceof Date ? value : new Date(value);
176
+ if (Number.isNaN(date.getTime())) {
177
+ throw new TypeError("now() must return a valid Date or date value");
178
+ }
179
+ return date;
180
+ };
181
+
182
+ const loadMemory = async (job) => {
183
+ const id = memoryId(job);
184
+ if (job.memory) return job.memory;
185
+ if (!id) throw new Error(`annotation job ${jobId(job)} has no memory id`);
186
+ if (!getMemory) {
187
+ throw new Error("annotation job has no memory and db.getMemory is absent");
188
+ }
189
+ const memory = await getMemory.call(db, id);
190
+ if (!memory) throw new Error(`memory not found: ${id}`);
191
+ return memory;
192
+ };
193
+
194
+ const loadSemantic = async (job, memory) => {
195
+ const embedded = semanticFrom(job) ?? semanticFrom(memory);
196
+ if (embedded) return parseObject(embedded, "semantic extraction");
197
+ if (!getSemantic) {
198
+ throw new Error(
199
+ "annotation job has no semantic extraction and db.getSemanticExtraction is absent",
200
+ );
201
+ }
202
+ const row = await getSemantic.call(db, memoryId(job) ?? memory.id);
203
+ const semantic = semanticFrom(row) ?? row;
204
+ if (!semantic) {
205
+ throw new Error(`semantic extraction not found: ${memoryId(job) ?? memory.id}`);
206
+ }
207
+ return parseObject(semantic, "semantic extraction");
208
+ };
209
+
210
+ const reschedule = async (job, error) => {
211
+ const attempts = attemptCount(job);
212
+ const attemptLimit = maximumAttempts(job, maxAttempts);
213
+ const id = jobId(job);
214
+ const message = errorMessage(error);
215
+ const failedAt = currentDate();
216
+
217
+ if (attempts >= attemptLimit && failJob) {
218
+ await failJob.call(db, id, {
219
+ error: message,
220
+ attempts,
221
+ failedAt: failedAt.toISOString(),
222
+ });
223
+ return { status: "failed", job, error };
224
+ }
225
+
226
+ const delayMs = calculateRetryDelay(attempts, {
227
+ baseRetryMs,
228
+ maxRetryMs,
229
+ });
230
+ const retryAt = new Date(failedAt.getTime() + delayMs);
231
+ await retryJob.call(db, {
232
+ jobId: id,
233
+ error: message,
234
+ attempts,
235
+ retryAt: retryAt.toISOString(),
236
+ terminal: attempts >= attemptLimit,
237
+ updatedAt: failedAt.toISOString(),
238
+ });
239
+ return {
240
+ status: attempts >= attemptLimit ? "failed" : "retrying",
241
+ job,
242
+ error,
243
+ retryAt,
244
+ delayMs,
245
+ };
246
+ };
247
+
248
+ const runOnce = async () => {
249
+ const claimedAt = currentDate();
250
+ const job = await claimJob.call(db, { now: claimedAt.toISOString() });
251
+ if (!job) return { status: "idle" };
252
+
253
+ try {
254
+ const memory = await loadMemory(job);
255
+ const queuedVersion = job.memoryVersion ?? job.memory_version;
256
+ if (queuedVersion !== undefined && memory.version !== undefined
257
+ && Number(queuedVersion) !== Number(memory.version)) {
258
+ await completeJob.call(db, {
259
+ jobId: jobId(job),
260
+ completedAt: currentDate().toISOString(),
261
+ });
262
+ return { status: "superseded", job, memory };
263
+ }
264
+ const semantic = await loadSemantic(job, memory);
265
+ const semanticInput = {
266
+ ...semantic,
267
+ text: semantic.text ?? memory.raw_text ?? memory.text,
268
+ };
269
+ const annotation = await annotateMemory(semanticInput, {
270
+ job,
271
+ memory,
272
+ });
273
+ if (!annotation || typeof annotation !== "object") {
274
+ throw new TypeError("annotateMemory() must return an annotation object");
275
+ }
276
+
277
+ const id = memoryId(job) ?? memory.id;
278
+ const activations = mapRegions({
279
+ ...semantic,
280
+ ...annotation,
281
+ });
282
+ const completedAt = currentDate().toISOString();
283
+ if (completeAnnotation) {
284
+ await completeAnnotation.call(db, {
285
+ memoryId: id,
286
+ annotation,
287
+ activations,
288
+ mappingVersion,
289
+ model: job.model,
290
+ schemaVersion: job.schemaVersion ?? job.schema_version,
291
+ jobId: jobId(job),
292
+ completedAt,
293
+ });
294
+ } else {
295
+ await saveAnnotation.call(db, {
296
+ memoryId: id,
297
+ annotation,
298
+ model: job.model,
299
+ schemaVersion: job.schemaVersion ?? job.schema_version,
300
+ jobId: jobId(job),
301
+ attempts: attemptCount(job),
302
+ });
303
+ await saveActivations.call(db, id, activations, mappingVersion);
304
+ await completeJob.call(db, { jobId: jobId(job), completedAt });
305
+ }
306
+
307
+ return { status: "completed", job, memory, annotation, activations };
308
+ } catch (error) {
309
+ return reschedule(job, error);
310
+ }
311
+ };
312
+
313
+ const waitForPoll = () =>
314
+ new Promise((resolve) => {
315
+ resolvePoll = resolve;
316
+ timer = setTimer(() => {
317
+ timer = null;
318
+ resolvePoll = null;
319
+ resolve();
320
+ }, pollIntervalMs);
321
+ });
322
+
323
+ const run = async ({ signal, recover = true } = {}) => {
324
+ stopped = false;
325
+ if (recover) {
326
+ const recoveredAt = currentDate();
327
+ await recoverJobs.call(db, {
328
+ now: recoveredAt.toISOString(),
329
+ retryAt: recoveredAt.toISOString(),
330
+ staleBefore: new Date(
331
+ recoveredAt.getTime() - staleAfterMs,
332
+ ).toISOString(),
333
+ });
334
+ }
335
+
336
+ while (!stopped && !signal?.aborted) {
337
+ const result = await runOnce();
338
+ if (result.status === "idle") await waitForPoll();
339
+ }
340
+ };
341
+
342
+ const start = () => {
343
+ if (loopPromise) return loopPromise;
344
+ stopped = false;
345
+ loopPromise = run()
346
+ .catch((error) => {
347
+ onError(error);
348
+ throw error;
349
+ })
350
+ .finally(() => {
351
+ loopPromise = null;
352
+ stopped = true;
353
+ });
354
+ return loopPromise;
355
+ };
356
+
357
+ const stop = async () => {
358
+ stopped = true;
359
+ if (timer !== null) {
360
+ clearTimer(timer);
361
+ timer = null;
362
+ resolvePoll?.();
363
+ resolvePoll = null;
364
+ }
365
+ await loopPromise;
366
+ };
367
+
368
+ return {
369
+ start,
370
+ run,
371
+ runOnce,
372
+ stop,
373
+ get running() {
374
+ return loopPromise !== null;
375
+ },
376
+ };
377
+ }
378
+
379
+ export async function runCognitiveWorkerCycle(dependencies) {
380
+ return createCognitiveWorker(dependencies).runOnce();
381
+ }