@hasna/knowledge 0.2.16 → 0.2.17

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,12 @@ 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
+
252
258
  ## Provider Registry
253
259
 
254
260
  AI provider setup is BYOK and AI SDK v6 based. The local provider layer tracks:
@@ -138,6 +138,9 @@ 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.
141
144
 
142
145
  ## Reindexing
143
146
 
package/package.json CHANGED
@@ -1,9 +1,10 @@
1
1
  {
2
2
  "name": "@hasna/knowledge",
3
- "version": "0.2.16",
3
+ "version": "0.2.17",
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,8 @@ interface Flags {
51
52
  dimensions?: number;
52
53
  semantic?: boolean;
53
54
  context?: boolean;
55
+ generate?: boolean;
56
+ approveWrite?: boolean;
54
57
  noColor?: boolean;
55
58
  scope?: string;
56
59
  olderThan?: number;
@@ -65,12 +68,13 @@ interface ParseResult {
65
68
  flags: Flags;
66
69
  }
67
70
 
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'];
71
+ const COMMANDS = ['add', 'list', 'get', 'delete', 'update', 'archive', 'restore', 'upsert', 'untag', 'export', 'prune', 'dedupe', 'stats', 'paths', 'storage', 'db', 'wiki', 'source', 'ingest', 'reindex', 'search', 'ask', 'build', 'embeddings', 'providers', 'safety', 'help'];
69
72
  const COMMAND_ALIASES: Record<string, string> = {
70
73
  ls: 'list',
71
74
  rm: 'delete',
72
75
  edit: 'update',
73
76
  unarchive: 'restore',
77
+ knowledge: 'ask',
74
78
  };
75
79
 
76
80
  function parseArgs(argv: string[]): ParseResult {
@@ -105,6 +109,8 @@ function parseArgs(argv: string[]): ParseResult {
105
109
  case '--dimensions': flags.dimensions = Number(argv[i + 1]); i += 1; break;
106
110
  case '--semantic': flags.semantic = true; break;
107
111
  case '--context': flags.context = true; break;
112
+ case '--generate': flags.generate = true; break;
113
+ case '--approve-write': flags.approveWrite = true; break;
108
114
  case '--fake': flags.fake = true; break;
109
115
  case '--no-color': flags.noColor = true; break;
110
116
  case '--scope': flags.scope = argv[i + 1]; i += 1; break;
@@ -151,6 +157,10 @@ function suggestCommand(input: string): string {
151
157
  return bestScore <= 3 ? best : '';
152
158
  }
153
159
 
160
+ function invokedAsKnowledge(): boolean {
161
+ return basename(process.argv[1] ?? '') === 'knowledge';
162
+ }
163
+
154
164
  function printGlobalHelp(): void {
155
165
  console.log(`open-knowledge - local agent knowledge store
156
166
 
@@ -180,6 +190,7 @@ Commands:
180
190
  ingest source <source-ref> Ingest a read-only source ref into knowledge.db
181
191
  reindex outbox <file|s3://> Consume open-files change events and invalidate chunks
182
192
  search <query> Hybrid search sources, wiki pages, indexes, or context
193
+ ask|build <prompt> Build a read-only citation answer/context pack
183
194
  embeddings status|index|search Build/query local vector embeddings
184
195
  providers status|models|check Inspect AI SDK provider config and credentials
185
196
  safety status|check|approve|audit|redact
@@ -193,6 +204,8 @@ Global Options:
193
204
  --dimensions <n> Embedding dimensions for local/fake providers
194
205
  --semantic Include vector semantic results in search
195
206
  --context Return a reranked citation context pack for search
207
+ --generate Call AI SDK text generation for ask/build
208
+ --approve-write Record approval intent for future durable wiki writes
196
209
  --fake Use deterministic fake embeddings for local tests
197
210
  --scope local|global|project Store scope (default: global ~/.hasna/apps/knowledge/)
198
211
  --no-color Disable color output
@@ -255,6 +268,7 @@ function printCommandHelp(command: string): void {
255
268
  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
269
  if (command === 'reindex') { console.log('Usage: open-knowledge reindex outbox <file|s3://bucket/key> [--scope local|global|project] [--json]'); return; }
257
270
  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; }
271
+ 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
272
  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
273
  if (command === 'providers') { console.log('Usage: open-knowledge providers status|models|check [provider|model-alias] [--scope local|global|project] [--json]'); return; }
260
274
  if (command === 'safety') { console.log('Usage: open-knowledge safety status|check|approve|audit|redact [args] [--scope local|global|project] [--json]'); return; }
@@ -302,18 +316,23 @@ async function run(argv: string[]): Promise<void> {
302
316
  if (flags.completions) {
303
317
  const shell = flags.completions;
304
318
  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`);
319
+ 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 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 --fake --no-color --scope --archived --include-archived" -- "$cur")); }; complete -F _open_knowledge open-knowledge`);
306
320
  } 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`);
321
+ 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 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" "(--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`);
308
322
  } 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"`);
323
+ 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 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 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
324
  } else {
311
325
  throw new Error("Invalid --completions value. Use 'bash', 'zsh', or 'fish'.");
312
326
  }
313
327
  return;
314
328
  }
315
329
 
316
- const command = resolveCommand(positional[0]);
330
+ let command = resolveCommand(positional[0]);
331
+ let commandArgOffset = 1;
332
+ if (invokedAsKnowledge() && command && !COMMANDS.includes(command)) {
333
+ command = 'ask';
334
+ commandArgOffset = 0;
335
+ }
317
336
 
318
337
  if (!command || flags.help || command === 'help') { printCommandHelp(positional[1]); return; }
319
338
 
@@ -613,6 +632,23 @@ async function run(argv: string[]): Promise<void> {
613
632
  return;
614
633
  }
615
634
 
635
+ if (command === 'ask' || command === 'build') {
636
+ const prompt = positional.slice(commandArgOffset).join(' ');
637
+ if (!prompt) throw new Error('Usage: open-knowledge ask <prompt>');
638
+ const result = await service.runPrompt({
639
+ prompt,
640
+ limit: flags.limit,
641
+ semantic: flags.semantic,
642
+ modelRef: flags.model,
643
+ dimensions: flags.dimensions,
644
+ fake: flags.fake,
645
+ generate: flags.generate,
646
+ approveWrite: flags.approveWrite,
647
+ });
648
+ output({ ok: true, ...result, message: result.generated ? 'Generated answer with citations' : 'Prepared citation context draft' }, flags.json);
649
+ return;
650
+ }
651
+
616
652
  if (command === 'providers') {
617
653
  const action = positional[1] ?? 'status';
618
654
  if (action === 'status') {
package/src/mcp.js CHANGED
@@ -203,6 +203,25 @@ 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
+
206
225
  registerTool(server, 'ok_add', 'Add a knowledge item', 'Add a new item to the knowledge store', {
207
226
  title: z.string().describe('Item title'),
208
227
  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,
@@ -234,6 +235,15 @@ export class KnowledgeService {
234
235
  config: this.config(),
235
236
  });
236
237
  }
238
+
239
+ async runPrompt(options: Omit<KnowledgePromptOptions, 'dbPath' | 'config'>) {
240
+ const workspace = this.ensureWorkspace();
241
+ return runKnowledgePrompt({
242
+ ...options,
243
+ dbPath: workspace.knowledgeDbPath,
244
+ config: this.config(),
245
+ });
246
+ }
237
247
  }
238
248
 
239
249
  export function createKnowledgeService(options: KnowledgeServiceOptions = {}): KnowledgeService {