@postnesia/hooks 0.1.5 → 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/README.md ADDED
@@ -0,0 +1,63 @@
1
+ # @postnesia/hooks
2
+
3
+ Session bootstrap hook for Postnesia. Loads core memories and high-importance L1 summaries from the database and injects them into the agent's context at session start.
4
+
5
+ ## How it works
6
+
7
+ At session start the hook queries the database for:
8
+ - All **core memories** (always loaded, never decay)
9
+ - **Working memory** — non-core memories with importance >= 3
10
+
11
+ Output is written to stdout in a structured block:
12
+
13
+ ```
14
+ --- POSTNESIA MEMORY CONTEXT ---
15
+
16
+ [CORE MEMORIES]
17
+ #1 [technical] How Postnesia is used...
18
+
19
+ [WORKING MEMORY — L1 summaries, importance >= 3]
20
+ #4 [lesson] (imp:4) Learned that...
21
+
22
+ --- END MEMORY CONTEXT ---
23
+ ```
24
+
25
+ Claude Code injects stdout from `SessionStart` hooks into the conversation, so the agent sees this context before the first user message.
26
+
27
+ If the database does not exist yet the hook exits silently (no error).
28
+
29
+ ## Usage
30
+
31
+ ### Claude Code (SessionStart hook)
32
+
33
+ Add to `.claude/settings.json`:
34
+
35
+ ```json
36
+ {
37
+ "hooks": {
38
+ "SessionStart": [
39
+ {
40
+ "matcher": "",
41
+ "hooks": [
42
+ {
43
+ "type": "command",
44
+ "command": "npx postnesia-claude"
45
+ }
46
+ ]
47
+ }
48
+ ]
49
+ }
50
+ }
51
+ ```
52
+
53
+ ### Programmatic (handler export)
54
+
55
+ The `./handler` export provides a handler that can be wired into other agent frameworks. It populates the `BOOTSTRAP.md` bootstrap file slot with the formatted memory context:
56
+
57
+ ```ts
58
+ import handler from '@postnesia/hooks/handler';
59
+ ```
60
+
61
+ ## Environment Variables
62
+
63
+ Inherits from `@postnesia/db` — requires `DATABASE_URL` to be set.
package/dist/claude.js CHANGED
@@ -1,4 +1,10 @@
1
1
  import { format, loadL1Memories } from './index.js';
2
+ const chunks = [];
3
+ for await (const chunk of process.stdin) {
4
+ chunks.push(chunk);
5
+ }
6
+ const payload = JSON.parse(Buffer.concat(chunks).toString('utf-8'));
7
+ console.log(payload, 'payload');
2
8
  const memories = loadL1Memories();
3
9
  const output = format(memories);
4
10
  if (output)
@@ -0,0 +1,8 @@
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
+ export {};
@@ -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/dist/index.d.ts CHANGED
@@ -10,6 +10,7 @@ interface L1Memory {
10
10
  core: number;
11
11
  importance: number;
12
12
  effective_importance: number;
13
+ content: string;
13
14
  content_l1: string;
14
15
  last_accessed: string;
15
16
  }
package/dist/index.js CHANGED
@@ -25,7 +25,7 @@ export function format(memories) {
25
25
  if (core.length > 0) {
26
26
  lines.push('\n[CORE MEMORIES]');
27
27
  for (const m of core) {
28
- lines.push(`#${m.id} [${m.type}] ${m.content_l1}`);
28
+ lines.push(m.content);
29
29
  }
30
30
  }
31
31
  if (regular.length > 0) {
package/package.json CHANGED
@@ -1,13 +1,15 @@
1
1
  {
2
2
  "name": "@postnesia/hooks",
3
- "version": "0.1.5",
3
+ "version": "0.1.7",
4
4
  "description": "Session bootstrap hook — loads L1 memory context at startup",
5
5
  "type": "module",
6
+ "private": false,
6
7
  "files": [
7
8
  "dist"
8
9
  ],
9
10
  "bin": {
10
- "postnesia-claude": "./dist/claude.js"
11
+ "postnesia-claude": "./dist/claude.js",
12
+ "postnesia-compact": "./dist/compact.js"
11
13
  },
12
14
  "exports": {
13
15
  ".": {
@@ -20,7 +22,8 @@
20
22
  }
21
23
  },
22
24
  "dependencies": {
23
- "@postnesia/db": "^0.1.3"
25
+ "@anthropic-ai/sdk": "^0.54.0",
26
+ "@postnesia/db": "0.1.7"
24
27
  },
25
28
  "devDependencies": {
26
29
  "@types/node": "^22.10.5",
@@ -28,6 +31,7 @@
28
31
  "typescript": "^5.7.3"
29
32
  },
30
33
  "scripts": {
31
- "build": "tsc -p tsconfig.json"
34
+ "build": "pnpm clean && tsc -p tsconfig.json",
35
+ "clean": "rm -rf ./dist"
32
36
  }
33
37
  }