@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 +2 -2
- package/src/pokelo-context.service.ts +18 -5
- package/src/pokelo-crypto.ts +5 -60
- package/src/pokelo-settings.service.ts +1 -1
- package/src/pokelo.module.ts +5 -4
- package/src/pokelo.spec.ts +62 -5
- package/LICENSE +0 -21
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@khirby/plugin-pokelo",
|
|
3
|
-
"version": "1.
|
|
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 {
|
|
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
|
|
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
|
-
.
|
|
231
|
-
|
|
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
|
package/src/pokelo-crypto.ts
CHANGED
|
@@ -1,60 +1,5 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
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('
|
|
102
|
+
throw AppException.badRequest('KHIRBY_SECRETS_KEY is not configured');
|
|
103
103
|
}
|
|
104
104
|
encryptedToken = encrypt(dto.token.trim());
|
|
105
105
|
}
|
package/src/pokelo.module.ts
CHANGED
|
@@ -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
|
|
9
|
-
* POKELO_CONTEXT_SERVICE
|
|
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 {}
|
package/src/pokelo.spec.ts
CHANGED
|
@@ -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('
|
|
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: [{
|
|
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
|
-
{
|
|
268
|
-
{
|
|
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.
|