@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.
@@ -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.10",
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') {
@@ -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'),
@@ -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
- return initializeWikiLayout(this.artifactStore());
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 {