@openanonymity/nanomem 0.1.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.
Files changed (66) hide show
  1. package/README.md +194 -0
  2. package/package.json +85 -0
  3. package/src/backends/BaseStorage.js +177 -0
  4. package/src/backends/filesystem.js +177 -0
  5. package/src/backends/indexeddb.js +208 -0
  6. package/src/backends/ram.js +113 -0
  7. package/src/backends/schema.js +42 -0
  8. package/src/bullets/bulletIndex.js +125 -0
  9. package/src/bullets/compaction.js +109 -0
  10. package/src/bullets/index.js +16 -0
  11. package/src/bullets/normalize.js +241 -0
  12. package/src/bullets/parser.js +199 -0
  13. package/src/bullets/scoring.js +53 -0
  14. package/src/cli/auth.js +323 -0
  15. package/src/cli/commands.js +411 -0
  16. package/src/cli/config.js +120 -0
  17. package/src/cli/diff.js +68 -0
  18. package/src/cli/help.js +84 -0
  19. package/src/cli/output.js +269 -0
  20. package/src/cli/spinner.js +54 -0
  21. package/src/cli.js +178 -0
  22. package/src/engine/compactor.js +247 -0
  23. package/src/engine/executors.js +152 -0
  24. package/src/engine/ingester.js +229 -0
  25. package/src/engine/retriever.js +414 -0
  26. package/src/engine/toolLoop.js +176 -0
  27. package/src/imports/chatgpt.js +160 -0
  28. package/src/imports/index.js +14 -0
  29. package/src/imports/markdown.js +104 -0
  30. package/src/imports/oaFastchat.js +124 -0
  31. package/src/index.js +199 -0
  32. package/src/llm/anthropic.js +264 -0
  33. package/src/llm/openai.js +179 -0
  34. package/src/prompt_sets/conversation/ingestion.js +51 -0
  35. package/src/prompt_sets/document/ingestion.js +43 -0
  36. package/src/prompt_sets/index.js +31 -0
  37. package/src/types.js +382 -0
  38. package/src/utils/portability.js +174 -0
  39. package/types/backends/BaseStorage.d.ts +42 -0
  40. package/types/backends/filesystem.d.ts +11 -0
  41. package/types/backends/indexeddb.d.ts +12 -0
  42. package/types/backends/ram.d.ts +8 -0
  43. package/types/backends/schema.d.ts +14 -0
  44. package/types/bullets/bulletIndex.d.ts +47 -0
  45. package/types/bullets/compaction.d.ts +10 -0
  46. package/types/bullets/index.d.ts +36 -0
  47. package/types/bullets/normalize.d.ts +95 -0
  48. package/types/bullets/parser.d.ts +31 -0
  49. package/types/bullets/scoring.d.ts +12 -0
  50. package/types/engine/compactor.d.ts +27 -0
  51. package/types/engine/executors.d.ts +46 -0
  52. package/types/engine/ingester.d.ts +29 -0
  53. package/types/engine/retriever.d.ts +50 -0
  54. package/types/engine/toolLoop.d.ts +9 -0
  55. package/types/imports/chatgpt.d.ts +14 -0
  56. package/types/imports/index.d.ts +3 -0
  57. package/types/imports/markdown.d.ts +31 -0
  58. package/types/imports/oaFastchat.d.ts +30 -0
  59. package/types/index.d.ts +21 -0
  60. package/types/llm/anthropic.d.ts +16 -0
  61. package/types/llm/openai.d.ts +16 -0
  62. package/types/prompt_sets/conversation/ingestion.d.ts +7 -0
  63. package/types/prompt_sets/document/ingestion.d.ts +7 -0
  64. package/types/prompt_sets/index.d.ts +11 -0
  65. package/types/types.d.ts +293 -0
  66. package/types/utils/portability.d.ts +33 -0
@@ -0,0 +1,269 @@
1
+ /**
2
+ * CLI output formatting — JSON for pipes, human-readable for terminals.
3
+ */
4
+
5
+ // ─── ANSI helpers ─────────────────────────────────────────────────
6
+
7
+ const c = {
8
+ reset: '\x1b[0m',
9
+ bold: '\x1b[1m',
10
+ dim: '\x1b[2m',
11
+ cyan: '\x1b[36m',
12
+ green: '\x1b[32m',
13
+ red: '\x1b[31m',
14
+ yellow: '\x1b[33m',
15
+ white: '\x1b[37m',
16
+ gray: '\x1b[90m',
17
+ };
18
+
19
+ function col(code, text) {
20
+ if (!process.stdout.isTTY) return text;
21
+ return `${code}${text}${c.reset}`;
22
+ }
23
+
24
+ const dim = t => col(c.dim, t);
25
+ const bold = t => col(c.bold, t);
26
+ const cyan = t => col(c.cyan, t);
27
+ const green = t => col(c.green, t);
28
+ const red = t => col(c.red, t);
29
+ const gray = t => col(c.gray, t);
30
+
31
+ // ─── Public ───────────────────────────────────────────────────────
32
+
33
+ export function formatOutput(result, flags) {
34
+ if (flags.json || !process.stdout.isTTY) {
35
+ return JSON.stringify(result, null, 2);
36
+ }
37
+ return formatHuman(result, flags);
38
+ }
39
+
40
+ function formatHuman(result, flags) {
41
+ if (result == null) return '';
42
+ if (typeof result === 'string') return maybeRenderMarkdown(result, flags);
43
+
44
+ // retrieve → print assembled context directly
45
+ if (result.assembledContext != null) {
46
+ return maybeRenderMarkdown(result.assembledContext, flags) || dim('No relevant context found.');
47
+ }
48
+
49
+ // read → print content directly
50
+ if ('content' in result && 'path' in result) {
51
+ return maybeRenderMarkdown(result.content ?? '', flags);
52
+ }
53
+
54
+ // ls → list files and dirs
55
+ if (result.files && result.dirs) {
56
+ const lines = [];
57
+ for (const d of result.dirs) lines.push(cyan(d + '/'));
58
+ for (const f of result.files) lines.push(gray(' ') + f);
59
+ return lines.join('\n') || dim('(empty)');
60
+ }
61
+
62
+ // tree
63
+ if (result.treeLines != null) {
64
+ return result.treeLines.join('\n') || dim('(empty)');
65
+ }
66
+
67
+ // status → grouped display
68
+ if ('provider' in result && 'storagePath' in result) {
69
+ return [
70
+ '',
71
+ ` ${bold('LLM')}`,
72
+ row('Provider', result.provider),
73
+ row('Model', result.model),
74
+ row('Base URL', result.baseUrl),
75
+ '',
76
+ ` ${bold('Storage')}`,
77
+ row('Backend', result.storage),
78
+ row('Path', result.storagePath),
79
+ row('Config', result.configFile),
80
+ row('Files', String(result.files)),
81
+ row('Directories', result.directories?.length ? result.directories.join(', ') : '(none)'),
82
+ '',
83
+ ].join('\n');
84
+ }
85
+
86
+ // search results
87
+ if (Array.isArray(result)) {
88
+ if (result.length === 0) return dim('No results.');
89
+ const query = result._query || '';
90
+ return result.map(r => {
91
+ if (!r.path) return JSON.stringify(r);
92
+ const matchLines = (r.lines || [])
93
+ .map(line => ' ' + highlightQuery(line, query))
94
+ .join('\n');
95
+ return `${bold(r.path)}\n${matchLines}`;
96
+ }).join('\n\n');
97
+ }
98
+
99
+ // status actions
100
+ if (result.status) return formatAction(result);
101
+
102
+ // generic object → key: value
103
+ const pad = Math.max(...Object.keys(result).map(k => k.length));
104
+ return Object.entries(result)
105
+ .map(([k, v]) => row(k.padEnd(pad), Array.isArray(v) ? v.join(', ') : String(v)))
106
+ .join('\n');
107
+ }
108
+
109
+ function formatAction(result) {
110
+ switch (result.status) {
111
+ case 'initialized':
112
+ return section(green('✓ Memory initialized'), [
113
+ ['Backend', result.storage],
114
+ ['Path', result.path],
115
+ ]);
116
+ case 'written':
117
+ return green(`✓ Written to ${result.path}`);
118
+ case 'deleted':
119
+ return green(`✓ Deleted ${result.path}`);
120
+ case 'compacted': {
121
+ const changed = result.filesChanged ?? 0;
122
+ const total = result.filesTotal ?? 0;
123
+ const detail = total > 0
124
+ ? `\n\n ${dim('Files reviewed')} ${bold(String(total))}\n ${dim('Files updated')} ${bold(String(changed))}`
125
+ : '';
126
+ return green('✓ Memory compacted') + detail;
127
+ }
128
+ case 'processed':
129
+ return section(green('✓ Facts extracted'), [
130
+ ['Files updated', result.writeCalls],
131
+ ]);
132
+ case 'skipped':
133
+ return dim('– Nothing to extract (conversation too short)');
134
+ case 'imported':
135
+ return section(green('✓ Chat history imported'), [
136
+ ['Sessions', result.sessions],
137
+ ['Files updated', result.totalWriteCalls],
138
+ ]);
139
+ case 'added':
140
+ return section(green('✓ Text added to memory'), [
141
+ ['Sessions', result.sessions],
142
+ ['Files updated', result.totalWriteCalls],
143
+ ]);
144
+ case 'exported':
145
+ return section(green('✓ Memory exported'), [
146
+ ['Files', result.files],
147
+ ['Format', result.format],
148
+ ['Path', result.path],
149
+ ]);
150
+ case 'cleared':
151
+ return section(green('✓ Memory cleared'), [
152
+ ['Files deleted', result.filesDeleted],
153
+ ['Path', result.path],
154
+ ]);
155
+ case 'logged_in':
156
+ return section(green('✓ Logged in'), [
157
+ ['Provider', result.provider],
158
+ ['Config', result.configFile],
159
+ ]);
160
+ case 'logged_in_interactive':
161
+ return null; // already printed by the interactive prompt
162
+ case 'error':
163
+ return red(`✗ Extraction failed: ${result.error || 'unknown error'}`);
164
+ default:
165
+ return green(`✓ ${result.status}`);
166
+ }
167
+ }
168
+
169
+ // ─── Helpers ──────────────────────────────────────────────────────
170
+
171
+ function row(key, value) {
172
+ return ` ${dim(key.padEnd(14))} ${value}`;
173
+ }
174
+
175
+ function section(heading, rows) {
176
+ if (!rows || rows.length === 0) return heading;
177
+ const pad = Math.max(...rows.map(([k]) => String(k).length));
178
+ const detail = rows
179
+ .map(([k, v]) => ` ${dim(String(k).padEnd(pad))} ${bold(String(v ?? ''))}`)
180
+ .join('\n');
181
+ return `${heading}\n\n${detail}`;
182
+ }
183
+
184
+ function highlightQuery(text, query) {
185
+ if (!query) return text;
186
+ const re = new RegExp(`(${query.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')})`, 'gi');
187
+ return text.replace(re, `\x1b[33;1m$1\x1b[0m`);
188
+ }
189
+
190
+ function maybeRenderMarkdown(text, flags = {}) {
191
+ if (!text) return text;
192
+ if (!process.stdout.isTTY) return text;
193
+ if (!flags['render']) return text;
194
+ return renderMarkdown(text);
195
+ }
196
+
197
+ function renderMarkdown(markdown) {
198
+ const lines = markdown.replace(/\r\n/g, '\n').split('\n');
199
+ const rendered = [];
200
+ let inFence = false;
201
+
202
+ for (const line of lines) {
203
+ if (/^\s*```/.test(line)) {
204
+ inFence = !inFence;
205
+ if (!inFence) rendered.push('');
206
+ continue;
207
+ }
208
+
209
+ if (inFence) {
210
+ rendered.push(` ${gray(line)}`);
211
+ continue;
212
+ }
213
+
214
+ const heading = /^(#{1,6})\s+(.*)$/.exec(line);
215
+ if (heading) {
216
+ const level = heading[1].length;
217
+ const text = renderInline(heading[2].trim());
218
+ rendered.push(level <= 2 ? bold(text) : cyan(text));
219
+ continue;
220
+ }
221
+
222
+ const quote = /^\s*>\s?(.*)$/.exec(line);
223
+ if (quote) {
224
+ rendered.push(`${gray('│')} ${renderInline(quote[1])}`);
225
+ continue;
226
+ }
227
+
228
+ const bullet = /^(\s*)[-*+]\s+(.*)$/.exec(line);
229
+ if (bullet) {
230
+ const indent = ' '.repeat(Math.min(bullet[1].length, 6));
231
+ rendered.push(`${indent}${cyan('•')} ${renderInline(bullet[2])}`);
232
+ continue;
233
+ }
234
+
235
+ const numbered = /^(\s*)\d+\.\s+(.*)$/.exec(line);
236
+ if (numbered) {
237
+ const indent = ' '.repeat(Math.min(numbered[1].length, 6));
238
+ rendered.push(`${indent}${cyan('•')} ${renderInline(numbered[2])}`);
239
+ continue;
240
+ }
241
+
242
+ const rule = /^\s*([-*_])(?:\s*\1){2,}\s*$/.test(line);
243
+ if (rule) {
244
+ rendered.push(gray('────────────────────────'));
245
+ continue;
246
+ }
247
+
248
+ rendered.push(renderInline(line));
249
+ }
250
+
251
+ return rendered.join('\n');
252
+ }
253
+
254
+ function renderInline(text) {
255
+ if (!text) return text;
256
+
257
+ let out = text;
258
+ out = out.replace(/`([^`]+)`/g, (_, code) => gray(code));
259
+ out = out.replace(/\[([^\]]+)\]\(([^)]+)\)/g, (_, label, url) => `${underline(label)} ${gray(`<${url}>`)}`);
260
+ out = out.replace(/\*\*([^*]+)\*\*/g, (_, value) => bold(value));
261
+ out = out.replace(/__([^_]+)__/g, (_, value) => bold(value));
262
+ out = out.replace(/(^|[^\*])\*([^*\n]+)\*(?!\*)/g, (_, prefix, value) => `${prefix}${dim(value)}`);
263
+ out = out.replace(/(^|[^_])_([^_\n]+)_(?!_)/g, (_, prefix, value) => `${prefix}${dim(value)}`);
264
+ return out;
265
+ }
266
+
267
+ function underline(text) {
268
+ return col('\x1b[4m', text);
269
+ }
@@ -0,0 +1,54 @@
1
+ /**
2
+ * Minimal TTY spinner — no dependencies.
3
+ * Writes to stderr so it doesn't pollute stdout piping.
4
+ */
5
+
6
+ const FRAMES = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'];
7
+ const INTERVAL_MS = 80;
8
+
9
+ const c = {
10
+ reset: '\x1b[0m',
11
+ dim: '\x1b[2m',
12
+ cyan: '\x1b[36m',
13
+ green: '\x1b[32m',
14
+ yellow: '\x1b[33m',
15
+ };
16
+
17
+ /**
18
+ * Create and start a spinner.
19
+ *
20
+ * @param {string} label Initial label text
21
+ * @returns {{ update: (label: string) => void, stop: (finalLine?: string) => void }}
22
+ */
23
+ export function createSpinner(label) {
24
+ if (!process.stderr.isTTY) {
25
+ // Non-TTY: just print the label once, no animation
26
+ process.stderr.write(` ${label}\n`);
27
+ return {
28
+ update: () => {},
29
+ stop: (finalLine) => { if (finalLine) process.stderr.write(finalLine + '\n'); },
30
+ };
31
+ }
32
+
33
+ let current = label;
34
+ let frame = 0;
35
+
36
+ function render() {
37
+ const spinner = FRAMES[frame++ % FRAMES.length];
38
+ process.stderr.write(`\r ${c.cyan}${spinner}${c.reset} ${c.dim}${current}${c.reset} `);
39
+ }
40
+
41
+ render();
42
+ const timer = setInterval(render, INTERVAL_MS);
43
+
44
+ return {
45
+ update(newLabel) {
46
+ current = newLabel;
47
+ },
48
+ stop(finalLine) {
49
+ clearInterval(timer);
50
+ process.stderr.write('\r\x1b[2K'); // clear line
51
+ if (finalLine) process.stderr.write(finalLine + '\n');
52
+ },
53
+ };
54
+ }
package/src/cli.js ADDED
@@ -0,0 +1,178 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * CLI entry point for @openanonymity/nanomem.
4
+ *
5
+ * Usage: nanomem <command> [args] [flags]
6
+ */
7
+
8
+ import { parseArgs } from 'node:util';
9
+ import { readFile } from 'node:fs/promises';
10
+ import { fileURLToPath } from 'node:url';
11
+ import { dirname, join } from 'node:path';
12
+
13
+ import { resolveConfig, createMemoryFromConfig } from './cli/config.js';
14
+ import { GLOBAL_HELP, COMMAND_HELP } from './cli/help.js';
15
+ import { formatOutput } from './cli/output.js';
16
+ import { createSpinner } from './cli/spinner.js';
17
+ import * as commands from './cli/commands.js';
18
+
19
+ // ─── Parse args ──────────────────────────────────────────────────
20
+
21
+ const OPTIONS = {
22
+ 'api-key': { type: 'string' },
23
+ 'model': { type: 'string' },
24
+ 'provider': { type: 'string' },
25
+ 'base-url': { type: 'string' },
26
+ 'storage': { type: 'string' },
27
+ 'path': { type: 'string' },
28
+ 'json': { type: 'boolean', default: false },
29
+ 'help': { type: 'boolean', short: 'h', default: false },
30
+ 'version': { type: 'boolean', short: 'v', default: false },
31
+ 'content': { type: 'string' },
32
+ 'format': { type: 'string' },
33
+ 'context': { type: 'string' },
34
+ 'session-id': { type: 'string' },
35
+ 'session-title': { type: 'string' },
36
+ 'confirm': { type: 'boolean', default: false },
37
+ 'render': { type: 'boolean', default: false },
38
+ };
39
+
40
+ const COMMAND_MAP = {
41
+ add: commands.add,
42
+ login: commands.login,
43
+ init: commands.init,
44
+ retrieve: commands.retrieve,
45
+ import: commands.importCmd,
46
+ compact: commands.compact,
47
+ tree: commands.tree,
48
+ ls: commands.ls,
49
+ read: commands.read,
50
+ write: commands.write,
51
+ delete: commands.del,
52
+ search: commands.search,
53
+ export: commands.exportCmd,
54
+ clear: commands.clear,
55
+ status: commands.status,
56
+ };
57
+
58
+ // ─── Main ────────────────────────────────────────────────────────
59
+
60
+ async function main() {
61
+ let values, positionals;
62
+ try {
63
+ ({ values, positionals } = parseArgs({ options: OPTIONS, allowPositionals: true, strict: true }));
64
+ } catch (err) {
65
+ die(err.message);
66
+ }
67
+
68
+ // --version
69
+ if (values.version) {
70
+ const pkgPath = join(dirname(fileURLToPath(import.meta.url)), '..', 'package.json');
71
+ const pkg = JSON.parse(await readFile(pkgPath, 'utf-8'));
72
+ console.log(pkg.version);
73
+ return;
74
+ }
75
+
76
+ const commandName = positionals[0];
77
+ const commandArgs = positionals.slice(1);
78
+
79
+ // --help (global or per-command)
80
+ if (values.help || !commandName) {
81
+ if (commandName && COMMAND_HELP[commandName]) {
82
+ console.log(COMMAND_HELP[commandName]);
83
+ } else {
84
+ console.log(GLOBAL_HELP);
85
+ }
86
+ return;
87
+ }
88
+
89
+ const handler = COMMAND_MAP[commandName];
90
+ if (!handler) {
91
+ die(`Unknown command: ${commandName}\n\n${GLOBAL_HELP}`);
92
+ }
93
+
94
+ const config = await resolveConfig(values);
95
+ const memOpts = {};
96
+
97
+ // Wire progress for import/extract — spinner per session with live tool call updates
98
+ const isImport = commandName === 'import' || commandName === 'add' || commandName === 'extract';
99
+ const showProgress = isImport && !values.json && process.stderr.isTTY;
100
+ const spinnerHolder = { current: null }; // shared mutable ref between onToolCall and import loop
101
+ if (showProgress) {
102
+ const TOOL_LABELS = {
103
+ create_new_file: 'creating file',
104
+ append_memory: 'appending',
105
+ update_memory: 'updating',
106
+ archive_memory: 'archiving',
107
+ delete_memory: 'cleaning up',
108
+ read_file: 'reading',
109
+ list_files: 'scanning',
110
+ };
111
+ memOpts.onToolCall = (name) => {
112
+ const label = TOOL_LABELS[name] || name;
113
+ spinnerHolder.current?.update(label + '…');
114
+ };
115
+ }
116
+
117
+ // Wire progress for retrieve — surface fallback warnings to the user
118
+ if (commandName === 'retrieve' && !values.json && process.stderr.isTTY) {
119
+ memOpts.onProgress = ({ stage, message }) => {
120
+ if (stage === 'fallback') {
121
+ process.stderr.write(`Warning: ${message}\n`);
122
+ }
123
+ };
124
+ }
125
+
126
+ if (commandName === 'compact' && !values.json && process.stderr.isTTY) {
127
+ const cc = {
128
+ reset: '\x1b[0m', dim: '\x1b[2m',
129
+ yellow: '\x1b[33m', gray: '\x1b[90m',
130
+ };
131
+ let activeSpinner = null;
132
+ process.stderr.write('\n');
133
+ memOpts.onCompactProgress = ({ stage, file, current, total, deduplicated, superseded, expired }) => {
134
+ if (stage === 'file') {
135
+ activeSpinner = createSpinner(`${cc.dim}(${current}/${total})${cc.reset} ${file}`);
136
+ } else if (stage === 'semantic') {
137
+ activeSpinner?.update(`reviewing working facts… ${cc.dim}${file}${cc.reset}`);
138
+ } else if (stage === 'file_done') {
139
+ const changes = [];
140
+ if (superseded > 0) changes.push(`${superseded} superseded`);
141
+ if (deduplicated > 0) changes.push(`${deduplicated} deduplicated`);
142
+ if (expired > 0) changes.push(`${expired} expired`);
143
+ const tag = changes.length > 0
144
+ ? `${cc.yellow}${changes.join(', ')}${cc.reset}`
145
+ : `${cc.dim}no changes${cc.reset}`;
146
+ activeSpinner?.stop(` ${cc.gray}${file.padEnd(36)}${cc.reset} ${tag}`);
147
+ activeSpinner = null;
148
+ }
149
+ };
150
+ }
151
+
152
+ const mem = createMemoryFromConfig(config, commandName, memOpts);
153
+
154
+ // Spinner for operations that give no other feedback
155
+ const useSpinner = !values.json && process.stderr.isTTY &&
156
+ (commandName === 'retrieve' || commandName === 'compact');
157
+ const spinner = useSpinner && commandName === 'retrieve'
158
+ ? createSpinner('searching memory…')
159
+ : null;
160
+
161
+ const result = await handler(commandArgs, values, mem, config, { showProgress, spinnerHolder });
162
+
163
+ spinner?.stop();
164
+
165
+ if (result != null) {
166
+ const output = formatOutput(result, values);
167
+ if (output) console.log(output);
168
+ }
169
+ }
170
+
171
+ function die(message) {
172
+ console.error(message);
173
+ process.exit(1);
174
+ }
175
+
176
+ main().catch(err => {
177
+ die(err.message || String(err));
178
+ });