@hasna/knowledge 0.2.19 → 0.2.21
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 +22 -0
- package/bin/open-knowledge-mcp.js +345 -14
- package/bin/open-knowledge.js +82 -70
- package/docs/architecture/ai-native-knowledge-base.md +8 -0
- package/docs/architecture/hybrid-semantic-search.md +9 -0
- package/package.json +1 -1
- package/src/auth.ts +123 -0
- package/src/cli.ts +109 -4
- package/src/remote-client.ts +268 -0
- package/src/service.ts +98 -0
- package/src/storage-contract.ts +28 -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:
|
|
@@ -180,3 +180,12 @@ Reindexing is driven by source revisions:
|
|
|
180
180
|
- Permission filters run before model context assembly.
|
|
181
181
|
- Retrieval internals can swap from JSON vectors to pgvector or managed vector
|
|
182
182
|
stores without changing CLI/MCP result contracts.
|
|
183
|
+
|
|
184
|
+
## Evaluation Fixtures
|
|
185
|
+
|
|
186
|
+
`tests/semantic-evals.test.ts` and `tests/fixtures/semantic-eval-fixtures.ts`
|
|
187
|
+
seed no-network corpora for retrieval quality checks. The fixtures cover
|
|
188
|
+
keyword hits, deterministic fake-vector fallback for synonym-style prompts,
|
|
189
|
+
citation correctness, stale revision filtering, non-read-only provenance
|
|
190
|
+
filtering, rerank ordering, generated wiki-page retrieval, missing-source
|
|
191
|
+
answers, and `knowledge <prompt>` context assembly.
|
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,6 +204,9 @@ 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
212
|
wiki init Initialize scalable wiki/schema/index/log artifacts
|
|
@@ -216,6 +233,13 @@ Global Options:
|
|
|
216
233
|
--generate Call AI SDK text generation for ask/build
|
|
217
234
|
--approve-write Record approval intent for future durable wiki writes
|
|
218
235
|
--provider <name> Provider override for web search
|
|
236
|
+
--mode local|hosted Configure OSS local or hosted-aware mode
|
|
237
|
+
--api-url <url> Hosted API origin (or KNOWLEDGE_API_URL)
|
|
238
|
+
--api-key <key> Hosted API key for auth login
|
|
239
|
+
--email <email> Hosted account email metadata
|
|
240
|
+
--org <slug> Hosted organization slug metadata
|
|
241
|
+
--org-id <id> Hosted organization id metadata
|
|
242
|
+
--user-id <id> Hosted user id metadata
|
|
219
243
|
--domain <domain> Restrict provider web search to a domain
|
|
220
244
|
--file-results File web snippets as web source refs
|
|
221
245
|
--full Force full embedding index rebuild
|
|
@@ -274,6 +298,9 @@ function printCommandHelp(command: string): void {
|
|
|
274
298
|
if (command === 'dedupe') { console.log('Usage: open-knowledge dedupe --yes [--json]'); return; }
|
|
275
299
|
if (command === 'stats') { console.log('Usage: open-knowledge stats [--json]'); return; }
|
|
276
300
|
if (command === 'paths') { console.log('Usage: open-knowledge paths [--scope local|global|project] [--json]'); return; }
|
|
301
|
+
if (command === 'setup') { console.log('Usage: open-knowledge setup --mode local|hosted [--api-url https://...] [--scope local|global|project] [--json]'); return; }
|
|
302
|
+
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; }
|
|
303
|
+
if (command === 'remote') { console.log('Usage: open-knowledge remote contracts|status [--scope local|global|project] [--json]'); return; }
|
|
277
304
|
if (command === 'storage') { console.log('Usage: open-knowledge storage status|validate [--scope local|global|project] [--json]'); return; }
|
|
278
305
|
if (command === 'db') { console.log('Usage: open-knowledge db init|stats [--scope local|global|project] [--json]'); return; }
|
|
279
306
|
if (command === 'wiki') { console.log('Usage: open-knowledge wiki init [--scope local|global|project] [--json]'); return; }
|
|
@@ -330,11 +357,11 @@ async function run(argv: string[]): Promise<void> {
|
|
|
330
357
|
if (flags.completions) {
|
|
331
358
|
const shell = flags.completions;
|
|
332
359
|
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`);
|
|
360
|
+
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
361
|
} 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`);
|
|
362
|
+
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
363
|
} 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"`);
|
|
364
|
+
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
365
|
} else {
|
|
339
366
|
throw new Error("Invalid --completions value. Use 'bash', 'zsh', or 'fish'.");
|
|
340
367
|
}
|
|
@@ -365,6 +392,84 @@ async function run(argv: string[]): Promise<void> {
|
|
|
365
392
|
return;
|
|
366
393
|
}
|
|
367
394
|
|
|
395
|
+
if (command === 'setup') {
|
|
396
|
+
const result = service.setup({
|
|
397
|
+
mode: flags.mode,
|
|
398
|
+
apiUrl: flags.apiUrl,
|
|
399
|
+
});
|
|
400
|
+
output(result, flags.json);
|
|
401
|
+
return;
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
if (command === 'auth') {
|
|
405
|
+
const action = positional[1] ?? 'whoami';
|
|
406
|
+
if (action === 'whoami' || action === 'status') {
|
|
407
|
+
const result = service.authStatus(process.env);
|
|
408
|
+
output({ ok: true, ...result, message: result.authenticated ? `Authenticated via ${result.source}` : 'Not authenticated' }, flags.json);
|
|
409
|
+
return;
|
|
410
|
+
}
|
|
411
|
+
if (action === 'login') {
|
|
412
|
+
const apiKey = flags.apiKey ?? process.env.KNOWLEDGE_API_KEY ?? process.env.HASNA_KNOWLEDGE_API_KEY;
|
|
413
|
+
if (!apiKey) throw new Error('Usage: open-knowledge auth login --api-key <key> [--email <email>]');
|
|
414
|
+
const auth = service.saveAuth({
|
|
415
|
+
apiKey,
|
|
416
|
+
email: flags.email,
|
|
417
|
+
orgSlug: flags.org,
|
|
418
|
+
orgId: flags.orgId,
|
|
419
|
+
userId: flags.userId,
|
|
420
|
+
apiUrl: flags.apiUrl,
|
|
421
|
+
}, process.env);
|
|
422
|
+
output({
|
|
423
|
+
ok: true,
|
|
424
|
+
authenticated: true,
|
|
425
|
+
email: auth.email ?? null,
|
|
426
|
+
org_slug: auth.org_slug ?? null,
|
|
427
|
+
api_url: auth.api_url ?? service.authStatus(process.env).api_url,
|
|
428
|
+
auth_path: service.authStatus(process.env).auth_path,
|
|
429
|
+
message: `Saved hosted credentials for ${auth.email ?? 'API key'}`,
|
|
430
|
+
}, flags.json);
|
|
431
|
+
return;
|
|
432
|
+
}
|
|
433
|
+
if (action === 'logout') {
|
|
434
|
+
const removed = service.clearAuth(process.env);
|
|
435
|
+
output({ ok: true, removed, message: removed ? 'Removed hosted credentials' : 'No hosted credentials found' }, flags.json);
|
|
436
|
+
return;
|
|
437
|
+
}
|
|
438
|
+
throw new Error("Invalid auth action. Use 'login', 'whoami', or 'logout'.");
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
if (command === 'remote') {
|
|
442
|
+
const action = positional[1] ?? 'status';
|
|
443
|
+
if (action === 'contracts' || action === 'contract') {
|
|
444
|
+
const auth = service.authStatus(process.env);
|
|
445
|
+
output({
|
|
446
|
+
ok: true,
|
|
447
|
+
authenticated: auth.authenticated,
|
|
448
|
+
api_url: auth.api_url,
|
|
449
|
+
contract: service.remoteContract(),
|
|
450
|
+
message: `Remote contract v${service.remoteContract().contract_version}`,
|
|
451
|
+
}, flags.json);
|
|
452
|
+
return;
|
|
453
|
+
}
|
|
454
|
+
if (action === 'status') {
|
|
455
|
+
const auth = service.authStatus(process.env);
|
|
456
|
+
const contract = service.remoteContract();
|
|
457
|
+
output({
|
|
458
|
+
ok: true,
|
|
459
|
+
mode: service.config().mode,
|
|
460
|
+
authenticated: auth.authenticated,
|
|
461
|
+
auth_source: auth.source,
|
|
462
|
+
api_url: auth.api_url,
|
|
463
|
+
client_ready: Boolean(service.remoteClient(process.env)),
|
|
464
|
+
contract_version: contract.contract_version,
|
|
465
|
+
capabilities: contract.capabilities,
|
|
466
|
+
message: auth.authenticated ? `Remote client ready for ${auth.api_url}` : 'Remote client not authenticated',
|
|
467
|
+
}, flags.json);
|
|
468
|
+
return;
|
|
469
|
+
}
|
|
470
|
+
throw new Error("Invalid remote action. Use 'contracts' or 'status'.");
|
|
471
|
+
}
|
|
472
|
+
|
|
368
473
|
if (command === 'storage') {
|
|
369
474
|
const action = positional[1] ?? 'status';
|
|
370
475
|
if (action === 'status') {
|
|
@@ -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
|
+
}
|