@hasna/knowledge 0.2.10 → 0.2.12
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 +40 -2
- package/bin/open-knowledge-mcp.js +386 -11
- package/bin/open-knowledge.js +46 -32
- package/docs/architecture/ai-native-knowledge-base.md +27 -0
- package/package.json +6 -2
- package/src/cli.ts +58 -4
- package/src/knowledge-db.ts +2 -0
- package/src/mcp.js +26 -0
- package/src/providers.ts +308 -0
- package/src/service.ts +35 -2
- package/src/storage-contract.ts +265 -0
- package/src/wiki-layout.ts +22 -6
- package/src/workspace.ts +41 -0
|
@@ -133,6 +133,18 @@ Raw files still route through `open-files`. Knowledge S3 storage is for derived
|
|
|
133
133
|
artifacts such as wiki pages, index shards, schema versions, logs, exports, and
|
|
134
134
|
run outputs.
|
|
135
135
|
|
|
136
|
+
The storage contract is inspectable through:
|
|
137
|
+
|
|
138
|
+
```bash
|
|
139
|
+
open-knowledge storage status --scope project --json
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
That contract names the local app path, SQLite catalog, generated artifact
|
|
143
|
+
classes, S3 bucket/prefix when configured, and the source ownership rule that
|
|
144
|
+
raw source bytes stay in `open-files`. The `storage_objects` table catalogs
|
|
145
|
+
generated artifacts by URI, kind, hash, size, and metadata so local mode and
|
|
146
|
+
remote/S3 mode share the same DB-facing shape.
|
|
147
|
+
|
|
136
148
|
## Wiki Model
|
|
137
149
|
|
|
138
150
|
The Karpathy-style wiki pattern is implemented as scalable artifacts, not three
|
|
@@ -208,6 +220,21 @@ The command should:
|
|
|
208
220
|
7. Record a run ledger with tool calls, sources, costs, outputs, and generated
|
|
209
221
|
records.
|
|
210
222
|
|
|
223
|
+
## Provider Registry
|
|
224
|
+
|
|
225
|
+
AI provider setup is BYOK and AI SDK v6 based. The local provider layer tracks:
|
|
226
|
+
|
|
227
|
+
- OpenAI via `@ai-sdk/openai`, defaulting to `openai:gpt-5.2`.
|
|
228
|
+
- Anthropic via `@ai-sdk/anthropic`, defaulting to
|
|
229
|
+
`anthropic:claude-sonnet-4-6`.
|
|
230
|
+
- DeepSeek via `@ai-sdk/deepseek`, defaulting to `deepseek:deepseek-chat`.
|
|
231
|
+
|
|
232
|
+
Model aliases live in config and can be inspected with
|
|
233
|
+
`open-knowledge providers models`. Credentials are resolved from env vars by
|
|
234
|
+
default, checked without making provider calls, and usage can be normalized into
|
|
235
|
+
the existing `provider_usage` table for future prompt, embedding, and web-search
|
|
236
|
+
runs.
|
|
237
|
+
|
|
211
238
|
## Non-Goals
|
|
212
239
|
|
|
213
240
|
- Do not make `open-knowledge` own raw source files.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hasna/knowledge",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.12",
|
|
4
4
|
"description": "Agent-friendly local knowledge CLI with JSON output, pagination, and safe destructive actions",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
"scripts": {
|
|
18
18
|
"test": "bun test",
|
|
19
19
|
"test:cli": "bun test tests/cli.test.ts",
|
|
20
|
-
"build": "bun build --target=bun --outfile=bin/open-knowledge.js --minify --external @aws-sdk/client-s3 --external @aws-sdk/credential-providers src/cli.ts && bun build --target=bun --outfile=bin/open-knowledge-mcp.js --external @modelcontextprotocol/sdk --external @aws-sdk/client-s3 --external @aws-sdk/credential-providers src/mcp.js",
|
|
20
|
+
"build": "bun build --target=bun --outfile=bin/open-knowledge.js --minify --external @aws-sdk/client-s3 --external @aws-sdk/credential-providers --external ai --external @ai-sdk/openai --external @ai-sdk/anthropic --external @ai-sdk/deepseek src/cli.ts && bun build --target=bun --outfile=bin/open-knowledge-mcp.js --external @modelcontextprotocol/sdk --external @aws-sdk/client-s3 --external @aws-sdk/credential-providers --external ai --external @ai-sdk/openai --external @ai-sdk/anthropic --external @ai-sdk/deepseek src/mcp.js",
|
|
21
21
|
"prepublishOnly": "bun run build",
|
|
22
22
|
"postinstall": "bun run build"
|
|
23
23
|
},
|
|
@@ -50,7 +50,11 @@
|
|
|
50
50
|
"dependencies": {
|
|
51
51
|
"@aws-sdk/client-s3": "^3.1063.0",
|
|
52
52
|
"@aws-sdk/credential-providers": "^3.1063.0",
|
|
53
|
+
"@ai-sdk/anthropic": "^3.0.81",
|
|
54
|
+
"@ai-sdk/deepseek": "^2.0.35",
|
|
55
|
+
"@ai-sdk/openai": "^3.0.68",
|
|
53
56
|
"@modelcontextprotocol/sdk": "^1.29.0",
|
|
57
|
+
"ai": "^6.0.197",
|
|
54
58
|
"zod": "^4.3.6"
|
|
55
59
|
},
|
|
56
60
|
"devDependencies": {
|
package/src/cli.ts
CHANGED
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
import { defaultStorePath, loadStore, saveStore, withLock, makeId, makeShortId, ensureStore, type KnowledgeItem } from './store';
|
|
8
8
|
import { openKnowledgeDb } from './knowledge-db';
|
|
9
9
|
import { createKnowledgeService } from './service';
|
|
10
|
+
import { assertProviderCredentials, parseModelRef, resolveModelRef, type AiProviderId } from './providers';
|
|
10
11
|
import { approvalStatus, assertS3ReadAllowed, assertWebSearchAllowed, createApprovalGate, recordAuditEvent, recordRedactionFindings, redactSecrets } from './safety';
|
|
11
12
|
import pkg from '../package.json' with { type: 'json' };
|
|
12
13
|
|
|
@@ -59,7 +60,7 @@ interface ParseResult {
|
|
|
59
60
|
flags: Flags;
|
|
60
61
|
}
|
|
61
62
|
|
|
62
|
-
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', 'storage', 'db', 'wiki', 'source', 'ingest', 'reindex', 'providers', 'safety', 'help'];
|
|
63
64
|
const COMMAND_ALIASES: Record<string, string> = {
|
|
64
65
|
ls: 'list',
|
|
65
66
|
rm: 'delete',
|
|
@@ -161,12 +162,14 @@ Commands:
|
|
|
161
162
|
dedupe Remove duplicate items by title+content (requires --yes)
|
|
162
163
|
stats Show knowledge base statistics
|
|
163
164
|
paths Show resolved workspace/store paths
|
|
165
|
+
storage status|validate Inspect local/S3 artifact storage contract
|
|
164
166
|
db init|stats Initialize or inspect local knowledge.db
|
|
165
167
|
wiki init Initialize scalable wiki/schema/index/log artifacts
|
|
166
168
|
source resolve <source-ref> Resolve read-only source content and citation evidence
|
|
167
169
|
ingest manifest <file|s3://> Ingest an open-files manifest into knowledge.db
|
|
168
170
|
ingest source <source-ref> Ingest a read-only source ref into knowledge.db
|
|
169
171
|
reindex outbox <file|s3://> Consume open-files change events and invalidate chunks
|
|
172
|
+
providers status|models|check Inspect AI SDK provider config and credentials
|
|
170
173
|
safety status|check|approve|audit|redact
|
|
171
174
|
help [command] Show help
|
|
172
175
|
|
|
@@ -228,11 +231,13 @@ function printCommandHelp(command: string): void {
|
|
|
228
231
|
if (command === 'dedupe') { console.log('Usage: open-knowledge dedupe --yes [--json]'); return; }
|
|
229
232
|
if (command === 'stats') { console.log('Usage: open-knowledge stats [--json]'); return; }
|
|
230
233
|
if (command === 'paths') { console.log('Usage: open-knowledge paths [--scope local|global|project] [--json]'); return; }
|
|
234
|
+
if (command === 'storage') { console.log('Usage: open-knowledge storage status|validate [--scope local|global|project] [--json]'); return; }
|
|
231
235
|
if (command === 'db') { console.log('Usage: open-knowledge db init|stats [--scope local|global|project] [--json]'); return; }
|
|
232
236
|
if (command === 'wiki') { console.log('Usage: open-knowledge wiki init [--scope local|global|project] [--json]'); return; }
|
|
233
237
|
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; }
|
|
234
238
|
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; }
|
|
235
239
|
if (command === 'reindex') { console.log('Usage: open-knowledge reindex outbox <file|s3://bucket/key> [--scope local|global|project] [--json]'); return; }
|
|
240
|
+
if (command === 'providers') { console.log('Usage: open-knowledge providers status|models|check [provider|model-alias] [--scope local|global|project] [--json]'); return; }
|
|
236
241
|
if (command === 'safety') { console.log('Usage: open-knowledge safety status|check|approve|audit|redact [args] [--scope local|global|project] [--json]'); return; }
|
|
237
242
|
printGlobalHelp();
|
|
238
243
|
}
|
|
@@ -278,11 +283,11 @@ async function run(argv: string[]): Promise<void> {
|
|
|
278
283
|
if (flags.completions) {
|
|
279
284
|
const shell = flags.completions;
|
|
280
285
|
if (shell === 'bash') {
|
|
281
|
-
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`);
|
|
286
|
+
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 storage 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`);
|
|
282
287
|
} else if (shell === 'zsh') {
|
|
283
|
-
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`);
|
|
288
|
+
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 storage 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`);
|
|
284
289
|
} else if (shell === 'fish') {
|
|
285
|
-
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"`);
|
|
290
|
+
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 storage 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"`);
|
|
286
291
|
} else {
|
|
287
292
|
throw new Error("Invalid --completions value. Use 'bash', 'zsh', or 'fish'.");
|
|
288
293
|
}
|
|
@@ -308,6 +313,31 @@ async function run(argv: string[]): Promise<void> {
|
|
|
308
313
|
return;
|
|
309
314
|
}
|
|
310
315
|
|
|
316
|
+
if (command === 'storage') {
|
|
317
|
+
const action = positional[1] ?? 'status';
|
|
318
|
+
if (action === 'status') {
|
|
319
|
+
const contract = service.storageContract();
|
|
320
|
+
const validation = service.validateStorage();
|
|
321
|
+
output({
|
|
322
|
+
ok: validation.ok,
|
|
323
|
+
...contract,
|
|
324
|
+
validation,
|
|
325
|
+
message: `${contract.storage_type} artifact storage at ${contract.artifact_store.uri_prefix}`,
|
|
326
|
+
}, flags.json);
|
|
327
|
+
return;
|
|
328
|
+
}
|
|
329
|
+
if (action === 'validate') {
|
|
330
|
+
const validation = service.validateStorage();
|
|
331
|
+
output({
|
|
332
|
+
ok: validation.ok,
|
|
333
|
+
validation,
|
|
334
|
+
message: validation.ok ? 'Storage contract valid' : `Storage contract invalid: ${validation.errors.join('; ')}`,
|
|
335
|
+
}, flags.json);
|
|
336
|
+
return;
|
|
337
|
+
}
|
|
338
|
+
throw new Error("Invalid storage action. Use 'status' or 'validate'.");
|
|
339
|
+
}
|
|
340
|
+
|
|
311
341
|
if (command === 'db') {
|
|
312
342
|
const action = positional[1] ?? 'init';
|
|
313
343
|
if (action !== 'init' && action !== 'stats') {
|
|
@@ -504,6 +534,30 @@ async function run(argv: string[]): Promise<void> {
|
|
|
504
534
|
return;
|
|
505
535
|
}
|
|
506
536
|
|
|
537
|
+
if (command === 'providers') {
|
|
538
|
+
const action = positional[1] ?? 'status';
|
|
539
|
+
if (action === 'status') {
|
|
540
|
+
const status = service.providerStatus();
|
|
541
|
+
const configured = status.providers.filter((entry) => entry.configured).length;
|
|
542
|
+
output({ ok: true, ...status, message: `${configured}/${status.providers.length} provider credential(s) configured` }, flags.json);
|
|
543
|
+
return;
|
|
544
|
+
}
|
|
545
|
+
if (action === 'models') {
|
|
546
|
+
const models = service.modelRegistry();
|
|
547
|
+
output({ ok: true, models, message: `${models.length} model alias(es)` }, flags.json);
|
|
548
|
+
return;
|
|
549
|
+
}
|
|
550
|
+
if (action === 'check') {
|
|
551
|
+
const target = positional[2] ?? 'default';
|
|
552
|
+
const modelRef = resolveModelRef(target, service.config());
|
|
553
|
+
const parsed = parseModelRef(modelRef);
|
|
554
|
+
const credential = assertProviderCredentials(parsed.provider as AiProviderId, service.config());
|
|
555
|
+
output({ ok: true, target, model_ref: modelRef, provider: parsed.provider, model: parsed.model, credential, message: `${parsed.provider} credentials configured` }, flags.json);
|
|
556
|
+
return;
|
|
557
|
+
}
|
|
558
|
+
throw new Error("Invalid providers action. Use 'status', 'models', or 'check'.");
|
|
559
|
+
}
|
|
560
|
+
|
|
507
561
|
ensureStore(storePath);
|
|
508
562
|
|
|
509
563
|
if (command === 'add') {
|
package/src/knowledge-db.ts
CHANGED
|
@@ -16,6 +16,7 @@ export interface KnowledgeDbStats {
|
|
|
16
16
|
redaction_findings: number;
|
|
17
17
|
audit_events: number;
|
|
18
18
|
approval_gates: number;
|
|
19
|
+
storage_objects: number;
|
|
19
20
|
}
|
|
20
21
|
|
|
21
22
|
const MIGRATION_1 = `
|
|
@@ -280,6 +281,7 @@ export function getKnowledgeDbStats(path: string): KnowledgeDbStats {
|
|
|
280
281
|
redaction_findings: count(db, 'redaction_findings'),
|
|
281
282
|
audit_events: count(db, 'audit_events'),
|
|
282
283
|
approval_gates: count(db, 'approval_gates'),
|
|
284
|
+
storage_objects: count(db, 'storage_objects'),
|
|
283
285
|
};
|
|
284
286
|
} finally {
|
|
285
287
|
db.close();
|
package/src/mcp.js
CHANGED
|
@@ -77,6 +77,18 @@ export function buildServer() {
|
|
|
77
77
|
return jsonText(createKnowledgeService({ scope }).paths());
|
|
78
78
|
});
|
|
79
79
|
|
|
80
|
+
registerTool(server, 'ok_storage_status', 'Knowledge storage status', 'Inspect local/S3 artifact storage, source ownership, and scalability contract', {
|
|
81
|
+
scope: scopeField,
|
|
82
|
+
}, async ({ scope }) => {
|
|
83
|
+
const service = createKnowledgeService({ scope });
|
|
84
|
+
const validation = service.validateStorage();
|
|
85
|
+
return jsonText({
|
|
86
|
+
ok: validation.ok,
|
|
87
|
+
...service.storageContract(),
|
|
88
|
+
validation,
|
|
89
|
+
});
|
|
90
|
+
});
|
|
91
|
+
|
|
80
92
|
registerTool(server, 'ok_parse_source_ref', 'Parse source reference', 'Parse and validate an open-files, S3, file, or web source ref', {
|
|
81
93
|
uri: z.string().describe('Source reference URI'),
|
|
82
94
|
}, async ({ uri }) => {
|
|
@@ -105,6 +117,20 @@ export function buildServer() {
|
|
|
105
117
|
}
|
|
106
118
|
});
|
|
107
119
|
|
|
120
|
+
registerTool(server, 'ok_provider_status', 'AI provider status', 'Inspect configured AI SDK providers, model aliases, and BYOK credential availability', {
|
|
121
|
+
scope: scopeField,
|
|
122
|
+
}, async ({ scope }) => {
|
|
123
|
+
const service = createKnowledgeService({ scope });
|
|
124
|
+
return jsonText({ ok: true, ...service.providerStatus() });
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
registerTool(server, 'ok_provider_models', 'AI provider models', 'List AI SDK model aliases and capability metadata', {
|
|
128
|
+
scope: scopeField,
|
|
129
|
+
}, async ({ scope }) => {
|
|
130
|
+
const service = createKnowledgeService({ scope });
|
|
131
|
+
return jsonText({ ok: true, models: service.modelRegistry() });
|
|
132
|
+
});
|
|
133
|
+
|
|
108
134
|
registerTool(server, 'ok_add', 'Add a knowledge item', 'Add a new item to the knowledge store', {
|
|
109
135
|
title: z.string().describe('Item title'),
|
|
110
136
|
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
|
+
}
|
package/src/service.ts
CHANGED
|
@@ -1,10 +1,18 @@
|
|
|
1
1
|
import { createArtifactStore } from './artifact-store';
|
|
2
2
|
import { consumeOpenFilesOutbox } from './outbox-consume';
|
|
3
|
-
import { getKnowledgeDbStats, migrateKnowledgeDb } from './knowledge-db';
|
|
3
|
+
import { getKnowledgeDbStats, migrateKnowledgeDb, openKnowledgeDb } from './knowledge-db';
|
|
4
4
|
import { ingestOpenFilesManifest } from './manifest-ingest';
|
|
5
5
|
import { ingestSourceRef } from './source-ingest';
|
|
6
6
|
import { resolveOpenFilesSource } from './source-resolver';
|
|
7
|
+
import { providerStatus, listModelRegistry, type ProviderStatusResult, type ModelRegistryEntry } from './providers';
|
|
7
8
|
import { resolveSafetyPolicy } from './safety';
|
|
9
|
+
import {
|
|
10
|
+
recordStorageObjects,
|
|
11
|
+
resolveStorageContract,
|
|
12
|
+
validateStorageConfig,
|
|
13
|
+
type StorageContract,
|
|
14
|
+
type StorageValidationResult,
|
|
15
|
+
} from './storage-contract';
|
|
8
16
|
import { initializeWikiLayout } from './wiki-layout';
|
|
9
17
|
import {
|
|
10
18
|
ensureKnowledgeWorkspace,
|
|
@@ -75,6 +83,14 @@ export class KnowledgeService {
|
|
|
75
83
|
return createArtifactStore(this.config(), this.ensureWorkspace());
|
|
76
84
|
}
|
|
77
85
|
|
|
86
|
+
storageContract(): StorageContract {
|
|
87
|
+
return resolveStorageContract(this.config(), this.ensureWorkspace(), this.scope);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
validateStorage(): StorageValidationResult {
|
|
91
|
+
return validateStorageConfig(this.config(), this.ensureWorkspace());
|
|
92
|
+
}
|
|
93
|
+
|
|
78
94
|
paths(): KnowledgePathsResult {
|
|
79
95
|
const workspace = this.ensureWorkspace();
|
|
80
96
|
return {
|
|
@@ -106,7 +122,16 @@ export class KnowledgeService {
|
|
|
106
122
|
}
|
|
107
123
|
|
|
108
124
|
async initWiki() {
|
|
109
|
-
|
|
125
|
+
const workspace = this.ensureWorkspace();
|
|
126
|
+
migrateKnowledgeDb(workspace.knowledgeDbPath);
|
|
127
|
+
const result = await initializeWikiLayout(this.artifactStore());
|
|
128
|
+
const db = openKnowledgeDb(workspace.knowledgeDbPath);
|
|
129
|
+
try {
|
|
130
|
+
recordStorageObjects(db, result.artifacts);
|
|
131
|
+
} finally {
|
|
132
|
+
db.close();
|
|
133
|
+
}
|
|
134
|
+
return result;
|
|
110
135
|
}
|
|
111
136
|
|
|
112
137
|
async ingestManifest(input: string) {
|
|
@@ -150,6 +175,14 @@ export class KnowledgeService {
|
|
|
150
175
|
safetyPolicy: this.safetyPolicy(),
|
|
151
176
|
});
|
|
152
177
|
}
|
|
178
|
+
|
|
179
|
+
providerStatus(env: Record<string, string | undefined> = process.env): ProviderStatusResult {
|
|
180
|
+
return providerStatus(this.config(), env);
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
modelRegistry(): ModelRegistryEntry[] {
|
|
184
|
+
return listModelRegistry(this.config());
|
|
185
|
+
}
|
|
153
186
|
}
|
|
154
187
|
|
|
155
188
|
export function createKnowledgeService(options: KnowledgeServiceOptions = {}): KnowledgeService {
|