synabun 1.3.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/CHANGELOG.md +665 -0
- package/LICENSE +191 -0
- package/README.md +853 -0
- package/hooks/claude-code/post-plan.mjs +391 -0
- package/hooks/claude-code/post-remember.mjs +239 -0
- package/hooks/claude-code/pre-compact.mjs +241 -0
- package/hooks/claude-code/pre-websearch.mjs +90 -0
- package/hooks/claude-code/prompt-submit.mjs +861 -0
- package/hooks/claude-code/session-start.mjs +494 -0
- package/hooks/claude-code/shared.mjs +368 -0
- package/hooks/claude-code/stop.mjs +646 -0
- package/hooks/claude-code/test-user-learning.mjs +531 -0
- package/mcp-server/package.json +38 -0
- package/mcp-server/run.mjs +10 -0
- package/mcp-server/src/config.ts +70 -0
- package/mcp-server/src/http.ts +93 -0
- package/mcp-server/src/index.ts +203 -0
- package/mcp-server/src/preload-http.ts +25 -0
- package/mcp-server/src/preload.ts +29 -0
- package/mcp-server/src/scripts/migrate-qdrant-to-sqlite.ts +506 -0
- package/mcp-server/src/services/categories.ts +429 -0
- package/mcp-server/src/services/discord.ts +702 -0
- package/mcp-server/src/services/file-checksums.ts +33 -0
- package/mcp-server/src/services/local-embeddings.ts +77 -0
- package/mcp-server/src/services/neural-interface.ts +344 -0
- package/mcp-server/src/services/sqlite.ts +823 -0
- package/mcp-server/src/tools/browser-advanced.ts +123 -0
- package/mcp-server/src/tools/browser-interact.ts +255 -0
- package/mcp-server/src/tools/browser-navigate.ts +77 -0
- package/mcp-server/src/tools/browser-observe.ts +1330 -0
- package/mcp-server/src/tools/browser.ts +107 -0
- package/mcp-server/src/tools/card-tools.ts +193 -0
- package/mcp-server/src/tools/card.ts +22 -0
- package/mcp-server/src/tools/category.ts +379 -0
- package/mcp-server/src/tools/discord-channel.ts +274 -0
- package/mcp-server/src/tools/discord-guild.ts +188 -0
- package/mcp-server/src/tools/discord-member.ts +158 -0
- package/mcp-server/src/tools/discord-message.ts +231 -0
- package/mcp-server/src/tools/discord-onboarding.ts +228 -0
- package/mcp-server/src/tools/discord-role.ts +209 -0
- package/mcp-server/src/tools/discord-thread.ts +110 -0
- package/mcp-server/src/tools/discord-webhook.ts +176 -0
- package/mcp-server/src/tools/discord.ts +40 -0
- package/mcp-server/src/tools/forget.ts +38 -0
- package/mcp-server/src/tools/git-tools.ts +141 -0
- package/mcp-server/src/tools/git.ts +13 -0
- package/mcp-server/src/tools/image-tools.ts +98 -0
- package/mcp-server/src/tools/image.ts +17 -0
- package/mcp-server/src/tools/leonardo-browser-tools.ts +281 -0
- package/mcp-server/src/tools/leonardo.ts +28 -0
- package/mcp-server/src/tools/loop.ts +348 -0
- package/mcp-server/src/tools/memories.ts +118 -0
- package/mcp-server/src/tools/recall.ts +323 -0
- package/mcp-server/src/tools/reflect.ts +145 -0
- package/mcp-server/src/tools/remember.ts +115 -0
- package/mcp-server/src/tools/response.ts +26 -0
- package/mcp-server/src/tools/restore.ts +34 -0
- package/mcp-server/src/tools/sync.ts +114 -0
- package/mcp-server/src/tools/tictactoe-tools.ts +98 -0
- package/mcp-server/src/tools/tictactoe.ts +13 -0
- package/mcp-server/src/tools/utils.ts +25 -0
- package/mcp-server/src/tools/whiteboard-tools.ts +260 -0
- package/mcp-server/src/tools/whiteboard.ts +22 -0
- package/mcp-server/src/tui.ts +940 -0
- package/mcp-server/src/types.ts +85 -0
- package/mcp-server/tsconfig.json +19 -0
- package/neural-interface/package.json +27 -0
- package/neural-interface/public/apple-touch-icon.png +0 -0
- package/neural-interface/public/category-logos/claude.svg +1 -0
- package/neural-interface/public/category-logos/gemini-color.svg +1 -0
- package/neural-interface/public/category-logos/openai.svg +1 -0
- package/neural-interface/public/category-logos/synabun.png +0 -0
- package/neural-interface/public/claude-chat.css +878 -0
- package/neural-interface/public/claude-chat.html +92 -0
- package/neural-interface/public/claude-chat.js +1168 -0
- package/neural-interface/public/favicon-16x16.png +0 -0
- package/neural-interface/public/favicon-32x32.png +0 -0
- package/neural-interface/public/gallery.svg +6 -0
- package/neural-interface/public/icon-192.png +0 -0
- package/neural-interface/public/icon-512.png +0 -0
- package/neural-interface/public/index-legacy.html +14677 -0
- package/neural-interface/public/index.html +212 -0
- package/neural-interface/public/index2d-legacy.html +8193 -0
- package/neural-interface/public/index2d.html +127 -0
- package/neural-interface/public/invite.html +351 -0
- package/neural-interface/public/leonardoai.svg +1 -0
- package/neural-interface/public/logo-synabun.png +0 -0
- package/neural-interface/public/logoHD.png +0 -0
- package/neural-interface/public/manifest.json +14 -0
- package/neural-interface/public/offline.html +299 -0
- package/neural-interface/public/onboarding.html +2335 -0
- package/neural-interface/public/openclaw-logo-text.png +0 -0
- package/neural-interface/public/pin-svgrepo-com.svg +25 -0
- package/neural-interface/public/sessionicon.svg +8 -0
- package/neural-interface/public/shared/ansi-parser.js +305 -0
- package/neural-interface/public/shared/api.js +1043 -0
- package/neural-interface/public/shared/colors.js +153 -0
- package/neural-interface/public/shared/constants.js +141 -0
- package/neural-interface/public/shared/html-shell.js +777 -0
- package/neural-interface/public/shared/html-term-renderer.js +1022 -0
- package/neural-interface/public/shared/i18n.js +147 -0
- package/neural-interface/public/shared/registry.js +82 -0
- package/neural-interface/public/shared/state.js +86 -0
- package/neural-interface/public/shared/storage.js +150 -0
- package/neural-interface/public/shared/styles.css +21212 -0
- package/neural-interface/public/shared/ui-automation-studio.js +3573 -0
- package/neural-interface/public/shared/ui-bookmarks.js +231 -0
- package/neural-interface/public/shared/ui-claude-panel.js +6333 -0
- package/neural-interface/public/shared/ui-command-runner.js +534 -0
- package/neural-interface/public/shared/ui-cost-widget.js +602 -0
- package/neural-interface/public/shared/ui-detail.js +1454 -0
- package/neural-interface/public/shared/ui-explorer.js +1356 -0
- package/neural-interface/public/shared/ui-file-explorer.js +2750 -0
- package/neural-interface/public/shared/ui-games.js +411 -0
- package/neural-interface/public/shared/ui-help.js +107 -0
- package/neural-interface/public/shared/ui-image-gallery.js +461 -0
- package/neural-interface/public/shared/ui-invite.js +519 -0
- package/neural-interface/public/shared/ui-keybinds.js +452 -0
- package/neural-interface/public/shared/ui-layouts.js +297 -0
- package/neural-interface/public/shared/ui-link.js +731 -0
- package/neural-interface/public/shared/ui-loading.js +255 -0
- package/neural-interface/public/shared/ui-menubar.js +499 -0
- package/neural-interface/public/shared/ui-multiselect.js +93 -0
- package/neural-interface/public/shared/ui-navbar.js +222 -0
- package/neural-interface/public/shared/ui-notifications.js +287 -0
- package/neural-interface/public/shared/ui-panels.js +309 -0
- package/neural-interface/public/shared/ui-resume.js +653 -0
- package/neural-interface/public/shared/ui-search.js +145 -0
- package/neural-interface/public/shared/ui-sessions.js +531 -0
- package/neural-interface/public/shared/ui-settings.js +5987 -0
- package/neural-interface/public/shared/ui-sidebar.js +1905 -0
- package/neural-interface/public/shared/ui-skills.js +2805 -0
- package/neural-interface/public/shared/ui-stats.js +38 -0
- package/neural-interface/public/shared/ui-sync.js +201 -0
- package/neural-interface/public/shared/ui-terminal.js +6385 -0
- package/neural-interface/public/shared/ui-tooltip.js +123 -0
- package/neural-interface/public/shared/ui-trash.js +439 -0
- package/neural-interface/public/shared/ui-tutorial-draw.js +875 -0
- package/neural-interface/public/shared/ui-tutorial-steps.js +2276 -0
- package/neural-interface/public/shared/ui-tutorial.js +1117 -0
- package/neural-interface/public/shared/ui-update.js +244 -0
- package/neural-interface/public/shared/ui-whiteboard.js +3262 -0
- package/neural-interface/public/shared/ui-workspaces.js +722 -0
- package/neural-interface/public/shared/utils.js +360 -0
- package/neural-interface/public/shared/vterm-buffer.js +1102 -0
- package/neural-interface/public/sw.js +52 -0
- package/neural-interface/public/synabun.png +0 -0
- package/neural-interface/public/toolbar-album.svg +8 -0
- package/neural-interface/public/toolbar-question.svg +6 -0
- package/neural-interface/public/toolbar-sessions.svg +6 -0
- package/neural-interface/public/variant/2d/background.js +113 -0
- package/neural-interface/public/variant/2d/context-menu.js +133 -0
- package/neural-interface/public/variant/2d/gfx.js +45 -0
- package/neural-interface/public/variant/2d/graph.js +1635 -0
- package/neural-interface/public/variant/2d/hulls.js +207 -0
- package/neural-interface/public/variant/2d/lasso.js +135 -0
- package/neural-interface/public/variant/2d/layout.js +594 -0
- package/neural-interface/public/variant/2d/main.js +521 -0
- package/neural-interface/public/variant/2d/minimap.js +312 -0
- package/neural-interface/public/variant/2d/settings-gfx.js +98 -0
- package/neural-interface/public/variant/3d/background.js +884 -0
- package/neural-interface/public/variant/3d/camera.js +661 -0
- package/neural-interface/public/variant/3d/gfx.js +116 -0
- package/neural-interface/public/variant/3d/graph.js +2399 -0
- package/neural-interface/public/variant/3d/main.js +500 -0
- package/neural-interface/public/variant/3d/settings-gfx.js +228 -0
- package/neural-interface/scripts/rebuild-pty.js +100 -0
- package/neural-interface/server.js +15000 -0
- package/neural-interface/templates/CLAUDE-template.md +211 -0
- package/package.json +53 -0
- package/postinstall.js +135 -0
- package/setup.js +260 -0
- package/skills/leonardo/SKILL.md +159 -0
- package/skills/leonardo/icon.svg +1 -0
- package/skills/leonardo/modules/browser-guide.md +378 -0
- package/skills/leonardo/modules/image-prompter.md +277 -0
- package/skills/leonardo/modules/model-advisor.md +100 -0
- package/skills/leonardo/modules/prompt-library.md +110 -0
- package/skills/leonardo/modules/style-guide.md +186 -0
- package/skills/leonardo/modules/video-prompter.md +311 -0
- package/skills/synabun/SKILL.md +185 -0
- package/skills/synabun/icon.png +0 -0
- package/skills/synabun/modules/audit.md +374 -0
- package/skills/synabun/modules/changelog.md +194 -0
- package/skills/synabun/modules/idea.md +144 -0
- package/skills/synabun/modules/memorize.md +188 -0
- package/skills/synabun/synabunicon.png +0 -0
|
@@ -0,0 +1,940 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// Load .env from parent directory before any service imports
|
|
4
|
+
import { readFileSync } from 'fs';
|
|
5
|
+
import { resolve, dirname } from 'path';
|
|
6
|
+
import { fileURLToPath } from 'url';
|
|
7
|
+
|
|
8
|
+
const __dirname_ = dirname(fileURLToPath(import.meta.url));
|
|
9
|
+
const envPath = process.env.DOTENV_PATH || resolve(__dirname_, '..', '..', '.env');
|
|
10
|
+
try {
|
|
11
|
+
const envContent = readFileSync(envPath, 'utf-8');
|
|
12
|
+
for (const line of envContent.split('\n')) {
|
|
13
|
+
const trimmed = line.trim();
|
|
14
|
+
if (!trimmed || trimmed.startsWith('#')) continue;
|
|
15
|
+
const eq = trimmed.indexOf('=');
|
|
16
|
+
if (eq === -1) continue;
|
|
17
|
+
const key = trimmed.slice(0, eq).trim();
|
|
18
|
+
const val = trimmed.slice(eq + 1).trim();
|
|
19
|
+
if (!process.env[key]) process.env[key] = val;
|
|
20
|
+
}
|
|
21
|
+
} catch { /* .env not found, rely on existing env vars */ }
|
|
22
|
+
|
|
23
|
+
import { select, input, confirm } from '@inquirer/prompts';
|
|
24
|
+
import chalk from 'chalk';
|
|
25
|
+
import { v4 as uuidv4 } from 'uuid';
|
|
26
|
+
import { ensureCollection, scrollMemories, searchMemories, getMemory, deleteMemory, updatePayload, updateVector, upsertMemory, getMemoryStats, countMemories, updatePayloadByFilter } from './services/sqlite.js';
|
|
27
|
+
import { getAllCategories, getCategories, getCustomCategories, getCategoryDescription, validateCategoryName, addCategory, removeCategory } from './services/categories.js';
|
|
28
|
+
import { generateEmbedding } from './services/local-embeddings.js';
|
|
29
|
+
import { detectProject } from './config.js';
|
|
30
|
+
import type { MemoryPayload, MemorySource } from './types.js';
|
|
31
|
+
|
|
32
|
+
// ── Box drawing characters ────────────────────────────────────────
|
|
33
|
+
|
|
34
|
+
const BOX = {
|
|
35
|
+
tl: '\u256D', tr: '\u256E', bl: '\u2570', br: '\u256F',
|
|
36
|
+
h: '\u2500', v: '\u2502',
|
|
37
|
+
ltee: '\u251C', rtee: '\u2524',
|
|
38
|
+
dh: '\u2550', dtl: '\u2554', dtr: '\u2557', dbl: '\u255A', dbr: '\u255D', dv: '\u2551',
|
|
39
|
+
} as const;
|
|
40
|
+
|
|
41
|
+
const ICONS = {
|
|
42
|
+
brain: '\u2022', // bullet
|
|
43
|
+
search: '\u25C9', // fisheye
|
|
44
|
+
plus: '\u253C', // cross
|
|
45
|
+
folder: '\u25A0', // square
|
|
46
|
+
chart: '\u25B2', // triangle
|
|
47
|
+
exit: '\u25C6', // diamond
|
|
48
|
+
tag: '#',
|
|
49
|
+
arrow: '\u25B6', // right triangle
|
|
50
|
+
dot: '\u2022',
|
|
51
|
+
star: '\u2605',
|
|
52
|
+
check: '\u2713',
|
|
53
|
+
cross: '\u2717',
|
|
54
|
+
sparkle: '\u2734',
|
|
55
|
+
memo: '\u2261', // triple bar
|
|
56
|
+
clock: '\u25CB', // circle
|
|
57
|
+
link: '\u221E', // infinity
|
|
58
|
+
edit: '\u270E', // pencil
|
|
59
|
+
trash: '\u2620', // skull
|
|
60
|
+
back: '\u25C0', // left triangle
|
|
61
|
+
more: '\u22EF', // midline ellipsis
|
|
62
|
+
} as const;
|
|
63
|
+
|
|
64
|
+
// ── Color helpers ──────────────────────────────────────────────────
|
|
65
|
+
|
|
66
|
+
// Dynamic category color palette — cycles through for categories without a preset
|
|
67
|
+
const CATEGORY_COLOR_PALETTE = [
|
|
68
|
+
'#5B9BD5', '#70AD47', '#FFC000', '#ED7D31', '#44C8F5',
|
|
69
|
+
'#A5A5A5', '#4472C4', '#FF6B9D', '#00B050', '#E040FB',
|
|
70
|
+
'#00BFA5', '#7C4DFF', '#FF5252', '#D32F2F', '#64FFDA',
|
|
71
|
+
'#536DFE', '#FFD740', '#69F0AE', '#18FFFF', '#B2FF59',
|
|
72
|
+
];
|
|
73
|
+
|
|
74
|
+
const CATEGORY_ICON_PALETTE = [
|
|
75
|
+
'\u25C8', '\u25A3', '\u2261', '\u2605', '\u25CF',
|
|
76
|
+
'\u25AC', '\u25A4', '\u2665', '\u25B2', '\u25C6',
|
|
77
|
+
'\u25CB', '\u25AA', '\u2726', '\u25D0', '\u25B6',
|
|
78
|
+
'\u2662', '\u2716', '\u25C9', '\u2756', '\u2318',
|
|
79
|
+
];
|
|
80
|
+
|
|
81
|
+
function getCategoryColorFn(cat: string): (s: string) => string {
|
|
82
|
+
const allCats = getAllCategories();
|
|
83
|
+
const idx = allCats.indexOf(cat);
|
|
84
|
+
const hex = idx >= 0 ? CATEGORY_COLOR_PALETTE[idx % CATEGORY_COLOR_PALETTE.length] : '#888888';
|
|
85
|
+
return chalk.hex(hex);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function getCategoryIcon(cat: string): string {
|
|
89
|
+
const allCats = getAllCategories();
|
|
90
|
+
const idx = allCats.indexOf(cat);
|
|
91
|
+
return idx >= 0 ? CATEGORY_ICON_PALETTE[idx % CATEGORY_ICON_PALETTE.length] : ICONS.dot;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const accent = chalk.hex('#7B68EE'); // medium slate blue - main brand color
|
|
95
|
+
const accent2 = chalk.hex('#9370DB'); // medium purple
|
|
96
|
+
const dim = chalk.hex('#555555');
|
|
97
|
+
const success = chalk.hex('#00E676');
|
|
98
|
+
const warn = chalk.hex('#FFAB40');
|
|
99
|
+
const danger = chalk.hex('#FF5252');
|
|
100
|
+
const info = chalk.hex('#40C4FF');
|
|
101
|
+
const muted = chalk.hex('#666666');
|
|
102
|
+
|
|
103
|
+
function colorCategory(cat: string): string {
|
|
104
|
+
const fn = getCategoryColorFn(cat);
|
|
105
|
+
const icon = getCategoryIcon(cat);
|
|
106
|
+
return fn(`${icon} ${cat}`);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function colorCategoryName(cat: string): string {
|
|
110
|
+
const fn = getCategoryColorFn(cat);
|
|
111
|
+
return fn(cat);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function importanceBadge(imp: number): string {
|
|
115
|
+
if (imp >= 9) return chalk.bgHex('#FF5252').white.bold(` ${imp} `);
|
|
116
|
+
if (imp >= 8) return chalk.bgHex('#FF7043').white(` ${imp} `);
|
|
117
|
+
if (imp >= 6) return chalk.bgHex('#FFA726').black(` ${imp} `);
|
|
118
|
+
if (imp >= 4) return chalk.bgHex('#78909C').white(` ${imp} `);
|
|
119
|
+
return chalk.bgHex('#455A64').white(` ${imp} `);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function formatAge(iso: string): string {
|
|
123
|
+
const ms = Date.now() - new Date(iso).getTime();
|
|
124
|
+
const mins = Math.floor(ms / 60000);
|
|
125
|
+
if (mins < 1) return 'just now';
|
|
126
|
+
if (mins < 60) return `${mins}m ago`;
|
|
127
|
+
const hours = Math.floor(mins / 60);
|
|
128
|
+
if (hours < 24) return `${hours}h ago`;
|
|
129
|
+
const days = Math.floor(hours / 24);
|
|
130
|
+
if (days === 1) return '1d ago';
|
|
131
|
+
if (days < 30) return `${days}d ago`;
|
|
132
|
+
const months = Math.floor(days / 30);
|
|
133
|
+
if (months === 1) return '1mo ago';
|
|
134
|
+
if (months < 12) return `${months}mo ago`;
|
|
135
|
+
const years = Math.floor(months / 12);
|
|
136
|
+
return years === 1 ? '1y ago' : `${years}y ago`;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function formatDate(iso: string): string {
|
|
140
|
+
return new Date(iso).toLocaleDateString('en-US', {
|
|
141
|
+
year: 'numeric', month: 'short', day: 'numeric',
|
|
142
|
+
hour: '2-digit', minute: '2-digit',
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function truncate(s: string, max: number): string {
|
|
147
|
+
const line = s.replace(/\n/g, ' ').trim();
|
|
148
|
+
return line.length <= max ? line : line.slice(0, max - 1) + '\u2026';
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// ── Box & layout helpers ──────────────────────────────────────────
|
|
152
|
+
|
|
153
|
+
function boxTop(width: number, title?: string): string {
|
|
154
|
+
if (title) {
|
|
155
|
+
const titleStr = ` ${title} `;
|
|
156
|
+
const stripped = titleStr.replace(/\x1B\[[0-9;]*m/g, '');
|
|
157
|
+
const remaining = width - stripped.length;
|
|
158
|
+
const left = Math.max(0, Math.floor(remaining / 2));
|
|
159
|
+
const right = Math.max(0, remaining - left);
|
|
160
|
+
return dim(BOX.tl + BOX.h.repeat(left)) + accent.bold(titleStr) + dim(BOX.h.repeat(right) + BOX.tr);
|
|
161
|
+
}
|
|
162
|
+
return dim(BOX.tl + BOX.h.repeat(width) + BOX.tr);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
function boxBottom(width: number): string {
|
|
166
|
+
return dim(BOX.bl + BOX.h.repeat(width) + BOX.br);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
function boxRow(width: number, content: string, pad = 1): string {
|
|
170
|
+
// Strip ANSI for length calculation
|
|
171
|
+
const stripped = content.replace(/\x1B\[[0-9;]*m/g, '');
|
|
172
|
+
const remaining = width - stripped.length - (pad * 2);
|
|
173
|
+
const right = Math.max(0, remaining);
|
|
174
|
+
return dim(BOX.v) + ' '.repeat(pad) + content + ' '.repeat(right + pad) + dim(BOX.v);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
function boxEmpty(width: number): string {
|
|
178
|
+
return dim(BOX.v) + ' '.repeat(width) + dim(BOX.v);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
function boxSep(width: number): string {
|
|
182
|
+
return dim(BOX.ltee + BOX.h.repeat(width) + BOX.rtee);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// Renders a double-bordered row: ║ <centered content padded to W> ║
|
|
186
|
+
function dboxRow(width: number, content: string): string {
|
|
187
|
+
const stripped = content.replace(/\x1B\[[0-9;]*m/g, '');
|
|
188
|
+
const totalPad = width - stripped.length;
|
|
189
|
+
const left = Math.max(0, Math.floor(totalPad / 2));
|
|
190
|
+
const right = Math.max(0, totalPad - left);
|
|
191
|
+
return dim(BOX.dv) + ' '.repeat(left) + content + ' '.repeat(right) + dim(BOX.dv);
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
// ── ASCII Art ─────────────────────────────────────────────────────
|
|
195
|
+
|
|
196
|
+
function printLogo(): void {
|
|
197
|
+
const W = 56;
|
|
198
|
+
|
|
199
|
+
// The art lines with fixed left indent for consistent visual alignment
|
|
200
|
+
const indent = ' ';
|
|
201
|
+
const rawArt = [
|
|
202
|
+
`${indent} __ __ `,
|
|
203
|
+
`${indent} | \\/ | ___ _ __ ___ ___ _ __ _ _`,
|
|
204
|
+
`${indent}| |\\/| |/ _ \\ '_ \` _ \\ / _ \\| '__| | | |`,
|
|
205
|
+
`${indent}| | | | __/ | | | | | (_) | | | |_| |`,
|
|
206
|
+
`${indent}|_| |_|\\___|_| |_| |_|\\___/|_| \\__, |`,
|
|
207
|
+
`${indent} |___/ `,
|
|
208
|
+
];
|
|
209
|
+
|
|
210
|
+
// Fixed-width row: ║ + left pad + content + right pad + ║
|
|
211
|
+
function artRow(content: string): string {
|
|
212
|
+
const stripped = content.replace(/\x1B\[[0-9;]*m/g, '');
|
|
213
|
+
const pad = Math.max(0, W - stripped.length);
|
|
214
|
+
const left = 3; // fixed left margin
|
|
215
|
+
const right = Math.max(0, pad - left);
|
|
216
|
+
return dim(BOX.dv) + ' '.repeat(left) + content + ' '.repeat(right) + dim(BOX.dv);
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
console.log('');
|
|
220
|
+
console.log(dim(BOX.dtl + BOX.dh.repeat(W) + BOX.dtr));
|
|
221
|
+
console.log(dboxRow(W, ''));
|
|
222
|
+
|
|
223
|
+
for (const line of rawArt) {
|
|
224
|
+
console.log(artRow(accent.bold(line)));
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
// Empty line before tagline
|
|
228
|
+
console.log(dboxRow(W, ''));
|
|
229
|
+
|
|
230
|
+
// Tagline (centered)
|
|
231
|
+
const tagline = accent2('Vector-Powered') + dim(' \u2022 ') + info('SQLite') + dim(' \u2022 ') + chalk.white('v2.0.0');
|
|
232
|
+
console.log(dboxRow(W, tagline));
|
|
233
|
+
|
|
234
|
+
console.log(dim(BOX.dbl + BOX.dh.repeat(W) + BOX.dbr));
|
|
235
|
+
console.log('');
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
function printScreenHeader(title: string, subtitle?: string): void {
|
|
239
|
+
const W = 56;
|
|
240
|
+
console.log('');
|
|
241
|
+
console.log(' ' + boxTop(W, title));
|
|
242
|
+
if (subtitle) {
|
|
243
|
+
console.log(' ' + boxRow(W, muted(subtitle)));
|
|
244
|
+
}
|
|
245
|
+
console.log(' ' + boxBottom(W));
|
|
246
|
+
console.log('');
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
function printSpinner(text: string): void {
|
|
250
|
+
console.log(muted(`\n ${ICONS.more} ${text}`));
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
// ── Prompt wrapper (Ctrl+C -> null instead of crash) ──────────────
|
|
254
|
+
|
|
255
|
+
async function safePrompt<T>(fn: () => Promise<T>): Promise<T | null> {
|
|
256
|
+
try {
|
|
257
|
+
return await fn();
|
|
258
|
+
} catch (err: unknown) {
|
|
259
|
+
if (err && typeof err === 'object' && 'name' in err && (err as { name: string }).name === 'ExitPromptError') {
|
|
260
|
+
return null;
|
|
261
|
+
}
|
|
262
|
+
throw err;
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
// ── Format memories ───────────────────────────────────────────────
|
|
267
|
+
|
|
268
|
+
function formatMemoryDetail(id: string, p: MemoryPayload): string {
|
|
269
|
+
const W = 62;
|
|
270
|
+
const lines: string[] = [];
|
|
271
|
+
|
|
272
|
+
lines.push(' ' + boxTop(W, `${ICONS.memo} Memory Detail`));
|
|
273
|
+
lines.push(' ' + boxEmpty(W));
|
|
274
|
+
|
|
275
|
+
// Content - may be multi-line
|
|
276
|
+
const contentLines = p.content.split('\n');
|
|
277
|
+
lines.push(' ' + boxRow(W, chalk.bold('Content')));
|
|
278
|
+
for (const cl of contentLines) {
|
|
279
|
+
lines.push(' ' + boxRow(W, ` ${chalk.white(cl)}`));
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
lines.push(' ' + boxSep(W));
|
|
283
|
+
|
|
284
|
+
// Metadata grid
|
|
285
|
+
const catColorFn = getCategoryColorFn(p.category);
|
|
286
|
+
const catIcon = getCategoryIcon(p.category);
|
|
287
|
+
|
|
288
|
+
lines.push(' ' + boxRow(W, `${muted('Category')} ${catColorFn(`${catIcon} ${p.category}`)}${p.subcategory ? muted(` / ${p.subcategory}`) : ''}`));
|
|
289
|
+
lines.push(' ' + boxRow(W, `${muted('Project')} ${info(p.project)}`));
|
|
290
|
+
lines.push(' ' + boxRow(W, `${muted('Importance')} ${importanceBadge(p.importance)}`));
|
|
291
|
+
lines.push(' ' + boxRow(W, `${muted('Source')} ${chalk.white(p.source)}`));
|
|
292
|
+
|
|
293
|
+
if (p.tags.length) {
|
|
294
|
+
const tagStr = p.tags.map(t => info(`${ICONS.tag}${t}`)).join(' ');
|
|
295
|
+
lines.push(' ' + boxRow(W, `${muted('Tags')} ${tagStr}`));
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
lines.push(' ' + boxSep(W));
|
|
299
|
+
|
|
300
|
+
// Timestamps
|
|
301
|
+
lines.push(' ' + boxRow(W, `${muted('Created')} ${chalk.white(formatDate(p.created_at))} ${dim(`(${formatAge(p.created_at)})`)}`));
|
|
302
|
+
lines.push(' ' + boxRow(W, `${muted('Updated')} ${chalk.white(formatDate(p.updated_at))}`));
|
|
303
|
+
lines.push(' ' + boxRow(W, `${muted('Accessed')} ${chalk.white(String(p.access_count))} time${p.access_count !== 1 ? 's' : ''} ${dim(`\u2022 last ${formatAge(p.accessed_at)}`)}`));
|
|
304
|
+
|
|
305
|
+
if (p.related_files?.length) {
|
|
306
|
+
lines.push(' ' + boxSep(W));
|
|
307
|
+
lines.push(' ' + boxRow(W, `${muted('Files')} ${p.related_files.map(f => dim(f)).join(', ')}`));
|
|
308
|
+
}
|
|
309
|
+
if (p.related_memory_ids?.length) {
|
|
310
|
+
if (!p.related_files?.length) lines.push(' ' + boxSep(W));
|
|
311
|
+
lines.push(' ' + boxRow(W, `${muted('Related')} ${p.related_memory_ids.map(r => dim(r.slice(0, 8) + '\u2026')).join(', ')}`));
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
lines.push(' ' + boxEmpty(W));
|
|
315
|
+
lines.push(' ' + boxRow(W, dim(`ID: ${id}`)));
|
|
316
|
+
lines.push(' ' + boxBottom(W));
|
|
317
|
+
|
|
318
|
+
return lines.join('\n');
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
function formatMemoryLine(id: string, p: MemoryPayload): string {
|
|
322
|
+
const catColorFn = getCategoryColorFn(p.category);
|
|
323
|
+
const catIconStr = getCategoryIcon(p.category);
|
|
324
|
+
const imp = importanceBadge(p.importance);
|
|
325
|
+
const age = dim(formatAge(p.created_at).padStart(7));
|
|
326
|
+
const content = chalk.white(truncate(p.content, 38));
|
|
327
|
+
const cat = catColorFn(catIconStr + ' ' + p.category.slice(0, 4).padEnd(4));
|
|
328
|
+
return `${imp} ${cat} ${content} ${age}`;
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
// ── Screens ───────────────────────────────────────────────────────
|
|
332
|
+
|
|
333
|
+
async function detailView(id: string, payload: MemoryPayload): Promise<void> {
|
|
334
|
+
while (true) {
|
|
335
|
+
console.log('\n' + formatMemoryDetail(id, payload));
|
|
336
|
+
|
|
337
|
+
const action = await safePrompt(() =>
|
|
338
|
+
select({
|
|
339
|
+
message: accent(` ${ICONS.arrow} Action`),
|
|
340
|
+
choices: [
|
|
341
|
+
{ name: `${ICONS.back} Back`, value: 'back' },
|
|
342
|
+
{ name: `${ICONS.edit} Edit`, value: 'edit' },
|
|
343
|
+
{ name: `${ICONS.trash} Delete`, value: 'delete' },
|
|
344
|
+
],
|
|
345
|
+
})
|
|
346
|
+
);
|
|
347
|
+
|
|
348
|
+
if (action === null || action === 'back') return;
|
|
349
|
+
|
|
350
|
+
if (action === 'delete') {
|
|
351
|
+
const yes = await safePrompt(() =>
|
|
352
|
+
confirm({ message: danger(` ${ICONS.trash} Delete this memory permanently?`), default: false })
|
|
353
|
+
);
|
|
354
|
+
if (yes) {
|
|
355
|
+
await deleteMemory(id);
|
|
356
|
+
console.log(success(`\n ${ICONS.check} Memory deleted.`));
|
|
357
|
+
return;
|
|
358
|
+
}
|
|
359
|
+
continue;
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
if (action === 'edit') {
|
|
363
|
+
await editMemory(id, payload);
|
|
364
|
+
const refreshed = await getMemory(id);
|
|
365
|
+
if (refreshed) {
|
|
366
|
+
payload = refreshed.payload as unknown as MemoryPayload;
|
|
367
|
+
} else {
|
|
368
|
+
console.log(warn(`\n ${ICONS.cross} Memory no longer exists.`));
|
|
369
|
+
return;
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
async function editMemory(id: string, payload: MemoryPayload): Promise<void> {
|
|
376
|
+
printScreenHeader(`${ICONS.edit} Edit Memory`, 'Select a field to modify');
|
|
377
|
+
|
|
378
|
+
const field = await safePrompt(() =>
|
|
379
|
+
select({
|
|
380
|
+
message: accent(' Field to edit'),
|
|
381
|
+
choices: [
|
|
382
|
+
{ name: `${ICONS.memo} Content: ${muted(truncate(payload.content, 35))}`, value: 'content' },
|
|
383
|
+
{ name: `${ICONS.folder} Category: ${colorCategoryName(payload.category)}`, value: 'category' },
|
|
384
|
+
{ name: `${ICONS.star} Importance: ${importanceBadge(payload.importance)}`, value: 'importance' },
|
|
385
|
+
{ name: `${ICONS.tag} Tags: ${payload.tags.length ? payload.tags.map(t => info(`#${t}`)).join(' ') : muted('none')}`, value: 'tags' },
|
|
386
|
+
{ name: `${ICONS.dot} Subcategory: ${muted(payload.subcategory || 'none')}`, value: 'subcategory' },
|
|
387
|
+
{ name: `${ICONS.folder} Project: ${info(payload.project)}`, value: 'project' },
|
|
388
|
+
{ name: `${ICONS.back} Cancel`, value: 'cancel' },
|
|
389
|
+
],
|
|
390
|
+
})
|
|
391
|
+
);
|
|
392
|
+
|
|
393
|
+
if (field === null || field === 'cancel') return;
|
|
394
|
+
|
|
395
|
+
const now = new Date().toISOString();
|
|
396
|
+
|
|
397
|
+
if (field === 'content') {
|
|
398
|
+
const val = await safePrompt(() =>
|
|
399
|
+
input({ message: ' New content:', default: payload.content })
|
|
400
|
+
);
|
|
401
|
+
if (val === null || val === payload.content) return;
|
|
402
|
+
const merged: MemoryPayload = { ...payload, content: val, updated_at: now };
|
|
403
|
+
printSpinner('Re-generating embedding...');
|
|
404
|
+
const vector = await generateEmbedding(val);
|
|
405
|
+
await updateVector(id, vector, merged);
|
|
406
|
+
console.log(success(` ${ICONS.check} Content updated & re-embedded.`));
|
|
407
|
+
} else if (field === 'category') {
|
|
408
|
+
const cats = getAllCategories();
|
|
409
|
+
const val = await safePrompt(() =>
|
|
410
|
+
select({
|
|
411
|
+
message: ' Category:',
|
|
412
|
+
choices: cats.map(c => ({
|
|
413
|
+
name: `${colorCategory(c)} ${dim(getCategoryDescription(c))}`,
|
|
414
|
+
value: c,
|
|
415
|
+
})),
|
|
416
|
+
default: payload.category,
|
|
417
|
+
})
|
|
418
|
+
);
|
|
419
|
+
if (val === null || val === payload.category) return;
|
|
420
|
+
await updatePayload(id, { category: val, updated_at: now });
|
|
421
|
+
console.log(success(` ${ICONS.check} Category ${ICONS.arrow} ${colorCategoryName(val)}`));
|
|
422
|
+
} else if (field === 'importance') {
|
|
423
|
+
const val = await safePrompt(() =>
|
|
424
|
+
input({
|
|
425
|
+
message: ' Importance (1-10):',
|
|
426
|
+
default: String(payload.importance),
|
|
427
|
+
validate: (v: string) => { const n = Number(v); return (Number.isInteger(n) && n >= 1 && n <= 10) || 'Must be 1-10'; },
|
|
428
|
+
})
|
|
429
|
+
);
|
|
430
|
+
if (val === null) return;
|
|
431
|
+
const num = Number(val);
|
|
432
|
+
if (num === payload.importance) return;
|
|
433
|
+
await updatePayload(id, { importance: num, updated_at: now });
|
|
434
|
+
console.log(success(` ${ICONS.check} Importance ${ICONS.arrow} ${importanceBadge(num)}`));
|
|
435
|
+
} else if (field === 'tags') {
|
|
436
|
+
const val = await safePrompt(() =>
|
|
437
|
+
input({
|
|
438
|
+
message: ' Tags (comma-separated):',
|
|
439
|
+
default: payload.tags.join(', '),
|
|
440
|
+
})
|
|
441
|
+
);
|
|
442
|
+
if (val === null) return;
|
|
443
|
+
const tags = val.split(',').map(t => t.trim()).filter(Boolean);
|
|
444
|
+
await updatePayload(id, { tags, updated_at: now });
|
|
445
|
+
console.log(success(` ${ICONS.check} Tags updated: ${tags.map(t => info(`#${t}`)).join(' ') || muted('none')}`));
|
|
446
|
+
} else if (field === 'subcategory') {
|
|
447
|
+
const val = await safePrompt(() =>
|
|
448
|
+
input({ message: ' Subcategory:', default: payload.subcategory || '' })
|
|
449
|
+
);
|
|
450
|
+
if (val === null) return;
|
|
451
|
+
await updatePayload(id, { subcategory: val || undefined, updated_at: now });
|
|
452
|
+
console.log(success(` ${ICONS.check} Subcategory ${ICONS.arrow} ${val || muted('none')}`));
|
|
453
|
+
} else if (field === 'project') {
|
|
454
|
+
const val = await safePrompt(() =>
|
|
455
|
+
input({ message: ' Project:', default: payload.project })
|
|
456
|
+
);
|
|
457
|
+
if (val === null) return;
|
|
458
|
+
await updatePayload(id, { project: val, updated_at: now });
|
|
459
|
+
console.log(success(` ${ICONS.check} Project ${ICONS.arrow} ${info(val)}`));
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
async function browseScreen(): Promise<void> {
|
|
464
|
+
let offset: string | undefined;
|
|
465
|
+
|
|
466
|
+
while (true) {
|
|
467
|
+
printScreenHeader(`${ICONS.brain} Browse Memories`, 'Navigate and select a memory to view details');
|
|
468
|
+
printSpinner('Loading memories...');
|
|
469
|
+
|
|
470
|
+
const result = await scrollMemories(undefined, 20, offset);
|
|
471
|
+
const points = result.points;
|
|
472
|
+
|
|
473
|
+
if (points.length === 0 && !offset) {
|
|
474
|
+
console.log(warn(`\n ${ICONS.cross} No memories found. Create some first!`));
|
|
475
|
+
await safePrompt(() => input({ message: ' Press Enter to go back' }));
|
|
476
|
+
return;
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
// Sort by created_at descending
|
|
480
|
+
points.sort((a, b) => {
|
|
481
|
+
const pa = (a.payload as unknown as MemoryPayload).created_at;
|
|
482
|
+
const pb = (b.payload as unknown as MemoryPayload).created_at;
|
|
483
|
+
return pb.localeCompare(pa);
|
|
484
|
+
});
|
|
485
|
+
|
|
486
|
+
const choices = points.map(pt => ({
|
|
487
|
+
name: ` ${formatMemoryLine(pt.id as string, pt.payload as unknown as MemoryPayload)}`,
|
|
488
|
+
value: pt.id as string,
|
|
489
|
+
}));
|
|
490
|
+
|
|
491
|
+
const nextOffset = result.next_page_offset as string | undefined;
|
|
492
|
+
if (nextOffset) {
|
|
493
|
+
choices.push({ name: info(` ${ICONS.more} Load more...`), value: '__more__' });
|
|
494
|
+
}
|
|
495
|
+
choices.push({ name: dim(` ${ICONS.back} Back to menu`), value: '__back__' });
|
|
496
|
+
|
|
497
|
+
const choice = await safePrompt(() =>
|
|
498
|
+
select({
|
|
499
|
+
message: accent(` ${ICONS.arrow} Select memory`),
|
|
500
|
+
choices,
|
|
501
|
+
pageSize: 15,
|
|
502
|
+
})
|
|
503
|
+
);
|
|
504
|
+
|
|
505
|
+
if (choice === null || choice === '__back__') return;
|
|
506
|
+
|
|
507
|
+
if (choice === '__more__') {
|
|
508
|
+
offset = nextOffset;
|
|
509
|
+
continue;
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
const mem = await getMemory(choice);
|
|
513
|
+
if (mem) {
|
|
514
|
+
await detailView(mem.id as string, mem.payload as unknown as MemoryPayload);
|
|
515
|
+
}
|
|
516
|
+
offset = undefined;
|
|
517
|
+
}
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
async function searchScreen(): Promise<void> {
|
|
521
|
+
while (true) {
|
|
522
|
+
printScreenHeader(`${ICONS.search} Search Memories`, 'Semantic search powered by local embeddings');
|
|
523
|
+
|
|
524
|
+
const query = await safePrompt(() =>
|
|
525
|
+
input({ message: accent(` ${ICONS.search} Query:`) })
|
|
526
|
+
);
|
|
527
|
+
if (query === null || query.trim() === '') return;
|
|
528
|
+
|
|
529
|
+
// Optional category filter
|
|
530
|
+
const cats = getAllCategories();
|
|
531
|
+
const catFilter = await safePrompt(() =>
|
|
532
|
+
select({
|
|
533
|
+
message: accent(' Filter by category'),
|
|
534
|
+
choices: [
|
|
535
|
+
{ name: `${ICONS.sparkle} All categories`, value: '__all__' },
|
|
536
|
+
...cats.map(c => ({ name: ` ${colorCategory(c)}`, value: c })),
|
|
537
|
+
],
|
|
538
|
+
})
|
|
539
|
+
);
|
|
540
|
+
if (catFilter === null) return;
|
|
541
|
+
|
|
542
|
+
// Optional importance filter
|
|
543
|
+
const minImpStr = await safePrompt(() =>
|
|
544
|
+
input({
|
|
545
|
+
message: accent(' Min importance (1-10, Enter = any):'),
|
|
546
|
+
default: '',
|
|
547
|
+
validate: (v: string) => v === '' || (Number.isInteger(Number(v)) && Number(v) >= 1 && Number(v) <= 10) || 'Must be 1-10 or empty',
|
|
548
|
+
})
|
|
549
|
+
);
|
|
550
|
+
if (minImpStr === null) return;
|
|
551
|
+
|
|
552
|
+
// Build search filter
|
|
553
|
+
const must: Record<string, unknown>[] = [];
|
|
554
|
+
if (catFilter !== '__all__') {
|
|
555
|
+
must.push({ key: 'category', match: { value: catFilter } });
|
|
556
|
+
}
|
|
557
|
+
if (minImpStr !== '') {
|
|
558
|
+
must.push({ key: 'importance', range: { gte: Number(minImpStr) } });
|
|
559
|
+
}
|
|
560
|
+
const filter = must.length > 0 ? { must } : undefined;
|
|
561
|
+
|
|
562
|
+
printSpinner('Generating embedding & searching...');
|
|
563
|
+
const vector = await generateEmbedding(query);
|
|
564
|
+
const results = await searchMemories(vector, 20, filter, 0.3);
|
|
565
|
+
|
|
566
|
+
if (results.length === 0) {
|
|
567
|
+
console.log(warn(`\n ${ICONS.cross} No matching memories found.`));
|
|
568
|
+
const again = await safePrompt(() =>
|
|
569
|
+
confirm({ message: accent(' Try another search?'), default: true })
|
|
570
|
+
);
|
|
571
|
+
if (again === null || !again) return;
|
|
572
|
+
continue;
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
const resultHeader = success(` ${ICONS.check} Found ${results.length} result${results.length !== 1 ? 's' : ''}`);
|
|
576
|
+
console.log(`\n${resultHeader}\n`);
|
|
577
|
+
|
|
578
|
+
const choices = results.map(r => {
|
|
579
|
+
const p = r.payload as unknown as MemoryPayload;
|
|
580
|
+
const pct = Math.round(r.score * 100);
|
|
581
|
+
const scoreBar = pct >= 80 ? success(`${pct}%`) : pct >= 50 ? warn(`${pct}%`) : dim(`${pct}%`);
|
|
582
|
+
return {
|
|
583
|
+
name: ` ${scoreBar.padEnd(18)} ${formatMemoryLine(r.id as string, p)}`,
|
|
584
|
+
value: r.id as string,
|
|
585
|
+
};
|
|
586
|
+
});
|
|
587
|
+
choices.push({ name: info(` ${ICONS.search} Search again`), value: '__again__' });
|
|
588
|
+
choices.push({ name: dim(` ${ICONS.back} Back to menu`), value: '__back__' });
|
|
589
|
+
|
|
590
|
+
const choice = await safePrompt(() =>
|
|
591
|
+
select({
|
|
592
|
+
message: accent(` ${ICONS.arrow} Select result`),
|
|
593
|
+
choices,
|
|
594
|
+
pageSize: 15,
|
|
595
|
+
})
|
|
596
|
+
);
|
|
597
|
+
|
|
598
|
+
if (choice === null || choice === '__back__') return;
|
|
599
|
+
if (choice === '__again__') continue;
|
|
600
|
+
|
|
601
|
+
const mem = await getMemory(choice);
|
|
602
|
+
if (mem) {
|
|
603
|
+
await detailView(mem.id as string, mem.payload as unknown as MemoryPayload);
|
|
604
|
+
}
|
|
605
|
+
}
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
async function createScreen(): Promise<void> {
|
|
609
|
+
printScreenHeader(`${ICONS.plus} Create Memory`, 'Store a new piece of knowledge');
|
|
610
|
+
|
|
611
|
+
// Content
|
|
612
|
+
const content = await safePrompt(() =>
|
|
613
|
+
input({
|
|
614
|
+
message: accent(` ${ICONS.memo} Content:`),
|
|
615
|
+
validate: (v: string) => v.trim().length > 0 || 'Content is required',
|
|
616
|
+
})
|
|
617
|
+
);
|
|
618
|
+
if (content === null) return;
|
|
619
|
+
|
|
620
|
+
// Category
|
|
621
|
+
const cats = getAllCategories();
|
|
622
|
+
const category = await safePrompt(() =>
|
|
623
|
+
select({
|
|
624
|
+
message: accent(` ${ICONS.folder} Category:`),
|
|
625
|
+
choices: cats.map(c => ({
|
|
626
|
+
name: ` ${colorCategory(c)} ${dim(getCategoryDescription(c))}`,
|
|
627
|
+
value: c,
|
|
628
|
+
})),
|
|
629
|
+
})
|
|
630
|
+
);
|
|
631
|
+
if (category === null) return;
|
|
632
|
+
|
|
633
|
+
// Project
|
|
634
|
+
const defaultProject = detectProject();
|
|
635
|
+
const project = await safePrompt(() =>
|
|
636
|
+
input({ message: accent(` ${ICONS.folder} Project:`), default: defaultProject })
|
|
637
|
+
);
|
|
638
|
+
if (project === null) return;
|
|
639
|
+
|
|
640
|
+
// Importance
|
|
641
|
+
const impStr = await safePrompt(() =>
|
|
642
|
+
input({
|
|
643
|
+
message: accent(` ${ICONS.star} Importance (1-10):`),
|
|
644
|
+
default: '5',
|
|
645
|
+
validate: (v: string) => { const n = Number(v); return (Number.isInteger(n) && n >= 1 && n <= 10) || 'Must be 1-10'; },
|
|
646
|
+
})
|
|
647
|
+
);
|
|
648
|
+
if (impStr === null) return;
|
|
649
|
+
const importance = Number(impStr);
|
|
650
|
+
|
|
651
|
+
// Tags
|
|
652
|
+
const tagsStr = await safePrompt(() =>
|
|
653
|
+
input({ message: accent(` ${ICONS.tag} Tags (comma-separated):`), default: '' })
|
|
654
|
+
);
|
|
655
|
+
if (tagsStr === null) return;
|
|
656
|
+
const tags = tagsStr.split(',').map(t => t.trim()).filter(Boolean);
|
|
657
|
+
|
|
658
|
+
// Source
|
|
659
|
+
const source = await safePrompt(() =>
|
|
660
|
+
select<MemorySource>({
|
|
661
|
+
message: accent(` ${ICONS.arrow} Source:`),
|
|
662
|
+
choices: [
|
|
663
|
+
{ name: ` ${chalk.white('user-told')} ${dim('User explicitly shared')}`, value: 'user-told' as MemorySource },
|
|
664
|
+
{ name: ` ${chalk.white('self-discovered')} ${dim('Found during work')}`, value: 'self-discovered' as MemorySource },
|
|
665
|
+
{ name: ` ${chalk.white('auto-saved')} ${dim('Session context')}`, value: 'auto-saved' as MemorySource },
|
|
666
|
+
],
|
|
667
|
+
default: 'user-told' as MemorySource,
|
|
668
|
+
})
|
|
669
|
+
);
|
|
670
|
+
if (source === null) return;
|
|
671
|
+
|
|
672
|
+
// Subcategory (optional)
|
|
673
|
+
const subcategory = await safePrompt(() =>
|
|
674
|
+
input({ message: accent(` ${ICONS.dot} Subcategory (optional):`), default: '' })
|
|
675
|
+
);
|
|
676
|
+
if (subcategory === null) return;
|
|
677
|
+
|
|
678
|
+
// Build payload
|
|
679
|
+
const now = new Date().toISOString();
|
|
680
|
+
const payload: MemoryPayload = {
|
|
681
|
+
content,
|
|
682
|
+
category,
|
|
683
|
+
project,
|
|
684
|
+
importance,
|
|
685
|
+
tags,
|
|
686
|
+
source,
|
|
687
|
+
subcategory: subcategory || undefined,
|
|
688
|
+
created_at: now,
|
|
689
|
+
updated_at: now,
|
|
690
|
+
accessed_at: now,
|
|
691
|
+
access_count: 0,
|
|
692
|
+
};
|
|
693
|
+
|
|
694
|
+
// Preview
|
|
695
|
+
console.log('\n' + formatMemoryDetail('(preview)', payload));
|
|
696
|
+
|
|
697
|
+
const ok = await safePrompt(() =>
|
|
698
|
+
confirm({ message: accent(` ${ICONS.sparkle} Save this memory?`), default: true })
|
|
699
|
+
);
|
|
700
|
+
if (ok === null || !ok) {
|
|
701
|
+
console.log(warn(` ${ICONS.cross} Cancelled.`));
|
|
702
|
+
return;
|
|
703
|
+
}
|
|
704
|
+
|
|
705
|
+
const id = uuidv4();
|
|
706
|
+
printSpinner('Generating embedding...');
|
|
707
|
+
const vector = await generateEmbedding(content);
|
|
708
|
+
await upsertMemory(id, vector, payload);
|
|
709
|
+
console.log(success(`\n ${ICONS.check} Memory saved!`) + dim(` ID: ${id}`));
|
|
710
|
+
}
|
|
711
|
+
|
|
712
|
+
async function categoriesScreen(): Promise<void> {
|
|
713
|
+
while (true) {
|
|
714
|
+
printScreenHeader(`${ICONS.folder} Categories`, 'Manage memory categories');
|
|
715
|
+
|
|
716
|
+
const action = await safePrompt(() =>
|
|
717
|
+
select({
|
|
718
|
+
message: accent(` ${ICONS.arrow} Action`),
|
|
719
|
+
choices: [
|
|
720
|
+
{ name: ` ${ICONS.memo} List all categories`, value: 'list' },
|
|
721
|
+
{ name: ` ${ICONS.plus} Create custom category`, value: 'create' },
|
|
722
|
+
{ name: ` ${ICONS.trash} Delete custom category`, value: 'delete' },
|
|
723
|
+
{ name: dim(` ${ICONS.back} Back to menu`), value: 'back' },
|
|
724
|
+
],
|
|
725
|
+
})
|
|
726
|
+
);
|
|
727
|
+
|
|
728
|
+
if (action === null || action === 'back') return;
|
|
729
|
+
|
|
730
|
+
if (action === 'list') {
|
|
731
|
+
const W = 52;
|
|
732
|
+
console.log('\n ' + boxTop(W, 'All Categories'));
|
|
733
|
+
|
|
734
|
+
const allCats = getCategories();
|
|
735
|
+
for (const c of allCats) {
|
|
736
|
+
console.log(' ' + boxRow(W, `${colorCategory(c.name)} ${dim(c.description)}`));
|
|
737
|
+
}
|
|
738
|
+
|
|
739
|
+
console.log(' ' + boxBottom(W));
|
|
740
|
+
console.log('');
|
|
741
|
+
}
|
|
742
|
+
|
|
743
|
+
if (action === 'create') {
|
|
744
|
+
const name = await safePrompt(() =>
|
|
745
|
+
input({
|
|
746
|
+
message: accent(' Category name (a-z, 0-9, hyphens):'),
|
|
747
|
+
validate: (v: string) => {
|
|
748
|
+
const result = validateCategoryName(v);
|
|
749
|
+
return result.valid || result.error!;
|
|
750
|
+
},
|
|
751
|
+
})
|
|
752
|
+
);
|
|
753
|
+
if (name === null) continue;
|
|
754
|
+
|
|
755
|
+
const description = await safePrompt(() =>
|
|
756
|
+
input({ message: accent(' Description:'), validate: (v: string) => v.trim().length > 0 || 'Required' })
|
|
757
|
+
);
|
|
758
|
+
if (description === null) continue;
|
|
759
|
+
|
|
760
|
+
addCategory(name, description);
|
|
761
|
+
console.log(success(`\n ${ICONS.check} Category "${name}" created.`));
|
|
762
|
+
}
|
|
763
|
+
|
|
764
|
+
if (action === 'delete') {
|
|
765
|
+
const custom = getCustomCategories();
|
|
766
|
+
if (custom.length === 0) {
|
|
767
|
+
console.log(warn(`\n ${ICONS.cross} No custom categories to delete.`));
|
|
768
|
+
continue;
|
|
769
|
+
}
|
|
770
|
+
|
|
771
|
+
const name = await safePrompt(() =>
|
|
772
|
+
select({
|
|
773
|
+
message: accent(' Delete which category?'),
|
|
774
|
+
choices: [
|
|
775
|
+
...custom.map(c => ({
|
|
776
|
+
name: ` ${chalk.hex('#FF6B9D')(`${ICONS.sparkle} ${c.name}`)} ${dim(`- ${c.description}`)}`,
|
|
777
|
+
value: c.name,
|
|
778
|
+
})),
|
|
779
|
+
{ name: dim(` ${ICONS.back} Cancel`), value: '__cancel__' },
|
|
780
|
+
],
|
|
781
|
+
})
|
|
782
|
+
);
|
|
783
|
+
if (name === null || name === '__cancel__') continue;
|
|
784
|
+
|
|
785
|
+
const count = await countMemories({
|
|
786
|
+
must: [{ key: 'category', match: { value: name } }],
|
|
787
|
+
});
|
|
788
|
+
|
|
789
|
+
if (count > 0) {
|
|
790
|
+
console.log(warn(`\n ${ICONS.cross} ${count} memories use this category.`));
|
|
791
|
+
const reassign = await safePrompt(() =>
|
|
792
|
+
confirm({ message: accent(' Reassign them to another category?'), default: true })
|
|
793
|
+
);
|
|
794
|
+
if (reassign === null || !reassign) continue;
|
|
795
|
+
|
|
796
|
+
const target = await safePrompt(() =>
|
|
797
|
+
select({
|
|
798
|
+
message: accent(' Reassign to:'),
|
|
799
|
+
choices: getAllCategories()
|
|
800
|
+
.filter(c => c !== name)
|
|
801
|
+
.map(c => ({ name: ` ${colorCategory(c)}`, value: c })),
|
|
802
|
+
})
|
|
803
|
+
);
|
|
804
|
+
if (target === null) continue;
|
|
805
|
+
|
|
806
|
+
await updatePayloadByFilter(
|
|
807
|
+
{ must: [{ key: 'category', match: { value: name } }] },
|
|
808
|
+
{ category: target, updated_at: new Date().toISOString() }
|
|
809
|
+
);
|
|
810
|
+
console.log(success(` ${ICONS.check} Reassigned ${count} memories to ${colorCategoryName(target)}.`));
|
|
811
|
+
}
|
|
812
|
+
|
|
813
|
+
const ok = await safePrompt(() =>
|
|
814
|
+
confirm({ message: danger(` ${ICONS.trash} Delete category "${name}"?`), default: false })
|
|
815
|
+
);
|
|
816
|
+
if (ok) {
|
|
817
|
+
removeCategory(name);
|
|
818
|
+
console.log(success(` ${ICONS.check} Category "${name}" deleted.`));
|
|
819
|
+
}
|
|
820
|
+
}
|
|
821
|
+
}
|
|
822
|
+
}
|
|
823
|
+
|
|
824
|
+
async function statsScreen(): Promise<void> {
|
|
825
|
+
printScreenHeader(`${ICONS.chart} Statistics`, 'Memory database overview');
|
|
826
|
+
printSpinner('Loading statistics...');
|
|
827
|
+
const stats = await getMemoryStats();
|
|
828
|
+
|
|
829
|
+
const W = 56;
|
|
830
|
+
console.log('\n ' + boxTop(W, 'Overview'));
|
|
831
|
+
console.log(' ' + boxRow(W, `${chalk.bold.white(String(stats.total))} total memories stored`));
|
|
832
|
+
if (stats.oldest) {
|
|
833
|
+
console.log(' ' + boxRow(W, `${muted('Oldest:')} ${chalk.white(formatDate(stats.oldest))} ${dim(`(${formatAge(stats.oldest)})`)}`));
|
|
834
|
+
}
|
|
835
|
+
if (stats.newest) {
|
|
836
|
+
console.log(' ' + boxRow(W, `${muted('Newest:')} ${chalk.white(formatDate(stats.newest))} ${dim(`(${formatAge(stats.newest)})`)}`));
|
|
837
|
+
}
|
|
838
|
+
console.log(' ' + boxBottom(W));
|
|
839
|
+
|
|
840
|
+
// Category chart
|
|
841
|
+
const catEntries = Object.entries(stats.by_category)
|
|
842
|
+
.filter(([, count]) => count > 0)
|
|
843
|
+
.sort((a, b) => b[1] - a[1]);
|
|
844
|
+
|
|
845
|
+
if (catEntries.length > 0) {
|
|
846
|
+
console.log('\n ' + boxTop(W, 'By Category'));
|
|
847
|
+
const maxCount = Math.max(...catEntries.map(([, c]) => c));
|
|
848
|
+
const barWidth = 24;
|
|
849
|
+
for (const [cat, count] of catEntries) {
|
|
850
|
+
const catColorFn = getCategoryColorFn(cat);
|
|
851
|
+
const catIconStr = getCategoryIcon(cat);
|
|
852
|
+
const filled = Math.max(1, Math.round((count / maxCount) * barWidth));
|
|
853
|
+
const empty = barWidth - filled;
|
|
854
|
+
const bar = catColorFn('\u2588'.repeat(filled)) + dim('\u2591'.repeat(empty));
|
|
855
|
+
// icon(1) + space(1) + name + padding = 16 chars visual
|
|
856
|
+
const nameVisual = `${catIconStr} ${cat}`;
|
|
857
|
+
const padLen = Math.max(1, 16 - nameVisual.length);
|
|
858
|
+
const label = catColorFn(nameVisual) + ' '.repeat(padLen);
|
|
859
|
+
console.log(' ' + boxRow(W, `${label}${bar} ${chalk.bold.white(String(count).padStart(3))}`));
|
|
860
|
+
}
|
|
861
|
+
console.log(' ' + boxBottom(W));
|
|
862
|
+
}
|
|
863
|
+
|
|
864
|
+
// Project breakdown
|
|
865
|
+
const projEntries = Object.entries(stats.by_project)
|
|
866
|
+
.sort((a, b) => b[1] - a[1]);
|
|
867
|
+
|
|
868
|
+
if (projEntries.length > 0) {
|
|
869
|
+
console.log('\n ' + boxTop(W, 'By Project'));
|
|
870
|
+
for (const [proj, count] of projEntries) {
|
|
871
|
+
const pct = Math.round((count / stats.total) * 100);
|
|
872
|
+
const pctBar = dim(`(${pct}%)`);
|
|
873
|
+
console.log(' ' + boxRow(W, `${info(proj.padEnd(20))} ${chalk.bold.white(String(count).padStart(4))} ${pctBar}`));
|
|
874
|
+
}
|
|
875
|
+
console.log(' ' + boxBottom(W));
|
|
876
|
+
}
|
|
877
|
+
|
|
878
|
+
console.log('');
|
|
879
|
+
await safePrompt(() => input({ message: dim(' Press Enter to go back') }));
|
|
880
|
+
}
|
|
881
|
+
|
|
882
|
+
// ── Main Menu ─────────────────────────────────────────────────────
|
|
883
|
+
|
|
884
|
+
async function mainMenu(): Promise<void> {
|
|
885
|
+
printLogo();
|
|
886
|
+
|
|
887
|
+
while (true) {
|
|
888
|
+
const choice = await safePrompt(() =>
|
|
889
|
+
select({
|
|
890
|
+
message: accent(` ${ICONS.arrow} Main Menu`),
|
|
891
|
+
choices: [
|
|
892
|
+
{ name: ` ${accent2(ICONS.brain)} ${chalk.white('Browse Memories')} ${dim('Paginated list')}`, value: 'browse' },
|
|
893
|
+
{ name: ` ${accent2(ICONS.search)} ${chalk.white('Search Memories')} ${dim('Semantic search')}`, value: 'search' },
|
|
894
|
+
{ name: ` ${accent2(ICONS.plus)} ${chalk.white('Create Memory')} ${dim('Store new knowledge')}`, value: 'create' },
|
|
895
|
+
{ name: ` ${accent2(ICONS.folder)} ${chalk.white('Manage Categories')} ${dim('Create & organize')}`, value: 'categories' },
|
|
896
|
+
{ name: ` ${accent2(ICONS.chart)} ${chalk.white('Statistics')} ${dim('Database overview')}`, value: 'stats' },
|
|
897
|
+
{ name: dim(` ${ICONS.exit} Exit`), value: 'exit' },
|
|
898
|
+
],
|
|
899
|
+
})
|
|
900
|
+
);
|
|
901
|
+
|
|
902
|
+
if (choice === null || choice === 'exit') {
|
|
903
|
+
console.log(dim(`\n ${ICONS.exit} Goodbye!\n`));
|
|
904
|
+
process.exit(0);
|
|
905
|
+
}
|
|
906
|
+
|
|
907
|
+
switch (choice) {
|
|
908
|
+
case 'browse': await browseScreen(); break;
|
|
909
|
+
case 'search': await searchScreen(); break;
|
|
910
|
+
case 'create': await createScreen(); break;
|
|
911
|
+
case 'categories': await categoriesScreen(); break;
|
|
912
|
+
case 'stats': await statsScreen(); break;
|
|
913
|
+
}
|
|
914
|
+
}
|
|
915
|
+
}
|
|
916
|
+
|
|
917
|
+
// ── Entry Point ───────────────────────────────────────────────────
|
|
918
|
+
|
|
919
|
+
async function main(): Promise<void> {
|
|
920
|
+
try {
|
|
921
|
+
// Suppress API key warnings
|
|
922
|
+
const origWarn = console.warn;
|
|
923
|
+
console.warn = (...args: unknown[]) => {
|
|
924
|
+
const msg = String(args[0] || '');
|
|
925
|
+
if (msg.includes('unsecure connection')) return;
|
|
926
|
+
origWarn.apply(console, args);
|
|
927
|
+
};
|
|
928
|
+
|
|
929
|
+
await ensureCollection();
|
|
930
|
+
|
|
931
|
+
console.warn = origWarn;
|
|
932
|
+
|
|
933
|
+
await mainMenu();
|
|
934
|
+
} catch (err) {
|
|
935
|
+
console.error(danger(`\n ${ICONS.cross} Fatal error:`), err);
|
|
936
|
+
process.exit(1);
|
|
937
|
+
}
|
|
938
|
+
}
|
|
939
|
+
|
|
940
|
+
main();
|