@pentatonic-ai/ai-agent-sdk 0.6.0 → 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 (94) hide show
  1. package/README.md +170 -69
  2. package/bin/__tests__/callback-server.test.js +4 -1
  3. package/bin/cli.js +41 -164
  4. package/bin/commands/config.js +251 -0
  5. package/package.json +2 -1
  6. package/packages/doctor/__tests__/detect.test.js +2 -6
  7. package/packages/doctor/src/checks/local-memory.js +164 -196
  8. package/packages/doctor/src/detect.js +11 -3
  9. package/packages/memory/src/corpus/adapters.js +104 -0
  10. package/packages/memory/src/corpus/cli.js +72 -7
  11. package/packages/memory/src/corpus/index.js +1 -1
  12. package/packages/memory-engine/.env.example +13 -0
  13. package/packages/memory-engine/README.md +131 -0
  14. package/packages/memory-engine/bench/README.md +99 -0
  15. package/packages/memory-engine/bench/scorecards-engine/agent-coding__pentatonic-baseline__20260427-142523.json +1115 -0
  16. package/packages/memory-engine/bench/scorecards-engine/chat-recall__pentatonic-baseline__20260427-142648.json +819 -0
  17. package/packages/memory-engine/bench/scorecards-engine/circular-economy__pentatonic-baseline__20260427-142757.json +1278 -0
  18. package/packages/memory-engine/bench/scorecards-engine/customer-support__pentatonic-baseline__20260427-142900.json +1018 -0
  19. package/packages/memory-engine/bench/scorecards-engine/marketplace-ops__pentatonic-baseline__20260427-142957.json +1038 -0
  20. package/packages/memory-engine/bench/scorecards-engine/product-catalogue__pentatonic-baseline__20260427-143122.json +961 -0
  21. package/packages/memory-engine/bench/scorecards-engine-via-docker/agent-coding__pentatonic-memory__20260427-161812.json +1115 -0
  22. package/packages/memory-engine/bench/scorecards-engine-via-docker/chat-recall__pentatonic-memory__20260427-161701.json +819 -0
  23. package/packages/memory-engine/bench/scorecards-engine-via-docker/circular-economy__pentatonic-memory__20260427-161713.json +1278 -0
  24. package/packages/memory-engine/bench/scorecards-engine-via-docker/customer-support__pentatonic-memory__20260427-161723.json +1018 -0
  25. package/packages/memory-engine/bench/scorecards-engine-via-docker/marketplace-ops__pentatonic-memory__20260427-161732.json +1038 -0
  26. package/packages/memory-engine/bench/scorecards-engine-via-docker/product-catalogue__pentatonic-memory__20260427-161741.json +937 -0
  27. package/packages/memory-engine/bench/scorecards-engine-via-l2-7-layer-populated/agent-coding__pentatonic-memory__20260427-184718.json +1115 -0
  28. package/packages/memory-engine/bench/scorecards-engine-via-l2-7-layer-populated/chat-recall__pentatonic-memory__20260427-184614.json +819 -0
  29. package/packages/memory-engine/bench/scorecards-engine-via-l2-7-layer-populated/circular-economy__pentatonic-memory__20260427-184809.json +1278 -0
  30. package/packages/memory-engine/bench/scorecards-engine-via-l2-7-layer-populated/customer-support__pentatonic-memory__20260427-184854.json +1018 -0
  31. package/packages/memory-engine/bench/scorecards-engine-via-l2-7-layer-populated/marketplace-ops__pentatonic-memory__20260427-184929.json +1038 -0
  32. package/packages/memory-engine/bench/scorecards-engine-via-l2-7-layer-populated/product-catalogue__pentatonic-memory__20260427-185015.json +961 -0
  33. package/packages/memory-engine/bench/scorecards-engine-via-l2-empty-layers/agent-coding__pentatonic-memory__20260427-175252.json +1115 -0
  34. package/packages/memory-engine/bench/scorecards-engine-via-l2-empty-layers/chat-recall__pentatonic-memory__20260427-175312.json +819 -0
  35. package/packages/memory-engine/bench/scorecards-engine-via-l2-empty-layers/circular-economy__pentatonic-memory__20260427-175335.json +1278 -0
  36. package/packages/memory-engine/bench/scorecards-engine-via-l2-empty-layers/customer-support__pentatonic-memory__20260427-175355.json +1018 -0
  37. package/packages/memory-engine/bench/scorecards-engine-via-l2-empty-layers/marketplace-ops__pentatonic-memory__20260427-175413.json +1038 -0
  38. package/packages/memory-engine/bench/scorecards-engine-via-l2-empty-layers/product-catalogue__pentatonic-memory__20260427-175430.json +883 -0
  39. package/packages/memory-engine/bench/scorecards-engine-via-shim/agent-coding__pentatonic-memory__20260427-155409.json +1115 -0
  40. package/packages/memory-engine/bench/scorecards-engine-via-shim/chat-recall__pentatonic-memory__20260427-155421.json +819 -0
  41. package/packages/memory-engine/bench/scorecards-engine-via-shim/circular-economy__pentatonic-memory__20260427-155433.json +1278 -0
  42. package/packages/memory-engine/bench/scorecards-engine-via-shim/customer-support__pentatonic-memory__20260427-155443.json +1018 -0
  43. package/packages/memory-engine/bench/scorecards-engine-via-shim/marketplace-ops__pentatonic-memory__20260427-155453.json +1038 -0
  44. package/packages/memory-engine/bench/scorecards-engine-via-shim/product-catalogue__pentatonic-memory__20260427-155503.json +937 -0
  45. package/packages/memory-engine/bench/scorecards-pentatonic-baseline/agent-coding__pentatonic-memory-latest__20260427-145103.json +1115 -0
  46. package/packages/memory-engine/bench/scorecards-pentatonic-baseline/agent-coding__pentatonic-memory__20260427-144909.json +1115 -0
  47. package/packages/memory-engine/bench/scorecards-pentatonic-baseline/chat-recall__pentatonic-memory-latest__20260427-145153.json +819 -0
  48. package/packages/memory-engine/bench/scorecards-pentatonic-baseline/chat-recall__pentatonic-memory__20260427-145120.json +542 -0
  49. package/packages/memory-engine/bench/scorecards-pentatonic-baseline/circular-economy__pentatonic-memory-latest__20260427-145313.json +1278 -0
  50. package/packages/memory-engine/bench/scorecards-pentatonic-baseline/circular-economy__pentatonic-memory__20260427-145207.json +894 -0
  51. package/packages/memory-engine/bench/scorecards-pentatonic-baseline/customer-support__pentatonic-memory-latest__20260427-145412.json +1018 -0
  52. package/packages/memory-engine/bench/scorecards-pentatonic-baseline/customer-support__pentatonic-memory__20260427-145327.json +680 -0
  53. package/packages/memory-engine/bench/scorecards-pentatonic-baseline/marketplace-ops__pentatonic-memory-latest__20260427-145517.json +1038 -0
  54. package/packages/memory-engine/bench/scorecards-pentatonic-baseline/marketplace-ops__pentatonic-memory__20260427-145422.json +693 -0
  55. package/packages/memory-engine/bench/scorecards-pentatonic-baseline/product-catalogue__pentatonic-memory-latest__20260427-145616.json +961 -0
  56. package/packages/memory-engine/bench/scorecards-pentatonic-baseline/product-catalogue__pentatonic-memory__20260427-145528.json +727 -0
  57. package/packages/memory-engine/compat/Dockerfile +11 -0
  58. package/packages/memory-engine/compat/server.py +680 -0
  59. package/packages/memory-engine/docker-compose.yml +243 -0
  60. package/packages/memory-engine/docs/MIGRATION.md +178 -0
  61. package/packages/memory-engine/docs/RUNBOOK-AWS.md +375 -0
  62. package/packages/memory-engine/docs/why-v05-underperforms.md +138 -0
  63. package/packages/memory-engine/engine/README.md +52 -0
  64. package/packages/memory-engine/engine/l2-hybridrag-proxy.py +1543 -0
  65. package/packages/memory-engine/engine/l5-comms-layer.py +663 -0
  66. package/packages/memory-engine/engine/l6-document-store.py +1018 -0
  67. package/packages/memory-engine/engine/services/l2/Dockerfile +41 -0
  68. package/packages/memory-engine/engine/services/l2/init_databases.py +81 -0
  69. package/packages/memory-engine/engine/services/l2/l2-hybridrag-proxy.py +1543 -0
  70. package/packages/memory-engine/engine/services/l4/Dockerfile +15 -0
  71. package/packages/memory-engine/engine/services/l4/server.py +235 -0
  72. package/packages/memory-engine/engine/services/l5/Dockerfile +9 -0
  73. package/packages/memory-engine/engine/services/l5/l5-comms-layer.py +678 -0
  74. package/packages/memory-engine/engine/services/l6/Dockerfile +11 -0
  75. package/packages/memory-engine/engine/services/l6/l6-document-store.py +1016 -0
  76. package/packages/memory-engine/engine/services/nv-embed/Dockerfile +28 -0
  77. package/packages/memory-engine/engine/services/nv-embed/server.py +152 -0
  78. package/packages/memory-engine/pme_memory/__init__.py +0 -0
  79. package/packages/memory-engine/pme_memory/__main__.py +129 -0
  80. package/packages/memory-engine/pme_memory/artifacts.py +95 -0
  81. package/packages/memory-engine/pme_memory/embed.py +74 -0
  82. package/packages/memory-engine/pme_memory/health.py +36 -0
  83. package/packages/memory-engine/pme_memory/hygiene.py +159 -0
  84. package/packages/memory-engine/pme_memory/indexer.py +200 -0
  85. package/packages/memory-engine/pme_memory/needs.py +55 -0
  86. package/packages/memory-engine/pme_memory/provenance.py +80 -0
  87. package/packages/memory-engine/pme_memory/scoring.py +168 -0
  88. package/packages/memory-engine/pme_memory/search.py +52 -0
  89. package/packages/memory-engine/pme_memory/store.py +86 -0
  90. package/packages/memory-engine/pme_memory/synthesis.py +114 -0
  91. package/packages/memory-engine/pyproject.toml +65 -0
  92. package/packages/memory-engine/scripts/kg-extractor.py +557 -0
  93. package/packages/memory-engine/scripts/kg-preflexor-v2.py +738 -0
  94. package/packages/memory-engine/tests/test_api_contract.sh +57 -0
@@ -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
  }
@@ -292,3 +292,107 @@ export function hostedAdapter(config, opts = {}) {
292
292
  },
293
293
  };
294
294
  }
295
+
296
+ /**
297
+ * Engine adapter — talks directly to the memory engine's HTTP API
298
+ * (`/store`, `/store-batch`, `/forget`) at `engineUrl`. Used for the
299
+ * local-OSS path where no TES is involved, and for any case where the
300
+ * caller wants to ingest straight into a Pentatonic-managed engine
301
+ * without going through the TES GraphQL surface.
302
+ *
303
+ * Wire format matches the engine's compat shim. References-mode
304
+ * metadata (kind: "code_reference") and arbitrary metadata pass
305
+ * through as JSONB on the engine side.
306
+ *
307
+ * @param {object} config
308
+ * @param {string} config.engineUrl - e.g. "http://localhost:8099"
309
+ * @param {string} [config.arena] - tenant scope; defaults to "default"
310
+ * @param {string} [config.apiKey] - optional Authorization: Bearer
311
+ * @param {object} [opts]
312
+ * @param {number} [opts.timeoutMs=30000]
313
+ * @returns {{ingestChunk, deleteByCorpusFile, init}}
314
+ */
315
+ export function engineAdapter(config, opts = {}) {
316
+ const engineUrl = (config.engineUrl || "").replace(/\/$/, "");
317
+ if (!engineUrl) {
318
+ throw new Error("engineAdapter: engineUrl is required");
319
+ }
320
+ const arena = config.arena || "default";
321
+ const apiKey = config.apiKey || null;
322
+ const timeoutMs = opts.timeoutMs ?? 30000;
323
+
324
+ function headers() {
325
+ const h = { "content-type": "application/json" };
326
+ if (apiKey) h["authorization"] = `Bearer ${apiKey}`;
327
+ return h;
328
+ }
329
+
330
+ async function http(path, body) {
331
+ const controller = new AbortController();
332
+ const timer = setTimeout(() => controller.abort(), timeoutMs);
333
+ try {
334
+ const res = await fetch(`${engineUrl}${path}`, {
335
+ method: "POST",
336
+ headers: headers(),
337
+ body: JSON.stringify(body),
338
+ signal: controller.signal,
339
+ });
340
+ clearTimeout(timer);
341
+ if (!res.ok) return { error: `engine_http_${res.status}` };
342
+ return { data: await res.json() };
343
+ } catch (err) {
344
+ clearTimeout(timer);
345
+ return {
346
+ error: err.name === "AbortError" ? "engine_timeout" : "engine_unreachable",
347
+ };
348
+ }
349
+ }
350
+
351
+ return {
352
+ /**
353
+ * Verify the engine is reachable before kicking off ingest.
354
+ * Engine /health returns 200 even when individual layers are
355
+ * "degraded"; we just check the HTTP path works.
356
+ */
357
+ async init() {
358
+ const controller = new AbortController();
359
+ const timer = setTimeout(() => controller.abort(), timeoutMs);
360
+ try {
361
+ const res = await fetch(`${engineUrl}/health`, {
362
+ headers: headers(),
363
+ signal: controller.signal,
364
+ });
365
+ clearTimeout(timer);
366
+ if (!res.ok) {
367
+ throw new Error(`engineAdapter: /health returned ${res.status}`);
368
+ }
369
+ } catch (err) {
370
+ clearTimeout(timer);
371
+ throw new Error(
372
+ `engineAdapter: engine at ${engineUrl} unreachable (${err.message})`
373
+ );
374
+ }
375
+ },
376
+
377
+ async ingestChunk(content, metadata) {
378
+ // Engine ingests via /store; one chunk per call. The corpus
379
+ // pipeline batches at the file level, but each chunk is its own
380
+ // /store call so we get a per-chunk id back. /store-batch is
381
+ // available for future bulk ingest if/when the pipeline rewires.
382
+ const body = { content, metadata: { ...metadata, arena } };
383
+ const result = await http("/store", body);
384
+ if (result.error) return { skipped: result.error };
385
+ return { id: result.data?.id };
386
+ },
387
+
388
+ async deleteByCorpusFile(repoAbs, relPath) {
389
+ const key = `${repoAbs}::${relPath}`;
390
+ const result = await http("/forget", {
391
+ metadata_contains: { corpus_file_key: key },
392
+ arena,
393
+ });
394
+ if (result.error) return 0;
395
+ return result.data?.deleted ?? 0;
396
+ },
397
+ };
398
+ }