@jhizzard/termdeck-stack 0.4.0 → 0.4.1
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/assets/hooks/README.md +99 -40
- package/assets/hooks/memory-session-end.js +205 -80
- package/package.json +1 -1
package/assets/hooks/README.md
CHANGED
|
@@ -2,72 +2,131 @@
|
|
|
2
2
|
|
|
3
3
|
The `@jhizzard/termdeck-stack` installer can drop `memory-session-end.js`
|
|
4
4
|
into `~/.claude/hooks/` and wire it into `~/.claude/settings.json` under
|
|
5
|
-
`hooks.Stop`. The installer prompts you before doing this; default is
|
|
5
|
+
`hooks.Stop`. The installer prompts you before doing this; default is
|
|
6
|
+
yes.
|
|
6
7
|
|
|
7
8
|
## What the hook does
|
|
8
9
|
|
|
9
|
-
On every Claude Code session close, Claude Code fires its `Stop` hook
|
|
10
|
-
a JSON payload on stdin:
|
|
10
|
+
On every Claude Code session close, Claude Code fires its `Stop` hook
|
|
11
|
+
with a JSON payload on stdin:
|
|
11
12
|
|
|
12
13
|
```json
|
|
13
|
-
{ "transcript_path": "/path/to/session
|
|
14
|
+
{ "transcript_path": "/path/to/session.jsonl", "cwd": "/path/where/you/were/working", "session_id": "..." }
|
|
14
15
|
```
|
|
15
16
|
|
|
16
17
|
The hook:
|
|
17
18
|
|
|
18
|
-
1. Skips transcripts smaller than 5 KB (no signal in tiny sessions
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
19
|
+
1. Skips transcripts smaller than 5 KB (no signal in tiny sessions —
|
|
20
|
+
override via `TERMDECK_HOOK_MIN_BYTES`).
|
|
21
|
+
2. Validates env vars (`SUPABASE_URL`, `SUPABASE_SERVICE_KEY`,
|
|
22
|
+
`OPENAI_API_KEY`); if any are missing, logs the missing list and
|
|
23
|
+
exits cleanly without blocking the session close.
|
|
24
|
+
3. Detects the project from `cwd` against a built-in regex table; falls
|
|
25
|
+
back to `"global"` when nothing matches. **The default table is
|
|
26
|
+
intentionally empty** — see "Customizing the project map" below to
|
|
27
|
+
add your own entries.
|
|
28
|
+
4. Builds a coarse session summary from the last ~30 messages of the
|
|
29
|
+
transcript (~7 KB cap to stay inside OpenAI's embedding-input
|
|
30
|
+
budget).
|
|
31
|
+
5. Embeds the summary via OpenAI `text-embedding-3-small` (1,536-dim).
|
|
32
|
+
6. POSTs **one row** to Supabase `/rest/v1/memory_items` with
|
|
33
|
+
`source_type='session_summary'`.
|
|
34
|
+
7. Logs every step to `~/.claude/hooks/memory-hook.log`.
|
|
35
|
+
|
|
36
|
+
The hook is **fail-soft**: any error (network, parse, env-var-missing,
|
|
37
|
+
malformed transcript) is logged and the hook exits 0. Claude Code's
|
|
38
|
+
session close is never blocked.
|
|
39
|
+
|
|
40
|
+
## Required environment
|
|
41
|
+
|
|
42
|
+
The hook needs three env vars at run time:
|
|
43
|
+
|
|
44
|
+
| Var | What | How to set |
|
|
45
|
+
|---|---|---|
|
|
46
|
+
| `SUPABASE_URL` | Your Supabase project URL (e.g. `https://abc.supabase.co`) | `~/.termdeck/secrets.env` (Tier 2) |
|
|
47
|
+
| `SUPABASE_SERVICE_KEY` | Service-role key with INSERT on `memory_items`. **Not the anon key.** | `~/.termdeck/secrets.env` |
|
|
48
|
+
| `OPENAI_API_KEY` | OpenAI key for embedding inference | `~/.termdeck/secrets.env` or your shell |
|
|
49
|
+
|
|
50
|
+
Claude Code propagates the parent shell's environment into hook
|
|
51
|
+
processes, so anything in your shell init or
|
|
52
|
+
`~/.termdeck/secrets.env` (sourced by `scripts/start.sh` /
|
|
53
|
+
`npx @jhizzard/termdeck`) is visible to the hook.
|
|
54
|
+
|
|
55
|
+
If any of the three is missing the log line will name them:
|
|
24
56
|
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
57
|
+
```
|
|
58
|
+
[2026-04-27T21:30:00.000Z] env-var-missing: OPENAI_API_KEY — set these in ~/.termdeck/secrets.env or your shell to enable Mnestra ingestion. Skipping.
|
|
59
|
+
```
|
|
28
60
|
|
|
29
|
-
##
|
|
61
|
+
## Customizing the project map
|
|
30
62
|
|
|
31
|
-
The hook
|
|
63
|
+
The hook ships with an **empty `PROJECT_MAP`** by default — every
|
|
64
|
+
session lands under `project: 'global'` until you add entries. To add
|
|
65
|
+
your own:
|
|
32
66
|
|
|
33
|
-
|
|
34
|
-
|
|
67
|
+
1. Open `~/.claude/hooks/memory-session-end.js` after the installer
|
|
68
|
+
has dropped it.
|
|
69
|
+
2. Find the `PROJECT_MAP` array near the top of the file.
|
|
70
|
+
3. Add one entry per project; each entry is `{ pattern, project }`
|
|
71
|
+
where `pattern` is a regex matched against `cwd`:
|
|
72
|
+
|
|
73
|
+
```js
|
|
74
|
+
const PROJECT_MAP = [
|
|
75
|
+
{ pattern: /\/PVB\//i, project: 'pvb' },
|
|
76
|
+
{ pattern: /\/my-startup\//i, project: 'my-startup' },
|
|
77
|
+
{ pattern: /chopin-nashville/i, project: 'chopin-nashville' },
|
|
78
|
+
];
|
|
35
79
|
```
|
|
36
80
|
|
|
37
|
-
|
|
81
|
+
First match wins. Iteration order is array order, so put more specific
|
|
82
|
+
patterns first. Anything that doesn't match falls through to
|
|
83
|
+
`'global'`.
|
|
38
84
|
|
|
39
|
-
|
|
40
|
-
|
|
85
|
+
The map is local-only — it's never sent to any service. Editing it
|
|
86
|
+
takes effect on the next Claude Code session close (no restart
|
|
87
|
+
needed).
|
|
41
88
|
|
|
42
|
-
|
|
43
|
-
fact and exits cleanly.** It does not error, does not block session
|
|
44
|
-
close, and does not leak a spawn. Fresh users who installed the stack
|
|
45
|
-
but do not have `rag-system` checked out will see this skip-message in
|
|
46
|
-
the log and nothing else — as if no hook were installed.
|
|
89
|
+
## Coexistence with Joshua's `rag-system` hook
|
|
47
90
|
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
91
|
+
If you have Joshua's private `rag-system` repo and his rag-system-based
|
|
92
|
+
session hook installed, this bundled hook and that one can coexist:
|
|
93
|
+
|
|
94
|
+
- The bundled hook writes `source_type='session_summary'` — one row
|
|
95
|
+
per session, summary-only.
|
|
96
|
+
- The `rag-system` hook writes `source_type='fact'` — multiple rows
|
|
97
|
+
per session via Claude Haiku fact extraction + dedup.
|
|
98
|
+
|
|
99
|
+
Different `source_type` values mean the two paths don't dedup against
|
|
100
|
+
each other. If both are installed at the same path
|
|
101
|
+
(`~/.claude/hooks/memory-session-end.js`) the installer will prompt
|
|
102
|
+
before overwriting; choose accordingly.
|
|
52
103
|
|
|
53
104
|
## How to disable
|
|
54
105
|
|
|
55
106
|
Two options:
|
|
56
107
|
|
|
57
|
-
1. Edit `~/.claude/settings.json` and remove the entry under
|
|
58
|
-
that references `memory-session-end.js`. Leave the
|
|
59
|
-
simply won't fire.
|
|
60
|
-
2. Or delete `~/.claude/hooks/memory-session-end.js`
|
|
108
|
+
1. Edit `~/.claude/settings.json` and remove the entry under
|
|
109
|
+
`hooks.Stop` that references `memory-session-end.js`. Leave the
|
|
110
|
+
file in place; it simply won't fire.
|
|
111
|
+
2. Or delete `~/.claude/hooks/memory-session-end.js` AND remove the
|
|
61
112
|
`settings.json` entry. (Removing only the file leaves a broken
|
|
62
|
-
`command` in settings — Claude Code will log a missing-file error
|
|
63
|
-
every session close.)
|
|
113
|
+
`command` in settings — Claude Code will log a missing-file error
|
|
114
|
+
on every session close.)
|
|
115
|
+
|
|
116
|
+
Re-running `npx @jhizzard/termdeck-stack` after disabling will
|
|
117
|
+
re-prompt to install. Decline at the prompt to stay opted out.
|
|
118
|
+
|
|
119
|
+
## Optional flags
|
|
64
120
|
|
|
65
|
-
|
|
66
|
-
|
|
121
|
+
| Env var | Effect |
|
|
122
|
+
|---|---|
|
|
123
|
+
| `TERMDECK_HOOK_DEBUG=1` | Verbose `[debug]` lines in the log |
|
|
124
|
+
| `TERMDECK_HOOK_MIN_BYTES=10000` | Override the 5 KB skip threshold |
|
|
67
125
|
|
|
68
126
|
## Log file
|
|
69
127
|
|
|
70
|
-
`~/.claude/hooks/memory-hook.log` accumulates one line per session
|
|
71
|
-
event. The hook never rotates it. If it
|
|
72
|
-
|
|
128
|
+
`~/.claude/hooks/memory-hook.log` accumulates one line per session
|
|
129
|
+
event (skips, errors, ingests). The hook never rotates it. If it
|
|
130
|
+
grows unwieldy you can truncate it
|
|
131
|
+
(`: > ~/.claude/hooks/memory-hook.log`) without affecting hook
|
|
73
132
|
behavior.
|
|
@@ -1,53 +1,64 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* TermDeck session-end memory hook.
|
|
2
|
+
* TermDeck session-end memory hook (Mnestra-direct, no rag-system dependency).
|
|
3
3
|
*
|
|
4
|
-
* Vendored
|
|
5
|
-
*
|
|
6
|
-
* into ~/.claude/settings.json under hooks.Stop. Fires on every Claude Code
|
|
7
|
-
* session close.
|
|
4
|
+
* Vendored into ~/.claude/hooks/memory-session-end.js by @jhizzard/termdeck-stack.
|
|
5
|
+
* Wired into ~/.claude/settings.json under hooks.Stop. Fires on Claude Code Stop event.
|
|
8
6
|
*
|
|
9
7
|
* Behavior:
|
|
10
|
-
* 1. Reads {transcript_path, cwd} from stdin (Claude Code
|
|
11
|
-
* 2. Skips small transcripts (<5KB).
|
|
12
|
-
* 3.
|
|
13
|
-
* 4.
|
|
14
|
-
*
|
|
8
|
+
* 1. Reads {transcript_path, cwd, session_id} from stdin (Claude Code Stop payload).
|
|
9
|
+
* 2. Skips small transcripts (< MIN_TRANSCRIPT_BYTES, default 5KB).
|
|
10
|
+
* 3. Validates env vars; logs and exits cleanly if any required key is missing.
|
|
11
|
+
* 4. Detects project from cwd against PROJECT_MAP (else "global"). Extend the
|
|
12
|
+
* map by editing the array below — see assets/hooks/README.md for guidance.
|
|
13
|
+
* 5. Builds a coarse session summary from the transcript (last ~30 message excerpts).
|
|
14
|
+
* 6. Embeds the summary via OpenAI text-embedding-3-small.
|
|
15
|
+
* 7. POSTs ONE row to Supabase /rest/v1/memory_items with source_type='session_summary'.
|
|
16
|
+
* 8. Logs every step to ~/.claude/hooks/memory-hook.log.
|
|
15
17
|
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
18
|
+
* Required env vars (validated at entry):
|
|
19
|
+
* - SUPABASE_URL e.g. https://luvvbrpaopnblvxdxwzb.supabase.co
|
|
20
|
+
* - SUPABASE_SERVICE_KEY service-role key (NOT the anon key — needs INSERT on memory_items)
|
|
21
|
+
* - OPENAI_API_KEY sk-... for text-embedding-3-small
|
|
19
22
|
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
+
* Optional:
|
|
24
|
+
* - TERMDECK_HOOK_DEBUG=1 verbose logging
|
|
25
|
+
* - TERMDECK_HOOK_MIN_BYTES=5000 transcript size threshold
|
|
26
|
+
*
|
|
27
|
+
* Fail-soft contract: any error (network, parse, env-var-missing, malformed transcript)
|
|
28
|
+
* logs and exits 0. Never blocks Claude Code session close.
|
|
29
|
+
*
|
|
30
|
+
* Co-existence with Joshua's personal rag-system hook: this bundled hook writes
|
|
31
|
+
* source_type='session_summary' (one row per session). Joshua's personal hook
|
|
32
|
+
* writes source_type='fact' (multiple rows from extractFacts pipeline). Different
|
|
33
|
+
* source_types coexist in memory_items without dedup collisions.
|
|
23
34
|
*/
|
|
24
35
|
|
|
25
|
-
|
|
26
|
-
|
|
36
|
+
'use strict';
|
|
37
|
+
|
|
38
|
+
const { existsSync, statSync, appendFileSync, readFileSync } = require('fs');
|
|
27
39
|
const { join } = require('path');
|
|
28
40
|
const os = require('os');
|
|
29
41
|
|
|
30
|
-
const RAG_DIR = process.env.TERMDECK_RAG_DIR
|
|
31
|
-
|| join(os.homedir(), 'Documents', 'Graciella', 'rag-system');
|
|
32
|
-
const PROCESS_SCRIPT = join(RAG_DIR, 'src', 'scripts', 'process-session.ts');
|
|
33
42
|
const LOG_FILE = join(os.homedir(), '.claude', 'hooks', 'memory-hook.log');
|
|
34
43
|
|
|
44
|
+
// PROJECT_MAP — minimal default. Users extend by adding entries to this array.
|
|
45
|
+
// Patterns match against the cwd reported by Claude Code at Stop time.
|
|
46
|
+
// First match wins; falls through to "global".
|
|
35
47
|
const PROJECT_MAP = [
|
|
36
|
-
|
|
37
|
-
{ pattern:
|
|
38
|
-
{ pattern: /
|
|
39
|
-
{ pattern: /rag-system/i, project: 'rag-system' },
|
|
40
|
-
{ pattern: /PianoCameraAI/i, project: 'piano-camera' },
|
|
41
|
-
{ pattern: /Practice Piano Network/i, project: 'ppn' },
|
|
42
|
-
{ pattern: /StanczakJosh/i, project: 'stanczak' },
|
|
43
|
-
{ pattern: /JoshIzPiano/i, project: 'joshizpiano' },
|
|
44
|
-
{ pattern: /AutumnArtist/i, project: 'autumn-artist' },
|
|
45
|
-
{ pattern: /Crosswords/i, project: 'crosswords' },
|
|
46
|
-
{ pattern: /gorgias/i, project: 'gorgias' },
|
|
47
|
-
{ pattern: /imessage-reader/i, project: 'imessage-reader' },
|
|
48
|
-
{ pattern: /antigravity/i, project: 'antigravity' },
|
|
48
|
+
// Example entries — uncomment + edit, or add your own:
|
|
49
|
+
// { pattern: /\/myproject\//i, project: 'my-project' },
|
|
50
|
+
// { pattern: /work-stuff/i, project: 'work' },
|
|
49
51
|
];
|
|
50
52
|
|
|
53
|
+
const MIN_TRANSCRIPT_BYTES = parseInt(process.env.TERMDECK_HOOK_MIN_BYTES || '5000', 10);
|
|
54
|
+
const DEBUG = process.env.TERMDECK_HOOK_DEBUG === '1';
|
|
55
|
+
|
|
56
|
+
function log(msg) {
|
|
57
|
+
try { appendFileSync(LOG_FILE, `[${new Date().toISOString()}] ${msg}\n`); }
|
|
58
|
+
catch (_) { /* fail-soft */ }
|
|
59
|
+
}
|
|
60
|
+
function debug(msg) { if (DEBUG) log(`[debug] ${msg}`); }
|
|
61
|
+
|
|
51
62
|
function detectProject(cwd) {
|
|
52
63
|
for (const { pattern, project } of PROJECT_MAP) {
|
|
53
64
|
if (pattern.test(cwd)) return project;
|
|
@@ -55,59 +66,173 @@ function detectProject(cwd) {
|
|
|
55
66
|
return 'global';
|
|
56
67
|
}
|
|
57
68
|
|
|
58
|
-
function
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
69
|
+
function readEnv() {
|
|
70
|
+
const required = ['SUPABASE_URL', 'SUPABASE_SERVICE_KEY', 'OPENAI_API_KEY'];
|
|
71
|
+
const missing = required.filter((k) => !process.env[k]);
|
|
72
|
+
if (missing.length) {
|
|
73
|
+
log(`env-var-missing: ${missing.join(', ')} — set these in ~/.termdeck/secrets.env or your shell to enable Mnestra ingestion. Skipping.`);
|
|
74
|
+
return null;
|
|
75
|
+
}
|
|
76
|
+
return {
|
|
77
|
+
supabaseUrl: process.env.SUPABASE_URL.replace(/\/$/, ''),
|
|
78
|
+
supabaseKey: process.env.SUPABASE_SERVICE_KEY,
|
|
79
|
+
openaiKey: process.env.OPENAI_API_KEY,
|
|
80
|
+
};
|
|
62
81
|
}
|
|
63
82
|
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
try {
|
|
69
|
-
const data = JSON.parse(input);
|
|
70
|
-
const transcriptPath = data.transcript_path;
|
|
71
|
-
const cwd = data.cwd || '';
|
|
83
|
+
function buildSummary(transcriptPath) {
|
|
84
|
+
let raw;
|
|
85
|
+
try { raw = readFileSync(transcriptPath, 'utf8'); }
|
|
86
|
+
catch (e) { log(`read-transcript-failed: ${e.message}`); return null; }
|
|
72
87
|
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
88
|
+
const lines = raw.split('\n').filter(Boolean);
|
|
89
|
+
const messages = [];
|
|
90
|
+
for (const line of lines) {
|
|
91
|
+
let msg;
|
|
92
|
+
try { msg = JSON.parse(line); } catch (_) { continue; }
|
|
93
|
+
const role = msg?.message?.role;
|
|
94
|
+
if (role !== 'user' && role !== 'assistant') continue;
|
|
95
|
+
const content = msg.message.content;
|
|
96
|
+
let text = '';
|
|
97
|
+
if (typeof content === 'string') text = content;
|
|
98
|
+
else if (Array.isArray(content)) {
|
|
99
|
+
text = content.filter((c) => c && c.type === 'text').map((c) => c.text).join(' ');
|
|
76
100
|
}
|
|
101
|
+
if (text) messages.push({ role, content: text.slice(0, 400) });
|
|
102
|
+
}
|
|
77
103
|
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
return;
|
|
83
|
-
}
|
|
84
|
-
} catch (e) {
|
|
85
|
-
log(`Cannot stat transcript: ${transcriptPath} — ${e.message}`);
|
|
86
|
-
return;
|
|
87
|
-
}
|
|
104
|
+
if (messages.length < 5) {
|
|
105
|
+
debug(`session-too-short: ${messages.length} messages, skipping`);
|
|
106
|
+
return null;
|
|
107
|
+
}
|
|
88
108
|
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
109
|
+
const tail = messages.slice(-30);
|
|
110
|
+
const summary =
|
|
111
|
+
`Session with ${messages.length} messages.\n\n` +
|
|
112
|
+
tail.map((m) => `[${m.role}] ${m.content}`).join('\n');
|
|
113
|
+
// OpenAI text-embedding-3-small accepts up to 8192 tokens (~32K chars).
|
|
114
|
+
// 7000 chars is a safe headroom that survives multibyte expansion.
|
|
115
|
+
return summary.slice(0, 7000);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
async function embedText(text, openaiKey) {
|
|
119
|
+
try {
|
|
120
|
+
const res = await fetch('https://api.openai.com/v1/embeddings', {
|
|
121
|
+
method: 'POST',
|
|
122
|
+
headers: {
|
|
123
|
+
'Content-Type': 'application/json',
|
|
124
|
+
'Authorization': `Bearer ${openaiKey}`,
|
|
125
|
+
},
|
|
126
|
+
body: JSON.stringify({ model: 'text-embedding-3-small', input: text }),
|
|
127
|
+
});
|
|
128
|
+
if (!res.ok) {
|
|
129
|
+
const body = await res.text().catch(() => '');
|
|
130
|
+
log(`openai-embed-failed: HTTP ${res.status} ${body.slice(0, 200)}`);
|
|
131
|
+
return null;
|
|
92
132
|
}
|
|
133
|
+
const data = await res.json();
|
|
134
|
+
return data?.data?.[0]?.embedding || null;
|
|
135
|
+
} catch (e) {
|
|
136
|
+
log(`openai-embed-exception: ${e.message}`);
|
|
137
|
+
return null;
|
|
138
|
+
}
|
|
139
|
+
}
|
|
93
140
|
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
141
|
+
async function postMemoryItem({ supabaseUrl, supabaseKey, content, embedding, project, sessionId }) {
|
|
142
|
+
try {
|
|
143
|
+
const res = await fetch(`${supabaseUrl}/rest/v1/memory_items`, {
|
|
144
|
+
method: 'POST',
|
|
145
|
+
headers: {
|
|
146
|
+
'Content-Type': 'application/json',
|
|
147
|
+
'apikey': supabaseKey,
|
|
148
|
+
'Authorization': `Bearer ${supabaseKey}`,
|
|
149
|
+
'Prefer': 'return=minimal',
|
|
150
|
+
},
|
|
151
|
+
body: JSON.stringify({
|
|
152
|
+
content,
|
|
153
|
+
embedding: `[${embedding.join(',')}]`,
|
|
154
|
+
source_type: 'session_summary',
|
|
155
|
+
category: 'workflow',
|
|
156
|
+
project,
|
|
157
|
+
source_session_id: sessionId || null,
|
|
158
|
+
}),
|
|
159
|
+
});
|
|
160
|
+
if (!res.ok) {
|
|
161
|
+
const body = await res.text().catch(() => '');
|
|
162
|
+
log(`supabase-insert-failed: HTTP ${res.status} ${body.slice(0, 200)}`);
|
|
163
|
+
return false;
|
|
164
|
+
}
|
|
165
|
+
return true;
|
|
110
166
|
} catch (e) {
|
|
111
|
-
log(`
|
|
167
|
+
log(`supabase-insert-exception: ${e.message}`);
|
|
168
|
+
return false;
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
async function processStdinPayload(input) {
|
|
173
|
+
let data;
|
|
174
|
+
try { data = JSON.parse(input); }
|
|
175
|
+
catch (e) { log(`parse-stdin-failed: ${e.message}`); return; }
|
|
176
|
+
|
|
177
|
+
const transcriptPath = data.transcript_path;
|
|
178
|
+
const cwd = data.cwd || '';
|
|
179
|
+
const sessionId =
|
|
180
|
+
data.session_id ||
|
|
181
|
+
(transcriptPath ? transcriptPath.split('/').pop().replace('.jsonl', '') : null);
|
|
182
|
+
|
|
183
|
+
if (!transcriptPath) { log('no-transcript-path: skipping'); return; }
|
|
184
|
+
|
|
185
|
+
let stat;
|
|
186
|
+
try { stat = statSync(transcriptPath); }
|
|
187
|
+
catch (e) { log(`cannot-stat-transcript: ${transcriptPath} — ${e.message}`); return; }
|
|
188
|
+
|
|
189
|
+
if (stat.size < MIN_TRANSCRIPT_BYTES) {
|
|
190
|
+
debug(`small-transcript: ${stat.size} bytes < ${MIN_TRANSCRIPT_BYTES}, skipping`);
|
|
191
|
+
return;
|
|
112
192
|
}
|
|
113
|
-
|
|
193
|
+
|
|
194
|
+
const env = readEnv();
|
|
195
|
+
if (!env) return;
|
|
196
|
+
|
|
197
|
+
const project = detectProject(cwd);
|
|
198
|
+
debug(`project="${project}", session=${sessionId}`);
|
|
199
|
+
|
|
200
|
+
const summary = buildSummary(transcriptPath);
|
|
201
|
+
if (!summary) return;
|
|
202
|
+
|
|
203
|
+
const embedding = await embedText(summary, env.openaiKey);
|
|
204
|
+
if (!embedding) return;
|
|
205
|
+
|
|
206
|
+
const ok = await postMemoryItem({
|
|
207
|
+
supabaseUrl: env.supabaseUrl,
|
|
208
|
+
supabaseKey: env.supabaseKey,
|
|
209
|
+
content: summary,
|
|
210
|
+
embedding,
|
|
211
|
+
project,
|
|
212
|
+
sessionId,
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
if (ok) log(`ingested: project="${project}" session=${sessionId} bytes=${summary.length}`);
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
// Module-export contract for testability. When run as a script (require.main === module),
|
|
219
|
+
// read stdin and process. When require()d (tests), expose helpers.
|
|
220
|
+
if (require.main === module) {
|
|
221
|
+
let input = '';
|
|
222
|
+
process.stdin.setEncoding('utf8');
|
|
223
|
+
process.stdin.on('data', (chunk) => { input += chunk; });
|
|
224
|
+
process.stdin.on('end', () => {
|
|
225
|
+
processStdinPayload(input).catch((e) => log(`hook-error: ${e.message}`));
|
|
226
|
+
});
|
|
227
|
+
} else {
|
|
228
|
+
module.exports = {
|
|
229
|
+
PROJECT_MAP,
|
|
230
|
+
detectProject,
|
|
231
|
+
readEnv,
|
|
232
|
+
buildSummary,
|
|
233
|
+
embedText,
|
|
234
|
+
postMemoryItem,
|
|
235
|
+
processStdinPayload,
|
|
236
|
+
LOG_FILE,
|
|
237
|
+
};
|
|
238
|
+
}
|
package/package.json
CHANGED