@hasna/knowledge 0.2.20 → 0.2.22
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 +38 -0
- package/bin/open-knowledge-mcp.js +927 -24
- package/bin/open-knowledge.js +153 -71
- package/docs/architecture/ai-native-knowledge-base.md +16 -0
- package/package.json +1 -1
- package/src/auth.ts +123 -0
- package/src/cli.ts +153 -10
- package/src/remote-client.ts +268 -0
- package/src/service.ts +142 -0
- package/src/storage-contract.ts +28 -0
- package/src/wiki-compiler.ts +711 -0
- package/src/workspace.ts +11 -0
|
@@ -43,6 +43,14 @@ The future hosted/SaaS wrapper owns:
|
|
|
43
43
|
The OSS package must stay useful without a hosted account. Hosted mode should be
|
|
44
44
|
an optional remote client over explicit API contracts.
|
|
45
45
|
|
|
46
|
+
The local hosted-aware contract follows the `open-skills` pattern: `mode` is
|
|
47
|
+
`local` by default, `setup --mode hosted` records `hosted.api_url`, env vars
|
|
48
|
+
`KNOWLEDGE_API_URL` and `KNOWLEDGE_API_KEY` can override local config, and
|
|
49
|
+
credentials live outside project state in `~/.hasna/knowledge/auth.json`.
|
|
50
|
+
`remote contracts` publishes the registry/search/ask/build/sync/status/logs and
|
|
51
|
+
artifact endpoints that a SaaS wrapper can implement. Local use, local search,
|
|
52
|
+
and local artifact generation do not require this remote API.
|
|
53
|
+
|
|
46
54
|
## Local Workspace
|
|
47
55
|
|
|
48
56
|
Project-local state lives at:
|
|
@@ -199,6 +207,14 @@ The database catalog tracks every schema, index shard, log partition, wiki page,
|
|
|
199
207
|
source citation, and generated artifact. Markdown remains the readable layer;
|
|
200
208
|
SQLite/Postgres and object storage carry the scalable catalog.
|
|
201
209
|
|
|
210
|
+
The first compile/write loop is local and approval-gated. `wiki compile`
|
|
211
|
+
generates cited pages from derived source chunks, creates concept backlinks,
|
|
212
|
+
updates index rows, records storage objects, and appends dated JSONL logs.
|
|
213
|
+
`wiki file-answer` writes answer notes only with `--approve-write`; otherwise it
|
|
214
|
+
returns the dry-run proposal. `wiki lint` checks missing/stale citations,
|
|
215
|
+
duplicates, orphan pages, unresolved source refs, contradiction markers, and
|
|
216
|
+
new-article candidates.
|
|
217
|
+
|
|
202
218
|
## Search Model
|
|
203
219
|
|
|
204
220
|
Search is hybrid:
|
package/package.json
CHANGED
package/src/auth.ts
ADDED
|
@@ -0,0 +1,123 @@
|
|
|
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
|
+
}
|
package/src/cli.ts
CHANGED
|
@@ -55,6 +55,13 @@ interface Flags {
|
|
|
55
55
|
generate?: boolean;
|
|
56
56
|
approveWrite?: boolean;
|
|
57
57
|
provider?: string;
|
|
58
|
+
mode?: string;
|
|
59
|
+
apiUrl?: string;
|
|
60
|
+
apiKey?: string;
|
|
61
|
+
email?: string;
|
|
62
|
+
org?: string;
|
|
63
|
+
orgId?: string;
|
|
64
|
+
userId?: string;
|
|
58
65
|
domain?: string[];
|
|
59
66
|
fileResults?: boolean;
|
|
60
67
|
full?: boolean;
|
|
@@ -72,7 +79,7 @@ interface ParseResult {
|
|
|
72
79
|
flags: Flags;
|
|
73
80
|
}
|
|
74
81
|
|
|
75
|
-
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'];
|
|
82
|
+
const COMMANDS = ['add', 'list', 'get', 'delete', 'update', 'archive', 'restore', 'upsert', 'untag', 'export', 'prune', 'dedupe', 'stats', 'paths', 'setup', 'auth', 'remote', 'storage', 'db', 'wiki', 'source', 'ingest', 'reindex', 'search', 'web', 'ask', 'build', 'embeddings', 'providers', 'safety', 'help'];
|
|
76
83
|
const COMMAND_ALIASES: Record<string, string> = {
|
|
77
84
|
ls: 'list',
|
|
78
85
|
rm: 'delete',
|
|
@@ -116,6 +123,13 @@ function parseArgs(argv: string[]): ParseResult {
|
|
|
116
123
|
case '--generate': flags.generate = true; break;
|
|
117
124
|
case '--approve-write': flags.approveWrite = true; break;
|
|
118
125
|
case '--provider': flags.provider = argv[i + 1]; i += 1; break;
|
|
126
|
+
case '--mode': flags.mode = argv[i + 1]; i += 1; break;
|
|
127
|
+
case '--api-url': flags.apiUrl = argv[i + 1]; i += 1; break;
|
|
128
|
+
case '--api-key': flags.apiKey = argv[i + 1]; i += 1; break;
|
|
129
|
+
case '--email': flags.email = argv[i + 1]; i += 1; break;
|
|
130
|
+
case '--org': flags.org = argv[i + 1]; i += 1; break;
|
|
131
|
+
case '--org-id': flags.orgId = argv[i + 1]; i += 1; break;
|
|
132
|
+
case '--user-id': flags.userId = argv[i + 1]; i += 1; break;
|
|
119
133
|
case '--domain': flags.domain = [...(flags.domain ?? []), argv[i + 1]]; i += 1; break;
|
|
120
134
|
case '--file-results': flags.fileResults = true; break;
|
|
121
135
|
case '--full': flags.full = true; break;
|
|
@@ -190,9 +204,13 @@ Commands:
|
|
|
190
204
|
dedupe Remove duplicate items by title+content (requires --yes)
|
|
191
205
|
stats Show knowledge base statistics
|
|
192
206
|
paths Show resolved workspace/store paths
|
|
207
|
+
setup Configure local or hosted mode
|
|
208
|
+
auth login|whoami|logout Manage hosted API credentials
|
|
209
|
+
remote contracts|status Inspect hosted client contracts/readiness
|
|
193
210
|
storage status|validate Inspect local/S3 artifact storage contract
|
|
194
211
|
db init|stats Initialize or inspect local knowledge.db
|
|
195
|
-
wiki init
|
|
212
|
+
wiki init|compile|file-answer|lint
|
|
213
|
+
Initialize, compile, file, or lint wiki artifacts
|
|
196
214
|
source resolve <source-ref> Resolve read-only source content and citation evidence
|
|
197
215
|
ingest manifest <file|s3://> Ingest an open-files manifest into knowledge.db
|
|
198
216
|
ingest source <source-ref> Ingest a read-only source ref into knowledge.db
|
|
@@ -216,6 +234,13 @@ Global Options:
|
|
|
216
234
|
--generate Call AI SDK text generation for ask/build
|
|
217
235
|
--approve-write Record approval intent for future durable wiki writes
|
|
218
236
|
--provider <name> Provider override for web search
|
|
237
|
+
--mode local|hosted Configure OSS local or hosted-aware mode
|
|
238
|
+
--api-url <url> Hosted API origin (or KNOWLEDGE_API_URL)
|
|
239
|
+
--api-key <key> Hosted API key for auth login
|
|
240
|
+
--email <email> Hosted account email metadata
|
|
241
|
+
--org <slug> Hosted organization slug metadata
|
|
242
|
+
--org-id <id> Hosted organization id metadata
|
|
243
|
+
--user-id <id> Hosted user id metadata
|
|
219
244
|
--domain <domain> Restrict provider web search to a domain
|
|
220
245
|
--file-results File web snippets as web source refs
|
|
221
246
|
--full Force full embedding index rebuild
|
|
@@ -274,9 +299,12 @@ function printCommandHelp(command: string): void {
|
|
|
274
299
|
if (command === 'dedupe') { console.log('Usage: open-knowledge dedupe --yes [--json]'); return; }
|
|
275
300
|
if (command === 'stats') { console.log('Usage: open-knowledge stats [--json]'); return; }
|
|
276
301
|
if (command === 'paths') { console.log('Usage: open-knowledge paths [--scope local|global|project] [--json]'); return; }
|
|
302
|
+
if (command === 'setup') { console.log('Usage: open-knowledge setup --mode local|hosted [--api-url https://...] [--scope local|global|project] [--json]'); return; }
|
|
303
|
+
if (command === 'auth') { console.log('Usage: open-knowledge auth login|whoami|logout [--api-key <key>] [--email <email>] [--org <slug>] [--api-url https://...] [--scope local|global|project] [--json]'); return; }
|
|
304
|
+
if (command === 'remote') { console.log('Usage: open-knowledge remote contracts|status [--scope local|global|project] [--json]'); return; }
|
|
277
305
|
if (command === 'storage') { console.log('Usage: open-knowledge storage status|validate [--scope local|global|project] [--json]'); return; }
|
|
278
306
|
if (command === 'db') { console.log('Usage: open-knowledge db init|stats [--scope local|global|project] [--json]'); return; }
|
|
279
|
-
if (command === 'wiki') { console.log('Usage: open-knowledge wiki init [--scope local|global|project] [--json]'); return; }
|
|
307
|
+
if (command === 'wiki') { console.log('Usage: open-knowledge wiki init|compile|file-answer|lint [query|prompt] [--title <title>] [--content <answer>] [--approve-write] [--limit <n>] [--scope local|global|project] [--json]'); return; }
|
|
280
308
|
if (command === 'source') { console.log('Usage: open-knowledge source resolve <source-ref> [--purpose knowledge_answer|knowledge_index] [--limit <n>] [--scope local|global|project] [--json]'); return; }
|
|
281
309
|
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; }
|
|
282
310
|
if (command === 'reindex') { console.log('Usage: open-knowledge reindex status|enqueue|embeddings|outbox [file|s3://bucket/key] [--full] [--fake] [--scope local|global|project] [--json]'); return; }
|
|
@@ -330,11 +358,11 @@ async function run(argv: string[]): Promise<void> {
|
|
|
330
358
|
if (flags.completions) {
|
|
331
359
|
const shell = flags.completions;
|
|
332
360
|
if (shell === 'bash') {
|
|
333
|
-
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 --full --fake --no-color --scope --archived --include-archived" -- "$cur")); }; complete -F _open_knowledge open-knowledge`);
|
|
361
|
+
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 setup auth remote 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 --mode --api-url --api-key --email --org --org-id --user-id --domain --file-results --full --fake --no-color --scope --archived --include-archived" -- "$cur")); }; complete -F _open_knowledge open-knowledge`);
|
|
334
362
|
} else if (shell === 'zsh') {
|
|
335
|
-
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" "(--full)--full" "(--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`);
|
|
363
|
+
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 setup auth remote 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" "(--full)--full" "(--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]:" "(--mode)--mode"\{local,hosted\}:" "(--api-url)--api-url[hosted API URL]:" "(--api-key)--api-key[hosted API key]:" "(--email)--email[email]:" "(--org)--org[org slug]:" "(--org-id)--org-id[org id]:" "(--user-id)--user-id[user id]:" "(--domain)--domain[domain]:" "(--no-color)--no-color[disable color]" "(--scope)--scope"\{local,global,project\}:" }; _open_knowledge`);
|
|
336
364
|
} else if (shell === 'fish') {
|
|
337
|
-
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 full; 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"`);
|
|
365
|
+
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 setup auth remote 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 mode; complete -c open-knowledge -l api-url; complete -c open-knowledge -l api-key; complete -c open-knowledge -l email; complete -c open-knowledge -l org; complete -c open-knowledge -l org-id; complete -c open-knowledge -l user-id; complete -c open-knowledge -l domain; complete -c open-knowledge -l file-results; complete -c open-knowledge -l full; 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"`);
|
|
338
366
|
} else {
|
|
339
367
|
throw new Error("Invalid --completions value. Use 'bash', 'zsh', or 'fish'.");
|
|
340
368
|
}
|
|
@@ -365,6 +393,84 @@ async function run(argv: string[]): Promise<void> {
|
|
|
365
393
|
return;
|
|
366
394
|
}
|
|
367
395
|
|
|
396
|
+
if (command === 'setup') {
|
|
397
|
+
const result = service.setup({
|
|
398
|
+
mode: flags.mode,
|
|
399
|
+
apiUrl: flags.apiUrl,
|
|
400
|
+
});
|
|
401
|
+
output(result, flags.json);
|
|
402
|
+
return;
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
if (command === 'auth') {
|
|
406
|
+
const action = positional[1] ?? 'whoami';
|
|
407
|
+
if (action === 'whoami' || action === 'status') {
|
|
408
|
+
const result = service.authStatus(process.env);
|
|
409
|
+
output({ ok: true, ...result, message: result.authenticated ? `Authenticated via ${result.source}` : 'Not authenticated' }, flags.json);
|
|
410
|
+
return;
|
|
411
|
+
}
|
|
412
|
+
if (action === 'login') {
|
|
413
|
+
const apiKey = flags.apiKey ?? process.env.KNOWLEDGE_API_KEY ?? process.env.HASNA_KNOWLEDGE_API_KEY;
|
|
414
|
+
if (!apiKey) throw new Error('Usage: open-knowledge auth login --api-key <key> [--email <email>]');
|
|
415
|
+
const auth = service.saveAuth({
|
|
416
|
+
apiKey,
|
|
417
|
+
email: flags.email,
|
|
418
|
+
orgSlug: flags.org,
|
|
419
|
+
orgId: flags.orgId,
|
|
420
|
+
userId: flags.userId,
|
|
421
|
+
apiUrl: flags.apiUrl,
|
|
422
|
+
}, process.env);
|
|
423
|
+
output({
|
|
424
|
+
ok: true,
|
|
425
|
+
authenticated: true,
|
|
426
|
+
email: auth.email ?? null,
|
|
427
|
+
org_slug: auth.org_slug ?? null,
|
|
428
|
+
api_url: auth.api_url ?? service.authStatus(process.env).api_url,
|
|
429
|
+
auth_path: service.authStatus(process.env).auth_path,
|
|
430
|
+
message: `Saved hosted credentials for ${auth.email ?? 'API key'}`,
|
|
431
|
+
}, flags.json);
|
|
432
|
+
return;
|
|
433
|
+
}
|
|
434
|
+
if (action === 'logout') {
|
|
435
|
+
const removed = service.clearAuth(process.env);
|
|
436
|
+
output({ ok: true, removed, message: removed ? 'Removed hosted credentials' : 'No hosted credentials found' }, flags.json);
|
|
437
|
+
return;
|
|
438
|
+
}
|
|
439
|
+
throw new Error("Invalid auth action. Use 'login', 'whoami', or 'logout'.");
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
if (command === 'remote') {
|
|
443
|
+
const action = positional[1] ?? 'status';
|
|
444
|
+
if (action === 'contracts' || action === 'contract') {
|
|
445
|
+
const auth = service.authStatus(process.env);
|
|
446
|
+
output({
|
|
447
|
+
ok: true,
|
|
448
|
+
authenticated: auth.authenticated,
|
|
449
|
+
api_url: auth.api_url,
|
|
450
|
+
contract: service.remoteContract(),
|
|
451
|
+
message: `Remote contract v${service.remoteContract().contract_version}`,
|
|
452
|
+
}, flags.json);
|
|
453
|
+
return;
|
|
454
|
+
}
|
|
455
|
+
if (action === 'status') {
|
|
456
|
+
const auth = service.authStatus(process.env);
|
|
457
|
+
const contract = service.remoteContract();
|
|
458
|
+
output({
|
|
459
|
+
ok: true,
|
|
460
|
+
mode: service.config().mode,
|
|
461
|
+
authenticated: auth.authenticated,
|
|
462
|
+
auth_source: auth.source,
|
|
463
|
+
api_url: auth.api_url,
|
|
464
|
+
client_ready: Boolean(service.remoteClient(process.env)),
|
|
465
|
+
contract_version: contract.contract_version,
|
|
466
|
+
capabilities: contract.capabilities,
|
|
467
|
+
message: auth.authenticated ? `Remote client ready for ${auth.api_url}` : 'Remote client not authenticated',
|
|
468
|
+
}, flags.json);
|
|
469
|
+
return;
|
|
470
|
+
}
|
|
471
|
+
throw new Error("Invalid remote action. Use 'contracts' or 'status'.");
|
|
472
|
+
}
|
|
473
|
+
|
|
368
474
|
if (command === 'storage') {
|
|
369
475
|
const action = positional[1] ?? 'status';
|
|
370
476
|
if (action === 'status') {
|
|
@@ -407,10 +513,47 @@ async function run(argv: string[]): Promise<void> {
|
|
|
407
513
|
|
|
408
514
|
if (command === 'wiki') {
|
|
409
515
|
const action = positional[1] ?? 'init';
|
|
410
|
-
if (action
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
516
|
+
if (action === 'init') {
|
|
517
|
+
const result = await service.initWiki();
|
|
518
|
+
output({ ok: true, ...result, message: `Initialized wiki layout in ${service.workspace.home}` }, flags.json);
|
|
519
|
+
return;
|
|
520
|
+
}
|
|
521
|
+
if (action === 'compile') {
|
|
522
|
+
const args = positional.slice(2);
|
|
523
|
+
const sourceRefs = args.filter((arg) => /^(open-files|file|s3|https?):\/\//.test(arg));
|
|
524
|
+
const query = args.filter((arg) => !/^(open-files|file|s3|https?):\/\//.test(arg)).join(' ');
|
|
525
|
+
const result = await service.compileWiki({
|
|
526
|
+
title: flags.title,
|
|
527
|
+
query: query || flags.search,
|
|
528
|
+
sourceRefs: sourceRefs.length > 0 ? sourceRefs : undefined,
|
|
529
|
+
limit: flags.limit,
|
|
530
|
+
});
|
|
531
|
+
output({ ok: true, ...result, message: `Compiled wiki page ${result.path}` }, flags.json);
|
|
532
|
+
return;
|
|
533
|
+
}
|
|
534
|
+
if (action === 'file-answer' || action === 'answer') {
|
|
535
|
+
const prompt = positional.slice(2).join(' ');
|
|
536
|
+
if (!prompt) throw new Error('Usage: open-knowledge wiki file-answer <prompt> --content <answer> --approve-write');
|
|
537
|
+
if (!flags.content) throw new Error('Missing --content <answer> for wiki file-answer.');
|
|
538
|
+
const result = await service.fileAnswer({
|
|
539
|
+
prompt,
|
|
540
|
+
answer: flags.content,
|
|
541
|
+
approveWrite: flags.approveWrite,
|
|
542
|
+
limit: flags.limit,
|
|
543
|
+
semantic: flags.semantic,
|
|
544
|
+
modelRef: flags.model,
|
|
545
|
+
dimensions: flags.dimensions,
|
|
546
|
+
fake: flags.fake,
|
|
547
|
+
});
|
|
548
|
+
output({ ok: true, ...result }, flags.json);
|
|
549
|
+
return;
|
|
550
|
+
}
|
|
551
|
+
if (action === 'lint') {
|
|
552
|
+
const result = service.lintWiki();
|
|
553
|
+
output({ ok: result.ok, ...result, message: result.ok ? 'Wiki lint passed' : `Wiki lint found ${result.issue_count} issue(s)` }, flags.json);
|
|
554
|
+
return;
|
|
555
|
+
}
|
|
556
|
+
throw new Error("Invalid wiki action. Use 'init', 'compile', 'file-answer', or 'lint'.");
|
|
414
557
|
}
|
|
415
558
|
|
|
416
559
|
if (command === 'safety') {
|
|
@@ -0,0 +1,268 @@
|
|
|
1
|
+
import { getKnowledgeApiKey, resolveKnowledgeApiUrl } from './auth';
|
|
2
|
+
import type { KnowledgeConfig } from './workspace';
|
|
3
|
+
|
|
4
|
+
export const REMOTE_KNOWLEDGE_CONTRACT_VERSION = 1 as const;
|
|
5
|
+
|
|
6
|
+
export type RemoteKnowledgeRunStatus = 'queued' | 'running' | 'completed' | 'failed' | 'canceled';
|
|
7
|
+
|
|
8
|
+
export interface RemoteKnowledgeSourceContract {
|
|
9
|
+
owner: 'open-files';
|
|
10
|
+
preferred_ref: 'open-files';
|
|
11
|
+
allowed_schemes: string[];
|
|
12
|
+
raw_source_bytes_stored_in_open_knowledge: false;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export interface RemoteKnowledgeArtifactContract {
|
|
16
|
+
storage_type: 'local' | 's3' | 'managed';
|
|
17
|
+
uri_prefix: string | null;
|
|
18
|
+
generated_only: true;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export interface RemoteKnowledgeRegistryContract {
|
|
22
|
+
contract_version: typeof REMOTE_KNOWLEDGE_CONTRACT_VERSION;
|
|
23
|
+
service: 'open-knowledge';
|
|
24
|
+
mode: 'local' | 'hosted';
|
|
25
|
+
capabilities: string[];
|
|
26
|
+
endpoints: {
|
|
27
|
+
registry: string;
|
|
28
|
+
search: string;
|
|
29
|
+
ask: string;
|
|
30
|
+
build: string;
|
|
31
|
+
sync: string;
|
|
32
|
+
run_status: string;
|
|
33
|
+
run_logs: string;
|
|
34
|
+
run_artifacts: string;
|
|
35
|
+
};
|
|
36
|
+
source_contract: RemoteKnowledgeSourceContract;
|
|
37
|
+
artifact_contract: RemoteKnowledgeArtifactContract;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export interface RemoteKnowledgeRunContract {
|
|
41
|
+
contract_version: typeof REMOTE_KNOWLEDGE_CONTRACT_VERSION;
|
|
42
|
+
id?: string;
|
|
43
|
+
type?: 'search' | 'ask' | 'build' | 'sync' | 'artifact' | 'status';
|
|
44
|
+
status?: RemoteKnowledgeRunStatus | string;
|
|
45
|
+
query?: string;
|
|
46
|
+
prompt?: string;
|
|
47
|
+
output_preview?: unknown;
|
|
48
|
+
citations?: unknown[];
|
|
49
|
+
artifacts?: unknown[];
|
|
50
|
+
usage?: Record<string, unknown>;
|
|
51
|
+
created_at?: string;
|
|
52
|
+
started_at?: string;
|
|
53
|
+
completed_at?: string;
|
|
54
|
+
duration_ms?: number;
|
|
55
|
+
error_code?: string;
|
|
56
|
+
error_message?: string;
|
|
57
|
+
error?: string;
|
|
58
|
+
details?: unknown;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export interface RemoteKnowledgeSearchRequest {
|
|
62
|
+
query: string;
|
|
63
|
+
limit?: number;
|
|
64
|
+
semantic?: boolean;
|
|
65
|
+
source_refs?: string[];
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export interface RemoteKnowledgePromptRequest extends RemoteKnowledgeSearchRequest {
|
|
69
|
+
prompt: string;
|
|
70
|
+
generate?: boolean;
|
|
71
|
+
approve_write?: boolean;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export interface RemoteKnowledgeSyncRequest {
|
|
75
|
+
source_refs?: string[];
|
|
76
|
+
artifact_prefix?: string;
|
|
77
|
+
mode?: 'pull' | 'push' | 'both';
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export interface RemoteKnowledgeLogEntry {
|
|
81
|
+
id?: string;
|
|
82
|
+
run_id?: string;
|
|
83
|
+
level?: string;
|
|
84
|
+
event?: string;
|
|
85
|
+
metadata?: Record<string, unknown>;
|
|
86
|
+
created_at?: string;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export interface RemoteKnowledgeArtifact {
|
|
90
|
+
id?: string;
|
|
91
|
+
uri?: string;
|
|
92
|
+
key?: string;
|
|
93
|
+
kind?: string;
|
|
94
|
+
content_type?: string;
|
|
95
|
+
hash?: string;
|
|
96
|
+
size_bytes?: number;
|
|
97
|
+
metadata?: Record<string, unknown>;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
101
|
+
return Boolean(value && typeof value === 'object' && !Array.isArray(value));
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function stringValue(record: Record<string, unknown>, key: string): string | undefined {
|
|
105
|
+
const value = record[key];
|
|
106
|
+
return typeof value === 'string' ? value : undefined;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function numberValue(record: Record<string, unknown>, key: string): number | undefined {
|
|
110
|
+
const value = record[key];
|
|
111
|
+
return typeof value === 'number' && Number.isFinite(value) ? value : undefined;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function arrayValue(record: Record<string, unknown>, key: string): unknown[] | undefined {
|
|
115
|
+
const value = record[key];
|
|
116
|
+
return Array.isArray(value) ? value : undefined;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
export function normalizeRemoteKnowledgeRunContract(payload: unknown, fallback?: Partial<RemoteKnowledgeRunContract>): RemoteKnowledgeRunContract {
|
|
120
|
+
const record = isRecord(payload) ? payload : {};
|
|
121
|
+
return {
|
|
122
|
+
contract_version: REMOTE_KNOWLEDGE_CONTRACT_VERSION,
|
|
123
|
+
id: stringValue(record, 'id') ?? fallback?.id,
|
|
124
|
+
type: (stringValue(record, 'type') as RemoteKnowledgeRunContract['type'] | undefined) ?? fallback?.type,
|
|
125
|
+
status: stringValue(record, 'status') ?? fallback?.status,
|
|
126
|
+
query: stringValue(record, 'query') ?? fallback?.query,
|
|
127
|
+
prompt: stringValue(record, 'prompt') ?? fallback?.prompt,
|
|
128
|
+
output_preview: Object.prototype.hasOwnProperty.call(record, 'output_preview') ? record.output_preview : fallback?.output_preview,
|
|
129
|
+
citations: arrayValue(record, 'citations') ?? fallback?.citations,
|
|
130
|
+
artifacts: arrayValue(record, 'artifacts') ?? fallback?.artifacts,
|
|
131
|
+
usage: isRecord(record.usage) ? record.usage : fallback?.usage,
|
|
132
|
+
created_at: stringValue(record, 'created_at') ?? fallback?.created_at,
|
|
133
|
+
started_at: stringValue(record, 'started_at') ?? fallback?.started_at,
|
|
134
|
+
completed_at: stringValue(record, 'completed_at') ?? fallback?.completed_at,
|
|
135
|
+
duration_ms: numberValue(record, 'duration_ms') ?? fallback?.duration_ms,
|
|
136
|
+
error_code: stringValue(record, 'error_code') ?? fallback?.error_code,
|
|
137
|
+
error_message: stringValue(record, 'error_message') ?? fallback?.error_message,
|
|
138
|
+
error: stringValue(record, 'error') ?? fallback?.error,
|
|
139
|
+
details: Object.prototype.hasOwnProperty.call(record, 'details') ? record.details : fallback?.details,
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
export function knowledgeRegistryContract(input: {
|
|
144
|
+
mode: 'local' | 'hosted';
|
|
145
|
+
sourceSchemes: string[];
|
|
146
|
+
storageType: 'local' | 's3' | 'managed';
|
|
147
|
+
artifactUriPrefix: string | null;
|
|
148
|
+
}): RemoteKnowledgeRegistryContract {
|
|
149
|
+
return {
|
|
150
|
+
contract_version: REMOTE_KNOWLEDGE_CONTRACT_VERSION,
|
|
151
|
+
service: 'open-knowledge',
|
|
152
|
+
mode: input.mode,
|
|
153
|
+
capabilities: [
|
|
154
|
+
'registry',
|
|
155
|
+
'search',
|
|
156
|
+
'ask',
|
|
157
|
+
'build',
|
|
158
|
+
'sync',
|
|
159
|
+
'status',
|
|
160
|
+
'logs',
|
|
161
|
+
'artifacts',
|
|
162
|
+
'open-files-source-refs',
|
|
163
|
+
's3-generated-artifacts',
|
|
164
|
+
],
|
|
165
|
+
endpoints: {
|
|
166
|
+
registry: '/api/v1/knowledge/registry',
|
|
167
|
+
search: '/api/v1/knowledge/search',
|
|
168
|
+
ask: '/api/v1/knowledge/ask',
|
|
169
|
+
build: '/api/v1/knowledge/build',
|
|
170
|
+
sync: '/api/v1/knowledge/sync',
|
|
171
|
+
run_status: '/api/v1/knowledge/runs/{run_id}',
|
|
172
|
+
run_logs: '/api/v1/knowledge/runs/{run_id}/logs',
|
|
173
|
+
run_artifacts: '/api/v1/knowledge/runs/{run_id}/artifacts',
|
|
174
|
+
},
|
|
175
|
+
source_contract: {
|
|
176
|
+
owner: 'open-files',
|
|
177
|
+
preferred_ref: 'open-files',
|
|
178
|
+
allowed_schemes: input.sourceSchemes,
|
|
179
|
+
raw_source_bytes_stored_in_open_knowledge: false,
|
|
180
|
+
},
|
|
181
|
+
artifact_contract: {
|
|
182
|
+
storage_type: input.storageType,
|
|
183
|
+
uri_prefix: input.artifactUriPrefix,
|
|
184
|
+
generated_only: true,
|
|
185
|
+
},
|
|
186
|
+
};
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
export class RemoteKnowledgeClient {
|
|
190
|
+
constructor(
|
|
191
|
+
private readonly apiKey: string,
|
|
192
|
+
private readonly apiUrl: string,
|
|
193
|
+
) {}
|
|
194
|
+
|
|
195
|
+
static fromConfig(config?: KnowledgeConfig, env: Record<string, string | undefined> = process.env): RemoteKnowledgeClient | null {
|
|
196
|
+
const key = getKnowledgeApiKey(env);
|
|
197
|
+
if (!key.apiKey) return null;
|
|
198
|
+
return new RemoteKnowledgeClient(key.apiKey, resolveKnowledgeApiUrl(config, env));
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
private async request(path: string, options: RequestInit = {}): Promise<Response> {
|
|
202
|
+
return fetch(`${this.apiUrl}${path}`, {
|
|
203
|
+
...options,
|
|
204
|
+
headers: {
|
|
205
|
+
Authorization: `Bearer ${this.apiKey}`,
|
|
206
|
+
'Content-Type': 'application/json',
|
|
207
|
+
...options.headers,
|
|
208
|
+
},
|
|
209
|
+
});
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
async registry(): Promise<RemoteKnowledgeRegistryContract> {
|
|
213
|
+
const response = await this.request('/api/v1/knowledge/registry');
|
|
214
|
+
return response.json() as Promise<RemoteKnowledgeRegistryContract>;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
async search(request: RemoteKnowledgeSearchRequest): Promise<RemoteKnowledgeRunContract> {
|
|
218
|
+
const response = await this.request('/api/v1/knowledge/search', {
|
|
219
|
+
method: 'POST',
|
|
220
|
+
body: JSON.stringify(request),
|
|
221
|
+
});
|
|
222
|
+
return normalizeRemoteKnowledgeRunContract(await response.json(), { type: 'search', query: request.query });
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
async ask(request: RemoteKnowledgePromptRequest): Promise<RemoteKnowledgeRunContract> {
|
|
226
|
+
const response = await this.request('/api/v1/knowledge/ask', {
|
|
227
|
+
method: 'POST',
|
|
228
|
+
body: JSON.stringify(request),
|
|
229
|
+
});
|
|
230
|
+
return normalizeRemoteKnowledgeRunContract(await response.json(), { type: 'ask', prompt: request.prompt });
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
async build(request: RemoteKnowledgePromptRequest): Promise<RemoteKnowledgeRunContract> {
|
|
234
|
+
const response = await this.request('/api/v1/knowledge/build', {
|
|
235
|
+
method: 'POST',
|
|
236
|
+
body: JSON.stringify(request),
|
|
237
|
+
});
|
|
238
|
+
return normalizeRemoteKnowledgeRunContract(await response.json(), { type: 'build', prompt: request.prompt });
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
async sync(request: RemoteKnowledgeSyncRequest = {}): Promise<RemoteKnowledgeRunContract> {
|
|
242
|
+
const response = await this.request('/api/v1/knowledge/sync', {
|
|
243
|
+
method: 'POST',
|
|
244
|
+
body: JSON.stringify(request),
|
|
245
|
+
});
|
|
246
|
+
return normalizeRemoteKnowledgeRunContract(await response.json(), { type: 'sync' });
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
async runStatus(runId: string): Promise<RemoteKnowledgeRunContract | null> {
|
|
250
|
+
const response = await this.request(`/api/v1/knowledge/runs/${encodeURIComponent(runId)}`);
|
|
251
|
+
if (!response.ok) return null;
|
|
252
|
+
return normalizeRemoteKnowledgeRunContract(await response.json(), { id: runId, type: 'status' });
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
async runLogs(runId: string): Promise<RemoteKnowledgeLogEntry[]> {
|
|
256
|
+
const response = await this.request(`/api/v1/knowledge/runs/${encodeURIComponent(runId)}/logs`);
|
|
257
|
+
if (!response.ok) return [];
|
|
258
|
+
const payload = await response.json();
|
|
259
|
+
return Array.isArray(payload) ? payload as RemoteKnowledgeLogEntry[] : [];
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
async runArtifacts(runId: string): Promise<RemoteKnowledgeArtifact[]> {
|
|
263
|
+
const response = await this.request(`/api/v1/knowledge/runs/${encodeURIComponent(runId)}/artifacts`);
|
|
264
|
+
if (!response.ok) return [];
|
|
265
|
+
const payload = await response.json();
|
|
266
|
+
return Array.isArray(payload) ? payload as RemoteKnowledgeArtifact[] : [];
|
|
267
|
+
}
|
|
268
|
+
}
|