@hasna/knowledge 0.1.0 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/store.ts ADDED
@@ -0,0 +1,103 @@
1
+ /**
2
+ * @hasna/knowledge
3
+ * Copyright 2026 Hasna Inc.
4
+ * Licensed under the Apache License, Version 2.0
5
+ */
6
+ import { mkdirSync, readFileSync, writeFileSync, existsSync, renameSync, unlinkSync } from 'node:fs';
7
+ import { dirname } from 'node:path';
8
+ import { homedir } from 'node:os';
9
+ import { randomUUID } from 'node:crypto';
10
+
11
+ export interface KnowledgeItem {
12
+ id: string;
13
+ title: string;
14
+ content: string;
15
+ url: string | null;
16
+ tags: string[];
17
+ created_at: string;
18
+ updated_at: string;
19
+ }
20
+
21
+ export interface Store {
22
+ items: KnowledgeItem[];
23
+ }
24
+
25
+ export function defaultStorePath(): string {
26
+ return `${homedir()}/.open-knowledge/db.json`;
27
+ }
28
+
29
+ export function ensureStore(path: string): void {
30
+ if (!existsSync(path)) {
31
+ mkdirSync(dirname(path), { recursive: true });
32
+ writeFileSync(path, JSON.stringify({ items: [] }, null, 2));
33
+ }
34
+ }
35
+
36
+ function lockPath(path: string): string {
37
+ return `${path}.lock`;
38
+ }
39
+
40
+ function acquireLock(lockPath: string, ownerId: string): void {
41
+ const maxWait = 5000;
42
+ const interval = 50;
43
+ const start = Date.now();
44
+ while (Date.now() - start < maxWait) {
45
+ try {
46
+ if (!existsSync(lockPath)) {
47
+ writeFileSync(lockPath, JSON.stringify({ owner: ownerId, ts: Date.now() }));
48
+ return;
49
+ }
50
+ const lock = JSON.parse(readFileSync(lockPath, 'utf8')) as { owner: string; ts: number };
51
+ if (Date.now() - lock.ts > 10000) {
52
+ unlinkSync(lockPath);
53
+ }
54
+ } catch {
55
+ // lock file disappeared, try again
56
+ }
57
+ const start2 = Date.now();
58
+ while (Date.now() - start2 < interval) {}
59
+ }
60
+ throw new Error(`Could not acquire lock on ${lockPath} after ${maxWait}ms`);
61
+ }
62
+
63
+ function releaseLock(lockPath: string, ownerId: string): void {
64
+ try {
65
+ if (existsSync(lockPath)) {
66
+ const lock = JSON.parse(readFileSync(lockPath, 'utf8')) as { owner: string; ts: number };
67
+ if (lock.owner === ownerId) {
68
+ unlinkSync(lockPath);
69
+ }
70
+ }
71
+ } catch {}
72
+ }
73
+
74
+ export function loadStore(path: string): Store {
75
+ ensureStore(path);
76
+ const raw = readFileSync(path, 'utf8');
77
+ const parsed = JSON.parse(raw) as Store;
78
+ if (!parsed || !Array.isArray(parsed.items)) {
79
+ return { items: [] };
80
+ }
81
+ return parsed;
82
+ }
83
+
84
+ export function saveStore(path: string, store: Store): void {
85
+ const tmp = `${path}.tmp.${randomUUID()}`;
86
+ writeFileSync(tmp, JSON.stringify(store, null, 2));
87
+ renameSync(tmp, path);
88
+ }
89
+
90
+ export function withLock<T>(path: string, fn: () => T): T {
91
+ const owner = randomUUID();
92
+ const lpath = lockPath(path);
93
+ acquireLock(lpath, owner);
94
+ try {
95
+ return fn();
96
+ } finally {
97
+ releaseLock(lpath, owner);
98
+ }
99
+ }
100
+
101
+ export function makeId(): string {
102
+ return `k_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 8)}`;
103
+ }
package/tests/cli.test.ts CHANGED
@@ -1,9 +1,16 @@
1
+ /**
2
+ * @hasna/knowledge
3
+ * Copyright 2026 Hasna Inc.
4
+ * Licensed under the Apache License, Version 2.0
5
+ */
1
6
  import { describe, expect, test } from 'bun:test';
2
7
  import { mkdtempSync, readFileSync } from 'node:fs';
3
8
  import { tmpdir } from 'node:os';
4
- import { join } from 'node:path';
9
+ import { join, dirname } from 'node:path';
10
+ import { fileURLToPath } from 'node:url';
5
11
 
6
- const CLI = '/home/hasna/workspace/hasna/opensource/opensourcedev/open-knowledge/src/cli.js';
12
+ const __dirname = dirname(fileURLToPath(import.meta.url));
13
+ const CLI = join(__dirname, '..', 'src', 'cli.ts');
7
14
 
8
15
  function runCli(args: string[]) {
9
16
  return Bun.spawnSync(['bun', CLI, ...args], {
package/tsconfig.json ADDED
@@ -0,0 +1,16 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2022",
4
+ "module": "ES2022",
5
+ "moduleResolution": "bundler",
6
+ "lib": ["ES2022"],
7
+ "outDir": "./dist",
8
+ "rootDir": "./src",
9
+ "strict": false,
10
+ "skipLibCheck": true,
11
+ "allowImportingExtensions": true,
12
+ "noEmit": true
13
+ },
14
+ "include": ["src/**/*", "tests/**/*"],
15
+ "exclude": ["node_modules", "dist", "bin"]
16
+ }
package/src/cli.js DELETED
@@ -1,279 +0,0 @@
1
- #!/usr/bin/env bun
2
- import { defaultStorePath, loadStore, saveStore, makeId } from './store.js';
3
- import pkg from '../package.json' with { type: 'json' };
4
-
5
- const COMMANDS = ['add', 'list', 'get', 'delete', 'help'];
6
- const COMMAND_ALIASES = {
7
- ls: 'list',
8
- rm: 'delete'
9
- };
10
-
11
- function parseArgs(argv) {
12
- const positional = [];
13
- const flags = {};
14
- for (let i = 0; i < argv.length; i += 1) {
15
- const token = argv[i];
16
- if (!token.startsWith('-')) {
17
- positional.push(token);
18
- continue;
19
- }
20
- if (token === '--json') {
21
- flags.json = true;
22
- continue;
23
- }
24
- if (token === '--yes' || token === '-y') {
25
- flags.yes = true;
26
- continue;
27
- }
28
- if (token === '--help' || token === '-h') {
29
- flags.help = true;
30
- continue;
31
- }
32
- if (token === '--version' || token === '-v') {
33
- flags.version = true;
34
- continue;
35
- }
36
- if (token === '--desc') {
37
- flags.desc = true;
38
- continue;
39
- }
40
- if (token === '--page' || token === '-p') {
41
- flags.page = Number(argv[i + 1]);
42
- i += 1;
43
- continue;
44
- }
45
- if (token === '--limit' || token === '-l') {
46
- flags.limit = Number(argv[i + 1]);
47
- i += 1;
48
- continue;
49
- }
50
- if (token === '--search' || token === '-s') {
51
- flags.search = argv[i + 1];
52
- i += 1;
53
- continue;
54
- }
55
- if (token === '--sort') {
56
- flags.sort = argv[i + 1];
57
- i += 1;
58
- continue;
59
- }
60
- if (token === '--id') {
61
- flags.id = argv[i + 1];
62
- i += 1;
63
- continue;
64
- }
65
- if (token === '--store') {
66
- flags.store = argv[i + 1];
67
- i += 1;
68
- continue;
69
- }
70
- throw new Error(`Unknown flag: ${token}. Run 'open-knowledge --help' for valid options.`);
71
- }
72
- return { positional, flags };
73
- }
74
-
75
- function resolveCommand(raw) {
76
- if (!raw) {
77
- return '';
78
- }
79
- return COMMAND_ALIASES[raw] ?? raw;
80
- }
81
-
82
- function levenshtein(a, b) {
83
- const dp = Array.from({ length: a.length + 1 }, () => Array(b.length + 1).fill(0));
84
- for (let i = 0; i <= a.length; i += 1) dp[i][0] = i;
85
- for (let j = 0; j <= b.length; j += 1) dp[0][j] = j;
86
- for (let i = 1; i <= a.length; i += 1) {
87
- for (let j = 1; j <= b.length; j += 1) {
88
- const cost = a[i - 1] === b[j - 1] ? 0 : 1;
89
- dp[i][j] = Math.min(dp[i - 1][j] + 1, dp[i][j - 1] + 1, dp[i - 1][j - 1] + cost);
90
- }
91
- }
92
- return dp[a.length][b.length];
93
- }
94
-
95
- function suggestCommand(input) {
96
- if (!input) {
97
- return '';
98
- }
99
- const all = [...COMMANDS, ...Object.keys(COMMAND_ALIASES)];
100
- let best = '';
101
- let bestScore = Number.POSITIVE_INFINITY;
102
- for (const candidate of all) {
103
- const score = levenshtein(input, candidate);
104
- if (score < bestScore) {
105
- bestScore = score;
106
- best = candidate;
107
- }
108
- }
109
- return bestScore <= 3 ? best : '';
110
- }
111
-
112
- function printGlobalHelp() {
113
- console.log(`open-knowledge - local agent knowledge store\n\nUsage:\n open-knowledge <command> [options]\n\nCommands:\n add <title> <content> Add an item\n list (alias: ls) List items (supports pagination/search/sort)\n get --id <id> Get one item\n delete (alias: rm) --id <id> Delete item (requires --yes)\n help [command] Show help\n\nGlobal Options:\n --json Output JSON\n --store <path> Override store path\n -v, --version Show version\n -h, --help Show help\n\nList Options:\n -p, --page <n> Page number (default: 1)\n -l, --limit <n> Items per page (default: 20)\n -s, --search <text> Filter by title/content\n --sort <created|title> Sort field (default: created)\n --desc Sort descending\n\nDelete Options:\n --id <id> Item id\n -y, --yes Confirm destructive action`);
114
- }
115
-
116
- function printCommandHelp(command) {
117
- if (command === 'add') {
118
- console.log('Usage: open-knowledge add <title> <content> [--json]');
119
- return;
120
- }
121
- if (command === 'list' || command === 'ls') {
122
- console.log('Usage: open-knowledge list|ls [-p <page>] [-l <limit>] [-s <search>] [--sort created|title] [--desc] [--json]');
123
- return;
124
- }
125
- if (command === 'get') {
126
- console.log('Usage: open-knowledge get --id <id> [--json]');
127
- return;
128
- }
129
- if (command === 'delete' || command === 'rm') {
130
- console.log('Usage: open-knowledge delete|rm --id <id> -y [--json]');
131
- return;
132
- }
133
- printGlobalHelp();
134
- }
135
-
136
- function output(data, asJson) {
137
- if (asJson) {
138
- console.log(JSON.stringify(data, null, 2));
139
- return;
140
- }
141
- if (typeof data === 'string') {
142
- console.log(data);
143
- return;
144
- }
145
- console.log(data.message ?? JSON.stringify(data, null, 2));
146
- }
147
-
148
- function requireId(flags) {
149
- if (!flags.id) {
150
- throw new Error('Missing required --id. Example: open-knowledge get --id <id>');
151
- }
152
- }
153
-
154
- function sortItems(items, flags) {
155
- const sort = flags.sort ?? 'created';
156
- if (sort !== 'created' && sort !== 'title') {
157
- throw new Error("Invalid --sort value. Use 'created' or 'title'.");
158
- }
159
- const sorted = [...items].sort((a, b) => {
160
- if (sort === 'title') {
161
- return a.title.localeCompare(b.title);
162
- }
163
- return a.created_at.localeCompare(b.created_at);
164
- });
165
- if (flags.desc) {
166
- sorted.reverse();
167
- }
168
- return { sorted, sort, direction: flags.desc ? 'desc' : 'asc' };
169
- }
170
-
171
- function run(argv) {
172
- const { positional, flags } = parseArgs(argv);
173
-
174
- if (flags.version) {
175
- output({ name: pkg.name, version: pkg.version }, flags.json);
176
- return;
177
- }
178
-
179
- const command = resolveCommand(positional[0]);
180
-
181
- if (!command || flags.help || command === 'help') {
182
- printCommandHelp(positional[1]);
183
- return;
184
- }
185
-
186
- const storePath = flags.store || defaultStorePath();
187
-
188
- if (command === 'add') {
189
- const title = positional[1];
190
- const content = positional[2];
191
- if (!title || !content) {
192
- throw new Error('Usage: open-knowledge add <title> <content>');
193
- }
194
- const db = loadStore(storePath);
195
- const item = {
196
- id: makeId(),
197
- title,
198
- content,
199
- created_at: new Date().toISOString()
200
- };
201
- db.items.push(item);
202
- saveStore(storePath, db);
203
- output({ ok: true, item, message: `Added ${item.id}` }, flags.json);
204
- return;
205
- }
206
-
207
- if (command === 'list') {
208
- const db = loadStore(storePath);
209
- const page = Number.isFinite(flags.page) && flags.page > 0 ? flags.page : 1;
210
- const limit = Number.isFinite(flags.limit) && flags.limit > 0 ? flags.limit : 20;
211
- const search = flags.search ? String(flags.search).toLowerCase() : '';
212
- const filtered = search
213
- ? db.items.filter((x) => x.title.toLowerCase().includes(search) || x.content.toLowerCase().includes(search))
214
- : db.items;
215
-
216
- const { sorted, sort, direction } = sortItems(filtered, flags);
217
- const start = (page - 1) * limit;
218
- const rows = sorted.slice(start, start + limit);
219
- const totalPages = Math.max(1, Math.ceil(sorted.length / limit));
220
-
221
- if (flags.json) {
222
- output({ ok: true, page, limit, total: sorted.length, total_pages: totalPages, sort, direction, items: rows }, true);
223
- return;
224
- }
225
- if (rows.length === 0) {
226
- output(`No items found (search=${search || 'none'})`, false);
227
- return;
228
- }
229
- for (const row of rows) {
230
- console.log(`${row.id}\t${row.title}\t${row.created_at}`);
231
- }
232
- console.log(`Page ${page}/${totalPages} | showing ${rows.length} of ${sorted.length} | sort=${sort} ${direction} | search=${search || 'none'}`);
233
- return;
234
- }
235
-
236
- if (command === 'get') {
237
- requireId(flags);
238
- const db = loadStore(storePath);
239
- const item = db.items.find((x) => x.id === flags.id);
240
- if (!item) {
241
- throw new Error(`Item not found: ${flags.id}`);
242
- }
243
- output({ ok: true, item, message: `${item.id}: ${item.title}` }, flags.json);
244
- return;
245
- }
246
-
247
- if (command === 'delete') {
248
- requireId(flags);
249
- if (!flags.yes) {
250
- throw new Error('Refusing delete without --yes. Re-run with: open-knowledge delete --id <id> --yes');
251
- }
252
- const db = loadStore(storePath);
253
- const before = db.items.length;
254
- db.items = db.items.filter((x) => x.id !== flags.id);
255
- const deleted = before !== db.items.length;
256
- saveStore(storePath, db);
257
- if (!deleted) {
258
- throw new Error(`Item not found: ${flags.id}`);
259
- }
260
- output({ ok: true, deleted_id: flags.id, message: `Deleted ${flags.id}` }, flags.json);
261
- return;
262
- }
263
-
264
- const suggestion = suggestCommand(positional[0]);
265
- const hint = suggestion ? ` Did you mean '${suggestion}'?` : '';
266
- throw new Error(`Unknown command: ${positional[0]}.${hint} Run 'open-knowledge --help' for available commands.`);
267
- }
268
-
269
- if (import.meta.main) {
270
- try {
271
- run(process.argv.slice(2));
272
- } catch (error) {
273
- const message = error instanceof Error ? error.message : String(error);
274
- console.error(`Error: ${message}`);
275
- process.exitCode = 1;
276
- }
277
- }
278
-
279
- export { run, parseArgs, suggestCommand, sortItems };
package/src/store.js DELETED
@@ -1,32 +0,0 @@
1
- import { mkdirSync, readFileSync, writeFileSync, existsSync } from 'node:fs';
2
- import { dirname } from 'node:path';
3
- import { homedir } from 'node:os';
4
-
5
- export function defaultStorePath() {
6
- return `${homedir()}/.open-knowledge/db.json`;
7
- }
8
-
9
- function ensureStore(path) {
10
- if (!existsSync(path)) {
11
- mkdirSync(dirname(path), { recursive: true });
12
- writeFileSync(path, JSON.stringify({ items: [] }, null, 2));
13
- }
14
- }
15
-
16
- export function loadStore(path) {
17
- ensureStore(path);
18
- const raw = readFileSync(path, 'utf8');
19
- const parsed = JSON.parse(raw);
20
- if (!parsed || !Array.isArray(parsed.items)) {
21
- return { items: [] };
22
- }
23
- return parsed;
24
- }
25
-
26
- export function saveStore(path, store) {
27
- writeFileSync(path, JSON.stringify(store, null, 2));
28
- }
29
-
30
- export function makeId() {
31
- return `k_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 8)}`;
32
- }