@hasna/knowledge 0.2.26 → 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.
Files changed (61) hide show
  1. package/README.md +61 -0
  2. package/bin/open-knowledge-mcp.js +85 -9
  3. package/bin/open-knowledge.js +86 -86
  4. package/dist/agent.d.ts +35 -0
  5. package/dist/artifact-store.d.ts +63 -0
  6. package/dist/auth.d.ts +35 -0
  7. package/dist/embeddings.d.ts +77 -0
  8. package/dist/index.d.ts +20 -0
  9. package/dist/index.js +5709 -0
  10. package/dist/knowledge-db.d.ts +27 -0
  11. package/dist/manifest-ingest.d.ts +35 -0
  12. package/dist/outbox-consume.d.ts +25 -0
  13. package/dist/provenance.d.ts +50 -0
  14. package/dist/providers.d.ts +89 -0
  15. package/dist/reindex.d.ts +37 -0
  16. package/dist/remote-client.d.ts +108 -0
  17. package/dist/retrieval.d.ts +71 -0
  18. package/dist/safety.d.ts +70 -0
  19. package/dist/sdk.d.ts +72 -0
  20. package/dist/search.d.ts +65 -0
  21. package/dist/service.d.ts +117 -0
  22. package/dist/source-ingest.d.ts +18 -0
  23. package/dist/source-ref.d.ts +30 -0
  24. package/dist/source-resolver.d.ts +92 -0
  25. package/dist/storage-contract.d.ts +106 -0
  26. package/dist/web-search.d.ts +40 -0
  27. package/dist/wiki-compiler.d.ts +67 -0
  28. package/dist/wiki-layout.d.ts +23 -0
  29. package/dist/workspace.d.ts +111 -0
  30. package/docs/architecture/ai-native-knowledge-base.md +24 -0
  31. package/docs/architecture/hosted-wrapper-responsibilities.md +8 -0
  32. package/docs/canonical-secrets-bootstrap-2026-06-08.md +127 -0
  33. package/package.json +15 -7
  34. package/src/agent.ts +0 -367
  35. package/src/artifact-store.ts +0 -184
  36. package/src/auth.ts +0 -123
  37. package/src/cli.ts +0 -1181
  38. package/src/embeddings.ts +0 -516
  39. package/src/knowledge-db.ts +0 -354
  40. package/src/manifest-ingest.ts +0 -515
  41. package/src/mcp-http.js +0 -110
  42. package/src/mcp.js +0 -1503
  43. package/src/outbox-consume.ts +0 -463
  44. package/src/provenance.ts +0 -93
  45. package/src/providers.ts +0 -308
  46. package/src/reindex.ts +0 -260
  47. package/src/remote-client.ts +0 -268
  48. package/src/retrieval.ts +0 -326
  49. package/src/safety.ts +0 -265
  50. package/src/schema.js +0 -25
  51. package/src/search.ts +0 -510
  52. package/src/service.ts +0 -432
  53. package/src/source-ingest.ts +0 -268
  54. package/src/source-ref.ts +0 -104
  55. package/src/source-resolver.ts +0 -436
  56. package/src/storage-contract.ts +0 -293
  57. package/src/store.ts +0 -113
  58. package/src/web-search.ts +0 -330
  59. package/src/wiki-compiler.ts +0 -711
  60. package/src/wiki-layout.ts +0 -251
  61. package/src/workspace.ts +0 -213
package/src/cli.ts DELETED
@@ -1,1181 +0,0 @@
1
- #!/usr/bin/env bun
2
- /**
3
- * @hasna/knowledge
4
- * Copyright 2026 Hasna Inc.
5
- * Licensed under the Apache License, Version 2.0
6
- */
7
- import { defaultStorePath, loadStore, saveStore, withLock, makeId, makeShortId, ensureStore, type KnowledgeItem } from './store';
8
- import { openKnowledgeDb } from './knowledge-db';
9
- import { createKnowledgeService } from './service';
10
- import { assertProviderCredentials, parseModelRef, resolveModelRef, type AiProviderId } from './providers';
11
- import { approvalStatus, assertS3ReadAllowed, assertWebSearchAllowed, createApprovalGate, recordAuditEvent, recordRedactionFindings, redactSecrets } from './safety';
12
- import { basename } from 'node:path';
13
- import pkg from '../package.json' with { type: 'json' };
14
-
15
- type LogLevel = 'debug' | 'info' | 'warn' | 'error';
16
- const LOG_LEVELS: Record<LogLevel, number> = { debug: 0, info: 1, warn: 2, error: 3 };
17
- const currentLevel = (): LogLevel => {
18
- if (process.env.DEBUG) return 'debug';
19
- if (process.env.LOG_LEVEL === 'debug') return 'debug';
20
- if (process.env.LOG_LEVEL === 'warn') return 'warn';
21
- if (process.env.LOG_LEVEL === 'error') return 'error';
22
- return 'info';
23
- };
24
- function log(level: LogLevel, msg: string, data?: Record<string, unknown>): void {
25
- if (LOG_LEVELS[level] < LOG_LEVELS[currentLevel()]) return;
26
- const prefix = { debug: '[DEBUG]', info: '[INFO]', warn: '[WARN]', error: '[ERROR]' }[level];
27
- const entry = data ? `${prefix} ${msg} ${JSON.stringify(data)}` : `${prefix} ${msg}`;
28
- if (level === 'error') console.error(entry);
29
- else console.error(entry);
30
- }
31
-
32
- interface Flags {
33
- json?: boolean;
34
- yes?: boolean;
35
- help?: boolean;
36
- version?: boolean;
37
- desc?: boolean;
38
- page?: number;
39
- limit?: number;
40
- search?: string;
41
- sort?: string;
42
- id?: string;
43
- store?: string;
44
- title?: string;
45
- content?: string;
46
- url?: string;
47
- tag?: string;
48
- format?: string;
49
- completions?: string;
50
- purpose?: string;
51
- model?: string;
52
- dimensions?: number;
53
- semantic?: boolean;
54
- context?: boolean;
55
- generate?: boolean;
56
- approveWrite?: boolean;
57
- provider?: string;
58
- mode?: string;
59
- apiUrl?: string;
60
- apiKey?: string;
61
- email?: string;
62
- org?: string;
63
- orgId?: string;
64
- userId?: string;
65
- domain?: string[];
66
- fileResults?: boolean;
67
- full?: boolean;
68
- noColor?: boolean;
69
- scope?: string;
70
- olderThan?: number;
71
- empty?: boolean;
72
- fake?: boolean;
73
- archived?: boolean;
74
- includeArchived?: boolean;
75
- }
76
-
77
- interface ParseResult {
78
- positional: string[];
79
- flags: Flags;
80
- }
81
-
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'];
83
- const COMMAND_ALIASES: Record<string, string> = {
84
- ls: 'list',
85
- rm: 'delete',
86
- edit: 'update',
87
- unarchive: 'restore',
88
- knowledge: 'ask',
89
- };
90
-
91
- function parseArgs(argv: string[]): ParseResult {
92
- const positional: string[] = [];
93
- const flags: Flags = {};
94
- for (let i = 0; i < argv.length; i += 1) {
95
- const token = argv[i];
96
- if (!token.startsWith('-')) {
97
- positional.push(token);
98
- continue;
99
- }
100
- switch (token) {
101
- case '--json': flags.json = true; break;
102
- case '--yes': case '-y': flags.yes = true; break;
103
- case '--help': case '-h': flags.help = true; break;
104
- case '--version': case '-v': flags.version = true; break;
105
- case '--desc': flags.desc = true; break;
106
- case '--page': case '-p': flags.page = Number(argv[i + 1]); i += 1; break;
107
- case '--limit': case '-l': flags.limit = Number(argv[i + 1]); i += 1; break;
108
- case '--search': case '-s': flags.search = argv[i + 1]; i += 1; break;
109
- case '--sort': flags.sort = argv[i + 1]; i += 1; break;
110
- case '--id': flags.id = argv[i + 1]; i += 1; break;
111
- case '--store': flags.store = argv[i + 1]; i += 1; break;
112
- case '--title': flags.title = argv[i + 1]; i += 1; break;
113
- case '--content': flags.content = argv[i + 1]; i += 1; break;
114
- case '--url': flags.url = argv[i + 1]; i += 1; break;
115
- case '--tag': case '-t': flags.tag = argv[i + 1]; i += 1; break;
116
- case '--format': flags.format = argv[i + 1]; i += 1; break;
117
- case '--completions': flags.completions = argv[i + 1]; i += 1; break;
118
- case '--purpose': flags.purpose = argv[i + 1]; i += 1; break;
119
- case '--model': flags.model = argv[i + 1]; i += 1; break;
120
- case '--dimensions': flags.dimensions = Number(argv[i + 1]); i += 1; break;
121
- case '--semantic': flags.semantic = true; break;
122
- case '--context': flags.context = true; break;
123
- case '--generate': flags.generate = true; break;
124
- case '--approve-write': flags.approveWrite = true; break;
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;
133
- case '--domain': flags.domain = [...(flags.domain ?? []), argv[i + 1]]; i += 1; break;
134
- case '--file-results': flags.fileResults = true; break;
135
- case '--full': flags.full = true; break;
136
- case '--fake': flags.fake = true; break;
137
- case '--no-color': flags.noColor = true; break;
138
- case '--scope': flags.scope = argv[i + 1]; i += 1; break;
139
- case '--older-than': flags.olderThan = Number(argv[i + 1]); i += 1; break;
140
- case '--empty': flags.empty = true; break;
141
- case '--archived': flags.archived = true; break;
142
- case '--include-archived': flags.includeArchived = true; break;
143
- default: throw new Error(`Unknown flag: ${token}. Run 'open-knowledge --help' for valid options.`);
144
- }
145
- }
146
- return { positional, flags };
147
- }
148
-
149
- function resolveCommand(raw: string): string {
150
- if (!raw) return '';
151
- return COMMAND_ALIASES[raw] ?? raw;
152
- }
153
-
154
- function levenshtein(a: string, b: string): number {
155
- const dp = Array.from({ length: a.length + 1 }, () => Array(b.length + 1).fill(0));
156
- for (let i = 0; i <= a.length; i += 1) dp[i][0] = i;
157
- for (let j = 0; j <= b.length; j += 1) dp[0][j] = j;
158
- for (let i = 1; i <= a.length; i += 1) {
159
- for (let j = 1; j <= b.length; j += 1) {
160
- const cost = a[i - 1] === b[j - 1] ? 0 : 1;
161
- dp[i][j] = Math.min(dp[i - 1][j] + 1, dp[i][j - 1] + 1, dp[i - 1][j - 1] + cost);
162
- }
163
- }
164
- return dp[a.length][b.length];
165
- }
166
-
167
- function suggestCommand(input: string): string {
168
- if (!input) return '';
169
- const all = [...COMMANDS, ...Object.keys(COMMAND_ALIASES)];
170
- let best = '';
171
- let bestScore = Number.POSITIVE_INFINITY;
172
- for (const candidate of all) {
173
- const score = levenshtein(input, candidate);
174
- if (score < bestScore) {
175
- bestScore = score;
176
- best = candidate;
177
- }
178
- }
179
- return bestScore <= 3 ? best : '';
180
- }
181
-
182
- function invokedAsKnowledge(): boolean {
183
- return basename(process.argv[1] ?? '') === 'knowledge';
184
- }
185
-
186
- function printGlobalHelp(): void {
187
- console.log(`open-knowledge - local agent knowledge store
188
-
189
- Usage:
190
- open-knowledge <command> [options]
191
-
192
- Commands:
193
- add <title> <content> Add an item
194
- list (alias: ls) List items (supports pagination/search/sort/tag)
195
- get --id <id> Get one item
196
- update --id <id> Update an item (--title, --content, --url, --tag)
197
- archive --id <id> Archive an item
198
- restore --id <id> Restore an archived item
199
- upsert [title] [content] Create or update an item by --id
200
- untag --id <id> -t <tag> Remove a tag from an item
201
- delete (alias: rm) --id <id> Delete item (requires --yes)
202
- export Export all items (--format jsonl)
203
- prune Remove old/empty items (requires --yes)
204
- dedupe Remove duplicate items by title+content (requires --yes)
205
- stats Show knowledge base statistics
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
210
- storage status|validate Inspect local/S3 artifact storage contract
211
- db init|stats Initialize or inspect local knowledge.db
212
- wiki init|compile|file-answer|lint
213
- Initialize, compile, file, or lint wiki artifacts
214
- source resolve <source-ref> Resolve read-only source content and citation evidence
215
- ingest manifest <file|s3://> Ingest an open-files manifest into knowledge.db
216
- ingest source <source-ref> Ingest a read-only source ref into knowledge.db
217
- reindex status|enqueue|embeddings|outbox Inspect/refresh search indexes
218
- search <query> Hybrid search sources, wiki pages, indexes, or context
219
- web search <query> Provider-native web search with citations
220
- ask|build <prompt> Build a read-only citation answer/context pack
221
- embeddings status|index|search Build/query local vector embeddings
222
- providers status|models|check Inspect AI SDK provider config and credentials
223
- safety status|check|approve|audit|redact
224
- help [command] Show help
225
-
226
- Global Options:
227
- --json Output JSON
228
- --store <path> Override store path
229
- --purpose <name> Read-only source purpose (default: knowledge_answer)
230
- --model <provider:model> AI/embedding model ref
231
- --dimensions <n> Embedding dimensions for local/fake providers
232
- --semantic Include vector semantic results in search
233
- --context Return a reranked citation context pack for search
234
- --generate Call AI SDK text generation for ask/build
235
- --approve-write Record approval intent for future durable wiki writes
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
244
- --domain <domain> Restrict provider web search to a domain
245
- --file-results File web snippets as web source refs
246
- --full Force full embedding index rebuild
247
- --fake Use deterministic fake embeddings for local tests
248
- --scope local|global|project Store scope (default: global ~/.hasna/apps/knowledge/)
249
- --no-color Disable color output
250
- --completions <shell> Output completions for bash|zsh|fish
251
- -v, --version Show version
252
- -h, --help Show help
253
-
254
- List Options:
255
- --format table|json Output format (default: table if TTY, json otherwise)
256
- -p, --page <n> Page number (default: 1)
257
- -l, --limit <n> Items per page (default: 20)
258
- -s, --search <text> Filter by title/content
259
- -t, --tag <tag> Filter by tag
260
- --sort <created|title> Sort field (default: created)
261
- --desc Sort descending
262
- --archived Show only archived items
263
- --include-archived Include archived items
264
-
265
- Add/Update Options:
266
- --url <url> Attach source URL
267
-
268
- Update Options:
269
- --id <id> Item id
270
- --title <title> New title
271
- --content <content> New content
272
- --url <url> New source URL
273
- -t, --tag <tag> Add a tag
274
-
275
- Delete Options:
276
- --id <id> Item id
277
- -y, --yes Confirm destructive action
278
-
279
- Export Options:
280
- --format jsonl Export as newline-delimited JSON (default: JSON array)
281
-
282
- Prune Options:
283
- --older-than <days> Remove items older than N days
284
- --empty Remove items with empty content`);
285
- }
286
-
287
- function printCommandHelp(command: string): void {
288
- if (command === 'add') { console.log('Usage: open-knowledge add <title> <content> [--url <url>] [-t <tag>] [--json]'); return; }
289
- if (command === 'list' || command === 'ls') { console.log('Usage: open-knowledge list|ls [--format table|json] [-p <page>] [-l <limit>] [-s <search>] [-t <tag>] [--sort created|title] [--desc] [--json]'); return; }
290
- if (command === 'get') { console.log('Usage: open-knowledge get --id <id> [--json]'); return; }
291
- if (command === 'update' || command === 'edit') { console.log('Usage: open-knowledge update|edit --id <id> [--title <title>] [--content <content>] [--url <url>] [-t <tag>] [--json]'); return; }
292
- if (command === 'archive') { console.log('Usage: open-knowledge archive --id <id> [--json]'); return; }
293
- if (command === 'restore' || command === 'unarchive') { console.log('Usage: open-knowledge restore|unarchive --id <id> [--json]'); return; }
294
- if (command === 'upsert') { console.log('Usage: open-knowledge upsert [title] [content] [--id <id>] [--title <title>] [--content <content>] [--url <url>] [-t <tag>] [--json]'); return; }
295
- if (command === 'untag') { console.log('Usage: open-knowledge untag --id <id> -t <tag> [--json]'); return; }
296
- if (command === 'delete' || command === 'rm') { console.log('Usage: open-knowledge delete|rm --id <id> -y [--json]'); return; }
297
- if (command === 'export') { console.log('Usage: open-knowledge export [--format jsonl] [--json]'); return; }
298
- if (command === 'prune') { console.log('Usage: open-knowledge prune --yes [--older-than <days>] [--empty] [--json]'); return; }
299
- if (command === 'dedupe') { console.log('Usage: open-knowledge dedupe --yes [--json]'); return; }
300
- if (command === 'stats') { console.log('Usage: open-knowledge stats [--json]'); return; }
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; }
305
- if (command === 'storage') { console.log('Usage: open-knowledge storage status|validate [--scope local|global|project] [--json]'); return; }
306
- if (command === 'db') { console.log('Usage: open-knowledge db init|stats [--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; }
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; }
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; }
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; }
311
- if (command === 'search') { console.log('Usage: open-knowledge search <query> [--context] [--semantic] [--model openai:text-embedding-3-small] [--limit <n>] [--dimensions <n>] [--fake] [--scope local|global|project] [--json]'); return; }
312
- if (command === 'web') { console.log('Usage: open-knowledge web search <query> [--provider openai|anthropic] [--model provider:model] [--domain <domain>] [--file-results] [--fake] [--scope local|global|project] [--json]'); return; }
313
- if (command === 'ask' || command === 'build' || command === 'knowledge') { console.log('Usage: open-knowledge ask|build <prompt> [--generate] [--semantic] [--model default|provider:model] [--approve-write] [--scope local|global|project] [--json]'); return; }
314
- if (command === 'embeddings') { console.log('Usage: open-knowledge embeddings status|index|search [query] [--model openai:text-embedding-3-small] [--limit <n>] [--dimensions <n>] [--fake] [--scope local|global|project] [--json]'); return; }
315
- if (command === 'providers') { console.log('Usage: open-knowledge providers status|models|check [provider|model-alias] [--scope local|global|project] [--json]'); return; }
316
- if (command === 'safety') { console.log('Usage: open-knowledge safety status|check|approve|audit|redact [args] [--scope local|global|project] [--json]'); return; }
317
- printGlobalHelp();
318
- }
319
-
320
- function useColor(flags: Flags): boolean {
321
- if (flags.noColor || process.env.NO_COLOR) return false;
322
- if (process.env.FORCE_COLOR) return true;
323
- return process.stdout.isTTY === true;
324
- }
325
-
326
- function output(data: unknown, asJson?: boolean, _flags?: Flags): void {
327
- if (asJson) { console.log(JSON.stringify(data, null, 2)); return; }
328
- if (typeof data === 'string') { console.log(data); return; }
329
- console.log((data as { message?: string }).message ?? JSON.stringify(data, null, 2));
330
- }
331
-
332
- function requireId(flags: Flags): asserts flags is Flags & { id: string } {
333
- if (!flags.id) throw new Error('Missing required --id. Example: open-knowledge get --id <id>');
334
- }
335
-
336
- function sortItems(items: KnowledgeItem[], flags: Flags): { sorted: KnowledgeItem[]; sort: string; direction: string } {
337
- const sort = flags.sort ?? 'created';
338
- if (sort !== 'created' && sort !== 'title') {
339
- throw new Error("Invalid --sort value. Use 'created' or 'title'.");
340
- }
341
- const sorted = [...items].sort((a, b) => {
342
- if (sort === 'title') return a.title.localeCompare(b.title);
343
- return a.created_at.localeCompare(b.created_at);
344
- });
345
- if (flags.desc) sorted.reverse();
346
- return { sorted, sort, direction: flags.desc ? 'desc' : 'asc' };
347
- }
348
-
349
- async function run(argv: string[]): Promise<void> {
350
- const { positional, flags } = parseArgs(argv);
351
- log('debug', 'CLI invoked', { command: positional[0], flags: { json: flags.json, store: flags.store } });
352
-
353
- if (flags.version) {
354
- console.log(flags.json ? JSON.stringify({ name: pkg.name, version: pkg.version }, null, 2) : `${pkg.name} ${pkg.version}`);
355
- return;
356
- }
357
-
358
- if (flags.completions) {
359
- const shell = flags.completions;
360
- if (shell === 'bash') {
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`);
362
- } else if (shell === 'zsh') {
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`);
364
- } else if (shell === 'fish') {
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"`);
366
- } else {
367
- throw new Error("Invalid --completions value. Use 'bash', 'zsh', or 'fish'.");
368
- }
369
- return;
370
- }
371
-
372
- let command = resolveCommand(positional[0]);
373
- let commandArgOffset = 1;
374
- if (invokedAsKnowledge() && command && !COMMANDS.includes(command)) {
375
- command = 'ask';
376
- commandArgOffset = 0;
377
- }
378
-
379
- if (!command || flags.help || command === 'help') { printCommandHelp(positional[1]); return; }
380
-
381
- const service = createKnowledgeService({ scope: flags.scope });
382
- let storePath = flags.store;
383
- if (!storePath) {
384
- if (flags.scope === 'project' || flags.scope === 'local') {
385
- storePath = service.jsonStorePath();
386
- } else {
387
- storePath = defaultStorePath();
388
- }
389
- }
390
-
391
- if (command === 'paths') {
392
- output(service.paths(), flags.json);
393
- return;
394
- }
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
-
474
- if (command === 'storage') {
475
- const action = positional[1] ?? 'status';
476
- if (action === 'status') {
477
- const contract = service.storageContract();
478
- const validation = service.validateStorage();
479
- output({
480
- ok: validation.ok,
481
- ...contract,
482
- validation,
483
- message: `${contract.storage_type} artifact storage at ${contract.artifact_store.uri_prefix}`,
484
- }, flags.json);
485
- return;
486
- }
487
- if (action === 'validate') {
488
- const validation = service.validateStorage();
489
- output({
490
- ok: validation.ok,
491
- validation,
492
- message: validation.ok ? 'Storage contract valid' : `Storage contract invalid: ${validation.errors.join('; ')}`,
493
- }, flags.json);
494
- return;
495
- }
496
- throw new Error("Invalid storage action. Use 'status' or 'validate'.");
497
- }
498
-
499
- if (command === 'db') {
500
- const action = positional[1] ?? 'init';
501
- if (action !== 'init' && action !== 'stats') {
502
- throw new Error("Invalid db action. Use 'init' or 'stats'.");
503
- }
504
- if (action === 'init') {
505
- const result = service.initDb();
506
- output({ ok: true, ...result, message: `Initialized ${result.path}` }, flags.json);
507
- return;
508
- }
509
- const stats = service.dbStats();
510
- output({ ok: true, path: service.workspace.knowledgeDbPath, ...stats, message: `knowledge.db schema v${stats.schema_version}` }, flags.json);
511
- return;
512
- }
513
-
514
- if (command === 'wiki') {
515
- const action = positional[1] ?? 'init';
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'.");
557
- }
558
-
559
- if (command === 'safety') {
560
- const action = positional[1] ?? 'status';
561
- const resolvedWorkspace = service.ensureWorkspace();
562
- const policy = service.safetyPolicy();
563
- service.initDb();
564
- const db = openKnowledgeDb(resolvedWorkspace.knowledgeDbPath);
565
- try {
566
- if (action === 'status') {
567
- output({
568
- ok: true,
569
- mode: policy.mode,
570
- workspace: resolvedWorkspace.home,
571
- allow_write_roots: policy.allowWriteRoots,
572
- read_only_source_access: policy.readOnlySourceAccess,
573
- network: policy.network,
574
- redaction: policy.redaction,
575
- approvals: policy.approvals,
576
- message: `Safety policy: ${policy.mode}`,
577
- }, flags.json);
578
- return;
579
- }
580
- if (action === 'check') {
581
- const checkAction = positional[2] ?? 'generated_write';
582
- const target = positional[3] ?? null;
583
- let decision: ReturnType<typeof approvalStatus> | { action: string; target_uri: string | null; approval_required: false; approved: boolean; decision: string };
584
- try {
585
- if (checkAction === 'web_search') {
586
- assertWebSearchAllowed(policy);
587
- decision = { action: checkAction, target_uri: target, approval_required: false, approved: true, decision: 'allow' };
588
- } else if (checkAction === 's3_read') {
589
- if (!target) throw new Error('safety check s3_read requires an s3:// target.');
590
- assertS3ReadAllowed(target, policy);
591
- decision = { action: checkAction, target_uri: target, approval_required: false, approved: true, decision: 'allow' };
592
- } else {
593
- decision = approvalStatus(db, policy, checkAction, target);
594
- }
595
- recordAuditEvent(db, {
596
- event_type: 'safety_check',
597
- action: checkAction,
598
- target_uri: target,
599
- decision: decision.decision === 'allow' ? 'allow' : 'requires_approval',
600
- metadata: decision,
601
- });
602
- output({ ok: true, ...decision, message: `Safety check ${decision.decision}` }, flags.json);
603
- return;
604
- } catch (error) {
605
- recordAuditEvent(db, {
606
- event_type: 'safety_check',
607
- action: checkAction,
608
- target_uri: target,
609
- decision: 'deny',
610
- metadata: { error: error instanceof Error ? error.message : String(error) },
611
- });
612
- throw error;
613
- }
614
- }
615
- if (action === 'approve') {
616
- const approveAction = positional[2] ?? 'generated_write';
617
- const target = positional[3] ?? null;
618
- const approval = createApprovalGate(db, {
619
- action: approveAction,
620
- target_uri: target,
621
- reason: 'local-cli approval',
622
- metadata: { scope: flags.scope ?? 'global' },
623
- });
624
- recordAuditEvent(db, {
625
- event_type: 'approval',
626
- action: approveAction,
627
- target_uri: target,
628
- decision: 'allow',
629
- metadata: { approval_id: approval.id },
630
- });
631
- output({ ok: true, ...approval, action: approveAction, target_uri: target, message: `Approved ${approveAction}` }, flags.json);
632
- return;
633
- }
634
- if (action === 'audit') {
635
- const rows = db.query<{
636
- id: string;
637
- event_type: string;
638
- action: string;
639
- target_uri: string | null;
640
- decision: string;
641
- metadata_json: string;
642
- created_at: string;
643
- }, []>(
644
- 'SELECT id, event_type, action, target_uri, decision, metadata_json, created_at FROM audit_events ORDER BY created_at DESC LIMIT 50',
645
- ).all().map((row) => ({
646
- id: row.id,
647
- event_type: row.event_type,
648
- action: row.action,
649
- target_uri: row.target_uri,
650
- decision: row.decision,
651
- metadata: JSON.parse(row.metadata_json),
652
- created_at: row.created_at,
653
- }));
654
- output({ ok: true, events: rows, message: `${rows.length} audit event(s)` }, flags.json);
655
- return;
656
- }
657
- if (action === 'redact') {
658
- const text = positional.slice(2).join(' ');
659
- if (!text) throw new Error('Usage: open-knowledge safety redact <text>');
660
- const result = redactSecrets(text, policy);
661
- if (result.findings.length > 0) {
662
- recordRedactionFindings(db, {
663
- source_uri: 'safety://redact',
664
- findings: result.findings,
665
- metadata: { command: 'safety redact' },
666
- });
667
- }
668
- recordAuditEvent(db, {
669
- event_type: 'redaction',
670
- action: 'safety_redact',
671
- target_uri: 'safety://redact',
672
- decision: result.findings.length > 0 ? 'redacted' : 'allow',
673
- metadata: { findings: result.findings.length },
674
- });
675
- output({ ok: true, text: result.text, findings: result.findings, message: `Redacted ${result.findings.length} finding(s)` }, flags.json);
676
- return;
677
- }
678
- throw new Error("Invalid safety action. Use 'status', 'check', 'approve', 'audit', or 'redact'.");
679
- } finally {
680
- db.close();
681
- }
682
- }
683
-
684
- if (command === 'source') {
685
- const action = positional[1] ?? '';
686
- if (action !== 'resolve') throw new Error("Invalid source action. Use 'resolve'.");
687
- const sourceRef = positional[2];
688
- if (!sourceRef) throw new Error('Usage: open-knowledge source resolve <source-ref>');
689
- const result = await service.resolveSource(sourceRef, {
690
- purpose: flags.purpose,
691
- limit: flags.limit,
692
- });
693
- output({
694
- ok: true,
695
- ...result,
696
- message: result.resolved
697
- ? `Resolved ${result.source_ref} (${result.content.chunks_returned}/${result.content.chunks_total} chunks)`
698
- : `Source not indexed: ${sourceRef}`,
699
- }, flags.json);
700
- return;
701
- }
702
-
703
- if (command === 'ingest') {
704
- const action = positional[1] ?? '';
705
- if (action === 'manifest') {
706
- const input = positional[2];
707
- if (!input) throw new Error('Usage: open-knowledge ingest manifest <file|s3://bucket/key>');
708
- const result = await service.ingestManifest(input);
709
- output({ ok: true, ...result, message: `Ingested ${result.items_seen} manifest item(s)` }, flags.json);
710
- return;
711
- }
712
- if (action === 'source') {
713
- const sourceRef = positional[2];
714
- if (!sourceRef) throw new Error('Usage: open-knowledge ingest source <source-ref>');
715
- const result = await service.ingestSource(sourceRef, flags.purpose);
716
- output({ ok: true, ...result, message: `Ingested source ${result.source_ref} (${result.chunks_inserted} chunks)` }, flags.json);
717
- return;
718
- }
719
- throw new Error("Invalid ingest action. Use 'manifest' or 'source'.");
720
- }
721
-
722
- if (command === 'reindex') {
723
- const action = positional[1] ?? 'status';
724
- if (action === 'status') {
725
- const result = service.reindexHealth({
726
- modelRef: flags.model,
727
- dimensions: flags.dimensions,
728
- fake: flags.fake,
729
- });
730
- output({ ok: true, ...result, message: `${result.missing_embeddings} chunk(s) missing embeddings` }, flags.json);
731
- return;
732
- }
733
- if (action === 'enqueue') {
734
- const result = service.enqueueReindex({
735
- modelRef: flags.model,
736
- dimensions: flags.dimensions,
737
- fake: flags.fake,
738
- });
739
- output({ ok: true, ...result, message: `Queued ${result.enqueued} embedding refresh item(s)` }, flags.json);
740
- return;
741
- }
742
- if (action === 'embeddings') {
743
- const result = await service.refreshEmbeddings({
744
- full: flags.full,
745
- limit: flags.limit,
746
- modelRef: flags.model,
747
- dimensions: flags.dimensions,
748
- fake: flags.fake,
749
- });
750
- output({ ok: true, ...result, message: `Embedded ${result.indexed.chunks_embedded} chunk(s)` }, flags.json);
751
- return;
752
- }
753
- if (action === 'outbox') {
754
- const input = positional[2];
755
- if (!input) throw new Error('Usage: open-knowledge reindex outbox <file|s3://bucket/key>');
756
- const result = await service.consumeOutbox(input);
757
- output({ ok: true, ...result, message: `Consumed ${result.events_seen} outbox event(s)` }, flags.json);
758
- return;
759
- }
760
- throw new Error("Invalid reindex action. Use 'status', 'enqueue', 'embeddings', or 'outbox'.");
761
- }
762
-
763
- if (command === 'embeddings') {
764
- const action = positional[1] ?? 'status';
765
- if (action === 'status') {
766
- const result = service.embeddingStatus();
767
- output({ ok: true, ...result, message: `${result.total_vector_entries} vector index entries` }, flags.json);
768
- return;
769
- }
770
- if (action === 'index') {
771
- const result = await service.indexEmbeddings({
772
- limit: flags.limit,
773
- modelRef: flags.model,
774
- dimensions: flags.dimensions,
775
- fake: flags.fake,
776
- });
777
- output({ ok: true, ...result, message: `Embedded ${result.chunks_embedded} chunk(s)` }, flags.json);
778
- return;
779
- }
780
- if (action === 'search') {
781
- const query = positional.slice(2).join(' ');
782
- if (!query) throw new Error('Usage: open-knowledge embeddings search <query>');
783
- const result = await service.semanticSearch({
784
- query,
785
- limit: flags.limit,
786
- modelRef: flags.model,
787
- dimensions: flags.dimensions,
788
- fake: flags.fake,
789
- });
790
- output({ ok: true, ...result, message: `${result.results.length} semantic result(s)` }, flags.json);
791
- return;
792
- }
793
- throw new Error("Invalid embeddings action. Use 'status', 'index', or 'search'.");
794
- }
795
-
796
- if (command === 'search') {
797
- const query = positional.slice(1).join(' ');
798
- if (!query) throw new Error('Usage: open-knowledge search <query>');
799
- if (flags.context) {
800
- const context = await service.retrieveContext({
801
- query,
802
- limit: flags.limit,
803
- semantic: flags.semantic,
804
- modelRef: flags.model,
805
- dimensions: flags.dimensions,
806
- fake: flags.fake,
807
- });
808
- output({ ok: true, ...context, message: `${context.excerpts.length} context excerpt(s)` }, flags.json);
809
- return;
810
- }
811
- const result = await service.search({
812
- query,
813
- limit: flags.limit,
814
- semantic: flags.semantic,
815
- modelRef: flags.model,
816
- dimensions: flags.dimensions,
817
- fake: flags.fake,
818
- });
819
- output({ ok: true, ...result, message: `${result.results.length} search result(s)` }, flags.json);
820
- return;
821
- }
822
-
823
- if (command === 'web') {
824
- const action = positional[1] ?? 'search';
825
- if (action !== 'search') throw new Error("Invalid web action. Use 'search'.");
826
- const query = positional.slice(2).join(' ');
827
- if (!query) throw new Error('Usage: open-knowledge web search <query>');
828
- const result = await service.webSearch({
829
- query,
830
- limit: flags.limit,
831
- modelRef: flags.model,
832
- provider: flags.provider as AiProviderId | undefined,
833
- domains: flags.domain,
834
- fake: flags.fake,
835
- fileResults: flags.fileResults,
836
- });
837
- output({ ok: true, ...result, message: `${result.sources.length} web source(s)` }, flags.json);
838
- return;
839
- }
840
-
841
- if (command === 'ask' || command === 'build') {
842
- const prompt = positional.slice(commandArgOffset).join(' ');
843
- if (!prompt) throw new Error('Usage: open-knowledge ask <prompt>');
844
- const result = await service.runPrompt({
845
- prompt,
846
- limit: flags.limit,
847
- semantic: flags.semantic,
848
- modelRef: flags.model,
849
- dimensions: flags.dimensions,
850
- fake: flags.fake,
851
- generate: flags.generate,
852
- approveWrite: flags.approveWrite,
853
- });
854
- output({ ok: true, ...result, message: result.generated ? 'Generated answer with citations' : 'Prepared citation context draft' }, flags.json);
855
- return;
856
- }
857
-
858
- if (command === 'providers') {
859
- const action = positional[1] ?? 'status';
860
- if (action === 'status') {
861
- const status = service.providerStatus();
862
- const configured = status.providers.filter((entry) => entry.configured).length;
863
- output({ ok: true, ...status, message: `${configured}/${status.providers.length} provider credential(s) configured` }, flags.json);
864
- return;
865
- }
866
- if (action === 'models') {
867
- const models = service.modelRegistry();
868
- output({ ok: true, models, message: `${models.length} model alias(es)` }, flags.json);
869
- return;
870
- }
871
- if (action === 'check') {
872
- const target = positional[2] ?? 'default';
873
- const modelRef = resolveModelRef(target, service.config());
874
- const parsed = parseModelRef(modelRef);
875
- const credential = assertProviderCredentials(parsed.provider as AiProviderId, service.config());
876
- output({ ok: true, target, model_ref: modelRef, provider: parsed.provider, model: parsed.model, credential, message: `${parsed.provider} credentials configured` }, flags.json);
877
- return;
878
- }
879
- throw new Error("Invalid providers action. Use 'status', 'models', or 'check'.");
880
- }
881
-
882
- ensureStore(storePath);
883
-
884
- if (command === 'add') {
885
- const title = positional[1];
886
- const content = positional[2];
887
- if (!title || !content) throw new Error('Usage: open-knowledge add <title> <content>');
888
- withLock(storePath, () => {
889
- const db = loadStore(storePath);
890
- const item: KnowledgeItem = {
891
- id: makeId(),
892
- title,
893
- content,
894
- url: flags.url ?? null,
895
- tags: flags.tag ? [flags.tag] : [],
896
- created_at: new Date().toISOString(),
897
- updated_at: new Date().toISOString(),
898
- };
899
- db.items.push(item);
900
- saveStore(storePath, db);
901
- log('info', 'Item added', { id: item.id, title: item.title });
902
- output({ ok: true, item, message: `Added ${item.id}` }, flags.json);
903
- });
904
- return;
905
- }
906
-
907
- if (command === 'list') {
908
- if (flags.format !== undefined && flags.format !== 'table' && flags.format !== 'json') {
909
- throw new Error("Invalid --format value for list. Use 'table' or 'json'.");
910
- }
911
- withLock(storePath, () => {
912
- const db = loadStore(storePath);
913
- const page = Number.isFinite(flags.page) && (flags.page as number) > 0 ? flags.page as number : 1;
914
- const limit = Number.isFinite(flags.limit) && (flags.limit as number) > 0 ? flags.limit as number : 20;
915
- const search = flags.search ? String(flags.search).toLowerCase() : '';
916
- const tag = flags.tag ? String(flags.tag).toLowerCase() : '';
917
- const useTable = flags.format === 'table' || (!flags.json && !flags.format && useColor(flags));
918
- const useJson = flags.json || flags.format === 'json';
919
-
920
- let filtered = db.items;
921
- if (flags.archived) filtered = filtered.filter((x) => x.archived === true);
922
- else if (!flags.includeArchived) filtered = filtered.filter((x) => !x.archived);
923
- if (search) filtered = filtered.filter((x) => x.title.toLowerCase().includes(search) || x.content.toLowerCase().includes(search));
924
- if (tag) filtered = filtered.filter((x) => x.tags && x.tags.map((t) => t.toLowerCase()).includes(tag));
925
-
926
- const { sorted, sort, direction } = sortItems(filtered, flags);
927
- const start = (page - 1) * limit;
928
- const rows = sorted.slice(start, start + limit);
929
- const totalPages = Math.max(1, Math.ceil(sorted.length / limit));
930
-
931
- if (useJson) { output({ ok: true, page, limit, total: sorted.length, total_pages: totalPages, sort, direction, items: rows }, true); return; }
932
- if (rows.length === 0) { output(`No items found (search=${search || 'none'}, tag=${tag || 'none'})`, false); return; }
933
- if (useTable) {
934
- const col = (v: string) => v;
935
- const header = `${col('ID')}\t${col('TITLE')}\t${col('CREATED')}\t${col('URL')}\t${col('TAGS')}`;
936
- console.log(header);
937
- for (const row of rows) {
938
- console.log(`${row.id}\t${col(row.title)}\t${row.created_at}\t${row.url ? col(row.url) : ''}\t${row.tags?.length ? col(`[${row.tags.join(', ')}]`) : ''}`);
939
- }
940
- console.log(`Page ${page}/${totalPages} | showing ${rows.length} of ${sorted.length} | sort=${sort} ${direction} | search=${search || 'none'} | tag=${tag || 'none'}`);
941
- } else {
942
- for (const row of rows) {
943
- console.log(`${row.id}\t${row.title}\t${row.created_at}${row.url ? `\t${row.url}` : ''}${row.tags?.length ? `\t[${row.tags.join(', ')}]` : ''}`);
944
- }
945
- console.log(`Page ${page}/${totalPages} | showing ${rows.length} of ${sorted.length} | sort=${sort} ${direction} | search=${search || 'none'} | tag=${tag || 'none'}`);
946
- }
947
- });
948
- return;
949
- }
950
-
951
- if (command === 'get') {
952
- requireId(flags);
953
- withLock(storePath, () => {
954
- const db = loadStore(storePath);
955
- const item = db.items.find((x) => x.id === flags.id || x.short_id === flags.id);
956
- if (!item) throw new Error(`Item not found: ${flags.id}`);
957
- output({ ok: true, item, message: `${item.id}: ${item.title}` }, flags.json);
958
- });
959
- return;
960
- }
961
-
962
- if (command === 'update') {
963
- requireId(flags);
964
- withLock(storePath, () => {
965
- const db = loadStore(storePath);
966
- const idx = db.items.findIndex((x) => x.id === flags.id || x.short_id === flags.id);
967
- if (idx === -1) throw new Error(`Item not found: ${flags.id}`);
968
- const item = db.items[idx];
969
- if (flags.title !== undefined) item.title = flags.title;
970
- if (flags.content !== undefined) item.content = flags.content;
971
- if (flags.url !== undefined) item.url = flags.url;
972
- if (flags.tag !== undefined) {
973
- item.tags = item.tags || [];
974
- if (!item.tags.map((t) => t.toLowerCase()).includes(flags.tag!.toLowerCase())) {
975
- item.tags.push(flags.tag!);
976
- }
977
- }
978
- item.updated_at = new Date().toISOString();
979
- db.items[idx] = item;
980
- saveStore(storePath, db);
981
- output({ ok: true, item, message: `Updated ${item.id}` }, flags.json);
982
- });
983
- return;
984
- }
985
-
986
- if (command === 'archive' || command === 'restore') {
987
- requireId(flags);
988
- withLock(storePath, () => {
989
- const db = loadStore(storePath);
990
- const idx = db.items.findIndex((x) => x.id === flags.id || x.short_id === flags.id);
991
- if (idx === -1) throw new Error(`Item not found: ${flags.id}`);
992
- const item = db.items[idx];
993
- item.archived = command === 'archive';
994
- item.updated_at = new Date().toISOString();
995
- db.items[idx] = item;
996
- saveStore(storePath, db);
997
- output({ ok: true, item, message: `${command === 'archive' ? 'Archived' : 'Restored'} ${item.id}` }, flags.json);
998
- });
999
- return;
1000
- }
1001
-
1002
- if (command === 'untag') {
1003
- requireId(flags);
1004
- if (!flags.tag) throw new Error('Missing required --tag. Example: open-knowledge untag --id <id> -t <tag>');
1005
- withLock(storePath, () => {
1006
- const db = loadStore(storePath);
1007
- const idx = db.items.findIndex((x) => x.id === flags.id || x.short_id === flags.id);
1008
- if (idx === -1) throw new Error(`Item not found: ${flags.id}`);
1009
- const item = db.items[idx];
1010
- const before = item.tags?.length ?? 0;
1011
- item.tags = (item.tags ?? []).filter((tag) => tag.toLowerCase() !== flags.tag!.toLowerCase());
1012
- item.updated_at = new Date().toISOString();
1013
- db.items[idx] = item;
1014
- saveStore(storePath, db);
1015
- output({ ok: true, item, removed: before - item.tags.length, message: `Removed tag from ${item.id}` }, flags.json);
1016
- });
1017
- return;
1018
- }
1019
-
1020
- if (command === 'upsert') {
1021
- const title = flags.title ?? positional[1];
1022
- const content = flags.content ?? positional[2];
1023
- withLock(storePath, () => {
1024
- const db = loadStore(storePath);
1025
- const idx = flags.id ? db.items.findIndex((x) => x.id === flags.id || x.short_id === flags.id) : -1;
1026
- const now = new Date().toISOString();
1027
- if (idx === -1) {
1028
- if (!title || !content) throw new Error('New item requires title and content. Example: open-knowledge upsert <title> <content> [--id <id>]');
1029
- const id = flags.id ?? makeId();
1030
- const item: KnowledgeItem = {
1031
- id,
1032
- short_id: makeShortId(id),
1033
- title,
1034
- content,
1035
- url: flags.url ?? null,
1036
- tags: flags.tag ? [flags.tag] : [],
1037
- metadata: {},
1038
- archived: false,
1039
- created_at: now,
1040
- updated_at: now,
1041
- };
1042
- db.items.push(item);
1043
- saveStore(storePath, db);
1044
- output({ ok: true, created: true, item, message: `Upserted ${item.id}` }, flags.json);
1045
- return;
1046
- }
1047
- const item = db.items[idx];
1048
- if (title !== undefined) item.title = title;
1049
- if (content !== undefined) item.content = content;
1050
- if (flags.url !== undefined) item.url = flags.url;
1051
- if (flags.tag !== undefined) {
1052
- item.tags = item.tags || [];
1053
- if (!item.tags.map((tag) => tag.toLowerCase()).includes(flags.tag.toLowerCase())) item.tags.push(flags.tag);
1054
- }
1055
- item.updated_at = now;
1056
- db.items[idx] = item;
1057
- saveStore(storePath, db);
1058
- output({ ok: true, created: false, item, message: `Upserted ${item.id}` }, flags.json);
1059
- });
1060
- return;
1061
- }
1062
-
1063
- if (command === 'delete') {
1064
- requireId(flags);
1065
- if (!flags.yes) throw new Error('Refusing delete without --yes. Re-run with: open-knowledge delete --id <id> --yes');
1066
- withLock(storePath, () => {
1067
- const db = loadStore(storePath);
1068
- const before = db.items.length;
1069
- db.items = db.items.filter((x) => x.id !== flags.id && x.short_id !== flags.id);
1070
- const deleted = before !== db.items.length;
1071
- saveStore(storePath, db);
1072
- if (!deleted) throw new Error(`Item not found: ${flags.id}`);
1073
- log('info', 'Item deleted', { id: flags.id });
1074
- output({ ok: true, deleted_id: flags.id, message: `Deleted ${flags.id}` }, flags.json);
1075
- });
1076
- return;
1077
- }
1078
-
1079
- if (command === 'export') {
1080
- const format = flags.format ?? 'json';
1081
- if (format !== 'json' && format !== 'jsonl') throw new Error("Invalid --format. Use 'json' or 'jsonl'.");
1082
- withLock(storePath, () => {
1083
- const db = loadStore(storePath);
1084
- if (format === 'jsonl') {
1085
- for (const item of db.items) console.log(JSON.stringify(item));
1086
- } else {
1087
- output({ ok: true, items: db.items }, flags.json);
1088
- }
1089
- });
1090
- return;
1091
- }
1092
-
1093
- if (command === 'prune') {
1094
- if (!flags.yes) throw new Error('Refusing prune without --yes. Re-run with: open-knowledge prune --yes [--older-than <days>] [--empty]');
1095
- withLock(storePath, () => {
1096
- const db = loadStore(storePath);
1097
- const before = db.items.length;
1098
- if (flags.olderThan !== undefined) {
1099
- const cutoff = new Date();
1100
- cutoff.setDate(cutoff.getDate() - flags.olderThan);
1101
- db.items = db.items.filter((x) => new Date(x.created_at) >= cutoff);
1102
- }
1103
- if (flags.empty) {
1104
- db.items = db.items.filter((x) => x.content.trim().length > 0);
1105
- }
1106
- const pruned = before - db.items.length;
1107
- saveStore(storePath, db);
1108
- log('info', 'Prune completed', { pruned, remaining: db.items.length });
1109
- output({ ok: true, pruned, remaining: db.items.length, message: `Pruned ${pruned} item(s)` }, flags.json);
1110
- });
1111
- return;
1112
- }
1113
-
1114
- if (command === 'dedupe') {
1115
- if (!flags.yes) throw new Error('Refusing dedupe without --yes. Re-run with: open-knowledge dedupe --yes [--json]');
1116
- withLock(storePath, () => {
1117
- const db = loadStore(storePath);
1118
- const seen = new Set<string>();
1119
- const before = db.items.length;
1120
- db.items = db.items.filter((x) => {
1121
- const key = `${x.title}\u0000${x.content}`;
1122
- if (seen.has(key)) return false;
1123
- seen.add(key);
1124
- return true;
1125
- });
1126
- const removed = before - db.items.length;
1127
- saveStore(storePath, db);
1128
- log('info', 'Dedupe completed', { removed, remaining: db.items.length });
1129
- output({ ok: true, removed, remaining: db.items.length, message: `Dedupe removed ${removed} duplicate(s)` }, flags.json);
1130
- });
1131
- return;
1132
- }
1133
-
1134
- if (command === 'stats') {
1135
- withLock(storePath, () => {
1136
- const db = loadStore(storePath);
1137
- const activeItems = db.items.filter((x) => !x.archived);
1138
- const total = activeItems.length;
1139
- const archived = db.items.length - total;
1140
- const withUrl = activeItems.filter((x) => x.url).length;
1141
- const withTags = activeItems.filter((x) => x.tags && x.tags.length > 0).length;
1142
- const oldest = total > 0 ? activeItems.map((x) => x.created_at).sort()[0] : null;
1143
- const newest = total > 0 ? activeItems.map((x) => x.created_at).sort()[total - 1] : null;
1144
- const tagCounts: Record<string, number> = {};
1145
- for (const item of activeItems) {
1146
- for (const tag of item.tags || []) {
1147
- tagCounts[tag] = (tagCounts[tag] || 0) + 1;
1148
- }
1149
- }
1150
- const topTags = Object.entries(tagCounts).sort((a, b) => b[1] - a[1]).slice(0, 5).map(([tag, count]) => ({ tag, count }));
1151
- output({
1152
- ok: true,
1153
- total,
1154
- archived,
1155
- with_url: withUrl,
1156
- with_tags: withTags,
1157
- oldest,
1158
- newest,
1159
- top_tags: topTags,
1160
- message: `${total} items | ${withUrl} with URL | ${withTags} with tags`,
1161
- }, flags.json);
1162
- });
1163
- return;
1164
- }
1165
-
1166
- const suggestion = suggestCommand(positional[0]);
1167
- const hint = suggestion ? ` Did you mean '${suggestion}'?` : '';
1168
- log('warn', 'Unknown command', { input: positional[0], suggestion });
1169
- throw new Error(`Unknown command: ${positional[0]}.${hint} Run 'open-knowledge --help' for available commands.`);
1170
- }
1171
-
1172
- if (import.meta.main) {
1173
- run(process.argv.slice(2)).catch((error) => {
1174
- const message = error instanceof Error ? error.message : String(error);
1175
- log('error', 'CLI error', { message, stack: error instanceof Error ? error.stack : undefined });
1176
- console.error(`Error: ${message}`);
1177
- process.exitCode = 1;
1178
- });
1179
- }
1180
-
1181
- export { run, parseArgs, suggestCommand, sortItems };