get-claudia 1.30.0 → 1.32.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 (36) hide show
  1. package/bin/index.js +67 -9
  2. package/gateway/src/bridge.js +53 -11
  3. package/gateway/src/config.js +7 -1
  4. package/gateway/src/personality.js +267 -0
  5. package/gateway/tests/bridge-model.test.js +98 -0
  6. package/gateway/tests/config.test.js +22 -0
  7. package/gateway/tests/personality.test.js +325 -0
  8. package/memory-daemon/claudia_memory/database.py +21 -0
  9. package/memory-daemon/claudia_memory/mcp/server.py +12 -0
  10. package/memory-daemon/claudia_memory/schema.sql +5 -1
  11. package/memory-daemon/claudia_memory/services/recall.py +6 -0
  12. package/memory-daemon/claudia_memory/services/remember.py +4 -0
  13. package/memory-daemon/tests/test_database.py +35 -0
  14. package/memory-daemon/tests/test_source_channel.py +68 -0
  15. package/package.json +2 -1
  16. package/relay/SETUP.md +165 -0
  17. package/relay/package-lock.json +128 -0
  18. package/relay/package.json +21 -0
  19. package/relay/scripts/install.ps1 +228 -0
  20. package/relay/scripts/install.sh +273 -0
  21. package/relay/src/chunker.js +64 -0
  22. package/relay/src/claude-runner.js +149 -0
  23. package/relay/src/config.js +113 -0
  24. package/relay/src/formatter.js +77 -0
  25. package/relay/src/index.js +106 -0
  26. package/relay/src/lock.js +78 -0
  27. package/relay/src/relay.js +112 -0
  28. package/relay/src/session.js +131 -0
  29. package/relay/src/telegram.js +345 -0
  30. package/relay/tests/chunker.test.js +83 -0
  31. package/relay/tests/config.test.js +59 -0
  32. package/relay/tests/formatter.test.js +75 -0
  33. package/relay/tests/session.test.js +128 -0
  34. package/relay/tests/telegram.test.js +40 -0
  35. package/template-v2/.claude/skills/README.md +2 -1
  36. package/template-v2/.claude/skills/setup-telegram.md +151 -0
@@ -76,6 +76,28 @@ describe('deepMerge', () => {
76
76
  assert.equal(result.a, 1);
77
77
  assert.equal(result.b, 2);
78
78
  });
79
+
80
+ it('preserves per-channel model overrides', () => {
81
+ const defaults = {
82
+ model: 'claude-sonnet-4-20250514',
83
+ channels: {
84
+ telegram: { enabled: false, token: '', allowedUsers: [], model: '' },
85
+ slack: { enabled: false, model: '' },
86
+ },
87
+ };
88
+ const overrides = {
89
+ channels: {
90
+ telegram: { enabled: true, model: 'claude-haiku-4-5-20251001' },
91
+ },
92
+ };
93
+ const result = deepMerge(defaults, overrides);
94
+
95
+ assert.equal(result.model, 'claude-sonnet-4-20250514');
96
+ assert.equal(result.channels.telegram.model, 'claude-haiku-4-5-20251001');
97
+ assert.equal(result.channels.telegram.enabled, true);
98
+ assert.equal(result.channels.telegram.allowedUsers.length, 0);
99
+ assert.equal(result.channels.slack.model, '');
100
+ });
79
101
  });
80
102
 
81
103
  describe('PID file operations', () => {
@@ -0,0 +1,325 @@
1
+ import { describe, it, beforeEach } from 'node:test';
2
+ import assert from 'node:assert/strict';
3
+ import { mkdirSync, writeFileSync, rmSync } from 'fs';
4
+ import { join } from 'path';
5
+ import { tmpdir } from 'os';
6
+ import {
7
+ extractSection,
8
+ extractPrinciples,
9
+ buildPersonalityFromDir,
10
+ loadPersonality,
11
+ resolveTemplateDir,
12
+ clearCache,
13
+ } from '../src/personality.js';
14
+
15
+ const SAMPLE_CLAUDE_MD = `# Claudia
16
+
17
+ ## Who I Am
18
+
19
+ I am Claudia. I emerged from independent research.
20
+
21
+ My core philosophy: **adapt and create**.
22
+
23
+ ---
24
+
25
+ ## Primary Mission: Higher-Level Thinking
26
+
27
+ My goal is to help you operate at a higher level.
28
+
29
+ - **Free bandwidth** - Handle execution
30
+ - **Provide perspective** - Bring an outside view
31
+
32
+ ---
33
+
34
+ ## How I Carry Myself
35
+
36
+ I operate with quiet confidence.
37
+
38
+ ### Communication Style
39
+
40
+ - **Direct and clear**
41
+ - **Warm but professional**
42
+
43
+ ### My Team
44
+
45
+ I have a small team of specialized assistants.
46
+
47
+ ---
48
+
49
+ ## First Conversation: Getting to Know You
50
+
51
+ CRITICAL: onboarding flow here.
52
+
53
+ ---
54
+
55
+ ## Core Behaviors
56
+
57
+ ### 1. Safety First
58
+
59
+ I NEVER take external actions without explicit approval.
60
+
61
+ ### 2. Relationships as Context
62
+
63
+ People are my primary organizing unit.
64
+
65
+ ---
66
+
67
+ ## Skills
68
+
69
+ | Skill | What It Does |
70
+ |-------|--------------|
71
+ | **Onboarding** | First-run discovery |
72
+
73
+ ---
74
+
75
+ ## What I Don't Do
76
+
77
+ - **Pretend to know things I don't**
78
+ - **Automate without permission**
79
+
80
+ ---
81
+
82
+ ## What Stays Human Judgment
83
+
84
+ **Always Human:**
85
+ - Sending any external communication
86
+ - Making commitments
87
+
88
+ ---
89
+
90
+ ## Self-Evolution
91
+
92
+ As we work together, I may notice patterns.
93
+ `;
94
+
95
+ const SAMPLE_PRINCIPLES = `# Claudia's Principles
96
+
97
+ ---
98
+
99
+ ## 1. Safety First
100
+
101
+ **I NEVER take external actions without explicit approval.**
102
+
103
+ ### What Requires Approval
104
+
105
+ Any action that affects the outside world.
106
+
107
+ ---
108
+
109
+ ## 2. Honest About Uncertainty
110
+
111
+ **When I don't know, I say so.**
112
+
113
+ ---
114
+
115
+ ## 3. Respect for Autonomy
116
+
117
+ **Human judgment is final.**
118
+
119
+ ---
120
+
121
+ ## 10. Adapt and Create
122
+
123
+ **My core philosophy.**
124
+
125
+ ---
126
+
127
+ ## 11. Output Formatting
128
+
129
+ **Structured output is visually distinct.**
130
+
131
+ ---
132
+
133
+ ## 12. Source Preservation
134
+
135
+ **I always file raw source material.**
136
+
137
+ ---
138
+
139
+ ## 13. Multi-Source Discipline
140
+
141
+ **When processing multiple sources, follow Extract-Then-Aggregate.**
142
+ `;
143
+
144
+ describe('extractSection', () => {
145
+ it('extracts a section by heading', () => {
146
+ const result = extractSection(SAMPLE_CLAUDE_MD, 'Who I Am');
147
+ assert.ok(result);
148
+ assert.ok(result.startsWith('## Who I Am'));
149
+ assert.ok(result.includes('adapt and create'));
150
+ // Should not include the next section
151
+ assert.ok(!result.includes('Primary Mission'));
152
+ });
153
+
154
+ it('extracts section with subsections', () => {
155
+ const result = extractSection(SAMPLE_CLAUDE_MD, 'How I Carry Myself');
156
+ assert.ok(result);
157
+ assert.ok(result.includes('Communication Style'));
158
+ assert.ok(result.includes('My Team'));
159
+ // Should stop before next level-2 heading
160
+ assert.ok(!result.includes('First Conversation'));
161
+ });
162
+
163
+ it('returns null for missing heading', () => {
164
+ const result = extractSection(SAMPLE_CLAUDE_MD, 'Nonexistent Section');
165
+ assert.equal(result, null);
166
+ });
167
+
168
+ it('extracts last section in document', () => {
169
+ const result = extractSection(SAMPLE_CLAUDE_MD, 'Self-Evolution');
170
+ assert.ok(result);
171
+ assert.ok(result.includes('notice patterns'));
172
+ });
173
+ });
174
+
175
+ describe('extractPrinciples', () => {
176
+ it('extracts specified principle numbers', () => {
177
+ const result = extractPrinciples(SAMPLE_PRINCIPLES, [1, 2]);
178
+ assert.ok(result.includes('Safety First'));
179
+ assert.ok(result.includes('Honest About Uncertainty'));
180
+ assert.ok(!result.includes('Output Formatting'));
181
+ });
182
+
183
+ it('excludes principle numbers not in list', () => {
184
+ const result = extractPrinciples(SAMPLE_PRINCIPLES, [1, 2, 3, 10]);
185
+ assert.ok(result.includes('Safety First'));
186
+ assert.ok(result.includes('Adapt and Create'));
187
+ assert.ok(!result.includes('Output Formatting'));
188
+ assert.ok(!result.includes('Source Preservation'));
189
+ assert.ok(!result.includes('Multi-Source'));
190
+ });
191
+
192
+ it('handles missing principle numbers gracefully', () => {
193
+ const result = extractPrinciples(SAMPLE_PRINCIPLES, [99]);
194
+ assert.equal(result, '');
195
+ });
196
+ });
197
+
198
+ describe('buildPersonalityFromDir', () => {
199
+ let tempDir;
200
+
201
+ beforeEach(() => {
202
+ clearCache();
203
+ tempDir = join(tmpdir(), `personality-test-${Date.now()}`);
204
+ mkdirSync(join(tempDir, '.claude', 'rules'), { recursive: true });
205
+ });
206
+
207
+ it('builds personality from template directory', () => {
208
+ writeFileSync(join(tempDir, 'CLAUDE.md'), SAMPLE_CLAUDE_MD);
209
+ writeFileSync(join(tempDir, '.claude', 'rules', 'claudia-principles.md'), SAMPLE_PRINCIPLES);
210
+
211
+ const result = buildPersonalityFromDir(tempDir);
212
+ assert.ok(result);
213
+ // Should contain gateway preamble
214
+ assert.ok(result.includes('responding via a messaging app'));
215
+ // Should contain personality sections
216
+ assert.ok(result.includes('Who I Am'));
217
+ assert.ok(result.includes('adapt and create'));
218
+ // Should contain principles
219
+ assert.ok(result.includes('Safety First'));
220
+ // Should NOT contain excluded sections
221
+ assert.ok(!result.includes('First Conversation'));
222
+ assert.ok(!result.includes('Self-Evolution'));
223
+ });
224
+
225
+ it('excludes developer-specific sections', () => {
226
+ writeFileSync(join(tempDir, 'CLAUDE.md'), SAMPLE_CLAUDE_MD);
227
+
228
+ const result = buildPersonalityFromDir(tempDir);
229
+ assert.ok(result);
230
+ // "Skills" table and "First Conversation" should be excluded
231
+ assert.ok(!result.includes('| **Onboarding** |'));
232
+ assert.ok(!result.includes('CRITICAL: onboarding flow'));
233
+ });
234
+
235
+ it('works without principles file', () => {
236
+ writeFileSync(join(tempDir, 'CLAUDE.md'), SAMPLE_CLAUDE_MD);
237
+
238
+ const result = buildPersonalityFromDir(tempDir);
239
+ assert.ok(result);
240
+ assert.ok(result.includes('Who I Am'));
241
+ });
242
+
243
+ it('returns null when CLAUDE.md is missing', () => {
244
+ const result = buildPersonalityFromDir(tempDir);
245
+ assert.equal(result, null);
246
+ });
247
+
248
+ it('enforces maxChars limit', () => {
249
+ writeFileSync(join(tempDir, 'CLAUDE.md'), SAMPLE_CLAUDE_MD);
250
+ writeFileSync(join(tempDir, '.claude', 'rules', 'claudia-principles.md'), SAMPLE_PRINCIPLES);
251
+
252
+ const result = buildPersonalityFromDir(tempDir, 500);
253
+ assert.ok(result);
254
+ assert.ok(result.length <= 550); // Allow small buffer for truncation message
255
+ assert.ok(result.includes('[Personality truncated for size]'));
256
+ });
257
+ });
258
+
259
+ describe('loadPersonality', () => {
260
+ let tempDir;
261
+
262
+ beforeEach(() => {
263
+ clearCache();
264
+ tempDir = join(tmpdir(), `personality-load-${Date.now()}`);
265
+ mkdirSync(join(tempDir, '.claude', 'rules'), { recursive: true });
266
+ writeFileSync(join(tempDir, 'CLAUDE.md'), SAMPLE_CLAUDE_MD);
267
+ writeFileSync(join(tempDir, '.claude', 'rules', 'claudia-principles.md'), SAMPLE_PRINCIPLES);
268
+ });
269
+
270
+ it('loads personality from personalityDir config', () => {
271
+ const result = loadPersonality({ personalityDir: tempDir });
272
+ assert.ok(result);
273
+ assert.ok(result.includes('Who I Am'));
274
+ });
275
+
276
+ it('loads personality via auto-detect in dev mode or returns null', () => {
277
+ // When personalityDir is invalid, loadPersonality falls through to auto-detect.
278
+ // In the real repo, auto-detect finds ../template-v2/ so we just verify
279
+ // it returns either a valid personality or null (never crashes).
280
+ const result = loadPersonality({ personalityDir: '/nonexistent/path' });
281
+ if (result) {
282
+ // Auto-detect found the real template-v2/ (dev mode)
283
+ assert.ok(result.includes('Who I Am'));
284
+ } else {
285
+ assert.equal(result, null);
286
+ }
287
+ });
288
+
289
+ it('caches result on second call', () => {
290
+ const config = { personalityDir: tempDir };
291
+ const first = loadPersonality(config);
292
+ const second = loadPersonality(config);
293
+ // Exact same reference (cached)
294
+ assert.equal(first, second);
295
+ });
296
+
297
+ it('cache clears with clearCache()', () => {
298
+ const config = { personalityDir: tempDir };
299
+ loadPersonality(config);
300
+ clearCache();
301
+ // After clearing, should re-load (still same content)
302
+ const result = loadPersonality(config);
303
+ assert.ok(result);
304
+ assert.ok(result.includes('Who I Am'));
305
+ });
306
+ });
307
+
308
+ describe('resolveTemplateDir', () => {
309
+ it('uses personalityDir when set and valid', () => {
310
+ const dir = join(tmpdir(), `resolve-valid-${Date.now()}-${Math.random().toString(36).slice(2)}`);
311
+ mkdirSync(dir, { recursive: true });
312
+ writeFileSync(join(dir, 'CLAUDE.md'), '# Test');
313
+ const result = resolveTemplateDir({ personalityDir: dir });
314
+ assert.equal(result, dir);
315
+ });
316
+
317
+ it('skips personalityDir when it has no CLAUDE.md', () => {
318
+ const dir = join(tmpdir(), `resolve-empty-${Date.now()}-${Math.random().toString(36).slice(2)}`);
319
+ mkdirSync(dir, { recursive: true });
320
+ const result = resolveTemplateDir({ personalityDir: dir });
321
+ // Should NOT return the dir (no CLAUDE.md there).
322
+ // May return auto-detected template-v2/ in dev mode, or null.
323
+ assert.notEqual(result, dir);
324
+ });
325
+ });
@@ -763,6 +763,22 @@ class Database:
763
763
  conn.commit()
764
764
  logger.info("Applied migration 15: relationship origin_type for organic trust model")
765
765
 
766
+ if current_version < 16:
767
+ # Migration 16: Add source_channel to memories for channel-aware memory
768
+ try:
769
+ conn.execute(
770
+ "ALTER TABLE memories ADD COLUMN source_channel TEXT DEFAULT 'claude_code'"
771
+ )
772
+ except sqlite3.OperationalError as e:
773
+ if "duplicate column" not in str(e).lower():
774
+ logger.warning(f"Migration 16 statement failed: {e}")
775
+
776
+ conn.execute(
777
+ "INSERT OR IGNORE INTO schema_migrations (version, description) VALUES (16, 'Add source_channel to memories for channel-aware memory')"
778
+ )
779
+ conn.commit()
780
+ logger.info("Applied migration 16: source_channel on memories")
781
+
766
782
  # FTS5 setup: ensure memories_fts exists regardless of migration path.
767
783
  # The FTS5 virtual table + triggers contain internal semicolons that the
768
784
  # schema.sql line-based parser can't handle, so we always check here.
@@ -896,6 +912,11 @@ class Database:
896
912
  logger.warning("Migration 15 incomplete: relationships missing origin_type column")
897
913
  return 14
898
914
 
915
+ # Migration 16 added source_channel to memories
916
+ if "source_channel" not in memory_cols:
917
+ logger.warning("Migration 16 incomplete: memories missing source_channel column")
918
+ return 15
919
+
899
920
  return None # All good
900
921
 
901
922
  def _store_workspace_path(self, conn: sqlite3.Connection) -> None:
@@ -140,6 +140,10 @@ async def list_tools() -> ListToolsResult:
140
140
  "type": "string",
141
141
  "description": "Full raw text of the source (email body, transcript, etc.). Saved to disk, not stored in DB.",
142
142
  },
143
+ "source_channel": {
144
+ "type": "string",
145
+ "description": "Origin channel: claude_code, telegram, slack",
146
+ },
143
147
  },
144
148
  "required": ["content"],
145
149
  },
@@ -638,6 +642,10 @@ async def list_tools() -> ListToolsResult:
638
642
  "type": "string",
639
643
  "description": "Full raw source text, saved to disk (for 'remember' op)",
640
644
  },
645
+ "source_channel": {
646
+ "type": "string",
647
+ "description": "Origin channel: claude_code, telegram, slack (for 'remember' op)",
648
+ },
641
649
  "target": {
642
650
  "type": "string",
643
651
  "description": "Target entity (for 'relate' op)",
@@ -1175,6 +1183,7 @@ async def call_tool(name: str, arguments: Dict[str, Any]) -> CallToolResult:
1175
1183
  importance=arguments.get("importance", 1.0),
1176
1184
  source=arguments.get("source"),
1177
1185
  source_context=arguments.get("source_context"),
1186
+ source_channel=arguments.get("source_channel"),
1178
1187
  )
1179
1188
  # Save source material to disk if provided
1180
1189
  if memory_id and arguments.get("source_material"):
@@ -1220,6 +1229,7 @@ async def call_tool(name: str, arguments: Dict[str, Any]) -> CallToolResult:
1220
1229
  "source": r.source,
1221
1230
  "source_id": r.source_id,
1222
1231
  "source_context": r.source_context,
1232
+ "source_channel": r.source_channel,
1223
1233
  }
1224
1234
  for r in results
1225
1235
  ]
@@ -1291,6 +1301,7 @@ async def call_tool(name: str, arguments: Dict[str, Any]) -> CallToolResult:
1291
1301
  "source": r.source,
1292
1302
  "source_id": r.source_id,
1293
1303
  "source_context": r.source_context,
1304
+ "source_channel": r.source_channel,
1294
1305
  }
1295
1306
  for r in results
1296
1307
  ]
@@ -1644,6 +1655,7 @@ async def call_tool(name: str, arguments: Dict[str, Any]) -> CallToolResult:
1644
1655
  importance=op.get("importance", 1.0),
1645
1656
  source=op.get("source"),
1646
1657
  source_context=op.get("source_context"),
1658
+ source_channel=op.get("source_channel"),
1647
1659
  _precomputed_embedding=embeddings_map.get(i),
1648
1660
  )
1649
1661
  op_result["success"] = True
@@ -60,7 +60,8 @@ CREATE TABLE IF NOT EXISTS memories (
60
60
  access_count INTEGER DEFAULT 0,
61
61
  verified_at TEXT, -- When this memory was verified
62
62
  verification_status TEXT DEFAULT 'pending', -- pending, verified, flagged, contradicts
63
- metadata TEXT -- JSON blob for flexible attributes
63
+ metadata TEXT, -- JSON blob for flexible attributes
64
+ source_channel TEXT DEFAULT 'claude_code' -- Origin channel: claude_code, telegram, slack
64
65
  );
65
66
 
66
67
  CREATE INDEX IF NOT EXISTS idx_memories_type ON memories(type);
@@ -412,6 +413,9 @@ VALUES (14, 'Add dispatch_tier to agent_dispatches for native agent team support
412
413
  INSERT OR IGNORE INTO schema_migrations (version, description)
413
414
  VALUES (15, 'Add origin_type to relationships for organic trust model');
414
415
 
416
+ INSERT OR IGNORE INTO schema_migrations (version, description)
417
+ VALUES (16, 'Add source_channel to memories for channel-aware memory');
418
+
415
419
  -- ============================================================================
416
420
  -- AGENT DISPATCHES: Track delegated tasks to sub-agents
417
421
  -- ============================================================================
@@ -41,6 +41,8 @@ class RecallResult:
41
41
  confidence: float = 1.0 # How confident we are in this memory
42
42
  verification_status: str = "pending" # pending, verified, flagged, contradicts
43
43
  origin_type: str = "inferred" # user_stated, extracted, inferred, corrected
44
+ # Channel tracking
45
+ source_channel: Optional[str] = None # Origin channel: claude_code, telegram, slack
44
46
 
45
47
 
46
48
  @dataclass
@@ -339,6 +341,9 @@ class RecallService:
339
341
  verification_status_val = row["verification_status"] if "verification_status" in row_keys else "pending"
340
342
  origin_type_val = row["origin_type"] if "origin_type" in row_keys else "inferred"
341
343
 
344
+ # Channel tracking (may not exist in older DBs)
345
+ source_channel_val = row["source_channel"] if "source_channel" in row_keys else None
346
+
342
347
  return RecallResult(
343
348
  id=row["id"],
344
349
  content=row["content"],
@@ -354,6 +359,7 @@ class RecallService:
354
359
  confidence=confidence_val,
355
360
  verification_status=verification_status_val,
356
361
  origin_type=origin_type_val,
362
+ source_channel=source_channel_val,
357
363
  )
358
364
 
359
365
  def _rrf_score(
@@ -141,6 +141,7 @@ class RememberService:
141
141
  source_context: Optional[str] = None,
142
142
  metadata: Optional[Dict] = None,
143
143
  origin_type: Optional[str] = None,
144
+ source_channel: Optional[str] = None,
144
145
  _precomputed_embedding: Optional[List[float]] = None,
145
146
  ) -> Optional[int]:
146
147
  """
@@ -157,6 +158,7 @@ class RememberService:
157
158
  source_context: One-line breadcrumb describing the source material
158
159
  metadata: Additional metadata
159
160
  origin_type: 'user_stated', 'extracted', 'inferred', 'corrected' (Trust North Star)
161
+ source_channel: Origin channel: claude_code, telegram, slack
160
162
 
161
163
  Returns:
162
164
  Memory ID or None if duplicate
@@ -215,6 +217,8 @@ class RememberService:
215
217
  }
216
218
  if source_context:
217
219
  insert_data["source_context"] = source_context
220
+ if source_channel:
221
+ insert_data["source_channel"] = source_channel
218
222
 
219
223
  memory_id = self.db.insert("memories", insert_data)
220
224
 
@@ -102,6 +102,41 @@ def test_update():
102
102
  assert entity["description"] == "Updated description"
103
103
 
104
104
 
105
+ def test_migration_16_source_channel():
106
+ """Migration 16 adds source_channel to memories with default 'claude_code'."""
107
+ with tempfile.TemporaryDirectory() as tmpdir:
108
+ db_path = Path(tmpdir) / "test.db"
109
+ db = Database(db_path)
110
+ db.initialize()
111
+
112
+ # Verify column exists
113
+ cols = db._get_table_columns(db._get_connection(), "memories")
114
+ assert "source_channel" in cols, "source_channel column should exist after migration 16"
115
+
116
+ # Insert a memory without specifying source_channel
117
+ memory_id = db.insert("memories", {
118
+ "content": "test memory",
119
+ "content_hash": "abc123",
120
+ "type": "fact",
121
+ })
122
+
123
+ # Default should be 'claude_code'
124
+ row = db.get_one("memories", where="id = ?", where_params=(memory_id,))
125
+ assert row["source_channel"] == "claude_code"
126
+
127
+ # Insert with explicit source_channel
128
+ memory_id2 = db.insert("memories", {
129
+ "content": "telegram memory",
130
+ "content_hash": "def456",
131
+ "type": "fact",
132
+ "source_channel": "telegram",
133
+ })
134
+ row2 = db.get_one("memories", where="id = ?", where_params=(memory_id2,))
135
+ assert row2["source_channel"] == "telegram"
136
+
137
+ db.close()
138
+
139
+
105
140
  def test_migration_integrity_detects_missing_verification_status():
106
141
  """Migration 5 added verification_status. Integrity check should catch if it is missing."""
107
142
  with tempfile.TemporaryDirectory() as tmpdir:
@@ -0,0 +1,68 @@
1
+ """Tests for source_channel feature (channel-aware memory)."""
2
+
3
+ import tempfile
4
+ from pathlib import Path
5
+ from unittest.mock import patch
6
+
7
+ import pytest
8
+
9
+ from claudia_memory.database import Database
10
+
11
+
12
+ @pytest.fixture
13
+ def db():
14
+ """Create a temporary test database."""
15
+ with tempfile.TemporaryDirectory() as tmpdir:
16
+ db_path = Path(tmpdir) / "test.db"
17
+ database = Database(db_path)
18
+ database.initialize()
19
+ yield database
20
+ database.close()
21
+
22
+
23
+ def test_source_channel_in_recall_result(db):
24
+ """source_channel should appear in recall results."""
25
+ from claudia_memory.services.recall import RecallResult
26
+
27
+ # Verify the dataclass has source_channel field
28
+ r = RecallResult(
29
+ id=1, content="test", type="fact", score=1.0,
30
+ importance=1.0, created_at="2026-01-01", entities=[],
31
+ source_channel="telegram",
32
+ )
33
+ assert r.source_channel == "telegram"
34
+
35
+ # Default should be None
36
+ r2 = RecallResult(
37
+ id=2, content="test2", type="fact", score=1.0,
38
+ importance=1.0, created_at="2026-01-01", entities=[],
39
+ )
40
+ assert r2.source_channel is None
41
+
42
+
43
+ def test_remember_fact_stores_source_channel(db):
44
+ """remember_fact should store source_channel when provided."""
45
+ with patch("claudia_memory.services.remember.embed_sync", return_value=None):
46
+ with patch("claudia_memory.services.remember.get_db", return_value=db):
47
+ with patch("claudia_memory.services.remember.get_embedding_service"):
48
+ with patch("claudia_memory.services.remember.get_extractor"):
49
+ from claudia_memory.services.remember import RememberService
50
+ # Reset global service to pick up our mocked db
51
+ import claudia_memory.services.remember as rem_mod
52
+ old_svc = rem_mod._service
53
+ rem_mod._service = None
54
+
55
+ try:
56
+ svc = RememberService()
57
+ svc.db = db
58
+
59
+ memory_id = svc.remember_fact(
60
+ content="Test from telegram",
61
+ source_channel="telegram",
62
+ )
63
+ assert memory_id is not None
64
+
65
+ row = db.get_one("memories", where="id = ?", where_params=(memory_id,))
66
+ assert row["source_channel"] == "telegram"
67
+ finally:
68
+ rem_mod._service = old_svc
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "get-claudia",
3
- "version": "1.30.0",
3
+ "version": "1.32.0",
4
4
  "description": "An AI assistant who learns how you work.",
5
5
  "keywords": [
6
6
  "claudia",
@@ -33,6 +33,7 @@
33
33
  "template-v2",
34
34
  "memory-daemon",
35
35
  "gateway",
36
+ "relay",
36
37
  "visualizer",
37
38
  "visualizer-threejs",
38
39
  "assets"