@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,251 @@
1
+ import { existsSync, readFileSync, writeFileSync, mkdirSync, copyFileSync } from "node:fs";
2
+ import { homedir } from "node:os";
3
+ import { join } from "node:path";
4
+
5
+ const DEFAULT_ENGINE_URL = "http://localhost:8099";
6
+
7
+ /**
8
+ * Top-level dispatcher for `tes config <sub>`.
9
+ *
10
+ * Subcommands:
11
+ * local — write `mode: local` + memory_url to the plugin config; print
12
+ * bring-up instructions for the engine docker stack.
13
+ * hosted — run the SDK login flow (writes ~/.config/tes/credentials.json
14
+ * and updates the plugin config to hosted mode).
15
+ * show — read the plugin config + credentials and print what's
16
+ * currently configured.
17
+ *
18
+ * Future:
19
+ * set <key> <value> — tweak engine env vars (EMBED_MODEL, EMBED_DIM, …)
20
+ * and reload docker-compose.
21
+ *
22
+ * @param {object} opts
23
+ * @param {string} opts.sub - subcommand name
24
+ * @param {string} [opts.endpoint] - default TES endpoint (forwarded to login)
25
+ * @param {string} [opts.engineUrl] - override for `local`
26
+ * @param {string} [opts.configDir] - override config-file dir (test hook)
27
+ * @param {Function} [opts.log]
28
+ * @param {Function} [opts.errLog]
29
+ */
30
+ export async function runConfigCommand(opts = {}) {
31
+ const log = opts.log || ((m) => process.stdout.write(m + "\n"));
32
+ const errLog = opts.errLog || ((m) => process.stderr.write(m + "\n"));
33
+ const sub = opts.sub;
34
+
35
+ switch (sub) {
36
+ case "local":
37
+ return runConfigLocal(opts);
38
+ case "hosted":
39
+ return runConfigHosted(opts);
40
+ case "show":
41
+ return runConfigShow(opts);
42
+ case undefined:
43
+ case "":
44
+ case "help":
45
+ case "--help":
46
+ case "-h":
47
+ printHelp(log);
48
+ return { exitCode: sub === undefined ? 1 : 0 };
49
+ default:
50
+ errLog(`Unknown subcommand: ${sub}`);
51
+ printHelp(errLog);
52
+ return { exitCode: 2 };
53
+ }
54
+ }
55
+
56
+ function printHelp(out) {
57
+ out("Usage: tes config <subcommand>");
58
+ out("");
59
+ out("Subcommands:");
60
+ out(" local Point Claude Code's plugin at a local memory engine");
61
+ out(" hosted Sign in with TES (browser flow); writes credentials");
62
+ out(" show Print the current plugin config + memory backend");
63
+ out("");
64
+ out("Flags (subcommand-specific):");
65
+ out(" --engine-url <url> For 'local'; default http://localhost:8099");
66
+ }
67
+
68
+ // ----------------------------------------------------------------------
69
+ // `tes config local`
70
+ // ----------------------------------------------------------------------
71
+
72
+ async function runConfigLocal(opts) {
73
+ const log = opts.log || ((m) => process.stdout.write(m + "\n"));
74
+ const errLog = opts.errLog || ((m) => process.stderr.write(m + "\n"));
75
+ const engineUrl = opts.engineUrl || DEFAULT_ENGINE_URL;
76
+
77
+ const configDir = opts.configDir || resolveConfigDir();
78
+ const configPath = join(configDir, "tes-memory.local.md");
79
+
80
+ try {
81
+ mkdirSync(configDir, { recursive: true });
82
+ } catch (err) {
83
+ errLog(`Error: cannot create config dir ${configDir}: ${err.message}`);
84
+ return { exitCode: 1 };
85
+ }
86
+
87
+ // Preserve any existing hosted config as comments so the user can
88
+ // flip back without re-running login.
89
+ let preserved = "";
90
+ if (existsSync(configPath)) {
91
+ try {
92
+ const fm = parseFrontmatter(readFileSync(configPath, "utf-8"));
93
+ if (fm && fm.tes_endpoint) {
94
+ copyFileSync(configPath, configPath + ".bak");
95
+ preserved = formatPreservedHosted(fm);
96
+ log(` Existing hosted config backed up to ${configPath}.bak`);
97
+ }
98
+ } catch {
99
+ // best-effort
100
+ }
101
+ }
102
+
103
+ const body =
104
+ `---\n` +
105
+ `mode: local\n` +
106
+ `memory_url: ${engineUrl}\n` +
107
+ preserved +
108
+ `---\n`;
109
+
110
+ try {
111
+ writeFileSync(configPath, body, { mode: 0o600 });
112
+ } catch (err) {
113
+ errLog(`Error: cannot write config ${configPath}: ${err.message}`);
114
+ return { exitCode: 1 };
115
+ }
116
+
117
+ log("");
118
+ log(`✓ Plugin config written: ${configPath}`);
119
+ log(` → Claude Code's tes-memory plugin now points at ${engineUrl}`);
120
+ log("");
121
+ log("Next steps to bring up the engine:");
122
+ log("");
123
+ log(" 1. Make sure Ollama is running and bound to all interfaces");
124
+ log(" (so docker containers can reach it via host.docker.internal):");
125
+ log("");
126
+ log(" sudo mkdir -p /etc/systemd/system/ollama.service.d");
127
+ log(" echo -e '[Service]\\nEnvironment=\"OLLAMA_HOST=0.0.0.0:11434\"' \\");
128
+ log(" | sudo tee /etc/systemd/system/ollama.service.d/override.conf");
129
+ log(" sudo systemctl daemon-reload && sudo systemctl restart ollama");
130
+ log(" ollama pull nomic-embed-text # if not already pulled");
131
+ log("");
132
+ log(" 2. Bring up the engine docker stack:");
133
+ log("");
134
+ log(" cd packages/memory-engine");
135
+ log(" cp .env.example .env # if no .env yet");
136
+ log(" # edit .env if you want a different embedding model/dim");
137
+ log(" docker compose up -d --scale nv-embed=0");
138
+ log("");
139
+ log(" 3. Verify it's healthy:");
140
+ log("");
141
+ log(` curl -s ${engineUrl}/health | jq`);
142
+ log("");
143
+ log(" 4. Reload Claude Code (close + reopen, or /reload-plugins).");
144
+ log("");
145
+ log(" Verify with /tes-memory:tes-status — should report:");
146
+ log(" ✓ Connected to local memory engine");
147
+ log("");
148
+ return { exitCode: 0, configPath };
149
+ }
150
+
151
+ // ----------------------------------------------------------------------
152
+ // `tes config hosted`
153
+ // ----------------------------------------------------------------------
154
+
155
+ async function runConfigHosted(opts) {
156
+ const { runLoginCommand } = await import("./login.js");
157
+ return runLoginCommand({ endpoint: opts.endpoint });
158
+ }
159
+
160
+ // ----------------------------------------------------------------------
161
+ // `tes config show`
162
+ // ----------------------------------------------------------------------
163
+
164
+ function runConfigShow(opts) {
165
+ const log = opts.log || ((m) => process.stdout.write(m + "\n"));
166
+ const configDir = opts.configDir || resolveConfigDir();
167
+ const configPath = join(configDir, "tes-memory.local.md");
168
+
169
+ log("");
170
+ log(`Plugin config: ${configPath}`);
171
+ if (!existsSync(configPath)) {
172
+ log(" (file does not exist — run `tes config local` or `tes config hosted`)");
173
+ return { exitCode: 1 };
174
+ }
175
+
176
+ const fm = parseFrontmatter(readFileSync(configPath, "utf-8"));
177
+ if (!fm) {
178
+ log(" (file exists but couldn't parse frontmatter)");
179
+ return { exitCode: 1 };
180
+ }
181
+
182
+ const mode = fm.mode || (fm.tes_endpoint ? "hosted" : "?");
183
+ log(` Mode: ${mode}`);
184
+ if (mode === "local") {
185
+ log(` memory_url: ${fm.memory_url || "(missing)"}`);
186
+ } else if (mode === "hosted") {
187
+ log(` endpoint: ${fm.tes_endpoint}`);
188
+ log(` client_id: ${fm.tes_client_id}`);
189
+ log(` user_id: ${fm.tes_user_id || "(unset)"}`);
190
+ }
191
+
192
+ // Also surface ~/.config/tes/credentials.json if present
193
+ const credPath = join(
194
+ process.env.XDG_CONFIG_HOME || join(homedir(), ".config"),
195
+ "tes",
196
+ "credentials.json"
197
+ );
198
+ if (existsSync(credPath)) {
199
+ try {
200
+ const c = JSON.parse(readFileSync(credPath, "utf-8"));
201
+ log("");
202
+ log(`Credentials: ${credPath}`);
203
+ log(` endpoint: ${c.endpoint}`);
204
+ log(` clientId: ${c.clientId}`);
205
+ } catch {
206
+ log(` (${credPath} unreadable)`);
207
+ }
208
+ }
209
+
210
+ log("");
211
+ return { exitCode: 0 };
212
+ }
213
+
214
+ // ----------------------------------------------------------------------
215
+ // Helpers
216
+ // ----------------------------------------------------------------------
217
+
218
+ function resolveConfigDir() {
219
+ const candidates = [
220
+ process.env.CLAUDE_CONFIG_DIR,
221
+ join(homedir(), ".claude-pentatonic"),
222
+ join(homedir(), ".claude"),
223
+ ].filter(Boolean);
224
+ for (const c of candidates) {
225
+ if (existsSync(c)) return c;
226
+ }
227
+ return candidates[candidates.length - 1];
228
+ }
229
+
230
+ function parseFrontmatter(content) {
231
+ const m = content.match(/^---\n([\s\S]*?)\n---/);
232
+ if (!m) return null;
233
+ const out = {};
234
+ for (const line of m[1].split("\n")) {
235
+ const kv = line.match(/^(\w+):\s*(.+)$/);
236
+ if (kv) out[kv[1]] = kv[2].trim();
237
+ }
238
+ return out;
239
+ }
240
+
241
+ function formatPreservedHosted(fm) {
242
+ const keys = ["tes_endpoint", "tes_client_id", "tes_api_key", "tes_user_id"];
243
+ const present = keys.filter((k) => fm[k]);
244
+ if (!present.length) return "";
245
+ return (
246
+ `# Hosted config preserved — uncomment + remove the local block above\n` +
247
+ `# to switch back. Original saved as <this-file>.bak.\n` +
248
+ present.map((k) => `# ${k}: ${fm[k]}`).join("\n") +
249
+ `\n`
250
+ );
251
+ }
@@ -0,0 +1,219 @@
1
+ import { generatePKCE } from "../lib/pkce.js";
2
+ import { startCallbackServer } from "../lib/callback-server.js";
3
+ import { writeCredentials } from "../lib/credentials.js";
4
+ import { hostname } from "node:os";
5
+ import { execFileSync } from "node:child_process";
6
+ import { Buffer } from "node:buffer";
7
+
8
+ /**
9
+ * Run the SDK login flow.
10
+ *
11
+ * Steps: PKCE → start localhost server → open browser → wait for code
12
+ * → exchange code for access_token → mint tes_* via GraphQL
13
+ * → write credentials.
14
+ *
15
+ * Exits non-zero (and returns { exitCode }) on any failure rather than
16
+ * throwing — caller (cli.js) maps exitCode to process.exit().
17
+ *
18
+ * @param {object} opts
19
+ * @param {string} opts.endpoint - e.g. https://api.pentatonic.com
20
+ * @param {Function} [opts.openBrowser] - injectable for tests
21
+ * @param {Function} [opts.log] - injectable for tests
22
+ * @param {Function} [opts.errLog] - injectable for tests
23
+ */
24
+ export async function runLoginCommand(opts = {}) {
25
+ const endpoint = opts.endpoint;
26
+ const log = opts.log || ((msg) => process.stdout.write(msg + "\n"));
27
+ const errLog = opts.errLog || ((msg) => process.stderr.write(msg + "\n"));
28
+ const openBrowser = opts.openBrowser || defaultOpenBrowser;
29
+
30
+ if (!endpoint) {
31
+ errLog("Error: endpoint is required");
32
+ return { exitCode: 1 };
33
+ }
34
+
35
+ const { verifier, challenge, state } = generatePKCE();
36
+
37
+ let server;
38
+ try {
39
+ server = await startCallbackServer({
40
+ ports: [14171, 14172, 14173, 0],
41
+ state,
42
+ timeoutMs: 5 * 60_000,
43
+ });
44
+ } catch (err) {
45
+ errLog(`Failed to start localhost callback listener: ${err.message}`);
46
+ return { exitCode: 2 };
47
+ }
48
+
49
+ const initUrl = new URL(`${endpoint}/cli-init`);
50
+ initUrl.searchParams.set("cb", `http://localhost:${server.port}/callback`);
51
+ initUrl.searchParams.set("state", state);
52
+ initUrl.searchParams.set("code_challenge", challenge);
53
+ initUrl.searchParams.set("code_challenge_method", "S256");
54
+
55
+ log("");
56
+ log(" Hosted TES Setup");
57
+ log(` Opening ${initUrl.toString()} in your browser…`);
58
+ log(` Listening on http://localhost:${server.port} for the callback (5 min timeout)`);
59
+
60
+ try {
61
+ openBrowser(initUrl.toString());
62
+ } catch (err) {
63
+ log(` (Could not auto-open browser: ${err.message})`);
64
+ log(` Please open this URL manually:`);
65
+ log(` ${initUrl.toString()}`);
66
+ }
67
+
68
+ let callback;
69
+ try {
70
+ callback = await server.result;
71
+ } catch (err) {
72
+ errLog(`Login failed: ${err.message}`);
73
+ return { exitCode: 3 };
74
+ }
75
+
76
+ // Exchange code for access_token at /oauth/token. The CLI POSTs to
77
+ // the same platform-level endpoint it was given — /oauth/token
78
+ // validates the verifier against the code regardless of which
79
+ // tenant subdomain issued it.
80
+ let accessToken;
81
+ try {
82
+ const tokenRes = await fetch(`${endpoint}/oauth/token`, {
83
+ method: "POST",
84
+ headers: { "Content-Type": "application/x-www-form-urlencoded" },
85
+ body: new URLSearchParams({
86
+ grant_type: "authorization_code",
87
+ code: callback.code,
88
+ redirect_uri: `http://localhost:${server.port}/callback`,
89
+ code_verifier: verifier,
90
+ }).toString(),
91
+ });
92
+ if (!tokenRes.ok) {
93
+ const body = await tokenRes.json().catch(() => ({}));
94
+ // Surface error_description if present — `invalid_grant` alone
95
+ // is ambiguous (could be expired code, PKCE mismatch, redirect_uri
96
+ // mismatch, replay, etc.). The description names the actual cause.
97
+ const detail = body.error_description
98
+ ? `${body.error}: ${body.error_description}`
99
+ : body.error || "unknown";
100
+ errLog(`Token exchange failed (${tokenRes.status}): ${detail}`);
101
+ return { exitCode: 4 };
102
+ }
103
+ const tokenBody = await tokenRes.json();
104
+ accessToken = tokenBody.access_token;
105
+ if (!accessToken) {
106
+ errLog("Token exchange returned no access_token");
107
+ return { exitCode: 4 };
108
+ }
109
+ } catch (err) {
110
+ errLog(`Token exchange request failed: ${err.message}`);
111
+ return { exitCode: 4 };
112
+ }
113
+
114
+ // Decode JWT claims (unverified) to extract clientId. The token came
115
+ // from /oauth/token over TLS so it's trusted; verification happens
116
+ // server-side on every subsequent API call.
117
+ const claims = decodeJwtClaims(accessToken);
118
+ const clientId = claims?.client_id || claims?.clientId;
119
+ if (!clientId) {
120
+ errLog("Access token missing clientId claim");
121
+ return { exitCode: 5 };
122
+ }
123
+
124
+ // Mint long-lived tes_* via GraphQL.
125
+ let plainTextToken;
126
+ try {
127
+ const graphqlRes = await fetch(`${endpoint}/api/graphql`, {
128
+ method: "POST",
129
+ headers: {
130
+ "Content-Type": "application/json",
131
+ Authorization: `Bearer ${accessToken}`,
132
+ "x-client-id": clientId,
133
+ },
134
+ body: JSON.stringify({
135
+ query: `mutation Mint($clientId: String!, $input: CreateApiTokenInput!) {
136
+ createClientApiToken(clientId: $clientId, input: $input) {
137
+ success
138
+ plainTextToken
139
+ }
140
+ }`,
141
+ variables: {
142
+ clientId,
143
+ input: {
144
+ name: `ai-events-sdk · ${hostname()}`,
145
+ role: "agent-events",
146
+ },
147
+ },
148
+ }),
149
+ });
150
+ const body = await graphqlRes.json();
151
+ if (body.errors?.length) {
152
+ errLog(`API key mint failed: ${body.errors[0].message}`);
153
+ return { exitCode: 6 };
154
+ }
155
+ plainTextToken = body?.data?.createClientApiToken?.plainTextToken;
156
+ if (!plainTextToken) {
157
+ errLog("API key mint returned no plainTextToken");
158
+ return { exitCode: 6 };
159
+ }
160
+ } catch (err) {
161
+ errLog(`API key mint request failed: ${err.message}`);
162
+ return { exitCode: 6 };
163
+ }
164
+
165
+ // Write the long-lived credentials. Endpoint becomes the tenant
166
+ // subdomain so subsequent SDK calls hit the right OAuth realm.
167
+ const tenantEndpoint =
168
+ endpoint === "https://api.pentatonic.com"
169
+ ? `https://${clientId}.api.pentatonic.com`
170
+ : endpoint;
171
+
172
+ await writeCredentials({
173
+ endpoint: tenantEndpoint,
174
+ clientId,
175
+ apiKey: plainTextToken,
176
+ });
177
+
178
+ log("");
179
+ log(` ✓ Connected as ${claims.email || "user"} on tenant \`${clientId}\``);
180
+ log(` ✓ Credentials written to ~/.config/tes/credentials.json`);
181
+ log("");
182
+ log(" Claude Code's tes-memory plugin and the OpenClaw pentatonic-memory");
183
+ log(" plugin will pick these credentials up automatically — restart them");
184
+ log(" if they're already running.");
185
+ log("");
186
+
187
+ return { exitCode: 0, clientId };
188
+ }
189
+
190
+ /**
191
+ * `init` alias — deprecated for one major release. Emits a stderr
192
+ * warning then delegates to runLoginCommand. Removed in the next
193
+ * major version.
194
+ */
195
+ export async function runInitAlias(opts = {}) {
196
+ const errLog = opts.errLog || ((m) => process.stderr.write(m + "\n"));
197
+ errLog(" Notice: `init` is deprecated, use `login` instead. (This alias will be removed in the next major release.)");
198
+ return runLoginCommand(opts);
199
+ }
200
+
201
+ function defaultOpenBrowser(url) {
202
+ const opener =
203
+ process.platform === "darwin"
204
+ ? "open"
205
+ : process.platform === "win32"
206
+ ? "start"
207
+ : "xdg-open";
208
+ execFileSync(opener, [url], { stdio: "ignore" });
209
+ }
210
+
211
+ function decodeJwtClaims(jwt) {
212
+ try {
213
+ const [, payload] = jwt.split(".");
214
+ const json = Buffer.from(payload.replace(/-/g, "+").replace(/_/g, "/"), "base64").toString("utf8");
215
+ return JSON.parse(json);
216
+ } catch {
217
+ return null;
218
+ }
219
+ }
@@ -0,0 +1,41 @@
1
+ import { readCredentials, pingTes } from "../lib/credentials.js";
2
+
3
+ /**
4
+ * Run the SDK whoami command. Prints "logged in as X on tenant Y" or
5
+ * "not logged in" / "creds invalid" depending on the state of
6
+ * ~/.config/tes/credentials.json and a GraphQL ping.
7
+ *
8
+ * Originally named `status` in the design spec; renamed because the
9
+ * corpus subcommand `tes status` already exists (shows tracked repos).
10
+ * `whoami` matches the standard CLI convention for "who am I logged
11
+ * in as" and avoids the conflict.
12
+ */
13
+ export async function runWhoamiCommand(opts = {}) {
14
+ const log = opts.log || ((m) => process.stdout.write(m + "\n"));
15
+ const errLog = opts.errLog || ((m) => process.stderr.write(m + "\n"));
16
+
17
+ const creds = await readCredentials();
18
+ if (!creds) {
19
+ log("");
20
+ log(" Not logged in. Run `npx @pentatonic-ai/ai-agent-sdk login` to connect.");
21
+ log("");
22
+ return { exitCode: 1 };
23
+ }
24
+
25
+ const ping = await pingTes(creds);
26
+ if (!ping.ok) {
27
+ if (ping.status === 401) {
28
+ errLog(" Credentials invalid (revoked or expired).");
29
+ errLog(" Run `npx @pentatonic-ai/ai-agent-sdk login` to refresh.");
30
+ return { exitCode: 2 };
31
+ }
32
+ errLog(` Could not verify credentials: ${ping.error}`);
33
+ return { exitCode: 3 };
34
+ }
35
+
36
+ log("");
37
+ log(` ✓ Logged in to tenant \`${creds.clientId}\` (${ping.clientName})`);
38
+ log(` Endpoint: ${creds.endpoint}`);
39
+ log("");
40
+ return { exitCode: 0 };
41
+ }
@@ -0,0 +1,137 @@
1
+ import { createServer } from "node:http";
2
+ import { URL } from "node:url";
3
+
4
+ const SUCCESS_HTML = `<!DOCTYPE html>
5
+ <html><head><meta charset="utf-8"><title>Connected</title>
6
+ <style>body{font-family:system-ui,sans-serif;max-width:480px;margin:80px auto;padding:24px;color:#1a1a1a;line-height:1.5}h1{font-size:1.4em}</style>
7
+ </head><body>
8
+ <h1>✓ Connected</h1>
9
+ <p>The CLI now has a token. You can close this tab.</p>
10
+ </body></html>`;
11
+
12
+ const ERROR_HTML = (msg) => `<!DOCTYPE html>
13
+ <html><head><meta charset="utf-8"><title>Error</title></head><body>
14
+ <h1>Connection failed</h1><p>${msg.replace(/</g, "&lt;")}</p>
15
+ <p>Return to the terminal and re-run the command.</p>
16
+ </body></html>`;
17
+
18
+ /**
19
+ * Start an HTTP server that listens for the OAuth localhost callback.
20
+ *
21
+ * Returns:
22
+ * - port (number): the bound port (resolved before any callback hits)
23
+ * - result (Promise<{code, state}>): resolves on first valid /callback
24
+ * hit; rejects on state mismatch, malformed query, or timeout.
25
+ * - cancel (function): close the server early without resolving.
26
+ *
27
+ * The server serves a tiny "you can close this tab" HTML page on the
28
+ * successful callback and closes itself after sending the response.
29
+ *
30
+ * @param {object} opts
31
+ * @param {number[]} opts.ports - Ordered list of ports to try; 0 means
32
+ * "let the OS pick". The first port that binds wins.
33
+ * @param {string} opts.state - The expected state value; callback must
34
+ * carry exactly this for resolution to fire.
35
+ * @param {number} [opts.timeoutMs=300000] - Default 5 min.
36
+ */
37
+ export async function startCallbackServer({ ports, state, timeoutMs = 300_000 }) {
38
+ if (!Array.isArray(ports) || ports.length === 0) {
39
+ throw new Error("startCallbackServer: ports[] required");
40
+ }
41
+ if (typeof state !== "string" || state.length === 0) {
42
+ throw new Error("startCallbackServer: state required");
43
+ }
44
+
45
+ const server = createServer();
46
+ await tryBind(server, ports);
47
+ const boundPort = server.address().port;
48
+
49
+ let resolveResult, rejectResult;
50
+ const result = new Promise((res, rej) => {
51
+ resolveResult = res;
52
+ rejectResult = rej;
53
+ });
54
+ // Attach a no-op catch so cancelling without awaiting result doesn't
55
+ // raise an unhandled-rejection (kills the Node process under
56
+ // --unhandled-rejections=strict, the default in modern Node). Real
57
+ // callers can still attach .catch() later — promises support multiple
58
+ // handlers and only the LAST unhandled state matters.
59
+ result.catch(() => {});
60
+
61
+ const timer = setTimeout(() => {
62
+ rejectResult(new Error(`Login timed out after ${Math.round(timeoutMs / 1000)}s`));
63
+ server.close();
64
+ }, timeoutMs);
65
+
66
+ server.on("request", (req, res) => {
67
+ try {
68
+ const url = new URL(req.url, `http://localhost:${boundPort}`);
69
+ if (url.pathname !== "/callback") {
70
+ res.writeHead(404).end("Not Found");
71
+ return;
72
+ }
73
+ const code = url.searchParams.get("code");
74
+ const gotState = url.searchParams.get("state");
75
+ if (!code || !gotState) {
76
+ res.writeHead(400, { "Content-Type": "text/html" }).end(ERROR_HTML("Missing code or state"));
77
+ rejectResult(new Error("Callback missing code or state"));
78
+ clearTimeout(timer);
79
+ server.close();
80
+ return;
81
+ }
82
+ if (gotState !== state) {
83
+ res.writeHead(400, { "Content-Type": "text/html" }).end(ERROR_HTML("State mismatch"));
84
+ rejectResult(new Error("Callback state mismatch (CSRF / replay protection)"));
85
+ clearTimeout(timer);
86
+ server.close();
87
+ return;
88
+ }
89
+ res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" }).end(SUCCESS_HTML);
90
+ resolveResult({ code, state: gotState });
91
+ clearTimeout(timer);
92
+ server.close();
93
+ } catch (err) {
94
+ rejectResult(err);
95
+ clearTimeout(timer);
96
+ try { res.writeHead(500).end(); } catch {}
97
+ server.close();
98
+ }
99
+ });
100
+
101
+ return {
102
+ port: boundPort,
103
+ result,
104
+ cancel() {
105
+ clearTimeout(timer);
106
+ try { rejectResult(new Error("cancelled")); } catch {}
107
+ server.close();
108
+ },
109
+ };
110
+ }
111
+
112
+ function tryBind(server, ports) {
113
+ return new Promise((resolve, reject) => {
114
+ const tryNext = (i) => {
115
+ if (i >= ports.length) {
116
+ reject(new Error(`Could not bind to any of ports: ${ports.join(", ")}`));
117
+ return;
118
+ }
119
+ const onError = (err) => {
120
+ server.removeListener("listening", onListening);
121
+ if (err.code === "EADDRINUSE" || err.code === "EACCES") {
122
+ tryNext(i + 1);
123
+ } else {
124
+ reject(err);
125
+ }
126
+ };
127
+ const onListening = () => {
128
+ server.removeListener("error", onError);
129
+ resolve();
130
+ };
131
+ server.once("error", onError);
132
+ server.once("listening", onListening);
133
+ server.listen(ports[i], "127.0.0.1");
134
+ };
135
+ tryNext(0);
136
+ });
137
+ }