claude-session-continuity-mcp 1.15.0 → 1.16.0

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
@@ -1,13 +1,14 @@
1
- # claude-session-continuity-mcp (v1.13.2)
1
+ # claude-session-continuity-mcp
2
2
 
3
- > **Zero Re-explanation Session Continuity for Claude Code**Automatic context capture + semantic search + auto error→solution pipeline
3
+ > **Never re-explain your project to Claude again.** 100% local session memory for Claude Code — auto context injection, semantic search, and error→solution recall. Zero config, zero API cost.
4
4
 
5
5
  [![npm version](https://img.shields.io/npm/v/claude-session-continuity-mcp.svg)](https://www.npmjs.com/package/claude-session-continuity-mcp)
6
+ [![npm downloads](https://img.shields.io/npm/dm/claude-session-continuity-mcp.svg)](https://www.npmjs.com/package/claude-session-continuity-mcp)
6
7
  [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
7
- [![Tests](https://img.shields.io/badge/tests-111%20passed-brightgreen.svg)]()
8
- [![Node](https://img.shields.io/badge/node-%3E%3D18.0.0-brightgreen.svg)]()
9
8
  [![claude-session-continuity-mcp MCP server](https://glama.ai/mcp/servers/leesgit/claude-session-continuity-mcp/badges/card.svg)](https://glama.ai/mcp/servers/leesgit/claude-session-continuity-mcp)
10
9
 
10
+ ![Session continuity demo — Claude auto-restores your project context on session start](assets/demo.gif)
11
+
11
12
  ## The Problem
12
13
 
13
14
  Every new Claude Code session:
@@ -62,6 +63,42 @@ Every new Claude Code session:
62
63
 
63
64
  ---
64
65
 
66
+ ## Why this over other memory tools?
67
+
68
+ Most Claude memory tools rely on **explicit tool calls** ("remember this"), a **cloud API**, or a **background AI worker**. This one is deliberately different:
69
+
70
+ | | claude-session-continuity-mcp | Typical cloud/AI-memory MCP |
71
+ |---|---|---|
72
+ | **Setup** | `npm i -g` → hooks auto-install | Manual server + API key |
73
+ | **Trigger** | 5 automatic hooks (no commands) | You call a `remember` tool |
74
+ | **Storage** | 100% local SQLite | Cloud / external service |
75
+ | **API cost** | **$0** — local embeddings | Per-token / subscription |
76
+ | **Latency** | < 5ms (on-device) | Network round-trip |
77
+ | **Privacy** | Never leaves your machine | Sent to a provider |
78
+ | **Search** | FTS5 + local semantic, KO/EN/JA cross-lingual | Varies |
79
+
80
+ If you want zero-config, offline, no-cost memory that just *happens* while you work — this is it.
81
+
82
+ ---
83
+
84
+ ## Works with Codex CLI too (v1.16.0+)
85
+
86
+ Beyond Claude Code, this also supports **OpenAI Codex CLI**. If `~/.codex` exists,
87
+ the installer registers the same hooks in `~/.codex/hooks.json` (SessionStart,
88
+ UserPromptSubmit, PreCompact, Stop), and the hooks auto-detect the host and emit
89
+ the right output format (Codex's `hookSpecificOutput.additionalContext`).
90
+
91
+ The same local `sessions.db` is shared, so context carries across both agents:
92
+ what you did in Codex is available in Claude Code and vice versa.
93
+
94
+ **Scope:** session save + context injection work on both. Codex file-change
95
+ tracking (PostToolUse) isn't wired yet — session save already covers most of it
96
+ via transcript parsing. Codex's `transcript_path` is treated as an unstable
97
+ interface (it can be null at startup), so host detection uses an installer-injected
98
+ `--codex` marker rather than relying on the path.
99
+
100
+ ---
101
+
65
102
  ## Quick Start
66
103
 
67
104
  ### Recommended: Global Installation
@@ -635,3 +672,11 @@ PRs welcome! Please:
635
672
 
636
673
  - [Model Context Protocol](https://modelcontextprotocol.io/) by Anthropic
637
674
  - [Xenova Transformers](https://github.com/xenova/transformers.js) for embeddings
675
+
676
+ ---
677
+
678
+ <div align="center">
679
+
680
+ **If this saves you from re-explaining your project, consider giving it a ⭐ — it genuinely helps others find it.**
681
+
682
+ </div>
@@ -16,6 +16,9 @@ const CLAUDE_DIR = path.join(os.homedir(), '.claude');
16
16
  const SETTINGS_FILE = path.join(CLAUDE_DIR, 'settings.json');
17
17
  const LEGACY_SETTINGS_FILE = path.join(CLAUDE_DIR, 'settings.local.json');
18
18
  const MCP_CONFIG_FILE = path.join(os.homedir(), '.claude.json');
19
+ // Codex CLI (2026-07-09): hooks register in ~/.codex/hooks.json (same JSON shape as Claude)
20
+ const CODEX_DIR = path.join(os.homedir(), '.codex');
21
+ const CODEX_HOOKS_FILE = path.join(CODEX_DIR, 'hooks.json');
19
22
  // 설치된 패키지 경로 찾기
20
23
  function getPackagePath() {
21
24
  // 1. 글로벌 설치 확인
@@ -123,6 +126,44 @@ function installMcpServer() {
123
126
  return false;
124
127
  }
125
128
  }
129
+ /**
130
+ * Register the same hooks in ~/.codex/hooks.json for OpenAI Codex CLI (2026-07-09).
131
+ * Only runs if ~/.codex exists (Codex installed). Preserves user's existing hooks;
132
+ * replaces only ours (matched by the claude-hook- command prefix).
133
+ */
134
+ function installCodexHooks() {
135
+ if (!fs.existsSync(CODEX_DIR))
136
+ return; // Codex not installed -> skip silently
137
+ const OUR_PREFIX = 'claude-hook-';
138
+ let hooksConfig = { hooks: {} };
139
+ if (fs.existsSync(CODEX_HOOKS_FILE)) {
140
+ try {
141
+ hooksConfig = JSON.parse(fs.readFileSync(CODEX_HOOKS_FILE, 'utf-8'));
142
+ }
143
+ catch {
144
+ hooksConfig = { hooks: {} };
145
+ }
146
+ }
147
+ const hooks = hooksConfig.hooks || {};
148
+ const merge = (event, ourEntries) => {
149
+ const existing = (hooks[event] || []);
150
+ const userEntries = existing.filter(e => !(e.hooks || []).some(h => h.command && h.command.includes(OUR_PREFIX)));
151
+ hooks[event] = [...userEntries, ...ourEntries];
152
+ };
153
+ // Codex uses the same event names as Claude (SessionStart/UserPromptSubmit/Stop...).
154
+ // Append "--codex" so hooks detect the host reliably: Codex passes transcript_path
155
+ // as null at SessionStart, so the argv marker is the only dependable signal.
156
+ merge('SessionStart', [{ hooks: [{ type: 'command', command: 'npm exec -- claude-hook-session-start --codex' }] }]);
157
+ merge('UserPromptSubmit', [{ hooks: [{ type: 'command', command: 'npm exec -- claude-hook-user-prompt --codex' }] }]);
158
+ merge('PreCompact', [{ hooks: [{ type: 'command', command: 'npm exec -- claude-hook-pre-compact --codex' }] }]);
159
+ merge('Stop', [{ hooks: [{ type: 'command', command: 'npm exec -- claude-hook-session-end --codex' }] }]);
160
+ hooksConfig.hooks = hooks;
161
+ try {
162
+ fs.writeFileSync(CODEX_HOOKS_FILE, JSON.stringify(hooksConfig, null, 2));
163
+ console.log('✅ Codex CLI hooks installed (~/.codex/hooks.json)');
164
+ }
165
+ catch { /* non-fatal: Codex hooks are optional */ }
166
+ }
126
167
  function install() {
127
168
  console.log('');
128
169
  console.log('╔════════════════════════════════════════════════════════════╗');
@@ -170,6 +211,9 @@ function install() {
170
211
  ]);
171
212
  settings.hooks = hooks;
172
213
  saveSettings(settings);
214
+ // Codex CLI hooks (2026-07-09): register the same hooks in ~/.codex/hooks.json
215
+ // if Codex is present. Hooks auto-detect the host and emit the right output format.
216
+ installCodexHooks();
173
217
  console.log('✅ Hooks installed (npm exec mode - works with local or global install!)');
174
218
  console.log(' SessionStart: context auto-load');
175
219
  console.log(' UserPromptSubmit: relevant memory injection');
@@ -224,6 +268,24 @@ function uninstall() {
224
268
  settings.hooks = hooks;
225
269
  }
226
270
  saveSettings(settings);
271
+ // Also remove our hooks from Codex (2026-07-09), preserving user's hooks.
272
+ if (fs.existsSync(CODEX_HOOKS_FILE)) {
273
+ try {
274
+ const cfg = JSON.parse(fs.readFileSync(CODEX_HOOKS_FILE, 'utf-8'));
275
+ const ch = cfg.hooks || {};
276
+ for (const event of ['SessionStart', 'UserPromptSubmit', 'PreCompact', 'Stop']) {
277
+ const existing = (ch[event] || []);
278
+ const remaining = existing.filter(e => !(e.hooks || []).some(h => h.command && h.command.includes(OUR_PREFIX)));
279
+ if (remaining.length === 0)
280
+ delete ch[event];
281
+ else
282
+ ch[event] = remaining;
283
+ }
284
+ cfg.hooks = ch;
285
+ fs.writeFileSync(CODEX_HOOKS_FILE, JSON.stringify(cfg, null, 2));
286
+ }
287
+ catch { /* non-fatal */ }
288
+ }
227
289
  console.log('✅ Hooks removed successfully!');
228
290
  }
229
291
  function status() {
@@ -14,7 +14,7 @@ import * as path from 'path';
14
14
  import * as readline from 'readline';
15
15
  import * as crypto from 'crypto';
16
16
  import Database from 'better-sqlite3';
17
- import { logHookError } from '../utils/logger.js';
17
+ import { logHookError, isCodexHost } from '../utils/logger.js';
18
18
  function detectWorkspaceRoot(cwd) {
19
19
  let current = cwd;
20
20
  const root = path.parse(current).root;
@@ -207,6 +207,8 @@ function parseUserText(entry) {
207
207
  }
208
208
  // system-reminder, local-command 태그 제거
209
209
  text = text.replace(/<system-reminder>[\s\S]*?<\/system-reminder>/g, '').trim();
210
+ // Codex support (2026-07-09): Codex injects <environment_context> into the first user message -> strip it
211
+ text = text.replace(/<environment_context>[\s\S]*?<\/environment_context>/g, '').trim();
210
212
  text = text.replace(/<local-command-caveat>[\s\S]*?<\/local-command-caveat>/g, '').trim();
211
213
  text = text.replace(/<command-name>[\s\S]*?<\/command-name>/g, '').trim();
212
214
  text = text.replace(/<command-message>[\s\S]*?<\/command-message>/g, '').trim();
@@ -237,6 +239,64 @@ function extractTextFromContent(content) {
237
239
  }
238
240
  return '';
239
241
  }
242
+ /**
243
+ * Codex support (2026-07-09): normalize a Codex CLI rollout JSONL line into the
244
+ * Claude transcript entry shape, so the rest of parseTranscriptSinglePass
245
+ * (role/content/timestamp/commit/user-request extraction) can be reused as-is.
246
+ *
247
+ * Codex format: {timestamp, type, payload}
248
+ * - payload.type=message, role=user/assistant, content[].text (input_text/output_text)
249
+ * - payload.type=function_call, arguments={command:["zsh","-lc","git commit..."]}
250
+ * - payload.type=session_meta (first line, ignored)
251
+ * - reasoning/token_count etc. are ignored
252
+ * Returns a Claude-shaped entry ({type, timestamp, message:{content:[...]}}) or null (skip).
253
+ */
254
+ function normalizeCodexLine(codexEntry) {
255
+ const p = codexEntry.payload;
256
+ if (!p)
257
+ return null;
258
+ const ts = codexEntry.timestamp;
259
+ // 1. user/assistant message -> text block
260
+ if (p.type === 'message' && (p.role === 'user' || p.role === 'assistant')) {
261
+ const text = (p.content || [])
262
+ .filter(c => c.type === 'input_text' || c.type === 'output_text' || c.type === 'text')
263
+ .map(c => c.text || '')
264
+ .join('\n');
265
+ if (!text)
266
+ return null;
267
+ return {
268
+ type: p.role === 'user' ? 'user' : 'assistant',
269
+ timestamp: ts,
270
+ message: { content: [{ type: 'text', text }] },
271
+ };
272
+ }
273
+ // 2. function_call(shell) -> tool_use block (for git commit extraction).
274
+ // Codex passes command as an argv array (["zsh","-lc","git commit -m ..."]).
275
+ if (p.type === 'function_call' && p.arguments) {
276
+ let cmd = '';
277
+ try {
278
+ const args = JSON.parse(p.arguments);
279
+ if (Array.isArray(args.command)) {
280
+ // Codex uses ["zsh","-lc","<actual cmd>"]. The commit regex requires
281
+ // "^git" or "&& git", so strip the shell wrapper (zsh/bash -lc/-c)
282
+ // and keep only the actual command.
283
+ const a = args.command;
284
+ cmd = (a.length >= 3 && /^(zsh|bash|sh)$/.test(a[0]) && /^-[lc]+$/.test(a[1]))
285
+ ? a.slice(2).join(' ')
286
+ : a.join(' ');
287
+ }
288
+ }
289
+ catch { /* arguments not JSON -> skip */ }
290
+ if (!cmd)
291
+ return null;
292
+ return {
293
+ type: 'assistant',
294
+ timestamp: ts,
295
+ message: { content: [{ type: 'tool_use', input: { command: cmd } }] },
296
+ };
297
+ }
298
+ return null; // skip reasoning/token_count/session_meta etc.
299
+ }
240
300
  /**
241
301
  * Single-Pass Transcript Parser
242
302
  * JSONL을 1회 스트림으로 읽으며 모든 데이터를 동시 추출
@@ -254,6 +314,8 @@ async function parseTranscriptSinglePass(transcriptPath) {
254
314
  };
255
315
  if (!transcriptPath || !fs.existsSync(transcriptPath))
256
316
  return result;
317
+ // Host detection: Codex uses ~/.codex/sessions/...rollout-*.jsonl, Claude uses ~/.claude/projects/...
318
+ const isCodex = isCodexHost(transcriptPath); // shared detection (argv marker or path)
257
319
  // commit 추출용 패턴
258
320
  const commitPatterns = [
259
321
  /git commit.*?-m\s*"\$\(cat <<'?EOF'?\n(.+?)(?:\n\n|\nCo-Authored|\nEOF)/s,
@@ -277,7 +339,11 @@ async function parseTranscriptSinglePass(transcriptPath) {
277
339
  if (!line.trim())
278
340
  continue;
279
341
  try {
280
- const entry = JSON.parse(line);
342
+ const rawEntry = JSON.parse(line);
343
+ // For Codex, normalize the rollout line into a Claude entry shape. Skipped lines (reasoning etc.) return null.
344
+ const entry = isCodex ? normalizeCodexLine(rawEntry) : rawEntry;
345
+ if (!entry)
346
+ continue;
281
347
  const role = entry.type || entry.role || '';
282
348
  const content = entry.message?.content;
283
349
  // === 0. Timestamp 추출 (세션 duration 계산용) ===
@@ -5,7 +5,7 @@
5
5
  import * as fs from 'fs';
6
6
  import * as path from 'path';
7
7
  import Database from 'better-sqlite3';
8
- import { logHookError } from '../utils/logger.js';
8
+ import { logHookError, emitContext, isCodexHost } from '../utils/logger.js';
9
9
  function detectWorkspaceRoot(cwd) {
10
10
  let current = cwd;
11
11
  const root = path.parse(current).root;
@@ -259,10 +259,13 @@ async function main() {
259
259
  const dbPath = path.join(workspaceRoot, '.claude', 'sessions.db');
260
260
  const context = loadContext(dbPath, project);
261
261
  if (context) {
262
- console.log(`\n<session-context project="${project}">\n${context}\n</session-context>\n`);
262
+ emitContext(`\n<session-context project="${project}">\n${context}\n</session-context>\n`, 'SessionStart', input.transcript_path);
263
263
  }
264
264
  else {
265
- console.log(`\n[Session] Project: ${project} (no context yet)\n`);
265
+ // Only Claude gets the "no context" placeholder; Codex would just get empty context.
266
+ if (!isCodexHost(input.transcript_path)) {
267
+ console.log(`\n[Session] Project: ${project} (no context yet)\n`);
268
+ }
266
269
  }
267
270
  process.exit(0);
268
271
  }
@@ -5,7 +5,7 @@
5
5
  import * as fs from 'fs';
6
6
  import * as path from 'path';
7
7
  import Database from 'better-sqlite3';
8
- import { logHookError } from '../utils/logger.js';
8
+ import { logHookError, emitContext } from '../utils/logger.js';
9
9
  function detectWorkspaceRoot(cwd) {
10
10
  let current = cwd;
11
11
  const root = path.parse(current).root;
@@ -83,6 +83,10 @@ const STOPWORDS = new Set([
83
83
  '하나', '둘', '셋', '정말', '진짜', '아주', '너무', '매우', '조금', '약간', '대충', '그냥', '일단',
84
84
  '그리고', '하지만', '그래서', '그래도', '근데', '그런데', '또는', '또한', '이런데', '그런데',
85
85
  '진행', '확인', '시작', '완료', '종료', '해줘', '해주세요', '부탁', '알려', '알려줘',
86
+ // 흔한 작업 동사·부사 (P1, 2026-07-08: 작은 코퍼스에서 df가 낮아 IDF를 통과해
87
+ // 오탐을 유발했음. "저장/기억/다시" 등이 GCP·영상 메모리를 엉뚱하게 소환)
88
+ '저장', '기억', '삭제', '수정', '검색', '등록', '변경', '추가', '제거', '생성', '실행', '설정',
89
+ '전체', '다시', '이렇게', '그렇게', '저렇게', '계속', '먼저', '바로', '같이', '제대로', '하나하나',
86
90
  // 범용 명사 (LIKE 검색 시 아무 메모리나 잡는 오탐원 — 2026-07-08 C3)
87
91
  '이름', '정보', '내용', '관련', '부분', '상태', '방법', '문제', '경우', '생각', '이야기', '얘기',
88
92
  '어떻게', '무엇', '뭐가', '왜', '어디', '언제', '어느', '어떤', '어떻', '얼마',
@@ -91,6 +95,11 @@ const STOPWORDS = new Set([
91
95
  // 영어 빈출 추가 — Next.js 같은 동음이의 발생하는 토큰
92
96
  'next', 'last', 'first', 'prev', 'previous', 'check', 'test', 'run', 'live', 'ready', 'verify', 'verification',
93
97
  'session', 'task', 'item', 'case', 'file', 'line', 'data', 'code', 'word', 'time', 'step', 'part', 'side',
98
+ // 영어 작업 동사 (P1 audit-7, 2026-07-08: 한국어 저장/기억/검색과 대칭. 이들이
99
+ // df 낮아 IDF 통과 시 오탐 유발. inCorpus 게이트가 사후 방어하나 명시 차단이 견고)
100
+ 'save', 'store', 'remember', 'search', 'find', 'delete', 'remove', 'update', 'create', 'add',
101
+ 'change', 'edit', 'fix', 'make', 'show', 'list', 'get', 'set', 'build', 'lint', 'deploy',
102
+ 'whole', 'thing', 'again', 'entire', 'stuff', 'something', 'anything', 'everything',
94
103
  // 영어 빈출 stopwords
95
104
  'the', 'a', 'an', 'is', 'are', 'was', 'were', 'be', 'been', 'being', 'have', 'has', 'had',
96
105
  'do', 'does', 'did', 'will', 'would', 'should', 'could', 'can', 'may', 'might', 'must',
@@ -120,6 +129,12 @@ function extractTriggerKeywords(prompt) {
120
129
  .split(/[\s,.!?;:/\\|]+/)
121
130
  .map(t => t.trim().toLowerCase())
122
131
  .filter(t => /^[a-z0-9가-힣]+$/i.test(t)) // 영숫자+한글만
132
+ // P1 (2026-07-08): 흔한 한국어 동사 어미만 벗겨 stopword 체크가 먹게 함.
133
+ // "저장해줘"→"저장"이 안 되면 STOPWORDS(저장)를 우회해 오탐.
134
+ // ⚠️ 단일 글자 조사(로/를/이/가 등)는 절삭 금지 — "경로→경","추가→추"처럼
135
+ // 진짜 2글자 명사를 훼손함(실측 확인). 명확한 다글자 동사 어미만.
136
+ .map(t => t.replace(/(해줘|해주세요|해주라|했어|했지|하는|하고|해서|합니다|드려|드릴게)$/, ''))
137
+ .filter(t => t.length > 0)
123
138
  .filter(t => {
124
139
  // 한국어 단일 음절은 거의 무의미, 2글자 이상 허용
125
140
  const hasHangul = /[가-힣]/.test(t);
@@ -154,7 +169,8 @@ function extractTriggerKeywords(prompt) {
154
169
  * 토큰("Next.js")에 의해 false positive 발생. df=0 토큰은 FTS5 매칭에
155
170
  * 기여 0이므로 trigger 조건 자체에서 제외하는 게 맞음.
156
171
  *
157
- * @returns 살아남은 키워드 (희소한 의미 토큰만)
172
+ * @returns 살아남은 키워드 + 각 df (P1 audit-7 2026-07-08: df를 함께 반환해
173
+ * searchTriggeredMemories의 inCorpus 재쿼리 제거)
158
174
  */
159
175
  function filterByIdf(db, keywords) {
160
176
  if (keywords.length === 0)
@@ -173,22 +189,22 @@ function filterByIdf(db, keywords) {
173
189
  // 예: 메모리에 "비번"으로 적혔지만 사용자가 "비밀번호" 입력 → df=0, 그러나
174
190
  // 같이 추출된 "서명키"가 매칭해주면 trigger 정상 작동
175
191
  if (df === 0) {
176
- survived.push(kw);
192
+ survived.push({ kw, df: 0 });
177
193
  continue;
178
194
  }
179
195
  const idf = Math.log(N / df);
180
196
  if (idf >= 2.0) {
181
- survived.push(kw);
197
+ survived.push({ kw, df });
182
198
  }
183
199
  }
184
200
  catch {
185
- survived.push(kw); // FTS5 구문 오류 시 보수적으로 살림
201
+ survived.push({ kw, df: -1 }); // FTS5 구문 오류 시 보수적으로 살림 (df 불명=-1)
186
202
  }
187
203
  }
188
204
  return survived;
189
205
  }
190
206
  catch {
191
- return keywords; // DF 측정 실패 시 원본 그대로
207
+ return keywords.map(kw => ({ kw, df: -1 })); // DF 측정 실패 시 원본 그대로
192
208
  }
193
209
  }
194
210
  /**
@@ -203,8 +219,17 @@ function searchTriggeredMemories(db, keywords, project) {
203
219
  const meaningful = filterByIdf(db, keywords);
204
220
  if (meaningful.length < 2)
205
221
  return []; // IDF 통과 토큰 2개 이상일 때만 trigger
222
+ // P1 (2026-07-08): 실제 코퍼스에 존재하는(df>0) 토큰이 2개 이상일 때만 trigger.
223
+ // filterByIdf가 df=0 토큰(코퍼스에 없음)을 살려주는데, 그게 length>=2 게이트를
224
+ // 우회시켜 실질 단일 토큰 매칭으로 trigger되던 오탐 차단.
225
+ // 예: "gmail draft로 저장" → gmail(df=1) + draft로(df=0) → 실매칭 1개인데 trigger됐음.
226
+ // audit-7 2026-07-08: filterByIdf가 반환한 df를 재사용(중복 쿼리 제거).
227
+ const inCorpus = meaningful.filter(m => m.df > 0);
228
+ if (inCorpus.length < 2)
229
+ return [];
230
+ const meaningfulKws = meaningful.map(m => m.kw);
206
231
  try {
207
- const ftsQuery = meaningful.map(k => `"${k.replace(/"/g, '""')}"`).join(' OR ');
232
+ const ftsQuery = meaningfulKws.map(k => `"${k.replace(/"/g, '""')}"`).join(' OR ');
208
233
  const projectFilter = project ? `AND (m.project = ? OR m.project IS NULL)` : '';
209
234
  const params = [ftsQuery];
210
235
  if (project)
@@ -239,7 +264,8 @@ function searchPastWork(db, keyword) {
239
264
  if (rawTokens.length === 0)
240
265
  return result;
241
266
  // IDF로 흔한 토큰 추가 제거. 남는 게 없으면(전부 흔함) 검색 안 함.
242
- const searchTokens = filterByIdf(db, rawTokens);
267
+ // audit-7 2026-07-08: filterByIdf가 {kw,df} 반환하도록 바뀜 → kw만 추출.
268
+ const searchTokens = filterByIdf(db, rawTokens).map(m => m.kw);
243
269
  if (searchTokens.length === 0)
244
270
  return result;
245
271
  // 1. sessions 검색 (최근 30일, 상위 3건) — 토큰 LIKE OR
@@ -456,7 +482,7 @@ async function main() {
456
482
  const pastSection = formatPastWork(pastWork);
457
483
  db.close();
458
484
  if (pastSection) {
459
- console.log(`\n<past-context project="${project}">\n${pastSection}\n</past-context>\n`);
485
+ emitContext(`\n<past-context project="${project}">\n${pastSection}\n</past-context>\n`, 'UserPromptSubmit', input.transcript_path);
460
486
  }
461
487
  }
462
488
  catch { /* ignore */ }
@@ -477,7 +503,7 @@ async function main() {
477
503
  const content = m.content.length > 120 ? m.content.slice(0, 120) + '...' : m.content;
478
504
  lines.push(`- [${m.memory_type}] ${content}`);
479
505
  }
480
- console.log(`\n<triggered-context project="${project ?? 'global'}" keywords="${triggerKws.join(',')}">\n${lines.join('\n')}\n</triggered-context>\n`);
506
+ emitContext(`\n<triggered-context project="${project ?? 'global'}" keywords="${triggerKws.join(',')}">\n${lines.join('\n')}\n</triggered-context>\n`, 'UserPromptSubmit', input.transcript_path);
481
507
  }
482
508
  }
483
509
  }
@@ -25,4 +25,21 @@ export declare const logger: Logger;
25
25
  * 로그 위치: $HOOK_ERROR_LOG > cwd/.claude/hook-errors.log > ~/.claude/hook-errors.log
26
26
  */
27
27
  export declare function logHookError(hook: string, err: unknown): void;
28
+ /**
29
+ * Detect whether the hook is running under OpenAI Codex CLI (vs Claude Code).
30
+ *
31
+ * Two signals (either is sufficient):
32
+ * 1. A "--codex" argv marker injected by our installer into ~/.codex/hooks.json.
33
+ * This is the reliable signal — Codex passes transcript_path as null at
34
+ * SessionStart (fresh session, rollout not yet written), so path alone fails.
35
+ * 2. Transcript path under ~/.codex/sessions/...rollout-*.jsonl (fallback for
36
+ * Stop/UserPromptSubmit where the rollout already exists).
37
+ */
38
+ export declare function isCodexHost(transcriptPath?: string): boolean;
39
+ /**
40
+ * Emit context to stdout in the host's expected format (Codex support, 2026-07-09).
41
+ * - Claude Code injects raw stdout text directly.
42
+ * - Codex CLI expects {hookSpecificOutput:{hookEventName, additionalContext}}.
43
+ */
44
+ export declare function emitContext(context: string, hookEventName: string, transcriptPath?: string): void;
28
45
  export {};
@@ -138,3 +138,35 @@ export function logHookError(hook, err) {
138
138
  }
139
139
  catch { /* never throw from the error logger */ }
140
140
  }
141
+ /**
142
+ * Detect whether the hook is running under OpenAI Codex CLI (vs Claude Code).
143
+ *
144
+ * Two signals (either is sufficient):
145
+ * 1. A "--codex" argv marker injected by our installer into ~/.codex/hooks.json.
146
+ * This is the reliable signal — Codex passes transcript_path as null at
147
+ * SessionStart (fresh session, rollout not yet written), so path alone fails.
148
+ * 2. Transcript path under ~/.codex/sessions/...rollout-*.jsonl (fallback for
149
+ * Stop/UserPromptSubmit where the rollout already exists).
150
+ */
151
+ export function isCodexHost(transcriptPath) {
152
+ if (process.argv.includes('--codex'))
153
+ return true;
154
+ if (!transcriptPath)
155
+ return false;
156
+ return transcriptPath.includes('/.codex/sessions/') || /rollout-.*\.jsonl$/.test(transcriptPath);
157
+ }
158
+ /**
159
+ * Emit context to stdout in the host's expected format (Codex support, 2026-07-09).
160
+ * - Claude Code injects raw stdout text directly.
161
+ * - Codex CLI expects {hookSpecificOutput:{hookEventName, additionalContext}}.
162
+ */
163
+ export function emitContext(context, hookEventName, transcriptPath) {
164
+ if (isCodexHost(transcriptPath)) {
165
+ process.stdout.write(JSON.stringify({
166
+ hookSpecificOutput: { hookEventName, additionalContext: context },
167
+ }));
168
+ }
169
+ else {
170
+ console.log(context);
171
+ }
172
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-session-continuity-mcp",
3
- "version": "1.15.0",
3
+ "version": "1.16.0",
4
4
  "description": "Session Continuity for Claude Code - Never re-explain your project again",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",