@pentatonic-ai/ai-agent-sdk 0.5.7 → 0.5.9
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/dist/index.cjs +244 -8
- package/dist/index.js +244 -8
- package/package.json +2 -2
- package/packages/doctor/__tests__/checks.test.js +357 -0
- package/packages/doctor/src/checks/claude-code.js +100 -0
- package/packages/doctor/src/checks/data-flow.js +252 -0
- package/packages/doctor/src/index.js +2 -0
- package/packages/doctor/src/runner.js +7 -3
- package/packages/memory/src/__tests__/api-contract.test.js +151 -0
- package/packages/memory/src/hosted.js +7 -0
- package/packages/memory/src/ingest.js +40 -1
- package/packages/memory/src/inject.js +83 -0
- package/src/client.js +20 -2
- package/src/wrapper.js +129 -6
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { universalChecks } from "../src/checks/universal.js";
|
|
2
2
|
import { hostedTesChecks } from "../src/checks/hosted-tes.js";
|
|
3
|
+
import { dataFlowChecks } from "../src/checks/data-flow.js";
|
|
4
|
+
import { claudeCodeChecks } from "../src/checks/claude-code.js";
|
|
3
5
|
import { platformChecks } from "../src/checks/platform.js";
|
|
4
6
|
|
|
5
7
|
// fetch mocking — we don't want any real network in unit tests.
|
|
@@ -185,3 +187,358 @@ describe("platform checks", () => {
|
|
|
185
187
|
expect(r.msg).toMatch(/no models loaded/);
|
|
186
188
|
});
|
|
187
189
|
});
|
|
190
|
+
|
|
191
|
+
describe("data-flow checks", () => {
|
|
192
|
+
beforeEach(() => {
|
|
193
|
+
process.env.TES_ENDPOINT = "https://example.test";
|
|
194
|
+
process.env.TES_API_KEY = "tes_test_key";
|
|
195
|
+
process.env.TES_CLIENT_ID = "test-client";
|
|
196
|
+
});
|
|
197
|
+
afterEach(() => {
|
|
198
|
+
delete process.env.TES_ENDPOINT;
|
|
199
|
+
delete process.env.TES_API_KEY;
|
|
200
|
+
delete process.env.TES_CLIENT_ID;
|
|
201
|
+
delete process.env.PENTATONIC_DOCTOR_PROBE_QUERY;
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
// Capture the request bodies so tests can assert on the GraphQL
|
|
205
|
+
// shape doctor sends — not just the response handling.
|
|
206
|
+
function captureFetch(handler) {
|
|
207
|
+
const calls = [];
|
|
208
|
+
globalThis.fetch = async (url, opts) => {
|
|
209
|
+
const body = opts?.body ? JSON.parse(opts.body) : null;
|
|
210
|
+
calls.push({ url, headers: opts?.headers || {}, body });
|
|
211
|
+
return handler(url, opts);
|
|
212
|
+
};
|
|
213
|
+
return calls;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
it("registers the three expected probes", () => {
|
|
217
|
+
const names = dataFlowChecks().map((c) => c.name);
|
|
218
|
+
expect(names).toContain("TES event stream has data");
|
|
219
|
+
expect(names).toContain("MEMORY_CREATED events for client");
|
|
220
|
+
expect(names).toContain("semanticSearchMemories returns hits");
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
// --- event stream check ---
|
|
224
|
+
|
|
225
|
+
it("event stream: sends GraphQL query with `limit:1` (not `first:1`)", async () => {
|
|
226
|
+
const calls = captureFetch(async () => ({
|
|
227
|
+
ok: true,
|
|
228
|
+
status: 200,
|
|
229
|
+
json: async () => ({ data: { events: { totalCount: 5 } } }),
|
|
230
|
+
}));
|
|
231
|
+
const c = dataFlowChecks().find(
|
|
232
|
+
(x) => x.name === "TES event stream has data"
|
|
233
|
+
);
|
|
234
|
+
await c.run();
|
|
235
|
+
expect(calls).toHaveLength(1);
|
|
236
|
+
expect(calls[0].body.query).toMatch(/events\(\s*limit:\s*1\s*\)/);
|
|
237
|
+
expect(calls[0].body.query).not.toMatch(/first\s*:/);
|
|
238
|
+
expect(calls[0].body.query).toMatch(/totalCount/);
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
it("event stream: warns when totalCount is 0", async () => {
|
|
242
|
+
captureFetch(async () => ({
|
|
243
|
+
ok: true,
|
|
244
|
+
status: 200,
|
|
245
|
+
json: async () => ({ data: { events: { totalCount: 0 } } }),
|
|
246
|
+
}));
|
|
247
|
+
const c = dataFlowChecks().find(
|
|
248
|
+
(x) => x.name === "TES event stream has data"
|
|
249
|
+
);
|
|
250
|
+
const r = await c.run();
|
|
251
|
+
expect(r.ok).toBe(false);
|
|
252
|
+
expect(r.msg).toMatch(/0 events yet/);
|
|
253
|
+
});
|
|
254
|
+
|
|
255
|
+
it("event stream: passes with a positive count", async () => {
|
|
256
|
+
captureFetch(async () => ({
|
|
257
|
+
ok: true,
|
|
258
|
+
status: 200,
|
|
259
|
+
json: async () => ({ data: { events: { totalCount: 42 } } }),
|
|
260
|
+
}));
|
|
261
|
+
const c = dataFlowChecks().find(
|
|
262
|
+
(x) => x.name === "TES event stream has data"
|
|
263
|
+
);
|
|
264
|
+
const r = await c.run();
|
|
265
|
+
expect(r.ok).toBe(true);
|
|
266
|
+
expect(r.detail.totalCount).toBe(42);
|
|
267
|
+
});
|
|
268
|
+
|
|
269
|
+
// --- memory-created check ---
|
|
270
|
+
|
|
271
|
+
it("memory-created: filter uses eventType + StringFilterInput wrapper", async () => {
|
|
272
|
+
const calls = captureFetch(async () => ({
|
|
273
|
+
ok: true,
|
|
274
|
+
status: 200,
|
|
275
|
+
json: async () => ({ data: { events: { totalCount: 3 } } }),
|
|
276
|
+
}));
|
|
277
|
+
const c = dataFlowChecks().find(
|
|
278
|
+
(x) => x.name === "MEMORY_CREATED events for client"
|
|
279
|
+
);
|
|
280
|
+
await c.run();
|
|
281
|
+
const { query, variables } = calls[0].body;
|
|
282
|
+
// Schema requires eventType (not "kind") with a StringFilterInput
|
|
283
|
+
// wrapper, and clientId likewise as a filter wrapper.
|
|
284
|
+
expect(query).toMatch(/eventType:\s*\{\s*eq:\s*\$eventType\s*\}/);
|
|
285
|
+
expect(query).toMatch(/clientId:\s*\{\s*eq:\s*\$client\s*\}/);
|
|
286
|
+
expect(query).not.toMatch(/\bkind\b/);
|
|
287
|
+
expect(variables.eventType).toBe("MEMORY_CREATED");
|
|
288
|
+
expect(variables.client).toBe("test-client");
|
|
289
|
+
});
|
|
290
|
+
|
|
291
|
+
it("memory-created: flags the client id in the warning", async () => {
|
|
292
|
+
captureFetch(async () => ({
|
|
293
|
+
ok: true,
|
|
294
|
+
status: 200,
|
|
295
|
+
json: async () => ({ data: { events: { totalCount: 0 } } }),
|
|
296
|
+
}));
|
|
297
|
+
const c = dataFlowChecks().find(
|
|
298
|
+
(x) => x.name === "MEMORY_CREATED events for client"
|
|
299
|
+
);
|
|
300
|
+
const r = await c.run();
|
|
301
|
+
expect(r.ok).toBe(false);
|
|
302
|
+
expect(r.msg).toMatch(/test-client/);
|
|
303
|
+
});
|
|
304
|
+
|
|
305
|
+
// --- semantic search check ---
|
|
306
|
+
|
|
307
|
+
it("semantic search: sends required clientId arg + selects similarity (not score)", async () => {
|
|
308
|
+
const calls = captureFetch(async () => ({
|
|
309
|
+
ok: true,
|
|
310
|
+
status: 200,
|
|
311
|
+
json: async () => ({
|
|
312
|
+
data: { semanticSearchMemories: [{ id: "m1", similarity: 0.8 }] },
|
|
313
|
+
}),
|
|
314
|
+
}));
|
|
315
|
+
const c = dataFlowChecks().find(
|
|
316
|
+
(x) => x.name === "semanticSearchMemories returns hits"
|
|
317
|
+
);
|
|
318
|
+
await c.run();
|
|
319
|
+
const { query, variables } = calls[0].body;
|
|
320
|
+
// clientId is required by the schema; doctor must send it.
|
|
321
|
+
expect(query).toMatch(/clientId:\s*\$clientId/);
|
|
322
|
+
expect(variables.clientId).toBe("test-client");
|
|
323
|
+
// Result type exposes `similarity`, not `score`.
|
|
324
|
+
expect(query).toMatch(/similarity/);
|
|
325
|
+
expect(query).not.toMatch(/\bscore\b/);
|
|
326
|
+
});
|
|
327
|
+
|
|
328
|
+
it("semantic search: warns on 0 hits", async () => {
|
|
329
|
+
captureFetch(async () => ({
|
|
330
|
+
ok: true,
|
|
331
|
+
status: 200,
|
|
332
|
+
json: async () => ({ data: { semanticSearchMemories: [] } }),
|
|
333
|
+
}));
|
|
334
|
+
const c = dataFlowChecks().find(
|
|
335
|
+
(x) => x.name === "semanticSearchMemories returns hits"
|
|
336
|
+
);
|
|
337
|
+
const r = await c.run();
|
|
338
|
+
expect(r.ok).toBe(false);
|
|
339
|
+
expect(r.msg).toMatch(/0 hits/);
|
|
340
|
+
});
|
|
341
|
+
|
|
342
|
+
it("semantic search: passes with hits", async () => {
|
|
343
|
+
captureFetch(async () => ({
|
|
344
|
+
ok: true,
|
|
345
|
+
status: 200,
|
|
346
|
+
json: async () => ({
|
|
347
|
+
data: { semanticSearchMemories: [{ id: "m1", similarity: 0.8 }] },
|
|
348
|
+
}),
|
|
349
|
+
}));
|
|
350
|
+
const c = dataFlowChecks().find(
|
|
351
|
+
(x) => x.name === "semanticSearchMemories returns hits"
|
|
352
|
+
);
|
|
353
|
+
const r = await c.run();
|
|
354
|
+
expect(r.ok).toBe(true);
|
|
355
|
+
expect(r.detail.hits).toBe(1);
|
|
356
|
+
});
|
|
357
|
+
|
|
358
|
+
it("semantic search: 'cannot query field' skips gracefully", async () => {
|
|
359
|
+
captureFetch(async () => ({
|
|
360
|
+
ok: true,
|
|
361
|
+
status: 200,
|
|
362
|
+
json: async () => ({
|
|
363
|
+
errors: [{ message: 'Cannot query field "semanticSearchMemories" on type "Query"' }],
|
|
364
|
+
}),
|
|
365
|
+
}));
|
|
366
|
+
const c = dataFlowChecks().find(
|
|
367
|
+
(x) => x.name === "semanticSearchMemories returns hits"
|
|
368
|
+
);
|
|
369
|
+
const r = await c.run();
|
|
370
|
+
expect(r.ok).toBe(true);
|
|
371
|
+
expect(r.msg).toMatch(/skipped/);
|
|
372
|
+
});
|
|
373
|
+
|
|
374
|
+
it("semantic search: schema-arg mismatches surface as errors, NOT silent skips", async () => {
|
|
375
|
+
// E.g. a missing required arg — error mentions the field name but
|
|
376
|
+
// is NOT the "Cannot query field" wording. Doctor must report,
|
|
377
|
+
// not pretend the deployment doesn't expose the field.
|
|
378
|
+
captureFetch(async () => ({
|
|
379
|
+
ok: true,
|
|
380
|
+
status: 200,
|
|
381
|
+
json: async () => ({
|
|
382
|
+
errors: [
|
|
383
|
+
{
|
|
384
|
+
message:
|
|
385
|
+
'Field "semanticSearchMemories" argument "clientId" of type "String!" is required',
|
|
386
|
+
},
|
|
387
|
+
],
|
|
388
|
+
}),
|
|
389
|
+
}));
|
|
390
|
+
const c = dataFlowChecks().find(
|
|
391
|
+
(x) => x.name === "semanticSearchMemories returns hits"
|
|
392
|
+
);
|
|
393
|
+
const r = await c.run();
|
|
394
|
+
expect(r.ok).toBe(false);
|
|
395
|
+
expect(r.msg).not.toMatch(/skipped/);
|
|
396
|
+
expect(r.msg).toMatch(/required/);
|
|
397
|
+
});
|
|
398
|
+
|
|
399
|
+
it("PENTATONIC_DOCTOR_PROBE_QUERY overrides the default probe text", async () => {
|
|
400
|
+
process.env.PENTATONIC_DOCTOR_PROBE_QUERY = "custom probe text";
|
|
401
|
+
const calls = captureFetch(async () => ({
|
|
402
|
+
ok: true,
|
|
403
|
+
status: 200,
|
|
404
|
+
json: async () => ({ data: { semanticSearchMemories: [] } }),
|
|
405
|
+
}));
|
|
406
|
+
const c = dataFlowChecks().find(
|
|
407
|
+
(x) => x.name === "semanticSearchMemories returns hits"
|
|
408
|
+
);
|
|
409
|
+
await c.run();
|
|
410
|
+
expect(calls[0].body.variables.q).toBe("custom probe text");
|
|
411
|
+
});
|
|
412
|
+
|
|
413
|
+
// --- auth header branching ---
|
|
414
|
+
|
|
415
|
+
it("uses Authorization: Bearer for tes_-prefixed keys", async () => {
|
|
416
|
+
const calls = captureFetch(async () => ({
|
|
417
|
+
ok: true,
|
|
418
|
+
status: 200,
|
|
419
|
+
json: async () => ({ data: { events: { totalCount: 1 } } }),
|
|
420
|
+
}));
|
|
421
|
+
process.env.TES_API_KEY = "tes_user_abc";
|
|
422
|
+
const c = dataFlowChecks().find(
|
|
423
|
+
(x) => x.name === "TES event stream has data"
|
|
424
|
+
);
|
|
425
|
+
await c.run();
|
|
426
|
+
expect(calls[0].headers.Authorization).toBe("Bearer tes_user_abc");
|
|
427
|
+
expect(calls[0].headers["x-service-key"]).toBeUndefined();
|
|
428
|
+
});
|
|
429
|
+
|
|
430
|
+
it("uses x-service-key for non-tes_ keys (internal service tokens)", async () => {
|
|
431
|
+
const calls = captureFetch(async () => ({
|
|
432
|
+
ok: true,
|
|
433
|
+
status: 200,
|
|
434
|
+
json: async () => ({ data: { events: { totalCount: 1 } } }),
|
|
435
|
+
}));
|
|
436
|
+
process.env.TES_API_KEY = "internal_svc_xyz";
|
|
437
|
+
const c = dataFlowChecks().find(
|
|
438
|
+
(x) => x.name === "TES event stream has data"
|
|
439
|
+
);
|
|
440
|
+
await c.run();
|
|
441
|
+
expect(calls[0].headers["x-service-key"]).toBe("internal_svc_xyz");
|
|
442
|
+
expect(calls[0].headers.Authorization).toBeUndefined();
|
|
443
|
+
});
|
|
444
|
+
|
|
445
|
+
it("sends x-client-id on every request", async () => {
|
|
446
|
+
const calls = captureFetch(async () => ({
|
|
447
|
+
ok: true,
|
|
448
|
+
status: 200,
|
|
449
|
+
json: async () => ({ data: { events: { totalCount: 1 } } }),
|
|
450
|
+
}));
|
|
451
|
+
const c = dataFlowChecks().find(
|
|
452
|
+
(x) => x.name === "TES event stream has data"
|
|
453
|
+
);
|
|
454
|
+
await c.run();
|
|
455
|
+
expect(calls[0].headers["x-client-id"]).toBe("test-client");
|
|
456
|
+
});
|
|
457
|
+
|
|
458
|
+
it("all three report missing env clearly", async () => {
|
|
459
|
+
delete process.env.TES_CLIENT_ID;
|
|
460
|
+
for (const c of dataFlowChecks()) {
|
|
461
|
+
const r = await c.run();
|
|
462
|
+
expect(r.ok).toBe(false);
|
|
463
|
+
expect(r.msg).toMatch(/TES_ENDPOINT|required/);
|
|
464
|
+
}
|
|
465
|
+
});
|
|
466
|
+
});
|
|
467
|
+
|
|
468
|
+
describe("Claude Code plugin check", () => {
|
|
469
|
+
it("reports installed + version when manifest is present at ~/.claude", async () => {
|
|
470
|
+
const [check] = claudeCodeChecks({
|
|
471
|
+
fileExists: (p) => p === "/home/fake/.claude/plugins/marketplaces/pentatonic-ai/.claude-plugin/plugin.json",
|
|
472
|
+
readFile: () =>
|
|
473
|
+
JSON.stringify({ name: "tes-memory", version: "0.5.3" }),
|
|
474
|
+
homedir: () => "/home/fake",
|
|
475
|
+
env: {},
|
|
476
|
+
});
|
|
477
|
+
const r = await check.run();
|
|
478
|
+
expect(r.ok).toBe(true);
|
|
479
|
+
expect(r.msg).toMatch(/tes-memory v0\.5\.3 installed/);
|
|
480
|
+
expect(r.detail.version).toBe("0.5.3");
|
|
481
|
+
expect(r.detail.path).toMatch(/\.claude\/plugins/);
|
|
482
|
+
});
|
|
483
|
+
|
|
484
|
+
it("falls through to ~/.claude-pentatonic when ~/.claude is empty", async () => {
|
|
485
|
+
const pentatonicPath =
|
|
486
|
+
"/home/fake/.claude-pentatonic/plugins/marketplaces/pentatonic-ai/.claude-plugin/plugin.json";
|
|
487
|
+
const [check] = claudeCodeChecks({
|
|
488
|
+
fileExists: (p) => p === pentatonicPath,
|
|
489
|
+
readFile: () =>
|
|
490
|
+
JSON.stringify({ name: "tes-memory", version: "0.5.3" }),
|
|
491
|
+
homedir: () => "/home/fake",
|
|
492
|
+
env: {},
|
|
493
|
+
});
|
|
494
|
+
const r = await check.run();
|
|
495
|
+
expect(r.ok).toBe(true);
|
|
496
|
+
expect(r.detail.path).toBe(pentatonicPath);
|
|
497
|
+
});
|
|
498
|
+
|
|
499
|
+
it("respects CLAUDE_CONFIG_DIR override (highest precedence)", async () => {
|
|
500
|
+
const overridePath =
|
|
501
|
+
"/custom/cfg/plugins/marketplaces/pentatonic-ai/.claude-plugin/plugin.json";
|
|
502
|
+
const [check] = claudeCodeChecks({
|
|
503
|
+
fileExists: (p) => p === overridePath,
|
|
504
|
+
readFile: () =>
|
|
505
|
+
JSON.stringify({ name: "tes-memory", version: "9.9.9" }),
|
|
506
|
+
homedir: () => "/home/fake",
|
|
507
|
+
env: { CLAUDE_CONFIG_DIR: "/custom/cfg" },
|
|
508
|
+
});
|
|
509
|
+
const r = await check.run();
|
|
510
|
+
expect(r.ok).toBe(true);
|
|
511
|
+
expect(r.detail.path).toBe(overridePath);
|
|
512
|
+
expect(r.detail.version).toBe("9.9.9");
|
|
513
|
+
});
|
|
514
|
+
|
|
515
|
+
it("reports the install command + all candidate paths when none exist", async () => {
|
|
516
|
+
const [check] = claudeCodeChecks({
|
|
517
|
+
fileExists: () => false,
|
|
518
|
+
homedir: () => "/home/fake",
|
|
519
|
+
env: { CLAUDE_CONFIG_DIR: "/custom/cfg" },
|
|
520
|
+
});
|
|
521
|
+
const r = await check.run();
|
|
522
|
+
expect(r.ok).toBe(false);
|
|
523
|
+
expect(r.msg).toMatch(/plugin install tes-memory/);
|
|
524
|
+
expect(r.detail.candidates).toEqual(
|
|
525
|
+
expect.arrayContaining([
|
|
526
|
+
expect.stringContaining("/custom/cfg/plugins"),
|
|
527
|
+
expect.stringContaining("/home/fake/.claude/plugins"),
|
|
528
|
+
expect.stringContaining("/home/fake/.claude-pentatonic/plugins"),
|
|
529
|
+
])
|
|
530
|
+
);
|
|
531
|
+
});
|
|
532
|
+
|
|
533
|
+
it("handles corrupt manifest json without throwing", async () => {
|
|
534
|
+
const [check] = claudeCodeChecks({
|
|
535
|
+
fileExists: () => true,
|
|
536
|
+
readFile: () => "{ not json",
|
|
537
|
+
homedir: () => "/home/fake",
|
|
538
|
+
env: {},
|
|
539
|
+
});
|
|
540
|
+
const r = await check.run();
|
|
541
|
+
expect(r.ok).toBe(false);
|
|
542
|
+
expect(r.msg).toMatch(/unreadable/);
|
|
543
|
+
});
|
|
544
|
+
});
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Claude Code plugin installation check.
|
|
3
|
+
*
|
|
4
|
+
* The SDK ships a Claude Code plugin (`tes-memory@pentatonic-ai`) that
|
|
5
|
+
* wires UserPromptSubmit / Stop hooks so CHAT_TURN + MEMORY_CREATED
|
|
6
|
+
* events actually get emitted. It's entirely possible for the server
|
|
7
|
+
* side to be healthy (TES reachable, key valid) while the client side
|
|
8
|
+
* is silently uninstalled — the hooks never fire and the event stream
|
|
9
|
+
* stays empty. This check tells users whether the plugin is present
|
|
10
|
+
* and what version they're on, so upstream feedback ("why am I not
|
|
11
|
+
* seeing memories?") lands faster.
|
|
12
|
+
*
|
|
13
|
+
* Resolution order mirrors `hooks/scripts/shared.js:loadConfig` — three
|
|
14
|
+
* candidate roots, first match wins:
|
|
15
|
+
*
|
|
16
|
+
* 1. $CLAUDE_CONFIG_DIR (explicit override, highest precedence)
|
|
17
|
+
* 2. ~/.claude (default Claude Code install)
|
|
18
|
+
* 3. ~/.claude-pentatonic (Pentatonic-branded variant)
|
|
19
|
+
*
|
|
20
|
+
* The check is universal-ish: it only reports positively when the
|
|
21
|
+
* plugin file is found. If the user isn't on Claude Code at all, the
|
|
22
|
+
* plugin absence is reported as info, not a failure.
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
import { existsSync as realExistsSync, readFileSync as realReadFileSync } from "fs";
|
|
26
|
+
import { join } from "path";
|
|
27
|
+
import { homedir as realHomedir } from "os";
|
|
28
|
+
|
|
29
|
+
import { SEVERITY } from "../index.js";
|
|
30
|
+
|
|
31
|
+
const PLUGIN_REL_PATH = [
|
|
32
|
+
"plugins",
|
|
33
|
+
"marketplaces",
|
|
34
|
+
"pentatonic-ai",
|
|
35
|
+
".claude-plugin",
|
|
36
|
+
"plugin.json",
|
|
37
|
+
];
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Build the ordered list of candidate manifest paths. First match wins.
|
|
41
|
+
* Same precedence as the SDK hook's loadConfig() so users on
|
|
42
|
+
* CLAUDE_CONFIG_DIR or .claude-pentatonic don't get false negatives.
|
|
43
|
+
*/
|
|
44
|
+
function candidateManifestPaths(home, env) {
|
|
45
|
+
const roots = [];
|
|
46
|
+
if (env?.CLAUDE_CONFIG_DIR) roots.push(env.CLAUDE_CONFIG_DIR);
|
|
47
|
+
roots.push(join(home, ".claude"));
|
|
48
|
+
roots.push(join(home, ".claude-pentatonic"));
|
|
49
|
+
return roots.map((root) => join(root, ...PLUGIN_REL_PATH));
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function checkClaudeCodePluginInstalled({
|
|
53
|
+
fileExists,
|
|
54
|
+
readFile,
|
|
55
|
+
homedir,
|
|
56
|
+
env,
|
|
57
|
+
} = {}) {
|
|
58
|
+
const exists = fileExists || realExistsSync;
|
|
59
|
+
const read = readFile || ((p) => realReadFileSync(p, "utf8"));
|
|
60
|
+
const resolveHome = typeof homedir === "function" ? homedir : realHomedir;
|
|
61
|
+
const resolveEnv = env || process.env;
|
|
62
|
+
const home = resolveHome();
|
|
63
|
+
|
|
64
|
+
return {
|
|
65
|
+
name: "tes-memory Claude Code plugin installed",
|
|
66
|
+
severity: SEVERITY.INFO,
|
|
67
|
+
run: async () => {
|
|
68
|
+
const candidates = candidateManifestPaths(home, resolveEnv);
|
|
69
|
+
const found = candidates.find((p) => exists(p));
|
|
70
|
+
if (!found) {
|
|
71
|
+
return {
|
|
72
|
+
ok: false,
|
|
73
|
+
msg:
|
|
74
|
+
"tes-memory plugin not found — run: /plugin marketplace add Pentatonic-Ltd/ai-agent-sdk && /plugin install tes-memory@pentatonic-ai",
|
|
75
|
+
detail: { candidates },
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
try {
|
|
79
|
+
const manifest = JSON.parse(read(found));
|
|
80
|
+
const version = typeof manifest.version === "string" ? manifest.version : "?";
|
|
81
|
+
const name = typeof manifest.name === "string" ? manifest.name : "tes-memory";
|
|
82
|
+
return {
|
|
83
|
+
ok: true,
|
|
84
|
+
msg: `${name} v${version} installed`,
|
|
85
|
+
detail: { name, version, path: found },
|
|
86
|
+
};
|
|
87
|
+
} catch (err) {
|
|
88
|
+
return {
|
|
89
|
+
ok: false,
|
|
90
|
+
msg: `plugin manifest unreadable: ${err.message}`,
|
|
91
|
+
detail: { path: found },
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
},
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export function claudeCodeChecks(seams = {}) {
|
|
99
|
+
return [checkClaudeCodePluginInstalled(seams)];
|
|
100
|
+
}
|