@pentatonic-ai/ai-agent-sdk 0.5.10 → 0.6.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 (33) hide show
  1. package/README.md +233 -163
  2. package/bin/__tests__/callback-server.test.js +67 -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 +101 -309
  8. package/bin/commands/login.js +219 -0
  9. package/bin/commands/whoami.js +41 -0
  10. package/bin/lib/callback-server.js +137 -0
  11. package/bin/lib/credentials.js +100 -0
  12. package/bin/lib/pkce.js +26 -0
  13. package/package.json +3 -2
  14. package/packages/memory/src/__tests__/api-contract.test.js +122 -13
  15. package/packages/memory/src/__tests__/corpus-chunkers.test.js +143 -0
  16. package/packages/memory/src/__tests__/corpus-discover.test.js +175 -0
  17. package/packages/memory/src/__tests__/corpus-ingest.test.js +236 -0
  18. package/packages/memory/src/__tests__/corpus-signatures.test.js +175 -0
  19. package/packages/memory/src/__tests__/corpus-state.test.js +161 -0
  20. package/packages/memory/src/__tests__/ingest-corpus-opts.test.js +129 -0
  21. package/packages/memory/src/__tests__/search-kind.test.js +108 -0
  22. package/packages/memory/src/corpus/adapters.js +294 -0
  23. package/packages/memory/src/corpus/chunkers.js +328 -0
  24. package/packages/memory/src/corpus/cli.js +548 -0
  25. package/packages/memory/src/corpus/discover.js +379 -0
  26. package/packages/memory/src/corpus/index.js +68 -0
  27. package/packages/memory/src/corpus/ingest.js +356 -0
  28. package/packages/memory/src/corpus/signatures.js +280 -0
  29. package/packages/memory/src/corpus/state.js +134 -0
  30. package/packages/memory/src/index.js +18 -0
  31. package/packages/memory/src/ingest.js +83 -31
  32. package/packages/memory/src/openclaw/index.js +39 -1
  33. package/packages/memory/src/search.js +30 -7
@@ -0,0 +1,58 @@
1
+ import { writeCredentials, readCredentials, credentialsPath } from "../lib/credentials.js";
2
+ import { mkdtemp, rm, stat, readFile } from "node:fs/promises";
3
+ import { tmpdir } from "node:os";
4
+ import { join } from "node:path";
5
+
6
+ describe("credentials helpers", () => {
7
+ let tmp;
8
+
9
+ beforeEach(async () => {
10
+ tmp = await mkdtemp(join(tmpdir(), "tes-cred-"));
11
+ process.env.XDG_CONFIG_HOME = tmp;
12
+ });
13
+
14
+ afterEach(async () => {
15
+ await rm(tmp, { recursive: true, force: true });
16
+ delete process.env.XDG_CONFIG_HOME;
17
+ });
18
+
19
+ it("writeCredentials creates ~/.config/tes/credentials.json with mode 0600", async () => {
20
+ await writeCredentials({
21
+ endpoint: "https://api.pentatonic.com",
22
+ clientId: "tes-demo",
23
+ apiKey: "tes_tes-demo_xxxxxx",
24
+ });
25
+ const path = credentialsPath();
26
+ expect(path).toBe(join(tmp, "tes", "credentials.json"));
27
+ const st = await stat(path);
28
+ expect(st.mode & 0o777).toBe(0o600);
29
+ const parsed = JSON.parse(await readFile(path, "utf8"));
30
+ expect(parsed).toEqual({
31
+ endpoint: "https://api.pentatonic.com",
32
+ clientId: "tes-demo",
33
+ apiKey: "tes_tes-demo_xxxxxx",
34
+ });
35
+ });
36
+
37
+ it("readCredentials returns null when the file does not exist", async () => {
38
+ const got = await readCredentials();
39
+ expect(got).toBeNull();
40
+ });
41
+
42
+ it("readCredentials round-trips a write", async () => {
43
+ await writeCredentials({
44
+ endpoint: "https://api.pentatonic.com",
45
+ clientId: "tes-demo",
46
+ apiKey: "tes_xxx",
47
+ });
48
+ const got = await readCredentials();
49
+ expect(got.clientId).toBe("tes-demo");
50
+ });
51
+
52
+ it("writeCredentials overwrites an existing file (login = re-auth)", async () => {
53
+ await writeCredentials({ endpoint: "a", clientId: "b", apiKey: "c" });
54
+ await writeCredentials({ endpoint: "x", clientId: "y", apiKey: "z" });
55
+ const got = await readCredentials();
56
+ expect(got).toEqual({ endpoint: "x", clientId: "y", apiKey: "z" });
57
+ });
58
+ });
@@ -0,0 +1,210 @@
1
+ import { jest } from "@jest/globals";
2
+ import { mkdtemp, rm, readFile } from "node:fs/promises";
3
+ import { tmpdir } from "node:os";
4
+ import { join } from "node:path";
5
+ import { Buffer } from "node:buffer";
6
+
7
+ // Hoisted mock for callback-server. Real one binds a real socket; for
8
+ // login command tests we want a deterministic stub.
9
+ jest.unstable_mockModule("../lib/callback-server.js", () => ({
10
+ startCallbackServer: jest.fn(),
11
+ }));
12
+
13
+ let runLoginCommand, runInitAlias;
14
+ let startCallbackServer;
15
+
16
+ // Build a fake JWT with given claims (unverified — login decodes claims
17
+ // without verifying).
18
+ function fakeJwt(claims) {
19
+ const header = Buffer.from(JSON.stringify({ alg: "HS256", typ: "JWT" }))
20
+ .toString("base64")
21
+ .replace(/\+/g, "-")
22
+ .replace(/\//g, "_")
23
+ .replace(/=+$/, "");
24
+ const payload = Buffer.from(JSON.stringify(claims))
25
+ .toString("base64")
26
+ .replace(/\+/g, "-")
27
+ .replace(/\//g, "_")
28
+ .replace(/=+$/, "");
29
+ return `${header}.${payload}.fake-signature`;
30
+ }
31
+
32
+ describe("login command", () => {
33
+ let tmp;
34
+ let fetchMock;
35
+
36
+ beforeEach(async () => {
37
+ tmp = await mkdtemp(join(tmpdir(), "tes-login-"));
38
+ process.env.XDG_CONFIG_HOME = tmp;
39
+
40
+ const cb = await import("../lib/callback-server.js");
41
+ startCallbackServer = cb.startCallbackServer;
42
+ startCallbackServer.mockReset();
43
+
44
+ startCallbackServer.mockResolvedValue({
45
+ port: 14171,
46
+ result: Promise.resolve({ code: "AUTH_CODE", state: "RETURNED_STATE" }),
47
+ cancel: jest.fn(),
48
+ });
49
+
50
+ fetchMock = jest.fn();
51
+ globalThis.fetch = fetchMock;
52
+
53
+ ({ runLoginCommand, runInitAlias } = await import("../commands/login.js"));
54
+ });
55
+
56
+ afterEach(async () => {
57
+ await rm(tmp, { recursive: true, force: true });
58
+ delete process.env.XDG_CONFIG_HOME;
59
+ });
60
+
61
+ it("happy path: code → access_token → tes_* key → credentials written", async () => {
62
+ let capturedState;
63
+ startCallbackServer.mockImplementationOnce(({ state }) => {
64
+ capturedState = state;
65
+ return Promise.resolve({
66
+ port: 14171,
67
+ result: Promise.resolve({ code: "AUTH_CODE", state }),
68
+ cancel: jest.fn(),
69
+ });
70
+ });
71
+
72
+ const accessToken = fakeJwt({ client_id: "tes-demo", email: "phil@x.com" });
73
+
74
+ fetchMock
75
+ // POST /oauth/token
76
+ .mockResolvedValueOnce({
77
+ ok: true,
78
+ status: 200,
79
+ json: async () => ({ access_token: accessToken, expires_in: 300 }),
80
+ })
81
+ // POST /api/graphql (createClientApiToken)
82
+ .mockResolvedValueOnce({
83
+ ok: true,
84
+ status: 200,
85
+ json: async () => ({
86
+ data: {
87
+ createClientApiToken: {
88
+ success: true,
89
+ plainTextToken: "tes_tes-demo_LIVE_KEY",
90
+ },
91
+ },
92
+ }),
93
+ });
94
+
95
+ const result = await runLoginCommand({
96
+ endpoint: "https://api.pentatonic.com",
97
+ openBrowser: jest.fn(),
98
+ log: () => {},
99
+ errLog: () => {},
100
+ });
101
+
102
+ expect(result.exitCode).toBe(0);
103
+ expect(typeof capturedState).toBe("string");
104
+ expect(capturedState.length).toBeGreaterThan(20);
105
+
106
+ const tokenCall = fetchMock.mock.calls[0];
107
+ expect(tokenCall[0]).toMatch(/\/oauth\/token$/);
108
+ expect(tokenCall[1].method).toBe("POST");
109
+ const tokenBody = new URLSearchParams(tokenCall[1].body);
110
+ expect(tokenBody.get("grant_type")).toBe("authorization_code");
111
+ expect(tokenBody.get("code")).toBe("AUTH_CODE");
112
+
113
+ const credPath = join(tmp, "tes", "credentials.json");
114
+ const creds = JSON.parse(await readFile(credPath, "utf8"));
115
+ expect(creds.apiKey).toBe("tes_tes-demo_LIVE_KEY");
116
+ expect(creds.endpoint).toBe("https://tes-demo.api.pentatonic.com");
117
+ expect(creds.clientId).toBe("tes-demo");
118
+ });
119
+
120
+ it("fails non-zero when /oauth/token rejects the code", async () => {
121
+ fetchMock.mockResolvedValueOnce({
122
+ ok: false,
123
+ status: 400,
124
+ json: async () => ({ error: "invalid_grant" }),
125
+ });
126
+ const result = await runLoginCommand({
127
+ endpoint: "https://api.pentatonic.com",
128
+ openBrowser: jest.fn(),
129
+ log: () => {},
130
+ errLog: () => {},
131
+ });
132
+ expect(result.exitCode).not.toBe(0);
133
+ });
134
+
135
+ it("fails non-zero when createClientApiToken fails", async () => {
136
+ const accessToken = fakeJwt({ client_id: "tes-demo" });
137
+ fetchMock
138
+ .mockResolvedValueOnce({
139
+ ok: true,
140
+ status: 200,
141
+ json: async () => ({ access_token: accessToken, expires_in: 300 }),
142
+ })
143
+ .mockResolvedValueOnce({
144
+ ok: true,
145
+ status: 200,
146
+ json: async () => ({ errors: [{ message: "permission denied" }] }),
147
+ });
148
+ const result = await runLoginCommand({
149
+ endpoint: "https://api.pentatonic.com",
150
+ openBrowser: jest.fn(),
151
+ log: () => {},
152
+ errLog: () => {},
153
+ });
154
+ expect(result.exitCode).not.toBe(0);
155
+ });
156
+
157
+ it("propagates port-conflict failure from callback-server", async () => {
158
+ startCallbackServer.mockRejectedValueOnce(
159
+ new Error("Could not bind to any of ports: 14171, 14172, 14173")
160
+ );
161
+ const result = await runLoginCommand({
162
+ endpoint: "https://api.pentatonic.com",
163
+ openBrowser: jest.fn(),
164
+ log: () => {},
165
+ errLog: () => {},
166
+ });
167
+ expect(result.exitCode).not.toBe(0);
168
+ });
169
+ });
170
+
171
+ describe("init alias", () => {
172
+ let runInitAlias;
173
+ let runLoginCommand;
174
+ let startCallbackServer;
175
+ let fetchMock;
176
+ let tmp;
177
+
178
+ beforeEach(async () => {
179
+ tmp = await mkdtemp(join(tmpdir(), "tes-login-"));
180
+ process.env.XDG_CONFIG_HOME = tmp;
181
+
182
+ const cb = await import("../lib/callback-server.js");
183
+ startCallbackServer = cb.startCallbackServer;
184
+ startCallbackServer.mockReset();
185
+ startCallbackServer.mockRejectedValue(new Error("not bound for this test"));
186
+
187
+ fetchMock = jest.fn();
188
+ globalThis.fetch = fetchMock;
189
+
190
+ ({ runInitAlias, runLoginCommand } = await import("../commands/login.js"));
191
+ });
192
+
193
+ afterEach(async () => {
194
+ await rm(tmp, { recursive: true, force: true });
195
+ delete process.env.XDG_CONFIG_HOME;
196
+ });
197
+
198
+ it("emits a one-line deprecation warning to stderr and delegates to login", async () => {
199
+ const errs = [];
200
+ const errLog = (m) => errs.push(m);
201
+ const result = await runInitAlias({
202
+ endpoint: "https://api.pentatonic.com",
203
+ openBrowser: jest.fn(),
204
+ log: () => {},
205
+ errLog,
206
+ });
207
+ expect(errs.join("\n")).toMatch(/init.*deprecated.*login/i);
208
+ expect(typeof result.exitCode).toBe("number");
209
+ });
210
+ });
@@ -0,0 +1,39 @@
1
+ import { generatePKCE } from "../lib/pkce.js";
2
+ import { createHash } from "node:crypto";
3
+
4
+ describe("generatePKCE", () => {
5
+ it("returns verifier between 43 and 128 chars (RFC 7636)", () => {
6
+ const { verifier } = generatePKCE();
7
+ expect(verifier.length).toBeGreaterThanOrEqual(43);
8
+ expect(verifier.length).toBeLessThanOrEqual(128);
9
+ });
10
+
11
+ it("verifier uses unreserved URL characters only", () => {
12
+ const { verifier } = generatePKCE();
13
+ expect(verifier).toMatch(/^[A-Za-z0-9\-._~]+$/);
14
+ });
15
+
16
+ it("challenge is base64url(SHA-256(verifier)) per RFC 7636", () => {
17
+ const { verifier, challenge } = generatePKCE();
18
+ const expected = createHash("sha256")
19
+ .update(verifier)
20
+ .digest("base64")
21
+ .replace(/\+/g, "-")
22
+ .replace(/\//g, "_")
23
+ .replace(/=+$/, "");
24
+ expect(challenge).toBe(expected);
25
+ });
26
+
27
+ it("returns a 32-byte (43-char base64url) state", () => {
28
+ const { state } = generatePKCE();
29
+ expect(state.length).toBeGreaterThanOrEqual(43);
30
+ expect(state).toMatch(/^[A-Za-z0-9\-._~]+$/);
31
+ });
32
+
33
+ it("two calls produce distinct verifier and state", () => {
34
+ const a = generatePKCE();
35
+ const b = generatePKCE();
36
+ expect(a.verifier).not.toBe(b.verifier);
37
+ expect(a.state).not.toBe(b.state);
38
+ });
39
+ });
@@ -0,0 +1,77 @@
1
+ import { jest } from "@jest/globals";
2
+ import { mkdtemp, rm, writeFile, mkdir } from "node:fs/promises";
3
+ import { tmpdir } from "node:os";
4
+ import { join } from "node:path";
5
+
6
+ let runWhoamiCommand;
7
+
8
+ describe("whoami command", () => {
9
+ let tmp;
10
+ let logs;
11
+ let errs;
12
+ let log;
13
+ let errLog;
14
+ let fetchMock;
15
+
16
+ beforeEach(async () => {
17
+ tmp = await mkdtemp(join(tmpdir(), "tes-whoami-"));
18
+ process.env.XDG_CONFIG_HOME = tmp;
19
+ logs = [];
20
+ errs = [];
21
+ log = (m) => logs.push(m);
22
+ errLog = (m) => errs.push(m);
23
+ fetchMock = jest.fn();
24
+ globalThis.fetch = fetchMock;
25
+ ({ runWhoamiCommand } = await import("../commands/whoami.js"));
26
+ });
27
+
28
+ afterEach(async () => {
29
+ await rm(tmp, { recursive: true, force: true });
30
+ delete process.env.XDG_CONFIG_HOME;
31
+ });
32
+
33
+ async function writeCreds(creds) {
34
+ const dir = join(tmp, "tes");
35
+ await mkdir(dir, { recursive: true });
36
+ await writeFile(join(dir, "credentials.json"), JSON.stringify(creds));
37
+ }
38
+
39
+ it("prints 'Not logged in' and exits 1 when no credentials", async () => {
40
+ const result = await runWhoamiCommand({ log, errLog });
41
+ expect(result.exitCode).toBe(1);
42
+ expect(logs.join("\n")).toMatch(/not logged in|run login/i);
43
+ });
44
+
45
+ it("prints tenant identity on healthy creds", async () => {
46
+ await writeCreds({
47
+ endpoint: "https://tes-demo.api.pentatonic.com",
48
+ clientId: "tes-demo",
49
+ apiKey: "tes_xxx",
50
+ });
51
+ fetchMock.mockResolvedValueOnce({
52
+ ok: true,
53
+ status: 200,
54
+ json: async () => ({ data: { memoryLayers: [{ id: "ml_tes-demo_episodic" }] } }),
55
+ });
56
+ const result = await runWhoamiCommand({ log, errLog });
57
+ expect(result.exitCode).toBe(0);
58
+ expect(logs.join("\n")).toMatch(/tes-demo/);
59
+ });
60
+
61
+ it("warns about invalid creds on 401 and exits 2", async () => {
62
+ await writeCreds({
63
+ endpoint: "https://tes-demo.api.pentatonic.com",
64
+ clientId: "tes-demo",
65
+ apiKey: "tes_xxx",
66
+ });
67
+ fetchMock.mockResolvedValueOnce({
68
+ ok: false,
69
+ status: 401,
70
+ json: async () => ({ errors: [{ message: "unauthorized" }] }),
71
+ text: async () => "unauthorized",
72
+ });
73
+ const result = await runWhoamiCommand({ log, errLog });
74
+ expect(result.exitCode).toBe(2);
75
+ expect(errs.join("\n") + logs.join("\n")).toMatch(/invalid|run login/i);
76
+ });
77
+ });