Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
95 changes: 90 additions & 5 deletions JavaDuckerMcpServer.java
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,8 @@ public class JavaDuckerMcpServer {
static final String BASE_URL = "http://" + HOST + ":" + PORT + "/api";
static final ObjectMapper MAPPER = new ObjectMapper();
static final HttpClient HTTP = HttpClient.newHttpClient();
static final boolean STALENESS_CHECK_ENABLED =
!"false".equalsIgnoreCase(System.getenv("JAVADUCKER_STALENESS_CHECK"));

public static void main(String[] args) throws Exception {
ensureServerRunning();
Expand Down Expand Up @@ -63,10 +65,19 @@ public static void main(String[] args) throws Exception {
"mode", str("exact, semantic, or hybrid (default)"),
"max_results", intParam("Max results to return (default 20)")),
"phrase")),
(ex, a) -> call(() -> search(
(String) a.get("phrase"),
(String) a.getOrDefault("mode", "hybrid"),
a.containsKey("max_results") ? ((Number) a.get("max_results")).intValue() : 20)))
(ex, a) -> call(() -> {
Map<String, Object> result = search(
(String) a.get("phrase"),
(String) a.getOrDefault("mode", "hybrid"),
a.containsKey("max_results") ? ((Number) a.get("max_results")).intValue() : 20);
try {
if (STALENESS_CHECK_ENABLED && result.containsKey("staleness_warning")) {
result.put("_footer", "\n⚠️ " + result.get("staleness_warning")
+ " Use javaducker_index_file to refresh.");
}
} catch (Exception ignored) { }
return result;
}))
.tool(
tool("javaducker_get_file_text",
"Retrieve the full extracted text of an indexed file by artifact_id. " +
Expand Down Expand Up @@ -109,7 +120,24 @@ public static void main(String[] args) throws Exception {
schema(props(
"artifact_id", str("Artifact ID to summarize")),
"artifact_id")),
(ex, a) -> call(() -> summarize((String) a.get("artifact_id"))))
(ex, a) -> call(() -> {
String artifactId = (String) a.get("artifact_id");
Map<String, Object> summary = summarize(artifactId);
if (STALENESS_CHECK_ENABLED) {
try {
Map<String, Object> status = getArtifactStatus(artifactId);
String path = (String) status.get("original_client_path");
if (path != null && !path.isBlank()) {
Map<String, Object> staleness = httpPost("/stale", Map.of("file_paths", List.of(path)));
List<?> staleList = (List<?>) staleness.get("stale");
if (staleList != null && !staleList.isEmpty()) {
summary.put("_warning", "⚠️ This file has changed since indexing — summary may be outdated.");
}
}
} catch (Exception ignored) { }
}
return summary;
}))
.tool(
tool("javaducker_map",
"Get a project map showing directory structure, file counts, largest files, and " +
Expand Down Expand Up @@ -331,11 +359,68 @@ public static void main(String[] args) throws Exception {
"Runtime config for a Reladomo object: DB connection, cache strategy. Omit name for full topology.",
schema(props("object_name", str("Object name (optional)")))),
(ex, a) -> call(() -> httpGet("/reladomo/config" + (a.containsKey("object_name") ? "?objectName=" + a.get("object_name") : ""))))
// ── Session Transcript tools ─────────────────────────────────
.tool(tool("javaducker_index_sessions",
"Index Claude Code session transcripts from a project directory. Makes past conversations searchable.",
schema(props(
"project_path", str("Path to project sessions directory (e.g. ~/.claude/projects/<hash>/)"),
"max_sessions", intParam("Max sessions to index (default: all)"),
"incremental", str("true to skip unchanged files (default: false)")),
"project_path")),
(ex, a) -> call(() -> {
Map<String, Object> body = new LinkedHashMap<>();
body.put("projectPath", a.get("project_path"));
if (a.containsKey("max_sessions")) body.put("maxSessions", ((Number) a.get("max_sessions")).intValue());
if ("true".equals(a.get("incremental"))) body.put("incremental", true);
return httpPost("/index-sessions", body);
}))
.tool(tool("javaducker_search_sessions",
"Search past Claude Code conversations. Returns matching excerpts with session ID and role.",
schema(props(
"phrase", str("Search phrase"),
"max_results", intParam("Max results (default 20)")),
"phrase")),
(ex, a) -> call(() -> httpPost("/search-sessions", Map.of(
"phrase", a.get("phrase"),
"max_results", ((Number) a.getOrDefault("max_results", 20)).intValue()))))
.tool(tool("javaducker_session_context",
"Get full historical context for a topic: session excerpts + related artifacts. One call for complete history.",
schema(props("topic", str("Topic or query to search for")), "topic")),
(ex, a) -> call(() -> sessionContext((String) a.get("topic"))))
// ── Session Decision tools ──────────────────────────────────
.tool(tool("javaducker_extract_decisions",
"Store decisions extracted from a session. Claude calls this after reading a session to record key decisions.",
schema(props(
"session_id", str("Session ID"),
"decisions", str("JSON array of {text, context?, tags?} objects")),
"session_id", "decisions")),
(ex, a) -> call(() -> {
List<Map<String, String>> decisions = MAPPER.readValue((String) a.get("decisions"), new TypeReference<>() {});
return httpPost("/extract-session-decisions", Map.of("sessionId", a.get("session_id"), "decisions", decisions));
}))
.tool(tool("javaducker_recent_decisions",
"List recent decisions from past sessions, optionally filtered by tag.",
schema(props(
"max_sessions", intParam("Max sessions to look back (default 5)"),
"tag", str("Optional tag filter")))),
(ex, a) -> call(() -> httpGet("/session-decisions?maxSessions=" +
((Number) a.getOrDefault("max_sessions", 5)).intValue() +
(a.containsKey("tag") ? "&tag=" + encode((String) a.get("tag")) : ""))))
.build();
}

// ── Tool implementations ──────────────────────────────────────────────────

static Map<String, Object> sessionContext(String topic) throws Exception {
Map<String, Object> sessionHits = httpPost("/search-sessions", Map.of("phrase", topic, "max_results", 5));
Map<String, Object> artifactHits = search(topic, "hybrid", 5);
Map<String, Object> result = new LinkedHashMap<>();
result.put("topic", topic);
result.put("session_excerpts", sessionHits.get("results"));
result.put("related_artifacts", artifactHits.get("results"));
return result;
}

static Map<String, Object> health() throws Exception {
return httpGet("/health");
}
Expand Down
26 changes: 13 additions & 13 deletions drom-plans/session-transcript-indexing.md
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
---
title: Session Transcript Indexing
status: in-progress
status: completed
created: 2026-03-28
updated: 2026-03-28
current_chapter: 3
current_chapter: 4
---

# Plan: Session Transcript Indexing
Expand Down Expand Up @@ -38,26 +38,26 @@ Index Claude Code conversation transcripts from `~/.claude/projects/` so that pa
> Sessions can be large (100K+ tokens). Only index assistant and user messages — skip tool_result payloads (they're the code itself, already indexed). Decision extraction happens in Chapter 4.

## Chapter 3: REST Endpoints & MCP Tools
**Status:** pending
**Status:** completed
**Depends on:** Chapter 2

- [ ] Add REST endpoints to `JavaDuckerRestController`: `POST /api/index-sessions` (body: `{projectPath, maxSessions?}`), `GET /api/sessions` (list indexed sessions with date, token count), `GET /api/session/{sessionId}` (full transcript), `POST /api/search-sessions` (body: `{phrase, mode, maxResults}` — search only session content)
- [ ] Add MCP tools to `JavaDuckerMcpServer.java`: `javaducker_index_sessions` (index sessions from a project path), `javaducker_search_sessions` (search past conversations — phrase, mode), `javaducker_session_decisions` (list decisions extracted from sessions, optionally filtered by tag)
- [ ] Add `javaducker_session_context` MCP tool — given a topic/query, return a compact context bundle: relevant session excerpts + any related MEMORY.md entries + related artifacts. One call to get full historical context
- [ ] Write integration test — index sample session JSONL, search it, verify results
- [x] Add REST endpoints to `JavaDuckerRestController`: `POST /api/index-sessions` (body: `{projectPath, maxSessions?}`), `GET /api/sessions` (list indexed sessions with date, token count), `GET /api/session/{sessionId}` (full transcript), `POST /api/search-sessions` (body: `{phrase, mode, maxResults}` — search only session content)
- [x] Add MCP tools to `JavaDuckerMcpServer.java`: `javaducker_index_sessions` (index sessions from a project path), `javaducker_search_sessions` (search past conversations — phrase, mode), `javaducker_session_decisions` (list decisions extracted from sessions, optionally filtered by tag)
- [x] Add `javaducker_session_context` MCP tool — given a topic/query, return a compact context bundle: relevant session excerpts + any related MEMORY.md entries + related artifacts. One call to get full historical context
- [x] Write integration test — index sample session JSONL, search it, verify results

**Notes:**
> `javaducker_session_context` is the high-value tool — it's what Claude calls when it needs to understand history. Keep the response compact: excerpts, not full transcripts.

## Chapter 4: Decision Extraction & Session Summaries
**Status:** pending
**Status:** completed
**Depends on:** Chapter 3

- [ ] Add `POST /api/extract-session-decisions` endpoint — accepts sessionId + list of decisions (text, context, tags). Stores in `session_decisions` table. Designed to be called by Claude after reading a session
- [ ] Add `javaducker_extract_decisions` MCP tool — write side for decision storage
- [ ] Add `javaducker_recent_decisions` MCP tool — return decisions from last N sessions, filterable by tag/topic
- [ ] Create `scripts/index-sessions.sh` — helper script that finds the project path in `~/.claude/projects/` and calls the index endpoint. Can be wired as a SessionStart hook for auto-indexing
- [ ] Write tests for decision storage and retrieval
- [x] Add `POST /api/extract-session-decisions` endpoint — accepts sessionId + list of decisions (text, context, tags). Stores in `session_decisions` table. Designed to be called by Claude after reading a session
- [x] Add `javaducker_extract_decisions` MCP tool — write side for decision storage
- [x] Add `javaducker_recent_decisions` MCP tool — return decisions from last N sessions, filterable by tag/topic
- [x] Create `scripts/index-sessions.sh` — helper script that finds the project path in `~/.claude/projects/` and calls the index endpoint. Can be wired as a SessionStart hook for auto-indexing
- [x] Write tests for decision storage and retrieval

**Notes:**
> Decision extraction is LLM-driven — Claude reads the transcript and identifies decisions. The MCP tool just stores what Claude extracts. This pairs with the content intelligence enrichment pipeline (O3).
Expand Down
10 changes: 5 additions & 5 deletions drom-plans/stale-index-warning.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
---
title: Stale Index Warning
status: in-progress
status: completed
created: 2026-03-28
updated: 2026-03-28
current_chapter: 4
Expand Down Expand Up @@ -59,12 +59,12 @@ Enhance the existing staleness infrastructure (`StalenessService`, `javaducker_s
> This is the tool Claude calls proactively or when starting a session. The response is designed to be actionable — it tells Claude exactly what to do next.

## Chapter 4: Search-Time Hook (MCP Server Enhancement)
**Status:** pending
**Status:** completed
**Depends on:** Chapter 2, Chapter 3

- [ ] In `JavaDuckerMcpServer`, wrap the `javaducker_search` handler: after returning search results, if `staleness_warning` is non-null, append a footer line: `"\n⚠️ {warning} Use javaducker_index to refresh."`
- [ ] Similarly, wrap `javaducker_summarize` — if the artifact being summarized is stale, prepend: `"⚠️ This file has changed since indexing — summary may be outdated."`
- [ ] Add a `--staleness-check` flag (default: on) to the MCP server config so users can disable if overhead is unwanted
- [x] In `JavaDuckerMcpServer`, wrap the `javaducker_search` handler: after returning search results, if `staleness_warning` is non-null, append a footer line: `"\n⚠️ {warning} Use javaducker_index to refresh."`
- [x] Similarly, wrap `javaducker_summarize` — if the artifact being summarized is stale, prepend: `"⚠️ This file has changed since indexing — summary may be outdated."`
- [x] Add a `--staleness-check` flag (default: on) to the MCP server config so users can disable if overhead is unwanted
- [ ] Test: verify search result includes footer when stale, omits when current

**Notes:**
Expand Down
22 changes: 22 additions & 0 deletions scripts/index-sessions.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
#!/usr/bin/env bash
# Index Claude Code session transcripts for the current project
set -euo pipefail

JAVADUCKER_PORT="${HTTP_PORT:-8080}"
JAVADUCKER_HOST="${JAVADUCKER_HOST:-localhost}"
BASE_URL="http://${JAVADUCKER_HOST}:${JAVADUCKER_PORT}/api"

# Find the project sessions directory
PROJECT_ROOT="${PROJECT_ROOT:-.}"
PROJECT_HASH=$(echo -n "$(cd "$PROJECT_ROOT" && pwd)" | md5sum | cut -d' ' -f1)
SESSIONS_DIR="$HOME/.claude/projects/${PROJECT_HASH}"

if [ ! -d "$SESSIONS_DIR" ]; then
echo "No sessions directory found at $SESSIONS_DIR"
exit 0
fi

echo "Indexing sessions from $SESSIONS_DIR..."
curl -s -X POST "${BASE_URL}/index-sessions" \
-H "Content-Type: application/json" \
-d "{\"projectPath\": \"${SESSIONS_DIR}\", \"incremental\": true}" | jq .
Loading
Loading