@hasna/knowledge 0.2.27 → 0.2.28
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 +41 -0
- package/bin/open-knowledge-mcp.js +15 -7
- package/bin/open-knowledge.js +17 -17
- package/dist/agent.d.ts +35 -0
- package/dist/artifact-store.d.ts +63 -0
- package/dist/auth.d.ts +35 -0
- package/dist/embeddings.d.ts +77 -0
- package/dist/index.d.ts +20 -0
- package/dist/index.js +5709 -0
- package/dist/knowledge-db.d.ts +27 -0
- package/dist/manifest-ingest.d.ts +35 -0
- package/dist/outbox-consume.d.ts +25 -0
- package/dist/provenance.d.ts +50 -0
- package/dist/providers.d.ts +89 -0
- package/dist/reindex.d.ts +37 -0
- package/dist/remote-client.d.ts +108 -0
- package/dist/retrieval.d.ts +71 -0
- package/dist/safety.d.ts +70 -0
- package/dist/sdk.d.ts +72 -0
- package/dist/search.d.ts +65 -0
- package/dist/service.d.ts +117 -0
- package/dist/source-ingest.d.ts +18 -0
- package/dist/source-ref.d.ts +30 -0
- package/dist/source-resolver.d.ts +92 -0
- package/dist/storage-contract.d.ts +106 -0
- package/dist/web-search.d.ts +40 -0
- package/dist/wiki-compiler.d.ts +67 -0
- package/dist/wiki-layout.d.ts +23 -0
- package/dist/workspace.d.ts +111 -0
- package/package.json +15 -7
- package/src/agent.ts +0 -367
- package/src/artifact-store.ts +0 -184
- package/src/auth.ts +0 -123
- package/src/cli.ts +0 -1184
- package/src/embeddings.ts +0 -516
- package/src/knowledge-db.ts +0 -354
- package/src/manifest-ingest.ts +0 -515
- package/src/mcp-http.js +0 -110
- package/src/mcp.js +0 -1503
- package/src/outbox-consume.ts +0 -463
- package/src/provenance.ts +0 -93
- package/src/providers.ts +0 -308
- package/src/reindex.ts +0 -260
- package/src/remote-client.ts +0 -268
- package/src/retrieval.ts +0 -326
- package/src/safety.ts +0 -265
- package/src/schema.js +0 -25
- package/src/search.ts +0 -510
- package/src/service.ts +0 -443
- package/src/source-ingest.ts +0 -268
- package/src/source-ref.ts +0 -104
- package/src/source-resolver.ts +0 -436
- package/src/storage-contract.ts +0 -346
- package/src/store.ts +0 -113
- package/src/web-search.ts +0 -330
- package/src/wiki-compiler.ts +0 -711
- package/src/wiki-layout.ts +0 -251
- package/src/workspace.ts +0 -251
package/src/agent.ts
DELETED
|
@@ -1,367 +0,0 @@
|
|
|
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/artifact-store.ts
DELETED
|
@@ -1,184 +0,0 @@
|
|
|
1
|
-
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
2
|
-
import { dirname, join, relative, sep } from 'node:path';
|
|
3
|
-
import type { KnowledgeConfig, KnowledgeWorkspace } from './workspace';
|
|
4
|
-
|
|
5
|
-
interface S3ClientLike {
|
|
6
|
-
send(command: unknown): Promise<any>;
|
|
7
|
-
}
|
|
8
|
-
|
|
9
|
-
export interface ArtifactWrite {
|
|
10
|
-
key: string;
|
|
11
|
-
body: string | Uint8Array;
|
|
12
|
-
content_type?: string;
|
|
13
|
-
metadata?: Record<string, string>;
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
export interface ArtifactStore {
|
|
17
|
-
readonly type: 'local' | 's3';
|
|
18
|
-
readonly canRead: boolean;
|
|
19
|
-
readonly canWrite: boolean;
|
|
20
|
-
put(entry: ArtifactWrite): Promise<{ key: string; uri: string }>;
|
|
21
|
-
getText(key: string): Promise<string>;
|
|
22
|
-
exists(key: string): Promise<boolean>;
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
export function normalizeArtifactKey(key: string): string {
|
|
26
|
-
const raw = key.replace(/\\/g, '/').trim();
|
|
27
|
-
if (!raw || raw.startsWith('/')) {
|
|
28
|
-
throw new Error(`Invalid artifact key: ${key}`);
|
|
29
|
-
}
|
|
30
|
-
const segments = raw.split('/').filter(Boolean);
|
|
31
|
-
if (segments.length === 0 || segments.some((segment) => segment === '.' || segment === '..')) {
|
|
32
|
-
throw new Error(`Invalid artifact key: ${key}`);
|
|
33
|
-
}
|
|
34
|
-
return segments.join('/');
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
function assertInside(root: string, target: string): void {
|
|
38
|
-
const rel = relative(root, target);
|
|
39
|
-
if (rel.startsWith('..') || rel === '..' || rel.startsWith(`..${sep}`)) {
|
|
40
|
-
throw new Error(`Artifact path escapes root: ${target}`);
|
|
41
|
-
}
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
export class LocalArtifactStore implements ArtifactStore {
|
|
45
|
-
readonly type = 'local' as const;
|
|
46
|
-
readonly canRead = true;
|
|
47
|
-
readonly canWrite = true;
|
|
48
|
-
|
|
49
|
-
constructor(private readonly root: string) {
|
|
50
|
-
mkdirSync(root, { recursive: true });
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
async put(entry: ArtifactWrite): Promise<{ key: string; uri: string }> {
|
|
54
|
-
const key = normalizeArtifactKey(entry.key);
|
|
55
|
-
const path = join(this.root, key);
|
|
56
|
-
assertInside(this.root, path);
|
|
57
|
-
mkdirSync(dirname(path), { recursive: true });
|
|
58
|
-
writeFileSync(path, entry.body);
|
|
59
|
-
return { key, uri: `file://${path}` };
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
async getText(key: string): Promise<string> {
|
|
63
|
-
const normalizedKey = normalizeArtifactKey(key);
|
|
64
|
-
const path = join(this.root, normalizedKey);
|
|
65
|
-
assertInside(this.root, path);
|
|
66
|
-
return readFileSync(path, 'utf8');
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
async exists(key: string): Promise<boolean> {
|
|
70
|
-
const normalizedKey = normalizeArtifactKey(key);
|
|
71
|
-
const path = join(this.root, normalizedKey);
|
|
72
|
-
assertInside(this.root, path);
|
|
73
|
-
return existsSync(path);
|
|
74
|
-
}
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
export interface S3ArtifactStoreOptions {
|
|
78
|
-
bucket: string;
|
|
79
|
-
prefix?: string;
|
|
80
|
-
region?: string;
|
|
81
|
-
profile?: string;
|
|
82
|
-
max_attempts?: number;
|
|
83
|
-
server_side_encryption?: 'AES256' | 'aws:kms';
|
|
84
|
-
kms_key_id?: string;
|
|
85
|
-
client?: S3ClientLike;
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
export class S3ArtifactStore implements ArtifactStore {
|
|
89
|
-
readonly type = 's3' as const;
|
|
90
|
-
readonly canRead = true;
|
|
91
|
-
readonly canWrite = true;
|
|
92
|
-
private client?: S3ClientLike;
|
|
93
|
-
|
|
94
|
-
constructor(private readonly options: S3ArtifactStoreOptions) {
|
|
95
|
-
this.client = options.client;
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
private async getClient(): Promise<S3ClientLike> {
|
|
99
|
-
if (this.client) return this.client;
|
|
100
|
-
const [{ S3Client }, { fromIni }] = await Promise.all([
|
|
101
|
-
import('@aws-sdk/client-s3'),
|
|
102
|
-
import('@aws-sdk/credential-providers'),
|
|
103
|
-
]);
|
|
104
|
-
this.client = new S3Client({
|
|
105
|
-
region: this.options.region,
|
|
106
|
-
credentials: this.options.profile ? fromIni({ profile: this.options.profile }) : undefined,
|
|
107
|
-
maxAttempts: this.options.max_attempts,
|
|
108
|
-
});
|
|
109
|
-
return this.client;
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
private objectKey(key: string): string {
|
|
113
|
-
const normalizedKey = normalizeArtifactKey(key);
|
|
114
|
-
const prefix = this.options.prefix ? normalizeArtifactKey(this.options.prefix) : '';
|
|
115
|
-
return prefix ? `${prefix}/${normalizedKey}` : normalizedKey;
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
async put(entry: ArtifactWrite): Promise<{ key: string; uri: string }> {
|
|
119
|
-
const [{ PutObjectCommand }, client] = await Promise.all([
|
|
120
|
-
import('@aws-sdk/client-s3'),
|
|
121
|
-
this.getClient(),
|
|
122
|
-
]);
|
|
123
|
-
const key = this.objectKey(entry.key);
|
|
124
|
-
await client.send(new PutObjectCommand({
|
|
125
|
-
Bucket: this.options.bucket,
|
|
126
|
-
Key: key,
|
|
127
|
-
Body: entry.body,
|
|
128
|
-
ContentType: entry.content_type,
|
|
129
|
-
Metadata: entry.metadata,
|
|
130
|
-
ServerSideEncryption: this.options.server_side_encryption,
|
|
131
|
-
SSEKMSKeyId: this.options.kms_key_id,
|
|
132
|
-
}));
|
|
133
|
-
return { key, uri: `s3://${this.options.bucket}/${key}` };
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
async getText(key: string): Promise<string> {
|
|
137
|
-
const [{ GetObjectCommand }, client] = await Promise.all([
|
|
138
|
-
import('@aws-sdk/client-s3'),
|
|
139
|
-
this.getClient(),
|
|
140
|
-
]);
|
|
141
|
-
const objectKey = this.objectKey(key);
|
|
142
|
-
const response = await client.send(new GetObjectCommand({
|
|
143
|
-
Bucket: this.options.bucket,
|
|
144
|
-
Key: objectKey,
|
|
145
|
-
}));
|
|
146
|
-
if (!response.Body) return '';
|
|
147
|
-
return await response.Body.transformToString();
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
async exists(key: string): Promise<boolean> {
|
|
151
|
-
const [{ HeadObjectCommand }, client] = await Promise.all([
|
|
152
|
-
import('@aws-sdk/client-s3'),
|
|
153
|
-
this.getClient(),
|
|
154
|
-
]);
|
|
155
|
-
const objectKey = this.objectKey(key);
|
|
156
|
-
try {
|
|
157
|
-
await client.send(new HeadObjectCommand({
|
|
158
|
-
Bucket: this.options.bucket,
|
|
159
|
-
Key: objectKey,
|
|
160
|
-
}));
|
|
161
|
-
return true;
|
|
162
|
-
} catch (error) {
|
|
163
|
-
const name = error instanceof Error ? error.name : '';
|
|
164
|
-
if (name === 'NotFound' || name === 'NoSuchKey' || name === 'NotFoundError') return false;
|
|
165
|
-
throw error;
|
|
166
|
-
}
|
|
167
|
-
}
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
export function createArtifactStore(config: KnowledgeConfig, workspace: KnowledgeWorkspace): ArtifactStore {
|
|
171
|
-
if (config.storage.type === 's3') {
|
|
172
|
-
if (!config.storage.s3?.bucket) throw new Error('S3 artifact storage requires storage.s3.bucket');
|
|
173
|
-
return new S3ArtifactStore({
|
|
174
|
-
bucket: config.storage.s3.bucket,
|
|
175
|
-
prefix: config.storage.s3.prefix,
|
|
176
|
-
region: config.storage.s3.region,
|
|
177
|
-
profile: config.storage.s3.profile,
|
|
178
|
-
max_attempts: config.storage.s3.max_attempts,
|
|
179
|
-
server_side_encryption: config.storage.s3.server_side_encryption,
|
|
180
|
-
kms_key_id: config.storage.s3.kms_key_id,
|
|
181
|
-
});
|
|
182
|
-
}
|
|
183
|
-
return new LocalArtifactStore(workspace.artifactsDir);
|
|
184
|
-
}
|
package/src/auth.ts
DELETED
|
@@ -1,123 +0,0 @@
|
|
|
1
|
-
import { existsSync, mkdirSync, readFileSync, unlinkSync, writeFileSync } from 'node:fs';
|
|
2
|
-
import { homedir } from 'node:os';
|
|
3
|
-
import { dirname, join } from 'node:path';
|
|
4
|
-
import type { KnowledgeConfig } from './workspace';
|
|
5
|
-
|
|
6
|
-
export interface KnowledgeAuthConfig {
|
|
7
|
-
api_key: string;
|
|
8
|
-
email?: string;
|
|
9
|
-
org_id?: string;
|
|
10
|
-
org_slug?: string;
|
|
11
|
-
user_id?: string;
|
|
12
|
-
api_url?: string;
|
|
13
|
-
created_at: string;
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
export interface KnowledgeAuthStatus {
|
|
17
|
-
authenticated: boolean;
|
|
18
|
-
source: 'env' | 'file' | 'none';
|
|
19
|
-
api_url: string;
|
|
20
|
-
auth_path: string;
|
|
21
|
-
email: string | null;
|
|
22
|
-
org_id: string | null;
|
|
23
|
-
org_slug: string | null;
|
|
24
|
-
user_id: string | null;
|
|
25
|
-
api_key_present: boolean;
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
export const DEFAULT_KNOWLEDGE_API_URL = 'https://knowledge.hasna.xyz';
|
|
29
|
-
|
|
30
|
-
export function normalizeKnowledgeApiOrigin(apiUrl: string): string {
|
|
31
|
-
const url = new URL(apiUrl);
|
|
32
|
-
if (url.protocol !== 'http:' && url.protocol !== 'https:') {
|
|
33
|
-
throw new Error('Knowledge API URL must use http or https.');
|
|
34
|
-
}
|
|
35
|
-
const pathname = url.pathname.replace(/\/+$/, '');
|
|
36
|
-
if (pathname === '/api' || pathname === '/api/v1') {
|
|
37
|
-
url.pathname = '/';
|
|
38
|
-
} else if (pathname.endsWith('/api/v1')) {
|
|
39
|
-
url.pathname = pathname.slice(0, -'/api/v1'.length) || '/';
|
|
40
|
-
} else if (pathname.endsWith('/api')) {
|
|
41
|
-
url.pathname = pathname.slice(0, -'/api'.length) || '/';
|
|
42
|
-
}
|
|
43
|
-
return url.toString().replace(/\/+$/, '');
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
export function knowledgeAuthPath(env: Record<string, string | undefined> = process.env): string {
|
|
47
|
-
if (env.HASNA_KNOWLEDGE_AUTH_PATH) return env.HASNA_KNOWLEDGE_AUTH_PATH;
|
|
48
|
-
const root = env.HASNA_KNOWLEDGE_AUTH_DIR ?? join(homedir(), '.hasna', 'knowledge');
|
|
49
|
-
return join(root, 'auth.json');
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
export function resolveKnowledgeApiUrl(
|
|
53
|
-
config?: KnowledgeConfig,
|
|
54
|
-
env: Record<string, string | undefined> = process.env,
|
|
55
|
-
): string {
|
|
56
|
-
return normalizeKnowledgeApiOrigin(env.KNOWLEDGE_API_URL ?? config?.hosted?.api_url ?? DEFAULT_KNOWLEDGE_API_URL);
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
export function getKnowledgeAuth(env: Record<string, string | undefined> = process.env): KnowledgeAuthConfig | null {
|
|
60
|
-
try {
|
|
61
|
-
const path = knowledgeAuthPath(env);
|
|
62
|
-
if (!existsSync(path)) return null;
|
|
63
|
-
const parsed = JSON.parse(readFileSync(path, 'utf8')) as KnowledgeAuthConfig;
|
|
64
|
-
return typeof parsed.api_key === 'string' && parsed.api_key.length > 0 ? parsed : null;
|
|
65
|
-
} catch {
|
|
66
|
-
return null;
|
|
67
|
-
}
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
export function saveKnowledgeAuth(
|
|
71
|
-
auth: Omit<KnowledgeAuthConfig, 'created_at'> & { created_at?: string },
|
|
72
|
-
env: Record<string, string | undefined> = process.env,
|
|
73
|
-
): KnowledgeAuthConfig {
|
|
74
|
-
const path = knowledgeAuthPath(env);
|
|
75
|
-
const stored: KnowledgeAuthConfig = {
|
|
76
|
-
...auth,
|
|
77
|
-
api_url: auth.api_url ? normalizeKnowledgeApiOrigin(auth.api_url) : undefined,
|
|
78
|
-
created_at: auth.created_at ?? new Date().toISOString(),
|
|
79
|
-
};
|
|
80
|
-
mkdirSync(dirname(path), { recursive: true, mode: 0o700 });
|
|
81
|
-
writeFileSync(path, `${JSON.stringify(stored, null, 2)}\n`, { mode: 0o600 });
|
|
82
|
-
return stored;
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
export function clearKnowledgeAuth(env: Record<string, string | undefined> = process.env): boolean {
|
|
86
|
-
try {
|
|
87
|
-
unlinkSync(knowledgeAuthPath(env));
|
|
88
|
-
return true;
|
|
89
|
-
} catch {
|
|
90
|
-
return false;
|
|
91
|
-
}
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
export function getKnowledgeApiKey(env: Record<string, string | undefined> = process.env): { apiKey: string | null; source: KnowledgeAuthStatus['source'] } {
|
|
95
|
-
if (env.KNOWLEDGE_API_KEY) return { apiKey: env.KNOWLEDGE_API_KEY, source: 'env' };
|
|
96
|
-
if (env.HASNA_KNOWLEDGE_API_KEY) return { apiKey: env.HASNA_KNOWLEDGE_API_KEY, source: 'env' };
|
|
97
|
-
const auth = getKnowledgeAuth(env);
|
|
98
|
-
return auth?.api_key ? { apiKey: auth.api_key, source: 'file' } : { apiKey: null, source: 'none' };
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
export function knowledgeAuthStatus(
|
|
102
|
-
config?: KnowledgeConfig,
|
|
103
|
-
env: Record<string, string | undefined> = process.env,
|
|
104
|
-
): KnowledgeAuthStatus {
|
|
105
|
-
const auth = getKnowledgeAuth(env);
|
|
106
|
-
const key = getKnowledgeApiKey(env);
|
|
107
|
-
const apiUrl = env.KNOWLEDGE_API_URL
|
|
108
|
-
? resolveKnowledgeApiUrl(config, env)
|
|
109
|
-
: auth?.api_url
|
|
110
|
-
? normalizeKnowledgeApiOrigin(auth.api_url)
|
|
111
|
-
: resolveKnowledgeApiUrl(config, env);
|
|
112
|
-
return {
|
|
113
|
-
authenticated: Boolean(key.apiKey),
|
|
114
|
-
source: key.source,
|
|
115
|
-
api_url: apiUrl,
|
|
116
|
-
auth_path: knowledgeAuthPath(env),
|
|
117
|
-
email: key.source === 'file' ? auth?.email ?? null : null,
|
|
118
|
-
org_id: key.source === 'file' ? auth?.org_id ?? null : null,
|
|
119
|
-
org_slug: key.source === 'file' ? auth?.org_slug ?? null : null,
|
|
120
|
-
user_id: key.source === 'file' ? auth?.user_id ?? null : null,
|
|
121
|
-
api_key_present: Boolean(key.apiKey),
|
|
122
|
-
};
|
|
123
|
-
}
|