@hasna/knowledge 0.2.9 → 0.2.11
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/README.md +27 -0
- package/bin/open-knowledge-mcp.js +1614 -34
- package/bin/open-knowledge.js +64 -63
- package/docs/architecture/ai-native-knowledge-base.md +19 -1
- package/package.json +6 -2
- package/src/cli.ts +49 -78
- package/src/mcp.js +19 -27
- package/src/providers.ts +308 -0
- package/src/service.ts +166 -0
- package/src/workspace.ts +41 -0
package/src/cli.ts
CHANGED
|
@@ -5,15 +5,10 @@
|
|
|
5
5
|
* Licensed under the Apache License, Version 2.0
|
|
6
6
|
*/
|
|
7
7
|
import { defaultStorePath, loadStore, saveStore, withLock, makeId, makeShortId, ensureStore, type KnowledgeItem } from './store';
|
|
8
|
-
import {
|
|
9
|
-
import {
|
|
10
|
-
import {
|
|
11
|
-
import {
|
|
12
|
-
import { ingestOpenFilesManifest } from './manifest-ingest';
|
|
13
|
-
import { ingestSourceRef } from './source-ingest';
|
|
14
|
-
import { consumeOpenFilesOutbox } from './outbox-consume';
|
|
15
|
-
import { resolveOpenFilesSource } from './source-resolver';
|
|
16
|
-
import { approvalStatus, assertS3ReadAllowed, assertWebSearchAllowed, createApprovalGate, recordAuditEvent, recordRedactionFindings, redactSecrets, resolveSafetyPolicy } from './safety';
|
|
8
|
+
import { openKnowledgeDb } from './knowledge-db';
|
|
9
|
+
import { createKnowledgeService } from './service';
|
|
10
|
+
import { assertProviderCredentials, parseModelRef, resolveModelRef, type AiProviderId } from './providers';
|
|
11
|
+
import { approvalStatus, assertS3ReadAllowed, assertWebSearchAllowed, createApprovalGate, recordAuditEvent, recordRedactionFindings, redactSecrets } from './safety';
|
|
17
12
|
import pkg from '../package.json' with { type: 'json' };
|
|
18
13
|
|
|
19
14
|
type LogLevel = 'debug' | 'info' | 'warn' | 'error';
|
|
@@ -65,7 +60,7 @@ interface ParseResult {
|
|
|
65
60
|
flags: Flags;
|
|
66
61
|
}
|
|
67
62
|
|
|
68
|
-
const COMMANDS = ['add', 'list', 'get', 'delete', 'update', 'archive', 'restore', 'upsert', 'untag', 'export', 'prune', 'dedupe', 'stats', 'paths', 'db', 'wiki', 'source', 'ingest', 'reindex', 'safety', 'help'];
|
|
63
|
+
const COMMANDS = ['add', 'list', 'get', 'delete', 'update', 'archive', 'restore', 'upsert', 'untag', 'export', 'prune', 'dedupe', 'stats', 'paths', 'db', 'wiki', 'source', 'ingest', 'reindex', 'providers', 'safety', 'help'];
|
|
69
64
|
const COMMAND_ALIASES: Record<string, string> = {
|
|
70
65
|
ls: 'list',
|
|
71
66
|
rm: 'delete',
|
|
@@ -173,6 +168,7 @@ Commands:
|
|
|
173
168
|
ingest manifest <file|s3://> Ingest an open-files manifest into knowledge.db
|
|
174
169
|
ingest source <source-ref> Ingest a read-only source ref into knowledge.db
|
|
175
170
|
reindex outbox <file|s3://> Consume open-files change events and invalidate chunks
|
|
171
|
+
providers status|models|check Inspect AI SDK provider config and credentials
|
|
176
172
|
safety status|check|approve|audit|redact
|
|
177
173
|
help [command] Show help
|
|
178
174
|
|
|
@@ -239,6 +235,7 @@ function printCommandHelp(command: string): void {
|
|
|
239
235
|
if (command === 'source') { console.log('Usage: open-knowledge source resolve <source-ref> [--purpose knowledge_answer|knowledge_index] [--limit <n>] [--scope local|global|project] [--json]'); return; }
|
|
240
236
|
if (command === 'ingest') { console.log('Usage: open-knowledge ingest manifest <file|s3://bucket/key> | source <source-ref> [--purpose knowledge_index] [--scope local|global|project] [--json]'); return; }
|
|
241
237
|
if (command === 'reindex') { console.log('Usage: open-knowledge reindex outbox <file|s3://bucket/key> [--scope local|global|project] [--json]'); return; }
|
|
238
|
+
if (command === 'providers') { console.log('Usage: open-knowledge providers status|models|check [provider|model-alias] [--scope local|global|project] [--json]'); return; }
|
|
242
239
|
if (command === 'safety') { console.log('Usage: open-knowledge safety status|check|approve|audit|redact [args] [--scope local|global|project] [--json]'); return; }
|
|
243
240
|
printGlobalHelp();
|
|
244
241
|
}
|
|
@@ -284,11 +281,11 @@ async function run(argv: string[]): Promise<void> {
|
|
|
284
281
|
if (flags.completions) {
|
|
285
282
|
const shell = flags.completions;
|
|
286
283
|
if (shell === 'bash') {
|
|
287
|
-
console.log(`_open_knowledge() { local cur; cur="${"$"}{COMP_WORDS[COMP_CWORD]}"; COMPREPLY=($(compgen -W "add list get update archive restore upsert untag delete export prune dedupe stats paths db wiki source ingest reindex safety help ls rm edit unarchive --json --yes --help --version --desc --page --limit --search --sort --id --store --title --content --url --tag --format --completions --purpose --no-color --scope --archived --include-archived" -- "$cur")); }; complete -F _open_knowledge open-knowledge`);
|
|
284
|
+
console.log(`_open_knowledge() { local cur; cur="${"$"}{COMP_WORDS[COMP_CWORD]}"; COMPREPLY=($(compgen -W "add list get update archive restore upsert untag delete export prune dedupe stats paths db wiki source ingest reindex providers safety help ls rm edit unarchive --json --yes --help --version --desc --page --limit --search --sort --id --store --title --content --url --tag --format --completions --purpose --no-color --scope --archived --include-archived" -- "$cur")); }; complete -F _open_knowledge open-knowledge`);
|
|
288
285
|
} else if (shell === 'zsh') {
|
|
289
|
-
console.log(`#compdef open-knowledge\n_open_knowledge() { _arguments -C "1: :(add list get update archive restore upsert untag delete export prune dedupe stats paths db wiki source ingest reindex safety help ls rm edit unarchive)" "(--json)--json" "(--yes)-y" "(--help)--help" "(--version)--version" "(--desc)--desc" "(--archived)--archived" "(--include-archived)--include-archived" "(-p --page)"{-p,--page}"[page number]:number:" "(-l --limit)"{-l,--limit}"[items per page]:number:" "(-s --search)"{-s,--search}"[search text]:text:" "(--sort)--sort"\{created,title\}:" "(--id)--id[item id]:id:" "(--store)--store[store path]:path:" "(--title)--title[new title]:" "(--content)--content[new content]:" "(--url)--url[source url]:" "(-t --tag)"{-t,--tag}"[tag]:tag:" "(--format)--format[json|jsonl]:" "(--completions)--completions[output completions]:shell:(bash zsh fish):" "(--purpose)--purpose[purpose]:" "(--no-color)--no-color[disable color]" "(--scope)--scope"\{local,global,project\}:" }; _open_knowledge`);
|
|
286
|
+
console.log(`#compdef open-knowledge\n_open_knowledge() { _arguments -C "1: :(add list get update archive restore upsert untag delete export prune dedupe stats paths db wiki source ingest reindex providers safety help ls rm edit unarchive)" "(--json)--json" "(--yes)-y" "(--help)--help" "(--version)--version" "(--desc)--desc" "(--archived)--archived" "(--include-archived)--include-archived" "(-p --page)"{-p,--page}"[page number]:number:" "(-l --limit)"{-l,--limit}"[items per page]:number:" "(-s --search)"{-s,--search}"[search text]:text:" "(--sort)--sort"\{created,title\}:" "(--id)--id[item id]:id:" "(--store)--store[store path]:path:" "(--title)--title[new title]:" "(--content)--content[new content]:" "(--url)--url[source url]:" "(-t --tag)"{-t,--tag}"[tag]:tag:" "(--format)--format[json|jsonl]:" "(--completions)--completions[output completions]:shell:(bash zsh fish):" "(--purpose)--purpose[purpose]:" "(--no-color)--no-color[disable color]" "(--scope)--scope"\{local,global,project\}:" }; _open_knowledge`);
|
|
290
287
|
} else if (shell === 'fish') {
|
|
291
|
-
console.log(`complete -c open-knowledge -f; complete -c open-knowledge -a "add list get update archive restore upsert untag delete export prune dedupe stats paths db wiki source ingest reindex safety help ls rm edit unarchive"; complete -c open-knowledge -l json; complete -c open-knowledge -l yes -s y; complete -c open-knowledge -l help -s h; complete -c open-knowledge -l version -s v; complete -c open-knowledge -l desc; complete -c open-knowledge -l archived; complete -c open-knowledge -l include-archived; complete -c open-knowledge -s p -l page; complete -c open-knowledge -s l -l limit; complete -c open-knowledge -s s -l search; complete -c open-knowledge -l sort; complete -c open-knowledge -l id; complete -c open-knowledge -l store; complete -c open-knowledge -l title; complete -c open-knowledge -l content; complete -c open-knowledge -l url; complete -c open-knowledge -s t -l tag; complete -c open-knowledge -l format; complete -c open-knowledge -l completions; complete -c open-knowledge -l purpose; complete -c open-knowledge -l no-color; complete -c open-knowledge -l scope -a "local global project"`);
|
|
288
|
+
console.log(`complete -c open-knowledge -f; complete -c open-knowledge -a "add list get update archive restore upsert untag delete export prune dedupe stats paths db wiki source ingest reindex providers safety help ls rm edit unarchive"; complete -c open-knowledge -l json; complete -c open-knowledge -l yes -s y; complete -c open-knowledge -l help -s h; complete -c open-knowledge -l version -s v; complete -c open-knowledge -l desc; complete -c open-knowledge -l archived; complete -c open-knowledge -l include-archived; complete -c open-knowledge -s p -l page; complete -c open-knowledge -s l -l limit; complete -c open-knowledge -s s -l search; complete -c open-knowledge -l sort; complete -c open-knowledge -l id; complete -c open-knowledge -l store; complete -c open-knowledge -l title; complete -c open-knowledge -l content; complete -c open-knowledge -l url; complete -c open-knowledge -s t -l tag; complete -c open-knowledge -l format; complete -c open-knowledge -l completions; complete -c open-knowledge -l purpose; complete -c open-knowledge -l no-color; complete -c open-knowledge -l scope -a "local global project"`);
|
|
292
289
|
} else {
|
|
293
290
|
throw new Error("Invalid --completions value. Use 'bash', 'zsh', or 'fish'.");
|
|
294
291
|
}
|
|
@@ -299,71 +296,49 @@ async function run(argv: string[]): Promise<void> {
|
|
|
299
296
|
|
|
300
297
|
if (!command || flags.help || command === 'help') { printCommandHelp(positional[1]); return; }
|
|
301
298
|
|
|
302
|
-
const
|
|
299
|
+
const service = createKnowledgeService({ scope: flags.scope });
|
|
303
300
|
let storePath = flags.store;
|
|
304
301
|
if (!storePath) {
|
|
305
302
|
if (flags.scope === 'project' || flags.scope === 'local') {
|
|
306
|
-
storePath =
|
|
303
|
+
storePath = service.jsonStorePath();
|
|
307
304
|
} else {
|
|
308
305
|
storePath = defaultStorePath();
|
|
309
306
|
}
|
|
310
307
|
}
|
|
311
308
|
|
|
312
309
|
if (command === 'paths') {
|
|
313
|
-
|
|
314
|
-
output({
|
|
315
|
-
ok: true,
|
|
316
|
-
scope: flags.scope ?? 'global',
|
|
317
|
-
home: resolvedWorkspace.home,
|
|
318
|
-
config_path: resolvedWorkspace.configPath,
|
|
319
|
-
json_store_path: resolvedWorkspace.jsonStorePath,
|
|
320
|
-
knowledge_db_path: resolvedWorkspace.knowledgeDbPath,
|
|
321
|
-
artifacts_dir: resolvedWorkspace.artifactsDir,
|
|
322
|
-
indexes_dir: resolvedWorkspace.indexesDir,
|
|
323
|
-
logs_dir: resolvedWorkspace.logsDir,
|
|
324
|
-
runs_dir: resolvedWorkspace.runsDir,
|
|
325
|
-
schemas_dir: resolvedWorkspace.schemasDir,
|
|
326
|
-
wiki_dir: resolvedWorkspace.wikiDir,
|
|
327
|
-
config: readKnowledgeConfig(resolvedWorkspace.configPath),
|
|
328
|
-
message: resolvedWorkspace.home,
|
|
329
|
-
}, flags.json);
|
|
310
|
+
output(service.paths(), flags.json);
|
|
330
311
|
return;
|
|
331
312
|
}
|
|
332
313
|
|
|
333
314
|
if (command === 'db') {
|
|
334
315
|
const action = positional[1] ?? 'init';
|
|
335
|
-
const resolvedWorkspace = ensureKnowledgeWorkspace(workspace.home);
|
|
336
316
|
if (action !== 'init' && action !== 'stats') {
|
|
337
317
|
throw new Error("Invalid db action. Use 'init' or 'stats'.");
|
|
338
318
|
}
|
|
339
319
|
if (action === 'init') {
|
|
340
|
-
const result =
|
|
320
|
+
const result = service.initDb();
|
|
341
321
|
output({ ok: true, ...result, message: `Initialized ${result.path}` }, flags.json);
|
|
342
322
|
return;
|
|
343
323
|
}
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
output({ ok: true, path: resolvedWorkspace.knowledgeDbPath, ...stats, message: `knowledge.db schema v${stats.schema_version}` }, flags.json);
|
|
324
|
+
const stats = service.dbStats();
|
|
325
|
+
output({ ok: true, path: service.workspace.knowledgeDbPath, ...stats, message: `knowledge.db schema v${stats.schema_version}` }, flags.json);
|
|
347
326
|
return;
|
|
348
327
|
}
|
|
349
328
|
|
|
350
329
|
if (command === 'wiki') {
|
|
351
330
|
const action = positional[1] ?? 'init';
|
|
352
331
|
if (action !== 'init') throw new Error("Invalid wiki action. Use 'init'.");
|
|
353
|
-
const
|
|
354
|
-
|
|
355
|
-
const artifactStore = createArtifactStore(config, resolvedWorkspace);
|
|
356
|
-
const result = await initializeWikiLayout(artifactStore);
|
|
357
|
-
output({ ok: true, ...result, message: `Initialized wiki layout in ${resolvedWorkspace.home}` }, flags.json);
|
|
332
|
+
const result = await service.initWiki();
|
|
333
|
+
output({ ok: true, ...result, message: `Initialized wiki layout in ${service.workspace.home}` }, flags.json);
|
|
358
334
|
return;
|
|
359
335
|
}
|
|
360
336
|
|
|
361
337
|
if (command === 'safety') {
|
|
362
338
|
const action = positional[1] ?? 'status';
|
|
363
|
-
const resolvedWorkspace =
|
|
364
|
-
const
|
|
365
|
-
|
|
366
|
-
migrateKnowledgeDb(resolvedWorkspace.knowledgeDbPath);
|
|
339
|
+
const resolvedWorkspace = service.ensureWorkspace();
|
|
340
|
+
const policy = service.safetyPolicy();
|
|
341
|
+
service.initDb();
|
|
367
342
|
const db = openKnowledgeDb(resolvedWorkspace.knowledgeDbPath);
|
|
368
343
|
try {
|
|
369
344
|
if (action === 'status') {
|
|
@@ -489,15 +464,9 @@ async function run(argv: string[]): Promise<void> {
|
|
|
489
464
|
if (action !== 'resolve') throw new Error("Invalid source action. Use 'resolve'.");
|
|
490
465
|
const sourceRef = positional[2];
|
|
491
466
|
if (!sourceRef) throw new Error('Usage: open-knowledge source resolve <source-ref>');
|
|
492
|
-
const
|
|
493
|
-
const config = readKnowledgeConfig(resolvedWorkspace.configPath);
|
|
494
|
-
const safetyPolicy = resolveSafetyPolicy(config, resolvedWorkspace);
|
|
495
|
-
const result = await resolveOpenFilesSource({
|
|
496
|
-
dbPath: resolvedWorkspace.knowledgeDbPath,
|
|
497
|
-
sourceRef,
|
|
467
|
+
const result = await service.resolveSource(sourceRef, {
|
|
498
468
|
purpose: flags.purpose,
|
|
499
469
|
limit: flags.limit,
|
|
500
|
-
safetyPolicy,
|
|
501
470
|
});
|
|
502
471
|
output({
|
|
503
472
|
ok: true,
|
|
@@ -511,31 +480,17 @@ async function run(argv: string[]): Promise<void> {
|
|
|
511
480
|
|
|
512
481
|
if (command === 'ingest') {
|
|
513
482
|
const action = positional[1] ?? '';
|
|
514
|
-
const resolvedWorkspace = ensureKnowledgeWorkspace(workspace.home);
|
|
515
|
-
const config = readKnowledgeConfig(resolvedWorkspace.configPath);
|
|
516
|
-
const safetyPolicy = resolveSafetyPolicy(config, resolvedWorkspace);
|
|
517
483
|
if (action === 'manifest') {
|
|
518
484
|
const input = positional[2];
|
|
519
485
|
if (!input) throw new Error('Usage: open-knowledge ingest manifest <file|s3://bucket/key>');
|
|
520
|
-
const result = await
|
|
521
|
-
dbPath: resolvedWorkspace.knowledgeDbPath,
|
|
522
|
-
input,
|
|
523
|
-
config,
|
|
524
|
-
safetyPolicy,
|
|
525
|
-
});
|
|
486
|
+
const result = await service.ingestManifest(input);
|
|
526
487
|
output({ ok: true, ...result, message: `Ingested ${result.items_seen} manifest item(s)` }, flags.json);
|
|
527
488
|
return;
|
|
528
489
|
}
|
|
529
490
|
if (action === 'source') {
|
|
530
491
|
const sourceRef = positional[2];
|
|
531
492
|
if (!sourceRef) throw new Error('Usage: open-knowledge ingest source <source-ref>');
|
|
532
|
-
const result = await
|
|
533
|
-
dbPath: resolvedWorkspace.knowledgeDbPath,
|
|
534
|
-
sourceRef,
|
|
535
|
-
purpose: flags.purpose,
|
|
536
|
-
config,
|
|
537
|
-
safetyPolicy,
|
|
538
|
-
});
|
|
493
|
+
const result = await service.ingestSource(sourceRef, flags.purpose);
|
|
539
494
|
output({ ok: true, ...result, message: `Ingested source ${result.source_ref} (${result.chunks_inserted} chunks)` }, flags.json);
|
|
540
495
|
return;
|
|
541
496
|
}
|
|
@@ -547,19 +502,35 @@ async function run(argv: string[]): Promise<void> {
|
|
|
547
502
|
if (action !== 'outbox') throw new Error("Invalid reindex action. Use 'outbox'.");
|
|
548
503
|
const input = positional[2];
|
|
549
504
|
if (!input) throw new Error('Usage: open-knowledge reindex outbox <file|s3://bucket/key>');
|
|
550
|
-
const
|
|
551
|
-
const config = readKnowledgeConfig(resolvedWorkspace.configPath);
|
|
552
|
-
const safetyPolicy = resolveSafetyPolicy(config, resolvedWorkspace);
|
|
553
|
-
const result = await consumeOpenFilesOutbox({
|
|
554
|
-
dbPath: resolvedWorkspace.knowledgeDbPath,
|
|
555
|
-
input,
|
|
556
|
-
config,
|
|
557
|
-
safetyPolicy,
|
|
558
|
-
});
|
|
505
|
+
const result = await service.consumeOutbox(input);
|
|
559
506
|
output({ ok: true, ...result, message: `Consumed ${result.events_seen} outbox event(s)` }, flags.json);
|
|
560
507
|
return;
|
|
561
508
|
}
|
|
562
509
|
|
|
510
|
+
if (command === 'providers') {
|
|
511
|
+
const action = positional[1] ?? 'status';
|
|
512
|
+
if (action === 'status') {
|
|
513
|
+
const status = service.providerStatus();
|
|
514
|
+
const configured = status.providers.filter((entry) => entry.configured).length;
|
|
515
|
+
output({ ok: true, ...status, message: `${configured}/${status.providers.length} provider credential(s) configured` }, flags.json);
|
|
516
|
+
return;
|
|
517
|
+
}
|
|
518
|
+
if (action === 'models') {
|
|
519
|
+
const models = service.modelRegistry();
|
|
520
|
+
output({ ok: true, models, message: `${models.length} model alias(es)` }, flags.json);
|
|
521
|
+
return;
|
|
522
|
+
}
|
|
523
|
+
if (action === 'check') {
|
|
524
|
+
const target = positional[2] ?? 'default';
|
|
525
|
+
const modelRef = resolveModelRef(target, service.config());
|
|
526
|
+
const parsed = parseModelRef(modelRef);
|
|
527
|
+
const credential = assertProviderCredentials(parsed.provider as AiProviderId, service.config());
|
|
528
|
+
output({ ok: true, target, model_ref: modelRef, provider: parsed.provider, model: parsed.model, credential, message: `${parsed.provider} credentials configured` }, flags.json);
|
|
529
|
+
return;
|
|
530
|
+
}
|
|
531
|
+
throw new Error("Invalid providers action. Use 'status', 'models', or 'check'.");
|
|
532
|
+
}
|
|
533
|
+
|
|
563
534
|
ensureStore(storePath);
|
|
564
535
|
|
|
565
536
|
if (command === 'add') {
|
package/src/mcp.js
CHANGED
|
@@ -5,10 +5,8 @@ import { z } from 'zod';
|
|
|
5
5
|
import { existsSync, readFileSync, writeFileSync } from 'node:fs';
|
|
6
6
|
import pkg from '../package.json' with { type: 'json' };
|
|
7
7
|
import { defaultStorePath, loadStore, saveStore, makeId, withLock } from './store.ts';
|
|
8
|
-
import { ensureKnowledgeWorkspace, readKnowledgeConfig, resolveScopedWorkspace } from './workspace.ts';
|
|
9
8
|
import { parseSourceRef } from './source-ref.ts';
|
|
10
|
-
import {
|
|
11
|
-
import { resolveSafetyPolicy } from './safety.ts';
|
|
9
|
+
import { createKnowledgeService } from './service.ts';
|
|
12
10
|
|
|
13
11
|
const storePathField = z.string().optional().describe('Path to the JSON store file');
|
|
14
12
|
const scopeField = z.enum(['local', 'global', 'project']).optional().describe('Workspace scope');
|
|
@@ -28,7 +26,7 @@ function shortIdFor(id) {
|
|
|
28
26
|
function resolveStorePath(storePath, scope) {
|
|
29
27
|
if (storePath) return storePath;
|
|
30
28
|
if (scope === 'project' || scope === 'local') {
|
|
31
|
-
return
|
|
29
|
+
return createKnowledgeService({ scope }).jsonStorePath();
|
|
32
30
|
}
|
|
33
31
|
return defaultStorePath();
|
|
34
32
|
}
|
|
@@ -76,22 +74,7 @@ export function buildServer() {
|
|
|
76
74
|
registerTool(server, 'ok_paths', 'Knowledge workspace paths', 'Show resolved workspace and store paths', {
|
|
77
75
|
scope: scopeField,
|
|
78
76
|
}, async ({ scope }) => {
|
|
79
|
-
|
|
80
|
-
return jsonText({
|
|
81
|
-
ok: true,
|
|
82
|
-
scope: scope ?? 'global',
|
|
83
|
-
home: workspace.home,
|
|
84
|
-
config_path: workspace.configPath,
|
|
85
|
-
json_store_path: workspace.jsonStorePath,
|
|
86
|
-
knowledge_db_path: workspace.knowledgeDbPath,
|
|
87
|
-
artifacts_dir: workspace.artifactsDir,
|
|
88
|
-
indexes_dir: workspace.indexesDir,
|
|
89
|
-
logs_dir: workspace.logsDir,
|
|
90
|
-
runs_dir: workspace.runsDir,
|
|
91
|
-
schemas_dir: workspace.schemasDir,
|
|
92
|
-
wiki_dir: workspace.wikiDir,
|
|
93
|
-
config: readKnowledgeConfig(workspace.configPath),
|
|
94
|
-
});
|
|
77
|
+
return jsonText(createKnowledgeService({ scope }).paths());
|
|
95
78
|
});
|
|
96
79
|
|
|
97
80
|
registerTool(server, 'ok_parse_source_ref', 'Parse source reference', 'Parse and validate an open-files, S3, file, or web source ref', {
|
|
@@ -110,16 +93,11 @@ export function buildServer() {
|
|
|
110
93
|
limit: z.number().optional().describe('Maximum chunks to return, default 10'),
|
|
111
94
|
scope: scopeField,
|
|
112
95
|
}, async ({ source_ref, purpose, limit, scope }) => {
|
|
113
|
-
const
|
|
114
|
-
const config = readKnowledgeConfig(workspace.configPath);
|
|
115
|
-
const safetyPolicy = resolveSafetyPolicy(config, workspace);
|
|
96
|
+
const service = createKnowledgeService({ scope });
|
|
116
97
|
try {
|
|
117
|
-
const result = await
|
|
118
|
-
dbPath: workspace.knowledgeDbPath,
|
|
119
|
-
sourceRef: source_ref,
|
|
98
|
+
const result = await service.resolveSource(source_ref, {
|
|
120
99
|
purpose,
|
|
121
100
|
limit,
|
|
122
|
-
safetyPolicy,
|
|
123
101
|
});
|
|
124
102
|
return jsonText({ ok: true, ...result });
|
|
125
103
|
} catch (error) {
|
|
@@ -127,6 +105,20 @@ export function buildServer() {
|
|
|
127
105
|
}
|
|
128
106
|
});
|
|
129
107
|
|
|
108
|
+
registerTool(server, 'ok_provider_status', 'AI provider status', 'Inspect configured AI SDK providers, model aliases, and BYOK credential availability', {
|
|
109
|
+
scope: scopeField,
|
|
110
|
+
}, async ({ scope }) => {
|
|
111
|
+
const service = createKnowledgeService({ scope });
|
|
112
|
+
return jsonText({ ok: true, ...service.providerStatus() });
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
registerTool(server, 'ok_provider_models', 'AI provider models', 'List AI SDK model aliases and capability metadata', {
|
|
116
|
+
scope: scopeField,
|
|
117
|
+
}, async ({ scope }) => {
|
|
118
|
+
const service = createKnowledgeService({ scope });
|
|
119
|
+
return jsonText({ ok: true, models: service.modelRegistry() });
|
|
120
|
+
});
|
|
121
|
+
|
|
130
122
|
registerTool(server, 'ok_add', 'Add a knowledge item', 'Add a new item to the knowledge store', {
|
|
131
123
|
title: z.string().describe('Item title'),
|
|
132
124
|
content: z.string().describe('Item content/body'),
|
package/src/providers.ts
ADDED
|
@@ -0,0 +1,308 @@
|
|
|
1
|
+
import { randomUUID } from 'node:crypto';
|
|
2
|
+
import type { Database } from 'bun:sqlite';
|
|
3
|
+
import type { KnowledgeConfig } from './workspace';
|
|
4
|
+
|
|
5
|
+
export type AiProviderId = 'openai' | 'anthropic' | 'deepseek';
|
|
6
|
+
|
|
7
|
+
export interface AiProviderSettings {
|
|
8
|
+
api_key_env: string;
|
|
9
|
+
base_url?: string;
|
|
10
|
+
default_model: string;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export interface AiProvidersConfig {
|
|
14
|
+
default_model?: string;
|
|
15
|
+
aliases?: Record<string, string>;
|
|
16
|
+
openai?: Partial<AiProviderSettings>;
|
|
17
|
+
anthropic?: Partial<AiProviderSettings>;
|
|
18
|
+
deepseek?: Partial<AiProviderSettings>;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export interface ModelCapabilities {
|
|
22
|
+
text_generation: boolean;
|
|
23
|
+
structured_output: boolean;
|
|
24
|
+
tool_usage: boolean;
|
|
25
|
+
tool_streaming: boolean;
|
|
26
|
+
image_input: boolean;
|
|
27
|
+
native_web_search: boolean;
|
|
28
|
+
reasoning: boolean;
|
|
29
|
+
embeddings: boolean;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export interface ModelRegistryEntry {
|
|
33
|
+
alias: string;
|
|
34
|
+
model_ref: string;
|
|
35
|
+
provider: AiProviderId;
|
|
36
|
+
model: string;
|
|
37
|
+
default: boolean;
|
|
38
|
+
capabilities: ModelCapabilities;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export interface ProviderCredentialStatus {
|
|
42
|
+
provider: AiProviderId;
|
|
43
|
+
api_key_env: string;
|
|
44
|
+
configured: boolean;
|
|
45
|
+
source: 'env' | 'missing';
|
|
46
|
+
base_url: string | null;
|
|
47
|
+
default_model: string;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export interface ProviderStatusResult {
|
|
51
|
+
default_model: string;
|
|
52
|
+
providers: ProviderCredentialStatus[];
|
|
53
|
+
models: ModelRegistryEntry[];
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export interface NormalizedProviderUsage {
|
|
57
|
+
provider: string;
|
|
58
|
+
model: string;
|
|
59
|
+
input_tokens: number;
|
|
60
|
+
output_tokens: number;
|
|
61
|
+
cost_usd: number;
|
|
62
|
+
metadata: Record<string, unknown>;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
type ProviderFactory = (settings: { apiKey: string; baseURL?: string }) => unknown;
|
|
66
|
+
|
|
67
|
+
export interface AiProviderRuntimeOptions {
|
|
68
|
+
config?: KnowledgeConfig;
|
|
69
|
+
env?: Record<string, string | undefined>;
|
|
70
|
+
factories?: Partial<Record<AiProviderId, ProviderFactory>>;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const DEFAULT_PROVIDER_SETTINGS: Record<AiProviderId, AiProviderSettings> = {
|
|
74
|
+
openai: {
|
|
75
|
+
api_key_env: 'OPENAI_API_KEY',
|
|
76
|
+
default_model: 'gpt-5.2',
|
|
77
|
+
},
|
|
78
|
+
anthropic: {
|
|
79
|
+
api_key_env: 'ANTHROPIC_API_KEY',
|
|
80
|
+
default_model: 'claude-sonnet-4-6',
|
|
81
|
+
},
|
|
82
|
+
deepseek: {
|
|
83
|
+
api_key_env: 'DEEPSEEK_API_KEY',
|
|
84
|
+
default_model: 'deepseek-chat',
|
|
85
|
+
},
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
const PROVIDER_CAPABILITIES: Record<AiProviderId, ModelCapabilities> = {
|
|
89
|
+
openai: {
|
|
90
|
+
text_generation: true,
|
|
91
|
+
structured_output: true,
|
|
92
|
+
tool_usage: true,
|
|
93
|
+
tool_streaming: true,
|
|
94
|
+
image_input: true,
|
|
95
|
+
native_web_search: true,
|
|
96
|
+
reasoning: true,
|
|
97
|
+
embeddings: true,
|
|
98
|
+
},
|
|
99
|
+
anthropic: {
|
|
100
|
+
text_generation: true,
|
|
101
|
+
structured_output: true,
|
|
102
|
+
tool_usage: true,
|
|
103
|
+
tool_streaming: true,
|
|
104
|
+
image_input: true,
|
|
105
|
+
native_web_search: false,
|
|
106
|
+
reasoning: true,
|
|
107
|
+
embeddings: false,
|
|
108
|
+
},
|
|
109
|
+
deepseek: {
|
|
110
|
+
text_generation: true,
|
|
111
|
+
structured_output: true,
|
|
112
|
+
tool_usage: true,
|
|
113
|
+
tool_streaming: true,
|
|
114
|
+
image_input: false,
|
|
115
|
+
native_web_search: false,
|
|
116
|
+
reasoning: true,
|
|
117
|
+
embeddings: false,
|
|
118
|
+
},
|
|
119
|
+
};
|
|
120
|
+
|
|
121
|
+
const BUILTIN_ALIASES: Record<string, string> = {
|
|
122
|
+
default: 'openai:gpt-5.2',
|
|
123
|
+
fast: 'openai:gpt-5-mini',
|
|
124
|
+
reasoning: 'anthropic:claude-opus-4-6',
|
|
125
|
+
sonnet: 'anthropic:claude-sonnet-4-6',
|
|
126
|
+
deepseek: 'deepseek:deepseek-chat',
|
|
127
|
+
'deepseek-reasoning': 'deepseek:deepseek-reasoner',
|
|
128
|
+
};
|
|
129
|
+
|
|
130
|
+
function providerConfig(config?: KnowledgeConfig): AiProvidersConfig {
|
|
131
|
+
return (config as KnowledgeConfig & { providers?: AiProvidersConfig }).providers ?? {};
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
export function providerSettings(config: KnowledgeConfig | undefined, provider: AiProviderId): AiProviderSettings {
|
|
135
|
+
const configured = providerConfig(config)[provider] ?? {};
|
|
136
|
+
return {
|
|
137
|
+
...DEFAULT_PROVIDER_SETTINGS[provider],
|
|
138
|
+
...configured,
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
export function modelAliases(config?: KnowledgeConfig): Record<string, string> {
|
|
143
|
+
const configured = providerConfig(config);
|
|
144
|
+
return {
|
|
145
|
+
...BUILTIN_ALIASES,
|
|
146
|
+
...(configured.default_model ? { default: configured.default_model } : {}),
|
|
147
|
+
...(configured.aliases ?? {}),
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
export function parseModelRef(modelRef: string): { provider: AiProviderId; model: string } {
|
|
152
|
+
const [provider, ...rest] = modelRef.split(':');
|
|
153
|
+
const model = rest.join(':');
|
|
154
|
+
if (provider !== 'openai' && provider !== 'anthropic' && provider !== 'deepseek') {
|
|
155
|
+
throw new Error(`Unsupported AI provider: ${provider}`);
|
|
156
|
+
}
|
|
157
|
+
if (!model) throw new Error(`Invalid model ref: ${modelRef}. Expected provider:model.`);
|
|
158
|
+
return { provider, model };
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
export function resolveModelRef(aliasOrRef: string, config?: KnowledgeConfig): string {
|
|
162
|
+
const aliases = modelAliases(config);
|
|
163
|
+
return aliases[aliasOrRef] ?? aliasOrRef;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
export function listModelRegistry(config?: KnowledgeConfig): ModelRegistryEntry[] {
|
|
167
|
+
const aliases = modelAliases(config);
|
|
168
|
+
return Object.entries(aliases).map(([alias, modelRef]) => {
|
|
169
|
+
const parsed = parseModelRef(modelRef);
|
|
170
|
+
return {
|
|
171
|
+
alias,
|
|
172
|
+
model_ref: modelRef,
|
|
173
|
+
provider: parsed.provider,
|
|
174
|
+
model: parsed.model,
|
|
175
|
+
default: alias === 'default',
|
|
176
|
+
capabilities: PROVIDER_CAPABILITIES[parsed.provider],
|
|
177
|
+
};
|
|
178
|
+
});
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
export function providerCredentialStatus(config: KnowledgeConfig | undefined, env: Record<string, string | undefined> = process.env): ProviderCredentialStatus[] {
|
|
182
|
+
return (Object.keys(DEFAULT_PROVIDER_SETTINGS) as AiProviderId[]).map((provider) => {
|
|
183
|
+
const settings = providerSettings(config, provider);
|
|
184
|
+
const configured = Boolean(env[settings.api_key_env]);
|
|
185
|
+
return {
|
|
186
|
+
provider,
|
|
187
|
+
api_key_env: settings.api_key_env,
|
|
188
|
+
configured,
|
|
189
|
+
source: configured ? 'env' : 'missing',
|
|
190
|
+
base_url: settings.base_url ?? null,
|
|
191
|
+
default_model: settings.default_model,
|
|
192
|
+
};
|
|
193
|
+
});
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
export function providerStatus(config?: KnowledgeConfig, env: Record<string, string | undefined> = process.env): ProviderStatusResult {
|
|
197
|
+
return {
|
|
198
|
+
default_model: resolveModelRef('default', config),
|
|
199
|
+
providers: providerCredentialStatus(config, env),
|
|
200
|
+
models: listModelRegistry(config),
|
|
201
|
+
};
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
export function assertProviderCredentials(provider: AiProviderId, config?: KnowledgeConfig, env: Record<string, string | undefined> = process.env): ProviderCredentialStatus {
|
|
205
|
+
const status = providerCredentialStatus(config, env).find((entry) => entry.provider === provider);
|
|
206
|
+
if (!status) throw new Error(`Unsupported AI provider: ${provider}`);
|
|
207
|
+
if (!status.configured) throw new Error(`Missing ${status.api_key_env} for ${provider}. Set the env var to use this provider.`);
|
|
208
|
+
return status;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
async function defaultFactory(provider: AiProviderId): Promise<ProviderFactory> {
|
|
212
|
+
if (provider === 'openai') {
|
|
213
|
+
const { createOpenAI } = await import('@ai-sdk/openai');
|
|
214
|
+
return createOpenAI as ProviderFactory;
|
|
215
|
+
}
|
|
216
|
+
if (provider === 'anthropic') {
|
|
217
|
+
const { createAnthropic } = await import('@ai-sdk/anthropic');
|
|
218
|
+
return createAnthropic as ProviderFactory;
|
|
219
|
+
}
|
|
220
|
+
const { createDeepSeek } = await import('@ai-sdk/deepseek');
|
|
221
|
+
return createDeepSeek as ProviderFactory;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
export async function createAiSdkProviderRegistry(options: AiProviderRuntimeOptions = {}) {
|
|
225
|
+
const { createProviderRegistry } = await import('ai');
|
|
226
|
+
const env = options.env ?? process.env;
|
|
227
|
+
const providers: Record<string, unknown> = {};
|
|
228
|
+
for (const provider of Object.keys(DEFAULT_PROVIDER_SETTINGS) as AiProviderId[]) {
|
|
229
|
+
const settings = providerSettings(options.config, provider);
|
|
230
|
+
const apiKey = env[settings.api_key_env];
|
|
231
|
+
if (!apiKey) continue;
|
|
232
|
+
const factory = options.factories?.[provider] ?? await defaultFactory(provider);
|
|
233
|
+
providers[provider] = factory({ apiKey, baseURL: settings.base_url });
|
|
234
|
+
}
|
|
235
|
+
return createProviderRegistry(providers as never);
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
export async function languageModelFor(aliasOrRef: string, options: AiProviderRuntimeOptions = {}) {
|
|
239
|
+
const modelRef = resolveModelRef(aliasOrRef, options.config);
|
|
240
|
+
const parsed = parseModelRef(modelRef);
|
|
241
|
+
assertProviderCredentials(parsed.provider, options.config, options.env);
|
|
242
|
+
const registry = await createAiSdkProviderRegistry(options);
|
|
243
|
+
return registry.languageModel(modelRef as `${string}:${string}`);
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
function usageNumber(usage: Record<string, unknown>, keys: string[]): number {
|
|
247
|
+
for (const key of keys) {
|
|
248
|
+
const value = usage[key];
|
|
249
|
+
if (typeof value === 'number' && Number.isFinite(value)) return value;
|
|
250
|
+
}
|
|
251
|
+
return 0;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
export function normalizeAiSdkUsage(input: {
|
|
255
|
+
provider: string;
|
|
256
|
+
model: string;
|
|
257
|
+
usage?: Record<string, unknown> | null;
|
|
258
|
+
providerMetadata?: Record<string, unknown> | null;
|
|
259
|
+
costUsd?: number;
|
|
260
|
+
}): NormalizedProviderUsage {
|
|
261
|
+
const usage = input.usage ?? {};
|
|
262
|
+
return {
|
|
263
|
+
provider: input.provider,
|
|
264
|
+
model: input.model,
|
|
265
|
+
input_tokens: usageNumber(usage, ['inputTokens', 'promptTokens', 'input_tokens', 'prompt_tokens']),
|
|
266
|
+
output_tokens: usageNumber(usage, ['outputTokens', 'completionTokens', 'output_tokens', 'completion_tokens']),
|
|
267
|
+
cost_usd: input.costUsd ?? 0,
|
|
268
|
+
metadata: {
|
|
269
|
+
usage,
|
|
270
|
+
provider_metadata: input.providerMetadata ?? {},
|
|
271
|
+
},
|
|
272
|
+
};
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
export function recordProviderUsage(db: Database, input: NormalizedProviderUsage & { run_id?: string | null; created_at?: string }): string {
|
|
276
|
+
const id = `usage_${randomUUID()}`;
|
|
277
|
+
db.run(
|
|
278
|
+
`INSERT INTO provider_usage (id, run_id, provider, model, input_tokens, output_tokens, cost_usd, metadata_json, created_at)
|
|
279
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
280
|
+
[
|
|
281
|
+
id,
|
|
282
|
+
input.run_id ?? null,
|
|
283
|
+
input.provider,
|
|
284
|
+
input.model,
|
|
285
|
+
input.input_tokens,
|
|
286
|
+
input.output_tokens,
|
|
287
|
+
input.cost_usd,
|
|
288
|
+
JSON.stringify(input.metadata),
|
|
289
|
+
input.created_at ?? new Date().toISOString(),
|
|
290
|
+
],
|
|
291
|
+
);
|
|
292
|
+
return id;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
export function createDeterministicFakeProvider(provider: AiProviderId): ProviderFactory {
|
|
296
|
+
return () => ({
|
|
297
|
+
languageModel: (modelId: string) => ({
|
|
298
|
+
provider,
|
|
299
|
+
modelId,
|
|
300
|
+
specificationVersion: 'v3',
|
|
301
|
+
}),
|
|
302
|
+
chat: (modelId: string) => ({
|
|
303
|
+
provider,
|
|
304
|
+
modelId,
|
|
305
|
+
specificationVersion: 'v3',
|
|
306
|
+
}),
|
|
307
|
+
});
|
|
308
|
+
}
|