@khirby/plugin-pokelo 1.0.0 → 1.1.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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@khirby/plugin-pokelo",
3
- "version": "1.0.0",
3
+ "version": "1.1.0",
4
4
  "description": "Khirby — Pokelo RAG knowledge base context for AI Compose",
5
5
  "main": "src/index.ts",
6
6
  "keywords": [
@@ -21,4 +21,4 @@
21
21
  "access": "public",
22
22
  "registry": "https://registry.npmjs.org"
23
23
  }
24
- }
24
+ }
@@ -1,5 +1,5 @@
1
1
  import { Injectable, Logger } from '@nestjs/common';
2
- import type { PokeloContextServiceLike, PokeloFetchOpts } from '../../../packages/plugin-host/src';
2
+ import type { KnowledgeContextLike, PokeloFetchOpts } from '../../../packages/plugin-host/src';
3
3
  import { PokeloSettingsService } from './pokelo-settings.service';
4
4
 
5
5
  const SNIPPET_LIMIT_TOTAL = 8;
@@ -9,12 +9,13 @@ const SNIPPET_MAX_CHARS = 800;
9
9
  type McpToolResult = {
10
10
  result?: {
11
11
  content?: Array<{ type?: string; text?: string }>;
12
+ isError?: boolean;
12
13
  };
13
14
  error?: { message?: string };
14
15
  };
15
16
 
16
17
  @Injectable()
17
- export class PokeloContextService implements PokeloContextServiceLike {
18
+ export class PokeloContextService implements KnowledgeContextLike {
18
19
  private readonly logger = new Logger(PokeloContextService.name);
19
20
 
20
21
  constructor(private readonly settings: PokeloSettingsService) {}
@@ -177,6 +178,11 @@ export class PokeloContextService implements PokeloContextServiceLike {
177
178
  if (envelope.error) {
178
179
  throw new Error(envelope.error.message ?? 'Pokelo MCP tool error');
179
180
  }
181
+ // Pokelo maps tool failures to CallToolResult.isError (HTTP 200), not JSON-RPC error.
182
+ if (envelope.result?.isError) {
183
+ const msg = envelope.result.content?.[0]?.text?.trim() || 'Pokelo MCP tool error';
184
+ throw new Error(msg);
185
+ }
180
186
 
181
187
  return envelope.result?.content?.[0]?.text ?? '';
182
188
  }
@@ -223,12 +229,19 @@ export function parseProjectList(text: string): Array<{ id: string; name: string
223
229
  if (!text.trim()) return [];
224
230
  try {
225
231
  const parsed = JSON.parse(text) as {
226
- items?: Array<{ id?: string; name?: string }>;
232
+ items?: Array<{ id?: string; projectId?: string; name?: string }>;
227
233
  };
228
234
  if (Array.isArray(parsed.items)) {
229
235
  return parsed.items
230
- .filter((p): p is { id: string; name: string } => !!p.id && !!p.name)
231
- .map((p) => ({ id: p.id, name: p.name }));
236
+ .map((p) => {
237
+ const id =
238
+ (typeof p.projectId === 'string' && p.projectId.trim()) ||
239
+ (typeof p.id === 'string' && p.id.trim()) ||
240
+ '';
241
+ const name = typeof p.name === 'string' ? p.name.trim() : '';
242
+ return { id, name };
243
+ })
244
+ .filter((p) => p.id && p.name);
232
245
  }
233
246
  } catch {
234
247
  // ignore
@@ -1,60 +1,5 @@
1
- import { createCipheriv, createDecipheriv, randomBytes } from 'crypto';
2
-
3
- const ALGORITHM = 'aes-256-gcm';
4
- const IV_BYTES = 12;
5
- const TAG_BYTES = 16;
6
-
7
- function getKey(): Buffer {
8
- const raw = process.env.POKELO_SECRETS_KEY?.trim();
9
- if (!raw) {
10
- throw new Error('POKELO_SECRETS_KEY is not set');
11
- }
12
- let buf: Buffer;
13
- if (/^[0-9a-fA-F]{64}$/.test(raw)) {
14
- buf = Buffer.from(raw, 'hex');
15
- } else {
16
- buf = Buffer.from(raw, 'base64');
17
- }
18
- if (buf.length !== 32) {
19
- throw new Error(
20
- "POKELO_SECRETS_KEY must be 32 bytes as hex (64 chars) or base64 — generate with: node -e \"console.log(require('crypto').randomBytes(32).toString('hex'))\"",
21
- );
22
- }
23
- return buf;
24
- }
25
-
26
- export function isPokeloSecretsKeyConfigured(): boolean {
27
- try {
28
- getKey();
29
- return true;
30
- } catch {
31
- return false;
32
- }
33
- }
34
-
35
- /**
36
- * Encrypts plaintext using AES-256-GCM.
37
- * Returns a base64 string: iv(12) + ciphertext + tag(16)
38
- */
39
- export function encrypt(plaintext: string): string {
40
- const key = getKey();
41
- const iv = randomBytes(IV_BYTES);
42
- const cipher = createCipheriv(ALGORITHM, key, iv);
43
- const encrypted = Buffer.concat([cipher.update(plaintext, 'utf8'), cipher.final()]);
44
- const tag = cipher.getAuthTag();
45
- return Buffer.concat([iv, encrypted, tag]).toString('base64');
46
- }
47
-
48
- /**
49
- * Decrypts a base64 blob produced by `encrypt`.
50
- */
51
- export function decrypt(ciphertext: string): string {
52
- const key = getKey();
53
- const buf = Buffer.from(ciphertext, 'base64');
54
- const iv = buf.subarray(0, IV_BYTES);
55
- const tag = buf.subarray(buf.length - TAG_BYTES);
56
- const encrypted = buf.subarray(IV_BYTES, buf.length - TAG_BYTES);
57
- const decipher = createDecipheriv(ALGORITHM, key, iv);
58
- decipher.setAuthTag(tag);
59
- return decipher.update(encrypted) + decipher.final('utf8');
60
- }
1
+ export {
2
+ decrypt,
3
+ encrypt,
4
+ isInstanceSecretsKeyConfigured as isPokeloSecretsKeyConfigured,
5
+ } from '../../../packages/plugin-host/src/instance-secrets';
@@ -99,7 +99,7 @@ export class PokeloSettingsService {
99
99
  let encryptedToken: string | undefined = undefined;
100
100
  if (dto.token !== undefined && dto.token.trim()) {
101
101
  if (!isPokeloSecretsKeyConfigured()) {
102
- throw AppException.badRequest('POKELO_SECRETS_KEY is not configured');
102
+ throw AppException.badRequest('KHIRBY_SECRETS_KEY is not configured');
103
103
  }
104
104
  encryptedToken = encrypt(dto.token.trim());
105
105
  }
@@ -1,12 +1,12 @@
1
1
  import { Global, Module } from '@nestjs/common';
2
- import { POKELO_CONTEXT_SERVICE } from '../../../packages/plugin-host/src';
2
+ import { KNOWLEDGE_CONTEXT, POKELO_CONTEXT_SERVICE } from '../../../packages/plugin-host/src';
3
3
  import { PokeloSettingsService } from './pokelo-settings.service';
4
4
  import { PokeloContextService } from './pokelo-context.service';
5
5
  import { PokeloSettingsController } from './pokelo-settings.controller';
6
6
 
7
7
  /**
8
- * @Global so AI Compose (sibling plugin module) can @Optional()-inject
9
- * POKELO_CONTEXT_SERVICE (ADR-0022).
8
+ * @Global so sibling plugins can @Optional()-inject KNOWLEDGE_CONTEXT (ADR-0047).
9
+ * POKELO_CONTEXT_SERVICE stays as a second provide until published consumers bump.
10
10
  */
11
11
  @Global()
12
12
  @Module({
@@ -14,8 +14,9 @@ import { PokeloSettingsController } from './pokelo-settings.controller';
14
14
  providers: [
15
15
  PokeloSettingsService,
16
16
  PokeloContextService,
17
+ { provide: KNOWLEDGE_CONTEXT, useExisting: PokeloContextService },
17
18
  { provide: POKELO_CONTEXT_SERVICE, useExisting: PokeloContextService },
18
19
  ],
19
- exports: [POKELO_CONTEXT_SERVICE],
20
+ exports: [KNOWLEDGE_CONTEXT, POKELO_CONTEXT_SERVICE],
20
21
  })
21
22
  export class PokeloModule {}
@@ -7,10 +7,16 @@ describe('pokelo-crypto', () => {
7
7
  const HEX_KEY = 'c'.repeat(64);
8
8
 
9
9
  beforeEach(() => {
10
+ delete process.env.KHIRBY_SECRETS_KEY;
11
+ delete process.env.MAIL_SECRETS_KEY;
12
+ delete process.env.AI_COMPOSE_SECRETS_KEY;
10
13
  process.env.POKELO_SECRETS_KEY = HEX_KEY;
11
14
  });
12
15
 
13
16
  afterEach(() => {
17
+ delete process.env.KHIRBY_SECRETS_KEY;
18
+ delete process.env.MAIL_SECRETS_KEY;
19
+ delete process.env.AI_COMPOSE_SECRETS_KEY;
14
20
  delete process.env.POKELO_SECRETS_KEY;
15
21
  });
16
22
 
@@ -26,9 +32,15 @@ describe('pokelo-crypto', () => {
26
32
  expect(isPokeloSecretsKeyConfigured()).toBe(false);
27
33
  });
28
34
 
35
+ it('isPokeloSecretsKeyConfigured returns true when KHIRBY_SECRETS_KEY is set', () => {
36
+ delete process.env.POKELO_SECRETS_KEY;
37
+ process.env.KHIRBY_SECRETS_KEY = HEX_KEY;
38
+ expect(isPokeloSecretsKeyConfigured()).toBe(true);
39
+ });
40
+
29
41
  it('throws on missing key at encrypt time', () => {
30
42
  delete process.env.POKELO_SECRETS_KEY;
31
- expect(() => encrypt('anything')).toThrow('POKELO_SECRETS_KEY is not set');
43
+ expect(() => encrypt('anything')).toThrow('KHIRBY_SECRETS_KEY is not set');
32
44
  });
33
45
  });
34
46
 
@@ -50,12 +62,28 @@ describe('parseSseJsonRpc / parseSearchMatches / parseProjectList', () => {
50
62
  expect(matches).toEqual(['Snippet A', 'Snippet B']);
51
63
  });
52
64
 
53
- it('parses project list JSON', () => {
65
+ it('parses project list JSON with current MCP projectId', () => {
66
+ const projects = parseProjectList(
67
+ JSON.stringify({
68
+ items: [{ projectId: 'p1', name: 'Bearly CRM', slug: 'bearly-crm-p1' }, { name: 'x' }],
69
+ }),
70
+ );
71
+ expect(projects).toEqual([{ id: 'p1', name: 'Bearly CRM' }]);
72
+ });
73
+
74
+ it('parses legacy project list JSON with id', () => {
54
75
  const projects = parseProjectList(
55
76
  JSON.stringify({ items: [{ id: 'p1', name: 'Bearly CRM' }, { id: 'x' }] }),
56
77
  );
57
78
  expect(projects).toEqual([{ id: 'p1', name: 'Bearly CRM' }]);
58
79
  });
80
+
81
+ it('prefers projectId over id when both are present', () => {
82
+ const projects = parseProjectList(
83
+ JSON.stringify({ items: [{ id: 'legacy', projectId: 'current', name: 'CRM' }] }),
84
+ );
85
+ expect(projects).toEqual([{ id: 'current', name: 'CRM' }]);
86
+ });
59
87
  });
60
88
 
61
89
  function makeSelectChain(returnValue: unknown[]) {
@@ -197,7 +225,7 @@ describe('PokeloContextService.fetchContext', () => {
197
225
  content: [
198
226
  {
199
227
  type: 'text',
200
- text: JSON.stringify({ items: [{ id: 'proj-uuid', name: 'Bearly CRM' }] }),
228
+ text: JSON.stringify({ items: [{ projectId: 'proj-uuid', name: 'Bearly CRM' }] }),
201
229
  },
202
230
  ],
203
231
  },
@@ -264,8 +292,8 @@ describe('PokeloContextService.fetchContext', () => {
264
292
  type: 'text',
265
293
  text: JSON.stringify({
266
294
  items: [
267
- { id: 'a', name: 'CRM' },
268
- { id: 'b', name: 'Finsly' },
295
+ { projectId: 'a', name: 'CRM' },
296
+ { projectId: 'b', name: 'Finsly' },
269
297
  ],
270
298
  }),
271
299
  },
@@ -319,4 +347,33 @@ describe('PokeloContextService.fetchContext', () => {
319
347
  const ctx = new PokeloContextService(settings);
320
348
  expect(await ctx.fetchContext('pricing')).toBe('');
321
349
  });
350
+
351
+ it('returns empty string on MCP tool isError instead of using the error as a snippet', async () => {
352
+ const settings = new PokeloSettingsService(
353
+ makeMockDb({
354
+ encryptedToken: encrypt('mcp_tok'),
355
+ projectIds: ['proj-uuid'],
356
+ projectId: 'proj-uuid',
357
+ baseUrl: 'https://rag.bearly.pro/v1',
358
+ }) as any,
359
+ makeMockRegistry(true) as any,
360
+ );
361
+
362
+ (global.fetch as jest.Mock).mockResolvedValue({
363
+ ok: true,
364
+ headers: { get: () => 'application/json' },
365
+ text: async () =>
366
+ JSON.stringify({
367
+ jsonrpc: '2.0',
368
+ id: 1,
369
+ result: {
370
+ isError: true,
371
+ content: [{ type: 'text', text: 'Project not found' }],
372
+ },
373
+ }),
374
+ });
375
+
376
+ const ctx = new PokeloContextService(settings);
377
+ expect(await ctx.fetchContext('pricing')).toBe('');
378
+ });
322
379
  });
package/LICENSE DELETED
@@ -1,21 +0,0 @@
1
- MIT License
2
-
3
- Copyright (c) 2026 Khirby Labs
4
-
5
- Permission is hereby granted, free of charge, to any person obtaining a copy
6
- of this software and associated documentation files (the "Software"), to deal
7
- in the Software without restriction, including without limitation the rights
8
- to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
- copies of the Software, and to permit persons to whom the Software is
10
- furnished to do so, subject to the following conditions:
11
-
12
- The above copyright notice and this permission notice shall be included in all
13
- copies or substantial portions of the Software.
14
-
15
- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
- SOFTWARE.