@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.
- package/README.md +345 -174
- package/bin/__tests__/callback-server.test.js +70 -0
- package/bin/__tests__/credentials.test.js +58 -0
- package/bin/__tests__/login.test.js +210 -0
- package/bin/__tests__/pkce.test.js +39 -0
- package/bin/__tests__/whoami.test.js +77 -0
- package/bin/cli.js +109 -440
- package/bin/commands/config.js +251 -0
- package/bin/commands/login.js +219 -0
- package/bin/commands/whoami.js +41 -0
- package/bin/lib/callback-server.js +137 -0
- package/bin/lib/credentials.js +100 -0
- package/bin/lib/pkce.js +26 -0
- package/package.json +4 -2
- package/packages/doctor/__tests__/detect.test.js +2 -6
- package/packages/doctor/src/checks/local-memory.js +164 -196
- package/packages/doctor/src/detect.js +11 -3
- package/packages/memory/src/__tests__/corpus-chunkers.test.js +143 -0
- package/packages/memory/src/__tests__/corpus-discover.test.js +175 -0
- package/packages/memory/src/__tests__/corpus-ingest.test.js +236 -0
- package/packages/memory/src/__tests__/corpus-signatures.test.js +175 -0
- package/packages/memory/src/__tests__/corpus-state.test.js +161 -0
- package/packages/memory/src/__tests__/ingest-corpus-opts.test.js +129 -0
- package/packages/memory/src/__tests__/search-kind.test.js +108 -0
- package/packages/memory/src/corpus/adapters.js +398 -0
- package/packages/memory/src/corpus/chunkers.js +328 -0
- package/packages/memory/src/corpus/cli.js +613 -0
- package/packages/memory/src/corpus/discover.js +379 -0
- package/packages/memory/src/corpus/index.js +68 -0
- package/packages/memory/src/corpus/ingest.js +356 -0
- package/packages/memory/src/corpus/signatures.js +280 -0
- package/packages/memory/src/corpus/state.js +134 -0
- package/packages/memory/src/index.js +18 -0
- package/packages/memory/src/ingest.js +20 -11
- package/packages/memory/src/openclaw/index.js +39 -1
- package/packages/memory/src/search.js +30 -7
- package/packages/memory-engine/.env.example +13 -0
- package/packages/memory-engine/README.md +131 -0
- package/packages/memory-engine/bench/README.md +99 -0
- package/packages/memory-engine/bench/scorecards-engine/agent-coding__pentatonic-baseline__20260427-142523.json +1115 -0
- package/packages/memory-engine/bench/scorecards-engine/chat-recall__pentatonic-baseline__20260427-142648.json +819 -0
- package/packages/memory-engine/bench/scorecards-engine/circular-economy__pentatonic-baseline__20260427-142757.json +1278 -0
- package/packages/memory-engine/bench/scorecards-engine/customer-support__pentatonic-baseline__20260427-142900.json +1018 -0
- package/packages/memory-engine/bench/scorecards-engine/marketplace-ops__pentatonic-baseline__20260427-142957.json +1038 -0
- package/packages/memory-engine/bench/scorecards-engine/product-catalogue__pentatonic-baseline__20260427-143122.json +961 -0
- package/packages/memory-engine/bench/scorecards-engine-via-docker/agent-coding__pentatonic-memory__20260427-161812.json +1115 -0
- package/packages/memory-engine/bench/scorecards-engine-via-docker/chat-recall__pentatonic-memory__20260427-161701.json +819 -0
- package/packages/memory-engine/bench/scorecards-engine-via-docker/circular-economy__pentatonic-memory__20260427-161713.json +1278 -0
- package/packages/memory-engine/bench/scorecards-engine-via-docker/customer-support__pentatonic-memory__20260427-161723.json +1018 -0
- package/packages/memory-engine/bench/scorecards-engine-via-docker/marketplace-ops__pentatonic-memory__20260427-161732.json +1038 -0
- package/packages/memory-engine/bench/scorecards-engine-via-docker/product-catalogue__pentatonic-memory__20260427-161741.json +937 -0
- package/packages/memory-engine/bench/scorecards-engine-via-l2-7-layer-populated/agent-coding__pentatonic-memory__20260427-184718.json +1115 -0
- package/packages/memory-engine/bench/scorecards-engine-via-l2-7-layer-populated/chat-recall__pentatonic-memory__20260427-184614.json +819 -0
- package/packages/memory-engine/bench/scorecards-engine-via-l2-7-layer-populated/circular-economy__pentatonic-memory__20260427-184809.json +1278 -0
- package/packages/memory-engine/bench/scorecards-engine-via-l2-7-layer-populated/customer-support__pentatonic-memory__20260427-184854.json +1018 -0
- package/packages/memory-engine/bench/scorecards-engine-via-l2-7-layer-populated/marketplace-ops__pentatonic-memory__20260427-184929.json +1038 -0
- package/packages/memory-engine/bench/scorecards-engine-via-l2-7-layer-populated/product-catalogue__pentatonic-memory__20260427-185015.json +961 -0
- package/packages/memory-engine/bench/scorecards-engine-via-l2-empty-layers/agent-coding__pentatonic-memory__20260427-175252.json +1115 -0
- package/packages/memory-engine/bench/scorecards-engine-via-l2-empty-layers/chat-recall__pentatonic-memory__20260427-175312.json +819 -0
- package/packages/memory-engine/bench/scorecards-engine-via-l2-empty-layers/circular-economy__pentatonic-memory__20260427-175335.json +1278 -0
- package/packages/memory-engine/bench/scorecards-engine-via-l2-empty-layers/customer-support__pentatonic-memory__20260427-175355.json +1018 -0
- package/packages/memory-engine/bench/scorecards-engine-via-l2-empty-layers/marketplace-ops__pentatonic-memory__20260427-175413.json +1038 -0
- package/packages/memory-engine/bench/scorecards-engine-via-l2-empty-layers/product-catalogue__pentatonic-memory__20260427-175430.json +883 -0
- package/packages/memory-engine/bench/scorecards-engine-via-shim/agent-coding__pentatonic-memory__20260427-155409.json +1115 -0
- package/packages/memory-engine/bench/scorecards-engine-via-shim/chat-recall__pentatonic-memory__20260427-155421.json +819 -0
- package/packages/memory-engine/bench/scorecards-engine-via-shim/circular-economy__pentatonic-memory__20260427-155433.json +1278 -0
- package/packages/memory-engine/bench/scorecards-engine-via-shim/customer-support__pentatonic-memory__20260427-155443.json +1018 -0
- package/packages/memory-engine/bench/scorecards-engine-via-shim/marketplace-ops__pentatonic-memory__20260427-155453.json +1038 -0
- package/packages/memory-engine/bench/scorecards-engine-via-shim/product-catalogue__pentatonic-memory__20260427-155503.json +937 -0
- package/packages/memory-engine/bench/scorecards-pentatonic-baseline/agent-coding__pentatonic-memory-latest__20260427-145103.json +1115 -0
- package/packages/memory-engine/bench/scorecards-pentatonic-baseline/agent-coding__pentatonic-memory__20260427-144909.json +1115 -0
- package/packages/memory-engine/bench/scorecards-pentatonic-baseline/chat-recall__pentatonic-memory-latest__20260427-145153.json +819 -0
- package/packages/memory-engine/bench/scorecards-pentatonic-baseline/chat-recall__pentatonic-memory__20260427-145120.json +542 -0
- package/packages/memory-engine/bench/scorecards-pentatonic-baseline/circular-economy__pentatonic-memory-latest__20260427-145313.json +1278 -0
- package/packages/memory-engine/bench/scorecards-pentatonic-baseline/circular-economy__pentatonic-memory__20260427-145207.json +894 -0
- package/packages/memory-engine/bench/scorecards-pentatonic-baseline/customer-support__pentatonic-memory-latest__20260427-145412.json +1018 -0
- package/packages/memory-engine/bench/scorecards-pentatonic-baseline/customer-support__pentatonic-memory__20260427-145327.json +680 -0
- package/packages/memory-engine/bench/scorecards-pentatonic-baseline/marketplace-ops__pentatonic-memory-latest__20260427-145517.json +1038 -0
- package/packages/memory-engine/bench/scorecards-pentatonic-baseline/marketplace-ops__pentatonic-memory__20260427-145422.json +693 -0
- package/packages/memory-engine/bench/scorecards-pentatonic-baseline/product-catalogue__pentatonic-memory-latest__20260427-145616.json +961 -0
- package/packages/memory-engine/bench/scorecards-pentatonic-baseline/product-catalogue__pentatonic-memory__20260427-145528.json +727 -0
- package/packages/memory-engine/compat/Dockerfile +11 -0
- package/packages/memory-engine/compat/server.py +680 -0
- package/packages/memory-engine/docker-compose.yml +243 -0
- package/packages/memory-engine/docs/MIGRATION.md +178 -0
- package/packages/memory-engine/docs/RUNBOOK-AWS.md +375 -0
- package/packages/memory-engine/docs/why-v05-underperforms.md +138 -0
- package/packages/memory-engine/engine/README.md +52 -0
- package/packages/memory-engine/engine/l2-hybridrag-proxy.py +1543 -0
- package/packages/memory-engine/engine/l5-comms-layer.py +663 -0
- package/packages/memory-engine/engine/l6-document-store.py +1018 -0
- package/packages/memory-engine/engine/services/l2/Dockerfile +41 -0
- package/packages/memory-engine/engine/services/l2/init_databases.py +81 -0
- package/packages/memory-engine/engine/services/l2/l2-hybridrag-proxy.py +1543 -0
- package/packages/memory-engine/engine/services/l4/Dockerfile +15 -0
- package/packages/memory-engine/engine/services/l4/server.py +235 -0
- package/packages/memory-engine/engine/services/l5/Dockerfile +9 -0
- package/packages/memory-engine/engine/services/l5/l5-comms-layer.py +678 -0
- package/packages/memory-engine/engine/services/l6/Dockerfile +11 -0
- package/packages/memory-engine/engine/services/l6/l6-document-store.py +1016 -0
- package/packages/memory-engine/engine/services/nv-embed/Dockerfile +28 -0
- package/packages/memory-engine/engine/services/nv-embed/server.py +152 -0
- package/packages/memory-engine/pme_memory/__init__.py +0 -0
- package/packages/memory-engine/pme_memory/__main__.py +129 -0
- package/packages/memory-engine/pme_memory/artifacts.py +95 -0
- package/packages/memory-engine/pme_memory/embed.py +74 -0
- package/packages/memory-engine/pme_memory/health.py +36 -0
- package/packages/memory-engine/pme_memory/hygiene.py +159 -0
- package/packages/memory-engine/pme_memory/indexer.py +200 -0
- package/packages/memory-engine/pme_memory/needs.py +55 -0
- package/packages/memory-engine/pme_memory/provenance.py +80 -0
- package/packages/memory-engine/pme_memory/scoring.py +168 -0
- package/packages/memory-engine/pme_memory/search.py +52 -0
- package/packages/memory-engine/pme_memory/store.py +86 -0
- package/packages/memory-engine/pme_memory/synthesis.py +114 -0
- package/packages/memory-engine/pyproject.toml +65 -0
- package/packages/memory-engine/scripts/kg-extractor.py +557 -0
- package/packages/memory-engine/scripts/kg-preflexor-v2.py +738 -0
- 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
|
+
}
|
package/bin/lib/pkce.js
ADDED
|
@@ -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.
|
|
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
|
|
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
|
|
2
|
+
* Local memory engine checks.
|
|
3
3
|
*
|
|
4
|
-
* Targets the stack started by
|
|
5
|
-
* -
|
|
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
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
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
|
|
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: "
|
|
26
|
-
severity: SEVERITY.
|
|
72
|
+
name: "plugin config (tes-memory.local.md)",
|
|
73
|
+
severity: SEVERITY.WARNING,
|
|
27
74
|
run: async () => {
|
|
28
|
-
const
|
|
29
|
-
if (!
|
|
75
|
+
const cfgPath = findPluginConfig();
|
|
76
|
+
if (!cfgPath) {
|
|
30
77
|
return {
|
|
31
78
|
ok: false,
|
|
32
|
-
msg: "
|
|
79
|
+
msg: "no tes-memory.local.md found — run `npx @pentatonic-ai/ai-agent-sdk config local`",
|
|
33
80
|
};
|
|
34
81
|
}
|
|
35
|
-
|
|
36
|
-
let pg;
|
|
82
|
+
let fm;
|
|
37
83
|
try {
|
|
38
|
-
|
|
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: "
|
|
94
|
+
msg: `${cfgPath}: mode is "${fm.mode || "(unset)"}" — expected "local"`,
|
|
95
|
+
detail: { mode: fm.mode, path: cfgPath },
|
|
43
96
|
};
|
|
44
97
|
}
|
|
45
|
-
|
|
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:
|
|
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
|
|
114
|
+
function checkEngineHealth() {
|
|
67
115
|
return {
|
|
68
|
-
name: "
|
|
116
|
+
name: "engine /health",
|
|
69
117
|
severity: SEVERITY.CRITICAL,
|
|
70
118
|
run: async () => {
|
|
71
|
-
const
|
|
72
|
-
if (!dsn) return { ok: false, msg: "DATABASE_URL not set" };
|
|
73
|
-
let pg;
|
|
119
|
+
const url = resolveEngineUrl();
|
|
74
120
|
try {
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
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:
|
|
94
|
-
detail:
|
|
128
|
+
msg: `${data.engine || "engine"} v${data.version || "?"} (${data.status})`,
|
|
129
|
+
detail: data,
|
|
95
130
|
};
|
|
96
131
|
} catch (err) {
|
|
97
|
-
return {
|
|
98
|
-
|
|
99
|
-
|
|
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
|
|
142
|
+
function checkEngineLayers() {
|
|
106
143
|
return {
|
|
107
|
-
name: "
|
|
144
|
+
name: "engine layers (L0–L6)",
|
|
108
145
|
severity: SEVERITY.WARNING,
|
|
109
146
|
run: async () => {
|
|
110
|
-
const
|
|
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
|
|
121
|
-
|
|
122
|
-
const
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
const
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
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
|
-
|
|
133
|
-
} catch (err) {
|
|
134
|
-
if (/relation .* does not exist/.test(err.message)) {
|
|
161
|
+
if (degradedList.length === 0) {
|
|
135
162
|
return {
|
|
136
|
-
ok:
|
|
137
|
-
msg:
|
|
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:
|
|
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
|
|
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: "
|
|
205
|
-
severity: SEVERITY.
|
|
185
|
+
name: "embedding endpoint (engine→external)",
|
|
186
|
+
severity: SEVERITY.WARNING,
|
|
206
187
|
run: async () => {
|
|
207
|
-
const 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
|
|
218
|
-
if (
|
|
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
|
|
230
|
-
if (
|
|
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:
|
|
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:
|
|
239
|
-
msg:
|
|
240
|
-
detail: {
|
|
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
|
|
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: "
|
|
252
|
-
severity: SEVERITY.
|
|
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(
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
};
|
|
265
|
-
} catch
|
|
266
|
-
|
|
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
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
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
|
-
|
|
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.
|
|
75
|
-
fileExists(
|
|
82
|
+
Boolean(env.MEMORY_ENGINE_URL) ||
|
|
83
|
+
localConfigPaths.some((p) => fileExists(p));
|
|
76
84
|
if (looksLocal) {
|
|
77
85
|
found.add(PATHS.LOCAL);
|
|
78
86
|
}
|