@hasna/knowledge 0.2.16 → 0.2.18

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.
@@ -249,6 +249,19 @@ The command should:
249
249
  7. Record a run ledger with tool calls, sources, costs, outputs, and generated
250
250
  records.
251
251
 
252
+ The first implementation exposes this as `open-knowledge ask|build <prompt>`
253
+ and the installed `knowledge <prompt>` bin alias. It retrieves read-only context,
254
+ returns a local citation draft by default, optionally calls AI SDK generation via
255
+ `--generate`, records `runs`, `run_events`, and `provider_usage`, and only
256
+ proposes durable wiki updates until the wiki compile/write task owns writes.
257
+
258
+ Provider-native web search is exposed separately as
259
+ `open-knowledge web search <query>` and MCP `ok_web_search`. Real network access
260
+ is safety-gated; OpenAI and Anthropic use provider web-search tools through AI
261
+ SDK, while DeepSeek remains a future fallback/external-search path. Returned web
262
+ snippets can optionally be filed as read-only `web` source refs for later local
263
+ search and citation.
264
+
252
265
  ## Provider Registry
253
266
 
254
267
  AI provider setup is BYOK and AI SDK v6 based. The local provider layer tracks:
@@ -138,6 +138,14 @@ The local context-pack implementation is available through
138
138
  search rows using exact-term coverage, citation availability, source freshness,
139
139
  and source/wiki authority, then emits excerpts and citation objects that preserve
140
140
  source refs, artifact URIs, revision/hash metadata, offsets, and provenance.
141
+ `open-knowledge ask|build <prompt>`, the installed `knowledge <prompt>` alias,
142
+ and MCP `knowledge_ask` wrap this context pack in a run ledger and return a
143
+ citation draft or explicit AI SDK generated answer.
144
+
145
+ Provider-native web search lives beside local retrieval. `open-knowledge web
146
+ search` and MCP `ok_web_search` are safety-gated, capture provider sources, and
147
+ can file snippets as read-only `web` source refs so later local retrieval treats
148
+ them like other cited sources.
141
149
 
142
150
  ## Reindexing
143
151
 
package/package.json CHANGED
@@ -1,9 +1,10 @@
1
1
  {
2
2
  "name": "@hasna/knowledge",
3
- "version": "0.2.16",
3
+ "version": "0.2.18",
4
4
  "description": "Agent-friendly local knowledge CLI with JSON output, pagination, and safe destructive actions",
5
5
  "type": "module",
6
6
  "bin": {
7
+ "knowledge": "bin/open-knowledge.js",
7
8
  "open-knowledge": "bin/open-knowledge.js",
8
9
  "open-knowledge-mcp": "bin/open-knowledge-mcp.js"
9
10
  },
package/src/agent.ts ADDED
@@ -0,0 +1,367 @@
1
+ import { randomUUID } from 'node:crypto';
2
+ import { migrateKnowledgeDb, openKnowledgeDb } from './knowledge-db';
3
+ import { languageModelFor, normalizeAiSdkUsage, parseModelRef, recordProviderUsage, resolveModelRef } from './providers';
4
+ import { retrieveKnowledgeContext, type KnowledgeContextPack, type RetrievalOptions } from './retrieval';
5
+ import type { KnowledgeConfig } from './workspace';
6
+
7
+ export interface KnowledgePromptOptions extends Omit<RetrievalOptions, 'query'> {
8
+ prompt: string;
9
+ generate?: boolean;
10
+ approveWrite?: boolean;
11
+ now?: Date;
12
+ }
13
+
14
+ export interface KnowledgePromptResult {
15
+ run_id: string;
16
+ prompt: string;
17
+ generated: boolean;
18
+ provider: string;
19
+ model: string;
20
+ answer: string;
21
+ context: KnowledgeContextPack;
22
+ citations: KnowledgeContextPack['citations'];
23
+ proposed_wiki_updates: Array<{
24
+ kind: 'answer_note';
25
+ title: string;
26
+ citations: string[];
27
+ requires_approval: boolean;
28
+ }>;
29
+ write_policy: {
30
+ approved: boolean;
31
+ durable_writes_performed: false;
32
+ reason: string;
33
+ };
34
+ usage: {
35
+ input_tokens: number;
36
+ output_tokens: number;
37
+ cost_usd: number;
38
+ };
39
+ warnings: string[];
40
+ }
41
+
42
+ function estimateTokens(text: string): number {
43
+ const words = text.trim().split(/\s+/).filter(Boolean).length;
44
+ return Math.max(1, Math.ceil(words * 1.25));
45
+ }
46
+
47
+ function citationLabel(index: number): string {
48
+ return `C${index + 1}`;
49
+ }
50
+
51
+ function localAnswer(prompt: string, context: KnowledgeContextPack): string {
52
+ if (context.excerpts.length === 0) {
53
+ return `No indexed knowledge matched the prompt: ${prompt}`;
54
+ }
55
+ const lines = [
56
+ `Found ${context.excerpts.length} relevant knowledge excerpt(s) for: ${prompt}`,
57
+ '',
58
+ ...context.excerpts.slice(0, 5).map((excerpt, index) => {
59
+ const citation = context.citations.find((entry) => entry.id === excerpt.citation_id);
60
+ const ref = citation?.source_ref ?? citation?.source_uri ?? citation?.artifact_path ?? citation?.artifact_uri ?? 'unknown source';
61
+ return `[${citationLabel(index)}] ${excerpt.text} (${ref})`;
62
+ }),
63
+ ];
64
+ return lines.join('\n');
65
+ }
66
+
67
+ function promptForModel(prompt: string, context: KnowledgeContextPack): string {
68
+ const citations = context.citations.map((citation, index) => ({
69
+ id: citationLabel(index),
70
+ source_ref: citation.source_ref,
71
+ source_uri: citation.source_uri,
72
+ artifact_path: citation.artifact_path,
73
+ revision: citation.revision,
74
+ hash: citation.hash,
75
+ quote: citation.quote,
76
+ }));
77
+ const excerpts = context.excerpts.map((excerpt, index) => ({
78
+ id: citationLabel(index),
79
+ kind: excerpt.kind,
80
+ text: excerpt.text,
81
+ score: excerpt.score,
82
+ }));
83
+ return [
84
+ `Prompt: ${prompt}`,
85
+ '',
86
+ 'Use only the provided context. Cite claims with citation ids like [C1]. If context is insufficient, say what is missing.',
87
+ '',
88
+ `Context excerpts:\n${JSON.stringify(excerpts, null, 2)}`,
89
+ '',
90
+ `Citations:\n${JSON.stringify(citations, null, 2)}`,
91
+ ].join('\n');
92
+ }
93
+
94
+ function proposedUpdates(prompt: string, context: KnowledgeContextPack): KnowledgePromptResult['proposed_wiki_updates'] {
95
+ if (context.citations.length === 0) return [];
96
+ return [{
97
+ kind: 'answer_note',
98
+ title: prompt.length > 80 ? `${prompt.slice(0, 77)}...` : prompt,
99
+ citations: context.citations.map((citation) => citation.id),
100
+ requires_approval: true,
101
+ }];
102
+ }
103
+
104
+ function insertRun(dbPath: string, input: {
105
+ runId: string;
106
+ prompt: string;
107
+ status: string;
108
+ provider: string;
109
+ model: string;
110
+ metadata: Record<string, unknown>;
111
+ now: string;
112
+ }): void {
113
+ const db = openKnowledgeDb(dbPath);
114
+ try {
115
+ db.run(
116
+ `INSERT INTO runs (id, type, prompt, status, provider, model, metadata_json, created_at, updated_at)
117
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`,
118
+ [
119
+ input.runId,
120
+ 'knowledge-prompt',
121
+ input.prompt,
122
+ input.status,
123
+ input.provider,
124
+ input.model,
125
+ JSON.stringify(input.metadata),
126
+ input.now,
127
+ input.now,
128
+ ],
129
+ );
130
+ } finally {
131
+ db.close();
132
+ }
133
+ }
134
+
135
+ function addRunEvent(dbPath: string, input: {
136
+ runId: string;
137
+ level: 'info' | 'warn' | 'error';
138
+ event: string;
139
+ metadata: Record<string, unknown>;
140
+ now: string;
141
+ }): void {
142
+ const db = openKnowledgeDb(dbPath);
143
+ try {
144
+ db.run(
145
+ `INSERT INTO run_events (id, run_id, level, event, metadata_json, created_at)
146
+ VALUES (?, ?, ?, ?, ?, ?)`,
147
+ [
148
+ `evt_${randomUUID()}`,
149
+ input.runId,
150
+ input.level,
151
+ input.event,
152
+ JSON.stringify(input.metadata),
153
+ input.now,
154
+ ],
155
+ );
156
+ } finally {
157
+ db.close();
158
+ }
159
+ }
160
+
161
+ function updateRun(dbPath: string, input: {
162
+ runId: string;
163
+ status: string;
164
+ provider: string;
165
+ model: string;
166
+ metadata: Record<string, unknown>;
167
+ now: string;
168
+ }): void {
169
+ const db = openKnowledgeDb(dbPath);
170
+ try {
171
+ db.run(
172
+ `UPDATE runs
173
+ SET status = ?, provider = ?, model = ?, metadata_json = ?, updated_at = ?
174
+ WHERE id = ?`,
175
+ [
176
+ input.status,
177
+ input.provider,
178
+ input.model,
179
+ JSON.stringify(input.metadata),
180
+ input.now,
181
+ input.runId,
182
+ ],
183
+ );
184
+ } finally {
185
+ db.close();
186
+ }
187
+ }
188
+
189
+ function recordUsage(dbPath: string, runId: string, usage: KnowledgePromptResult['usage'], provider: string, model: string, now: string, metadata: Record<string, unknown> = {}): void {
190
+ const db = openKnowledgeDb(dbPath);
191
+ try {
192
+ recordProviderUsage(db, {
193
+ run_id: runId,
194
+ provider,
195
+ model,
196
+ input_tokens: usage.input_tokens,
197
+ output_tokens: usage.output_tokens,
198
+ cost_usd: usage.cost_usd,
199
+ metadata,
200
+ created_at: now,
201
+ });
202
+ } finally {
203
+ db.close();
204
+ }
205
+ }
206
+
207
+ export async function runKnowledgePrompt(options: KnowledgePromptOptions): Promise<KnowledgePromptResult> {
208
+ const prompt = options.prompt.trim();
209
+ if (!prompt) throw new Error('Knowledge prompt is required.');
210
+ const now = (options.now ?? new Date()).toISOString();
211
+ const runId = `run_${randomUUID()}`;
212
+ const modelRef = resolveModelRef(options.modelRef ?? 'default', options.config);
213
+ const parsed = parseModelRef(modelRef);
214
+
215
+ migrateKnowledgeDb(options.dbPath);
216
+ insertRun(options.dbPath, {
217
+ runId,
218
+ prompt,
219
+ status: options.generate ? 'running' : 'dry_run',
220
+ provider: options.generate ? parsed.provider : 'local',
221
+ model: options.generate ? parsed.model : 'context-draft',
222
+ metadata: {
223
+ semantic: options.semantic === true || options.fake === true || Boolean(options.modelRef),
224
+ approve_write: options.approveWrite === true,
225
+ generated: options.generate === true,
226
+ },
227
+ now,
228
+ });
229
+
230
+ const { prompt: _prompt, generate: _generate, approveWrite: _approveWrite, now: _now, ...retrievalOptions } = options;
231
+ const context = await retrieveKnowledgeContext({
232
+ ...retrievalOptions,
233
+ query: prompt,
234
+ });
235
+ addRunEvent(options.dbPath, {
236
+ runId,
237
+ level: 'info',
238
+ event: 'context_retrieved',
239
+ metadata: {
240
+ results: context.results.length,
241
+ citations: context.citations.length,
242
+ warnings: context.warnings,
243
+ },
244
+ now,
245
+ });
246
+
247
+ let answer = localAnswer(prompt, context);
248
+ let generated = false;
249
+ let provider = 'local';
250
+ let model = 'context-draft';
251
+ let usage = {
252
+ input_tokens: estimateTokens(prompt) + context.excerpts.reduce((sum, excerpt) => sum + estimateTokens(excerpt.text), 0),
253
+ output_tokens: estimateTokens(answer),
254
+ cost_usd: 0,
255
+ };
256
+ const warnings = [...context.warnings];
257
+
258
+ if (options.generate) {
259
+ try {
260
+ if (options.fake) {
261
+ generated = true;
262
+ provider = parsed.provider;
263
+ model = parsed.model;
264
+ answer = `Fake generated answer for: ${prompt}\n\n${answer}`;
265
+ } else {
266
+ const { generateText } = await import('ai');
267
+ const languageModel = await languageModelFor(modelRef, {
268
+ config: options.config,
269
+ env: options.env,
270
+ });
271
+ const result = await generateText({
272
+ model: languageModel as never,
273
+ system: 'You answer company knowledge-base prompts using only provided context and citation ids.',
274
+ prompt: promptForModel(prompt, context),
275
+ });
276
+ generated = true;
277
+ provider = parsed.provider;
278
+ model = parsed.model;
279
+ answer = result.text;
280
+ const normalized = normalizeAiSdkUsage({
281
+ provider,
282
+ model,
283
+ usage: result.usage as Record<string, unknown> | undefined,
284
+ providerMetadata: result.providerMetadata as Record<string, unknown> | undefined,
285
+ });
286
+ usage = {
287
+ input_tokens: normalized.input_tokens,
288
+ output_tokens: normalized.output_tokens,
289
+ cost_usd: normalized.cost_usd,
290
+ };
291
+ }
292
+ } catch (error) {
293
+ addRunEvent(options.dbPath, {
294
+ runId,
295
+ level: 'error',
296
+ event: 'answer_generation_failed',
297
+ metadata: { message: error instanceof Error ? error.message : String(error) },
298
+ now,
299
+ });
300
+ updateRun(options.dbPath, {
301
+ runId,
302
+ status: 'failed',
303
+ provider: parsed.provider,
304
+ model: parsed.model,
305
+ metadata: {
306
+ generated: false,
307
+ error: error instanceof Error ? error.message : String(error),
308
+ },
309
+ now,
310
+ });
311
+ throw error;
312
+ }
313
+ }
314
+
315
+ const updates = proposedUpdates(prompt, context);
316
+ const writePolicy = {
317
+ approved: options.approveWrite === true,
318
+ durable_writes_performed: false as const,
319
+ reason: options.approveWrite
320
+ ? 'Approval flag recorded; durable wiki writing is deferred to the wiki compile task.'
321
+ : 'Dry-run mode: proposed wiki updates require approval before durable writes.',
322
+ };
323
+ addRunEvent(options.dbPath, {
324
+ runId,
325
+ level: 'info',
326
+ event: generated ? 'answer_generated' : 'answer_drafted',
327
+ metadata: {
328
+ provider,
329
+ model,
330
+ proposed_updates: updates.length,
331
+ durable_writes_performed: false,
332
+ },
333
+ now,
334
+ });
335
+ recordUsage(options.dbPath, runId, usage, provider, model, now, {
336
+ generated,
337
+ citations: context.citations.length,
338
+ });
339
+ updateRun(options.dbPath, {
340
+ runId,
341
+ status: generated ? 'completed' : 'dry_run',
342
+ provider,
343
+ model,
344
+ metadata: {
345
+ generated,
346
+ citations: context.citations.length,
347
+ proposed_updates: updates.length,
348
+ approve_write: options.approveWrite === true,
349
+ },
350
+ now,
351
+ });
352
+
353
+ return {
354
+ run_id: runId,
355
+ prompt,
356
+ generated,
357
+ provider,
358
+ model,
359
+ answer,
360
+ context,
361
+ citations: context.citations,
362
+ proposed_wiki_updates: updates,
363
+ write_policy: writePolicy,
364
+ usage,
365
+ warnings,
366
+ };
367
+ }
package/src/cli.ts CHANGED
@@ -9,6 +9,7 @@ import { openKnowledgeDb } from './knowledge-db';
9
9
  import { createKnowledgeService } from './service';
10
10
  import { assertProviderCredentials, parseModelRef, resolveModelRef, type AiProviderId } from './providers';
11
11
  import { approvalStatus, assertS3ReadAllowed, assertWebSearchAllowed, createApprovalGate, recordAuditEvent, recordRedactionFindings, redactSecrets } from './safety';
12
+ import { basename } from 'node:path';
12
13
  import pkg from '../package.json' with { type: 'json' };
13
14
 
14
15
  type LogLevel = 'debug' | 'info' | 'warn' | 'error';
@@ -51,6 +52,11 @@ interface Flags {
51
52
  dimensions?: number;
52
53
  semantic?: boolean;
53
54
  context?: boolean;
55
+ generate?: boolean;
56
+ approveWrite?: boolean;
57
+ provider?: string;
58
+ domain?: string[];
59
+ fileResults?: boolean;
54
60
  noColor?: boolean;
55
61
  scope?: string;
56
62
  olderThan?: number;
@@ -65,12 +71,13 @@ interface ParseResult {
65
71
  flags: Flags;
66
72
  }
67
73
 
68
- const COMMANDS = ['add', 'list', 'get', 'delete', 'update', 'archive', 'restore', 'upsert', 'untag', 'export', 'prune', 'dedupe', 'stats', 'paths', 'storage', 'db', 'wiki', 'source', 'ingest', 'reindex', 'search', 'embeddings', 'providers', 'safety', 'help'];
74
+ const COMMANDS = ['add', 'list', 'get', 'delete', 'update', 'archive', 'restore', 'upsert', 'untag', 'export', 'prune', 'dedupe', 'stats', 'paths', 'storage', 'db', 'wiki', 'source', 'ingest', 'reindex', 'search', 'web', 'ask', 'build', 'embeddings', 'providers', 'safety', 'help'];
69
75
  const COMMAND_ALIASES: Record<string, string> = {
70
76
  ls: 'list',
71
77
  rm: 'delete',
72
78
  edit: 'update',
73
79
  unarchive: 'restore',
80
+ knowledge: 'ask',
74
81
  };
75
82
 
76
83
  function parseArgs(argv: string[]): ParseResult {
@@ -105,6 +112,11 @@ function parseArgs(argv: string[]): ParseResult {
105
112
  case '--dimensions': flags.dimensions = Number(argv[i + 1]); i += 1; break;
106
113
  case '--semantic': flags.semantic = true; break;
107
114
  case '--context': flags.context = true; break;
115
+ case '--generate': flags.generate = true; break;
116
+ case '--approve-write': flags.approveWrite = true; break;
117
+ case '--provider': flags.provider = argv[i + 1]; i += 1; break;
118
+ case '--domain': flags.domain = [...(flags.domain ?? []), argv[i + 1]]; i += 1; break;
119
+ case '--file-results': flags.fileResults = true; break;
108
120
  case '--fake': flags.fake = true; break;
109
121
  case '--no-color': flags.noColor = true; break;
110
122
  case '--scope': flags.scope = argv[i + 1]; i += 1; break;
@@ -151,6 +163,10 @@ function suggestCommand(input: string): string {
151
163
  return bestScore <= 3 ? best : '';
152
164
  }
153
165
 
166
+ function invokedAsKnowledge(): boolean {
167
+ return basename(process.argv[1] ?? '') === 'knowledge';
168
+ }
169
+
154
170
  function printGlobalHelp(): void {
155
171
  console.log(`open-knowledge - local agent knowledge store
156
172
 
@@ -180,6 +196,8 @@ Commands:
180
196
  ingest source <source-ref> Ingest a read-only source ref into knowledge.db
181
197
  reindex outbox <file|s3://> Consume open-files change events and invalidate chunks
182
198
  search <query> Hybrid search sources, wiki pages, indexes, or context
199
+ web search <query> Provider-native web search with citations
200
+ ask|build <prompt> Build a read-only citation answer/context pack
183
201
  embeddings status|index|search Build/query local vector embeddings
184
202
  providers status|models|check Inspect AI SDK provider config and credentials
185
203
  safety status|check|approve|audit|redact
@@ -193,6 +211,11 @@ Global Options:
193
211
  --dimensions <n> Embedding dimensions for local/fake providers
194
212
  --semantic Include vector semantic results in search
195
213
  --context Return a reranked citation context pack for search
214
+ --generate Call AI SDK text generation for ask/build
215
+ --approve-write Record approval intent for future durable wiki writes
216
+ --provider <name> Provider override for web search
217
+ --domain <domain> Restrict provider web search to a domain
218
+ --file-results File web snippets as web source refs
196
219
  --fake Use deterministic fake embeddings for local tests
197
220
  --scope local|global|project Store scope (default: global ~/.hasna/apps/knowledge/)
198
221
  --no-color Disable color output
@@ -255,6 +278,8 @@ function printCommandHelp(command: string): void {
255
278
  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; }
256
279
  if (command === 'reindex') { console.log('Usage: open-knowledge reindex outbox <file|s3://bucket/key> [--scope local|global|project] [--json]'); return; }
257
280
  if (command === 'search') { console.log('Usage: open-knowledge search <query> [--context] [--semantic] [--model openai:text-embedding-3-small] [--limit <n>] [--dimensions <n>] [--fake] [--scope local|global|project] [--json]'); return; }
281
+ if (command === 'web') { console.log('Usage: open-knowledge web search <query> [--provider openai|anthropic] [--model provider:model] [--domain <domain>] [--file-results] [--fake] [--scope local|global|project] [--json]'); return; }
282
+ if (command === 'ask' || command === 'build' || command === 'knowledge') { console.log('Usage: open-knowledge ask|build <prompt> [--generate] [--semantic] [--model default|provider:model] [--approve-write] [--scope local|global|project] [--json]'); return; }
258
283
  if (command === 'embeddings') { console.log('Usage: open-knowledge embeddings status|index|search [query] [--model openai:text-embedding-3-small] [--limit <n>] [--dimensions <n>] [--fake] [--scope local|global|project] [--json]'); return; }
259
284
  if (command === 'providers') { console.log('Usage: open-knowledge providers status|models|check [provider|model-alias] [--scope local|global|project] [--json]'); return; }
260
285
  if (command === 'safety') { console.log('Usage: open-knowledge safety status|check|approve|audit|redact [args] [--scope local|global|project] [--json]'); return; }
@@ -302,18 +327,23 @@ async function run(argv: string[]): Promise<void> {
302
327
  if (flags.completions) {
303
328
  const shell = flags.completions;
304
329
  if (shell === 'bash') {
305
- 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 search embeddings 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 --model --dimensions --semantic --context --fake --no-color --scope --archived --include-archived" -- "$cur")); }; complete -F _open_knowledge open-knowledge`);
330
+ 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 search web ask build embeddings providers safety help ls rm edit unarchive knowledge --json --yes --help --version --desc --page --limit --search --sort --id --store --title --content --url --tag --format --completions --purpose --model --dimensions --semantic --context --generate --approve-write --provider --domain --file-results --fake --no-color --scope --archived --include-archived" -- "$cur")); }; complete -F _open_knowledge open-knowledge`);
306
331
  } else if (shell === 'zsh') {
307
- 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 search embeddings providers safety help ls rm edit unarchive)" "(--json)--json" "(--yes)-y" "(--help)--help" "(--version)--version" "(--desc)--desc" "(--archived)--archived" "(--include-archived)--include-archived" "(--semantic)--semantic" "(--context)--context" "(--fake)--fake" "(-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]:" "(--model)--model[model ref]:" "(--dimensions)--dimensions[embedding dimensions]:number:" "(--no-color)--no-color[disable color]" "(--scope)--scope"\{local,global,project\}:" }; _open_knowledge`);
332
+ 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 search web ask build embeddings providers safety help ls rm edit unarchive knowledge)" "(--json)--json" "(--yes)-y" "(--help)--help" "(--version)--version" "(--desc)--desc" "(--archived)--archived" "(--include-archived)--include-archived" "(--semantic)--semantic" "(--context)--context" "(--generate)--generate" "(--approve-write)--approve-write" "(--file-results)--file-results" "(--fake)--fake" "(-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]:" "(--model)--model[model ref]:" "(--dimensions)--dimensions[embedding dimensions]:number:" "(--provider)--provider[provider]:" "(--domain)--domain[domain]:" "(--no-color)--no-color[disable color]" "(--scope)--scope"\{local,global,project\}:" }; _open_knowledge`);
308
333
  } else if (shell === 'fish') {
309
- 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 search embeddings 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 -l semantic; complete -c open-knowledge -l context; complete -c open-knowledge -l fake; 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 model; complete -c open-knowledge -l dimensions; complete -c open-knowledge -l no-color; complete -c open-knowledge -l scope -a "local global project"`);
334
+ 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 search web ask build embeddings providers safety help ls rm edit unarchive knowledge"; 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 -l semantic; complete -c open-knowledge -l context; complete -c open-knowledge -l generate; complete -c open-knowledge -l approve-write; complete -c open-knowledge -l provider; complete -c open-knowledge -l domain; complete -c open-knowledge -l file-results; complete -c open-knowledge -l fake; 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 model; complete -c open-knowledge -l dimensions; complete -c open-knowledge -l no-color; complete -c open-knowledge -l scope -a "local global project"`);
310
335
  } else {
311
336
  throw new Error("Invalid --completions value. Use 'bash', 'zsh', or 'fish'.");
312
337
  }
313
338
  return;
314
339
  }
315
340
 
316
- const command = resolveCommand(positional[0]);
341
+ let command = resolveCommand(positional[0]);
342
+ let commandArgOffset = 1;
343
+ if (invokedAsKnowledge() && command && !COMMANDS.includes(command)) {
344
+ command = 'ask';
345
+ commandArgOffset = 0;
346
+ }
317
347
 
318
348
  if (!command || flags.help || command === 'help') { printCommandHelp(positional[1]); return; }
319
349
 
@@ -613,6 +643,41 @@ async function run(argv: string[]): Promise<void> {
613
643
  return;
614
644
  }
615
645
 
646
+ if (command === 'web') {
647
+ const action = positional[1] ?? 'search';
648
+ if (action !== 'search') throw new Error("Invalid web action. Use 'search'.");
649
+ const query = positional.slice(2).join(' ');
650
+ if (!query) throw new Error('Usage: open-knowledge web search <query>');
651
+ const result = await service.webSearch({
652
+ query,
653
+ limit: flags.limit,
654
+ modelRef: flags.model,
655
+ provider: flags.provider as AiProviderId | undefined,
656
+ domains: flags.domain,
657
+ fake: flags.fake,
658
+ fileResults: flags.fileResults,
659
+ });
660
+ output({ ok: true, ...result, message: `${result.sources.length} web source(s)` }, flags.json);
661
+ return;
662
+ }
663
+
664
+ if (command === 'ask' || command === 'build') {
665
+ const prompt = positional.slice(commandArgOffset).join(' ');
666
+ if (!prompt) throw new Error('Usage: open-knowledge ask <prompt>');
667
+ const result = await service.runPrompt({
668
+ prompt,
669
+ limit: flags.limit,
670
+ semantic: flags.semantic,
671
+ modelRef: flags.model,
672
+ dimensions: flags.dimensions,
673
+ fake: flags.fake,
674
+ generate: flags.generate,
675
+ approveWrite: flags.approveWrite,
676
+ });
677
+ output({ ok: true, ...result, message: result.generated ? 'Generated answer with citations' : 'Prepared citation context draft' }, flags.json);
678
+ return;
679
+ }
680
+
616
681
  if (command === 'providers') {
617
682
  const action = positional[1] ?? 'status';
618
683
  if (action === 'status') {
package/src/mcp.js CHANGED
@@ -203,6 +203,43 @@ export function buildServer() {
203
203
  }
204
204
  });
205
205
 
206
+ registerTool(server, 'knowledge_ask', 'Knowledge prompt answer', 'Answer a prompt using read-only knowledge context and optional AI SDK generation', {
207
+ scope: scopeField,
208
+ prompt: z.string().describe('Prompt to answer with the knowledge base'),
209
+ limit: z.number().optional().describe('Maximum context results'),
210
+ semantic: z.boolean().optional().describe('Include vector semantic results'),
211
+ generate: z.boolean().optional().describe('Call AI SDK text generation; omitted returns a local citation draft'),
212
+ approve_write: z.boolean().optional().describe('Record approval intent for future durable wiki writes'),
213
+ model: z.string().optional().describe('Model alias/ref, default configured provider default'),
214
+ dimensions: z.number().optional().describe('Embedding dimensions for deterministic fake mode'),
215
+ fake: z.boolean().optional().describe('Use deterministic fake embeddings/generation for local tests'),
216
+ }, async ({ scope, prompt, limit, semantic, generate, approve_write, model, dimensions, fake }) => {
217
+ const service = createKnowledgeService({ scope });
218
+ try {
219
+ return jsonText({ ok: true, ...await service.runPrompt({ prompt, limit, semantic, generate, approveWrite: approve_write, modelRef: model, dimensions, fake }) });
220
+ } catch (error) {
221
+ return errorText(error instanceof Error ? error.message : String(error));
222
+ }
223
+ });
224
+
225
+ registerTool(server, 'ok_web_search', 'Provider web search', 'Run safety-gated provider-native web search and return citations/sources', {
226
+ scope: scopeField,
227
+ query: z.string().describe('Web search query'),
228
+ limit: z.number().optional().describe('Maximum sources'),
229
+ provider: z.enum(['openai', 'anthropic', 'deepseek']).optional().describe('Provider override'),
230
+ model: z.string().optional().describe('Model alias/ref'),
231
+ domains: z.array(z.string()).optional().describe('Allowed domains'),
232
+ fake: z.boolean().optional().describe('Use deterministic fake web results'),
233
+ file_results: z.boolean().optional().describe('File web snippets as web source refs'),
234
+ }, async ({ scope, query, limit, provider, model, domains, fake, file_results }) => {
235
+ const service = createKnowledgeService({ scope });
236
+ try {
237
+ return jsonText({ ok: true, ...await service.webSearch({ query, limit, provider, modelRef: model, domains, fake, fileResults: file_results }) });
238
+ } catch (error) {
239
+ return errorText(error instanceof Error ? error.message : String(error));
240
+ }
241
+ });
242
+
206
243
  registerTool(server, 'ok_add', 'Add a knowledge item', 'Add a new item to the knowledge store', {
207
244
  title: z.string().describe('Item title'),
208
245
  content: z.string().describe('Item content/body'),
package/src/providers.ts CHANGED
@@ -128,7 +128,7 @@ const BUILTIN_ALIASES: Record<string, string> = {
128
128
  };
129
129
 
130
130
  function providerConfig(config?: KnowledgeConfig): AiProvidersConfig {
131
- return (config as KnowledgeConfig & { providers?: AiProvidersConfig }).providers ?? {};
131
+ return (config as KnowledgeConfig & { providers?: AiProvidersConfig } | undefined)?.providers ?? {};
132
132
  }
133
133
 
134
134
  export function providerSettings(config: KnowledgeConfig | undefined, provider: AiProviderId): AiProviderSettings {
package/src/service.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  import { createArtifactStore } from './artifact-store';
2
+ import { runKnowledgePrompt, type KnowledgePromptOptions } from './agent';
2
3
  import {
3
4
  embeddingIndexStatus,
4
5
  indexKnowledgeEmbeddings,
@@ -15,6 +16,7 @@ import { providerStatus, listModelRegistry, type ProviderStatusResult, type Mode
15
16
  import { retrieveKnowledgeContext, type RetrievalOptions } from './retrieval';
16
17
  import { hybridSearch, type HybridSearchOptions } from './search';
17
18
  import { resolveSafetyPolicy } from './safety';
19
+ import { runProviderWebSearch, type WebSearchOptions } from './web-search';
18
20
  import {
19
21
  recordStorageObjects,
20
22
  resolveStorageContract,
@@ -234,6 +236,25 @@ export class KnowledgeService {
234
236
  config: this.config(),
235
237
  });
236
238
  }
239
+
240
+ async runPrompt(options: Omit<KnowledgePromptOptions, 'dbPath' | 'config'>) {
241
+ const workspace = this.ensureWorkspace();
242
+ return runKnowledgePrompt({
243
+ ...options,
244
+ dbPath: workspace.knowledgeDbPath,
245
+ config: this.config(),
246
+ });
247
+ }
248
+
249
+ async webSearch(options: Omit<WebSearchOptions, 'dbPath' | 'config' | 'safetyPolicy'>) {
250
+ const workspace = this.ensureWorkspace();
251
+ return runProviderWebSearch({
252
+ ...options,
253
+ dbPath: workspace.knowledgeDbPath,
254
+ config: this.config(),
255
+ safetyPolicy: this.safetyPolicy(),
256
+ });
257
+ }
237
258
  }
238
259
 
239
260
  export function createKnowledgeService(options: KnowledgeServiceOptions = {}): KnowledgeService {