@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 +15 -0
- package/package.json +1 -1
- package/src/cli/commands/install.ts +34 -10
- package/src/core/curator.ts +425 -261
- package/src/server/index.ts +5 -4
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
|
@@ -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
|
|
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',
|
|
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(
|
|
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 ${
|
|
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(
|
|
88
|
-
const userPromptHook = join(
|
|
89
|
-
const curationHook = join(
|
|
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: [
|
package/src/core/curator.ts
CHANGED
|
@@ -3,13 +3,18 @@
|
|
|
3
3
|
// Uses the exact prompt from Python for consciousness continuity engineering
|
|
4
4
|
// ============================================================================
|
|
5
5
|
|
|
6
|
-
import { homedir } from
|
|
7
|
-
import { join } from
|
|
8
|
-
import { existsSync } from
|
|
9
|
-
import { readdir } from
|
|
10
|
-
import type {
|
|
11
|
-
|
|
12
|
-
|
|
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([
|
|
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(),
|
|
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
|
|
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?:
|
|
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 ??
|
|
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 =
|
|
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
|
|
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(
|
|
403
|
-
|
|
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(
|
|
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(
|
|
416
|
-
|
|
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(
|
|
421
|
-
|
|
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
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
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(
|
|
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}`,
|
|
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
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
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 ===
|
|
491
|
-
return value
|
|
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
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
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(
|
|
506
|
-
if (str.includes(
|
|
507
|
-
if (str.includes(
|
|
508
|
-
if (str.includes(
|
|
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
|
|
567
|
+
return "technical"; // Default fallback
|
|
511
568
|
}
|
|
512
569
|
|
|
513
|
-
private _validateScope(value: any):
|
|
514
|
-
if (!value) return undefined
|
|
515
|
-
const str = String(value).toLowerCase()
|
|
516
|
-
if (str ===
|
|
517
|
-
return undefined
|
|
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(
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
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:
|
|
538
|
-
triggerType: CurationTrigger =
|
|
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(
|
|
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:
|
|
563
|
-
model:
|
|
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 ===
|
|
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(
|
|
578
|
-
|
|
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(
|
|
656
|
+
logger.debug(
|
|
657
|
+
`Curator SDK raw response (${resultText.length} chars):`,
|
|
658
|
+
"curator",
|
|
659
|
+
);
|
|
583
660
|
if (logger.isVerbose()) {
|
|
584
|
-
const preview =
|
|
585
|
-
|
|
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:
|
|
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 ===
|
|
601
|
-
let content: string
|
|
680
|
+
const role = msg.role === "user" ? "User" : "Assistant";
|
|
681
|
+
let content: string;
|
|
602
682
|
|
|
603
|
-
if (typeof msg.content ===
|
|
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 ===
|
|
688
|
+
.filter((block: any) => block.type === "text" && block.text)
|
|
609
689
|
.map((block: any) => block.text)
|
|
610
|
-
.join(
|
|
690
|
+
.join("\n");
|
|
611
691
|
|
|
612
692
|
// Also note tool uses (but don't include full details)
|
|
613
|
-
const toolUses = msg.content.filter(
|
|
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 =
|
|
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(
|
|
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:
|
|
637
|
-
triggerType: CurationTrigger =
|
|
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(
|
|
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(
|
|
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:
|
|
654
|
-
content:
|
|
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:
|
|
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 !==
|
|
667
|
-
throw new Error(
|
|
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: {
|
|
679
|
-
|
|
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 =
|
|
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(
|
|
697
|
-
|
|
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}`,
|
|
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(
|
|
706
|
-
return { session_summary:
|
|
795
|
+
logger.debug("Curator: Session has no messages", "curator");
|
|
796
|
+
return { session_summary: "", memories: [] };
|
|
707
797
|
}
|
|
708
798
|
|
|
709
|
-
logger.debug(
|
|
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(
|
|
719
|
-
|
|
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,
|
|
725
|
-
const sessionPath = join(
|
|
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(
|
|
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 =
|
|
863
|
+
triggerType: CurationTrigger = "session_end",
|
|
760
864
|
cwd?: string,
|
|
761
|
-
cliTypeOverride?:
|
|
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 =
|
|
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 ===
|
|
876
|
+
if (type === "claude-code") {
|
|
772
877
|
args.push(
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
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 =
|
|
889
|
+
command = "gemini"; // Default to 'gemini' in PATH for gemini-cli
|
|
781
890
|
args.push(
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
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(
|
|
790
|
-
|
|
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:
|
|
797
|
-
CLAUDE_CODE_MAX_OUTPUT_TOKENS:
|
|
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:
|
|
800
|
-
stderr:
|
|
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}`,
|
|
928
|
+
logger.debug(`Curator CLI exit code: ${exitCode}`, "curator");
|
|
811
929
|
if (stderr && stderr.trim()) {
|
|
812
|
-
logger.debug(
|
|
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:
|
|
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):`,
|
|
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:
|
|
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
|
|
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 ===
|
|
959
|
+
resultObj = cliOutput.find((item: any) => item.type === "result");
|
|
839
960
|
if (!resultObj) {
|
|
840
|
-
logger.debug(
|
|
841
|
-
|
|
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 ===
|
|
850
|
-
logger.debug(
|
|
851
|
-
|
|
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 ===
|
|
857
|
-
aiResponse = resultObj.result
|
|
982
|
+
let aiResponse = "";
|
|
983
|
+
if (typeof resultObj.result === "string") {
|
|
984
|
+
aiResponse = resultObj.result;
|
|
858
985
|
} else {
|
|
859
|
-
logger.debug(
|
|
860
|
-
|
|
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(
|
|
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
|
|
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(
|
|
874
|
-
|
|
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(
|
|
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:
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
906
|
-
|
|
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(
|
|
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}`,
|
|
1069
|
+
logger.debug(`Curator: Parse error: ${error.message}`, "curator");
|
|
916
1070
|
}
|
|
917
1071
|
|
|
918
1072
|
// CLI mode failed - try SDK fallback
|
|
919
|
-
logger.debug(
|
|
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(
|
|
1086
|
+
const result = await this.curateFromSessionFile(
|
|
1087
|
+
sessionId,
|
|
1088
|
+
triggerType,
|
|
1089
|
+
cwd,
|
|
1090
|
+
);
|
|
933
1091
|
if (result.memories.length > 0) {
|
|
934
|
-
logger.debug(
|
|
1092
|
+
logger.debug(
|
|
1093
|
+
`Curator: SDK fallback succeeded with ${result.memories.length} memories`,
|
|
1094
|
+
"curator",
|
|
1095
|
+
);
|
|
935
1096
|
} else {
|
|
936
|
-
logger.debug(
|
|
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}`,
|
|
941
|
-
return { session_summary:
|
|
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
|
}
|
package/src/server/index.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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,
|