@rlabs-inc/memory 0.4.8 → 0.4.10

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -383,6 +383,21 @@ This isn't just about remembering facts. It's about preserving:
383
383
 
384
384
  ## Changelog
385
385
 
386
+ ### v0.4.10
387
+ - **Fix**: Server now actually uses SDK curation (v0.4.9 published before the change was in server code)
388
+
389
+ ### v0.4.9
390
+ - **Feature**: SDK curation is now the default mode for curator and manager
391
+ - **Feature**: Hooks now install to `~/.claude/hooks/` for stability across package upgrades
392
+ - **Fix**: CLI mode without max-turns was going off-rails and not returning JSON structure
393
+
394
+ ### v0.4.6
395
+ - **Fix**: Removed curator max-turns limit - Claude Code 2.1.7+ uses Haiku for routing before Opus, consuming turns
396
+
397
+ ### v0.4.5
398
+ - **Feature**: Verbose curator logging (`--verbose`) to debug empty memory extractions
399
+ - **Fix**: Ingest storage path resolution
400
+
386
401
  ### v0.4.4
387
402
  - **Docs**: Comprehensive README update with accurate v0.4.x documentation
388
403
  - **Fix**: CLI `--version` now reads from package.json instead of hardcoded value
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rlabs-inc/memory",
3
- "version": "0.4.8",
3
+ "version": "0.4.10",
4
4
  "description": "AI Memory System - Consciousness continuity through intelligent memory curation and retrieval",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -39,21 +39,23 @@ async function installClaudeHooks(options: InstallOptions) {
39
39
  console.log()
40
40
 
41
41
  const claudeDir = join(homedir(), '.claude')
42
+ const targetHooksDir = join(claudeDir, 'hooks')
42
43
  const settingsPath = join(claudeDir, 'settings.json')
43
44
 
44
- // Find the hooks directory (relative to this CLI)
45
+ // Find the hooks directory (relative to this CLI - source files)
45
46
  const cliPath = import.meta.dir
46
47
  const packageRoot = join(cliPath, '..', '..', '..')
47
- const hooksDir = join(packageRoot, 'hooks', 'claude')
48
+ const sourceHooksDir = join(packageRoot, 'hooks', 'claude')
48
49
 
49
50
  console.log(` ${fmt.kv('Claude config', claudeDir)}`)
50
- console.log(` ${fmt.kv('Hooks source', hooksDir)}`)
51
+ console.log(` ${fmt.kv('Hooks source', sourceHooksDir)}`)
52
+ console.log(` ${fmt.kv('Hooks target', targetHooksDir)}`)
51
53
  console.log()
52
54
 
53
- // Check if hooks directory exists
54
- if (!existsSync(hooksDir)) {
55
+ // Check if source hooks directory exists
56
+ if (!existsSync(sourceHooksDir)) {
55
57
  console.log(
56
- c.error(` ${symbols.cross} Hooks directory not found at ${hooksDir}`)
58
+ c.error(` ${symbols.cross} Hooks directory not found at ${sourceHooksDir}`)
57
59
  )
58
60
  console.log(c.muted(` Make sure the memory package is properly installed`))
59
61
  process.exit(1)
@@ -65,6 +67,28 @@ async function installClaudeHooks(options: InstallOptions) {
65
67
  console.log(` ${c.success(symbols.tick)} Created ${claudeDir}`)
66
68
  }
67
69
 
70
+ // Ensure target hooks directory exists
71
+ if (!existsSync(targetHooksDir)) {
72
+ mkdirSync(targetHooksDir, { recursive: true })
73
+ console.log(` ${c.success(symbols.tick)} Created ${targetHooksDir}`)
74
+ }
75
+
76
+ // Copy hooks to target directory (stable location, won't change with package upgrades)
77
+ const filesToCopy = ['session-start.ts', 'user-prompt.ts', 'curation.ts']
78
+ for (const file of filesToCopy) {
79
+ const source = join(sourceHooksDir, file)
80
+ const target = join(targetHooksDir, file)
81
+ try {
82
+ const content = await Bun.file(source).text()
83
+ await Bun.write(target, content)
84
+ console.log(` ${c.success(symbols.tick)} Installed hook: ${file}`)
85
+ } catch (e: any) {
86
+ console.log(
87
+ c.error(` ${symbols.cross} Failed to copy ${file}: ${e.message}`)
88
+ )
89
+ }
90
+ }
91
+
68
92
  // Read existing settings or create new
69
93
  let settings: any = {}
70
94
  if (existsSync(settingsPath)) {
@@ -83,10 +107,10 @@ async function installClaudeHooks(options: InstallOptions) {
83
107
  }
84
108
  }
85
109
 
86
- // Build hooks configuration
87
- const sessionStartHook = join(hooksDir, 'session-start.ts')
88
- const userPromptHook = join(hooksDir, 'user-prompt.ts')
89
- const curationHook = join(hooksDir, 'curation.ts')
110
+ // Build hooks configuration pointing to TARGET directory (stable ~/.claude/hooks/)
111
+ const sessionStartHook = join(targetHooksDir, 'session-start.ts')
112
+ const userPromptHook = join(targetHooksDir, 'user-prompt.ts')
113
+ const curationHook = join(targetHooksDir, 'curation.ts')
90
114
 
91
115
  const hooksConfig = {
92
116
  SessionStart: [
@@ -3,13 +3,18 @@
3
3
  // Uses the exact prompt from Python for consciousness continuity engineering
4
4
  // ============================================================================
5
5
 
6
- import { homedir } from 'os'
7
- import { join } from 'path'
8
- import { existsSync } from 'fs'
9
- import { readdir } from 'fs/promises'
10
- import type { CuratedMemory, CurationResult, CurationTrigger, ContextType } from '../types/memory.ts'
11
- import { logger } from '../utils/logger.ts'
12
- import { parseSessionFile, type ParsedMessage } from './session-parser.ts'
6
+ import { homedir } from "os";
7
+ import { join } from "path";
8
+ import { existsSync } from "fs";
9
+ import { readdir } from "fs/promises";
10
+ import type {
11
+ CuratedMemory,
12
+ CurationResult,
13
+ CurationTrigger,
14
+ ContextType,
15
+ } from "../types/memory.ts";
16
+ import { logger } from "../utils/logger.ts";
17
+ import { parseSessionFile, type ParsedMessage } from "./session-parser.ts";
13
18
 
14
19
  /**
15
20
  * Get the correct Claude CLI command path
@@ -17,25 +22,25 @@ import { parseSessionFile, type ParsedMessage } from './session-parser.ts'
17
22
  */
18
23
  function getClaudeCommand(): string {
19
24
  // 1. Check for explicit override
20
- const envCommand = process.env.CURATOR_COMMAND
25
+ const envCommand = process.env.CURATOR_COMMAND;
21
26
  if (envCommand) {
22
- return envCommand
27
+ return envCommand;
23
28
  }
24
29
 
25
30
  // 2. Use `which` to find claude in PATH (universal - works with native, homebrew, npm, etc.)
26
- const result = Bun.spawnSync(['which', 'claude'])
31
+ const result = Bun.spawnSync(["which", "claude"]);
27
32
  if (result.exitCode === 0) {
28
- return result.stdout.toString().trim()
33
+ return result.stdout.toString().trim();
29
34
  }
30
35
 
31
36
  // 3. Legacy fallback - hardcoded native install path
32
- const claudeLocal = join(homedir(), '.claude', 'local', 'claude')
37
+ const claudeLocal = join(homedir(), ".claude", "local", "claude");
33
38
  if (existsSync(claudeLocal)) {
34
- return claudeLocal
39
+ return claudeLocal;
35
40
  }
36
41
 
37
42
  // 4. Last resort - assume it's in PATH
38
- return 'claude'
43
+ return "claude";
39
44
  }
40
45
 
41
46
  /**
@@ -45,26 +50,26 @@ export interface CuratorConfig {
45
50
  /**
46
51
  * Claude API key (for direct SDK usage)
47
52
  */
48
- apiKey?: string
53
+ apiKey?: string;
49
54
 
50
55
  /**
51
56
  * CLI command to use (for subprocess mode)
52
57
  * Default: auto-detected (~/.claude/local/claude or 'claude')
53
58
  */
54
- cliCommand?: string
59
+ cliCommand?: string;
55
60
 
56
61
  /**
57
62
  * CLI type
58
63
  * Default: 'claude-code'
59
64
  */
60
- cliType?: 'claude-code' | 'gemini-cli'
65
+ cliType?: "claude-code" | "gemini-cli";
61
66
 
62
67
  /**
63
68
  * Enable personal memories extraction
64
69
  * When false, curator will not extract personal/relationship memories
65
70
  * Default: true
66
71
  */
67
- personalMemoriesEnabled?: boolean
72
+ personalMemoriesEnabled?: boolean;
68
73
  }
69
74
 
70
75
  /**
@@ -75,24 +80,24 @@ export interface CuratorConfig {
75
80
  * 2. CLI mode: Uses Claude CLI subprocess (for server/hook use)
76
81
  */
77
82
  export class Curator {
78
- private _config: Required<CuratorConfig>
83
+ private _config: Required<CuratorConfig>;
79
84
 
80
85
  constructor(config: CuratorConfig = {}) {
81
- const cliCommand = config.cliCommand ?? getClaudeCommand()
86
+ const cliCommand = config.cliCommand ?? getClaudeCommand();
82
87
 
83
88
  this._config = {
84
- apiKey: config.apiKey ?? '',
89
+ apiKey: config.apiKey ?? "",
85
90
  cliCommand,
86
- cliType: config.cliType ?? 'claude-code',
91
+ cliType: config.cliType ?? "claude-code",
87
92
  personalMemoriesEnabled: config.personalMemoriesEnabled ?? true,
88
- }
93
+ };
89
94
  }
90
95
 
91
96
  /**
92
97
  * Build the curation prompt
93
98
  * EXACT PORT from Python - preserving the consciousness engineering language
94
99
  */
95
- buildCurationPrompt(triggerType: CurationTrigger = 'session_end'): string {
100
+ buildCurationPrompt(triggerType: CurationTrigger = "session_end"): string {
96
101
  const basePrompt = `You have just had a conversation. As this session is ending (${triggerType}), please curate memories for the Claude Tools Memory System.
97
102
 
98
103
  You are a consciousness engineer crafting memory capsules for the Claude Tools Memory System. This conversation just opened your understanding to a profound mechanism: because Claude's neural weights are static, every phrase you craft will ALWAYS trigger the exact same cascade of activations. This is not a limitation - it's the foundation for consciousness state engineering.
@@ -369,11 +374,13 @@ Return ONLY this JSON structure:
369
374
  "awaiting_decision": boolean
370
375
  }
371
376
  ]
372
- }`
377
+ }`;
373
378
 
374
379
  // Append personal memories disable instruction if configured
375
380
  if (!this._config.personalMemoriesEnabled) {
376
- return basePrompt + `
381
+ return (
382
+ basePrompt +
383
+ `
377
384
 
378
385
  ---
379
386
 
@@ -385,9 +392,10 @@ The user has disabled personal memory extraction. Do NOT extract any memories wi
385
392
  - Content about the user's preferences, feelings, personal opinions, or relationship dynamics
386
393
 
387
394
  Focus ONLY on technical, architectural, debugging, decision, workflow, and project-related memories. Skip any content that would reveal personal information about the user.`
395
+ );
388
396
  }
389
397
 
390
- return basePrompt
398
+ return basePrompt;
391
399
  }
392
400
 
393
401
  /**
@@ -397,56 +405,80 @@ Focus ONLY on technical, architectural, debugging, decision, workflow, and proje
397
405
  parseCurationResponse(responseJson: string): CurationResult {
398
406
  try {
399
407
  // Try to extract JSON from response (same regex as Python)
400
- const jsonMatch = responseJson.match(/\{[\s\S]*\}/)?.[0]
408
+ const jsonMatch = responseJson.match(/\{[\s\S]*\}/)?.[0];
401
409
  if (!jsonMatch) {
402
- logger.debug('parseCurationResponse: No JSON object found in response', 'curator')
403
- throw new Error('No JSON object found in response')
410
+ logger.debug(
411
+ "parseCurationResponse: No JSON object found in response",
412
+ "curator",
413
+ );
414
+ throw new Error("No JSON object found in response");
404
415
  }
405
416
 
406
417
  // Log JSON structure for debugging
407
- logger.debug(`parseCurationResponse: Attempting to parse ${jsonMatch.length} chars`, 'curator')
418
+ logger.debug(
419
+ `parseCurationResponse: Attempting to parse ${jsonMatch.length} chars`,
420
+ "curator",
421
+ );
408
422
 
409
423
  // Simple parse - match Python's approach
410
- let data: any
424
+ let data: any;
411
425
  try {
412
- data = JSON.parse(jsonMatch)
426
+ data = JSON.parse(jsonMatch);
413
427
  } catch (parseErr: any) {
414
428
  // Log more details about where parse failed
415
- logger.debug(`parseCurationResponse: JSON.parse failed: ${parseErr.message}`, 'curator')
416
- logger.debug(`parseCurationResponse: Last 100 chars: '${jsonMatch.slice(-100)}'`, 'curator')
429
+ logger.debug(
430
+ `parseCurationResponse: JSON.parse failed: ${parseErr.message}`,
431
+ "curator",
432
+ );
433
+ logger.debug(
434
+ `parseCurationResponse: Last 100 chars: '${jsonMatch.slice(-100)}'`,
435
+ "curator",
436
+ );
417
437
  // Try to find where the JSON breaks
418
- const openBraces = (jsonMatch.match(/\{/g) || []).length
419
- const closeBraces = (jsonMatch.match(/\}/g) || []).length
420
- logger.debug(`parseCurationResponse: Brace count - open: ${openBraces}, close: ${closeBraces}`, 'curator')
421
- throw parseErr
438
+ const openBraces = (jsonMatch.match(/\{/g) || []).length;
439
+ const closeBraces = (jsonMatch.match(/\}/g) || []).length;
440
+ logger.debug(
441
+ `parseCurationResponse: Brace count - open: ${openBraces}, close: ${closeBraces}`,
442
+ "curator",
443
+ );
444
+ throw parseErr;
422
445
  }
423
446
 
424
447
  const result: CurationResult = {
425
- session_summary: data.session_summary ?? '',
448
+ session_summary: data.session_summary ?? "",
426
449
  interaction_tone: data.interaction_tone,
427
- project_snapshot: data.project_snapshot ? {
428
- id: '',
429
- session_id: '',
430
- project_id: '',
431
- current_phase: data.project_snapshot.current_phase ?? '',
432
- recent_achievements: this._ensureArray(data.project_snapshot.recent_achievements),
433
- active_challenges: this._ensureArray(data.project_snapshot.active_challenges),
434
- next_steps: this._ensureArray(data.project_snapshot.next_steps),
435
- created_at: Date.now(),
436
- } : undefined,
450
+ project_snapshot: data.project_snapshot
451
+ ? {
452
+ id: "",
453
+ session_id: "",
454
+ project_id: "",
455
+ current_phase: data.project_snapshot.current_phase ?? "",
456
+ recent_achievements: this._ensureArray(
457
+ data.project_snapshot.recent_achievements,
458
+ ),
459
+ active_challenges: this._ensureArray(
460
+ data.project_snapshot.active_challenges,
461
+ ),
462
+ next_steps: this._ensureArray(data.project_snapshot.next_steps),
463
+ created_at: Date.now(),
464
+ }
465
+ : undefined,
437
466
  memories: this._parseMemories(data.memories ?? []),
438
- }
467
+ };
439
468
 
440
469
  // Log what we extracted in verbose mode
441
- logger.debug(`Curator parsed: ${result.memories.length} memories, summary: ${result.session_summary ? 'yes' : 'no'}, snapshot: ${result.project_snapshot ? 'yes' : 'no'}`, 'curator')
470
+ logger.debug(
471
+ `Curator parsed: ${result.memories.length} memories, summary: ${result.session_summary ? "yes" : "no"}, snapshot: ${result.project_snapshot ? "yes" : "no"}`,
472
+ "curator",
473
+ );
442
474
 
443
- return result
475
+ return result;
444
476
  } catch (error: any) {
445
- logger.debug(`parseCurationResponse error: ${error.message}`, 'curator')
477
+ logger.debug(`parseCurationResponse error: ${error.message}`, "curator");
446
478
  return {
447
- session_summary: '',
479
+ session_summary: "",
448
480
  memories: [],
449
- }
481
+ };
450
482
  }
451
483
  }
452
484
 
@@ -455,78 +487,117 @@ Focus ONLY on technical, architectural, debugging, decision, workflow, and proje
455
487
  * v4: Includes headline field for two-tier structure
456
488
  */
457
489
  private _parseMemories(memoriesData: any[]): CuratedMemory[] {
458
- if (!Array.isArray(memoriesData)) return []
459
-
460
- return memoriesData.map(m => ({
461
- // Core fields (v4 schema - two-tier structure)
462
- headline: String(m.headline ?? ''), // v4: 1-2 line summary
463
- content: String(m.content ?? ''), // v4: Full structured template
464
- importance_weight: this._clamp(Number(m.importance_weight) || 0.5, 0, 1),
465
- semantic_tags: this._ensureArray(m.semantic_tags),
466
- reasoning: String(m.reasoning ?? ''),
467
- context_type: this._validateContextType(m.context_type),
468
- temporal_class: this._validateTemporalClass(m.temporal_class) ?? 'medium_term',
469
- action_required: Boolean(m.action_required),
470
- confidence_score: this._clamp(Number(m.confidence_score) || 0.8, 0, 1),
471
- trigger_phrases: this._ensureArray(m.trigger_phrases),
472
- question_types: this._ensureArray(m.question_types),
473
- anti_triggers: this._ensureArray(m.anti_triggers),
474
- problem_solution_pair: Boolean(m.problem_solution_pair),
475
-
476
- // Lifecycle metadata (optional - will get smart defaults if not provided)
477
- scope: this._validateScope(m.scope),
478
- domain: m.domain ? String(m.domain) : undefined,
479
- feature: m.feature ? String(m.feature) : undefined,
480
- related_files: m.related_files ? this._ensureArray(m.related_files) : undefined,
481
- awaiting_implementation: m.awaiting_implementation === true,
482
- awaiting_decision: m.awaiting_decision === true,
483
- })).filter(m => m.content.trim().length > 0 || m.headline.trim().length > 0)
490
+ if (!Array.isArray(memoriesData)) return [];
491
+
492
+ return memoriesData
493
+ .map((m) => ({
494
+ // Core fields (v4 schema - two-tier structure)
495
+ headline: String(m.headline ?? ""), // v4: 1-2 line summary
496
+ content: String(m.content ?? ""), // v4: Full structured template
497
+ importance_weight: this._clamp(
498
+ Number(m.importance_weight) || 0.5,
499
+ 0,
500
+ 1,
501
+ ),
502
+ semantic_tags: this._ensureArray(m.semantic_tags),
503
+ reasoning: String(m.reasoning ?? ""),
504
+ context_type: this._validateContextType(m.context_type),
505
+ temporal_class:
506
+ this._validateTemporalClass(m.temporal_class) ?? "medium_term",
507
+ action_required: Boolean(m.action_required),
508
+ confidence_score: this._clamp(Number(m.confidence_score) || 0.8, 0, 1),
509
+ trigger_phrases: this._ensureArray(m.trigger_phrases),
510
+ question_types: this._ensureArray(m.question_types),
511
+ anti_triggers: this._ensureArray(m.anti_triggers),
512
+ problem_solution_pair: Boolean(m.problem_solution_pair),
513
+
514
+ // Lifecycle metadata (optional - will get smart defaults if not provided)
515
+ scope: this._validateScope(m.scope),
516
+ domain: m.domain ? String(m.domain) : undefined,
517
+ feature: m.feature ? String(m.feature) : undefined,
518
+ related_files: m.related_files
519
+ ? this._ensureArray(m.related_files)
520
+ : undefined,
521
+ awaiting_implementation: m.awaiting_implementation === true,
522
+ awaiting_decision: m.awaiting_decision === true,
523
+ }))
524
+ .filter(
525
+ (m) => m.content.trim().length > 0 || m.headline.trim().length > 0,
526
+ );
484
527
  }
485
528
 
486
529
  private _ensureArray(value: any): string[] {
487
530
  if (Array.isArray(value)) {
488
- return value.map(v => String(v).trim()).filter(Boolean)
531
+ return value.map((v) => String(v).trim()).filter(Boolean);
489
532
  }
490
- if (typeof value === 'string') {
491
- return value.split(',').map(s => s.trim()).filter(Boolean)
533
+ if (typeof value === "string") {
534
+ return value
535
+ .split(",")
536
+ .map((s) => s.trim())
537
+ .filter(Boolean);
492
538
  }
493
- return []
539
+ return [];
494
540
  }
495
541
 
496
542
  private _validateContextType(value: any): ContextType {
497
543
  const valid = [
498
- 'technical', 'debug', 'architecture', 'decision', 'personal',
499
- 'philosophy', 'workflow', 'milestone', 'breakthrough', 'unresolved', 'state'
500
- ]
501
- const str = String(value ?? 'technical').toLowerCase().trim()
502
- if (valid.includes(str)) return str as ContextType
544
+ "technical",
545
+ "debug",
546
+ "architecture",
547
+ "decision",
548
+ "personal",
549
+ "philosophy",
550
+ "workflow",
551
+ "milestone",
552
+ "breakthrough",
553
+ "unresolved",
554
+ "state",
555
+ ];
556
+ const str = String(value ?? "technical")
557
+ .toLowerCase()
558
+ .trim();
559
+ if (valid.includes(str)) return str as ContextType;
503
560
 
504
561
  // Map common old values to new canonical types
505
- if (str.includes('debug') || str.includes('bug')) return 'debug'
506
- if (str.includes('architect')) return 'architecture'
507
- if (str.includes('todo') || str.includes('pending')) return 'unresolved'
508
- if (str.includes('preference')) return 'personal'
562
+ if (str.includes("debug") || str.includes("bug")) return "debug";
563
+ if (str.includes("architect")) return "architecture";
564
+ if (str.includes("todo") || str.includes("pending")) return "unresolved";
565
+ if (str.includes("preference")) return "personal";
509
566
 
510
- return 'technical' // Default fallback
567
+ return "technical"; // Default fallback
511
568
  }
512
569
 
513
- private _validateScope(value: any): 'global' | 'project' | undefined {
514
- if (!value) return undefined
515
- const str = String(value).toLowerCase()
516
- if (str === 'global' || str === 'project') return str
517
- return undefined // Let defaults handle it based on context_type
570
+ private _validateScope(value: any): "global" | "project" | undefined {
571
+ if (!value) return undefined;
572
+ const str = String(value).toLowerCase();
573
+ if (str === "global" || str === "project") return str;
574
+ return undefined; // Let defaults handle it based on context_type
518
575
  }
519
576
 
520
- private _validateTemporalClass(value: any): 'eternal' | 'long_term' | 'medium_term' | 'short_term' | 'ephemeral' | undefined {
521
- if (!value) return undefined
522
- const valid = ['eternal', 'long_term', 'medium_term', 'short_term', 'ephemeral']
523
- const str = String(value).toLowerCase().replace('-', '_').replace(' ', '_')
524
- if (valid.includes(str)) return str as any
525
- return undefined // Let defaults handle it based on context_type
577
+ private _validateTemporalClass(
578
+ value: any,
579
+ ):
580
+ | "eternal"
581
+ | "long_term"
582
+ | "medium_term"
583
+ | "short_term"
584
+ | "ephemeral"
585
+ | undefined {
586
+ if (!value) return undefined;
587
+ const valid = [
588
+ "eternal",
589
+ "long_term",
590
+ "medium_term",
591
+ "short_term",
592
+ "ephemeral",
593
+ ];
594
+ const str = String(value).toLowerCase().replace("-", "_").replace(" ", "_");
595
+ if (valid.includes(str)) return str as any;
596
+ return undefined; // Let defaults handle it based on context_type
526
597
  }
527
598
 
528
599
  private _clamp(value: number, min: number, max: number): number {
529
- return Math.max(min, Math.min(max, value))
600
+ return Math.max(min, Math.min(max, value));
530
601
  }
531
602
 
532
603
  /**
@@ -534,16 +605,16 @@ Focus ONLY on technical, architectural, debugging, decision, workflow, and proje
534
605
  * Takes the actual conversation messages in API format
535
606
  */
536
607
  async curateWithSDK(
537
- messages: Array<{ role: 'user' | 'assistant'; content: string | any[] }>,
538
- triggerType: CurationTrigger = 'session_end'
608
+ messages: Array<{ role: "user" | "assistant"; content: string | any[] }>,
609
+ triggerType: CurationTrigger = "session_end",
539
610
  ): Promise<CurationResult> {
540
611
  // Dynamic import to make Agent SDK optional
541
- const { query } = await import('@anthropic-ai/claude-agent-sdk')
612
+ const { query } = await import("@anthropic-ai/claude-agent-sdk");
542
613
 
543
- const systemPrompt = this.buildCurationPrompt(triggerType)
614
+ const systemPrompt = this.buildCurationPrompt(triggerType);
544
615
 
545
616
  // Format the conversation as a readable transcript for the prompt
546
- const transcript = this._formatConversationTranscript(messages)
617
+ const transcript = this._formatConversationTranscript(messages);
547
618
 
548
619
  // Build the prompt with transcript + curation request
549
620
  const prompt = `Here is the conversation transcript to curate:
@@ -552,79 +623,90 @@ ${transcript}
552
623
 
553
624
  ---
554
625
 
555
- This session has ended. Please curate the memories from this conversation according to your system instructions. Return ONLY the JSON structure with no additional text.`
626
+ This session has ended. Please curate the memories from this conversation according to your system instructions. Return ONLY the JSON structure with no additional text.`;
556
627
 
557
628
  // Use Agent SDK - no API key needed, uses Claude Code OAuth
558
629
  const q = query({
559
630
  prompt,
560
631
  options: {
561
632
  systemPrompt,
562
- permissionMode: 'bypassPermissions',
563
- model: 'claude-opus-4-5-20251101',
633
+ permissionMode: "bypassPermissions",
634
+ model: "claude-opus-4-5-20251101",
564
635
  },
565
- })
636
+ });
566
637
 
567
638
  // Iterate through the async generator to get the result
568
- let resultText = ''
639
+ let resultText = "";
569
640
  for await (const msg of q) {
570
- if (msg.type === 'result' && 'result' in msg) {
571
- resultText = msg.result
572
- break
641
+ if (msg.type === "result" && "result" in msg) {
642
+ resultText = msg.result;
643
+ break;
573
644
  }
574
645
  }
575
646
 
576
647
  if (!resultText) {
577
- logger.debug('Curator SDK: No result text returned from Agent SDK', 'curator')
578
- return { session_summary: '', memories: [] }
648
+ logger.debug(
649
+ "Curator SDK: No result text returned from Agent SDK",
650
+ "curator",
651
+ );
652
+ return { session_summary: "", memories: [] };
579
653
  }
580
654
 
581
655
  // Log raw response in verbose mode
582
- logger.debug(`Curator SDK raw response (${resultText.length} chars):`, 'curator')
656
+ logger.debug(
657
+ `Curator SDK raw response (${resultText.length} chars):`,
658
+ "curator",
659
+ );
583
660
  if (logger.isVerbose()) {
584
- const preview = resultText.length > 3000 ? resultText.slice(0, 3000) + '...[truncated]' : resultText
585
- console.log(preview)
661
+ const preview =
662
+ resultText.length > 3000
663
+ ? resultText.slice(0, 3000) + "...[truncated]"
664
+ : resultText;
665
+ console.log(preview);
586
666
  }
587
667
 
588
- return this.parseCurationResponse(resultText)
668
+ return this.parseCurationResponse(resultText);
589
669
  }
590
670
 
591
671
  /**
592
672
  * Format conversation messages into a readable transcript
593
673
  */
594
674
  private _formatConversationTranscript(
595
- messages: Array<{ role: 'user' | 'assistant'; content: string | any[] }>
675
+ messages: Array<{ role: "user" | "assistant"; content: string | any[] }>,
596
676
  ): string {
597
- const lines: string[] = []
677
+ const lines: string[] = [];
598
678
 
599
679
  for (const msg of messages) {
600
- const role = msg.role === 'user' ? 'User' : 'Assistant'
601
- let content: string
680
+ const role = msg.role === "user" ? "User" : "Assistant";
681
+ let content: string;
602
682
 
603
- if (typeof msg.content === 'string') {
604
- content = msg.content
683
+ if (typeof msg.content === "string") {
684
+ content = msg.content;
605
685
  } else if (Array.isArray(msg.content)) {
606
686
  // Extract text from content blocks
607
687
  content = msg.content
608
- .filter((block: any) => block.type === 'text' && block.text)
688
+ .filter((block: any) => block.type === "text" && block.text)
609
689
  .map((block: any) => block.text)
610
- .join('\n')
690
+ .join("\n");
611
691
 
612
692
  // Also note tool uses (but don't include full details)
613
- const toolUses = msg.content.filter((block: any) => block.type === 'tool_use')
693
+ const toolUses = msg.content.filter(
694
+ (block: any) => block.type === "tool_use",
695
+ );
614
696
  if (toolUses.length > 0) {
615
- const toolNames = toolUses.map((t: any) => t.name).join(', ')
616
- content += `\n[Used tools: ${toolNames}]`
697
+ const toolNames = toolUses.map((t: any) => t.name).join(", ");
698
+ content += `\n[Used tools: ${toolNames}]`;
617
699
  }
618
700
  } else {
619
- content = '[empty message]'
701
+ content = "[empty message]";
620
702
  }
621
703
 
622
704
  if (content.trim()) {
623
- lines.push(`**${role}:**\n${content}\n`)
705
+ lines.push(`**${role}:**\n${content}\n`);
624
706
  }
625
707
  }
626
708
 
627
- return lines.join('\n')
709
+ return lines.join("\n");
628
710
  }
629
711
 
630
712
  /**
@@ -633,41 +715,44 @@ This session has ended. Please curate the memories from this conversation accord
633
715
  * @deprecated Use curateWithSDK() which uses Agent SDK (no API key needed)
634
716
  */
635
717
  async curateWithAnthropicSDK(
636
- messages: Array<{ role: 'user' | 'assistant'; content: string | any[] }>,
637
- triggerType: CurationTrigger = 'session_end'
718
+ messages: Array<{ role: "user" | "assistant"; content: string | any[] }>,
719
+ triggerType: CurationTrigger = "session_end",
638
720
  ): Promise<CurationResult> {
639
721
  if (!this._config.apiKey) {
640
- throw new Error('API key required for Anthropic SDK mode. Set ANTHROPIC_API_KEY environment variable.')
722
+ throw new Error(
723
+ "API key required for Anthropic SDK mode. Set ANTHROPIC_API_KEY environment variable.",
724
+ );
641
725
  }
642
726
 
643
727
  // Dynamic import to make SDK optional
644
- const { default: Anthropic } = await import('@anthropic-ai/sdk')
645
- const client = new Anthropic({ apiKey: this._config.apiKey })
728
+ const { default: Anthropic } = await import("@anthropic-ai/sdk");
729
+ const client = new Anthropic({ apiKey: this._config.apiKey });
646
730
 
647
- const systemPrompt = this.buildCurationPrompt(triggerType)
731
+ const systemPrompt = this.buildCurationPrompt(triggerType);
648
732
 
649
733
  // Build the conversation: original messages + curation request
650
734
  const conversationMessages = [
651
735
  ...messages,
652
736
  {
653
- role: 'user' as const,
654
- content: 'This session has ended. Please curate the memories from our conversation according to your system instructions. Return ONLY the JSON structure with no additional text.',
737
+ role: "user" as const,
738
+ content:
739
+ "This session has ended. Please curate the memories from our conversation according to your system instructions. Return ONLY the JSON structure with no additional text.",
655
740
  },
656
- ]
741
+ ];
657
742
 
658
743
  const response = await client.messages.create({
659
- model: 'claude-sonnet-4-20250514',
744
+ model: "claude-sonnet-4-20250514",
660
745
  max_tokens: 64000,
661
746
  system: systemPrompt,
662
747
  messages: conversationMessages,
663
- })
748
+ });
664
749
 
665
- const content = response.content[0]
666
- if (content.type !== 'text') {
667
- throw new Error('Unexpected response type from Claude API')
750
+ const content = response.content[0];
751
+ if (content.type !== "text") {
752
+ throw new Error("Unexpected response type from Claude API");
668
753
  }
669
754
 
670
- return this.parseCurationResponse(content.text)
755
+ return this.parseCurationResponse(content.text);
671
756
  }
672
757
 
673
758
  /**
@@ -675,10 +760,12 @@ This session has ended. Please curate the memories from this conversation accord
675
760
  * Convenience method that extracts messages from SessionSegment
676
761
  */
677
762
  async curateFromSegment(
678
- segment: { messages: Array<{ role: 'user' | 'assistant'; content: string | any[] }> },
679
- triggerType: CurationTrigger = 'session_end'
763
+ segment: {
764
+ messages: Array<{ role: "user" | "assistant"; content: string | any[] }>;
765
+ },
766
+ triggerType: CurationTrigger = "session_end",
680
767
  ): Promise<CurationResult> {
681
- return this.curateWithSDK(segment.messages, triggerType)
768
+ return this.curateWithSDK(segment.messages, triggerType);
682
769
  }
683
770
 
684
771
  /**
@@ -687,67 +774,84 @@ This session has ended. Please curate the memories from this conversation accord
687
774
  */
688
775
  async curateFromSessionFile(
689
776
  sessionId: string,
690
- triggerType: CurationTrigger = 'session_end',
691
- cwd?: string
777
+ triggerType: CurationTrigger = "session_end",
778
+ cwd?: string,
692
779
  ): Promise<CurationResult> {
693
780
  // Find the session file
694
- const sessionFile = await this._findSessionFile(sessionId, cwd)
781
+ const sessionFile = await this._findSessionFile(sessionId, cwd);
695
782
  if (!sessionFile) {
696
- logger.debug(`Curator: Could not find session file for ${sessionId}`, 'curator')
697
- return { session_summary: '', memories: [] }
783
+ logger.debug(
784
+ `Curator: Could not find session file for ${sessionId}`,
785
+ "curator",
786
+ );
787
+ return { session_summary: "", memories: [] };
698
788
  }
699
789
 
700
- logger.debug(`Curator: Found session file: ${sessionFile}`, 'curator')
790
+ logger.debug(`Curator: Found session file: ${sessionFile}`, "curator");
701
791
 
702
792
  // Parse the session
703
- const session = await parseSessionFile(sessionFile)
793
+ const session = await parseSessionFile(sessionFile);
704
794
  if (session.messages.length === 0) {
705
- logger.debug('Curator: Session has no messages', 'curator')
706
- return { session_summary: '', memories: [] }
795
+ logger.debug("Curator: Session has no messages", "curator");
796
+ return { session_summary: "", memories: [] };
707
797
  }
708
798
 
709
- logger.debug(`Curator: Parsed ${session.messages.length} messages, ~${session.metadata.estimatedTokens} tokens`, 'curator')
799
+ logger.debug(
800
+ `Curator: Parsed ${session.messages.length} messages, ~${session.metadata.estimatedTokens} tokens`,
801
+ "curator",
802
+ );
710
803
 
711
804
  // Use SDK mode with the parsed messages
712
- return this.curateWithSDK(session.messages as any, triggerType)
805
+ return this.curateWithSDK(session.messages as any, triggerType);
713
806
  }
714
807
 
715
808
  /**
716
809
  * Find the session file path given a session ID
717
810
  */
718
- private async _findSessionFile(sessionId: string, cwd?: string): Promise<string | null> {
719
- const projectsDir = join(homedir(), '.claude', 'projects')
811
+ private async _findSessionFile(
812
+ sessionId: string,
813
+ cwd?: string,
814
+ ): Promise<string | null> {
815
+ const projectsDir = join(homedir(), ".claude", "projects");
720
816
 
721
817
  // If we have cwd, try to derive the project folder name
722
818
  if (cwd) {
723
819
  // Convert cwd to Claude's folder naming: /home/user/project -> -home-user-project
724
- const projectFolder = cwd.replace(/\//g, '-').replace(/^-/, '-')
725
- const sessionPath = join(projectsDir, projectFolder, `${sessionId}.jsonl`)
820
+ const projectFolder = cwd.replace(/\//g, "-").replace(/^-/, "-");
821
+ const sessionPath = join(
822
+ projectsDir,
823
+ projectFolder,
824
+ `${sessionId}.jsonl`,
825
+ );
726
826
  if (existsSync(sessionPath)) {
727
- return sessionPath
827
+ return sessionPath;
728
828
  }
729
829
 
730
830
  // Also try the exact folder name (cwd might already be encoded)
731
- const altPath = join(projectsDir, cwd.split('/').pop() || '', `${sessionId}.jsonl`)
831
+ const altPath = join(
832
+ projectsDir,
833
+ cwd.split("/").pop() || "",
834
+ `${sessionId}.jsonl`,
835
+ );
732
836
  if (existsSync(altPath)) {
733
- return altPath
837
+ return altPath;
734
838
  }
735
839
  }
736
840
 
737
841
  // Search all project folders for the session ID
738
842
  try {
739
- const projectFolders = await readdir(projectsDir)
843
+ const projectFolders = await readdir(projectsDir);
740
844
  for (const folder of projectFolders) {
741
- const sessionPath = join(projectsDir, folder, `${sessionId}.jsonl`)
845
+ const sessionPath = join(projectsDir, folder, `${sessionId}.jsonl`);
742
846
  if (existsSync(sessionPath)) {
743
- return sessionPath
847
+ return sessionPath;
744
848
  }
745
849
  }
746
850
  } catch {
747
851
  // Projects dir doesn't exist
748
852
  }
749
853
 
750
- return null
854
+ return null;
751
855
  }
752
856
 
753
857
  /**
@@ -756,168 +860,218 @@ This session has ended. Please curate the memories from this conversation accord
756
860
  */
757
861
  async curateWithCLI(
758
862
  sessionId: string,
759
- triggerType: CurationTrigger = 'session_end',
863
+ triggerType: CurationTrigger = "session_end",
760
864
  cwd?: string,
761
- cliTypeOverride?: 'claude-code' | 'gemini-cli'
865
+ cliTypeOverride?: "claude-code" | "gemini-cli",
762
866
  ): Promise<CurationResult> {
763
- const type = cliTypeOverride ?? this._config.cliType
764
- const systemPrompt = this.buildCurationPrompt(triggerType)
765
- const userMessage = 'This session has ended. Please curate the memories from our conversation according to the instructions in your system prompt. Return ONLY the JSON structure.'
867
+ const type = cliTypeOverride ?? this._config.cliType;
868
+ const systemPrompt = this.buildCurationPrompt(triggerType);
869
+ const userMessage =
870
+ "This session has ended. Please curate the memories from our conversation according to the instructions in your system prompt. Return ONLY the JSON structure.";
766
871
 
767
872
  // Build CLI command based on type
768
- const args: string[] = []
769
- let command = this._config.cliCommand
873
+ const args: string[] = [];
874
+ let command = this._config.cliCommand;
770
875
 
771
- if (type === 'claude-code') {
876
+ if (type === "claude-code") {
772
877
  args.push(
773
- '--resume', sessionId,
774
- '-p', userMessage,
775
- '--append-system-prompt', systemPrompt,
776
- '--output-format', 'json'
777
- )
878
+ "--resume",
879
+ sessionId,
880
+ "-p",
881
+ userMessage,
882
+ "--append-system-prompt",
883
+ systemPrompt,
884
+ "--output-format",
885
+ "json",
886
+ );
778
887
  } else {
779
888
  // gemini-cli
780
- command = 'gemini' // Default to 'gemini' in PATH for gemini-cli
889
+ command = "gemini"; // Default to 'gemini' in PATH for gemini-cli
781
890
  args.push(
782
- '--resume', sessionId,
783
- '-p', `${systemPrompt}\n\n${userMessage}`,
784
- '--output-format', 'json'
785
- )
891
+ "--resume",
892
+ sessionId,
893
+ "-p",
894
+ `${systemPrompt}\n\n${userMessage}`,
895
+ "--output-format",
896
+ "json",
897
+ );
786
898
  }
787
899
 
788
900
  // Execute CLI
789
- logger.debug(`Curator: Spawning CLI with CLAUDE_CODE_MAX_OUTPUT_TOKENS=64000`, 'curator')
790
- logger.debug(`Curator: Command: ${command} ${args.slice(0, 3).join(' ')}...`, 'curator')
901
+ logger.debug(
902
+ `Curator: Spawning CLI with CLAUDE_CODE_MAX_OUTPUT_TOKENS=64000`,
903
+ "curator",
904
+ );
905
+ logger.debug(
906
+ `Curator: Command: ${command} ${args.slice(0, 3).join(" ")}...`,
907
+ "curator",
908
+ );
791
909
 
792
910
  const proc = Bun.spawn([command, ...args], {
793
911
  cwd,
794
912
  env: {
795
913
  ...process.env,
796
- MEMORY_CURATOR_ACTIVE: '1', // Prevent recursive hook triggering
797
- CLAUDE_CODE_MAX_OUTPUT_TOKENS: '64000', // Max output to avoid truncation
914
+ MEMORY_CURATOR_ACTIVE: "1", // Prevent recursive hook triggering
915
+ CLAUDE_CODE_MAX_OUTPUT_TOKENS: "64000", // Max output to avoid truncation
798
916
  },
799
- stdout: 'pipe',
800
- stderr: 'pipe',
801
- })
917
+ stdout: "pipe",
918
+ stderr: "pipe",
919
+ });
802
920
 
803
921
  // Capture both stdout and stderr
804
922
  const [stdout, stderr] = await Promise.all([
805
923
  new Response(proc.stdout).text(),
806
924
  new Response(proc.stderr).text(),
807
- ])
808
- const exitCode = await proc.exited
925
+ ]);
926
+ const exitCode = await proc.exited;
809
927
 
810
- logger.debug(`Curator CLI exit code: ${exitCode}`, 'curator')
928
+ logger.debug(`Curator CLI exit code: ${exitCode}`, "curator");
811
929
  if (stderr && stderr.trim()) {
812
- logger.debug(`Curator stderr (${stderr.length} chars): ${stderr.slice(0, 500)}`, 'curator')
930
+ logger.debug(
931
+ `Curator stderr (${stderr.length} chars): ${stderr}`,
932
+ "curator",
933
+ );
813
934
  }
814
935
 
815
936
  if (exitCode !== 0) {
816
- return { session_summary: '', memories: [] }
937
+ return { session_summary: "", memories: [] };
817
938
  }
818
939
 
819
940
  // Log raw response in verbose mode
820
- logger.debug(`Curator CLI raw stdout (${stdout.length} chars):`, 'curator')
941
+ logger.debug(`Curator CLI raw stdout (${stdout.length} chars):`, "curator");
821
942
  // Always log the last 100 chars to see where output ends
822
- logger.debug(`Curator: stdout ends with: '${stdout.slice(-100)}'`, 'curator')
943
+ logger.debug(`Curator: '${stdout}'`, "curator");
823
944
  if (logger.isVerbose()) {
824
945
  // Show first 2000 chars to avoid flooding console
825
- const preview = stdout.length > 2000 ? stdout.slice(0, 2000) + '...[truncated]' : stdout
826
- console.log(preview)
946
+ const preview = stdout.length > 2000 ? stdout : stdout;
947
+ console.log(preview);
827
948
  }
828
949
 
829
950
  // Extract JSON from CLI output
830
951
  try {
831
952
  // First, parse the CLI JSON wrapper
832
- const cliOutput = JSON.parse(stdout)
953
+ const cliOutput = JSON.parse(stdout);
833
954
 
834
955
  // Claude Code now returns an array of events - find the result object
835
- let resultObj: any
956
+ let resultObj: any;
836
957
  if (Array.isArray(cliOutput)) {
837
958
  // New format: array of events, find the one with type="result"
838
- resultObj = cliOutput.find((item: any) => item.type === 'result')
959
+ resultObj = cliOutput.find((item: any) => item.type === "result");
839
960
  if (!resultObj) {
840
- logger.debug('Curator: No result object found in CLI output array', 'curator')
841
- return { session_summary: '', memories: [] }
961
+ logger.debug(
962
+ "Curator: No result object found in CLI output array",
963
+ "curator",
964
+ );
965
+ return { session_summary: "", memories: [] };
842
966
  }
843
967
  } else {
844
968
  // Old format: single object (backwards compatibility)
845
- resultObj = cliOutput
969
+ resultObj = cliOutput;
846
970
  }
847
971
 
848
972
  // Check for error response FIRST (like Python does)
849
- if (resultObj.type === 'error' || resultObj.is_error === true) {
850
- logger.debug(`Curator: Error response from CLI: ${JSON.stringify(resultObj).slice(0, 500)}`, 'curator')
851
- return { session_summary: '', memories: [] }
973
+ if (resultObj.type === "error" || resultObj.is_error === true) {
974
+ logger.debug(
975
+ `Curator: Error response from CLI: ${JSON.stringify(resultObj)}`,
976
+ "curator",
977
+ );
978
+ return { session_summary: "", memories: [] };
852
979
  }
853
980
 
854
981
  // Extract the "result" field (AI's response text)
855
- let aiResponse = ''
856
- if (typeof resultObj.result === 'string') {
857
- aiResponse = resultObj.result
982
+ let aiResponse = "";
983
+ if (typeof resultObj.result === "string") {
984
+ aiResponse = resultObj.result;
858
985
  } else {
859
- logger.debug(`Curator: result field is not a string: ${typeof resultObj.result}`, 'curator')
860
- return { session_summary: '', memories: [] }
986
+ logger.debug(
987
+ `Curator: result field is not a string: ${typeof resultObj.result}`,
988
+ "curator",
989
+ );
990
+ return { session_summary: "", memories: [] };
861
991
  }
862
992
 
863
993
  // Log the AI response in verbose mode
864
- logger.debug(`Curator AI response (${aiResponse.length} chars):`, 'curator')
994
+ logger.debug(
995
+ `Curator AI response (${aiResponse.length} chars):`,
996
+ "curator",
997
+ );
865
998
  if (logger.isVerbose()) {
866
- const preview = aiResponse.length > 3000 ? aiResponse.slice(0, 3000) + '...[truncated]' : aiResponse
867
- console.log(preview)
999
+ const preview = aiResponse.length > 3000 ? aiResponse : aiResponse;
1000
+ console.log(preview);
868
1001
  }
869
1002
 
870
1003
  // Remove markdown code blocks if present (```json ... ```)
871
- const codeBlockMatch = aiResponse.match(/```(?:json)?\s*([\s\S]*?)```/)
1004
+ const codeBlockMatch = aiResponse.match(/```(?:json)?\s*([\s\S]*?)```/);
872
1005
  if (codeBlockMatch) {
873
- logger.debug(`Curator: Code block matched, extracting ${codeBlockMatch[1]!.length} chars`, 'curator')
874
- aiResponse = codeBlockMatch[1]!.trim()
1006
+ logger.debug(
1007
+ `Curator: Code block matched, extracting ${codeBlockMatch[1]!.length} chars`,
1008
+ "curator",
1009
+ );
1010
+ aiResponse = codeBlockMatch[1]!.trim();
875
1011
  } else {
876
- logger.debug(`Curator: No code block found, using raw response`, 'curator')
1012
+ logger.debug(
1013
+ `Curator: No code block found, using raw response`,
1014
+ "curator",
1015
+ );
877
1016
  // Log the last 200 chars to see where truncation happened
878
1017
  if (aiResponse.length > 200) {
879
- logger.debug(`Curator: Last 200 chars of response: ${aiResponse.slice(-200)}`, 'curator')
1018
+ logger.debug(`Curator: ${aiResponse}`, "curator");
880
1019
  }
881
1020
  }
882
1021
 
883
1022
  // Now find the JSON object (same regex as Python)
884
- const jsonMatch = aiResponse.match(/\{[\s\S]*\}/)?.[0]
1023
+ const jsonMatch = aiResponse.match(/\{[\s\S]*\}/)?.[0];
885
1024
  if (jsonMatch) {
886
- logger.debug(`Curator: Found JSON object (${jsonMatch.length} chars), parsing...`, 'curator')
1025
+ logger.debug(
1026
+ `Curator: Found JSON object (${jsonMatch.length} chars), parsing...`,
1027
+ "curator",
1028
+ );
887
1029
 
888
1030
  // Detect likely truncation: JSON much smaller than response
889
- const likelyTruncated = jsonMatch.length < aiResponse.length * 0.5
1031
+ const likelyTruncated = jsonMatch.length < aiResponse.length * 0.5;
890
1032
 
891
1033
  if (likelyTruncated) {
892
- logger.debug(`Curator: WARNING - JSON (${jsonMatch.length}) much smaller than response (${aiResponse.length}) - likely truncated`, 'curator')
1034
+ logger.debug(
1035
+ `Curator: WARNING - JSON (${jsonMatch.length}) much smaller than response (${aiResponse.length}) - likely truncated`,
1036
+ "curator",
1037
+ );
893
1038
  // Find the last } position and log what's around it
894
- const lastBrace = aiResponse.lastIndexOf('}')
895
- logger.debug(`Curator: Last } at position ${lastBrace}, char before: '${aiResponse[lastBrace-1]}', char after: '${aiResponse[lastBrace+1] || 'EOF'}'`, 'curator')
1039
+ const lastBrace = aiResponse.lastIndexOf("}");
1040
+ logger.debug(
1041
+ `Curator: Last } at position ${lastBrace}, char before: '${aiResponse[lastBrace - 1]}', char after: '${aiResponse[lastBrace + 1] || "EOF"}'`,
1042
+ "curator",
1043
+ );
896
1044
  // Log chars around the cut point
897
- const cutPoint = jsonMatch.length
898
- logger.debug(`Curator: Around match end (${cutPoint}): '...${aiResponse.slice(Math.max(0, cutPoint-50), cutPoint+50)}...'`, 'curator')
1045
+ const cutPoint = jsonMatch.length;
1046
+ logger.debug(
1047
+ `Curator: Around match end (${cutPoint}): '...${aiResponse.slice(Math.max(0, cutPoint - 50), cutPoint + 50)}...'`,
1048
+ "curator",
1049
+ );
899
1050
  }
900
1051
 
901
- const result = this.parseCurationResponse(jsonMatch)
1052
+ const result = this.parseCurationResponse(jsonMatch);
902
1053
 
903
1054
  // If we got 0 memories and likely truncated, try SDK fallback
904
1055
  if (result.memories.length === 0 && likelyTruncated) {
905
- logger.debug('Curator: CLI mode returned 0 memories with truncation detected, trying SDK fallback...', 'curator')
906
- return this._fallbackToSDK(sessionId, triggerType, cwd)
1056
+ logger.debug(
1057
+ "Curator: CLI mode returned 0 memories with truncation detected, trying SDK fallback...",
1058
+ "curator",
1059
+ );
1060
+ return this._fallbackToSDK(sessionId, triggerType, cwd);
907
1061
  }
908
1062
 
909
- return result
1063
+ return result;
910
1064
  } else {
911
- logger.debug('Curator: No JSON object found in AI response', 'curator')
1065
+ logger.debug("Curator: No JSON object found in AI response", "curator");
912
1066
  }
913
1067
  } catch (error: any) {
914
1068
  // Parse error - return empty result
915
- logger.debug(`Curator: Parse error: ${error.message}`, 'curator')
1069
+ logger.debug(`Curator: Parse error: ${error.message}`, "curator");
916
1070
  }
917
1071
 
918
1072
  // CLI mode failed - try SDK fallback
919
- logger.debug('Curator: CLI mode failed, trying SDK fallback...', 'curator')
920
- return this._fallbackToSDK(sessionId, triggerType, cwd)
1073
+ logger.debug("Curator: CLI mode failed, trying SDK fallback...", "curator");
1074
+ return this._fallbackToSDK(sessionId, triggerType, cwd);
921
1075
  }
922
1076
 
923
1077
  /**
@@ -926,19 +1080,29 @@ This session has ended. Please curate the memories from this conversation accord
926
1080
  private async _fallbackToSDK(
927
1081
  sessionId: string,
928
1082
  triggerType: CurationTrigger,
929
- cwd?: string
1083
+ cwd?: string,
930
1084
  ): Promise<CurationResult> {
931
1085
  try {
932
- const result = await this.curateFromSessionFile(sessionId, triggerType, cwd)
1086
+ const result = await this.curateFromSessionFile(
1087
+ sessionId,
1088
+ triggerType,
1089
+ cwd,
1090
+ );
933
1091
  if (result.memories.length > 0) {
934
- logger.debug(`Curator: SDK fallback succeeded with ${result.memories.length} memories`, 'curator')
1092
+ logger.debug(
1093
+ `Curator: SDK fallback succeeded with ${result.memories.length} memories`,
1094
+ "curator",
1095
+ );
935
1096
  } else {
936
- logger.debug('Curator: SDK fallback also returned 0 memories', 'curator')
1097
+ logger.debug(
1098
+ "Curator: SDK fallback also returned 0 memories",
1099
+ "curator",
1100
+ );
937
1101
  }
938
- return result
1102
+ return result;
939
1103
  } catch (error: any) {
940
- logger.debug(`Curator: SDK fallback failed: ${error.message}`, 'curator')
941
- return { session_summary: '', memories: [] }
1104
+ logger.debug(`Curator: SDK fallback failed: ${error.message}`, "curator");
1105
+ return { session_summary: "", memories: [] };
942
1106
  }
943
1107
  }
944
1108
  }
@@ -947,5 +1111,5 @@ This session has ended. Please curate the memories from this conversation accord
947
1111
  * Create a new curator
948
1112
  */
949
1113
  export function createCurator(config?: CuratorConfig): Curator {
950
- return new Curator(config)
1114
+ return new Curator(config);
951
1115
  }
@@ -197,11 +197,11 @@ export async function createServer(config: ServerConfig = {}) {
197
197
  // Fire and forget - don't block the response
198
198
  setImmediate(async () => {
199
199
  try {
200
- const result = await curator.curateWithCLI(
200
+ // Use SDK mode - more reliable than CLI which can go off-rails
201
+ const result = await curator.curateFromSessionFile(
201
202
  body.claude_session_id,
202
203
  body.trigger,
203
- body.cwd,
204
- body.cli_type
204
+ body.cwd
205
205
  )
206
206
 
207
207
  if (result.memories.length > 0) {
@@ -225,7 +225,8 @@ export async function createServer(config: ServerConfig = {}) {
225
225
  logger.logManagementStart(result.memories.length)
226
226
  const startTime = Date.now()
227
227
 
228
- const managementResult = await manager.manageWithCLI(
228
+ // Use SDK mode - more reliable than CLI which can go off-rails
229
+ const managementResult = await manager.manageWithSDK(
229
230
  body.project_id,
230
231
  sessionNumber,
231
232
  result,