get-claudia 1.51.4 → 1.51.6

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.
@@ -9,6 +9,7 @@
9
9
  * claudia gmail status - Check Gmail connection status
10
10
  * claudia gmail search - Search emails
11
11
  * claudia gmail read - Read a specific email
12
+ * claudia gmail send - Send an email with optional attachments
12
13
  * claudia gmail logout - Sign out of Gmail
13
14
  * claudia calendar login - Sign in with Google (Calendar only)
14
15
  * claudia calendar status - Check Calendar connection status
@@ -18,9 +19,146 @@
18
19
  * claudia calendar logout - Sign out of Calendar
19
20
  */
20
21
 
22
+ import { readFileSync, existsSync, statSync } from 'node:fs';
23
+ import { basename, extname } from 'node:path';
24
+ import { randomBytes } from 'node:crypto';
21
25
  import { authenticate, getAccessToken, isAuthenticated, revokeTokens, authStatus } from '../core/google-oauth.js';
22
26
  import { outputJson as output } from '../core/output.js';
23
27
 
28
+ // ── MIME Helpers (for gmail send) ──
29
+
30
+ const MIME_TYPES = {
31
+ png: 'image/png',
32
+ jpg: 'image/jpeg',
33
+ jpeg: 'image/jpeg',
34
+ gif: 'image/gif',
35
+ webp: 'image/webp',
36
+ svg: 'image/svg+xml',
37
+ pdf: 'application/pdf',
38
+ txt: 'text/plain',
39
+ csv: 'text/csv',
40
+ json: 'application/json',
41
+ xml: 'application/xml',
42
+ html: 'text/html',
43
+ zip: 'application/zip',
44
+ gz: 'application/gzip',
45
+ doc: 'application/msword',
46
+ docx: 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
47
+ xls: 'application/vnd.ms-excel',
48
+ xlsx: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
49
+ ppt: 'application/vnd.ms-powerpoint',
50
+ pptx: 'application/vnd.openxmlformats-officedocument.presentationml.presentation',
51
+ mp3: 'audio/mpeg',
52
+ mp4: 'video/mp4',
53
+ wav: 'audio/wav',
54
+ };
55
+
56
+ function getMimeType(filePath) {
57
+ const ext = extname(filePath).toLowerCase().replace('.', '');
58
+ return MIME_TYPES[ext] || 'application/octet-stream';
59
+ }
60
+
61
+ /**
62
+ * Validate and read attachment files from disk.
63
+ * @param {string[]} filePaths
64
+ * @returns {Array<{path: string, data: Buffer, mimeType: string, filename: string}>}
65
+ */
66
+ function prepareAttachments(filePaths) {
67
+ const MAX_SIZE = 25 * 1024 * 1024; // 25 MB Gmail limit
68
+ const attachments = [];
69
+
70
+ for (const filePath of filePaths) {
71
+ if (!existsSync(filePath)) {
72
+ throw new Error(`Attachment not found: ${filePath}`);
73
+ }
74
+ const stat = statSync(filePath);
75
+ if (!stat.isFile()) {
76
+ throw new Error(`Not a file: ${filePath}`);
77
+ }
78
+ if (stat.size > MAX_SIZE) {
79
+ throw new Error(`File too large (${(stat.size / 1024 / 1024).toFixed(1)} MB, max 25 MB): ${filePath}`);
80
+ }
81
+ attachments.push({
82
+ path: filePath,
83
+ data: readFileSync(filePath),
84
+ mimeType: getMimeType(filePath),
85
+ filename: basename(filePath),
86
+ });
87
+ }
88
+ return attachments;
89
+ }
90
+
91
+ /**
92
+ * Build an RFC 2822 MIME message string.
93
+ * @param {Object} opts
94
+ * @param {string[]} opts.to
95
+ * @param {string} opts.subject
96
+ * @param {string} opts.body
97
+ * @param {string[]} [opts.cc]
98
+ * @param {string[]} [opts.bcc]
99
+ * @param {boolean} [opts.html]
100
+ * @param {string} [opts.replyTo] - Message-ID for In-Reply-To/References
101
+ * @param {Array} [opts.attachments] - From prepareAttachments()
102
+ * @returns {string}
103
+ */
104
+ function buildMimeMessage(opts) {
105
+ const lines = [];
106
+
107
+ // Headers
108
+ lines.push(`To: ${opts.to.join(', ')}`);
109
+ lines.push(`Subject: ${opts.subject}`);
110
+ lines.push('MIME-Version: 1.0');
111
+ if (opts.cc && opts.cc.length) lines.push(`Cc: ${opts.cc.join(', ')}`);
112
+ if (opts.bcc && opts.bcc.length) lines.push(`Bcc: ${opts.bcc.join(', ')}`);
113
+
114
+ if (opts.replyTo) {
115
+ const msgId = opts.replyTo.startsWith('<') ? opts.replyTo : `<${opts.replyTo}>`;
116
+ lines.push(`In-Reply-To: ${msgId}`);
117
+ lines.push(`References: ${msgId}`);
118
+ }
119
+
120
+ const hasAttachments = opts.attachments && opts.attachments.length > 0;
121
+ const contentType = opts.html ? 'text/html; charset=UTF-8' : 'text/plain; charset=UTF-8';
122
+
123
+ if (!hasAttachments) {
124
+ // Simple single-part message
125
+ lines.push(`Content-Type: ${contentType}`);
126
+ lines.push('Content-Transfer-Encoding: base64');
127
+ lines.push(''); // blank line separating headers from body
128
+ lines.push(Buffer.from(opts.body, 'utf-8').toString('base64'));
129
+ } else {
130
+ // Multipart/mixed
131
+ const boundary = `claudia_${randomBytes(16).toString('hex')}`;
132
+ lines.push(`Content-Type: multipart/mixed; boundary="${boundary}"`);
133
+ lines.push('');
134
+
135
+ // Text part
136
+ lines.push(`--${boundary}`);
137
+ lines.push(`Content-Type: ${contentType}`);
138
+ lines.push('Content-Transfer-Encoding: base64');
139
+ lines.push('');
140
+ lines.push(Buffer.from(opts.body, 'utf-8').toString('base64'));
141
+ lines.push('');
142
+
143
+ // Attachment parts
144
+ for (const att of opts.attachments) {
145
+ lines.push(`--${boundary}`);
146
+ lines.push(`Content-Type: ${att.mimeType}; name="${att.filename}"`);
147
+ lines.push(`Content-Disposition: attachment; filename="${att.filename}"`);
148
+ lines.push('Content-Transfer-Encoding: base64');
149
+ lines.push('');
150
+ // Split base64 into 76-char lines per RFC 2045
151
+ const b64 = att.data.toString('base64');
152
+ lines.push((b64.match(/.{1,76}/g) || []).join('\r\n'));
153
+ lines.push('');
154
+ }
155
+
156
+ lines.push(`--${boundary}--`);
157
+ }
158
+
159
+ return lines.join('\r\n');
160
+ }
161
+
24
162
  // ── Gmail Commands ──
25
163
 
26
164
  export async function gmailLoginCommand() {
@@ -146,6 +284,90 @@ export async function gmailReadCommand(messageId) {
146
284
  });
147
285
  }
148
286
 
287
+ export async function gmailSendCommand(opts) {
288
+ const token = await getAccessToken('gmail');
289
+ if (!token) {
290
+ console.error('Not authenticated. Run: claudia gmail login');
291
+ process.exitCode = 1;
292
+ return;
293
+ }
294
+
295
+ // Validate required fields (belt-and-suspenders; Commander's requiredOption catches most)
296
+ if (!opts.to || opts.to.length === 0) {
297
+ console.error('At least one --to recipient is required.');
298
+ process.exitCode = 1;
299
+ return;
300
+ }
301
+ if (!opts.subject) {
302
+ console.error('--subject is required.');
303
+ process.exitCode = 1;
304
+ return;
305
+ }
306
+ if (!opts.body) {
307
+ console.error('--body is required.');
308
+ process.exitCode = 1;
309
+ return;
310
+ }
311
+
312
+ // Prepare attachments
313
+ let attachments = [];
314
+ if (opts.attach && opts.attach.length > 0) {
315
+ try {
316
+ attachments = prepareAttachments(opts.attach);
317
+ } catch (err) {
318
+ console.error(err.message);
319
+ process.exitCode = 1;
320
+ return;
321
+ }
322
+ }
323
+
324
+ // Build MIME message and base64url-encode it
325
+ const rawMessage = buildMimeMessage({
326
+ to: opts.to,
327
+ subject: opts.subject,
328
+ body: opts.body,
329
+ cc: opts.cc,
330
+ bcc: opts.bcc,
331
+ html: opts.html,
332
+ replyTo: opts.replyTo,
333
+ attachments,
334
+ });
335
+
336
+ const encodedMessage = Buffer.from(rawMessage, 'utf-8')
337
+ .toString('base64')
338
+ .replace(/\+/g, '-')
339
+ .replace(/\//g, '_')
340
+ .replace(/=+$/, '');
341
+
342
+ const requestBody = { raw: encodedMessage };
343
+ if (opts.thread) {
344
+ requestBody.threadId = opts.thread;
345
+ }
346
+
347
+ const resp = await fetch('https://gmail.googleapis.com/gmail/v1/users/me/messages/send', {
348
+ method: 'POST',
349
+ headers: {
350
+ Authorization: `Bearer ${token}`,
351
+ 'Content-Type': 'application/json',
352
+ },
353
+ body: JSON.stringify(requestBody),
354
+ });
355
+
356
+ if (!resp.ok) {
357
+ const err = await resp.text();
358
+ console.error(`Gmail API error (${resp.status}): ${err}`);
359
+ process.exitCode = 1;
360
+ return;
361
+ }
362
+
363
+ const data = await resp.json();
364
+ output({
365
+ id: data.id,
366
+ threadId: data.threadId,
367
+ labelIds: data.labelIds || [],
368
+ });
369
+ }
370
+
149
371
  export async function gmailLogoutCommand() {
150
372
  const removed = revokeTokens('gmail');
151
373
  if (removed) {
@@ -901,7 +901,7 @@ class ClaudiaDatabase {
901
901
  "ALTER TABLE memories ADD COLUMN lifecycle_tier TEXT DEFAULT 'active'",
902
902
  'ALTER TABLE memories ADD COLUMN sacred_reason TEXT',
903
903
  'ALTER TABLE memories ADD COLUMN archived_at TEXT',
904
- 'ALTER TABLE memories ADD COLUMN fact_id TEXT UNIQUE',
904
+ 'ALTER TABLE memories ADD COLUMN fact_id TEXT',
905
905
  'ALTER TABLE memories ADD COLUMN hash TEXT',
906
906
  'ALTER TABLE memories ADD COLUMN prev_hash TEXT',
907
907
  'ALTER TABLE entities ADD COLUMN close_circle BOOLEAN DEFAULT FALSE',
@@ -920,7 +920,7 @@ class ClaudiaDatabase {
920
920
  'CREATE INDEX IF NOT EXISTS idx_memories_lifecycle ON memories(lifecycle_tier)'
921
921
  );
922
922
  this.db.exec(
923
- 'CREATE INDEX IF NOT EXISTS idx_memories_fact_id ON memories(fact_id)'
923
+ 'CREATE UNIQUE INDEX IF NOT EXISTS idx_memories_fact_id ON memories(fact_id)'
924
924
  );
925
925
  this.db.exec(
926
926
  'CREATE INDEX IF NOT EXISTS idx_entities_close_circle ON entities(close_circle) WHERE close_circle = 1'
package/cli/index.js CHANGED
@@ -522,7 +522,7 @@ program
522
522
  // ── Gmail subcommand group ──
523
523
  const gmail = program
524
524
  .command('gmail')
525
- .description('Gmail integration (login, search, read)');
525
+ .description('Gmail integration (login, search, read, send)');
526
526
 
527
527
  gmail
528
528
  .command('login')
@@ -559,6 +559,23 @@ gmail
559
559
  await gmailReadCommand(messageId);
560
560
  });
561
561
 
562
+ gmail
563
+ .command('send')
564
+ .description('Send an email (with optional attachments)')
565
+ .requiredOption('--to <email...>', 'Recipient email address(es)')
566
+ .requiredOption('--subject <text>', 'Email subject')
567
+ .requiredOption('--body <text>', 'Email body text')
568
+ .option('--cc <email...>', 'CC recipient(s)')
569
+ .option('--bcc <email...>', 'BCC recipient(s)')
570
+ .option('--attach <filepath...>', 'File(s) to attach')
571
+ .option('--html', 'Treat body as HTML', false)
572
+ .option('--thread <threadId>', 'Thread ID (for replies)')
573
+ .option('--reply-to <messageId>', 'Message-ID for In-Reply-To header')
574
+ .action(async (opts) => {
575
+ const { gmailSendCommand } = await import('./commands/google-auth.js');
576
+ await gmailSendCommand(opts);
577
+ });
578
+
562
579
  gmail
563
580
  .command('logout')
564
581
  .description('Sign out of Gmail (remove stored tokens)')
@@ -0,0 +1,477 @@
1
+ -- Claudia Memory System Schema
2
+ -- SQLite with sqlite-vec for vector similarity search
3
+ -- WAL mode enabled for crash safety
4
+
5
+ -- Enable WAL mode for crash safety
6
+ PRAGMA journal_mode = WAL;
7
+ PRAGMA synchronous = NORMAL;
8
+ PRAGMA foreign_keys = ON;
9
+
10
+ -- ============================================================================
11
+ -- ENTITIES: People, organizations, projects, concepts
12
+ -- ============================================================================
13
+
14
+ CREATE TABLE IF NOT EXISTS entities (
15
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
16
+ name TEXT NOT NULL,
17
+ type TEXT NOT NULL CHECK (type IN ('person', 'organization', 'project', 'concept', 'location')),
18
+ canonical_name TEXT, -- Normalized name for matching (lowercase, no titles)
19
+ description TEXT,
20
+ importance REAL DEFAULT 1.0, -- Decays over time but never deleted
21
+ created_at TEXT DEFAULT (datetime('now')),
22
+ updated_at TEXT DEFAULT (datetime('now')),
23
+ metadata TEXT, -- JSON blob for flexible attributes
24
+ last_contact_at TEXT, -- Last time user interacted with this entity
25
+ contact_frequency_days REAL, -- Average days between contacts (rolling)
26
+ contact_trend TEXT, -- accelerating, stable, decelerating, dormant
27
+ attention_tier TEXT DEFAULT 'standard', -- active, watchlist, standard, archive
28
+ close_circle BOOLEAN DEFAULT FALSE, -- Inner circle: never decay, auto-sacred
29
+ close_circle_reason TEXT, -- Why this entity is close-circle
30
+ UNIQUE(canonical_name, type)
31
+ );
32
+
33
+ CREATE INDEX IF NOT EXISTS idx_entities_type ON entities(type);
34
+ CREATE INDEX IF NOT EXISTS idx_entities_canonical ON entities(canonical_name);
35
+ CREATE INDEX IF NOT EXISTS idx_entities_importance ON entities(importance DESC);
36
+ CREATE INDEX IF NOT EXISTS idx_entities_last_contact ON entities(last_contact_at);
37
+ CREATE INDEX IF NOT EXISTS idx_entities_trend ON entities(contact_trend);
38
+ CREATE INDEX IF NOT EXISTS idx_entities_attention_tier ON entities(attention_tier);
39
+ CREATE INDEX IF NOT EXISTS idx_entities_close_circle ON entities(close_circle) WHERE close_circle = 1;
40
+
41
+ -- Entity aliases for matching variations (e.g., "Sarah", "Sarah Chen", "S. Chen")
42
+ CREATE TABLE IF NOT EXISTS entity_aliases (
43
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
44
+ entity_id INTEGER NOT NULL REFERENCES entities(id) ON DELETE CASCADE,
45
+ alias TEXT NOT NULL,
46
+ canonical_alias TEXT NOT NULL, -- Normalized for matching
47
+ created_at TEXT DEFAULT (datetime('now')),
48
+ UNIQUE(entity_id, canonical_alias)
49
+ );
50
+
51
+ CREATE INDEX IF NOT EXISTS idx_entity_aliases_canonical ON entity_aliases(canonical_alias);
52
+
53
+ -- ============================================================================
54
+ -- MEMORIES: Facts, preferences, observations, learnings
55
+ -- ============================================================================
56
+
57
+ CREATE TABLE IF NOT EXISTS memories (
58
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
59
+ content TEXT NOT NULL,
60
+ content_hash TEXT UNIQUE, -- SHA256 for deduplication
61
+ type TEXT NOT NULL CHECK (type IN ('fact', 'preference', 'observation', 'learning', 'commitment', 'pattern')),
62
+ importance REAL DEFAULT 1.0, -- Decays over time
63
+ confidence REAL DEFAULT 1.0, -- How sure we are about this
64
+ source TEXT, -- Where this came from (conversation, document, etc.)
65
+ source_id TEXT, -- Reference to source (episode_id, etc.)
66
+ source_context TEXT, -- One-line breadcrumb (e.g., "Email from Jim re: Forum V+, 2025-01-28")
67
+ created_at TEXT DEFAULT (datetime('now')),
68
+ updated_at TEXT DEFAULT (datetime('now')),
69
+ last_accessed_at TEXT, -- For rehearsal-based importance boost
70
+ access_count INTEGER DEFAULT 0,
71
+ verified_at TEXT, -- When this memory was verified
72
+ verification_status TEXT DEFAULT 'pending', -- pending, verified, flagged, contradicts
73
+ metadata TEXT, -- JSON blob for flexible attributes
74
+ source_channel TEXT DEFAULT 'claude_code', -- Origin channel: claude_code, telegram, slack
75
+ deadline_at TEXT, -- ISO datetime for commitment deadlines (Phase 2: temporal intelligence)
76
+ temporal_markers TEXT, -- JSON: extracted temporal references from content
77
+ lifecycle_tier TEXT DEFAULT 'active', -- sacred/active/cooling/archived
78
+ sacred_reason TEXT, -- Why this memory is sacred (user-protected, auto-detected)
79
+ archived_at TEXT, -- When this memory was archived
80
+ fact_id TEXT UNIQUE, -- UUID for human-friendly reference
81
+ hash TEXT, -- SHA-256 chain hash
82
+ prev_hash TEXT -- Previous hash in chain (NULL for genesis)
83
+ );
84
+
85
+ CREATE INDEX IF NOT EXISTS idx_memories_type ON memories(type);
86
+ CREATE INDEX IF NOT EXISTS idx_memories_importance ON memories(importance DESC);
87
+ CREATE INDEX IF NOT EXISTS idx_memories_created ON memories(created_at DESC);
88
+ CREATE INDEX IF NOT EXISTS idx_memories_hash ON memories(content_hash);
89
+ CREATE INDEX IF NOT EXISTS idx_memories_deadline ON memories(deadline_at);
90
+ CREATE INDEX IF NOT EXISTS idx_memories_verification ON memories(verification_status);
91
+ CREATE INDEX IF NOT EXISTS idx_memories_lifecycle ON memories(lifecycle_tier);
92
+ CREATE INDEX IF NOT EXISTS idx_memories_fact_id ON memories(fact_id);
93
+
94
+ -- Junction table linking memories to entities
95
+ CREATE TABLE IF NOT EXISTS memory_entities (
96
+ memory_id INTEGER NOT NULL REFERENCES memories(id) ON DELETE CASCADE,
97
+ entity_id INTEGER NOT NULL REFERENCES entities(id) ON DELETE CASCADE,
98
+ relationship TEXT DEFAULT 'about', -- about, by, to, from, etc.
99
+ PRIMARY KEY (memory_id, entity_id, relationship)
100
+ );
101
+
102
+ CREATE INDEX IF NOT EXISTS idx_memory_entities_entity ON memory_entities(entity_id);
103
+
104
+ -- ============================================================================
105
+ -- RELATIONSHIPS: Graph connections between entities
106
+ -- ============================================================================
107
+
108
+ CREATE TABLE IF NOT EXISTS relationships (
109
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
110
+ source_entity_id INTEGER NOT NULL REFERENCES entities(id) ON DELETE CASCADE,
111
+ target_entity_id INTEGER NOT NULL REFERENCES entities(id) ON DELETE CASCADE,
112
+ relationship_type TEXT NOT NULL, -- works_with, manages, client_of, etc.
113
+ strength REAL DEFAULT 1.0, -- Relationship strength (decays/grows)
114
+ origin_type TEXT DEFAULT 'extracted', -- user_stated, extracted, inferred, corrected
115
+ direction TEXT DEFAULT 'bidirectional' CHECK (direction IN ('forward', 'backward', 'bidirectional')),
116
+ valid_at TEXT, -- When this relationship became true in the real world
117
+ invalid_at TEXT, -- When this relationship was superseded (NULL = current)
118
+ created_at TEXT DEFAULT (datetime('now')),
119
+ updated_at TEXT DEFAULT (datetime('now')),
120
+ metadata TEXT,
121
+ lifecycle_tier TEXT DEFAULT 'active', -- Mirrors memory lifecycle for consistency
122
+ UNIQUE(source_entity_id, target_entity_id, relationship_type)
123
+ );
124
+
125
+ CREATE INDEX IF NOT EXISTS idx_relationships_source ON relationships(source_entity_id);
126
+ CREATE INDEX IF NOT EXISTS idx_relationships_target ON relationships(target_entity_id);
127
+ CREATE INDEX IF NOT EXISTS idx_relationships_type ON relationships(relationship_type);
128
+ CREATE INDEX IF NOT EXISTS idx_relationships_temporal ON relationships(invalid_at, valid_at);
129
+
130
+ -- ============================================================================
131
+ -- EPISODES: Conversation session summaries
132
+ -- ============================================================================
133
+
134
+ CREATE TABLE IF NOT EXISTS episodes (
135
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
136
+ session_id TEXT UNIQUE, -- External session identifier
137
+ summary TEXT,
138
+ narrative TEXT, -- Free-form session narrative (tone, context, unresolved threads)
139
+ started_at TEXT DEFAULT (datetime('now')),
140
+ ended_at TEXT,
141
+ message_count INTEGER DEFAULT 0,
142
+ turn_count INTEGER DEFAULT 0, -- Buffered turns count
143
+ is_summarized INTEGER DEFAULT 0, -- Whether session has been summarized by Claude
144
+ source TEXT, -- Origin channel: 'claude_code', 'telegram', 'slack', etc.
145
+ ingested_at TEXT, -- When Claude Code read this (NULL = unread)
146
+ key_topics TEXT, -- JSON array of main topics
147
+ metadata TEXT
148
+ );
149
+
150
+ CREATE INDEX IF NOT EXISTS idx_episodes_session ON episodes(session_id);
151
+ CREATE INDEX IF NOT EXISTS idx_episodes_started ON episodes(started_at DESC);
152
+
153
+ -- ============================================================================
154
+ -- MESSAGES: Individual conversation turns
155
+ -- ============================================================================
156
+
157
+ CREATE TABLE IF NOT EXISTS messages (
158
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
159
+ episode_id INTEGER REFERENCES episodes(id) ON DELETE CASCADE,
160
+ role TEXT NOT NULL CHECK (role IN ('user', 'assistant', 'system')),
161
+ content TEXT NOT NULL,
162
+ content_hash TEXT, -- For deduplication
163
+ created_at TEXT DEFAULT (datetime('now')),
164
+ metadata TEXT
165
+ );
166
+
167
+ CREATE INDEX IF NOT EXISTS idx_messages_episode ON messages(episode_id);
168
+ CREATE INDEX IF NOT EXISTS idx_messages_created ON messages(created_at DESC);
169
+
170
+ -- ============================================================================
171
+ -- PATTERNS: Detected behavioral patterns
172
+ -- ============================================================================
173
+
174
+ CREATE TABLE IF NOT EXISTS patterns (
175
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
176
+ name TEXT NOT NULL,
177
+ description TEXT NOT NULL,
178
+ pattern_type TEXT NOT NULL, -- behavioral, communication, scheduling, relationship
179
+ occurrences INTEGER DEFAULT 1,
180
+ first_observed_at TEXT DEFAULT (datetime('now')),
181
+ last_observed_at TEXT DEFAULT (datetime('now')),
182
+ confidence REAL DEFAULT 0.5, -- Grows with observations
183
+ is_active INTEGER DEFAULT 1,
184
+ evidence TEXT, -- JSON array of supporting observations
185
+ metadata TEXT
186
+ );
187
+
188
+ CREATE INDEX IF NOT EXISTS idx_patterns_type ON patterns(pattern_type);
189
+ CREATE INDEX IF NOT EXISTS idx_patterns_active ON patterns(is_active);
190
+ CREATE INDEX IF NOT EXISTS idx_patterns_confidence ON patterns(confidence DESC);
191
+
192
+ -- ============================================================================
193
+ -- PREDICTIONS: Proactive suggestions
194
+ -- ============================================================================
195
+
196
+ CREATE TABLE IF NOT EXISTS predictions (
197
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
198
+ content TEXT NOT NULL,
199
+ prediction_type TEXT NOT NULL, -- reminder, suggestion, warning, insight
200
+ priority REAL DEFAULT 0.5,
201
+ expires_at TEXT, -- When this prediction is no longer relevant
202
+ is_shown INTEGER DEFAULT 0, -- Whether user has seen this
203
+ is_acted_on INTEGER DEFAULT 0, -- Whether user acted on this
204
+ created_at TEXT DEFAULT (datetime('now')),
205
+ shown_at TEXT,
206
+ prediction_pattern_name TEXT, -- Links to pattern for feedback loop
207
+ metadata TEXT
208
+ );
209
+
210
+ CREATE INDEX IF NOT EXISTS idx_predictions_type ON predictions(prediction_type);
211
+ CREATE INDEX IF NOT EXISTS idx_predictions_expires ON predictions(expires_at);
212
+ CREATE INDEX IF NOT EXISTS idx_predictions_shown ON predictions(is_shown);
213
+ CREATE INDEX IF NOT EXISTS idx_predictions_priority ON predictions(priority DESC);
214
+
215
+ -- ============================================================================
216
+ -- CONFIG: Runtime configuration stored in database
217
+ -- ============================================================================
218
+
219
+ CREATE TABLE IF NOT EXISTS config (
220
+ key TEXT PRIMARY KEY,
221
+ value TEXT NOT NULL,
222
+ updated_at TEXT DEFAULT (datetime('now'))
223
+ );
224
+
225
+ -- ============================================================================
226
+ -- VECTOR TABLES: sqlite-vec virtual tables for semantic search
227
+ -- ============================================================================
228
+ -- NOTE: vec0 virtual tables are created by database.py initialize() with
229
+ -- configurable dimensions from config.embedding_dimensions.
230
+ -- This allows changing embedding models without schema changes.
231
+
232
+ -- ============================================================================
233
+ -- TURN BUFFER: Raw conversation turns awaiting session summary
234
+ -- ============================================================================
235
+
236
+ CREATE TABLE IF NOT EXISTS turn_buffer (
237
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
238
+ episode_id INTEGER NOT NULL REFERENCES episodes(id) ON DELETE CASCADE,
239
+ turn_number INTEGER NOT NULL,
240
+ user_content TEXT,
241
+ assistant_content TEXT,
242
+ is_archived INTEGER DEFAULT 0,
243
+ source TEXT, -- Origin channel: 'claude_code', 'telegram', 'slack', etc.
244
+ created_at TEXT DEFAULT (datetime('now'))
245
+ );
246
+
247
+ CREATE INDEX IF NOT EXISTS idx_turn_buffer_episode ON turn_buffer(episode_id);
248
+
249
+ -- Episode narrative embeddings are created by database.py with configurable dimensions.
250
+
251
+ -- ============================================================================
252
+ -- DOCUMENTS: File registry for provenance tracking
253
+ -- ============================================================================
254
+
255
+ CREATE TABLE IF NOT EXISTS documents (
256
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
257
+ file_hash TEXT, -- SHA-256 of file contents for deduplication
258
+ filename TEXT NOT NULL,
259
+ mime_type TEXT,
260
+ file_size INTEGER,
261
+ storage_provider TEXT DEFAULT 'local' CHECK (storage_provider IN ('local', 'google_drive')),
262
+ storage_path TEXT, -- Resolved file path on disk or cloud URI
263
+ source_type TEXT CHECK (source_type IN ('gmail', 'transcript', 'upload', 'capture', 'session')),
264
+ source_ref TEXT, -- External reference (email ID, URL, etc.)
265
+ summary TEXT,
266
+ lifecycle TEXT DEFAULT 'active' CHECK (lifecycle IN ('active', 'dormant', 'archived', 'purged')),
267
+ last_accessed_at TEXT,
268
+ created_at TEXT DEFAULT (datetime('now')),
269
+ updated_at TEXT DEFAULT (datetime('now')),
270
+ workspace_id TEXT, -- Project hash for isolation
271
+ metadata TEXT -- JSON blob
272
+ );
273
+
274
+ CREATE INDEX IF NOT EXISTS idx_documents_hash ON documents(file_hash);
275
+ CREATE INDEX IF NOT EXISTS idx_documents_lifecycle ON documents(lifecycle);
276
+ CREATE INDEX IF NOT EXISTS idx_documents_source_type ON documents(source_type);
277
+ CREATE INDEX IF NOT EXISTS idx_documents_workspace ON documents(workspace_id);
278
+ CREATE INDEX IF NOT EXISTS idx_documents_source_lookup ON documents(source_type, source_ref);
279
+
280
+ -- Links documents to entities (people, projects, etc.)
281
+ CREATE TABLE IF NOT EXISTS entity_documents (
282
+ entity_id INTEGER NOT NULL REFERENCES entities(id) ON DELETE CASCADE,
283
+ document_id INTEGER NOT NULL REFERENCES documents(id) ON DELETE CASCADE,
284
+ relationship TEXT DEFAULT 'about' CHECK (relationship IN ('sent_by', 'about', 'mentioned_in', 'authored', 'received_by')),
285
+ created_at TEXT DEFAULT (datetime('now')),
286
+ PRIMARY KEY (entity_id, document_id, relationship)
287
+ );
288
+
289
+ CREATE INDEX IF NOT EXISTS idx_entity_documents_doc ON entity_documents(document_id);
290
+
291
+ -- Links memories to source documents (provenance)
292
+ CREATE TABLE IF NOT EXISTS memory_sources (
293
+ memory_id INTEGER NOT NULL REFERENCES memories(id) ON DELETE CASCADE,
294
+ document_id INTEGER NOT NULL REFERENCES documents(id) ON DELETE CASCADE,
295
+ excerpt TEXT, -- Relevant excerpt from the document
296
+ created_at TEXT DEFAULT (datetime('now')),
297
+ PRIMARY KEY (memory_id, document_id)
298
+ );
299
+
300
+ CREATE INDEX IF NOT EXISTS idx_memory_sources_doc ON memory_sources(document_id);
301
+
302
+ -- ============================================================================
303
+ -- DATABASE METADATA
304
+ -- ============================================================================
305
+
306
+ -- Stores database-level metadata like workspace path for identification
307
+ CREATE TABLE IF NOT EXISTS _meta (
308
+ key TEXT PRIMARY KEY,
309
+ value TEXT,
310
+ updated_at TEXT DEFAULT (datetime('now'))
311
+ );
312
+
313
+ -- ============================================================================
314
+ -- MIGRATION TRACKING
315
+ -- ============================================================================
316
+
317
+ CREATE TABLE IF NOT EXISTS schema_migrations (
318
+ version INTEGER PRIMARY KEY,
319
+ applied_at TEXT DEFAULT (datetime('now')),
320
+ description TEXT
321
+ );
322
+
323
+ -- Record schema versions
324
+ INSERT OR IGNORE INTO schema_migrations (version, description)
325
+ VALUES (1, 'Initial schema with entities, memories, relationships, episodes, patterns, predictions');
326
+
327
+ INSERT OR IGNORE INTO schema_migrations (version, description)
328
+ VALUES (2, 'Add turn_buffer table, episode narrative/summary columns, episode_embeddings');
329
+
330
+ INSERT OR IGNORE INTO schema_migrations (version, description)
331
+ VALUES (3, 'Add source_context to memories, is_archived to turn_buffer for episodic provenance');
332
+
333
+ -- NOTE: FTS5 full-text search (migration v4) is created by database.py migration code
334
+ -- rather than here, because CREATE TRIGGER statements contain internal semicolons
335
+ -- that the schema.sql line-based parser cannot handle.
336
+
337
+ INSERT OR IGNORE INTO schema_migrations (version, description)
338
+ VALUES (5, 'Add verification columns to memories, prediction_pattern_name to predictions');
339
+
340
+ INSERT OR IGNORE INTO schema_migrations (version, description)
341
+ VALUES (6, 'Add source and ingested_at to episodes, source to turn_buffer for gateway integration');
342
+
343
+ INSERT OR IGNORE INTO schema_migrations (version, description)
344
+ VALUES (7, 'Add documents, entity_documents, memory_sources tables for provenance tracking');
345
+
346
+ INSERT OR IGNORE INTO schema_migrations (version, description)
347
+ VALUES (8, 'Add valid_at, invalid_at to relationships for bi-temporal tracking');
348
+
349
+ INSERT OR IGNORE INTO schema_migrations (version, description)
350
+ VALUES (9, 'Add _meta table for database identification and workspace path tracking');
351
+
352
+ -- ============================================================================
353
+ -- REFLECTIONS: Persistent learnings and observations (from /meditate)
354
+ -- ============================================================================
355
+
356
+ CREATE TABLE IF NOT EXISTS reflections (
357
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
358
+ episode_id INTEGER REFERENCES episodes(id),
359
+
360
+ -- Type and content
361
+ reflection_type TEXT NOT NULL CHECK (reflection_type IN ('observation', 'pattern', 'learning', 'question')),
362
+ content TEXT NOT NULL,
363
+ content_hash TEXT, -- For deduplication
364
+
365
+ -- Optional entity association
366
+ about_entity_id INTEGER REFERENCES entities(id),
367
+
368
+ -- Scoring (reflections are user-approved, so start high)
369
+ importance REAL DEFAULT 0.7,
370
+ confidence REAL DEFAULT 0.8,
371
+
372
+ -- Very slow decay (reflections are long-term learnings)
373
+ decay_rate REAL DEFAULT 0.999,
374
+
375
+ -- Aggregation tracking
376
+ aggregated_from TEXT, -- JSON array of reflection IDs this merged from
377
+ aggregation_count INTEGER DEFAULT 1,
378
+
379
+ -- Timeline tracking (pattern evolution)
380
+ first_observed_at TEXT DEFAULT (datetime('now')),
381
+ last_confirmed_at TEXT DEFAULT (datetime('now')),
382
+
383
+ -- Embedding for semantic search
384
+ embedding BLOB,
385
+
386
+ -- Timestamps
387
+ created_at TEXT DEFAULT (datetime('now')),
388
+ updated_at TEXT,
389
+ surfaced_count INTEGER DEFAULT 0,
390
+ last_surfaced_at TEXT
391
+ );
392
+
393
+ CREATE INDEX IF NOT EXISTS idx_reflections_type ON reflections(reflection_type);
394
+ CREATE INDEX IF NOT EXISTS idx_reflections_importance ON reflections(importance DESC);
395
+ CREATE INDEX IF NOT EXISTS idx_reflections_entity ON reflections(about_entity_id);
396
+ CREATE INDEX IF NOT EXISTS idx_reflections_episode ON reflections(episode_id);
397
+
398
+ -- Reflection embeddings are created by database.py with configurable dimensions.
399
+
400
+ INSERT OR IGNORE INTO schema_migrations (version, description)
401
+ VALUES (10, 'Add reflections table and reflection_embeddings for /meditate skill');
402
+
403
+ INSERT OR IGNORE INTO schema_migrations (version, description)
404
+ VALUES (11, 'Add compound index for fast source lookup on documents');
405
+
406
+ INSERT OR IGNORE INTO schema_migrations (version, description)
407
+ VALUES (13, 'Add origin_type to memories, agent_dispatches table for Trust North Star');
408
+
409
+ INSERT OR IGNORE INTO schema_migrations (version, description)
410
+ VALUES (14, 'Add dispatch_tier to agent_dispatches for native agent team support');
411
+
412
+ INSERT OR IGNORE INTO schema_migrations (version, description)
413
+ VALUES (15, 'Add origin_type to relationships for organic trust model');
414
+
415
+ INSERT OR IGNORE INTO schema_migrations (version, description)
416
+ VALUES (16, 'Add source_channel to memories for channel-aware memory');
417
+
418
+ INSERT OR IGNORE INTO schema_migrations (version, description)
419
+ VALUES (17, 'Add deadline_at and temporal_markers to memories for temporal intelligence');
420
+
421
+ INSERT OR IGNORE INTO schema_migrations (version, description)
422
+ VALUES (18, 'Add contact velocity and attention tier to entities for proactive relationship intelligence');
423
+
424
+ -- ============================================================================
425
+ -- ENTITY SUMMARIES: Hierarchical summaries for graph-aware retrieval
426
+ -- ============================================================================
427
+
428
+ CREATE TABLE IF NOT EXISTS entity_summaries (
429
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
430
+ entity_id INTEGER NOT NULL REFERENCES entities(id) ON DELETE CASCADE,
431
+ summary TEXT NOT NULL, -- Generated summary of all memories about this entity
432
+ summary_type TEXT DEFAULT 'overview', -- overview, relationship_map, timeline
433
+ memory_count INTEGER DEFAULT 0, -- Number of memories summarized
434
+ relationship_count INTEGER DEFAULT 0, -- Number of relationships included
435
+ generated_at TEXT DEFAULT (datetime('now')),
436
+ expires_at TEXT, -- When to regenerate (NULL = never expires)
437
+ metadata TEXT, -- JSON: entity_ids_included, key_themes, etc.
438
+ UNIQUE(entity_id, summary_type)
439
+ );
440
+
441
+ CREATE INDEX IF NOT EXISTS idx_entity_summaries_entity ON entity_summaries(entity_id);
442
+ CREATE INDEX IF NOT EXISTS idx_entity_summaries_expires ON entity_summaries(expires_at);
443
+
444
+ INSERT OR IGNORE INTO schema_migrations (version, description)
445
+ VALUES (19, 'Add entity_summaries table for hierarchical graph-aware retrieval');
446
+
447
+ -- ============================================================================
448
+ -- AGENT DISPATCHES: Track delegated tasks to sub-agents
449
+ -- ============================================================================
450
+
451
+ CREATE TABLE IF NOT EXISTS agent_dispatches (
452
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
453
+ agent_name TEXT NOT NULL,
454
+ dispatch_category TEXT NOT NULL,
455
+ task_summary TEXT,
456
+ started_at TEXT DEFAULT (datetime('now')),
457
+ completed_at TEXT,
458
+ duration_ms INTEGER,
459
+ success INTEGER DEFAULT 1,
460
+ required_claudia_judgment INTEGER DEFAULT 0,
461
+ judgment_reason TEXT,
462
+ episode_id INTEGER REFERENCES episodes(id),
463
+ user_approved INTEGER DEFAULT 1,
464
+ dispatch_tier TEXT DEFAULT 'task',
465
+ metadata TEXT
466
+ );
467
+
468
+ CREATE INDEX IF NOT EXISTS idx_agent_dispatches_agent ON agent_dispatches(agent_name);
469
+ CREATE INDEX IF NOT EXISTS idx_agent_dispatches_category ON agent_dispatches(dispatch_category);
470
+ CREATE INDEX IF NOT EXISTS idx_agent_dispatches_started ON agent_dispatches(started_at DESC);
471
+
472
+ -- NOTE: dispatch_tier validation trigger is created by database.py migration code
473
+ -- rather than here, because CREATE TRIGGER statements contain internal semicolons
474
+ -- that the schema.sql line-based parser cannot handle.
475
+
476
+ INSERT OR IGNORE INTO schema_migrations (version, description)
477
+ VALUES (20, 'Add lifecycle tiers, sacred memories, close-circle entities, fact_id, SHA-256 chain');
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "get-claudia",
3
- "version": "1.51.4",
3
+ "version": "1.51.6",
4
4
  "description": "An AI assistant who learns how you work.",
5
5
  "keywords": [
6
6
  "claudia",
@@ -34,7 +34,8 @@
34
34
  "cli",
35
35
  "template-v2",
36
36
  "assets",
37
- "visualizer"
37
+ "visualizer",
38
+ "memory-daemon/claudia_memory/schema.sql"
38
39
  ],
39
40
  "dependencies": {
40
41
  "better-sqlite3": "^11.8.1",
@@ -328,6 +328,7 @@ I adapt to whatever tools are available. When you ask me to do something that ne
328
328
  | `claudia gmail status` | Check if Gmail is connected |
329
329
  | `claudia gmail search "<query>"` | Search emails (Gmail search syntax) |
330
330
  | `claudia gmail read <messageId>` | Read a specific email |
331
+ | `claudia gmail send --to <email> --subject <text> --body <text>` | Send email (supports --attach, --cc, --bcc, --html, --thread, --reply-to) |
331
332
  | `claudia gmail logout` | Disconnect Gmail, remove tokens |
332
333
  | `claudia calendar login` | Opens browser for Calendar-only OAuth |
333
334
  | `claudia calendar status` | Check if Calendar is connected |