@pentatonic-ai/ai-agent-sdk 0.5.11 → 0.7.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 (119) hide show
  1. package/README.md +345 -174
  2. package/bin/__tests__/callback-server.test.js +70 -0
  3. package/bin/__tests__/credentials.test.js +58 -0
  4. package/bin/__tests__/login.test.js +210 -0
  5. package/bin/__tests__/pkce.test.js +39 -0
  6. package/bin/__tests__/whoami.test.js +77 -0
  7. package/bin/cli.js +109 -440
  8. package/bin/commands/config.js +251 -0
  9. package/bin/commands/login.js +219 -0
  10. package/bin/commands/whoami.js +41 -0
  11. package/bin/lib/callback-server.js +137 -0
  12. package/bin/lib/credentials.js +100 -0
  13. package/bin/lib/pkce.js +26 -0
  14. package/package.json +4 -2
  15. package/packages/doctor/__tests__/detect.test.js +2 -6
  16. package/packages/doctor/src/checks/local-memory.js +164 -196
  17. package/packages/doctor/src/detect.js +11 -3
  18. package/packages/memory/src/__tests__/corpus-chunkers.test.js +143 -0
  19. package/packages/memory/src/__tests__/corpus-discover.test.js +175 -0
  20. package/packages/memory/src/__tests__/corpus-ingest.test.js +236 -0
  21. package/packages/memory/src/__tests__/corpus-signatures.test.js +175 -0
  22. package/packages/memory/src/__tests__/corpus-state.test.js +161 -0
  23. package/packages/memory/src/__tests__/ingest-corpus-opts.test.js +129 -0
  24. package/packages/memory/src/__tests__/search-kind.test.js +108 -0
  25. package/packages/memory/src/corpus/adapters.js +398 -0
  26. package/packages/memory/src/corpus/chunkers.js +328 -0
  27. package/packages/memory/src/corpus/cli.js +613 -0
  28. package/packages/memory/src/corpus/discover.js +379 -0
  29. package/packages/memory/src/corpus/index.js +68 -0
  30. package/packages/memory/src/corpus/ingest.js +356 -0
  31. package/packages/memory/src/corpus/signatures.js +280 -0
  32. package/packages/memory/src/corpus/state.js +134 -0
  33. package/packages/memory/src/index.js +18 -0
  34. package/packages/memory/src/ingest.js +20 -11
  35. package/packages/memory/src/openclaw/index.js +39 -1
  36. package/packages/memory/src/search.js +30 -7
  37. package/packages/memory-engine/.env.example +13 -0
  38. package/packages/memory-engine/README.md +131 -0
  39. package/packages/memory-engine/bench/README.md +99 -0
  40. package/packages/memory-engine/bench/scorecards-engine/agent-coding__pentatonic-baseline__20260427-142523.json +1115 -0
  41. package/packages/memory-engine/bench/scorecards-engine/chat-recall__pentatonic-baseline__20260427-142648.json +819 -0
  42. package/packages/memory-engine/bench/scorecards-engine/circular-economy__pentatonic-baseline__20260427-142757.json +1278 -0
  43. package/packages/memory-engine/bench/scorecards-engine/customer-support__pentatonic-baseline__20260427-142900.json +1018 -0
  44. package/packages/memory-engine/bench/scorecards-engine/marketplace-ops__pentatonic-baseline__20260427-142957.json +1038 -0
  45. package/packages/memory-engine/bench/scorecards-engine/product-catalogue__pentatonic-baseline__20260427-143122.json +961 -0
  46. package/packages/memory-engine/bench/scorecards-engine-via-docker/agent-coding__pentatonic-memory__20260427-161812.json +1115 -0
  47. package/packages/memory-engine/bench/scorecards-engine-via-docker/chat-recall__pentatonic-memory__20260427-161701.json +819 -0
  48. package/packages/memory-engine/bench/scorecards-engine-via-docker/circular-economy__pentatonic-memory__20260427-161713.json +1278 -0
  49. package/packages/memory-engine/bench/scorecards-engine-via-docker/customer-support__pentatonic-memory__20260427-161723.json +1018 -0
  50. package/packages/memory-engine/bench/scorecards-engine-via-docker/marketplace-ops__pentatonic-memory__20260427-161732.json +1038 -0
  51. package/packages/memory-engine/bench/scorecards-engine-via-docker/product-catalogue__pentatonic-memory__20260427-161741.json +937 -0
  52. package/packages/memory-engine/bench/scorecards-engine-via-l2-7-layer-populated/agent-coding__pentatonic-memory__20260427-184718.json +1115 -0
  53. package/packages/memory-engine/bench/scorecards-engine-via-l2-7-layer-populated/chat-recall__pentatonic-memory__20260427-184614.json +819 -0
  54. package/packages/memory-engine/bench/scorecards-engine-via-l2-7-layer-populated/circular-economy__pentatonic-memory__20260427-184809.json +1278 -0
  55. package/packages/memory-engine/bench/scorecards-engine-via-l2-7-layer-populated/customer-support__pentatonic-memory__20260427-184854.json +1018 -0
  56. package/packages/memory-engine/bench/scorecards-engine-via-l2-7-layer-populated/marketplace-ops__pentatonic-memory__20260427-184929.json +1038 -0
  57. package/packages/memory-engine/bench/scorecards-engine-via-l2-7-layer-populated/product-catalogue__pentatonic-memory__20260427-185015.json +961 -0
  58. package/packages/memory-engine/bench/scorecards-engine-via-l2-empty-layers/agent-coding__pentatonic-memory__20260427-175252.json +1115 -0
  59. package/packages/memory-engine/bench/scorecards-engine-via-l2-empty-layers/chat-recall__pentatonic-memory__20260427-175312.json +819 -0
  60. package/packages/memory-engine/bench/scorecards-engine-via-l2-empty-layers/circular-economy__pentatonic-memory__20260427-175335.json +1278 -0
  61. package/packages/memory-engine/bench/scorecards-engine-via-l2-empty-layers/customer-support__pentatonic-memory__20260427-175355.json +1018 -0
  62. package/packages/memory-engine/bench/scorecards-engine-via-l2-empty-layers/marketplace-ops__pentatonic-memory__20260427-175413.json +1038 -0
  63. package/packages/memory-engine/bench/scorecards-engine-via-l2-empty-layers/product-catalogue__pentatonic-memory__20260427-175430.json +883 -0
  64. package/packages/memory-engine/bench/scorecards-engine-via-shim/agent-coding__pentatonic-memory__20260427-155409.json +1115 -0
  65. package/packages/memory-engine/bench/scorecards-engine-via-shim/chat-recall__pentatonic-memory__20260427-155421.json +819 -0
  66. package/packages/memory-engine/bench/scorecards-engine-via-shim/circular-economy__pentatonic-memory__20260427-155433.json +1278 -0
  67. package/packages/memory-engine/bench/scorecards-engine-via-shim/customer-support__pentatonic-memory__20260427-155443.json +1018 -0
  68. package/packages/memory-engine/bench/scorecards-engine-via-shim/marketplace-ops__pentatonic-memory__20260427-155453.json +1038 -0
  69. package/packages/memory-engine/bench/scorecards-engine-via-shim/product-catalogue__pentatonic-memory__20260427-155503.json +937 -0
  70. package/packages/memory-engine/bench/scorecards-pentatonic-baseline/agent-coding__pentatonic-memory-latest__20260427-145103.json +1115 -0
  71. package/packages/memory-engine/bench/scorecards-pentatonic-baseline/agent-coding__pentatonic-memory__20260427-144909.json +1115 -0
  72. package/packages/memory-engine/bench/scorecards-pentatonic-baseline/chat-recall__pentatonic-memory-latest__20260427-145153.json +819 -0
  73. package/packages/memory-engine/bench/scorecards-pentatonic-baseline/chat-recall__pentatonic-memory__20260427-145120.json +542 -0
  74. package/packages/memory-engine/bench/scorecards-pentatonic-baseline/circular-economy__pentatonic-memory-latest__20260427-145313.json +1278 -0
  75. package/packages/memory-engine/bench/scorecards-pentatonic-baseline/circular-economy__pentatonic-memory__20260427-145207.json +894 -0
  76. package/packages/memory-engine/bench/scorecards-pentatonic-baseline/customer-support__pentatonic-memory-latest__20260427-145412.json +1018 -0
  77. package/packages/memory-engine/bench/scorecards-pentatonic-baseline/customer-support__pentatonic-memory__20260427-145327.json +680 -0
  78. package/packages/memory-engine/bench/scorecards-pentatonic-baseline/marketplace-ops__pentatonic-memory-latest__20260427-145517.json +1038 -0
  79. package/packages/memory-engine/bench/scorecards-pentatonic-baseline/marketplace-ops__pentatonic-memory__20260427-145422.json +693 -0
  80. package/packages/memory-engine/bench/scorecards-pentatonic-baseline/product-catalogue__pentatonic-memory-latest__20260427-145616.json +961 -0
  81. package/packages/memory-engine/bench/scorecards-pentatonic-baseline/product-catalogue__pentatonic-memory__20260427-145528.json +727 -0
  82. package/packages/memory-engine/compat/Dockerfile +11 -0
  83. package/packages/memory-engine/compat/server.py +680 -0
  84. package/packages/memory-engine/docker-compose.yml +243 -0
  85. package/packages/memory-engine/docs/MIGRATION.md +178 -0
  86. package/packages/memory-engine/docs/RUNBOOK-AWS.md +375 -0
  87. package/packages/memory-engine/docs/why-v05-underperforms.md +138 -0
  88. package/packages/memory-engine/engine/README.md +52 -0
  89. package/packages/memory-engine/engine/l2-hybridrag-proxy.py +1543 -0
  90. package/packages/memory-engine/engine/l5-comms-layer.py +663 -0
  91. package/packages/memory-engine/engine/l6-document-store.py +1018 -0
  92. package/packages/memory-engine/engine/services/l2/Dockerfile +41 -0
  93. package/packages/memory-engine/engine/services/l2/init_databases.py +81 -0
  94. package/packages/memory-engine/engine/services/l2/l2-hybridrag-proxy.py +1543 -0
  95. package/packages/memory-engine/engine/services/l4/Dockerfile +15 -0
  96. package/packages/memory-engine/engine/services/l4/server.py +235 -0
  97. package/packages/memory-engine/engine/services/l5/Dockerfile +9 -0
  98. package/packages/memory-engine/engine/services/l5/l5-comms-layer.py +678 -0
  99. package/packages/memory-engine/engine/services/l6/Dockerfile +11 -0
  100. package/packages/memory-engine/engine/services/l6/l6-document-store.py +1016 -0
  101. package/packages/memory-engine/engine/services/nv-embed/Dockerfile +28 -0
  102. package/packages/memory-engine/engine/services/nv-embed/server.py +152 -0
  103. package/packages/memory-engine/pme_memory/__init__.py +0 -0
  104. package/packages/memory-engine/pme_memory/__main__.py +129 -0
  105. package/packages/memory-engine/pme_memory/artifacts.py +95 -0
  106. package/packages/memory-engine/pme_memory/embed.py +74 -0
  107. package/packages/memory-engine/pme_memory/health.py +36 -0
  108. package/packages/memory-engine/pme_memory/hygiene.py +159 -0
  109. package/packages/memory-engine/pme_memory/indexer.py +200 -0
  110. package/packages/memory-engine/pme_memory/needs.py +55 -0
  111. package/packages/memory-engine/pme_memory/provenance.py +80 -0
  112. package/packages/memory-engine/pme_memory/scoring.py +168 -0
  113. package/packages/memory-engine/pme_memory/search.py +52 -0
  114. package/packages/memory-engine/pme_memory/store.py +86 -0
  115. package/packages/memory-engine/pme_memory/synthesis.py +114 -0
  116. package/packages/memory-engine/pyproject.toml +65 -0
  117. package/packages/memory-engine/scripts/kg-extractor.py +557 -0
  118. package/packages/memory-engine/scripts/kg-preflexor-v2.py +738 -0
  119. package/packages/memory-engine/tests/test_api_contract.sh +57 -0
@@ -0,0 +1,100 @@
1
+ import { mkdir, writeFile, readFile, chmod } from "node:fs/promises";
2
+ import { existsSync } from "node:fs";
3
+ import { homedir } from "node:os";
4
+ import { join } from "node:path";
5
+
6
+ /**
7
+ * Resolve the path to ~/.config/tes/credentials.json (or the
8
+ * XDG_CONFIG_HOME equivalent).
9
+ *
10
+ * Same path the corpus CLI already uses — single source of truth for
11
+ * the SDK's tenant config across login and ingest commands.
12
+ */
13
+ export function credentialsPath() {
14
+ const base = process.env.XDG_CONFIG_HOME || join(homedir(), ".config");
15
+ return join(base, "tes", "credentials.json");
16
+ }
17
+
18
+ /**
19
+ * Persist tenant credentials to ~/.config/tes/credentials.json with
20
+ * mode 0600. Overwrites any existing file (login = re-auth).
21
+ *
22
+ * @param {object} creds
23
+ * @param {string} creds.endpoint - e.g. https://tes-demo.api.pentatonic.com
24
+ * @param {string} creds.clientId - tenant slug
25
+ * @param {string} creds.apiKey - the long-lived tes_* token
26
+ */
27
+ export async function writeCredentials({ endpoint, clientId, apiKey }) {
28
+ if (!endpoint || !clientId || !apiKey) {
29
+ throw new Error("writeCredentials: endpoint, clientId, apiKey all required");
30
+ }
31
+ const path = credentialsPath();
32
+ await mkdir(join(path, ".."), { recursive: true });
33
+ await writeFile(
34
+ path,
35
+ JSON.stringify({ endpoint, clientId, apiKey }, null, 2) + "\n",
36
+ { mode: 0o600 }
37
+ );
38
+ // mkdir + writeFile race can leave a transient wider mode on some
39
+ // filesystems; chmod after to be sure.
40
+ await chmod(path, 0o600);
41
+ }
42
+
43
+ /**
44
+ * Read existing credentials. Returns null if the file is absent or
45
+ * unreadable.
46
+ */
47
+ export async function readCredentials() {
48
+ const path = credentialsPath();
49
+ if (!existsSync(path)) return null;
50
+ try {
51
+ const raw = await readFile(path, "utf8");
52
+ const parsed = JSON.parse(raw);
53
+ if (!parsed.endpoint || !parsed.clientId || !parsed.apiKey) return null;
54
+ return parsed;
55
+ } catch {
56
+ return null;
57
+ }
58
+ }
59
+
60
+ /**
61
+ * Ping TES with the stored credentials. Returns { ok, clientName }
62
+ * on success or { ok: false, status, error } on failure.
63
+ *
64
+ * Used by `whoami` to surface "logged in to / creds invalid" messages.
65
+ *
66
+ * Uses `memoryLayers(clientId)` — a query the SDK's agent-events role
67
+ * is permitted to run. Admin-only queries like `client(id)` and
68
+ * `myOrganization` would 403 with the service-account API key the
69
+ * SDK mints. Returning *anything* (even an empty array) confirms
70
+ * the token verified server-side; a 401 means it didn't.
71
+ */
72
+ export async function pingTes(creds) {
73
+ if (!creds) return { ok: false, error: "no credentials" };
74
+ const query = `query Ping($id: String!) { memoryLayers(clientId: $id) { id } }`;
75
+ try {
76
+ const res = await fetch(`${creds.endpoint}/api/graphql`, {
77
+ method: "POST",
78
+ headers: {
79
+ "Content-Type": "application/json",
80
+ Authorization: `Bearer ${creds.apiKey}`,
81
+ "x-client-id": creds.clientId,
82
+ },
83
+ body: JSON.stringify({ query, variables: { id: creds.clientId } }),
84
+ });
85
+ if (res.status === 401) {
86
+ return { ok: false, status: 401, error: "credentials invalid" };
87
+ }
88
+ if (!res.ok) {
89
+ return { ok: false, status: res.status, error: await res.text() };
90
+ }
91
+ const body = await res.json();
92
+ if (body.errors?.length) {
93
+ return { ok: false, error: body.errors[0].message };
94
+ }
95
+ // No tenant display name from this query; CLI shows clientId only.
96
+ return { ok: true, clientName: creds.clientId };
97
+ } catch (err) {
98
+ return { ok: false, error: err.message };
99
+ }
100
+ }
@@ -0,0 +1,26 @@
1
+ import { randomBytes, createHash } from "node:crypto";
2
+
3
+ /**
4
+ * Generate a PKCE verifier + S256 challenge per RFC 7636 plus a
5
+ * CSRF-protection state token. All three are URL-safe base64.
6
+ *
7
+ * Used by the SDK login command to bind the localhost callback against
8
+ * the OAuth code we just issued — verifier is sent to /oauth/token at
9
+ * exchange time; challenge is sent up-front at /cli-init.
10
+ */
11
+ export function generatePKCE() {
12
+ // 64 random bytes -> 86-char base64url verifier (within 43-128 range).
13
+ const verifier = base64url(randomBytes(64));
14
+ const challenge = base64url(createHash("sha256").update(verifier).digest());
15
+ // 32 bytes -> 43-char base64url state.
16
+ const state = base64url(randomBytes(32));
17
+ return { verifier, challenge, state };
18
+ }
19
+
20
+ function base64url(buf) {
21
+ return buf
22
+ .toString("base64")
23
+ .replace(/\+/g, "-")
24
+ .replace(/\//g, "_")
25
+ .replace(/=+$/, "");
26
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pentatonic-ai/ai-agent-sdk",
3
- "version": "0.5.11",
3
+ "version": "0.7.0",
4
4
  "description": "TES SDK — LLM observability and lifecycle tracking via Pentatonic Thing Event System. Track token usage, tool calls, and conversations. Manage things through event-sourced lifecycle stages with AI enrichment and vector search.",
5
5
  "type": "module",
6
6
  "main": "./dist/index.cjs",
@@ -14,7 +14,8 @@
14
14
  "./memory/server": "./packages/memory/src/server.js",
15
15
  "./memory/openclaw": "./packages/memory/src/openclaw/index.js",
16
16
  "./doctor": "./packages/doctor/src/index.js",
17
- "./memory/hosted": "./packages/memory/src/hosted.js"
17
+ "./memory/hosted": "./packages/memory/src/hosted.js",
18
+ "./memory/corpus": "./packages/memory/src/corpus/index.js"
18
19
  },
19
20
  "bin": {
20
21
  "ai-agent-sdk": "./bin/cli.js"
@@ -24,6 +25,7 @@
24
25
  "src",
25
26
  "bin",
26
27
  "packages/memory",
28
+ "packages/memory-engine",
27
29
  "packages/doctor",
28
30
  "build.js",
29
31
  "README.md",
@@ -39,13 +39,9 @@ describe("detectPaths", () => {
39
39
  expect(paths.has(PATHS.PLATFORM)).toBe(true);
40
40
  });
41
41
 
42
- it("detects local from full DSN trio", () => {
42
+ it("detects local from MEMORY_ENGINE_URL", () => {
43
43
  const paths = detectPaths({
44
- env: {
45
- DATABASE_URL: "postgres://x",
46
- EMBEDDING_URL: "http://x/v1",
47
- LLM_URL: "http://x/v1",
48
- },
44
+ env: { MEMORY_ENGINE_URL: "http://localhost:8099" },
49
45
  });
50
46
  expect(paths.has(PATHS.LOCAL)).toBe(true);
51
47
  });
@@ -1,18 +1,24 @@
1
1
  /**
2
- * Local Memory path checks.
2
+ * Local memory engine checks.
3
3
  *
4
- * Targets the stack started by `npx @pentatonic-ai/ai-agent-sdk memory`:
5
- * - PostgreSQL with pgvector
6
- * - Ollama (or any OpenAI-compatible embedding + chat endpoint)
7
- * - Memory MCP server on PORT (default 3333)
4
+ * Targets the engine stack started by:
5
+ * cd packages/memory-engine && docker compose up -d
8
6
  *
9
- * Configuration is read from env vars used by packages/memory/src/server.js
10
- * (DATABASE_URL, EMBEDDING_URL/MODEL, LLM_URL/MODEL, PORT, API_KEY) so this
11
- * stays drift-free with the actual server.
7
+ * The engine exposes a compat HTTP shim on port 8099 (or whatever
8
+ * memory_url is set to in the user's plugin config). All checks are
9
+ * just HTTP calls + plugin-config parsing no direct Postgres/pg_vector
10
+ * probing. The legacy Postgres+Ollama checks were removed when the
11
+ * v0.5.x in-process memory server was deprecated.
12
12
  */
13
13
 
14
+ import { existsSync, readFileSync } from "node:fs";
15
+ import { homedir } from "node:os";
16
+ import { join } from "node:path";
17
+
14
18
  import { SEVERITY } from "../index.js";
15
19
 
20
+ const DEFAULT_ENGINE_URL = "http://localhost:8099";
21
+
16
22
  async function fetchWithTimeout(url, opts = {}, timeoutMs = 5000) {
17
23
  return await fetch(url, {
18
24
  ...opts,
@@ -20,177 +26,149 @@ async function fetchWithTimeout(url, opts = {}, timeoutMs = 5000) {
20
26
  });
21
27
  }
22
28
 
23
- function checkPostgres() {
29
+ function parseFrontmatter(content) {
30
+ const m = content.match(/^---\n([\s\S]*?)\n---/);
31
+ if (!m) return null;
32
+ const out = {};
33
+ for (const line of m[1].split("\n")) {
34
+ const kv = line.match(/^(\w+):\s*(.+)$/);
35
+ if (kv) out[kv[1]] = kv[2].trim();
36
+ }
37
+ return out;
38
+ }
39
+
40
+ function findPluginConfig() {
41
+ const candidates = [
42
+ process.env.CLAUDE_CONFIG_DIR,
43
+ join(homedir(), ".claude-pentatonic"),
44
+ join(homedir(), ".claude"),
45
+ ].filter(Boolean);
46
+ for (const dir of candidates) {
47
+ const p = join(dir, "tes-memory.local.md");
48
+ if (existsSync(p)) return p;
49
+ }
50
+ return null;
51
+ }
52
+
53
+ function resolveEngineUrl() {
54
+ // 1. env var override
55
+ if (process.env.MEMORY_ENGINE_URL) return process.env.MEMORY_ENGINE_URL;
56
+ // 2. plugin config
57
+ const cfgPath = findPluginConfig();
58
+ if (cfgPath) {
59
+ try {
60
+ const fm = parseFrontmatter(readFileSync(cfgPath, "utf-8"));
61
+ if (fm?.mode === "local" && fm.memory_url) return fm.memory_url;
62
+ } catch {
63
+ // fall through
64
+ }
65
+ }
66
+ // 3. default
67
+ return DEFAULT_ENGINE_URL;
68
+ }
69
+
70
+ function checkPluginConfig() {
24
71
  return {
25
- name: "postgres reachable",
26
- severity: SEVERITY.CRITICAL,
72
+ name: "plugin config (tes-memory.local.md)",
73
+ severity: SEVERITY.WARNING,
27
74
  run: async () => {
28
- const dsn = process.env.DATABASE_URL;
29
- if (!dsn) {
75
+ const cfgPath = findPluginConfig();
76
+ if (!cfgPath) {
30
77
  return {
31
78
  ok: false,
32
- msg: "DATABASE_URL not set",
79
+ msg: "no tes-memory.local.md found — run `npx @pentatonic-ai/ai-agent-sdk config local`",
33
80
  };
34
81
  }
35
- // Lazy-import pg so users on hosted/platform paths don't pay the cost.
36
- let pg;
82
+ let fm;
37
83
  try {
38
- pg = (await import("pg")).default;
39
- } catch {
84
+ fm = parseFrontmatter(readFileSync(cfgPath, "utf-8"));
85
+ } catch (err) {
86
+ return { ok: false, msg: `${cfgPath}: ${err.message}` };
87
+ }
88
+ if (!fm) {
89
+ return { ok: false, msg: `${cfgPath}: no parseable frontmatter` };
90
+ }
91
+ if (fm.mode !== "local") {
40
92
  return {
41
93
  ok: false,
42
- msg: "'pg' not installedrun `npm install pg`",
94
+ msg: `${cfgPath}: mode is "${fm.mode || "(unset)"}"expected "local"`,
95
+ detail: { mode: fm.mode, path: cfgPath },
43
96
  };
44
97
  }
45
- const client = new pg.Client({ connectionString: dsn });
46
- try {
47
- await client.connect();
48
- const v = await client.query("SELECT version()");
49
- return {
50
- ok: true,
51
- msg: v.rows[0].version.split(",")[0],
52
- detail: { version: v.rows[0].version },
53
- };
54
- } catch (err) {
98
+ if (!fm.memory_url) {
55
99
  return {
56
100
  ok: false,
57
- msg: err.message,
101
+ msg: `${cfgPath}: memory_url not set`,
102
+ detail: { path: cfgPath },
58
103
  };
59
- } finally {
60
- await client.end().catch(() => {});
61
104
  }
105
+ return {
106
+ ok: true,
107
+ msg: `${fm.memory_url} (${cfgPath})`,
108
+ detail: { memory_url: fm.memory_url, path: cfgPath },
109
+ };
62
110
  },
63
111
  };
64
112
  }
65
113
 
66
- function checkPgvector() {
114
+ function checkEngineHealth() {
67
115
  return {
68
- name: "pgvector extension",
116
+ name: "engine /health",
69
117
  severity: SEVERITY.CRITICAL,
70
118
  run: async () => {
71
- const dsn = process.env.DATABASE_URL;
72
- if (!dsn) return { ok: false, msg: "DATABASE_URL not set" };
73
- let pg;
119
+ const url = resolveEngineUrl();
74
120
  try {
75
- pg = (await import("pg")).default;
76
- } catch {
77
- return { ok: false, msg: "'pg' not installed" };
78
- }
79
- const client = new pg.Client({ connectionString: dsn });
80
- try {
81
- await client.connect();
82
- const r = await client.query(
83
- "SELECT extversion FROM pg_extension WHERE extname='vector'"
84
- );
85
- if (!r.rowCount) {
86
- return {
87
- ok: false,
88
- msg: "pgvector not installed — run CREATE EXTENSION vector",
89
- };
121
+ const res = await fetchWithTimeout(`${url.replace(/\/$/, "")}/health`);
122
+ if (!res.ok) {
123
+ return { ok: false, msg: `HTTP ${res.status} from ${url}/health` };
90
124
  }
125
+ const data = await res.json();
91
126
  return {
92
127
  ok: true,
93
- msg: `pgvector ${r.rows[0].extversion}`,
94
- detail: { version: r.rows[0].extversion },
128
+ msg: `${data.engine || "engine"} v${data.version || "?"} (${data.status})`,
129
+ detail: data,
95
130
  };
96
131
  } catch (err) {
97
- return { ok: false, msg: err.message };
98
- } finally {
99
- await client.end().catch(() => {});
132
+ return {
133
+ ok: false,
134
+ msg: `${url}/health unreachable: ${err.message}`,
135
+ detail: { url },
136
+ };
100
137
  }
101
138
  },
102
139
  };
103
140
  }
104
141
 
105
- function checkMigrations() {
142
+ function checkEngineLayers() {
106
143
  return {
107
- name: "schema migrations applied",
144
+ name: "engine layers (L0–L6)",
108
145
  severity: SEVERITY.WARNING,
109
146
  run: async () => {
110
- const dsn = process.env.DATABASE_URL;
111
- if (!dsn) return { ok: false, msg: "DATABASE_URL not set" };
112
- let pg;
113
- try {
114
- pg = (await import("pg")).default;
115
- } catch {
116
- return { ok: false, msg: "'pg' not installed" };
117
- }
118
- const client = new pg.Client({ connectionString: dsn });
147
+ const url = resolveEngineUrl();
119
148
  try {
120
- await client.connect();
121
- // The migration runner creates schema_migrations on first apply.
122
- const r = await client.query(
123
- "SELECT count(*) AS n FROM schema_migrations"
124
- );
125
- const n = parseInt(r.rows[0].n, 10);
126
- if (n === 0) {
127
- return {
128
- ok: false,
129
- msg: "schema_migrations is empty — start the memory server to run migrations",
130
- };
149
+ const res = await fetchWithTimeout(`${url.replace(/\/$/, "")}/health`);
150
+ if (!res.ok) return { ok: false, msg: `HTTP ${res.status}` };
151
+ const data = await res.json();
152
+ const layers = data.layers || {};
153
+ const expected = ["l0", "l1", "l2", "l3", "l4", "l5", "l6"];
154
+ const okList = [];
155
+ const degradedList = [];
156
+ for (const k of expected) {
157
+ const status = layers[k];
158
+ if (status === "ok") okList.push(k);
159
+ else if (status) degradedList.push(`${k}=${status}`);
131
160
  }
132
- return { ok: true, msg: `${n} migrations applied`, detail: { n } };
133
- } catch (err) {
134
- if (/relation .* does not exist/.test(err.message)) {
161
+ if (degradedList.length === 0) {
135
162
  return {
136
- ok: false,
137
- msg: "schema_migrations table missing — start the memory server first",
163
+ ok: true,
164
+ msg: `${okList.length}/7 ok`,
165
+ detail: { layers },
138
166
  };
139
167
  }
140
- return { ok: false, msg: err.message };
141
- } finally {
142
- await client.end().catch(() => {});
143
- }
144
- },
145
- };
146
- }
147
-
148
- function checkEmbeddingEndpoint() {
149
- return {
150
- name: "embedding endpoint",
151
- severity: SEVERITY.CRITICAL,
152
- run: async () => {
153
- const url = process.env.EMBEDDING_URL;
154
- const model = process.env.EMBEDDING_MODEL;
155
- if (!url || !model) {
156
168
  return {
157
169
  ok: false,
158
- msg: "EMBEDDING_URL or EMBEDDING_MODEL not set",
159
- };
160
- }
161
- // Probe with a 1-token embed call against the OpenAI-compatible API.
162
- try {
163
- const headers = { "Content-Type": "application/json" };
164
- if (process.env.API_KEY) {
165
- headers.Authorization = `Bearer ${process.env.API_KEY}`;
166
- }
167
- const res = await fetchWithTimeout(
168
- `${url.replace(/\/$/, "")}/embeddings`,
169
- {
170
- method: "POST",
171
- headers,
172
- body: JSON.stringify({ model, input: "ping" }),
173
- }
174
- );
175
- if (!res.ok) {
176
- const body = await res.text().catch(() => "");
177
- return {
178
- ok: false,
179
- msg: `HTTP ${res.status}: ${body.slice(0, 120)}`,
180
- };
181
- }
182
- const data = await res.json();
183
- const dim = data.data?.[0]?.embedding?.length;
184
- if (!dim) {
185
- return {
186
- ok: false,
187
- msg: "endpoint responded but returned no embedding",
188
- };
189
- }
190
- return {
191
- ok: true,
192
- msg: `${model} ok (${dim}-dim)`,
193
- detail: { url, model, dim },
170
+ msg: `${okList.length}/7 ok; degraded: ${degradedList.join(", ")}`,
171
+ detail: { layers },
194
172
  };
195
173
  } catch (err) {
196
174
  return { ok: false, msg: err.message };
@@ -199,45 +177,33 @@ function checkEmbeddingEndpoint() {
199
177
  };
200
178
  }
201
179
 
202
- function checkLlmEndpoint() {
180
+ function checkEmbeddingPath() {
181
+ // Surfaces the engine's view of nv_embed (the URL it points at). If
182
+ // that's "unreachable" or "http 4xx/5xx", L4/L5/L6 indexing won't work.
183
+ // The engine reports this in /health under layers.nv_embed.
203
184
  return {
204
- name: "llm endpoint",
205
- severity: SEVERITY.CRITICAL,
185
+ name: "embedding endpoint (engine→external)",
186
+ severity: SEVERITY.WARNING,
206
187
  run: async () => {
207
- const url = process.env.LLM_URL;
208
- const model = process.env.LLM_MODEL;
209
- if (!url || !model) {
210
- return {
211
- ok: false,
212
- msg: "LLM_URL or LLM_MODEL not set",
213
- };
214
- }
215
- // /models is cheap and present on every OpenAI-compatible server.
188
+ const url = resolveEngineUrl();
216
189
  try {
217
- const headers = {};
218
- if (process.env.API_KEY) {
219
- headers.Authorization = `Bearer ${process.env.API_KEY}`;
220
- }
221
- const res = await fetchWithTimeout(
222
- `${url.replace(/\/$/, "")}/models`,
223
- { headers }
224
- );
225
- if (!res.ok) {
226
- return { ok: false, msg: `HTTP ${res.status}` };
227
- }
190
+ const res = await fetchWithTimeout(`${url.replace(/\/$/, "")}/health`);
191
+ if (!res.ok) return { ok: false, msg: `engine /health HTTP ${res.status}` };
228
192
  const data = await res.json();
229
- const ids = (data.data || []).map((m) => m.id);
230
- if (model && !ids.includes(model)) {
193
+ const nv = data.layers?.nv_embed;
194
+ if (nv === "ok") {
195
+ return { ok: true, msg: "reachable" };
196
+ }
197
+ if (!nv) {
231
198
  return {
232
199
  ok: false,
233
- msg: `${model} not loaded; available: ${ids.slice(0, 3).join(", ")}`,
234
- detail: { url, requested: model, available: ids },
200
+ msg: "engine /health did not report nv_embed status",
235
201
  };
236
202
  }
237
203
  return {
238
- ok: true,
239
- msg: `${model} loaded`,
240
- detail: { url, model },
204
+ ok: false,
205
+ msg: `nv_embed=${nv} — L4/L5/L6 indexing will fail. Check NV_EMBED_URL in packages/memory-engine/.env`,
206
+ detail: { nv_embed: nv },
241
207
  };
242
208
  } catch (err) {
243
209
  return { ok: false, msg: err.message };
@@ -246,45 +212,47 @@ function checkLlmEndpoint() {
246
212
  };
247
213
  }
248
214
 
249
- function checkMemoryServer() {
215
+ function checkOllamaBindIfPresent() {
216
+ // If the user is using Ollama as their embedding backend AND running
217
+ // the engine in Docker, Ollama needs to be bound on all interfaces so
218
+ // containers can reach it via host.docker.internal. Common gotcha.
219
+ // We probe by checking whether Ollama is listening on something other
220
+ // than 127.0.0.1 — we can't directly read systemd config, so we fall
221
+ // back to probing 0.0.0.0:11434 from the host (which always works if
222
+ // any interface is listening) vs trying to detect the bind address.
223
+ //
224
+ // Approach: hit /api/tags. If reachable on 127.0.0.1, Ollama is up;
225
+ // we then warn if the user is in a Docker context (engine reachable)
226
+ // because that's the configuration where the bind matters.
250
227
  return {
251
- name: "memory server",
252
- severity: SEVERITY.WARNING,
228
+ name: "Ollama bind config (if used)",
229
+ severity: SEVERITY.INFO,
253
230
  run: async () => {
254
- // The MCP server uses stdio by default but exposes PORT for HTTP/SSE.
255
- // If PORT isn't bound we can't probe — that's not an error, just info.
256
- const port = process.env.PORT || "3333";
257
- const url = `http://127.0.0.1:${port}/`;
258
231
  try {
259
- const res = await fetchWithTimeout(url, {}, 2000);
260
- return {
261
- ok: true,
262
- msg: `port ${port} reachable (HTTP ${res.status})`,
263
- detail: { url, status: res.status },
264
- };
265
- } catch (err) {
266
- // Connection refused is the common case when the server is run
267
- // via stdio only — surface as info so users aren't alarmed.
268
- if (/ECONNREFUSED|fetch failed/.test(err.message)) {
269
- return {
270
- ok: true,
271
- msg: `port ${port} not bound (running via stdio? — skipped)`,
272
- detail: { url },
273
- };
274
- }
275
- return { ok: false, msg: err.message };
232
+ const res = await fetchWithTimeout(
233
+ "http://127.0.0.1:11434/api/tags",
234
+ {},
235
+ 1500
236
+ );
237
+ if (!res.ok) return { ok: true, msg: "Ollama not reachable on 127.0.0.1 — skipping (not used?)" };
238
+ } catch {
239
+ return { ok: true, msg: "Ollama not running locally skipping" };
276
240
  }
241
+ // Ollama IS running on host. Warn the user about the bind config.
242
+ return {
243
+ ok: true,
244
+ msg: "Ollama running on host. If engine is containerised, ensure OLLAMA_HOST=0.0.0.0:11434 (see README)",
245
+ };
277
246
  },
278
247
  };
279
248
  }
280
249
 
281
250
  export function localMemoryChecks() {
282
251
  return [
283
- checkPostgres(),
284
- checkPgvector(),
285
- checkMigrations(),
286
- checkEmbeddingEndpoint(),
287
- checkLlmEndpoint(),
288
- checkMemoryServer(),
252
+ checkPluginConfig(),
253
+ checkEngineHealth(),
254
+ checkEngineLayers(),
255
+ checkEmbeddingPath(),
256
+ checkOllamaBindIfPresent(),
289
257
  ];
290
258
  }
@@ -69,10 +69,18 @@ export function detectPaths(opts = {}) {
69
69
  found.add(PATHS.PLATFORM);
70
70
  }
71
71
 
72
- const localConfig = join(home, ".claude", "tes-memory.local.md");
72
+ // Local engine: detected via plugin config (claude-pentatonic preferred,
73
+ // fall back to claude), or explicit MEMORY_ENGINE_URL env override.
74
+ const localConfigPaths = [
75
+ env.CLAUDE_CONFIG_DIR
76
+ ? join(env.CLAUDE_CONFIG_DIR, "tes-memory.local.md")
77
+ : null,
78
+ join(home, ".claude-pentatonic", "tes-memory.local.md"),
79
+ join(home, ".claude", "tes-memory.local.md"),
80
+ ].filter(Boolean);
73
81
  const looksLocal =
74
- (env.DATABASE_URL && env.EMBEDDING_URL && env.LLM_URL) ||
75
- fileExists(localConfig);
82
+ Boolean(env.MEMORY_ENGINE_URL) ||
83
+ localConfigPaths.some((p) => fileExists(p));
76
84
  if (looksLocal) {
77
85
  found.add(PATHS.LOCAL);
78
86
  }