@sashabogi/foundation 0.1.13 → 2.0.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 (57) hide show
  1. package/README.md +632 -238
  2. package/dist/cli.d.ts +1 -0
  3. package/dist/cli.d.ts.map +1 -1
  4. package/dist/cli.js +159 -1
  5. package/dist/cli.js.map +1 -1
  6. package/dist/index.d.ts +2 -2
  7. package/dist/index.js +3 -3
  8. package/dist/index.js.map +1 -1
  9. package/dist/providers/anthropic.d.ts.map +1 -1
  10. package/dist/providers/anthropic.js +4 -6
  11. package/dist/providers/anthropic.js.map +1 -1
  12. package/dist/providers/fireworks.d.ts +5 -0
  13. package/dist/providers/fireworks.d.ts.map +1 -1
  14. package/dist/providers/fireworks.js +18 -0
  15. package/dist/providers/fireworks.js.map +1 -1
  16. package/dist/providers/kimi-code.d.ts +5 -0
  17. package/dist/providers/kimi-code.d.ts.map +1 -1
  18. package/dist/providers/kimi-code.js +18 -0
  19. package/dist/providers/kimi-code.js.map +1 -1
  20. package/dist/providers/zai.d.ts +5 -0
  21. package/dist/providers/zai.d.ts.map +1 -1
  22. package/dist/providers/zai.js +18 -0
  23. package/dist/providers/zai.js.map +1 -1
  24. package/dist/tools/gaia/index.d.ts +29 -15
  25. package/dist/tools/gaia/index.d.ts.map +1 -1
  26. package/dist/tools/gaia/index.js +1206 -274
  27. package/dist/tools/gaia/index.js.map +1 -1
  28. package/dist/tools/gaia/storage.d.ts +72 -0
  29. package/dist/tools/gaia/storage.d.ts.map +1 -0
  30. package/dist/tools/gaia/storage.js +344 -0
  31. package/dist/tools/gaia/storage.js.map +1 -0
  32. package/dist/tools/gaia/test-storage-manual.d.ts +6 -0
  33. package/dist/tools/gaia/test-storage-manual.d.ts.map +1 -0
  34. package/dist/tools/gaia/test-storage-manual.js +120 -0
  35. package/dist/tools/gaia/test-storage-manual.js.map +1 -0
  36. package/dist/tools/memoria/index.d.ts +25 -0
  37. package/dist/tools/memoria/index.d.ts.map +1 -0
  38. package/dist/tools/memoria/index.js +269 -0
  39. package/dist/tools/memoria/index.js.map +1 -0
  40. package/dist/tools/memoria/storage.d.ts +72 -0
  41. package/dist/tools/memoria/storage.d.ts.map +1 -0
  42. package/dist/tools/memoria/storage.js +344 -0
  43. package/dist/tools/memoria/storage.js.map +1 -0
  44. package/dist/tools/memoria/storage.test.d.ts +7 -0
  45. package/dist/tools/memoria/storage.test.d.ts.map +1 -0
  46. package/dist/tools/memoria/storage.test.js +350 -0
  47. package/dist/tools/memoria/storage.test.js.map +1 -0
  48. package/dist/tools/memoria/test-storage-manual.d.ts +6 -0
  49. package/dist/tools/memoria/test-storage-manual.d.ts.map +1 -0
  50. package/dist/tools/memoria/test-storage-manual.js +120 -0
  51. package/dist/tools/memoria/test-storage-manual.js.map +1 -0
  52. package/dist/types/index.d.ts +38 -0
  53. package/dist/types/index.d.ts.map +1 -1
  54. package/docs/MIGRATION-GUIDE.md +771 -0
  55. package/package.json +2 -2
  56. package/docs/REFACTORING-PLAN.md +0 -374
  57. package/docs/TESTING-WITH-NATIVE-SWARM.md +0 -266
@@ -1,39 +1,59 @@
1
1
  /**
2
- * Gaia Module Tools
2
+ * Gaia Module Tools v2.0
3
3
  *
4
- * Workflow patterns - shared memory and collective consciousness.
4
+ * Workflow patterns + Advanced memory - collective consciousness.
5
5
  * Named after Gaia from Foundation - "We are all one, and one is all."
6
6
  *
7
- * Preserves context across sessions, learns from mistakes, enables
8
- * seamless handoffs between Claude instances.
7
+ * v2: SQLite + FTS5 memory system with 5-tier hierarchy and BM25 ranking.
9
8
  *
10
- * Tools:
11
- * - gaia_worktree_create: Create git worktree with session
12
- * - gaia_worktree_list: List active worktrees
13
- * - gaia_worktree_switch: Navigate between worktrees
14
- * - gaia_worktree_cleanup: Remove stale worktrees
15
- * - gaia_session_register: Track Claude session purpose
16
- * - gaia_session_status: View active sessions
17
- * - gaia_session_handoff: Prepare context for handoff
18
- * - gaia_session_complete: Mark session as done
9
+ * Tools (19 total):
10
+ * Workflow Tools (12):
11
+ * - gaia_checkpoint: Save full structured state to disk
12
+ * - gaia_status: Return lightweight index card (~150 tokens)
13
+ * - gaia_query: Keyword search across checkpoint
14
+ * - gaia_get_decisions: List architectural decisions
15
+ * - gaia_get_progress: Task progress summary
16
+ * - gaia_get_changes: Files changed in session
17
+ * - gaia_handoff: Create session handoff document (NEW v2.0)
18
+ * - gaia_observe: Auto-detect patterns and observations (NEW v2.0)
19
+ * - gaia_migrate: Migrate v1 checkpoint data to v2 (NEW v2.0)
19
20
  * - gaia_learn: Record correction for CLAUDE.md
20
21
  * - gaia_apply: Write learnings to CLAUDE.md
21
22
  * - gaia_review: Review accumulated learnings
22
- * - gaia_remember: Save context state
23
- * - gaia_recall: Restore previous context
23
+ *
24
+ * Memory Tools (5):
25
+ * - gaia_save: Save a new memory (SQLite + FTS5)
26
+ * - gaia_search: Search memories with BM25 + composite scoring
27
+ * - gaia_get: Get memory by ID
28
+ * - gaia_delete: Delete memory + cascade links
29
+ * - gaia_stats: Get memory statistics
30
+ *
31
+ * Linking Tools (2):
32
+ * - gaia_link: Create typed link between memories
33
+ * - gaia_graph: Get link graph for a memory
24
34
  */
25
35
  import { z } from 'zod';
26
36
  import { nanoid } from 'nanoid';
37
+ import { existsSync, readFileSync, writeFileSync, mkdirSync } from 'fs';
38
+ import { join } from 'path';
39
+ import { homedir } from 'os';
27
40
  import { StorageService } from '../../services/storage.service.js';
28
41
  import { GitService } from '../../services/git.service.js';
29
- const SessionTaskTypeSchema = z.enum([
30
- 'feature',
31
- 'bugfix',
32
- 'refactor',
33
- 'review',
34
- 'exploration',
35
- 'documentation',
36
- ]);
42
+ import { MemoriaStorage } from './storage.js';
43
+ // =============================================================================
44
+ // Memory Storage (v2.0)
45
+ // =============================================================================
46
+ let memoryStorage = null;
47
+ function getMemoryStorage() {
48
+ if (!memoryStorage) {
49
+ const dbPath = join(homedir(), '.foundation', 'gaia-memory.db');
50
+ memoryStorage = new MemoriaStorage(dbPath);
51
+ }
52
+ return memoryStorage;
53
+ }
54
+ // =============================================================================
55
+ // Zod Schemas
56
+ // =============================================================================
37
57
  const LearningCategorySchema = z.enum([
38
58
  'code_style',
39
59
  'architecture',
@@ -46,236 +66,863 @@ const LearningCategorySchema = z.enum([
46
66
  'other',
47
67
  ]);
48
68
  const LearningScopeSchema = z.enum(['global', 'project']);
69
+ const CheckpointDecisionSchema = z.object({
70
+ topic: z.string(),
71
+ decision: z.string(),
72
+ rationale: z.string(),
73
+ timestamp: z.number().optional(),
74
+ });
75
+ const CheckpointProgressSchema = z.object({
76
+ task: z.string(),
77
+ status: z.enum(['pending', 'in_progress', 'completed', 'blocked']),
78
+ details: z.string().optional(),
79
+ });
80
+ const CheckpointChangeSchema = z.object({
81
+ file: z.string(),
82
+ action: z.enum(['created', 'modified', 'deleted']),
83
+ summary: z.string(),
84
+ });
85
+ const CheckpointContextSchema = z.object({
86
+ branch: z.string().optional(),
87
+ openQuestions: z.array(z.string()).optional(),
88
+ relevantFiles: z.array(z.string()).optional(),
89
+ notes: z.string().optional(),
90
+ });
91
+ // =============================================================================
92
+ // Helper Functions
93
+ // =============================================================================
94
+ function getSessionsDir() {
95
+ return join(process.cwd(), '.foundation', 'sessions');
96
+ }
97
+ function ensureDir(dir) {
98
+ if (!existsSync(dir)) {
99
+ mkdirSync(dir, { recursive: true });
100
+ }
101
+ }
102
+ function generateIndexCard(checkpoint) {
103
+ const lines = [];
104
+ lines.push(`# ${checkpoint.purpose}`);
105
+ lines.push(`Project: ${checkpoint.project || 'unknown'} | Branch: ${checkpoint.sections.context.branch || '?'}`);
106
+ lines.push(`Saved: ${new Date(checkpoint.createdAt).toISOString().slice(0, 16)}`);
107
+ lines.push('');
108
+ lines.push(checkpoint.summary);
109
+ lines.push('');
110
+ const { decisions, progress, changes, context } = checkpoint.sections;
111
+ const completed = progress.filter(p => p.status === 'completed').length;
112
+ const total = progress.length;
113
+ lines.push(`Progress: ${completed}/${total} tasks | ${decisions.length} decisions | ${changes.length} files changed`);
114
+ if (context.openQuestions && context.openQuestions.length > 0) {
115
+ lines.push(`Open questions: ${context.openQuestions.length}`);
116
+ }
117
+ // Hard cap: keep under ~200 tokens (roughly 150 words / 800 chars)
118
+ const result = lines.join('\n');
119
+ if (result.length > 800) {
120
+ return result.slice(0, 797) + '...';
121
+ }
122
+ return result;
123
+ }
124
+ function readJsonFile(filePath) {
125
+ try {
126
+ if (!existsSync(filePath))
127
+ return null;
128
+ const raw = readFileSync(filePath, 'utf-8');
129
+ return JSON.parse(raw);
130
+ }
131
+ catch {
132
+ return null;
133
+ }
134
+ }
135
+ // =============================================================================
136
+ // Tool Registration
137
+ // =============================================================================
49
138
  export function registerGaiaTools(server) {
50
139
  const storage = StorageService.getInstance();
51
140
  const git = GitService.getInstance();
52
141
  // ===========================================================================
53
- // Worktree Tools
142
+ // Checkpoint Tools (v2)
54
143
  // ===========================================================================
55
- // gaia_worktree_create
56
- server.tool('gaia_worktree_create', 'Create a git worktree for parallel Claude sessions. Returns shell alias for quick navigation.', {
57
- branch: z.string().describe('Branch name for the worktree'),
58
- alias: z.string().optional().describe('Single letter alias for quick switching (e.g., "a" for `za`)'),
59
- purpose: z.string().optional().describe('Purpose of this worktree'),
60
- baseBranch: z.string().optional().describe('Base branch (default: main)'),
61
- }, async ({ branch, alias, purpose, baseBranch = 'main' }) => {
144
+ // gaia_checkpoint — Save full structured state to disk
145
+ server.tool('gaia_checkpoint', 'Save full session state to disk. The ONLY write operation. Creates checkpoint.json, index.txt, and decisions.json in .foundation/sessions/latest/.', {
146
+ summary: z.string().describe('One-line summary of current state (~20 words)'),
147
+ purpose: z.string().describe('What this session is working on'),
148
+ project: z.string().optional().describe('Project name'),
149
+ decisions: z.array(CheckpointDecisionSchema).optional().describe('Architectural decisions made'),
150
+ progress: z.array(CheckpointProgressSchema).optional().describe('Task progress'),
151
+ changes: z.array(CheckpointChangeSchema).optional().describe('Files changed'),
152
+ context: CheckpointContextSchema.optional().describe('Additional context (branch, questions, files)'),
153
+ }, async ({ summary, purpose, project, decisions = [], progress = [], changes = [], context = {} }) => {
62
154
  try {
63
- const repoRoot = await git.getRepoRoot();
64
- const worktreePath = `${repoRoot}-${alias || branch.split('/').pop()}`;
65
- await git.createWorktree(worktreePath, branch, { baseBranch });
66
- const worktree = {
67
- id: `wt_${nanoid(8)}`,
68
- path: worktreePath,
69
- branch,
70
- baseBranch,
71
- alias,
72
- purpose,
73
- status: 'active',
155
+ // Auto-fill branch from git if not provided
156
+ let branch = context.branch;
157
+ if (!branch) {
158
+ try {
159
+ branch = await git.getCurrentBranch();
160
+ }
161
+ catch {
162
+ branch = undefined;
163
+ }
164
+ }
165
+ const sections = {
166
+ decisions: decisions.map(d => ({
167
+ ...d,
168
+ timestamp: d.timestamp || Date.now(),
169
+ })),
170
+ progress,
171
+ changes,
172
+ context: { ...context, branch },
173
+ };
174
+ const checkpoint = {
175
+ id: `ckpt_${nanoid(8)}`,
176
+ version: 1,
74
177
  createdAt: Date.now(),
75
- lastAccessedAt: Date.now(),
178
+ project,
179
+ summary,
180
+ purpose,
181
+ sections,
76
182
  };
77
- storage.addWorktree(worktree);
78
- let output = `✓ Created worktree at ${worktreePath}\n\n`;
79
- if (alias) {
80
- output += `Shell alias: \`alias z${alias}='cd ${worktreePath} && claude'\`\n`;
81
- }
82
- output += `\nTo navigate: \`cd ${worktreePath}\``;
183
+ // Write to .foundation/sessions/latest/
184
+ const latestDir = join(getSessionsDir(), 'latest');
185
+ ensureDir(latestDir);
186
+ // 1. Full checkpoint
187
+ writeFileSync(join(latestDir, 'checkpoint.json'), JSON.stringify(checkpoint, null, 2), 'utf-8');
188
+ // 2. Index card (~150 tokens)
189
+ const indexCard = generateIndexCard(checkpoint);
190
+ writeFileSync(join(latestDir, 'index.txt'), indexCard, 'utf-8');
191
+ // 3. Decisions extract
192
+ writeFileSync(join(latestDir, 'decisions.json'), JSON.stringify(sections.decisions, null, 2), 'utf-8');
193
+ // Also archive by ID
194
+ const archiveDir = join(getSessionsDir(), checkpoint.id);
195
+ ensureDir(archiveDir);
196
+ writeFileSync(join(archiveDir, 'checkpoint.json'), JSON.stringify(checkpoint, null, 2), 'utf-8');
197
+ writeFileSync(join(archiveDir, 'index.txt'), indexCard, 'utf-8');
198
+ writeFileSync(join(archiveDir, 'decisions.json'), JSON.stringify(sections.decisions, null, 2), 'utf-8');
83
199
  return {
84
- content: [{ type: 'text', text: output }],
200
+ content: [{
201
+ type: 'text',
202
+ text: `✓ Checkpoint saved: ${checkpoint.id}\n\n${indexCard}`,
203
+ }],
85
204
  };
86
205
  }
87
206
  catch (error) {
88
207
  return {
89
- content: [{ type: 'text', text: `Error: ${error.message}` }],
208
+ content: [{ type: 'text', text: `Error saving checkpoint: ${error.message}` }],
90
209
  };
91
210
  }
92
211
  });
93
- // gaia_worktree_list
94
- server.tool('gaia_worktree_list', 'List all active worktrees with their status and session info.', {}, async () => {
95
- const worktrees = storage.getWorktrees();
96
- if (worktrees.length === 0) {
212
+ // gaia_status — Return lightweight index card only
213
+ server.tool('gaia_status', 'Return the index card for the latest checkpoint (~150 tokens). Use this at session start to understand context without loading full state.', {}, async () => {
214
+ const indexPath = join(getSessionsDir(), 'latest', 'index.txt');
215
+ if (!existsSync(indexPath)) {
97
216
  return {
98
- content: [{ type: 'text', text: 'No active worktrees.' }],
217
+ content: [{ type: 'text', text: 'No checkpoint found. Use gaia_checkpoint to save session state.' }],
99
218
  };
100
219
  }
101
- let output = '## Active Worktrees\n\n';
102
- for (const wt of worktrees) {
103
- output += `### ${wt.alias ? `[${wt.alias}] ` : ''}${wt.branch}\n`;
104
- output += `- Path: \`${wt.path}\`\n`;
105
- output += `- Status: ${wt.status}\n`;
106
- if (wt.purpose)
107
- output += `- Purpose: ${wt.purpose}\n`;
108
- output += '\n';
109
- }
220
+ const indexCard = readFileSync(indexPath, 'utf-8');
110
221
  return {
111
- content: [{ type: 'text', text: output }],
222
+ content: [{ type: 'text', text: indexCard }],
112
223
  };
113
224
  });
114
- // gaia_worktree_switch
115
- server.tool('gaia_worktree_switch', 'Get the command to switch to a worktree by alias or branch.', {
116
- target: z.string().describe('Worktree alias or branch name'),
117
- }, async ({ target }) => {
118
- const worktree = storage.getWorktreeByAlias(target) ||
119
- storage.getWorktrees().find((w) => w.branch.includes(target));
120
- if (!worktree) {
225
+ // gaia_query — Keyword search across checkpoint
226
+ server.tool('gaia_query', 'Search the latest checkpoint by keyword. Returns only matching sections to minimize token usage.', {
227
+ keyword: z.string().describe('Keyword or phrase to search for (case-insensitive)'),
228
+ section: z.enum(['decisions', 'progress', 'changes', 'context']).optional().describe('Limit search to a specific section'),
229
+ }, async ({ keyword, section }) => {
230
+ const checkpoint = readJsonFile(join(getSessionsDir(), 'latest', 'checkpoint.json'));
231
+ if (!checkpoint) {
232
+ return {
233
+ content: [{ type: 'text', text: 'No checkpoint found. Use gaia_checkpoint to save session state.' }],
234
+ };
235
+ }
236
+ const kw = keyword.toLowerCase();
237
+ const results = [];
238
+ const searchIn = section ? [section] : ['decisions', 'progress', 'changes', 'context'];
239
+ for (const s of searchIn) {
240
+ if (s === 'decisions') {
241
+ const matches = checkpoint.sections.decisions.filter(d => d.topic.toLowerCase().includes(kw) ||
242
+ d.decision.toLowerCase().includes(kw) ||
243
+ d.rationale.toLowerCase().includes(kw));
244
+ if (matches.length > 0) {
245
+ results.push(`## Decisions (${matches.length} matches)`);
246
+ for (const m of matches) {
247
+ results.push(`- **${m.topic}**: ${m.decision}`);
248
+ results.push(` _Rationale_: ${m.rationale}`);
249
+ }
250
+ }
251
+ }
252
+ if (s === 'progress') {
253
+ const matches = checkpoint.sections.progress.filter(p => p.task.toLowerCase().includes(kw) ||
254
+ (p.details?.toLowerCase().includes(kw) ?? false));
255
+ if (matches.length > 0) {
256
+ results.push(`## Progress (${matches.length} matches)`);
257
+ for (const m of matches) {
258
+ results.push(`- [${m.status}] ${m.task}${m.details ? `: ${m.details}` : ''}`);
259
+ }
260
+ }
261
+ }
262
+ if (s === 'changes') {
263
+ const matches = checkpoint.sections.changes.filter(c => c.file.toLowerCase().includes(kw) ||
264
+ c.summary.toLowerCase().includes(kw));
265
+ if (matches.length > 0) {
266
+ results.push(`## Changes (${matches.length} matches)`);
267
+ for (const m of matches) {
268
+ results.push(`- ${m.action} \`${m.file}\`: ${m.summary}`);
269
+ }
270
+ }
271
+ }
272
+ if (s === 'context') {
273
+ const ctx = checkpoint.sections.context;
274
+ const ctxMatches = [];
275
+ if (ctx.branch?.toLowerCase().includes(kw))
276
+ ctxMatches.push(`Branch: ${ctx.branch}`);
277
+ if (ctx.notes?.toLowerCase().includes(kw))
278
+ ctxMatches.push(`Notes: ${ctx.notes}`);
279
+ if (ctx.openQuestions) {
280
+ const qMatches = ctx.openQuestions.filter(q => q.toLowerCase().includes(kw));
281
+ if (qMatches.length > 0)
282
+ ctxMatches.push(`Questions: ${qMatches.join('; ')}`);
283
+ }
284
+ if (ctx.relevantFiles) {
285
+ const fMatches = ctx.relevantFiles.filter(f => f.toLowerCase().includes(kw));
286
+ if (fMatches.length > 0)
287
+ ctxMatches.push(`Files: ${fMatches.join(', ')}`);
288
+ }
289
+ if (ctxMatches.length > 0) {
290
+ results.push(`## Context (${ctxMatches.length} matches)`);
291
+ results.push(...ctxMatches.map(m => `- ${m}`));
292
+ }
293
+ }
294
+ }
295
+ if (results.length === 0) {
121
296
  return {
122
- content: [{ type: 'text', text: `No worktree found for "${target}"` }],
297
+ content: [{ type: 'text', text: `No matches for "${keyword}" in checkpoint.` }],
123
298
  };
124
299
  }
125
- storage.updateWorktree(worktree.id, { lastAccessedAt: Date.now() });
126
300
  return {
127
- content: [
128
- {
129
- type: 'text',
130
- text: `To switch: \`cd ${worktree.path} && claude\``,
131
- },
132
- ],
301
+ content: [{ type: 'text', text: results.join('\n') }],
133
302
  };
134
303
  });
135
- // gaia_worktree_cleanup
136
- server.tool('gaia_worktree_cleanup', 'Remove stale or completed worktrees.', {
137
- target: z.string().optional().describe('Specific worktree ID or alias to remove'),
138
- staleOnly: z.boolean().optional().describe('Only remove stale worktrees (default: false)'),
139
- }, async ({ target, staleOnly = false }) => {
140
- // TODO: Implement cleanup logic
304
+ // gaia_get_decisions — List architectural decisions
305
+ server.tool('gaia_get_decisions', 'Get all architectural decisions from the latest checkpoint. Reads decisions.json directly.', {}, async () => {
306
+ const decisions = readJsonFile(join(getSessionsDir(), 'latest', 'decisions.json'));
307
+ if (!decisions || decisions.length === 0) {
308
+ return {
309
+ content: [{ type: 'text', text: 'No decisions recorded. Use gaia_checkpoint to save decisions.' }],
310
+ };
311
+ }
312
+ let output = `## Decisions (${decisions.length})\n\n`;
313
+ for (const d of decisions) {
314
+ output += `### ${d.topic}\n`;
315
+ output += `**Decision**: ${d.decision}\n`;
316
+ output += `**Rationale**: ${d.rationale}\n`;
317
+ output += `_${new Date(d.timestamp).toISOString().slice(0, 16)}_\n\n`;
318
+ }
141
319
  return {
142
- content: [{ type: 'text', text: '[Stub] Would cleanup worktrees' }],
320
+ content: [{ type: 'text', text: output }],
143
321
  };
144
322
  });
145
- // ===========================================================================
146
- // Session Tools
147
- // ===========================================================================
148
- // gaia_session_register
149
- server.tool('gaia_session_register', 'Register a new Claude session with its purpose for tracking. "We are all one."', {
150
- purpose: z.string().describe('What this session is working on'),
151
- taskType: SessionTaskTypeSchema.describe('Type of task'),
152
- project: z.string().optional().describe('Project name'),
153
- worktreeId: z.string().optional().describe('Associated worktree ID'),
154
- }, async ({ purpose, taskType, project, worktreeId }) => {
155
- const session = {
156
- id: `sess_${nanoid(8)}`,
157
- purpose,
158
- taskType,
159
- project,
160
- worktreeId,
161
- status: 'active',
162
- notes: [],
163
- startedAt: Date.now(),
164
- lastActivityAt: Date.now(),
165
- };
166
- storage.addSession(session);
323
+ // gaia_get_progress — Task progress summary
324
+ server.tool('gaia_get_progress', 'Get task progress from the latest checkpoint.', {}, async () => {
325
+ const checkpoint = readJsonFile(join(getSessionsDir(), 'latest', 'checkpoint.json'));
326
+ if (!checkpoint) {
327
+ return {
328
+ content: [{ type: 'text', text: 'No checkpoint found. Use gaia_checkpoint to save session state.' }],
329
+ };
330
+ }
331
+ const { progress } = checkpoint.sections;
332
+ if (progress.length === 0) {
333
+ return {
334
+ content: [{ type: 'text', text: 'No tasks tracked. Use gaia_checkpoint with progress data.' }],
335
+ };
336
+ }
337
+ const completed = progress.filter(p => p.status === 'completed').length;
338
+ const inProgress = progress.filter(p => p.status === 'in_progress').length;
339
+ const blocked = progress.filter(p => p.status === 'blocked').length;
340
+ const pending = progress.filter(p => p.status === 'pending').length;
341
+ let output = `## Progress: ${completed}/${progress.length} complete\n\n`;
342
+ output += `Completed: ${completed} | In progress: ${inProgress} | Blocked: ${blocked} | Pending: ${pending}\n\n`;
343
+ for (const p of progress) {
344
+ const icon = p.status === 'completed' ? '✓' : p.status === 'in_progress' ? '→' : p.status === 'blocked' ? '✗' : '○';
345
+ output += `${icon} [${p.status}] ${p.task}`;
346
+ if (p.details)
347
+ output += ` — ${p.details}`;
348
+ output += '\n';
349
+ }
167
350
  return {
168
- content: [
169
- {
170
- type: 'text',
171
- text: `✓ Session registered: ${session.id}\n\nPurpose: ${purpose}\nType: ${taskType}`,
172
- },
173
- ],
351
+ content: [{ type: 'text', text: output }],
174
352
  };
175
353
  });
176
- // gaia_session_status
177
- server.tool('gaia_session_status', 'View status of active sessions.', {
178
- sessionId: z.string().optional().describe('Specific session ID (or show all active)'),
179
- }, async ({ sessionId }) => {
180
- if (sessionId) {
181
- const session = storage.getSession(sessionId);
182
- if (!session) {
183
- return {
184
- content: [{ type: 'text', text: `Session not found: ${sessionId}` }],
185
- };
186
- }
354
+ // gaia_get_changes — Files changed in session
355
+ server.tool('gaia_get_changes', 'Get files changed from the latest checkpoint.', {}, async () => {
356
+ const checkpoint = readJsonFile(join(getSessionsDir(), 'latest', 'checkpoint.json'));
357
+ if (!checkpoint) {
187
358
  return {
188
- content: [
189
- {
190
- type: 'text',
191
- text: `## Session ${session.id}\n\n- Purpose: ${session.purpose}\n- Status: ${session.status}\n- Type: ${session.taskType}`,
192
- },
193
- ],
359
+ content: [{ type: 'text', text: 'No checkpoint found. Use gaia_checkpoint to save session state.' }],
194
360
  };
195
361
  }
196
- const sessions = storage.getActiveSessions();
197
- if (sessions.length === 0) {
362
+ const { changes } = checkpoint.sections;
363
+ if (changes.length === 0) {
198
364
  return {
199
- content: [{ type: 'text', text: 'No active sessions.' }],
365
+ content: [{ type: 'text', text: 'No changes tracked. Use gaia_checkpoint with changes data.' }],
200
366
  };
201
367
  }
202
- let output = '## Active Sessions\n\n';
203
- for (const s of sessions) {
204
- output += `- **${s.id}**: ${s.purpose} (${s.status})\n`;
368
+ let output = `## Changes (${changes.length} files)\n\n`;
369
+ for (const c of changes) {
370
+ output += `- **${c.action}** \`${c.file}\`: ${c.summary}\n`;
205
371
  }
206
372
  return {
207
373
  content: [{ type: 'text', text: output }],
208
374
  };
209
375
  });
210
- // gaia_session_handoff
211
- server.tool('gaia_session_handoff', 'Generate a handoff document for transferring context to another session.', {
212
- sessionId: z.string().describe('Session ID to create handoff for'),
213
- includeGitStatus: z.boolean().optional().describe('Include git status (default: true)'),
214
- includeChanges: z.boolean().optional().describe('Include recent changes (default: true)'),
215
- nextSteps: z.array(z.string()).optional().describe('Next steps to include'),
216
- }, async ({ sessionId, includeGitStatus = true, includeChanges = true, nextSteps = [] }) => {
217
- const session = storage.getSession(sessionId);
218
- if (!session) {
219
- return {
220
- content: [{ type: 'text', text: `Session not found: ${sessionId}` }],
221
- };
222
- }
223
- let handoff = `# Session Handoff: ${session.id}\n\n`;
224
- handoff += `## Purpose\n${session.purpose}\n\n`;
225
- handoff += `## Type\n${session.taskType}\n\n`;
226
- if (includeGitStatus) {
227
- try {
228
- const status = await git.getStatus();
229
- handoff += `## Git Status\n`;
230
- handoff += `- Branch: ${status.branch}\n`;
231
- if (status.staged.length)
232
- handoff += `- Staged: ${status.staged.length} files\n`;
233
- if (status.modified.length)
234
- handoff += `- Modified: ${status.modified.length} files\n`;
235
- handoff += '\n';
376
+ // ===========================================================================
377
+ // Session Handoff Tool (v2)
378
+ // ===========================================================================
379
+ // gaia_handoff Create session handoff document
380
+ server.tool('gaia_handoff', 'Create a comprehensive handoff document for transferring session context to the next session. Combines checkpoint data with relevant memories.', {
381
+ next_steps: z.array(z.string()).describe('List of next steps for the next session'),
382
+ context_notes: z.string().optional().describe('Additional context notes for the next session'),
383
+ include_memories: z.boolean().optional().describe('Include relevant memories from memory system (default: true)'),
384
+ memory_query: z.string().optional().describe('Query to find relevant memories (default: recent project memories)'),
385
+ }, async ({ next_steps, context_notes, include_memories = true, memory_query }) => {
386
+ try {
387
+ const handoffDir = join(homedir(), '.foundation', 'handoffs');
388
+ ensureDir(handoffDir);
389
+ // Read latest checkpoint if exists
390
+ const latestDir = join(getSessionsDir(), 'latest');
391
+ const checkpoint = readJsonFile(join(latestDir, 'checkpoint.json'));
392
+ // Build handoff document
393
+ const lines = [];
394
+ lines.push('# Session Handoff Document');
395
+ lines.push('');
396
+ lines.push(`Generated: ${new Date().toISOString()}`);
397
+ lines.push('');
398
+ lines.push('---');
399
+ lines.push('');
400
+ // Session state
401
+ if (checkpoint) {
402
+ lines.push('## Current Session State');
403
+ lines.push('');
404
+ lines.push(`**Project**: ${checkpoint.project || 'N/A'}`);
405
+ lines.push(`**Purpose**: ${checkpoint.purpose}`);
406
+ lines.push(`**Branch**: ${checkpoint.sections.context.branch || 'N/A'}`);
407
+ lines.push('');
408
+ lines.push(`**Summary**: ${checkpoint.summary}`);
409
+ lines.push('');
410
+ // Decisions
411
+ if (checkpoint.sections.decisions.length > 0) {
412
+ lines.push('### Recent Decisions');
413
+ lines.push('');
414
+ checkpoint.sections.decisions.forEach(d => {
415
+ lines.push(`**${d.topic}**`);
416
+ lines.push(`- Decision: ${d.decision}`);
417
+ lines.push(`- Rationale: ${d.rationale}`);
418
+ lines.push('');
419
+ });
420
+ }
421
+ // Progress
422
+ if (checkpoint.sections.progress.length > 0) {
423
+ lines.push('### Task Progress');
424
+ lines.push('');
425
+ checkpoint.sections.progress.forEach(p => {
426
+ const emoji = p.status === 'completed' ? '✓' : p.status === 'in_progress' ? '⏳' : p.status === 'blocked' ? '🚫' : '○';
427
+ lines.push(`${emoji} **${p.task}** (${p.status})`);
428
+ if (p.details)
429
+ lines.push(` ${p.details}`);
430
+ lines.push('');
431
+ });
432
+ }
433
+ // Changes
434
+ if (checkpoint.sections.changes.length > 0) {
435
+ lines.push('### Files Changed');
436
+ lines.push('');
437
+ checkpoint.sections.changes.forEach(c => {
438
+ const emoji = c.action === 'created' ? '+' : c.action === 'modified' ? '~' : '-';
439
+ lines.push(`${emoji} \`${c.file}\`: ${c.summary}`);
440
+ });
441
+ lines.push('');
442
+ }
443
+ // Open questions
444
+ if (checkpoint.sections.context.openQuestions && checkpoint.sections.context.openQuestions.length > 0) {
445
+ lines.push('### Open Questions');
446
+ lines.push('');
447
+ checkpoint.sections.context.openQuestions.forEach(q => {
448
+ lines.push(`- ${q}`);
449
+ });
450
+ lines.push('');
451
+ }
452
+ // Relevant files
453
+ if (checkpoint.sections.context.relevantFiles && checkpoint.sections.context.relevantFiles.length > 0) {
454
+ lines.push('### Relevant Files');
455
+ lines.push('');
456
+ checkpoint.sections.context.relevantFiles.forEach(f => {
457
+ lines.push(`- \`${f}\``);
458
+ });
459
+ lines.push('');
460
+ }
461
+ // Context notes from checkpoint
462
+ if (checkpoint.sections.context.notes) {
463
+ lines.push('### Context Notes');
464
+ lines.push('');
465
+ lines.push(checkpoint.sections.context.notes);
466
+ lines.push('');
467
+ }
468
+ }
469
+ else {
470
+ lines.push('## Current Session State');
471
+ lines.push('');
472
+ lines.push('_No checkpoint found for current session._');
473
+ lines.push('');
236
474
  }
237
- catch {
238
- // Not in a git repo
475
+ // Relevant memories
476
+ if (include_memories) {
477
+ lines.push('---');
478
+ lines.push('');
479
+ lines.push('## Relevant Memories');
480
+ lines.push('');
481
+ try {
482
+ const storage = getMemoryStorage();
483
+ const query = memory_query || (checkpoint?.project || 'project');
484
+ const memories = storage.search({
485
+ query,
486
+ tiers: ['project', 'session'],
487
+ limit: 10,
488
+ });
489
+ if (memories.length > 0) {
490
+ memories.forEach((result, idx) => {
491
+ lines.push(`### ${idx + 1}. ${result.memory.content.substring(0, 100)}...`);
492
+ lines.push('');
493
+ lines.push(`- **Tier**: ${result.memory.tier}`);
494
+ lines.push(`- **Tags**: ${result.memory.tags.join(', ')}`);
495
+ lines.push(`- **Relevance**: ${result.score.toFixed(2)}`);
496
+ if (result.memory.related_files.length > 0) {
497
+ lines.push(`- **Files**: ${result.memory.related_files.join(', ')}`);
498
+ }
499
+ lines.push('');
500
+ lines.push(result.memory.content);
501
+ lines.push('');
502
+ });
503
+ }
504
+ else {
505
+ lines.push('_No relevant memories found._');
506
+ lines.push('');
507
+ }
508
+ }
509
+ catch (error) {
510
+ lines.push('_Error retrieving memories._');
511
+ lines.push('');
512
+ }
239
513
  }
514
+ // Next steps
515
+ lines.push('---');
516
+ lines.push('');
517
+ lines.push('## Next Steps');
518
+ lines.push('');
519
+ next_steps.forEach((step, idx) => {
520
+ lines.push(`${idx + 1}. ${step}`);
521
+ });
522
+ lines.push('');
523
+ // Additional context
524
+ if (context_notes) {
525
+ lines.push('---');
526
+ lines.push('');
527
+ lines.push('## Additional Context');
528
+ lines.push('');
529
+ lines.push(context_notes);
530
+ lines.push('');
531
+ }
532
+ // Footer
533
+ lines.push('---');
534
+ lines.push('');
535
+ lines.push('*Generated by Foundation Gaia v2.0*');
536
+ const handoffContent = lines.join('\n');
537
+ const timestamp = new Date().toISOString().replace(/[:.]/g, '-').slice(0, 19);
538
+ const filename = `handoff-${timestamp}.md`;
539
+ const filePath = join(handoffDir, filename);
540
+ writeFileSync(filePath, handoffContent, 'utf-8');
541
+ return {
542
+ content: [{
543
+ type: 'text',
544
+ text: `✓ Handoff document created: ${filename}\n\nPath: ${filePath}\n\n${handoffContent.substring(0, 500)}...\n\n[Full document saved to file]`,
545
+ }],
546
+ };
547
+ }
548
+ catch (error) {
549
+ return {
550
+ content: [{
551
+ type: 'text',
552
+ text: `Error creating handoff: ${error.message}`,
553
+ }],
554
+ isError: true,
555
+ };
240
556
  }
241
- if (nextSteps.length > 0) {
242
- handoff += `## Next Steps\n`;
243
- for (const step of nextSteps) {
244
- handoff += `- ${step}\n`;
557
+ });
558
+ // ===========================================================================
559
+ // Autonomous Observation System (v2)
560
+ // ===========================================================================
561
+ /**
562
+ * Analyze session patterns and detect observations
563
+ */
564
+ function analyzeSessionPatterns(options) {
565
+ const observations = [];
566
+ const { checkpoint, lookback_days = 30 } = options;
567
+ try {
568
+ const memStorage = getMemoryStorage();
569
+ const cutoffTime = Date.now() - (lookback_days * 24 * 60 * 60 * 1000);
570
+ // Pattern 1: Repeated topics in decisions
571
+ if (checkpoint && checkpoint.sections.decisions.length > 0) {
572
+ const topics = checkpoint.sections.decisions.map(d => d.topic);
573
+ const topicCounts = new Map();
574
+ topics.forEach(t => topicCounts.set(t, (topicCounts.get(t) || 0) + 1));
575
+ topicCounts.forEach((count, topic) => {
576
+ if (count >= 2) {
577
+ observations.push({
578
+ pattern: `Repeated decision-making on: ${topic}`,
579
+ evidence: `${count} decisions made about ${topic} in current session`,
580
+ confidence: count >= 3 ? 'high' : 'medium',
581
+ });
582
+ }
583
+ });
584
+ }
585
+ // Pattern 2: Blocked tasks
586
+ if (checkpoint && checkpoint.sections.progress.length > 0) {
587
+ const blockedTasks = checkpoint.sections.progress.filter(p => p.status === 'blocked');
588
+ if (blockedTasks.length > 0) {
589
+ const blockers = blockedTasks.map(t => `"${t.task}"`).join(', ');
590
+ observations.push({
591
+ pattern: 'Task blockers detected',
592
+ evidence: `${blockedTasks.length} blocked task(s): ${blockers}`,
593
+ confidence: 'high',
594
+ });
595
+ }
596
+ }
597
+ // Pattern 3: Frequent file changes
598
+ if (checkpoint && checkpoint.sections.changes.length > 0) {
599
+ const files = checkpoint.sections.changes.map(c => c.file);
600
+ const fileCounts = new Map();
601
+ files.forEach(f => fileCounts.set(f, (fileCounts.get(f) || 0) + 1));
602
+ fileCounts.forEach((count, file) => {
603
+ if (count >= 3) {
604
+ observations.push({
605
+ pattern: `Frequent modifications to: ${file}`,
606
+ evidence: `${count} changes to ${file} in current session`,
607
+ confidence: 'medium',
608
+ });
609
+ }
610
+ });
611
+ }
612
+ // Pattern 4: Open questions (knowledge gaps)
613
+ if (checkpoint && checkpoint.sections.context.openQuestions && checkpoint.sections.context.openQuestions.length > 0) {
614
+ const questions = checkpoint.sections.context.openQuestions;
615
+ if (questions.length >= 3) {
616
+ observations.push({
617
+ pattern: 'Multiple open questions - potential knowledge gaps',
618
+ evidence: `${questions.length} unresolved questions: ${questions.slice(0, 2).join('; ')}${questions.length > 2 ? '...' : ''}`,
619
+ confidence: 'medium',
620
+ });
621
+ }
622
+ }
623
+ // Pattern 5: Frequent tags in recent memories
624
+ const allMemories = memStorage.search({
625
+ query: '*',
626
+ tiers: ['project', 'session'],
627
+ limit: 100,
628
+ });
629
+ const recentMemories = allMemories.filter(m => m.memory.created_at >= cutoffTime);
630
+ if (recentMemories.length >= 5) {
631
+ const tagCounts = new Map();
632
+ recentMemories.forEach(m => {
633
+ m.memory.tags.forEach(tag => {
634
+ tagCounts.set(tag, (tagCounts.get(tag) || 0) + 1);
635
+ });
636
+ });
637
+ const sortedTags = Array.from(tagCounts.entries())
638
+ .sort((a, b) => b[1] - a[1])
639
+ .slice(0, 3);
640
+ if (sortedTags.length > 0 && sortedTags[0][1] >= 5) {
641
+ const topTags = sortedTags.map(([tag, count]) => `${tag} (${count}x)`).join(', ');
642
+ observations.push({
643
+ pattern: `High activity in areas: ${sortedTags.map(t => t[0]).join(', ')}`,
644
+ evidence: `Frequent tags in recent memories: ${topTags}`,
645
+ confidence: 'high',
646
+ });
647
+ }
648
+ }
649
+ // Pattern 6: Reverted changes (created then deleted)
650
+ if (checkpoint && checkpoint.sections.changes.length >= 2) {
651
+ const fileActions = new Map();
652
+ checkpoint.sections.changes.forEach(c => {
653
+ if (!fileActions.has(c.file))
654
+ fileActions.set(c.file, []);
655
+ fileActions.get(c.file).push(c.action);
656
+ });
657
+ fileActions.forEach((actions, file) => {
658
+ if (actions.includes('created') && actions.includes('deleted')) {
659
+ observations.push({
660
+ pattern: `File created and deleted: ${file}`,
661
+ evidence: 'May indicate trial-and-error or false start',
662
+ confidence: 'medium',
663
+ });
664
+ }
665
+ });
245
666
  }
246
667
  }
247
- return {
248
- content: [{ type: 'text', text: handoff }],
249
- };
668
+ catch (error) {
669
+ // Silently fail pattern analysis - don't break the tool
670
+ }
671
+ return observations;
672
+ }
673
+ // gaia_observe — Analyze patterns and auto-save observations
674
+ server.tool('gaia_observe', 'Analyze session patterns and automatically capture observations. Detects recurring patterns, blockers, knowledge gaps, and anti-patterns.', {
675
+ lookback_days: z.number().optional().describe('Days of history to analyze (default: 30)'),
676
+ auto_save: z.boolean().optional().describe('Automatically save observations to memory (default: true)'),
677
+ min_confidence: z.enum(['low', 'medium', 'high']).optional().describe('Minimum confidence level to save (default: medium)'),
678
+ }, async ({ lookback_days = 30, auto_save = true, min_confidence = 'medium' }) => {
679
+ try {
680
+ // Read latest checkpoint
681
+ const latestDir = join(getSessionsDir(), 'latest');
682
+ const checkpoint = readJsonFile(join(latestDir, 'checkpoint.json'));
683
+ // Analyze patterns
684
+ const patterns = analyzeSessionPatterns({ checkpoint, lookback_days });
685
+ if (patterns.length === 0) {
686
+ return {
687
+ content: [{
688
+ type: 'text',
689
+ text: '✓ Analysis complete - no significant patterns detected.',
690
+ }],
691
+ };
692
+ }
693
+ // Filter by confidence
694
+ const confidenceLevels = { low: 1, medium: 2, high: 3 };
695
+ const minLevel = confidenceLevels[min_confidence];
696
+ const filteredPatterns = patterns.filter(p => confidenceLevels[p.confidence] >= minLevel);
697
+ if (filteredPatterns.length === 0) {
698
+ return {
699
+ content: [{
700
+ type: 'text',
701
+ text: `✓ Analysis complete - ${patterns.length} pattern(s) found but none meet ${min_confidence} confidence threshold.`,
702
+ }],
703
+ };
704
+ }
705
+ // Build response
706
+ const lines = [];
707
+ lines.push(`## Observations Detected (${filteredPatterns.length})`);
708
+ lines.push('');
709
+ const savedObservations = [];
710
+ // Auto-save observations if enabled
711
+ if (auto_save) {
712
+ const memStorage = getMemoryStorage();
713
+ filteredPatterns.forEach((obs, idx) => {
714
+ const confidenceEmoji = obs.confidence === 'high' ? '🔴' : obs.confidence === 'medium' ? '🟡' : '🟢';
715
+ lines.push(`${idx + 1}. ${confidenceEmoji} **${obs.pattern}**`);
716
+ lines.push(` - Evidence: ${obs.evidence}`);
717
+ lines.push(` - Confidence: ${obs.confidence}`);
718
+ // Save to observation tier
719
+ const memory = memStorage.saveMemory({
720
+ tier: 'observation',
721
+ content: `OBSERVATION: ${obs.pattern}. Evidence: ${obs.evidence}`,
722
+ tags: ['auto-observed', `confidence-${obs.confidence}`, 'pattern-detection'],
723
+ related_files: checkpoint?.sections.context.relevantFiles || [],
724
+ metadata: {
725
+ pattern_type: obs.pattern.split(':')[0].toLowerCase().replace(/\s+/g, '_'),
726
+ confidence: obs.confidence,
727
+ evidence: obs.evidence,
728
+ detected_at: new Date().toISOString(),
729
+ lookback_days,
730
+ },
731
+ });
732
+ savedObservations.push(memory.id);
733
+ lines.push(` - Saved: ${memory.id}`);
734
+ lines.push('');
735
+ });
736
+ lines.push('---');
737
+ lines.push('');
738
+ lines.push(`✓ Saved ${savedObservations.length} observation(s) to memory (tier: observation)`);
739
+ }
740
+ else {
741
+ filteredPatterns.forEach((obs, idx) => {
742
+ const confidenceEmoji = obs.confidence === 'high' ? '🔴' : obs.confidence === 'medium' ? '🟡' : '🟢';
743
+ lines.push(`${idx + 1}. ${confidenceEmoji} **${obs.pattern}**`);
744
+ lines.push(` - Evidence: ${obs.evidence}`);
745
+ lines.push(` - Confidence: ${obs.confidence}`);
746
+ lines.push('');
747
+ });
748
+ lines.push('---');
749
+ lines.push('');
750
+ lines.push('💡 Set `auto_save: true` to save these observations to memory.');
751
+ }
752
+ return {
753
+ content: [{
754
+ type: 'text',
755
+ text: lines.join('\n'),
756
+ }],
757
+ };
758
+ }
759
+ catch (error) {
760
+ return {
761
+ content: [{
762
+ type: 'text',
763
+ text: `Error analyzing patterns: ${error.message}`,
764
+ }],
765
+ isError: true,
766
+ };
767
+ }
250
768
  });
251
- // gaia_session_complete
252
- server.tool('gaia_session_complete', 'Mark a session as completed.', {
253
- sessionId: z.string().describe('Session ID to complete'),
254
- outcome: z.enum(['completed', 'abandoned']).optional().describe('Session outcome'),
255
- notes: z.string().optional().describe('Final notes'),
256
- }, async ({ sessionId, outcome = 'completed', notes }) => {
257
- const session = storage.getSession(sessionId);
258
- if (!session) {
259
- return {
260
- content: [{ type: 'text', text: `Session not found: ${sessionId}` }],
261
- };
262
- }
263
- storage.updateSession(sessionId, {
264
- status: outcome,
265
- completedAt: Date.now(),
266
- notes: notes ? [...session.notes, notes] : session.notes,
267
- });
268
- return {
269
- content: [
270
- {
271
- type: 'text',
272
- text: `✓ Session ${sessionId} marked as ${outcome}`,
273
- },
274
- ],
275
- };
769
+ // ===========================================================================
770
+ // Migration Tool (v1 v2)
771
+ // ===========================================================================
772
+ // gaia_migrate Migrate v1 checkpoint data to v2 memory system
773
+ server.tool('gaia_migrate', 'Migrate Gaia v1 checkpoint data to v2 memory system. Converts decisions, progress, and learnings to searchable memories.', {
774
+ dry_run: z.boolean().optional().describe('Preview migration without saving (default: true)'),
775
+ include_checkpoints: z.boolean().optional().describe('Migrate checkpoint decisions (default: true)'),
776
+ sessions_limit: z.number().optional().describe('Limit number of sessions to migrate (default: all)'),
777
+ }, async ({ dry_run = true, include_checkpoints = true, sessions_limit }) => {
778
+ try {
779
+ const lines = [];
780
+ lines.push(`## Gaia v1 → v2 Migration${dry_run ? ' (DRY RUN)' : ''}`);
781
+ lines.push('');
782
+ let totalDecisions = 0;
783
+ let totalProgress = 0;
784
+ let sessionsProcessed = 0;
785
+ const migratedMemories = [];
786
+ if (include_checkpoints) {
787
+ // Scan sessions directory for v1 checkpoints
788
+ const sessionsDir = getSessionsDir();
789
+ if (!existsSync(sessionsDir)) {
790
+ lines.push('⚠️ No v1 sessions directory found');
791
+ lines.push(` Expected: ${sessionsDir}`);
792
+ lines.push('');
793
+ lines.push('Nothing to migrate.');
794
+ return {
795
+ content: [{
796
+ type: 'text',
797
+ text: lines.join('\n'),
798
+ }],
799
+ };
800
+ }
801
+ // Read all session directories
802
+ const { readdirSync, statSync } = await import('fs');
803
+ const sessionDirs = readdirSync(sessionsDir)
804
+ .filter(name => {
805
+ const path = join(sessionsDir, name);
806
+ return statSync(path).isDirectory() && name !== 'latest';
807
+ })
808
+ .sort()
809
+ .reverse(); // Most recent first
810
+ const dirsToProcess = sessions_limit
811
+ ? sessionDirs.slice(0, sessions_limit)
812
+ : sessionDirs;
813
+ lines.push(`### Checkpoint Migration`);
814
+ lines.push('');
815
+ lines.push(`Found ${sessionDirs.length} session(s)`);
816
+ lines.push(`Processing ${dirsToProcess.length} session(s)`);
817
+ lines.push('');
818
+ const memStorage = getMemoryStorage();
819
+ for (const sessionDir of dirsToProcess) {
820
+ const checkpointPath = join(sessionsDir, sessionDir, 'checkpoint.json');
821
+ const checkpoint = readJsonFile(checkpointPath);
822
+ if (!checkpoint)
823
+ continue;
824
+ sessionsProcessed++;
825
+ lines.push(`#### Session: ${sessionDir}`);
826
+ lines.push(`Project: ${checkpoint.project || 'N/A'}`);
827
+ lines.push(`Purpose: ${checkpoint.purpose}`);
828
+ lines.push('');
829
+ // Migrate decisions
830
+ if (checkpoint.sections.decisions.length > 0) {
831
+ totalDecisions += checkpoint.sections.decisions.length;
832
+ for (const decision of checkpoint.sections.decisions) {
833
+ const content = `DECISION (migrated from v1): ${decision.topic}. Decision: ${decision.decision}. Rationale: ${decision.rationale}`;
834
+ if (!dry_run) {
835
+ const memory = memStorage.saveMemory({
836
+ tier: 'project',
837
+ content,
838
+ tags: ['migrated-v1', 'decision', decision.topic.toLowerCase().replace(/\s+/g, '-')],
839
+ related_files: checkpoint.sections.context.relevantFiles || [],
840
+ metadata: {
841
+ migrated_from: 'v1',
842
+ original_checkpoint_id: checkpoint.id,
843
+ original_timestamp: decision.timestamp,
844
+ decision_topic: decision.topic,
845
+ },
846
+ });
847
+ migratedMemories.push(memory.id);
848
+ lines.push(` ✓ Decision: ${decision.topic} → ${memory.id}`);
849
+ }
850
+ else {
851
+ lines.push(` • Decision: ${decision.topic}`);
852
+ }
853
+ }
854
+ }
855
+ // Migrate completed progress items as observations
856
+ const completedTasks = checkpoint.sections.progress.filter(p => p.status === 'completed');
857
+ if (completedTasks.length > 0) {
858
+ totalProgress += completedTasks.length;
859
+ for (const task of completedTasks) {
860
+ const content = `COMPLETED (migrated from v1): ${task.task}${task.details ? `. ${task.details}` : ''}`;
861
+ if (!dry_run) {
862
+ const memory = memStorage.saveMemory({
863
+ tier: 'session',
864
+ content,
865
+ tags: ['migrated-v1', 'completed', 'progress'],
866
+ related_files: [],
867
+ metadata: {
868
+ migrated_from: 'v1',
869
+ original_checkpoint_id: checkpoint.id,
870
+ task_status: task.status,
871
+ },
872
+ });
873
+ migratedMemories.push(memory.id);
874
+ lines.push(` ✓ Progress: ${task.task.substring(0, 50)} → ${memory.id}`);
875
+ }
876
+ else {
877
+ lines.push(` • Progress: ${task.task.substring(0, 50)}`);
878
+ }
879
+ }
880
+ }
881
+ lines.push('');
882
+ }
883
+ }
884
+ // Summary
885
+ lines.push('---');
886
+ lines.push('');
887
+ lines.push('### Migration Summary');
888
+ lines.push('');
889
+ lines.push(`Sessions processed: ${sessionsProcessed}`);
890
+ lines.push(`Decisions: ${totalDecisions}`);
891
+ lines.push(`Completed tasks: ${totalProgress}`);
892
+ lines.push(`Total items: ${totalDecisions + totalProgress}`);
893
+ if (dry_run) {
894
+ lines.push('');
895
+ lines.push('⚠️ **DRY RUN MODE** - No data was saved');
896
+ lines.push(' Set `dry_run: false` to perform actual migration');
897
+ }
898
+ else {
899
+ lines.push('');
900
+ lines.push(`✓ Migrated ${migratedMemories.length} memory/memories to v2 system`);
901
+ lines.push('');
902
+ lines.push('**Next steps:**');
903
+ lines.push('1. Search migrated data: `gaia_search({ query: "migrated-v1", tags: ["migrated-v1"] })`');
904
+ lines.push('2. Verify decisions: `gaia_search({ query: "decision", tiers: ["project"] })`');
905
+ lines.push('3. Review in context: Use `current_file` parameter for proximity boosting');
906
+ }
907
+ return {
908
+ content: [{
909
+ type: 'text',
910
+ text: lines.join('\n'),
911
+ }],
912
+ };
913
+ }
914
+ catch (error) {
915
+ return {
916
+ content: [{
917
+ type: 'text',
918
+ text: `Error during migration: ${error.message}`,
919
+ }],
920
+ isError: true,
921
+ };
922
+ }
276
923
  });
277
924
  // ===========================================================================
278
- // Learning Tools
925
+ // Learning Tools (unchanged from v1)
279
926
  // ===========================================================================
280
927
  // gaia_learn
281
928
  server.tool('gaia_learn', 'Capture a correction for CLAUDE.md. Gaia learns from mistakes so all future sessions benefit.', {
@@ -370,97 +1017,382 @@ export function registerGaiaTools(server) {
370
1017
  };
371
1018
  });
372
1019
  // ===========================================================================
373
- // Context Tools (Memory)
1020
+ // Memory Tools (v2.0 - SQLite + FTS5)
374
1021
  // ===========================================================================
375
- // gaia_remember
376
- server.tool('gaia_remember', 'Save current context state for later restoration. Gaia remembers everything.', {
377
- name: z.string().describe('Name for this memory'),
378
- description: z.string().optional().describe('Description of the context'),
379
- includeGit: z.boolean().optional().describe('Include git info (default: true)'),
380
- includeClaudeMd: z.boolean().optional().describe('Include CLAUDE.md (default: true)'),
381
- notes: z.string().optional().describe('Additional notes'),
382
- }, async ({ name, description, includeGit = true, includeClaudeMd = true, notes }) => {
383
- const data = { notes };
384
- if (includeGit) {
385
- try {
386
- const status = await git.getStatus();
387
- const log = await git.getLog({ count: 5 });
388
- data.gitBranch = status.branch;
389
- data.gitStatus = JSON.stringify(status);
390
- data.gitLog = log.map((l) => `${l.shortHash} ${l.message}`);
391
- }
392
- catch {
393
- // Not in git repo
394
- }
1022
+ // gaia_save
1023
+ server.tool('gaia_save', 'Save a new memory with tier, content, tags, and related files. Returns the created memory with ID.', {
1024
+ tier: z.enum(['session', 'project', 'global', 'note', 'observation'])
1025
+ .describe('Memory tier: session (ephemeral), project (cwd), global (user-wide), note (manual), observation (autonomous)'),
1026
+ content: z.string().describe('Memory content (what to remember)'),
1027
+ tags: z.array(z.string()).optional().describe('Tags for categorization (optional)'),
1028
+ related_files: z.array(z.string()).optional().describe('Related file paths (optional)'),
1029
+ session_id: z.string().optional().describe('Session ID for session-tier memories (optional)'),
1030
+ project_path: z.string().optional().describe('Project path for project-tier memories (optional)'),
1031
+ metadata: z.record(z.any()).optional().describe('Additional structured metadata (optional)'),
1032
+ }, async ({ tier, content, tags = [], related_files = [], session_id, project_path, metadata }) => {
1033
+ try {
1034
+ const storage = getMemoryStorage();
1035
+ const memory = storage.saveMemory({
1036
+ tier,
1037
+ content,
1038
+ tags,
1039
+ related_files,
1040
+ session_id,
1041
+ project_path,
1042
+ metadata,
1043
+ });
1044
+ return {
1045
+ content: [{
1046
+ type: 'text',
1047
+ text: JSON.stringify({
1048
+ success: true,
1049
+ memory: {
1050
+ id: memory.id,
1051
+ tier: memory.tier,
1052
+ content: memory.content,
1053
+ tags: memory.tags,
1054
+ related_files: memory.related_files,
1055
+ created_at: memory.created_at,
1056
+ },
1057
+ }, null, 2),
1058
+ }],
1059
+ };
1060
+ }
1061
+ catch (error) {
1062
+ return {
1063
+ content: [{
1064
+ type: 'text',
1065
+ text: `Error saving memory: ${error instanceof Error ? error.message : String(error)}`,
1066
+ }],
1067
+ isError: true,
1068
+ };
1069
+ }
1070
+ });
1071
+ // gaia_search
1072
+ server.tool('gaia_search', 'Search memories using FTS5 full-text search with composite scoring (BM25 + recency + tier + proximity + frequency). Returns ranked results.', {
1073
+ query: z.string().describe('Search query (supports FTS5 syntax: "word1 word2", "phrase", word*)'),
1074
+ tiers: z.array(z.enum(['session', 'project', 'global', 'note', 'observation'])).optional()
1075
+ .describe('Filter by tiers (optional)'),
1076
+ limit: z.number().optional().describe('Maximum results to return (default: 20, max: 100)'),
1077
+ current_file: z.string().optional().describe('Current file path for proximity scoring (optional)'),
1078
+ project_path: z.string().optional().describe('Current project path for context (optional)'),
1079
+ }, async ({ query, tiers, limit = 20, current_file, project_path }) => {
1080
+ try {
1081
+ const storage = getMemoryStorage();
1082
+ const results = storage.search({
1083
+ query,
1084
+ tiers,
1085
+ limit: Math.min(limit, 100),
1086
+ context: current_file || project_path ? { current_file, project_path } : undefined,
1087
+ });
1088
+ return {
1089
+ content: [{
1090
+ type: 'text',
1091
+ text: JSON.stringify({
1092
+ success: true,
1093
+ count: results.length,
1094
+ results: results.map(r => ({
1095
+ id: r.memory.id,
1096
+ tier: r.memory.tier,
1097
+ content: r.memory.content,
1098
+ tags: r.memory.tags,
1099
+ related_files: r.memory.related_files,
1100
+ created_at: r.memory.created_at,
1101
+ score: r.score,
1102
+ score_breakdown: {
1103
+ relevance: r.relevance_score,
1104
+ recency: r.recency_score,
1105
+ tier: r.tier_score,
1106
+ proximity: r.proximity_score,
1107
+ frequency: r.frequency_score,
1108
+ },
1109
+ })),
1110
+ }, null, 2),
1111
+ }],
1112
+ };
1113
+ }
1114
+ catch (error) {
1115
+ return {
1116
+ content: [{
1117
+ type: 'text',
1118
+ text: `Error searching memories: ${error instanceof Error ? error.message : String(error)}`,
1119
+ }],
1120
+ isError: true,
1121
+ };
395
1122
  }
396
- if (includeClaudeMd) {
397
- try {
398
- data.claudeMd = await git.readClaudeMd();
1123
+ });
1124
+ // gaia_get
1125
+ server.tool('gaia_get', 'Get a memory by ID. Increments access count and updates last accessed timestamp.', {
1126
+ id: z.string().describe('Memory ID (starts with "mem_")'),
1127
+ }, async ({ id }) => {
1128
+ try {
1129
+ const storage = getMemoryStorage();
1130
+ const memory = storage.getMemory(id);
1131
+ if (!memory) {
1132
+ return {
1133
+ content: [{
1134
+ type: 'text',
1135
+ text: JSON.stringify({
1136
+ success: false,
1137
+ error: `Memory not found: ${id}`,
1138
+ }, null, 2),
1139
+ }],
1140
+ };
399
1141
  }
400
- catch {
401
- // No CLAUDE.md
1142
+ return {
1143
+ content: [{
1144
+ type: 'text',
1145
+ text: JSON.stringify({
1146
+ success: true,
1147
+ memory: {
1148
+ id: memory.id,
1149
+ tier: memory.tier,
1150
+ content: memory.content,
1151
+ tags: memory.tags,
1152
+ related_files: memory.related_files,
1153
+ session_id: memory.session_id,
1154
+ project_path: memory.project_path,
1155
+ created_at: memory.created_at,
1156
+ accessed_at: memory.accessed_at,
1157
+ access_count: memory.access_count,
1158
+ metadata: memory.metadata,
1159
+ },
1160
+ }, null, 2),
1161
+ }],
1162
+ };
1163
+ }
1164
+ catch (error) {
1165
+ return {
1166
+ content: [{
1167
+ type: 'text',
1168
+ text: `Error getting memory: ${error instanceof Error ? error.message : String(error)}`,
1169
+ }],
1170
+ isError: true,
1171
+ };
1172
+ }
1173
+ });
1174
+ // gaia_delete
1175
+ server.tool('gaia_delete', 'Delete a memory by ID. Automatically deletes all associated links (CASCADE).', {
1176
+ id: z.string().describe('Memory ID to delete'),
1177
+ }, async ({ id }) => {
1178
+ try {
1179
+ const storage = getMemoryStorage();
1180
+ const result = storage.deleteMemory(id);
1181
+ if (!result.success) {
1182
+ return {
1183
+ content: [{
1184
+ type: 'text',
1185
+ text: JSON.stringify({
1186
+ success: false,
1187
+ error: `Memory not found: ${id}`,
1188
+ }, null, 2),
1189
+ }],
1190
+ };
402
1191
  }
1192
+ return {
1193
+ content: [{
1194
+ type: 'text',
1195
+ text: JSON.stringify({
1196
+ success: true,
1197
+ deleted_memory_id: id,
1198
+ deleted_links: result.deleted_links,
1199
+ }, null, 2),
1200
+ }],
1201
+ };
1202
+ }
1203
+ catch (error) {
1204
+ return {
1205
+ content: [{
1206
+ type: 'text',
1207
+ text: `Error deleting memory: ${error instanceof Error ? error.message : String(error)}`,
1208
+ }],
1209
+ isError: true,
1210
+ };
1211
+ }
1212
+ });
1213
+ // gaia_stats
1214
+ server.tool('gaia_stats', 'Get Gaia memory statistics: total memories, breakdown by tier, links, database size, age range.', {}, async () => {
1215
+ try {
1216
+ const storage = getMemoryStorage();
1217
+ const stats = storage.getStats();
1218
+ return {
1219
+ content: [{
1220
+ type: 'text',
1221
+ text: JSON.stringify({
1222
+ success: true,
1223
+ stats: {
1224
+ total_memories: stats.total_memories,
1225
+ by_tier: stats.by_tier,
1226
+ total_links: stats.total_links,
1227
+ database_size_mb: parseFloat(stats.total_size_mb.toFixed(2)),
1228
+ oldest_memory: stats.oldest_memory,
1229
+ newest_memory: stats.newest_memory,
1230
+ age_range_days: stats.oldest_memory > 0
1231
+ ? Math.floor((stats.newest_memory - stats.oldest_memory) / (1000 * 60 * 60 * 24))
1232
+ : 0,
1233
+ },
1234
+ }, null, 2),
1235
+ }],
1236
+ };
1237
+ }
1238
+ catch (error) {
1239
+ return {
1240
+ content: [{
1241
+ type: 'text',
1242
+ text: `Error getting stats: ${error instanceof Error ? error.message : String(error)}`,
1243
+ }],
1244
+ isError: true,
1245
+ };
403
1246
  }
404
- const snapshot = {
405
- id: `mem_${nanoid(8)}`,
406
- name,
407
- description,
408
- data,
409
- createdAt: Date.now(),
410
- };
411
- storage.addSnapshot(snapshot);
412
- return {
413
- content: [
414
- {
415
- type: 'text',
416
- text: `✓ Memory saved: ${snapshot.id}\n\nName: ${name}`,
417
- },
418
- ],
419
- };
420
1247
  });
421
- // gaia_recall
422
- server.tool('gaia_recall', 'Restore a previously saved context. Gaia recalls everything.', {
423
- memoryId: z.string().optional().describe('Memory ID to restore'),
424
- name: z.string().optional().describe('Memory name to restore'),
425
- }, async ({ memoryId, name }) => {
426
- const snapshot = memoryId
427
- ? storage.getSnapshot(memoryId)
428
- : name
429
- ? storage.getSnapshotByName(name)
430
- : null;
431
- if (!snapshot) {
432
- // List available memories
433
- const all = storage.getSnapshots();
434
- if (all.length === 0) {
1248
+ // ===========================================================================
1249
+ // Cross-Prompt Linking Tools
1250
+ // ===========================================================================
1251
+ // gaia_link
1252
+ server.tool('gaia_link', 'Create a typed link between two memories. Builds cross-prompt dependency graph.', {
1253
+ from_memory_id: z.string().describe('Source memory ID'),
1254
+ to_memory_id: z.string().describe('Target memory ID'),
1255
+ link_type: z.enum(['depends_on', 'extends', 'reverts', 'related', 'contradicts'])
1256
+ .describe('Link type: depends_on (requires), extends (builds on), reverts (undoes), related (associated), contradicts (conflicts)'),
1257
+ }, async ({ from_memory_id, to_memory_id, link_type }) => {
1258
+ try {
1259
+ const storage = getMemoryStorage();
1260
+ // Verify both memories exist
1261
+ const fromMemory = storage.getMemory(from_memory_id);
1262
+ const toMemory = storage.getMemory(to_memory_id);
1263
+ if (!fromMemory) {
435
1264
  return {
436
- content: [{ type: 'text', text: 'No memories available.' }],
1265
+ content: [{
1266
+ type: 'text',
1267
+ text: JSON.stringify({
1268
+ success: false,
1269
+ error: `Source memory not found: ${from_memory_id}`,
1270
+ }, null, 2),
1271
+ }],
437
1272
  };
438
1273
  }
439
- let output = '## Available Memories\n\n';
440
- for (const s of all) {
441
- output += `- **${s.name}** (${s.id}): ${s.description || 'No description'}\n`;
1274
+ if (!toMemory) {
1275
+ return {
1276
+ content: [{
1277
+ type: 'text',
1278
+ text: JSON.stringify({
1279
+ success: false,
1280
+ error: `Target memory not found: ${to_memory_id}`,
1281
+ }, null, 2),
1282
+ }],
1283
+ };
442
1284
  }
1285
+ const link = storage.createLink({
1286
+ from_memory_id,
1287
+ to_memory_id,
1288
+ link_type,
1289
+ });
443
1290
  return {
444
- content: [{ type: 'text', text: output }],
1291
+ content: [{
1292
+ type: 'text',
1293
+ text: JSON.stringify({
1294
+ success: true,
1295
+ link: {
1296
+ from_memory_id: link.from_memory_id,
1297
+ to_memory_id: link.to_memory_id,
1298
+ link_type: link.link_type,
1299
+ created_at: link.created_at,
1300
+ },
1301
+ from_memory_content: fromMemory.content.substring(0, 100),
1302
+ to_memory_content: toMemory.content.substring(0, 100),
1303
+ }, null, 2),
1304
+ }],
445
1305
  };
446
1306
  }
447
- let output = `## Recalled: ${snapshot.name}\n\n`;
448
- if (snapshot.data.gitBranch) {
449
- output += `**Git Branch**: ${snapshot.data.gitBranch}\n\n`;
1307
+ catch (error) {
1308
+ return {
1309
+ content: [{
1310
+ type: 'text',
1311
+ text: `Error creating link: ${error instanceof Error ? error.message : String(error)}`,
1312
+ }],
1313
+ isError: true,
1314
+ };
450
1315
  }
451
- if (snapshot.data.gitLog) {
452
- output += `**Recent Commits**:\n`;
453
- for (const log of snapshot.data.gitLog) {
454
- output += `- ${log}\n`;
1316
+ });
1317
+ // gaia_graph
1318
+ server.tool('gaia_graph', 'Get the link graph for a memory. Returns all linked memories grouped by link type.', {
1319
+ memory_id: z.string().describe('Memory ID to get links for'),
1320
+ }, async ({ memory_id }) => {
1321
+ try {
1322
+ const storage = getMemoryStorage();
1323
+ // Verify memory exists
1324
+ const memory = storage.getMemory(memory_id);
1325
+ if (!memory) {
1326
+ return {
1327
+ content: [{
1328
+ type: 'text',
1329
+ text: JSON.stringify({
1330
+ success: false,
1331
+ error: `Memory not found: ${memory_id}`,
1332
+ }, null, 2),
1333
+ }],
1334
+ };
455
1335
  }
456
- output += '\n';
1336
+ const links = storage.getLinks(memory_id);
1337
+ // Count total links
1338
+ const totalLinks = Object.values(links).reduce((sum, arr) => sum + arr.length, 0);
1339
+ return {
1340
+ content: [{
1341
+ type: 'text',
1342
+ text: JSON.stringify({
1343
+ success: true,
1344
+ memory_id,
1345
+ memory_content: memory.content.substring(0, 100),
1346
+ total_links: totalLinks,
1347
+ links: {
1348
+ depends_on: links.depends_on.map(m => ({
1349
+ id: m.id,
1350
+ content: m.content.substring(0, 100),
1351
+ tier: m.tier,
1352
+ })),
1353
+ extends: links.extends.map(m => ({
1354
+ id: m.id,
1355
+ content: m.content.substring(0, 100),
1356
+ tier: m.tier,
1357
+ })),
1358
+ reverts: links.reverts.map(m => ({
1359
+ id: m.id,
1360
+ content: m.content.substring(0, 100),
1361
+ tier: m.tier,
1362
+ })),
1363
+ related: links.related.map(m => ({
1364
+ id: m.id,
1365
+ content: m.content.substring(0, 100),
1366
+ tier: m.tier,
1367
+ })),
1368
+ contradicts: links.contradicts.map(m => ({
1369
+ id: m.id,
1370
+ content: m.content.substring(0, 100),
1371
+ tier: m.tier,
1372
+ })),
1373
+ },
1374
+ }, null, 2),
1375
+ }],
1376
+ };
457
1377
  }
458
- if (snapshot.data.notes) {
459
- output += `**Notes**: ${snapshot.data.notes}\n`;
1378
+ catch (error) {
1379
+ return {
1380
+ content: [{
1381
+ type: 'text',
1382
+ text: `Error getting graph: ${error instanceof Error ? error.message : String(error)}`,
1383
+ }],
1384
+ isError: true,
1385
+ };
460
1386
  }
461
- return {
462
- content: [{ type: 'text', text: output }],
463
- };
464
1387
  });
465
1388
  }
1389
+ /**
1390
+ * Cleanup function to close database connection
1391
+ */
1392
+ export function cleanupGaiaMemory() {
1393
+ if (memoryStorage) {
1394
+ memoryStorage.close();
1395
+ memoryStorage = null;
1396
+ }
1397
+ }
466
1398
  //# sourceMappingURL=index.js.map