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.
Files changed (187) hide show
  1. package/CHANGELOG.md +665 -0
  2. package/LICENSE +191 -0
  3. package/README.md +853 -0
  4. package/hooks/claude-code/post-plan.mjs +391 -0
  5. package/hooks/claude-code/post-remember.mjs +239 -0
  6. package/hooks/claude-code/pre-compact.mjs +241 -0
  7. package/hooks/claude-code/pre-websearch.mjs +90 -0
  8. package/hooks/claude-code/prompt-submit.mjs +861 -0
  9. package/hooks/claude-code/session-start.mjs +494 -0
  10. package/hooks/claude-code/shared.mjs +368 -0
  11. package/hooks/claude-code/stop.mjs +646 -0
  12. package/hooks/claude-code/test-user-learning.mjs +531 -0
  13. package/mcp-server/package.json +38 -0
  14. package/mcp-server/run.mjs +10 -0
  15. package/mcp-server/src/config.ts +70 -0
  16. package/mcp-server/src/http.ts +93 -0
  17. package/mcp-server/src/index.ts +203 -0
  18. package/mcp-server/src/preload-http.ts +25 -0
  19. package/mcp-server/src/preload.ts +29 -0
  20. package/mcp-server/src/scripts/migrate-qdrant-to-sqlite.ts +506 -0
  21. package/mcp-server/src/services/categories.ts +429 -0
  22. package/mcp-server/src/services/discord.ts +702 -0
  23. package/mcp-server/src/services/file-checksums.ts +33 -0
  24. package/mcp-server/src/services/local-embeddings.ts +77 -0
  25. package/mcp-server/src/services/neural-interface.ts +344 -0
  26. package/mcp-server/src/services/sqlite.ts +823 -0
  27. package/mcp-server/src/tools/browser-advanced.ts +123 -0
  28. package/mcp-server/src/tools/browser-interact.ts +255 -0
  29. package/mcp-server/src/tools/browser-navigate.ts +77 -0
  30. package/mcp-server/src/tools/browser-observe.ts +1330 -0
  31. package/mcp-server/src/tools/browser.ts +107 -0
  32. package/mcp-server/src/tools/card-tools.ts +193 -0
  33. package/mcp-server/src/tools/card.ts +22 -0
  34. package/mcp-server/src/tools/category.ts +379 -0
  35. package/mcp-server/src/tools/discord-channel.ts +274 -0
  36. package/mcp-server/src/tools/discord-guild.ts +188 -0
  37. package/mcp-server/src/tools/discord-member.ts +158 -0
  38. package/mcp-server/src/tools/discord-message.ts +231 -0
  39. package/mcp-server/src/tools/discord-onboarding.ts +228 -0
  40. package/mcp-server/src/tools/discord-role.ts +209 -0
  41. package/mcp-server/src/tools/discord-thread.ts +110 -0
  42. package/mcp-server/src/tools/discord-webhook.ts +176 -0
  43. package/mcp-server/src/tools/discord.ts +40 -0
  44. package/mcp-server/src/tools/forget.ts +38 -0
  45. package/mcp-server/src/tools/git-tools.ts +141 -0
  46. package/mcp-server/src/tools/git.ts +13 -0
  47. package/mcp-server/src/tools/image-tools.ts +98 -0
  48. package/mcp-server/src/tools/image.ts +17 -0
  49. package/mcp-server/src/tools/leonardo-browser-tools.ts +281 -0
  50. package/mcp-server/src/tools/leonardo.ts +28 -0
  51. package/mcp-server/src/tools/loop.ts +348 -0
  52. package/mcp-server/src/tools/memories.ts +118 -0
  53. package/mcp-server/src/tools/recall.ts +323 -0
  54. package/mcp-server/src/tools/reflect.ts +145 -0
  55. package/mcp-server/src/tools/remember.ts +115 -0
  56. package/mcp-server/src/tools/response.ts +26 -0
  57. package/mcp-server/src/tools/restore.ts +34 -0
  58. package/mcp-server/src/tools/sync.ts +114 -0
  59. package/mcp-server/src/tools/tictactoe-tools.ts +98 -0
  60. package/mcp-server/src/tools/tictactoe.ts +13 -0
  61. package/mcp-server/src/tools/utils.ts +25 -0
  62. package/mcp-server/src/tools/whiteboard-tools.ts +260 -0
  63. package/mcp-server/src/tools/whiteboard.ts +22 -0
  64. package/mcp-server/src/tui.ts +940 -0
  65. package/mcp-server/src/types.ts +85 -0
  66. package/mcp-server/tsconfig.json +19 -0
  67. package/neural-interface/package.json +27 -0
  68. package/neural-interface/public/apple-touch-icon.png +0 -0
  69. package/neural-interface/public/category-logos/claude.svg +1 -0
  70. package/neural-interface/public/category-logos/gemini-color.svg +1 -0
  71. package/neural-interface/public/category-logos/openai.svg +1 -0
  72. package/neural-interface/public/category-logos/synabun.png +0 -0
  73. package/neural-interface/public/claude-chat.css +878 -0
  74. package/neural-interface/public/claude-chat.html +92 -0
  75. package/neural-interface/public/claude-chat.js +1168 -0
  76. package/neural-interface/public/favicon-16x16.png +0 -0
  77. package/neural-interface/public/favicon-32x32.png +0 -0
  78. package/neural-interface/public/gallery.svg +6 -0
  79. package/neural-interface/public/icon-192.png +0 -0
  80. package/neural-interface/public/icon-512.png +0 -0
  81. package/neural-interface/public/index-legacy.html +14677 -0
  82. package/neural-interface/public/index.html +212 -0
  83. package/neural-interface/public/index2d-legacy.html +8193 -0
  84. package/neural-interface/public/index2d.html +127 -0
  85. package/neural-interface/public/invite.html +351 -0
  86. package/neural-interface/public/leonardoai.svg +1 -0
  87. package/neural-interface/public/logo-synabun.png +0 -0
  88. package/neural-interface/public/logoHD.png +0 -0
  89. package/neural-interface/public/manifest.json +14 -0
  90. package/neural-interface/public/offline.html +299 -0
  91. package/neural-interface/public/onboarding.html +2335 -0
  92. package/neural-interface/public/openclaw-logo-text.png +0 -0
  93. package/neural-interface/public/pin-svgrepo-com.svg +25 -0
  94. package/neural-interface/public/sessionicon.svg +8 -0
  95. package/neural-interface/public/shared/ansi-parser.js +305 -0
  96. package/neural-interface/public/shared/api.js +1043 -0
  97. package/neural-interface/public/shared/colors.js +153 -0
  98. package/neural-interface/public/shared/constants.js +141 -0
  99. package/neural-interface/public/shared/html-shell.js +777 -0
  100. package/neural-interface/public/shared/html-term-renderer.js +1022 -0
  101. package/neural-interface/public/shared/i18n.js +147 -0
  102. package/neural-interface/public/shared/registry.js +82 -0
  103. package/neural-interface/public/shared/state.js +86 -0
  104. package/neural-interface/public/shared/storage.js +150 -0
  105. package/neural-interface/public/shared/styles.css +21212 -0
  106. package/neural-interface/public/shared/ui-automation-studio.js +3573 -0
  107. package/neural-interface/public/shared/ui-bookmarks.js +231 -0
  108. package/neural-interface/public/shared/ui-claude-panel.js +6333 -0
  109. package/neural-interface/public/shared/ui-command-runner.js +534 -0
  110. package/neural-interface/public/shared/ui-cost-widget.js +602 -0
  111. package/neural-interface/public/shared/ui-detail.js +1454 -0
  112. package/neural-interface/public/shared/ui-explorer.js +1356 -0
  113. package/neural-interface/public/shared/ui-file-explorer.js +2750 -0
  114. package/neural-interface/public/shared/ui-games.js +411 -0
  115. package/neural-interface/public/shared/ui-help.js +107 -0
  116. package/neural-interface/public/shared/ui-image-gallery.js +461 -0
  117. package/neural-interface/public/shared/ui-invite.js +519 -0
  118. package/neural-interface/public/shared/ui-keybinds.js +452 -0
  119. package/neural-interface/public/shared/ui-layouts.js +297 -0
  120. package/neural-interface/public/shared/ui-link.js +731 -0
  121. package/neural-interface/public/shared/ui-loading.js +255 -0
  122. package/neural-interface/public/shared/ui-menubar.js +499 -0
  123. package/neural-interface/public/shared/ui-multiselect.js +93 -0
  124. package/neural-interface/public/shared/ui-navbar.js +222 -0
  125. package/neural-interface/public/shared/ui-notifications.js +287 -0
  126. package/neural-interface/public/shared/ui-panels.js +309 -0
  127. package/neural-interface/public/shared/ui-resume.js +653 -0
  128. package/neural-interface/public/shared/ui-search.js +145 -0
  129. package/neural-interface/public/shared/ui-sessions.js +531 -0
  130. package/neural-interface/public/shared/ui-settings.js +5987 -0
  131. package/neural-interface/public/shared/ui-sidebar.js +1905 -0
  132. package/neural-interface/public/shared/ui-skills.js +2805 -0
  133. package/neural-interface/public/shared/ui-stats.js +38 -0
  134. package/neural-interface/public/shared/ui-sync.js +201 -0
  135. package/neural-interface/public/shared/ui-terminal.js +6385 -0
  136. package/neural-interface/public/shared/ui-tooltip.js +123 -0
  137. package/neural-interface/public/shared/ui-trash.js +439 -0
  138. package/neural-interface/public/shared/ui-tutorial-draw.js +875 -0
  139. package/neural-interface/public/shared/ui-tutorial-steps.js +2276 -0
  140. package/neural-interface/public/shared/ui-tutorial.js +1117 -0
  141. package/neural-interface/public/shared/ui-update.js +244 -0
  142. package/neural-interface/public/shared/ui-whiteboard.js +3262 -0
  143. package/neural-interface/public/shared/ui-workspaces.js +722 -0
  144. package/neural-interface/public/shared/utils.js +360 -0
  145. package/neural-interface/public/shared/vterm-buffer.js +1102 -0
  146. package/neural-interface/public/sw.js +52 -0
  147. package/neural-interface/public/synabun.png +0 -0
  148. package/neural-interface/public/toolbar-album.svg +8 -0
  149. package/neural-interface/public/toolbar-question.svg +6 -0
  150. package/neural-interface/public/toolbar-sessions.svg +6 -0
  151. package/neural-interface/public/variant/2d/background.js +113 -0
  152. package/neural-interface/public/variant/2d/context-menu.js +133 -0
  153. package/neural-interface/public/variant/2d/gfx.js +45 -0
  154. package/neural-interface/public/variant/2d/graph.js +1635 -0
  155. package/neural-interface/public/variant/2d/hulls.js +207 -0
  156. package/neural-interface/public/variant/2d/lasso.js +135 -0
  157. package/neural-interface/public/variant/2d/layout.js +594 -0
  158. package/neural-interface/public/variant/2d/main.js +521 -0
  159. package/neural-interface/public/variant/2d/minimap.js +312 -0
  160. package/neural-interface/public/variant/2d/settings-gfx.js +98 -0
  161. package/neural-interface/public/variant/3d/background.js +884 -0
  162. package/neural-interface/public/variant/3d/camera.js +661 -0
  163. package/neural-interface/public/variant/3d/gfx.js +116 -0
  164. package/neural-interface/public/variant/3d/graph.js +2399 -0
  165. package/neural-interface/public/variant/3d/main.js +500 -0
  166. package/neural-interface/public/variant/3d/settings-gfx.js +228 -0
  167. package/neural-interface/scripts/rebuild-pty.js +100 -0
  168. package/neural-interface/server.js +15000 -0
  169. package/neural-interface/templates/CLAUDE-template.md +211 -0
  170. package/package.json +53 -0
  171. package/postinstall.js +135 -0
  172. package/setup.js +260 -0
  173. package/skills/leonardo/SKILL.md +159 -0
  174. package/skills/leonardo/icon.svg +1 -0
  175. package/skills/leonardo/modules/browser-guide.md +378 -0
  176. package/skills/leonardo/modules/image-prompter.md +277 -0
  177. package/skills/leonardo/modules/model-advisor.md +100 -0
  178. package/skills/leonardo/modules/prompt-library.md +110 -0
  179. package/skills/leonardo/modules/style-guide.md +186 -0
  180. package/skills/leonardo/modules/video-prompter.md +311 -0
  181. package/skills/synabun/SKILL.md +185 -0
  182. package/skills/synabun/icon.png +0 -0
  183. package/skills/synabun/modules/audit.md +374 -0
  184. package/skills/synabun/modules/changelog.md +194 -0
  185. package/skills/synabun/modules/idea.md +144 -0
  186. package/skills/synabun/modules/memorize.md +188 -0
  187. package/skills/synabun/synabunicon.png +0 -0
@@ -0,0 +1,118 @@
1
+ import { z } from 'zod';
2
+ import { scrollMemories, getMemoryStats } from '../services/sqlite.js';
3
+ import { validateCategory } from '../services/categories.js';
4
+ import type { MemoryPayload } from '../types.js';
5
+ import { text } from './response.js';
6
+
7
+ export function buildMemoriesSchema() {
8
+ return {
9
+ action: z
10
+ .enum(['recent', 'stats', 'by-category', 'by-project'] as const)
11
+ .describe(
12
+ 'recent=latest memories, stats=counts and health, by-category=filter by type, by-project=filter by project.'
13
+ ),
14
+ category: z
15
+ .string()
16
+ .optional()
17
+ .describe('Category to filter by (for by-category action).'),
18
+ project: z
19
+ .string()
20
+ .optional()
21
+ .describe('Filter by project (for by-project action).'),
22
+ limit: z
23
+ .coerce.number()
24
+ .min(1)
25
+ .max(50)
26
+ .optional()
27
+ .describe('Number of results (default 10).'),
28
+ };
29
+ }
30
+
31
+ export const memoriesSchema = buildMemoriesSchema();
32
+
33
+ export const memoriesDescription =
34
+ 'Browse recent memories or get statistics. Use this to see what you remember about a project, review recent learnings, or check memory health.';
35
+
36
+ function formatAge(isoDate: string): string {
37
+ const diffMs = Date.now() - new Date(isoDate).getTime();
38
+ const days = Math.floor(diffMs / (1000 * 60 * 60 * 24));
39
+ if (days === 0) return 'today';
40
+ if (days === 1) return '1 day ago';
41
+ if (days < 30) return `${days} days ago`;
42
+ const months = Math.floor(days / 30);
43
+ if (months === 1) return '1 month ago';
44
+ return `${months} months ago`;
45
+ }
46
+
47
+ export async function handleMemories(args: {
48
+ action: string;
49
+ category?: string;
50
+ project?: string;
51
+ limit?: number;
52
+ }) {
53
+ const action = args.action;
54
+ const category = args.category;
55
+ const project = args.project;
56
+ const limit = args.limit ?? 10;
57
+
58
+ if (category) {
59
+ const catCheck = validateCategory(category);
60
+ if (!catCheck.valid) {
61
+ return text(catCheck.error!);
62
+ }
63
+ }
64
+
65
+ if (action === 'stats') {
66
+ const stats = await getMemoryStats();
67
+
68
+ const categoryLines = Object.entries(stats.by_category)
69
+ .filter(([, count]) => count > 0)
70
+ .map(([cat, count]) => ` ${cat}: ${count}`)
71
+ .join('\n');
72
+
73
+ const projectLines = Object.entries(stats.by_project)
74
+ .map(([proj, count]) => ` ${proj}: ${count}`)
75
+ .join('\n');
76
+
77
+ return text(`Memory Statistics:\n\nTotal memories: ${stats.total}\n\nBy category:\n${categoryLines || ' (none)'}\n\nBy project:\n${projectLines || ' (none)'}\n\nOldest: ${stats.oldest ? formatAge(stats.oldest) : 'n/a'}\nNewest: ${stats.newest ? formatAge(stats.newest) : 'n/a'}`);
78
+ }
79
+
80
+ const must: Record<string, unknown>[] = [];
81
+ if ((action === 'by-category' || action === 'recent') && category) {
82
+ must.push({ key: 'category', match: { value: category } });
83
+ }
84
+ if ((action === 'by-project' || action === 'recent') && project) {
85
+ must.push({ key: 'project', match: { value: project } });
86
+ }
87
+
88
+ const filter = must.length > 0 ? { must } : undefined;
89
+ const result = await scrollMemories(filter, limit);
90
+
91
+ const sorted = result.points
92
+ .map((p) => ({
93
+ id: p.id as string,
94
+ payload: p.payload as unknown as MemoryPayload,
95
+ }))
96
+ .filter((m) => m.payload?.content && m.payload?.created_at)
97
+ .sort((a, b) => b.payload.created_at.localeCompare(a.payload.created_at));
98
+
99
+ if (sorted.length === 0) {
100
+ return text(`No memories found${category ? ` for category "${category}"` : ''}${project ? ` for project "${project}"` : ''}.`);
101
+ }
102
+
103
+ const lines = sorted.map((m, i) => {
104
+ const p = m.payload;
105
+ const tagStr = p.tags?.length ? ` [${p.tags.join(', ')}]` : '';
106
+ const sub = p.subcategory ? `/${p.subcategory}` : '';
107
+ return `${i + 1}. [${m.id}] ${p.category}${sub} | ${p.project} | imp:${p.importance} | ${formatAge(p.created_at)}${tagStr}\n ${p.content.slice(0, 150)}${p.content.length > 150 ? '...' : ''}`;
108
+ });
109
+
110
+ const title =
111
+ action === 'recent'
112
+ ? 'Recent memories'
113
+ : action === 'by-category'
114
+ ? `Memories in "${category}"`
115
+ : `Memories for "${project}"`;
116
+
117
+ return text(`${title} (${sorted.length}):\n\n${lines.join('\n\n')}`);
118
+ }
@@ -0,0 +1,323 @@
1
+ import { z } from 'zod';
2
+ import { readFileSync } from 'node:fs';
3
+ import path from 'node:path';
4
+ import { generateEmbedding } from '../services/local-embeddings.js';
5
+ import { searchMemories, searchMemoriesFTS, updatePayload, searchSessionChunks } from '../services/sqlite.js';
6
+ import { validateCategory } from '../services/categories.js';
7
+ import { coerceStringArray } from './utils.js';
8
+ import type { MemoryPayload, SessionChunkPayload } from '../types.js';
9
+ import { config, detectProject } from '../config.js';
10
+ import { text } from './response.js';
11
+
12
+ export function buildRecallSchema() {
13
+ return {
14
+ query: z.string().describe('What to search for, in natural language.'),
15
+ category: z
16
+ .string()
17
+ .optional()
18
+ .describe('Optional: filter by category name.'),
19
+ project: z
20
+ .string()
21
+ .optional()
22
+ .describe(
23
+ 'Optional: filter by project. If omitted, searches all projects but boosts current project.'
24
+ ),
25
+ tags: coerceStringArray()
26
+ .optional()
27
+ .describe('Optional: filter by tags (any match).'),
28
+ limit: z
29
+ .coerce.number()
30
+ .min(1)
31
+ .max(20)
32
+ .optional()
33
+ .describe('Number of results (default 5).'),
34
+ min_importance: z
35
+ .coerce.number()
36
+ .min(1)
37
+ .max(10)
38
+ .optional()
39
+ .describe('Minimum importance threshold.'),
40
+ min_score: z
41
+ .coerce.number()
42
+ .min(0)
43
+ .max(1)
44
+ .optional()
45
+ .describe('Minimum similarity score 0-1 (default 0.3).'),
46
+ include_sessions: z
47
+ .boolean()
48
+ .optional()
49
+ .describe('Override session chunk search. Auto-triggers on temporal queries or sparse results. Set true to force, false to disable.'),
50
+ recency_boost: z
51
+ .boolean()
52
+ .optional()
53
+ .describe('Prioritize recent memories. Shifts scoring to favor recency over semantic similarity (14-day half-life, 55% recency weight). Ideal for session-start boot queries.'),
54
+ };
55
+ }
56
+
57
+ export const recallSchema = buildRecallSchema();
58
+
59
+ export const recallDescription =
60
+ 'Search your persistent memory for relevant information. Use this at the start of any task to check what you already know, or when you need context about past decisions, known issues, or architectural patterns.';
61
+
62
+ function applyTimeDecay(
63
+ rawScore: number,
64
+ createdAt: string,
65
+ importance: number,
66
+ accessCount: number,
67
+ recencyBoost = false
68
+ ): number {
69
+ const ageInDays =
70
+ (Date.now() - new Date(createdAt).getTime()) / (1000 * 60 * 60 * 24);
71
+ const accessBoost = Math.min(0.1, accessCount * 0.01);
72
+
73
+ if (recencyBoost) {
74
+ // Recency-first scoring: 14-day half-life, recency dominates
75
+ const recencyMultiplier = Math.pow(0.5, ageInDays / 14);
76
+ return rawScore * 0.35 + recencyMultiplier * 0.55 + accessBoost;
77
+ }
78
+
79
+ // Default scoring: semantic similarity dominates, 90-day half-life
80
+ if (importance >= 8) return rawScore;
81
+ const recencyMultiplier = Math.pow(0.5, ageInDays / 90);
82
+ return rawScore * 0.7 + recencyMultiplier * 0.2 + accessBoost;
83
+ }
84
+
85
+ function formatAge(isoDate: string): string {
86
+ const diffMs = Date.now() - new Date(isoDate).getTime();
87
+ const days = Math.floor(diffMs / (1000 * 60 * 60 * 24));
88
+ if (days === 0) return 'today';
89
+ if (days === 1) return '1 day ago';
90
+ if (days < 30) return `${days} days ago`;
91
+ const months = Math.floor(days / 30);
92
+ if (months === 1) return '1 month ago';
93
+ return `${months} months ago`;
94
+ }
95
+
96
+ interface RecallDefaults {
97
+ limit: number;
98
+ minImportance: number;
99
+ minScore: number;
100
+ maxChars: number;
101
+ includeSessions: 'auto' | 'always' | 'never';
102
+ recencyBoost: boolean;
103
+ }
104
+
105
+ const RECALL_DEFAULTS: RecallDefaults = {
106
+ limit: 5,
107
+ minImportance: 0,
108
+ minScore: 0.3,
109
+ maxChars: 0,
110
+ includeSessions: 'auto',
111
+ recencyBoost: false,
112
+ };
113
+
114
+ function getRecallDefaults(): RecallDefaults {
115
+ try {
116
+ const settingsPath = path.resolve(config.dataDir, 'display-settings.json');
117
+ const data = JSON.parse(readFileSync(settingsPath, 'utf-8'));
118
+ const d = data.recallDefaults;
119
+ if (!d) {
120
+ // Legacy: only recallMaxChars exists
121
+ return { ...RECALL_DEFAULTS, maxChars: data.recallMaxChars ?? 0 };
122
+ }
123
+ return {
124
+ limit: d.limit ?? RECALL_DEFAULTS.limit,
125
+ minImportance: d.minImportance ?? RECALL_DEFAULTS.minImportance,
126
+ minScore: d.minScore ?? RECALL_DEFAULTS.minScore,
127
+ maxChars: d.maxChars ?? RECALL_DEFAULTS.maxChars,
128
+ includeSessions: d.includeSessions ?? RECALL_DEFAULTS.includeSessions,
129
+ recencyBoost: d.recencyBoost ?? RECALL_DEFAULTS.recencyBoost,
130
+ };
131
+ } catch {
132
+ return { ...RECALL_DEFAULTS };
133
+ }
134
+ }
135
+
136
+ export async function handleRecall(args: {
137
+ query: string;
138
+ category?: string;
139
+ project?: string;
140
+ tags?: string[];
141
+ limit?: number;
142
+ min_importance?: number;
143
+ min_score?: number;
144
+ include_sessions?: boolean;
145
+ recency_boost?: boolean;
146
+ }) {
147
+ const defaults = getRecallDefaults();
148
+ const query = args.query;
149
+ const category = args.category;
150
+ const project = args.project;
151
+ const tags = args.tags;
152
+ const limit = args.limit ?? defaults.limit;
153
+ const minImportance = args.min_importance ?? (defaults.minImportance > 0 ? defaults.minImportance : undefined);
154
+ const minScore = args.min_score ?? defaults.minScore;
155
+ const includeSessionsExplicit = args.include_sessions ??
156
+ (defaults.includeSessions === 'always' ? true : defaults.includeSessions === 'never' ? false : undefined);
157
+ const recencyBoost = args.recency_boost ?? defaults.recencyBoost;
158
+
159
+ if (category) {
160
+ const catCheck = validateCategory(category);
161
+ if (!catCheck.valid) {
162
+ return text(catCheck.error!);
163
+ }
164
+ }
165
+
166
+ const vector = await generateEmbedding(query);
167
+
168
+ const must: Record<string, unknown>[] = [];
169
+ if (category) must.push({ key: 'category', match: { value: category } });
170
+ if (project) must.push({ key: 'project', match: { value: project } });
171
+ if (tags && tags.length > 0) {
172
+ for (const tag of tags) {
173
+ must.push({ key: 'tags', match: { value: tag } });
174
+ }
175
+ }
176
+ if (minImportance) {
177
+ must.push({ key: 'importance', range: { gte: minImportance } });
178
+ }
179
+
180
+ const filter = must.length > 0 ? { must } : undefined;
181
+ // Use a lower raw threshold for the DB query — time decay, importance,
182
+ // and project boosts can lift borderline results well above minScore.
183
+ // The actual minScore filter is applied after adjustments below.
184
+ const rawThreshold = Math.min(minScore, minScore * 0.5);
185
+ const results = await searchMemories(vector, limit * 2, filter, rawThreshold);
186
+
187
+ const currentProject = detectProject();
188
+ const scored = results
189
+ .map((r) => {
190
+ const payload = r.payload as unknown as MemoryPayload;
191
+ let adjustedScore = applyTimeDecay(
192
+ r.score,
193
+ payload.created_at,
194
+ payload.importance,
195
+ payload.access_count,
196
+ recencyBoost
197
+ );
198
+ if (!project && payload.project === currentProject) {
199
+ adjustedScore *= 1.2;
200
+ }
201
+ return { id: r.id as string, score: adjustedScore, payload };
202
+ })
203
+ .filter((r) => r.score >= minScore)
204
+ .sort((a, b) => b.score - a.score)
205
+ .slice(0, limit);
206
+
207
+ // FTS5 keyword fallback — when vector results are weak, supplement with
208
+ // full-text search to catch exact identifiers, error codes, proper nouns
209
+ const FTS_TRIGGER_SCORE = 0.45;
210
+ const vectorWeak = scored.length === 0 ||
211
+ (scored.length < limit && scored[0]?.score < FTS_TRIGGER_SCORE);
212
+
213
+ if (vectorWeak) {
214
+ try {
215
+ const existingIds = new Set(scored.map(r => r.id));
216
+ const ftsResults = await searchMemoriesFTS(query, limit, filter, existingIds);
217
+ for (const fts of ftsResults) {
218
+ if (scored.length >= limit) break;
219
+ scored.push(fts);
220
+ }
221
+ // Re-sort after merge
222
+ scored.sort((a, b) => b.score - a.score);
223
+ } catch {
224
+ // FTS fallback failure is non-fatal
225
+ }
226
+ }
227
+
228
+ // Update access tracking (fire-and-forget)
229
+ const now = new Date().toISOString();
230
+ for (const result of scored) {
231
+ updatePayload(result.id, {
232
+ accessed_at: now,
233
+ access_count: result.payload.access_count + 1,
234
+ }).catch(() => {});
235
+ }
236
+
237
+ if (scored.length === 0) {
238
+ return text(`No memories found for "${query}"${category ? ` in category ${category}` : ''}${project ? ` for project ${project}` : ''}.`);
239
+ }
240
+
241
+ const maxChars = defaults.maxChars;
242
+
243
+ const lines = scored.map((r, i) => {
244
+ const p = r.payload;
245
+ const score = (r.score * 100).toFixed(0);
246
+ const tagStr = p.tags?.length ? ` [${p.tags.join(', ')}]` : '';
247
+ const sub = p.subcategory ? `/${p.subcategory}` : '';
248
+ const files = p.related_files?.length
249
+ ? `\n Files: ${p.related_files.join(', ')}`
250
+ : '';
251
+ const displayContent = (maxChars > 0 && p.content.length > maxChars)
252
+ ? p.content.substring(0, maxChars) + '...'
253
+ : p.content;
254
+ return `${i + 1}. [${r.id}] (${score}% match, importance: ${p.importance}, ${formatAge(p.created_at)})\n ${p.category}${sub} | ${p.project}${tagStr}\n ${displayContent}${files}`;
255
+ });
256
+
257
+ // Session chunk search — auto-trigger when useful, or explicit override
258
+ const shouldIncludeSessions = (() => {
259
+ // Explicit override takes priority
260
+ if (includeSessionsExplicit !== undefined) return includeSessionsExplicit;
261
+ // Temporal/process keywords → likely asking about past sessions
262
+ const q = query.toLowerCase();
263
+ const sessionKeywords = [
264
+ 'yesterday', 'last session', 'last time', 'remember when',
265
+ 'how did we', 'what did we', 'earlier today', 'previous session',
266
+ 'conversation about', 'we discussed', 'we worked on', 'we talked about',
267
+ 'that session', 'that conversation', 'the other day', 'last week',
268
+ 'before', 'recently', 'a while ago', 'few days ago',
269
+ ];
270
+ if (sessionKeywords.some(kw => q.includes(kw))) return true;
271
+ // Sparse/weak memory results → widen the net
272
+ if (scored.length < 3) return true;
273
+ if (scored.length > 0 && scored[0].score < 0.45) return true;
274
+ return false;
275
+ })();
276
+
277
+ let sessionLines: string[] = [];
278
+ if (shouldIncludeSessions) {
279
+ try {
280
+ const sessionFilter: Record<string, unknown> = {};
281
+ if (project) {
282
+ sessionFilter.must = [{ key: 'project', match: { value: project } }];
283
+ }
284
+ const sessionLimit = Math.max(3, Math.floor(limit / 2));
285
+ const sessionResults = await searchSessionChunks(vector, sessionLimit * 2, Object.keys(sessionFilter).length > 0 ? sessionFilter : undefined, rawThreshold);
286
+
287
+ // Deduplicate: skip chunks whose dedup_memory_id matches a returned memory
288
+ const memoryIds = new Set(scored.map((r) => r.id));
289
+ const filteredSessions = sessionResults.filter((r) => {
290
+ const payload = r.payload as unknown as SessionChunkPayload;
291
+ return !payload.dedup_memory_id || !memoryIds.has(payload.dedup_memory_id);
292
+ }).slice(0, sessionLimit);
293
+
294
+ sessionLines = filteredSessions.map((r, i) => {
295
+ const p = r.payload as unknown as SessionChunkPayload;
296
+ const score = (r.score * 100).toFixed(0);
297
+ const timeRange = p.start_timestamp && p.end_timestamp
298
+ ? `${new Date(p.start_timestamp).toISOString().slice(0, 16)} - ${new Date(p.end_timestamp).toISOString().slice(11, 16)}`
299
+ : 'unknown time';
300
+ const toolStr = p.tools_used?.length ? `Tools: ${p.tools_used.join(', ')}` : '';
301
+ const fileStr = p.files_modified?.length ? `Files: ${p.files_modified.join(', ')}` : '';
302
+ const linkedMems = p.related_memory_ids?.length ? `Linked memories: ${p.related_memory_ids.join(', ')}` : '';
303
+ const details = [toolStr, fileStr, linkedMems].filter(Boolean).join(' | ');
304
+ return `SESSION: [${r.id}] (${score}% match, ${timeRange}, branch: ${p.git_branch || 'unknown'})\n Session: ${p.session_id} | Chunk ${p.chunk_index + 1} | ${p.project}\n ${p.summary}\n ${details}`;
305
+ });
306
+ } catch {
307
+ // Session search failure is non-fatal
308
+ }
309
+ }
310
+
311
+ const allLines = [...lines];
312
+ if (sessionLines.length > 0) {
313
+ allLines.push('', '--- Session Context ---');
314
+ allLines.push(...sessionLines);
315
+ }
316
+
317
+ const totalCount = scored.length + sessionLines.length;
318
+ const label = sessionLines.length > 0
319
+ ? `Found ${scored.length} memories and ${sessionLines.length} session chunks`
320
+ : `Found ${scored.length} memories`;
321
+
322
+ return text(`${label} for "${query}":\n\n${allLines.join('\n\n')}`);
323
+ }
@@ -0,0 +1,145 @@
1
+ import { z } from 'zod';
2
+ import { getMemory, updatePayload, updateVector } from '../services/sqlite.js';
3
+ import { generateEmbedding } from '../services/local-embeddings.js';
4
+ import { validateCategory } from '../services/categories.js';
5
+ import { coerceStringArray } from './utils.js';
6
+ import type { MemoryPayload } from '../types.js';
7
+ import { invalidateCache } from '../services/neural-interface.js';
8
+ import { computeChecksums } from '../services/file-checksums.js';
9
+ import { text } from './response.js';
10
+
11
+ export function buildReflectSchema() {
12
+ return {
13
+ memory_id: z.string().describe('The ID of the memory to update. MUST be the full UUID format (e.g., 8f7cab3b-644e-4cea-8662-de0ca695bdf2), not a shortened version. Use recall to get the full UUID.'),
14
+ content: z
15
+ .string()
16
+ .optional()
17
+ .describe(
18
+ 'Updated content. If provided, the embedding vector is regenerated.'
19
+ ),
20
+ importance: z.coerce.number().min(1).max(10).optional().describe('Updated importance score.'),
21
+ tags: coerceStringArray()
22
+ .optional()
23
+ .describe('Replace all tags with these.'),
24
+ add_tags: coerceStringArray()
25
+ .optional()
26
+ .describe('Add tags without replacing existing ones.'),
27
+ subcategory: z.string().optional().describe('Updated subcategory.'),
28
+ category: z
29
+ .string()
30
+ .optional()
31
+ .describe('Change the category name.'),
32
+ related_files: coerceStringArray()
33
+ .optional()
34
+ .describe('Updated related file paths.'),
35
+ related_memory_ids: coerceStringArray()
36
+ .optional()
37
+ .describe('Link to related memories.'),
38
+ project: z.string().optional().describe('Change the project this memory belongs to (e.g. "criticalpixel", "synabun", "synabun-website").'),
39
+ };
40
+ }
41
+
42
+ export const reflectSchema = buildReflectSchema();
43
+
44
+ export const reflectDescription =
45
+ 'Update or annotate an existing memory. Use this when you discover additional context, when a decision changes, or when you want to adjust importance based on new information.';
46
+
47
+ export async function handleReflect(args: {
48
+ memory_id: string;
49
+ content?: string;
50
+ importance?: number;
51
+ tags?: string[];
52
+ add_tags?: string[];
53
+ subcategory?: string;
54
+ category?: string;
55
+ related_files?: string[];
56
+ related_memory_ids?: string[];
57
+ project?: string;
58
+ }) {
59
+ const memoryId = args.memory_id;
60
+
61
+ // Validate UUID format
62
+ const uuidRegex = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
63
+ if (!uuidRegex.test(memoryId)) {
64
+ return text(`Invalid memory_id format. Expected full UUID (e.g., 8f7cab3b-644e-4cea-8662-de0ca695bdf2), got: ${memoryId}\n\nUse the full UUID returned by 'remember', or call 'recall' to find the full UUID of an existing memory.`);
65
+ }
66
+
67
+ if (args.category) {
68
+ const catCheck = validateCategory(args.category);
69
+ if (!catCheck.valid) {
70
+ return text(catCheck.error!);
71
+ }
72
+ }
73
+
74
+ const existing = await getMemory(memoryId);
75
+ if (!existing) {
76
+ return text(`Memory "${memoryId}" not found.`);
77
+ }
78
+
79
+ const payload = existing.payload as unknown as MemoryPayload;
80
+ const now = new Date().toISOString();
81
+ const updates: Partial<MemoryPayload> = { updated_at: now };
82
+ const changes: string[] = [];
83
+
84
+ if (args.content) {
85
+ updates.content = args.content;
86
+ changes.push('content');
87
+ }
88
+ if (args.importance !== undefined) {
89
+ updates.importance = args.importance;
90
+ changes.push(`importance -> ${args.importance}`);
91
+ }
92
+ if (args.category) {
93
+ updates.category = args.category;
94
+ changes.push(`category -> ${args.category}`);
95
+ }
96
+ if (args.subcategory) {
97
+ updates.subcategory = args.subcategory;
98
+ changes.push(`subcategory -> ${args.subcategory}`);
99
+ }
100
+ if (args.tags) {
101
+ updates.tags = args.tags;
102
+ changes.push(`tags replaced`);
103
+ }
104
+ if (args.add_tags) {
105
+ const existingTags = payload.tags || [];
106
+ updates.tags = [...new Set([...existingTags, ...args.add_tags])];
107
+ changes.push(`tags added: ${args.add_tags.join(', ')}`);
108
+ }
109
+ if (args.related_files) {
110
+ updates.related_files = args.related_files;
111
+ changes.push('related_files updated');
112
+ }
113
+ if (args.related_memory_ids) {
114
+ updates.related_memory_ids = args.related_memory_ids;
115
+ changes.push('related_memory_ids updated');
116
+ }
117
+ if (args.project) {
118
+ updates.project = args.project;
119
+ changes.push(`project -> ${args.project}`);
120
+ }
121
+
122
+ if (changes.length === 0) {
123
+ return text('No changes specified.');
124
+ }
125
+
126
+ // Recompute file checksums whenever the memory is updated
127
+ const finalFiles = (updates.related_files ?? payload.related_files);
128
+ if (finalFiles?.length) {
129
+ const cs = computeChecksums(finalFiles);
130
+ updates.file_checksums = Object.keys(cs).length > 0 ? cs : undefined;
131
+ }
132
+
133
+ if (args.content) {
134
+ const mergedPayload: MemoryPayload = { ...payload, ...updates } as MemoryPayload;
135
+ const vector = await generateEmbedding(mergedPayload.content);
136
+ await updateVector(memoryId, vector, mergedPayload);
137
+ } else {
138
+ await updatePayload(memoryId, updates);
139
+ }
140
+
141
+ // Invalidate Neural Interface link cache (fire-and-forget)
142
+ invalidateCache('reflect');
143
+
144
+ return text(`Updated [${memoryId.slice(0, 8)}]: ${changes.join(', ')}`);
145
+ }
@@ -0,0 +1,115 @@
1
+ import { z } from 'zod';
2
+ import { v4 as uuidv4 } from 'uuid';
3
+ import { generateEmbedding } from '../services/local-embeddings.js';
4
+ import { upsertMemory } from '../services/sqlite.js';
5
+ import { validateCategory } from '../services/categories.js';
6
+ import { coerceStringArray } from './utils.js';
7
+ import type { MemoryPayload, MemorySource } from '../types.js';
8
+ import { detectProject } from '../config.js';
9
+ import { computeChecksums } from '../services/file-checksums.js';
10
+ import { invalidateCache } from '../services/neural-interface.js';
11
+ import { text } from './response.js';
12
+
13
+ export function buildRememberSchema() {
14
+ return {
15
+ content: z
16
+ .string()
17
+ .describe('The information to remember. Be specific and include context.'),
18
+ category: z
19
+ .string()
20
+ .describe('Category name. Call category tool with action "list" to see valid categories.'),
21
+ project: z
22
+ .string()
23
+ .optional()
24
+ .describe(
25
+ 'Project this belongs to (e.g. "criticalpixel"). Defaults to auto-detected from working directory.'
26
+ ),
27
+ tags: coerceStringArray()
28
+ .optional()
29
+ .describe('Tags for categorization (e.g. ["redis", "cache", "pricing"])'),
30
+ importance: z
31
+ .coerce.number()
32
+ .min(1)
33
+ .max(10)
34
+ .optional()
35
+ .describe(
36
+ '1=trivial, 5=normal, 7=significant, 8+=critical. Default 5. Use 8+ for hard-won bug fixes and architecture decisions.'
37
+ ),
38
+ subcategory: z
39
+ .string()
40
+ .optional()
41
+ .describe(
42
+ 'Optional refinement: architecture, bug-fix, api-quirk, performance, config, deployment, etc.'
43
+ ),
44
+ source: z
45
+ .enum(['user-told', 'self-discovered', 'auto-saved'] as const)
46
+ .optional()
47
+ .describe(
48
+ 'How this was learned. user-told=user explicitly shared, self-discovered=found during work, auto-saved=session context.'
49
+ ),
50
+ related_files: coerceStringArray()
51
+ .optional()
52
+ .describe('File paths this memory relates to.'),
53
+ };
54
+ }
55
+
56
+ export const rememberSchema = buildRememberSchema();
57
+
58
+ export const rememberDescription =
59
+ 'Store a piece of information in persistent memory. Use this when you learn something important, make a decision, discover a pattern, fix a hard bug, or want to preserve context for future sessions.';
60
+
61
+ export async function handleRemember(args: {
62
+ content: string;
63
+ category: string;
64
+ project?: string;
65
+ tags?: string[];
66
+ importance?: number;
67
+ subcategory?: string;
68
+ source?: string;
69
+ related_files?: string[];
70
+ }) {
71
+ const catCheck = validateCategory(args.category);
72
+ if (!catCheck.valid) {
73
+ return text(catCheck.error!);
74
+ }
75
+
76
+ const content = args.content;
77
+ const category = args.category;
78
+ const project = args.project || detectProject();
79
+ const tags = args.tags || [];
80
+ const importance = args.importance ?? 5;
81
+ const subcategory = args.subcategory;
82
+ const source = (args.source as MemorySource) || 'self-discovered';
83
+ const related_files = args.related_files;
84
+
85
+ const id = uuidv4();
86
+ const now = new Date().toISOString();
87
+
88
+ const payload: MemoryPayload = {
89
+ content,
90
+ category,
91
+ subcategory,
92
+ project,
93
+ tags,
94
+ importance,
95
+ source,
96
+ created_at: now,
97
+ updated_at: now,
98
+ accessed_at: now,
99
+ access_count: 0,
100
+ related_files,
101
+ file_checksums: (() => {
102
+ if (!related_files?.length) return undefined;
103
+ const cs = computeChecksums(related_files);
104
+ return Object.keys(cs).length > 0 ? cs : undefined;
105
+ })(),
106
+ };
107
+
108
+ const vector = await generateEmbedding(content);
109
+ await upsertMemory(id, vector, payload);
110
+
111
+ // Invalidate Neural Interface link cache (fire-and-forget)
112
+ invalidateCache('remember');
113
+
114
+ return text(`Remembered [${id}] (${category}/${project}, importance: ${importance}): "${content.slice(0, 100)}${content.length > 100 ? '...' : ''}"`);
115
+ }