@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.
- package/README.md +35 -1
- package/bin/open-knowledge-mcp.js +2553 -1887
- package/bin/open-knowledge.js +104 -83
- package/docs/architecture/ai-native-knowledge-base.md +13 -0
- package/docs/architecture/hybrid-semantic-search.md +8 -0
- package/package.json +2 -1
- package/src/agent.ts +367 -0
- package/src/cli.ts +70 -5
- package/src/mcp.js +37 -0
- package/src/providers.ts +1 -1
- package/src/service.ts +21 -0
- package/src/web-search.ts +330 -0
|
@@ -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.
|
|
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
|
-
|
|
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 })
|
|
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 {
|