@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,70 @@
1
+ import { startCallbackServer } from "../lib/callback-server.js";
2
+
3
+ async function fetchCallback(port, qs) {
4
+ // Use 127.0.0.1 not "localhost" — undici (Node 18+) resolves localhost to
5
+ // ::1 first, but the server binds to 127.0.0.1 only, so on IPv6-preferring
6
+ // hosts (GitHub Actions runners) the IPv6 attempt ECONNREFUSEs.
7
+ const url = `http://127.0.0.1:${port}/callback?${qs}`;
8
+ const res = await fetch(url);
9
+ return { status: res.status, text: await res.text() };
10
+ }
11
+
12
+ describe("startCallbackServer", () => {
13
+ it("resolves with {code, state} when callback hits with matching state", async () => {
14
+ const expectedState = "abc123";
15
+ const { port, result } = await startCallbackServer({
16
+ ports: [0],
17
+ state: expectedState,
18
+ timeoutMs: 5000,
19
+ });
20
+ const fetchPromise = fetchCallback(
21
+ port,
22
+ `code=AUTH_CODE_XYZ&state=${expectedState}`
23
+ );
24
+ const callback = await result;
25
+ const httpRes = await fetchPromise;
26
+ expect(callback.code).toBe("AUTH_CODE_XYZ");
27
+ expect(callback.state).toBe(expectedState);
28
+ expect(httpRes.status).toBe(200);
29
+ expect(httpRes.text).toMatch(/close this tab/i);
30
+ });
31
+
32
+ it("rejects when state does not match", async () => {
33
+ const { port, result } = await startCallbackServer({
34
+ ports: [0],
35
+ state: "EXPECTED",
36
+ timeoutMs: 5000,
37
+ });
38
+ fetchCallback(port, "code=ANY&state=ATTACKER").catch(() => {});
39
+ await expect(result).rejects.toThrow(/state/i);
40
+ });
41
+
42
+ it("rejects on timeout", async () => {
43
+ const { result } = await startCallbackServer({
44
+ ports: [0],
45
+ state: "S",
46
+ timeoutMs: 100,
47
+ });
48
+ await expect(result).rejects.toThrow(/timeout|timed out/i);
49
+ });
50
+
51
+ it("uses the first available port from the list", async () => {
52
+ // Bind one server to a known port to force the next attempt.
53
+ const blocker = await startCallbackServer({
54
+ ports: [0],
55
+ state: "BLOCKER",
56
+ timeoutMs: 30000,
57
+ });
58
+ const blockedPort = blocker.port;
59
+ // Now ask the second server to try the blocked port first, then fall
60
+ // through to OS-assigned. We expect it to land on a different port.
61
+ const second = await startCallbackServer({
62
+ ports: [blockedPort, 0],
63
+ state: "S",
64
+ timeoutMs: 30000,
65
+ });
66
+ expect(second.port).not.toBe(blockedPort);
67
+ second.cancel();
68
+ blocker.cancel();
69
+ });
70
+ });
@@ -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
+ });