@hasna/knowledge 0.2.27 → 0.2.28

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