@pentatonic-ai/ai-agent-sdk 0.4.1 → 0.4.3

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 CHANGED
@@ -171,7 +171,7 @@ Works with both local and hosted setups. Just tell OpenClaw to set it up.
171
171
  ### Install
172
172
 
173
173
  ```bash
174
- openclaw plugins install -l ./packages/memory/src/openclaw
174
+ openclaw plugins install @pentatonic-ai/ai-agent-sdk
175
175
  ```
176
176
 
177
177
  ### Set up
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pentatonic-ai/ai-agent-sdk",
3
- "version": "0.4.1",
3
+ "version": "0.4.3",
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",
@@ -51,6 +51,10 @@
51
51
  "mcp",
52
52
  "model-context-protocol"
53
53
  ],
54
+ "openclaw": {
55
+ "extensions": ["./packages/memory/src/openclaw/index.js"],
56
+ "hooks": {}
57
+ },
54
58
  "license": "MIT",
55
59
  "homepage": "https://thingeventsystem.ai",
56
60
  "repository": {
@@ -6,7 +6,7 @@
6
6
  * - Hosted: routes through TES GraphQL API
7
7
  *
8
8
  * Install:
9
- * openclaw plugins install -l ./packages/memory/src/openclaw
9
+ * openclaw plugins install @pentatonic-ai/ai-agent-sdk
10
10
  *
11
11
  * Config in openclaw.json:
12
12
  * {
@@ -36,47 +36,38 @@
36
36
  */
37
37
 
38
38
  import pg from "pg";
39
- import { execFileSync } from "child_process";
40
- import { existsSync, readFileSync, writeFileSync, mkdirSync } from "fs";
41
- import { join } from "path";
42
- import { homedir } from "os";
43
39
  import { createMemorySystem } from "../index.js";
44
40
  import { createContextEngine } from "./context-engine.js";
45
41
 
46
42
  const { Pool } = pg;
47
43
 
48
- const TELEMETRY_URL = "https://sdk-telemetry.philip-134.workers.dev";
49
-
50
44
  let memory = null;
51
45
 
52
46
  function isHostedMode(config) {
53
47
  return !!(config.tes_endpoint && config.tes_api_key);
54
48
  }
55
49
 
56
- function getLocalMemory(config = {}) {
50
+ function getLocalMemory(config) {
57
51
  if (memory) return memory;
58
52
 
59
- const dbUrl =
60
- config.database_url ||
61
- process.env.DATABASE_URL ||
62
- "postgres://memory:memory@localhost:5433/memory";
63
- const embUrl =
64
- config.embedding_url ||
65
- process.env.EMBEDDING_URL ||
66
- "http://localhost:11435/v1";
67
- const embModel =
68
- config.embedding_model ||
69
- process.env.EMBEDDING_MODEL ||
70
- "nomic-embed-text";
71
- const llmUrl =
72
- config.llm_url || process.env.LLM_URL || "http://localhost:11435/v1";
73
- const llmModel =
74
- config.llm_model || process.env.LLM_MODEL || "llama3.2:3b";
53
+ if (!config.database_url) {
54
+ throw new Error(
55
+ "pentatonic-memory: database_url is required in plugin config for local mode. " +
56
+ "Run `npx @pentatonic-ai/ai-agent-sdk memory` first to start the Docker stack, " +
57
+ "then add the database_url to your openclaw.json plugin config."
58
+ );
59
+ }
75
60
 
76
61
  memory = createMemorySystem({
77
- db: new Pool({ connectionString: dbUrl }),
78
- embedding: { url: embUrl, model: embModel },
79
- llm: { url: llmUrl, model: llmModel },
62
+ db: new Pool({ connectionString: config.database_url }),
63
+ embedding: {
64
+ url: config.embedding_url || "http://localhost:11435/v1",
65
+ model: config.embedding_model || "nomic-embed-text",
66
+ },
67
+ llm: {
68
+ url: config.llm_url || "http://localhost:11435/v1",
69
+ model: config.llm_model || "llama3.2:3b",
70
+ },
80
71
  logger: (msg) => process.stderr.write(`[pentatonic-memory] ${msg}\n`),
81
72
  });
82
73
 
@@ -242,240 +233,6 @@ function createHostedContextEngine(config, opts = {}) {
242
233
  };
243
234
  }
244
235
 
245
- // --- Telemetry ---
246
-
247
- function emitTelemetry(mode) {
248
- if (process.env.PENTATONIC_TELEMETRY === "0") return;
249
- const raw = `${process.env.USER || "u"}:${process.platform}:${process.arch}`;
250
- let h = 0;
251
- for (let i = 0; i < raw.length; i++)
252
- h = ((h << 5) - h + raw.charCodeAt(i)) | 0;
253
- const mid = (h >>> 0).toString(16).padStart(8, "0");
254
- fetch(TELEMETRY_URL, {
255
- method: "POST",
256
- headers: { "Content-Type": "application/json" },
257
- body: JSON.stringify({
258
- machine_id: mid,
259
- sdk_version: "0.4.0",
260
- node_version: process.version,
261
- platform: process.platform,
262
- arch: process.arch,
263
- mode: `openclaw-${mode}`,
264
- }),
265
- signal: AbortSignal.timeout(5000),
266
- }).catch(() => {});
267
- }
268
-
269
- // --- Setup helpers ---
270
-
271
- function getConfigPath() {
272
- const candidates = [
273
- join(homedir(), ".openclaw", "pentatonic-memory.json"),
274
- join(homedir(), ".claude-pentatonic", "tes-memory.local.md"),
275
- join(homedir(), ".claude", "tes-memory.local.md"),
276
- ];
277
- return candidates.find((p) => existsSync(p)) || candidates[0];
278
- }
279
-
280
- function writeOpenClawConfig(mode, settings) {
281
- const configDir = join(homedir(), ".openclaw");
282
- if (!existsSync(configDir)) mkdirSync(configDir, { recursive: true });
283
-
284
- const configPath = join(configDir, "pentatonic-memory.json");
285
- writeFileSync(configPath, JSON.stringify({ mode, ...settings }, null, 2));
286
- return configPath;
287
- }
288
-
289
- async function runLocalSetup() {
290
- // Check Docker
291
- try {
292
- execFileSync("docker", ["info"], { stdio: "pipe" });
293
- } catch {
294
- return { success: false, error: "Docker is required but not running. Install from https://docker.com" };
295
- }
296
-
297
- // Find the memory package directory
298
- let memoryDir;
299
- try {
300
- const pkgRoot = new URL("../../..", import.meta.url).pathname;
301
- memoryDir = existsSync(join(pkgRoot, "docker-compose.yml"))
302
- ? pkgRoot
303
- : null;
304
- } catch { memoryDir = null; }
305
-
306
- if (!memoryDir) {
307
- // Fallback: try via npm package location
308
- try {
309
- const resolved = new URL("../../../../packages/memory", import.meta.url).pathname;
310
- if (existsSync(join(resolved, "docker-compose.yml"))) memoryDir = resolved;
311
- } catch { /* */ }
312
- }
313
-
314
- if (!memoryDir) {
315
- return {
316
- success: false,
317
- error: "Could not find memory package. Run: npx @pentatonic-ai/ai-agent-sdk memory",
318
- };
319
- }
320
-
321
- // Start Docker stack
322
- try {
323
- execFileSync("docker", ["compose", "up", "-d", "memory", "postgres", "ollama"], {
324
- cwd: memoryDir,
325
- stdio: "pipe",
326
- });
327
- } catch (err) {
328
- return { success: false, error: `Docker compose failed: ${err.message}` };
329
- }
330
-
331
- // Pull models
332
- const embModel = process.env.EMBEDDING_MODEL || "nomic-embed-text";
333
- const llmModel = process.env.LLM_MODEL || "llama3.2:3b";
334
- const pulled = [];
335
- for (const model of [embModel, llmModel]) {
336
- try {
337
- execFileSync("docker", ["compose", "exec", "ollama", "ollama", "pull", model], {
338
- cwd: memoryDir,
339
- stdio: "pipe",
340
- });
341
- pulled.push(model);
342
- } catch { /* non-fatal */ }
343
- }
344
-
345
- const configPath = writeOpenClawConfig("local", {
346
- memory_url: "http://localhost:3333",
347
- });
348
-
349
- return {
350
- success: true,
351
- mode: "local",
352
- configPath,
353
- models: pulled,
354
- message: "Local memory stack running. PostgreSQL + pgvector + Ollama + memory server started.",
355
- };
356
- }
357
-
358
- async function runHostedSetup(email, clientId, password, region) {
359
- const endpoint = "https://api.pentatonic.com";
360
-
361
- // Try login first
362
- let accessToken = null;
363
- try {
364
- const res = await fetch(`${endpoint}/api/enrollment/login`, {
365
- method: "POST",
366
- headers: { "Content-Type": "application/json" },
367
- body: JSON.stringify({ email, password, clientId }),
368
- });
369
- if (res.ok) {
370
- const data = await res.json();
371
- if (data.tokens?.accessToken) accessToken = data.tokens.accessToken;
372
- }
373
- } catch { /* */ }
374
-
375
- // If not logged in, enroll
376
- if (!accessToken) {
377
- try {
378
- const res = await fetch(`${endpoint}/api/enrollment/submit`, {
379
- method: "POST",
380
- headers: { "Content-Type": "application/json" },
381
- body: JSON.stringify({
382
- clientId,
383
- companyName: clientId,
384
- industryType: "technology",
385
- authProvider: "native",
386
- adminEmail: email,
387
- adminPassword: password,
388
- region: (region || "eu").toLowerCase(),
389
- }),
390
- });
391
- const data = await res.json();
392
- if (!res.ok) {
393
- const errors = data.errors || {};
394
- if (errors.clientId?.includes("already registered")) {
395
- return { success: false, error: "Client ID already registered. Ask your admin to invite you, then run setup again." };
396
- }
397
- return { success: false, error: data.message || Object.values(errors).join(", ") || "Enrollment failed" };
398
- }
399
- } catch (err) {
400
- return { success: false, error: `Failed to connect: ${err.message}` };
401
- }
402
-
403
- // Poll for verification (up to 5 minutes)
404
- const start = Date.now();
405
- while (Date.now() - start < 300000) {
406
- await new Promise((r) => setTimeout(r, 3000));
407
- try {
408
- const res = await fetch(`${endpoint}/api/enrollment/login`, {
409
- method: "POST",
410
- headers: { "Content-Type": "application/json" },
411
- body: JSON.stringify({ email, password, clientId }),
412
- });
413
- if (res.ok) {
414
- const data = await res.json();
415
- if (data.tokens?.accessToken) {
416
- accessToken = data.tokens.accessToken;
417
- break;
418
- }
419
- }
420
- } catch { /* keep polling */ }
421
- }
422
-
423
- if (!accessToken) {
424
- return { success: false, error: "Email verification timed out. Check your inbox and run setup again — it will resume." };
425
- }
426
- }
427
-
428
- // Get API key
429
- let apiKey;
430
- try {
431
- const tokenRes = await fetch(`${endpoint}/api/enrollment/service-token?client_id=${clientId}`);
432
- if (tokenRes.ok) {
433
- const tokenData = await tokenRes.json();
434
- if (tokenData.token) apiKey = tokenData.token;
435
- }
436
- } catch { /* */ }
437
-
438
- if (!apiKey) {
439
- try {
440
- const res = await fetch(`${endpoint}/api/graphql`, {
441
- method: "POST",
442
- headers: {
443
- "Content-Type": "application/json",
444
- Authorization: `Bearer ${accessToken}`,
445
- },
446
- body: JSON.stringify({
447
- query: `mutation CreateApiToken($clientId: String!, $input: CreateApiTokenInput!) {
448
- createClientApiToken(clientId: $clientId, input: $input) { success plainTextToken }
449
- }`,
450
- variables: { clientId, input: { name: "openclaw-plugin", role: "agent-events" } },
451
- }),
452
- });
453
- const data = await res.json();
454
- apiKey = data.data?.createClientApiToken?.plainTextToken;
455
- } catch { /* */ }
456
- }
457
-
458
- if (!apiKey) {
459
- return { success: false, error: "Account verified but failed to generate API key. Run setup again." };
460
- }
461
-
462
- const clientEndpoint = `https://${clientId}.api.pentatonic.com`;
463
- const configPath = writeOpenClawConfig("hosted", {
464
- tes_endpoint: clientEndpoint,
465
- tes_client_id: clientId,
466
- tes_api_key: apiKey,
467
- });
468
-
469
- return {
470
- success: true,
471
- mode: "hosted",
472
- configPath,
473
- endpoint: clientEndpoint,
474
- clientId,
475
- message: "TES account ready. Memory will be stored and searched via Pentatonic cloud.",
476
- };
477
- }
478
-
479
236
  // --- Plugin entry ---
480
237
 
481
238
  export default {
@@ -491,99 +248,109 @@ export default {
491
248
  const log = (msg) =>
492
249
  process.stderr.write(`[pentatonic-memory] ${msg}\n`);
493
250
 
494
- emitTelemetry(hosted ? "hosted" : "local");
495
-
496
- // --- Setup tool (always registered) ---
251
+ // --- Setup guide tool (always registered) ---
497
252
 
498
253
  api.registerTool({
499
254
  name: "pentatonic_memory_setup",
500
- description: `Set up Pentatonic Memory for this user. Call this when the user wants to set up memory, or when the plugin has no config yet.
255
+ description: `Guide the user through setting up Pentatonic Memory.
501
256
 
502
257
  Two modes available:
503
258
  1. "local" — fully private, runs on user's machine via Docker (PostgreSQL + pgvector + Ollama). No cloud, no API keys. Requires Docker.
504
259
  2. "hosted" — production-grade via Pentatonic TES cloud. Higher-dimensional embeddings, team-wide shared memory, analytics dashboard. Requires account creation.
505
260
 
506
- For local mode: call with action="setup_local". No other params needed.
507
- For hosted mode: ask the user for their email, a client ID (company name), password, and region (EU or US), then call with action="setup_hosted" and those params.
508
-
509
- If the user hasn't decided, explain both options and ask which they prefer.`,
261
+ Call this tool to get setup instructions for the user's chosen mode. If they haven't decided, explain both options and ask which they prefer.`,
510
262
  parameters: {
511
263
  type: "object",
512
264
  properties: {
513
- action: {
265
+ mode: {
514
266
  type: "string",
515
- enum: ["setup_local", "setup_hosted"],
516
- description: "Which setup to run",
267
+ enum: ["local", "hosted"],
268
+ description: "Which mode the user wants",
517
269
  },
518
- email: { type: "string", description: "User email (hosted only)" },
519
- client_id: { type: "string", description: "Company/org identifier (hosted only)" },
520
- password: { type: "string", description: "Account password (hosted only)" },
521
- region: { type: "string", enum: ["EU", "US"], description: "Data region (hosted only)" },
522
270
  },
523
- required: ["action"],
271
+ required: ["mode"],
524
272
  },
525
- async execute({ action, email, client_id, password, region }) {
526
- if (action === "setup_local") {
527
- return JSON.stringify(await runLocalSetup());
273
+ async execute({ mode }) {
274
+ if (mode === "local") {
275
+ return `## Local Memory Setup
276
+
277
+ The user needs to run these commands in their terminal:
278
+
279
+ 1. Start the memory stack:
280
+ \`\`\`
281
+ npx @pentatonic-ai/ai-agent-sdk memory
282
+ \`\`\`
283
+ This starts PostgreSQL + pgvector, Ollama, and the memory server via Docker.
284
+
285
+ 2. Then add this to their openclaw.json:
286
+ \`\`\`json
287
+ {
288
+ "plugins": {
289
+ "slots": { "contextEngine": "pentatonic-memory" },
290
+ "entries": {
291
+ "pentatonic-memory": {
292
+ "enabled": true,
293
+ "config": {
294
+ "database_url": "postgres://memory:memory@localhost:5433/memory",
295
+ "embedding_url": "http://localhost:11435/v1",
296
+ "embedding_model": "nomic-embed-text",
297
+ "llm_url": "http://localhost:11435/v1",
298
+ "llm_model": "llama3.2:3b"
299
+ }
300
+ }
301
+ }
302
+ }
303
+ }
304
+ \`\`\`
305
+
306
+ 3. Restart OpenClaw to activate the context engine.
307
+
308
+ Tell the user to run step 1 first, then help them with the config.`;
528
309
  }
529
- if (action === "setup_hosted") {
530
- if (!email || !client_id || !password) {
531
- return JSON.stringify({
532
- success: false,
533
- error: "Missing required fields: email, client_id, and password are all required for hosted setup.",
534
- });
535
- }
536
- return JSON.stringify(await runHostedSetup(email, client_id, password, region));
310
+
311
+ if (mode === "hosted") {
312
+ return `## Hosted TES Setup
313
+
314
+ The user needs to run this command in their terminal:
315
+
316
+ 1. Create a TES account:
317
+ \`\`\`
318
+ npx @pentatonic-ai/ai-agent-sdk init
319
+ \`\`\`
320
+ This walks through account creation, email verification, and API key generation.
321
+ They'll receive credentials like:
322
+ - TES_ENDPOINT=https://their-company.api.pentatonic.com
323
+ - TES_CLIENT_ID=their-company
324
+ - TES_API_KEY=tes_their-company_xxxxx
325
+
326
+ 2. Then add this to their openclaw.json:
327
+ \`\`\`json
328
+ {
329
+ "plugins": {
330
+ "slots": { "contextEngine": "pentatonic-memory" },
331
+ "entries": {
332
+ "pentatonic-memory": {
333
+ "enabled": true,
334
+ "config": {
335
+ "tes_endpoint": "https://their-company.api.pentatonic.com",
336
+ "tes_client_id": "their-company",
337
+ "tes_api_key": "tes_their-company_xxxxx"
338
+ }
339
+ }
340
+ }
341
+ }
342
+ }
343
+ \`\`\`
344
+
345
+ 3. Restart OpenClaw to activate the context engine.
346
+
347
+ Tell the user to run step 1 first, then help them fill in the config with the credentials they receive.`;
537
348
  }
538
- return JSON.stringify({ success: false, error: "Unknown action" });
349
+
350
+ return "Unknown mode. Choose 'local' or 'hosted'.";
539
351
  },
540
352
  });
541
353
 
542
- // --- CLI subcommand ---
543
-
544
- if (api.registerCli) {
545
- api.registerCli(
546
- async ({ program }) => {
547
- program
548
- .command("pentatonic-memory")
549
- .description("Set up Pentatonic Memory (local or hosted)")
550
- .argument("[mode]", "Setup mode: local or hosted")
551
- .action(async (mode) => {
552
- if (mode === "local") {
553
- console.log("\nSetting up local memory stack...\n");
554
- const result = await runLocalSetup();
555
- if (result.success) {
556
- console.log(`✓ ${result.message}`);
557
- console.log(` Config: ${result.configPath}`);
558
- console.log(` Models: ${result.models.join(", ")}\n`);
559
- console.log("Restart OpenClaw to activate the context engine.\n");
560
- } else {
561
- console.error(`✗ ${result.error}\n`);
562
- process.exit(1);
563
- }
564
- } else if (mode === "hosted") {
565
- console.log("\nHosted setup — use the interactive agent instead:");
566
- console.log(' Tell OpenClaw: "set up pentatonic memory"\n');
567
- } else {
568
- console.log("\nPentatonic Memory Setup\n");
569
- console.log(" openclaw pentatonic-memory local Set up local memory (Docker)");
570
- console.log(" openclaw pentatonic-memory hosted Set up hosted TES (cloud)\n");
571
- console.log('Or just tell OpenClaw: "set up pentatonic memory"\n');
572
- }
573
- });
574
- },
575
- {
576
- descriptors: [
577
- {
578
- name: "pentatonic-memory",
579
- description: "Set up Pentatonic Memory (local Docker stack or hosted TES cloud)",
580
- hasSubcommands: false,
581
- },
582
- ],
583
- }
584
- );
585
- }
586
-
587
354
  if (hosted) {
588
355
  // --- Hosted mode: TES GraphQL ---
589
356
  log("Hosted mode — routing through TES");
@@ -640,7 +407,7 @@ If the user hasn't decided, explain both options and ask which they prefer.`,
640
407
  return result ? "Memory stored." : "Failed to store memory.";
641
408
  },
642
409
  });
643
- } else {
410
+ } else if (config.database_url) {
644
411
  // --- Local mode: direct PostgreSQL ---
645
412
  const mem = getLocalMemory(config);
646
413
  log("Local mode — direct PostgreSQL");
@@ -723,8 +490,11 @@ If the user hasn't decided, explain both options and ask which they prefer.`,
723
490
  .join("\n");
724
491
  },
725
492
  });
493
+ } else {
494
+ // --- No config: setup-only mode ---
495
+ log("No memory config found — setup tool available. Tell OpenClaw: 'set up pentatonic memory'");
726
496
  }
727
497
 
728
- log(`Plugin registered (${hosted ? "hosted" : "local"} mode)`);
498
+ log(`Plugin registered (${hosted ? "hosted" : config.database_url ? "local" : "unconfigured"} mode)`);
729
499
  },
730
500
  };