Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
0342ec3
MAESTRO: Add transcript parser for extracting thinking blocks
thedotmack Feb 8, 2026
2dd2d4c
MAESTRO: Add thoughts table with FTS5 search for thinking block storage
thedotmack Feb 8, 2026
f084389
MAESTRO: Add thought storage and retrieval methods to SessionStore
thedotmack Feb 8, 2026
c3f4220
MAESTRO: Add thoughts extraction handler for Stop hook integration
thedotmack Feb 8, 2026
b30ec90
MAESTRO: Add thoughts-extract subcommand to hook dispatch system
thedotmack Feb 8, 2026
d61faf6
MAESTRO: Add thoughts-extract subcommand to hook dispatch system
thedotmack Feb 8, 2026
9138932
MAESTRO: Add ThoughtsRoutes REST API for thoughts storage and retrieval
thedotmack Feb 8, 2026
7db7dad
MAESTRO: Add syncThought() method to ChromaSync for thought vector em…
thedotmack Feb 8, 2026
ce09d8b
MAESTRO: Add syncThoughts() batch method to ChromaSync for bulk thoug…
thedotmack Feb 8, 2026
3a11ee7
MAESTRO: Wire ChromaSync into ThoughtsRoutes POST endpoint for vector…
thedotmack Feb 8, 2026
448d422
MAESTRO: Add thought backfill to ensureBackfilled() for Chroma vector…
thedotmack Feb 8, 2026
5dbb250
MAESTRO: Fix thoughts table migration missing from SessionStore and d…
thedotmack Feb 8, 2026
5f3257d
MAESTRO: Integrate thoughts into unified search infrastructure across…
thedotmack Feb 10, 2026
3edb2c8
MAESTRO: Add broadcastThoughtStored SSE method for real-time thought …
thedotmack Feb 10, 2026
3c4d01f
MAESTRO: Wire broadcastThoughtStored SSE call into ThoughtsRoutes POS…
thedotmack Feb 10, 2026
6b79d4f
MAESTRO: Rebuild and deploy plugin with thoughts SSE broadcasting int…
thedotmack Feb 10, 2026
36a962e
MAESTRO: Add SSE broadcast and Chroma sync to SessionRoutes thoughts …
thedotmack Feb 10, 2026
67c7b63
Merge remote-tracking branch 'origin/main' into thoughts-feed
thedotmack Feb 13, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions plugin/hooks/hooks.json
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,11 @@
"command": "node \"${CLAUDE_PLUGIN_ROOT}/scripts/bun-runner.js\" \"${CLAUDE_PLUGIN_ROOT}/scripts/worker-service.cjs\" hook claude-code summarize",
"timeout": 120
},
{
"type": "command",
"command": "node \"${CLAUDE_PLUGIN_ROOT}/scripts/bun-runner.js\" \"${CLAUDE_PLUGIN_ROOT}/scripts/worker-service.cjs\" hook claude-code thoughts-extract",
"timeout": 60
},
{
"type": "command",
"command": "node \"${CLAUDE_PLUGIN_ROOT}/scripts/bun-runner.js\" \"${CLAUDE_PLUGIN_ROOT}/scripts/worker-service.cjs\" hook claude-code session-complete",
Expand Down
120 changes: 83 additions & 37 deletions plugin/scripts/context-generator.cjs

Large diffs are not rendered by default.

26 changes: 13 additions & 13 deletions plugin/scripts/mcp-server.cjs

Large diffs are not rendered by default.

572 changes: 306 additions & 266 deletions plugin/scripts/worker-service.cjs

Large diffs are not rendered by default.

4 changes: 4 additions & 0 deletions src/cli/handlers/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,12 +13,14 @@ import { summarizeHandler } from './summarize.js';
import { userMessageHandler } from './user-message.js';
import { fileEditHandler } from './file-edit.js';
import { sessionCompleteHandler } from './session-complete.js';
import { thoughtsExtractHandler } from './thoughts-extract.js';

export type EventType =
| 'context' // SessionStart - inject context
| 'session-init' // UserPromptSubmit - initialize session
| 'observation' // PostToolUse - save observation
| 'summarize' // Stop - generate summary (phase 1)
| 'thoughts-extract' // Stop - extract thinking blocks (phase 1.5)
| 'session-complete' // Stop - complete session (phase 2) - fixes #842
| 'user-message' // SessionStart (parallel) - display to user
| 'file-edit'; // Cursor afterFileEdit
Expand All @@ -28,6 +30,7 @@ const handlers: Record<EventType, EventHandler> = {
'session-init': sessionInitHandler,
'observation': observationHandler,
'summarize': summarizeHandler,
'thoughts-extract': thoughtsExtractHandler,
'session-complete': sessionCompleteHandler,
'user-message': userMessageHandler,
'file-edit': fileEditHandler
Expand Down Expand Up @@ -64,3 +67,4 @@ export { summarizeHandler } from './summarize.js';
export { userMessageHandler } from './user-message.js';
export { fileEditHandler } from './file-edit.js';
export { sessionCompleteHandler } from './session-complete.js';
export { thoughtsExtractHandler } from './thoughts-extract.js';
50 changes: 50 additions & 0 deletions src/cli/handlers/thoughts-extract.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
/**
* Thoughts Extract Handler - Stop (Phase 1.5)
*
* Extracts thinking blocks from the transcript and stores them via the worker API.
* Runs AFTER summarize and BEFORE session-complete in the Stop hook chain.
*/

import type { EventHandler, NormalizedHookInput, HookResult } from '../types.js';
import { handleThoughtsExtraction } from '../../hooks/handlers/thoughts.js';
import { logger } from '../../utils/logger.js';
import { HOOK_EXIT_CODES } from '../../shared/hook-constants.js';

export const thoughtsExtractHandler: EventHandler = {
async execute(input: NormalizedHookInput): Promise<HookResult> {
const { sessionId, transcriptPath } = input;

if (!transcriptPath) {
logger.warn('HOOK', 'thoughts-extract: Missing transcriptPath, skipping');
return { continue: true, suppressOutput: true, exitCode: HOOK_EXIT_CODES.SUCCESS };
}

if (!sessionId) {
logger.warn('HOOK', 'thoughts-extract: Missing sessionId, skipping');
return { continue: true, suppressOutput: true, exitCode: HOOK_EXIT_CODES.SUCCESS };
}

try {
// handleThoughtsExtraction extracts thinking blocks from the transcript
// and sends them to POST /api/sessions/thoughts on the worker.
// The worker endpoint resolves memorySessionId and project from contentSessionId.
const result = await handleThoughtsExtraction({
transcriptPath,
sessionId,
memorySessionId: '',
project: '',
});

logger.info('HOOK', `thoughts-extract: Stored ${result.thoughtsStored} thoughts`, {
contentSessionId: sessionId
});
} catch (error) {
// Log but don't fail - thoughts extraction should never block session completion
logger.warn('HOOK', 'thoughts-extract: Error extracting thoughts', {
error: (error as Error).message
});
}

return { continue: true, suppressOutput: true, exitCode: HOOK_EXIT_CODES.SUCCESS };
}
};
51 changes: 51 additions & 0 deletions src/hooks/handlers/thinking.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import { readFileSync, existsSync } from 'fs';
import { logger } from '../../utils/logger.js';
import type { ThinkingContent } from '../../types/transcript.js';

export interface ThinkingBlock {
thinking: string;
timestamp: number;
messageIndex: number;
}

/**
* Extract thinking blocks from a Claude Code JSONL transcript file.
* Reads line-by-line, parses each as JSON, and collects thinking content
* from assistant messages.
*/
export function extractThinkingBlocks(transcriptPath: string): ThinkingBlock[] {
if (!transcriptPath || !existsSync(transcriptPath)) {
return [];
}

const content = readFileSync(transcriptPath, 'utf-8').trim();
if (!content) {
return [];
}

const lines = content.split('\n');
const thinkingBlocks: ThinkingBlock[] = [];

lines.forEach((line, lineIndex) => {
try {
const entry = JSON.parse(line);

if (entry.type !== 'assistant') return;
if (!Array.isArray(entry.message?.content)) return;

for (const block of entry.message.content) {
if (block.type === 'thinking' && block.thinking) {
thinkingBlocks.push({
thinking: (block as ThinkingContent).thinking,
timestamp: entry.timestamp ? new Date(entry.timestamp).getTime() : Date.now(),
messageIndex: lineIndex,
});
}
}
} catch {
logger.debug('THINKING', 'Skipping malformed transcript line', { lineIndex, transcriptPath });
}
});

return thinkingBlocks;
}
84 changes: 84 additions & 0 deletions src/hooks/handlers/thoughts.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
import { extractThinkingBlocks } from './thinking.js';
import { fetchWithTimeout, getWorkerPort, ensureWorkerRunning } from '../../shared/worker-utils.js';
import { logger } from '../../utils/logger.js';
import { HOOK_TIMEOUTS, getTimeout } from '../../shared/hook-constants.js';
import type { ThoughtInput } from '../../services/sqlite/thoughts/types.js';

export interface ThoughtsExtractionInput {
transcriptPath: string;
sessionId: string;
memorySessionId: string;
project: string;
}

export interface ThoughtsExtractionResult {
thoughtsStored: number;
}

const THOUGHTS_TIMEOUT_MS = getTimeout(HOOK_TIMEOUTS.DEFAULT);

/**
* Extract thinking blocks from a transcript and store them via the worker API.
* Called during the Stop hook to persist Claude's internal reasoning.
*/
export async function handleThoughtsExtraction(
input: ThoughtsExtractionInput
): Promise<ThoughtsExtractionResult> {
const blocks = extractThinkingBlocks(input.transcriptPath);

if (blocks.length === 0) {
logger.debug('THOUGHTS', 'No thinking blocks found in transcript', {
transcriptPath: input.transcriptPath
});
return { thoughtsStored: 0 };
}

const thoughts: ThoughtInput[] = blocks.map(block => ({
thinking_text: block.thinking,
thinking_summary: null,
message_index: block.messageIndex,
}));

logger.info('THOUGHTS', `Extracted ${thoughts.length} thinking blocks, sending to worker`, {
contentSessionId: input.sessionId,
project: input.project
});

const workerReady = await ensureWorkerRunning();
if (!workerReady) {
logger.warn('THOUGHTS', 'Worker not available, skipping thought storage');
return { thoughtsStored: 0 };
}

const port = getWorkerPort();
const response = await fetchWithTimeout(
`http://127.0.0.1:${port}/api/sessions/thoughts`,
{
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
memorySessionId: input.memorySessionId,
contentSessionId: input.sessionId,
project: input.project,
thoughts,
}),
},
THOUGHTS_TIMEOUT_MS
);

if (!response.ok) {
const text = await response.text();
logger.warn('THOUGHTS', 'Failed to store thoughts', {
status: response.status,
body: text
});
return { thoughtsStored: 0 };
}

const result = await response.json() as { ids: number[] };
logger.info('THOUGHTS', `Stored ${result.ids.length} thoughts`, {
contentSessionId: input.sessionId
});

return { thoughtsStored: result.ids.length };
}
Loading