@postnesia/hooks 0.1.6 → 0.1.7
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/dist/compact.d.ts +8 -0
- package/dist/compact.js +138 -0
- package/package.json +5 -3
package/dist/compact.js
ADDED
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* PreCompact Hook
|
|
4
|
+
* Reads the conversation transcript before context compaction,
|
|
5
|
+
* summarizes it via Claude, and persists a journal entry + memories
|
|
6
|
+
* directly into the Postnesia DB.
|
|
7
|
+
*/
|
|
8
|
+
import fs from 'fs';
|
|
9
|
+
import Anthropic from '@anthropic-ai/sdk';
|
|
10
|
+
import { getDb, queries, createMemory } from '@postnesia/db';
|
|
11
|
+
import { embed } from '@postnesia/db/embeddings';
|
|
12
|
+
// ---------------------------------------------------------------------------
|
|
13
|
+
// Helpers
|
|
14
|
+
// ---------------------------------------------------------------------------
|
|
15
|
+
function extractText(content) {
|
|
16
|
+
if (typeof content === 'string')
|
|
17
|
+
return content;
|
|
18
|
+
return content
|
|
19
|
+
.filter(b => b.type === 'text' && b.text)
|
|
20
|
+
.map(b => b.text)
|
|
21
|
+
.join('\n');
|
|
22
|
+
}
|
|
23
|
+
function buildTranscriptText(messages) {
|
|
24
|
+
return messages
|
|
25
|
+
.map(m => `[${m.role.toUpperCase()}]\n${extractText(m.content)}`)
|
|
26
|
+
.join('\n\n---\n\n');
|
|
27
|
+
}
|
|
28
|
+
// ---------------------------------------------------------------------------
|
|
29
|
+
// Main
|
|
30
|
+
// ---------------------------------------------------------------------------
|
|
31
|
+
const chunks = [];
|
|
32
|
+
for await (const chunk of process.stdin) {
|
|
33
|
+
chunks.push(chunk);
|
|
34
|
+
}
|
|
35
|
+
const raw = Buffer.concat(chunks).toString('utf-8').trim();
|
|
36
|
+
if (!raw)
|
|
37
|
+
process.exit(0);
|
|
38
|
+
let payload;
|
|
39
|
+
try {
|
|
40
|
+
payload = JSON.parse(raw);
|
|
41
|
+
}
|
|
42
|
+
catch {
|
|
43
|
+
process.exit(0);
|
|
44
|
+
}
|
|
45
|
+
if (payload.hook_event_name !== 'PreCompact' || !payload.transcript_path) {
|
|
46
|
+
process.exit(0);
|
|
47
|
+
}
|
|
48
|
+
// Read the transcript
|
|
49
|
+
let messages = [];
|
|
50
|
+
try {
|
|
51
|
+
const transcriptRaw = fs.readFileSync(payload.transcript_path, 'utf-8');
|
|
52
|
+
messages = JSON.parse(transcriptRaw);
|
|
53
|
+
}
|
|
54
|
+
catch {
|
|
55
|
+
process.exit(0);
|
|
56
|
+
}
|
|
57
|
+
if (!Array.isArray(messages) || messages.length === 0)
|
|
58
|
+
process.exit(0);
|
|
59
|
+
const transcriptText = buildTranscriptText(messages);
|
|
60
|
+
const today = new Date().toISOString().slice(0, 10);
|
|
61
|
+
// ---------------------------------------------------------------------------
|
|
62
|
+
// Summarize with Claude
|
|
63
|
+
// ---------------------------------------------------------------------------
|
|
64
|
+
const client = new Anthropic();
|
|
65
|
+
const systemPrompt = `You are a memory and journal assistant. Given a conversation transcript, extract:
|
|
66
|
+
1. A journal entry summarizing what was worked on
|
|
67
|
+
2. Key memories worth persisting long-term
|
|
68
|
+
|
|
69
|
+
Return ONLY valid JSON matching this exact schema (no markdown, no extra text):
|
|
70
|
+
{
|
|
71
|
+
"date": "YYYY-MM-DD",
|
|
72
|
+
"content": "Narrative description of what was done in this session",
|
|
73
|
+
"learned": "Key technical or contextual learnings from this session",
|
|
74
|
+
"keyMoments": "Significant decisions, breakthroughs, or pivots",
|
|
75
|
+
"memories": [
|
|
76
|
+
{
|
|
77
|
+
"content": "Full memory content",
|
|
78
|
+
"content_l1": "Short one-line summary (max 200 chars)",
|
|
79
|
+
"type": "event|decision|lesson|preference|person|technical",
|
|
80
|
+
"importance": 1-5,
|
|
81
|
+
"tags": ["tag1", "tag2"]
|
|
82
|
+
}
|
|
83
|
+
]
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
Only include memories that are genuinely worth remembering long-term (importance >= 3).
|
|
87
|
+
Keep memories focused and factual. Today's date is ${today}.`;
|
|
88
|
+
let summary;
|
|
89
|
+
try {
|
|
90
|
+
const response = await client.messages.create({
|
|
91
|
+
model: 'claude-haiku-4-5-20251001',
|
|
92
|
+
max_tokens: 2048,
|
|
93
|
+
system: systemPrompt,
|
|
94
|
+
messages: [
|
|
95
|
+
{
|
|
96
|
+
role: 'user',
|
|
97
|
+
content: `Summarize this conversation transcript:\n\n${transcriptText.slice(0, 40000)}`,
|
|
98
|
+
},
|
|
99
|
+
],
|
|
100
|
+
});
|
|
101
|
+
const text = response.content.find(b => b.type === 'text')?.text ?? '';
|
|
102
|
+
summary = JSON.parse(text);
|
|
103
|
+
}
|
|
104
|
+
catch (err) {
|
|
105
|
+
// If summarization fails, exit cleanly — don't break the compact
|
|
106
|
+
process.stderr.write(`[postnesia compact] summarization failed: ${err}\n`);
|
|
107
|
+
process.exit(0);
|
|
108
|
+
}
|
|
109
|
+
// ---------------------------------------------------------------------------
|
|
110
|
+
// Persist to Postnesia DB
|
|
111
|
+
// ---------------------------------------------------------------------------
|
|
112
|
+
try {
|
|
113
|
+
const db = getDb();
|
|
114
|
+
// Journal entry
|
|
115
|
+
queries.insertJournal(db).run(summary.date ?? today, summary.content ?? '', summary.learned ?? null, summary.keyMoments ?? null, null // mood
|
|
116
|
+
);
|
|
117
|
+
// Memories
|
|
118
|
+
for (const mem of (summary.memories ?? [])) {
|
|
119
|
+
if (!mem.content || !mem.type || !mem.importance)
|
|
120
|
+
continue;
|
|
121
|
+
const embedding = await embed(mem.content);
|
|
122
|
+
createMemory(db, {
|
|
123
|
+
timestamp: new Date().toISOString(),
|
|
124
|
+
content: mem.content,
|
|
125
|
+
content_l1: mem.content_l1 ?? mem.content.slice(0, 200),
|
|
126
|
+
type: mem.type,
|
|
127
|
+
core: 0,
|
|
128
|
+
importance: mem.importance,
|
|
129
|
+
tags: mem.tags ?? [],
|
|
130
|
+
embedding,
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
process.stderr.write(`[postnesia compact] saved journal entry for ${summary.date} with ${summary.memories?.length ?? 0} memories\n`);
|
|
134
|
+
}
|
|
135
|
+
catch (err) {
|
|
136
|
+
process.stderr.write(`[postnesia compact] db write failed: ${err}\n`);
|
|
137
|
+
}
|
|
138
|
+
process.exit(0);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@postnesia/hooks",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.7",
|
|
4
4
|
"description": "Session bootstrap hook — loads L1 memory context at startup",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"private": false,
|
|
@@ -8,7 +8,8 @@
|
|
|
8
8
|
"dist"
|
|
9
9
|
],
|
|
10
10
|
"bin": {
|
|
11
|
-
"postnesia-claude": "./dist/claude.js"
|
|
11
|
+
"postnesia-claude": "./dist/claude.js",
|
|
12
|
+
"postnesia-compact": "./dist/compact.js"
|
|
12
13
|
},
|
|
13
14
|
"exports": {
|
|
14
15
|
".": {
|
|
@@ -21,7 +22,8 @@
|
|
|
21
22
|
}
|
|
22
23
|
},
|
|
23
24
|
"dependencies": {
|
|
24
|
-
"@
|
|
25
|
+
"@anthropic-ai/sdk": "^0.54.0",
|
|
26
|
+
"@postnesia/db": "0.1.7"
|
|
25
27
|
},
|
|
26
28
|
"devDependencies": {
|
|
27
29
|
"@types/node": "^22.10.5",
|