preflight-dev 3.1.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/LICENSE +21 -0
- package/README.md +172 -0
- package/bin/cli.js +11 -0
- package/dist/cli/init.d.ts +2 -0
- package/dist/cli/init.js +154 -0
- package/dist/cli/init.js.map +1 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +122 -0
- package/dist/index.js.map +1 -0
- package/dist/lib/config.d.ts +34 -0
- package/dist/lib/config.js +118 -0
- package/dist/lib/config.js.map +1 -0
- package/dist/lib/embeddings.d.ts +11 -0
- package/dist/lib/embeddings.js +88 -0
- package/dist/lib/embeddings.js.map +1 -0
- package/dist/lib/files.d.ts +15 -0
- package/dist/lib/files.js +60 -0
- package/dist/lib/files.js.map +1 -0
- package/dist/lib/git-extractor.d.ts +9 -0
- package/dist/lib/git-extractor.js +116 -0
- package/dist/lib/git-extractor.js.map +1 -0
- package/dist/lib/git.d.ts +29 -0
- package/dist/lib/git.js +86 -0
- package/dist/lib/git.js.map +1 -0
- package/dist/lib/session-parser.d.ts +45 -0
- package/dist/lib/session-parser.js +267 -0
- package/dist/lib/session-parser.js.map +1 -0
- package/dist/lib/state.d.ts +21 -0
- package/dist/lib/state.js +86 -0
- package/dist/lib/state.js.map +1 -0
- package/dist/lib/timeline-db.d.ts +67 -0
- package/dist/lib/timeline-db.js +380 -0
- package/dist/lib/timeline-db.js.map +1 -0
- package/dist/lib/triage.d.ts +29 -0
- package/dist/lib/triage.js +193 -0
- package/dist/lib/triage.js.map +1 -0
- package/dist/profiles.d.ts +3 -0
- package/dist/profiles.js +65 -0
- package/dist/profiles.js.map +1 -0
- package/dist/tools/audit-workspace.d.ts +2 -0
- package/dist/tools/audit-workspace.js +86 -0
- package/dist/tools/audit-workspace.js.map +1 -0
- package/dist/tools/checkpoint.d.ts +2 -0
- package/dist/tools/checkpoint.js +108 -0
- package/dist/tools/checkpoint.js.map +1 -0
- package/dist/tools/clarify-intent.d.ts +2 -0
- package/dist/tools/clarify-intent.js +180 -0
- package/dist/tools/clarify-intent.js.map +1 -0
- package/dist/tools/enrich-agent-task.d.ts +2 -0
- package/dist/tools/enrich-agent-task.js +97 -0
- package/dist/tools/enrich-agent-task.js.map +1 -0
- package/dist/tools/generate-scorecard.d.ts +2 -0
- package/dist/tools/generate-scorecard.js +617 -0
- package/dist/tools/generate-scorecard.js.map +1 -0
- package/dist/tools/log-correction.d.ts +2 -0
- package/dist/tools/log-correction.js +76 -0
- package/dist/tools/log-correction.js.map +1 -0
- package/dist/tools/onboard-project.d.ts +2 -0
- package/dist/tools/onboard-project.js +179 -0
- package/dist/tools/onboard-project.js.map +1 -0
- package/dist/tools/preflight-check.d.ts +2 -0
- package/dist/tools/preflight-check.js +229 -0
- package/dist/tools/preflight-check.js.map +1 -0
- package/dist/tools/prompt-score.d.ts +2 -0
- package/dist/tools/prompt-score.js +132 -0
- package/dist/tools/prompt-score.js.map +1 -0
- package/dist/tools/scan-sessions.d.ts +2 -0
- package/dist/tools/scan-sessions.js +182 -0
- package/dist/tools/scan-sessions.js.map +1 -0
- package/dist/tools/scope-work.d.ts +2 -0
- package/dist/tools/scope-work.js +214 -0
- package/dist/tools/scope-work.js.map +1 -0
- package/dist/tools/search-history.d.ts +2 -0
- package/dist/tools/search-history.js +130 -0
- package/dist/tools/search-history.js.map +1 -0
- package/dist/tools/sequence-tasks.d.ts +2 -0
- package/dist/tools/sequence-tasks.js +165 -0
- package/dist/tools/sequence-tasks.js.map +1 -0
- package/dist/tools/session-handoff.d.ts +2 -0
- package/dist/tools/session-handoff.js +113 -0
- package/dist/tools/session-handoff.js.map +1 -0
- package/dist/tools/session-health.d.ts +2 -0
- package/dist/tools/session-health.js +111 -0
- package/dist/tools/session-health.js.map +1 -0
- package/dist/tools/session-stats.d.ts +2 -0
- package/dist/tools/session-stats.js +112 -0
- package/dist/tools/session-stats.js.map +1 -0
- package/dist/tools/sharpen-followup.d.ts +2 -0
- package/dist/tools/sharpen-followup.js +192 -0
- package/dist/tools/sharpen-followup.js.map +1 -0
- package/dist/tools/timeline-view.d.ts +2 -0
- package/dist/tools/timeline-view.js +165 -0
- package/dist/tools/timeline-view.js.map +1 -0
- package/dist/tools/token-audit.d.ts +2 -0
- package/dist/tools/token-audit.js +227 -0
- package/dist/tools/token-audit.js.map +1 -0
- package/dist/tools/verify-completion.d.ts +2 -0
- package/dist/tools/verify-completion.js +154 -0
- package/dist/tools/verify-completion.js.map +1 -0
- package/dist/tools/what-changed.d.ts +2 -0
- package/dist/tools/what-changed.js +40 -0
- package/dist/tools/what-changed.js.map +1 -0
- package/dist/types.d.ts +78 -0
- package/dist/types.js +2 -0
- package/dist/types.js.map +1 -0
- package/package.json +52 -0
- package/src/cli/init.ts +133 -0
- package/src/index.ts +135 -0
- package/src/lib/config.ts +157 -0
- package/src/lib/embeddings.ts +118 -0
- package/src/lib/files.ts +59 -0
- package/src/lib/git-extractor.ts +137 -0
- package/src/lib/git.ts +89 -0
- package/src/lib/session-parser.ts +325 -0
- package/src/lib/state.ts +86 -0
- package/src/lib/timeline-db.ts +490 -0
- package/src/lib/triage.ts +255 -0
- package/src/profiles.ts +70 -0
- package/src/templates/config.yml +23 -0
- package/src/templates/triage.yml +27 -0
- package/src/tools/audit-workspace.ts +97 -0
- package/src/tools/checkpoint.ts +119 -0
- package/src/tools/clarify-intent.ts +191 -0
- package/src/tools/enrich-agent-task.ts +108 -0
- package/src/tools/generate-scorecard.ts +673 -0
- package/src/tools/log-correction.ts +89 -0
- package/src/tools/onboard-project.ts +214 -0
- package/src/tools/preflight-check.ts +263 -0
- package/src/tools/prompt-score.ts +150 -0
- package/src/tools/scan-sessions.ts +209 -0
- package/src/tools/scope-work.ts +238 -0
- package/src/tools/search-history.ts +145 -0
- package/src/tools/sequence-tasks.ts +182 -0
- package/src/tools/session-handoff.ts +125 -0
- package/src/tools/session-health.ts +107 -0
- package/src/tools/session-stats.ts +134 -0
- package/src/tools/sharpen-followup.ts +200 -0
- package/src/tools/timeline-view.ts +181 -0
- package/src/tools/token-audit.ts +259 -0
- package/src/tools/verify-completion.ts +159 -0
- package/src/tools/what-changed.ts +48 -0
- package/src/types.ts +87 -0
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
// =============================================================================
|
|
2
|
+
// session_stats — Lightweight JSONL session analysis (no embeddings needed)
|
|
3
|
+
// =============================================================================
|
|
4
|
+
|
|
5
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
6
|
+
import { z } from "zod";
|
|
7
|
+
import { readdir, readFile } from "node:fs/promises";
|
|
8
|
+
import { join } from "node:path";
|
|
9
|
+
import { homedir } from "node:os";
|
|
10
|
+
|
|
11
|
+
interface SessionInfo {
|
|
12
|
+
file: string;
|
|
13
|
+
turns: number;
|
|
14
|
+
corrections: number;
|
|
15
|
+
compactions: number;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
async function findSessionFiles(): Promise<string[]> {
|
|
19
|
+
const baseDir = join(homedir(), ".claude", "projects");
|
|
20
|
+
const files: string[] = [];
|
|
21
|
+
|
|
22
|
+
async function walk(dir: string): Promise<void> {
|
|
23
|
+
try {
|
|
24
|
+
const entries = await readdir(dir, { withFileTypes: true });
|
|
25
|
+
for (const entry of entries) {
|
|
26
|
+
const full = join(dir, entry.name);
|
|
27
|
+
if (entry.isDirectory()) {
|
|
28
|
+
await walk(full);
|
|
29
|
+
} else if (entry.name.endsWith(".jsonl")) {
|
|
30
|
+
files.push(full);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
} catch {
|
|
34
|
+
// skip inaccessible dirs
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
await walk(baseDir);
|
|
39
|
+
return files;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
async function analyzeSession(filePath: string): Promise<SessionInfo> {
|
|
43
|
+
const content = await readFile(filePath, "utf-8");
|
|
44
|
+
const lines = content.trim().split("\n").filter(Boolean);
|
|
45
|
+
let turns = 0;
|
|
46
|
+
let corrections = 0;
|
|
47
|
+
let compactions = 0;
|
|
48
|
+
|
|
49
|
+
for (const line of lines) {
|
|
50
|
+
try {
|
|
51
|
+
const obj = JSON.parse(line);
|
|
52
|
+
if (obj.type === "human" || obj.role === "human" || obj.role === "user") {
|
|
53
|
+
turns++;
|
|
54
|
+
}
|
|
55
|
+
// Detect corrections: messages containing "no", "wrong", "actually", "instead"
|
|
56
|
+
const text = (obj.message || obj.content || "").toString().toLowerCase();
|
|
57
|
+
if (turns > 0 && /\b(no[,.]|wrong|actually|instead|that's not|not what i)\b/.test(text)) {
|
|
58
|
+
corrections++;
|
|
59
|
+
}
|
|
60
|
+
// Detect compactions
|
|
61
|
+
if (obj.type === "summary" || obj.type === "compaction" || text.includes("compacted") || text.includes("context window")) {
|
|
62
|
+
compactions++;
|
|
63
|
+
}
|
|
64
|
+
} catch {
|
|
65
|
+
// skip malformed lines
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
return { file: filePath, turns, corrections, compactions };
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export function registerSessionStats(server: McpServer): void {
|
|
73
|
+
server.tool(
|
|
74
|
+
"session_stats",
|
|
75
|
+
"Analyze Claude Code session history from JSONL files. Returns total sessions, prompts, correction rate, and more. No embeddings needed.",
|
|
76
|
+
{
|
|
77
|
+
projectFilter: z.string().optional().describe("Filter to sessions matching this project path substring"),
|
|
78
|
+
},
|
|
79
|
+
async ({ projectFilter }) => {
|
|
80
|
+
const files = await findSessionFiles();
|
|
81
|
+
const filtered = projectFilter
|
|
82
|
+
? files.filter((f) => f.includes(projectFilter))
|
|
83
|
+
: files;
|
|
84
|
+
|
|
85
|
+
if (filtered.length === 0) {
|
|
86
|
+
return {
|
|
87
|
+
content: [
|
|
88
|
+
{
|
|
89
|
+
type: "text" as const,
|
|
90
|
+
text: "No session files found in ~/.claude/projects/",
|
|
91
|
+
},
|
|
92
|
+
],
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const sessions = await Promise.all(filtered.map(analyzeSession));
|
|
97
|
+
const totalTurns = sessions.reduce((s, x) => s + x.turns, 0);
|
|
98
|
+
const totalCorrections = sessions.reduce((s, x) => s + x.corrections, 0);
|
|
99
|
+
const totalCompactions = sessions.reduce((s, x) => s + x.compactions, 0);
|
|
100
|
+
const avgTurns = sessions.length > 0 ? (totalTurns / sessions.length).toFixed(1) : "0";
|
|
101
|
+
const correctionRate = totalTurns > 0 ? ((totalCorrections / totalTurns) * 100).toFixed(1) : "0";
|
|
102
|
+
|
|
103
|
+
// Find most active branches (group by parent dir)
|
|
104
|
+
const branchCounts = new Map<string, number>();
|
|
105
|
+
for (const s of sessions) {
|
|
106
|
+
const parts = s.file.split("/");
|
|
107
|
+
const branch = parts.slice(-2, -1)[0] || "unknown";
|
|
108
|
+
branchCounts.set(branch, (branchCounts.get(branch) || 0) + s.turns);
|
|
109
|
+
}
|
|
110
|
+
const topBranches = [...branchCounts.entries()]
|
|
111
|
+
.sort((a, b) => b[1] - a[1])
|
|
112
|
+
.slice(0, 5)
|
|
113
|
+
.map(([name, count]) => ` ${name}: ${count} prompts`)
|
|
114
|
+
.join("\n");
|
|
115
|
+
|
|
116
|
+
const report = [
|
|
117
|
+
`📊 Session Stats`,
|
|
118
|
+
`────────────────────────`,
|
|
119
|
+
`Sessions: ${sessions.length}`,
|
|
120
|
+
`Total prompts: ${totalTurns}`,
|
|
121
|
+
`Corrections: ${totalCorrections} (${correctionRate}% rate)`,
|
|
122
|
+
`Compactions: ${totalCompactions}`,
|
|
123
|
+
`Avg session len: ${avgTurns} turns`,
|
|
124
|
+
``,
|
|
125
|
+
`Most active branches:`,
|
|
126
|
+
topBranches || " (none)",
|
|
127
|
+
].join("\n");
|
|
128
|
+
|
|
129
|
+
return {
|
|
130
|
+
content: [{ type: "text" as const, text: report }],
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
);
|
|
134
|
+
}
|
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
// CATEGORY 4: sharpen_followup — Follow-up Specificity
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
4
|
+
import { run } from "../lib/git.js";
|
|
5
|
+
import { now } from "../lib/state.js";
|
|
6
|
+
|
|
7
|
+
/** Parse git porcelain output into deduplicated file paths, handling renames (R/C) */
|
|
8
|
+
function parsePortelainFiles(output: string): string[] {
|
|
9
|
+
if (!output.trim()) return [];
|
|
10
|
+
const files = new Set<string>();
|
|
11
|
+
for (const line of output.split("\n").filter(Boolean)) {
|
|
12
|
+
if (line.length < 4) continue;
|
|
13
|
+
const status = line.slice(0, 2);
|
|
14
|
+
const rest = line.slice(3);
|
|
15
|
+
if (status.startsWith("R") || status.startsWith("C")) {
|
|
16
|
+
// "R old -> new" — include both old and new
|
|
17
|
+
const parts = rest.split(" -> ");
|
|
18
|
+
parts.forEach((p) => { const t = p.trim(); if (t) files.add(t); });
|
|
19
|
+
} else {
|
|
20
|
+
const t = rest.trim();
|
|
21
|
+
if (t) files.add(t);
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
return [...files];
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** Get recently changed files, safe for first commit / shallow clones */
|
|
28
|
+
function getRecentChangedFiles(): string[] {
|
|
29
|
+
// Try HEAD~1..HEAD, fall back to just staged, then unstaged
|
|
30
|
+
const commands = [
|
|
31
|
+
"git diff --name-only HEAD~1 HEAD 2>/dev/null",
|
|
32
|
+
"git diff --name-only --cached 2>/dev/null",
|
|
33
|
+
"git diff --name-only 2>/dev/null",
|
|
34
|
+
];
|
|
35
|
+
const results = new Set<string>();
|
|
36
|
+
for (const cmd of commands) {
|
|
37
|
+
const out = run(cmd);
|
|
38
|
+
if (out) out.split("\n").filter(Boolean).forEach((f) => results.add(f));
|
|
39
|
+
if (results.size > 0) break; // first successful source is enough
|
|
40
|
+
}
|
|
41
|
+
return [...results];
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function registerSharpenFollowup(server: McpServer): void {
|
|
45
|
+
server.tool(
|
|
46
|
+
"sharpen_followup",
|
|
47
|
+
`Detects vague follow-up prompts and sharpens them with specific files, scope, and context from previous actions and git state. Call when the user says things like "fix it", "do the same for the others", "now the tests" without specifying files or scope.`,
|
|
48
|
+
{
|
|
49
|
+
followup_message: z.string().describe("The user's follow-up message to analyze"),
|
|
50
|
+
previous_action: z.string().describe("Description of what was just done"),
|
|
51
|
+
previous_files: z.array(z.string()).optional().describe("Files involved in the previous action"),
|
|
52
|
+
},
|
|
53
|
+
async ({ followup_message, previous_action, previous_files }) => {
|
|
54
|
+
const msg = followup_message.trim();
|
|
55
|
+
const assumptions: string[] = [];
|
|
56
|
+
const questions: string[] = [];
|
|
57
|
+
let confidence: "high" | "medium" | "low" = "high";
|
|
58
|
+
|
|
59
|
+
// Vagueness detection
|
|
60
|
+
const pronounPattern = /\b(it|them|this|that|those|the others?|these)\b/gi;
|
|
61
|
+
const scopePattern = /\b(all|everything|the rest|everywhere|each one|every)\b/gi;
|
|
62
|
+
const hasPathRef = /[\/\\]|\.(?:ts|js|tsx|jsx|py|rs|go|md|json|yaml|yml|toml|css|html|sh)\b/.test(msg);
|
|
63
|
+
const isBareCommand = msg.length < 30 && !hasPathRef;
|
|
64
|
+
|
|
65
|
+
const pronounMatches = [...new Set([...msg.matchAll(pronounPattern)].map(m => m[0].toLowerCase()))];
|
|
66
|
+
const scopeMatches = [...new Set([...msg.matchAll(scopePattern)].map(m => m[0].toLowerCase()))];
|
|
67
|
+
|
|
68
|
+
const vagueSignals: string[] = [];
|
|
69
|
+
if (pronounMatches.length > 0) vagueSignals.push(`pronouns without antecedents: ${pronounMatches.join(", ")}`);
|
|
70
|
+
if (scopeMatches.length > 0) vagueSignals.push(`scope words without specifics: ${scopeMatches.join(", ")}`);
|
|
71
|
+
if (isBareCommand) vagueSignals.push("bare command with no file/path reference");
|
|
72
|
+
|
|
73
|
+
// If no vagueness detected, pass through
|
|
74
|
+
if (vagueSignals.length === 0) {
|
|
75
|
+
const output = [
|
|
76
|
+
"## Follow-up Analysis",
|
|
77
|
+
"",
|
|
78
|
+
`**Original:** ${msg}`,
|
|
79
|
+
`**Sharpened:** ${msg}`,
|
|
80
|
+
`**Confidence:** high`,
|
|
81
|
+
"",
|
|
82
|
+
"_Follow-up is already specific enough — no changes needed._",
|
|
83
|
+
].join("\n");
|
|
84
|
+
return { content: [{ type: "text" as const, text: output }] };
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// Gather context to resolve ambiguity
|
|
88
|
+
const contextFiles: string[] = [...(previous_files ?? [])];
|
|
89
|
+
const recentChanged = getRecentChangedFiles();
|
|
90
|
+
const porcelainOutput = run("git status --porcelain 2>/dev/null");
|
|
91
|
+
const untrackedOrModified = parsePortelainFiles(porcelainOutput);
|
|
92
|
+
|
|
93
|
+
const allKnownFiles = [...new Set([...contextFiles, ...recentChanged, ...untrackedOrModified])].filter(Boolean);
|
|
94
|
+
|
|
95
|
+
let sharpened = msg;
|
|
96
|
+
|
|
97
|
+
// Resolve singular pronouns: "it" / "this" / "that"
|
|
98
|
+
const singularPronouns = pronounMatches.filter(p => ["it", "this", "that"].includes(p));
|
|
99
|
+
if (singularPronouns.length > 0) {
|
|
100
|
+
if (contextFiles.length === 1) {
|
|
101
|
+
for (const p of singularPronouns) {
|
|
102
|
+
sharpened = sharpened.replace(new RegExp(`\\b${p}\\b`, "i"), contextFiles[0]);
|
|
103
|
+
}
|
|
104
|
+
assumptions.push(`Resolved ${singularPronouns.map(p => `"${p}"`).join(", ")} → ${contextFiles[0]} (only file from previous action)`);
|
|
105
|
+
} else if (contextFiles.length > 1) {
|
|
106
|
+
confidence = "low";
|
|
107
|
+
questions.push(`Which file do you mean? Previous action touched: ${contextFiles.join(", ")}`);
|
|
108
|
+
} else if (recentChanged.length === 1) {
|
|
109
|
+
for (const p of singularPronouns) {
|
|
110
|
+
sharpened = sharpened.replace(new RegExp(`\\b${p}\\b`, "i"), recentChanged[0]);
|
|
111
|
+
}
|
|
112
|
+
assumptions.push(`Resolved ${singularPronouns.map(p => `"${p}"`).join(", ")} → ${recentChanged[0]} (only recent git change)`);
|
|
113
|
+
confidence = "medium";
|
|
114
|
+
} else {
|
|
115
|
+
confidence = "low";
|
|
116
|
+
questions.push("Which file or component are you referring to? No single obvious target found.");
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// Resolve plural pronouns: "them" / "the others" / "these" / "those"
|
|
121
|
+
const pluralPronouns = pronounMatches.filter(p => ["them", "the others", "those", "these"].includes(p));
|
|
122
|
+
if (pluralPronouns.length > 0) {
|
|
123
|
+
const otherFiles = allKnownFiles.filter(f => !contextFiles.slice(0, 1).includes(f));
|
|
124
|
+
if (otherFiles.length > 0 && otherFiles.length <= 10) {
|
|
125
|
+
for (const p of pluralPronouns) {
|
|
126
|
+
sharpened = sharpened.replace(new RegExp(`\\b${p.replace(/\s+/g, "\\s+")}\\b`, "i"), otherFiles.join(", "));
|
|
127
|
+
}
|
|
128
|
+
assumptions.push(`Resolved ${pluralPronouns.map(p => `"${p}"`).join(", ")} → remaining files: ${otherFiles.join(", ")}`);
|
|
129
|
+
confidence = otherFiles.length <= 3 ? "medium" : "low";
|
|
130
|
+
} else if (otherFiles.length > 10) {
|
|
131
|
+
confidence = "low";
|
|
132
|
+
questions.push(`Found ${otherFiles.length} candidate files — too many to assume. Which subset do you mean?`);
|
|
133
|
+
} else {
|
|
134
|
+
confidence = "low";
|
|
135
|
+
questions.push('What does "the others" refer to? No additional files found in context.');
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// Resolve scope words
|
|
140
|
+
if (scopeMatches.length > 0 && !hasPathRef) {
|
|
141
|
+
if (allKnownFiles.length > 0 && allKnownFiles.length <= 8) {
|
|
142
|
+
assumptions.push(`Scope "${scopeMatches[0]}" interpreted as: ${allKnownFiles.join(", ")}`);
|
|
143
|
+
confidence = confidence === "high" ? "medium" : "low";
|
|
144
|
+
} else if (allKnownFiles.length > 8) {
|
|
145
|
+
confidence = "low";
|
|
146
|
+
questions.push(`"${scopeMatches[0]}" is ambiguous — ${allKnownFiles.length} files in scope. Please specify a directory or glob pattern.`);
|
|
147
|
+
} else {
|
|
148
|
+
confidence = "low";
|
|
149
|
+
questions.push(`What does "${scopeMatches[0]}" cover? No files found in recent context.`);
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// Bare command enrichment
|
|
154
|
+
if (isBareCommand && contextFiles.length > 0) {
|
|
155
|
+
sharpened = `${sharpened} in ${contextFiles.join(", ")}`;
|
|
156
|
+
assumptions.push(`Added file scope from previous action: ${contextFiles.join(", ")}`);
|
|
157
|
+
if (confidence === "high") confidence = "medium";
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// Build markdown output
|
|
161
|
+
const lines = [
|
|
162
|
+
"## Follow-up Analysis",
|
|
163
|
+
"",
|
|
164
|
+
`**Original:** ${msg}`,
|
|
165
|
+
`**Sharpened:** ${confidence === "low" && questions.length > 0 ? "(needs clarification)" : sharpened}`,
|
|
166
|
+
`**Confidence:** ${confidence}`,
|
|
167
|
+
`**Previous action:** ${previous_action}`,
|
|
168
|
+
"",
|
|
169
|
+
];
|
|
170
|
+
|
|
171
|
+
if (vagueSignals.length > 0) {
|
|
172
|
+
lines.push("### Vague Signals Detected");
|
|
173
|
+
vagueSignals.forEach((s) => lines.push(`- ⚠️ ${s}`));
|
|
174
|
+
lines.push("");
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
if (assumptions.length > 0) {
|
|
178
|
+
lines.push("### Assumptions Made");
|
|
179
|
+
assumptions.forEach((a) => lines.push(`- ${a}`));
|
|
180
|
+
lines.push("");
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
if (questions.length > 0) {
|
|
184
|
+
lines.push("### Clarifying Questions");
|
|
185
|
+
questions.forEach((q) => lines.push(`- ❓ ${q}`));
|
|
186
|
+
lines.push("");
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
if (allKnownFiles.length > 0) {
|
|
190
|
+
lines.push("### Available Context Files");
|
|
191
|
+
allKnownFiles.slice(0, 20).forEach((f) => lines.push(`- \`${f}\``));
|
|
192
|
+
lines.push("");
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
lines.push(`_Generated ${now()}_`);
|
|
196
|
+
|
|
197
|
+
return { content: [{ type: "text" as const, text: lines.join("\n") }] };
|
|
198
|
+
}
|
|
199
|
+
);
|
|
200
|
+
}
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
3
|
+
import { getTimeline, listIndexedProjects } from "../lib/timeline-db.js";
|
|
4
|
+
import { getRelatedProjects } from "../lib/config.js";
|
|
5
|
+
import type { SearchScope } from "../types.js";
|
|
6
|
+
|
|
7
|
+
const RELATIVE_DATE_RE = /^(\d+)(days?|weeks?|months?|years?)$/;
|
|
8
|
+
|
|
9
|
+
function parseRelativeDate(input: string): string {
|
|
10
|
+
const match = input.match(RELATIVE_DATE_RE);
|
|
11
|
+
if (!match) return input;
|
|
12
|
+
const [, numStr, unit] = match;
|
|
13
|
+
const num = parseInt(numStr, 10);
|
|
14
|
+
const d = new Date();
|
|
15
|
+
if (unit.startsWith("day")) d.setDate(d.getDate() - num);
|
|
16
|
+
else if (unit.startsWith("week")) d.setDate(d.getDate() - num * 7);
|
|
17
|
+
else if (unit.startsWith("month")) d.setMonth(d.getMonth() - num);
|
|
18
|
+
else if (unit.startsWith("year")) d.setFullYear(d.getFullYear() - num);
|
|
19
|
+
return d.toISOString();
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const TYPE_ICONS: Record<string, string> = {
|
|
23
|
+
prompt: "💬",
|
|
24
|
+
assistant: "🤖",
|
|
25
|
+
tool_call: "🔧",
|
|
26
|
+
correction: "❌",
|
|
27
|
+
commit: "📦",
|
|
28
|
+
compaction: "🗜️",
|
|
29
|
+
sub_agent_spawn: "🚀",
|
|
30
|
+
error: "⚠️",
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
/** Get project directories to search based on scope */
|
|
34
|
+
async function getSearchProjects(scope: SearchScope): Promise<string[]> {
|
|
35
|
+
const currentProject = process.env.CLAUDE_PROJECT_DIR;
|
|
36
|
+
|
|
37
|
+
switch (scope) {
|
|
38
|
+
case "current":
|
|
39
|
+
return currentProject ? [currentProject] : [];
|
|
40
|
+
|
|
41
|
+
case "related":
|
|
42
|
+
const related = getRelatedProjects();
|
|
43
|
+
return currentProject ? [currentProject, ...related] : related;
|
|
44
|
+
|
|
45
|
+
case "all":
|
|
46
|
+
const projects = await listIndexedProjects();
|
|
47
|
+
return projects.map(p => p.project);
|
|
48
|
+
|
|
49
|
+
default:
|
|
50
|
+
return currentProject ? [currentProject] : [];
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export function registerTimeline(server: McpServer) {
|
|
55
|
+
server.tool(
|
|
56
|
+
"timeline",
|
|
57
|
+
"Chronological view of project events grouped by day. Shows prompts, responses, tool calls, corrections, and commits in order.",
|
|
58
|
+
{
|
|
59
|
+
scope: z.enum(["current", "related", "all"]).default("current").describe("Search scope: current project, related projects (PREFLIGHT_RELATED), or all indexed projects"),
|
|
60
|
+
project: z.string().optional().describe("Filter to a specific project name (overrides scope)"),
|
|
61
|
+
branch: z.string().optional(),
|
|
62
|
+
author: z.string().optional().describe("Filter commits to this author (partial match, case-insensitive)"),
|
|
63
|
+
since: z.string().optional(),
|
|
64
|
+
until: z.string().optional(),
|
|
65
|
+
type: z.enum(["prompt", "assistant", "correction", "commit", "tool_call", "compaction", "sub_agent_spawn", "error", "all"]).default("all"),
|
|
66
|
+
limit: z.number().default(50),
|
|
67
|
+
offset: z.number().default(0),
|
|
68
|
+
},
|
|
69
|
+
async (params) => {
|
|
70
|
+
const since = params.since ? parseRelativeDate(params.since) : undefined;
|
|
71
|
+
const until = params.until ? parseRelativeDate(params.until) : undefined;
|
|
72
|
+
|
|
73
|
+
// Determine which projects to search
|
|
74
|
+
let projectDirs: string[];
|
|
75
|
+
if (params.project) {
|
|
76
|
+
// Specific project overrides scope
|
|
77
|
+
projectDirs = [params.project];
|
|
78
|
+
} else {
|
|
79
|
+
projectDirs = await getSearchProjects(params.scope);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
if (projectDirs.length === 0) {
|
|
83
|
+
return {
|
|
84
|
+
content: [{
|
|
85
|
+
type: "text",
|
|
86
|
+
text: `## Timeline\n_No projects found for scope "${params.scope}". Make sure CLAUDE_PROJECT_DIR is set or projects are onboarded._`
|
|
87
|
+
}]
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
let events = await getTimeline({
|
|
92
|
+
project_dirs: projectDirs,
|
|
93
|
+
project: undefined, // Don't filter by single project when using project_dirs
|
|
94
|
+
branch: params.branch,
|
|
95
|
+
since,
|
|
96
|
+
until,
|
|
97
|
+
type: params.type === "all" ? undefined : params.type,
|
|
98
|
+
limit: params.limit,
|
|
99
|
+
offset: params.offset,
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
// Post-filter by author
|
|
103
|
+
if (params.author) {
|
|
104
|
+
const authorLower = params.author.toLowerCase();
|
|
105
|
+
events = events.filter((e: any) => {
|
|
106
|
+
if (e.type !== "commit") return true; // only filter commits
|
|
107
|
+
try {
|
|
108
|
+
const meta = JSON.parse(e.metadata || "{}");
|
|
109
|
+
return (meta.author || "").toLowerCase().includes(authorLower);
|
|
110
|
+
} catch { return true; }
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
if (events.length === 0) {
|
|
115
|
+
return { content: [{ type: "text", text: "## Timeline\n_No events found for the given filters._" }] };
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// Group by day
|
|
119
|
+
const days = new Map<string, any[]>();
|
|
120
|
+
for (const event of events) {
|
|
121
|
+
const day = event.timestamp ? new Date(event.timestamp).toISOString().slice(0, 10) : "unknown";
|
|
122
|
+
if (!days.has(day)) days.set(day, []);
|
|
123
|
+
days.get(day)!.push(event);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// Header
|
|
127
|
+
const proj = params.project || "all projects";
|
|
128
|
+
const branch = params.branch ? ` / ${params.branch}` : "";
|
|
129
|
+
const sortedDays = [...days.keys()].sort().reverse();
|
|
130
|
+
const dateRange = sortedDays.length > 1
|
|
131
|
+
? `${sortedDays[sortedDays.length - 1]} to ${sortedDays[0]}`
|
|
132
|
+
: sortedDays[0];
|
|
133
|
+
|
|
134
|
+
const lines: string[] = [
|
|
135
|
+
`## Timeline: ${proj}${branch}`,
|
|
136
|
+
`_${dateRange} (${events.length} events)_`,
|
|
137
|
+
"",
|
|
138
|
+
];
|
|
139
|
+
|
|
140
|
+
for (const day of sortedDays) {
|
|
141
|
+
lines.push(`### ${day}`);
|
|
142
|
+
const dayEvents = days.get(day)!;
|
|
143
|
+
// Sort by timestamp within day
|
|
144
|
+
dayEvents.sort((a: any, b: any) => {
|
|
145
|
+
const ta = a.timestamp ? new Date(a.timestamp).getTime() : 0;
|
|
146
|
+
const tb = b.timestamp ? new Date(b.timestamp).getTime() : 0;
|
|
147
|
+
return ta - tb;
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
for (const event of dayEvents) {
|
|
151
|
+
const time = event.timestamp
|
|
152
|
+
? new Date(event.timestamp).toISOString().slice(11, 16)
|
|
153
|
+
: "??:??";
|
|
154
|
+
const icon = TYPE_ICONS[event.type] || "❓";
|
|
155
|
+
let content = (event.content || event.summary || "").slice(0, 120).replace(/\n/g, " ");
|
|
156
|
+
|
|
157
|
+
// Format based on type
|
|
158
|
+
if (event.type === "commit") {
|
|
159
|
+
const hash = event.commit_hash ? event.commit_hash.slice(0, 7) + ": " : "";
|
|
160
|
+
content = `commit: "${hash}${content}"`;
|
|
161
|
+
} else if (event.type === "tool_call") {
|
|
162
|
+
const tool = event.tool_name || "";
|
|
163
|
+
const target = content ? ` → ${content}` : "";
|
|
164
|
+
content = `${tool}${target}`;
|
|
165
|
+
} else {
|
|
166
|
+
content = `"${content}"`;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
lines.push(`- ${time} ${icon} ${content}`);
|
|
170
|
+
}
|
|
171
|
+
lines.push("");
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
if (events.length === params.limit) {
|
|
175
|
+
lines.push(`_Showing ${params.limit} events (offset ${params.offset}). Use offset=${params.offset + params.limit} for more._`);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
return { content: [{ type: "text", text: lines.join("\n") }] };
|
|
179
|
+
}
|
|
180
|
+
);
|
|
181
|
+
}
|