ai-hist 0.4.1 → 0.8.2
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 +34 -156
- package/dist/cli.d.ts +3 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +248 -0
- package/dist/cli.js.map +1 -0
- package/dist/cloud-client.d.ts +0 -1
- package/dist/cloud-client.d.ts.map +1 -1
- package/dist/cloud-client.js +0 -2
- package/dist/cloud-client.js.map +1 -1
- package/dist/index.d.ts +153 -211
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +263 -902
- package/dist/index.js.map +1 -1
- package/dist/mcp-server.d.ts +0 -9
- package/dist/mcp-server.d.ts.map +1 -1
- package/dist/mcp-server.js +52 -675
- package/dist/mcp-server.js.map +1 -1
- package/package.json +14 -8
- package/dist/cloud-push.d.ts +0 -47
- package/dist/cloud-push.d.ts.map +0 -1
- package/dist/cloud-push.js +0 -111
- package/dist/cloud-push.js.map +0 -1
- package/dist/cloud-push.test.d.ts +0 -2
- package/dist/cloud-push.test.d.ts.map +0 -1
- package/dist/cloud-push.test.js +0 -68
- package/dist/cloud-push.test.js.map +0 -1
- package/dist/jsonl-sources.d.ts +0 -35
- package/dist/jsonl-sources.d.ts.map +0 -1
- package/dist/jsonl-sources.js +0 -354
- package/dist/jsonl-sources.js.map +0 -1
- package/dist/mcp-smoke.test.d.ts +0 -2
- package/dist/mcp-smoke.test.d.ts.map +0 -1
- package/dist/mcp-smoke.test.js +0 -424
- package/dist/mcp-smoke.test.js.map +0 -1
- package/dist/pair-client.d.ts +0 -41
- package/dist/pair-client.d.ts.map +0 -1
- package/dist/pair-client.js +0 -60
- package/dist/pair-client.js.map +0 -1
- package/dist/pair-client.test.d.ts +0 -2
- package/dist/pair-client.test.d.ts.map +0 -1
- package/dist/pair-client.test.js +0 -118
- package/dist/pair-client.test.js.map +0 -1
- package/dist/trajectory-sources.d.ts +0 -36
- package/dist/trajectory-sources.d.ts.map +0 -1
- package/dist/trajectory-sources.js +0 -201
- package/dist/trajectory-sources.js.map +0 -1
package/dist/mcp-server.js
CHANGED
|
@@ -1,681 +1,58 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
/**
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
import { formatPairWarnings, pairCheck } from "./pair-client.js";
|
|
17
|
-
// ---------------------------------------------------------------------------
|
|
18
|
-
// Lazy singleton — opens on first tool call and reuses across all calls.
|
|
19
|
-
// ---------------------------------------------------------------------------
|
|
20
|
-
let _histPromise = null;
|
|
21
|
-
function normalizeProjectArg(value) {
|
|
22
|
-
const trimmed = value?.trim();
|
|
23
|
-
if (!trimmed)
|
|
24
|
-
return undefined;
|
|
25
|
-
return resolve(trimmed);
|
|
2
|
+
/** Thin MCP adapters over the public `ai-hist` TypeScript SDK. */
|
|
3
|
+
import { readFileSync } from 'node:fs';
|
|
4
|
+
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
5
|
+
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
6
|
+
import { z } from 'zod';
|
|
7
|
+
import { discoverSessions, getSession, getSessionEventsPage, listSessionCatalogPage, recent, search, stats, sync, } from './index.js';
|
|
8
|
+
const READ = { readOnlyHint: true, idempotentHint: true, openWorldHint: false };
|
|
9
|
+
const WRITE = { readOnlyHint: false, idempotentHint: true, openWorldHint: false };
|
|
10
|
+
const SOURCE = z.enum(['claude', 'codex', 'cursor', 'grok', 'relay', 'trajectory', 'opencode']);
|
|
11
|
+
const CATALOG_SOURCE = z.enum(['claude', 'codex', 'cursor', 'grok', 'relay', 'opencode']);
|
|
12
|
+
const packageVersion = JSON.parse(readFileSync(new URL('../package.json', import.meta.url), 'utf8')).version;
|
|
13
|
+
const server = new McpServer({ name: 'ai-hist', version: packageVersion }, { capabilities: { tools: {} } });
|
|
14
|
+
function result(value) {
|
|
15
|
+
return { content: [{ type: 'text', text: JSON.stringify(value, null, 2) }] };
|
|
26
16
|
}
|
|
27
|
-
function
|
|
28
|
-
for (let i = 2; i < process.argv.length; i++) {
|
|
29
|
-
const arg = process.argv[i];
|
|
30
|
-
if (arg === "--project" || arg === "--project-path") {
|
|
31
|
-
const next = process.argv[i + 1];
|
|
32
|
-
if (next && !next.startsWith("-")) {
|
|
33
|
-
const value = normalizeProjectArg(next);
|
|
34
|
-
if (value)
|
|
35
|
-
return value;
|
|
36
|
-
i++;
|
|
37
|
-
}
|
|
38
|
-
continue;
|
|
39
|
-
}
|
|
40
|
-
for (const prefix of ["--project=", "--project-path="]) {
|
|
41
|
-
if (arg.startsWith(prefix)) {
|
|
42
|
-
const value = normalizeProjectArg(arg.slice(prefix.length));
|
|
43
|
-
if (value)
|
|
44
|
-
return value;
|
|
45
|
-
}
|
|
46
|
-
}
|
|
47
|
-
}
|
|
48
|
-
return undefined;
|
|
49
|
-
}
|
|
50
|
-
function getHist() {
|
|
51
|
-
if (!_histPromise) {
|
|
52
|
-
_histPromise = openAiHist({ fallback: "jsonl", projectScope: configuredProjectScope() }).catch((err) => {
|
|
53
|
-
_histPromise = null;
|
|
54
|
-
throw err;
|
|
55
|
-
});
|
|
56
|
-
}
|
|
57
|
-
return _histPromise;
|
|
58
|
-
}
|
|
59
|
-
function fmtEntry(e, maxChars = 300) {
|
|
60
|
-
const dt = new Date(e.timestampMs).toISOString().slice(0, 16).replace("T", " ");
|
|
61
|
-
const proj = e.project ? ` [${e.project}]` : "";
|
|
62
|
-
const text = e.prompt.length > maxChars
|
|
63
|
-
? e.prompt.slice(0, maxChars).replace(/\n/g, " ") + "..."
|
|
64
|
-
: e.prompt.replace(/\n/g, " ");
|
|
65
|
-
return `#${e.id} ${dt} (${e.source})${proj}\n session_id: ${e.sessionId ?? "(none)"}\n ${text}`;
|
|
66
|
-
}
|
|
67
|
-
function fmtTrajectory(t, maxChars = 500) {
|
|
68
|
-
const dt = new Date(t.timestampMs).toISOString().slice(0, 16).replace("T", " ");
|
|
69
|
-
const title = t.task.title ?? "(untitled task)";
|
|
70
|
-
const project = t.projectId ? ` [${t.projectId}]` : "";
|
|
71
|
-
const persona = t.personaId ? ` persona: ${t.personaId}` : "";
|
|
72
|
-
const decisions = t.decisions
|
|
73
|
-
.map((d, index) => {
|
|
74
|
-
const alternatives = d.alternatives.length > 0 ? ` alternatives: ${d.alternatives.join(", ")}` : "";
|
|
75
|
-
return `${index + 1}. ${d.question}\n chosen: ${d.chosen}\n reasoning: ${d.reasoning}${alternatives}`;
|
|
76
|
-
})
|
|
77
|
-
.join("\n");
|
|
78
|
-
const retroParts = [
|
|
79
|
-
t.retrospective.summary ? `summary: ${t.retrospective.summary}` : "",
|
|
80
|
-
t.retrospective.approach ? `approach: ${t.retrospective.approach}` : "",
|
|
81
|
-
t.retrospective.learnings.length > 0 ? `learnings: ${t.retrospective.learnings.join("; ")}` : "",
|
|
82
|
-
t.retrospective.confidence != null ? `confidence: ${String(t.retrospective.confidence)}` : "",
|
|
83
|
-
].filter(Boolean);
|
|
84
|
-
const body = [
|
|
85
|
-
`trajectory_id: ${t.id}`,
|
|
86
|
-
`${dt}${project}${persona}`,
|
|
87
|
-
`task: ${title}`,
|
|
88
|
-
t.task.description ? `description: ${t.task.description}` : "",
|
|
89
|
-
decisions ? `decisions:\n${decisions}` : "",
|
|
90
|
-
retroParts.length > 0 ? `retrospective:\n${retroParts.join("\n")}` : "",
|
|
91
|
-
].filter(Boolean).join("\n");
|
|
92
|
-
return body.length > maxChars ? body.slice(0, maxChars).replace(/\n/g, " ") + "..." : body;
|
|
93
|
-
}
|
|
94
|
-
const READ_ONLY = { readOnlyHint: true, idempotentHint: true, openWorldHint: false };
|
|
95
|
-
const WRITE_LOCAL = { readOnlyHint: false, idempotentHint: true, openWorldHint: false };
|
|
96
|
-
const REMOTE_READ_ONLY = { readOnlyHint: true, idempotentHint: true, openWorldHint: true };
|
|
97
|
-
const SOURCE_SCHEMA = z.enum(["claude", "codex", "cursor", "grok", "relay", "trajectory", "opencode"]);
|
|
98
|
-
// ---------------------------------------------------------------------------
|
|
99
|
-
// Server
|
|
100
|
-
// ---------------------------------------------------------------------------
|
|
101
|
-
const server = new McpServer({ name: "ai-hist", version: "1.0.0" }, { capabilities: { tools: {}, prompts: {} } });
|
|
102
|
-
// ---------------------------------------------------------------------------
|
|
103
|
-
// System prompt
|
|
104
|
-
// ---------------------------------------------------------------------------
|
|
105
|
-
server.prompt("agent-history-guide", "How to search and use your AI coding agent history via ai-hist", () => ({
|
|
106
|
-
messages: [
|
|
107
|
-
{
|
|
108
|
-
role: "user",
|
|
109
|
-
content: {
|
|
110
|
-
type: "text",
|
|
111
|
-
text: `You are connected to the ai-hist MCP server, which gives you access to a
|
|
112
|
-
unified history of AI coding agent sessions across Claude Code, Codex, Cursor,
|
|
113
|
-
Grok, OpenCode, Agent Relay, and trajectories — all searchable in a single index.
|
|
114
|
-
|
|
115
|
-
## Available tools
|
|
116
|
-
|
|
117
|
-
- **search_history** — Full-text search across all prompts. Start here when looking
|
|
118
|
-
for past work on a specific topic, bug, feature, or keyword. Supports boolean
|
|
119
|
-
operators (AND, OR, NOT) and prefix wildcards (*).
|
|
120
|
-
|
|
121
|
-
- **get_session** — Read the full conversation thread for a session_id. Use after
|
|
122
|
-
search_history or recent_history to see the complete context of a session, and
|
|
123
|
-
to get the exact CLI command to resume it.
|
|
124
|
-
|
|
125
|
-
- **get_context** — Reconstruct what you were working on at a specific moment in
|
|
126
|
-
time. Shows the full session the entry belongs to, plus nearby entries from other
|
|
127
|
-
sessions within a configurable time window.
|
|
128
|
-
|
|
129
|
-
- **recent_history** — Browse what was worked on recently across all agents, or
|
|
130
|
-
filter by source and project.
|
|
131
|
-
|
|
132
|
-
- **pack_evidence** — Assemble a concise, token-budget-aware summary of the most
|
|
133
|
-
relevant past sessions before starting a new task. Each entry includes a resume
|
|
134
|
-
command so you can continue where you left off.
|
|
135
|
-
|
|
136
|
-
- **history_stats** — Get a quick overview of what is indexed: total counts by
|
|
137
|
-
source, date range, and top projects.
|
|
138
|
-
|
|
139
|
-
- **tag_session**, **untag_session**, **list_tags** — Organize sessions with
|
|
140
|
-
logical tags so future searches can filter to a named workstream.
|
|
141
|
-
|
|
142
|
-
- **search_trajectories** — Search compacted per-run decision trajectories: the
|
|
143
|
-
WHY behind persona work, including decisions and retrospectives.
|
|
144
|
-
|
|
145
|
-
- **why_for_task** — Return the best matching trajectory's decisions and
|
|
146
|
-
retrospective for a task query.
|
|
147
|
-
|
|
148
|
-
- **get_handoff** — Find sessions for a repo/branch and generate a warm-start
|
|
149
|
-
handoff brief so you can continue work in a different CLI. Use this when you
|
|
150
|
-
need to switch from Claude Code to Codex (or vice versa) because one is down
|
|
151
|
-
or quota-exhausted. Ask: "where did codex leave off on branch feat/auth in
|
|
152
|
-
/my-repo?" and this tool returns ranked candidates with resume commands and
|
|
153
|
-
a ready-to-paste warm-start command for the target CLI.
|
|
154
|
-
|
|
155
|
-
- **pair_check** — Ask relayhistory-cloud for advisory in-session warnings before
|
|
156
|
-
a prompt, plan, or tool action. It sends only minimal current context and returns
|
|
157
|
-
cited, scrubbed warnings from prior convergence events.
|
|
158
|
-
|
|
159
|
-
## Tips
|
|
160
|
-
|
|
161
|
-
- If a tool returns "database not found", the user should run \`ai-hist sync\` to
|
|
162
|
-
populate the database from their local history files.
|
|
163
|
-
- session_id values from search results can be passed directly to get_session.
|
|
164
|
-
- Use pack_evidence before starting a complex task to pull in relevant prior context.`,
|
|
165
|
-
},
|
|
166
|
-
},
|
|
167
|
-
],
|
|
168
|
-
}));
|
|
169
|
-
// ---------------------------------------------------------------------------
|
|
170
|
-
// Tools
|
|
171
|
-
// ---------------------------------------------------------------------------
|
|
172
|
-
server.tool("pair_check", "Ask relayhistory-cloud Pair for advisory warnings before a prompt, plan, or tool action. " +
|
|
173
|
-
"This shells to `ai-hist pair check --json`, which calls POST /v1/pair/check using stored " +
|
|
174
|
-
"relayhistory auth. Send only minimal current context: repo/cwd, " +
|
|
175
|
-
"task summary, files, pending tool/action, and a short recent prompt summary. The response cites " +
|
|
176
|
-
"scrubbed convergence-event evidence and never blocks by itself.", {
|
|
177
|
-
projectId: z.string().optional().describe("Canonical project id if known. Optional; server can infer from path/repo."),
|
|
178
|
-
repoPath: z.string().optional().describe("Local repository path or normalized repository identity."),
|
|
179
|
-
cwd: z.string().optional().describe("Current working directory for the session."),
|
|
180
|
-
gitRemote: z.string().optional().describe("Git remote URL if known, used by the server to infer project identity."),
|
|
181
|
-
task: z.string().optional().describe("Short current task or prompt summary. Do not send raw transcript."),
|
|
182
|
-
files: z.array(z.string()).optional().default([]).describe("Files in scope or about to be read/edited."),
|
|
183
|
-
tool: z.string().optional().describe("Pending tool name, for example Edit, Write, Bash, or apply_patch."),
|
|
184
|
-
target: z.string().optional().describe("Primary pending target, such as a file path or command target."),
|
|
185
|
-
action: z.string().optional().describe("Short action label, for example edit, write, read, run-tests, or plan."),
|
|
186
|
-
recentPrompt: z.string().optional().describe("Short recent prompt summary. Keep it minimal; do not send full transcript."),
|
|
187
|
-
limit: z.number().int().min(1).max(10).optional().default(5).describe("Maximum warnings to return."),
|
|
188
|
-
}, REMOTE_READ_ONLY, async ({ projectId, repoPath, cwd, gitRemote, task, files, tool, target, action, recentPrompt, limit }) => {
|
|
17
|
+
async function call(operation) {
|
|
189
18
|
try {
|
|
190
|
-
|
|
191
|
-
context: { projectId, repoPath, cwd, gitRemote, task, files, tool, target, action, recentPrompt },
|
|
192
|
-
limit,
|
|
193
|
-
});
|
|
194
|
-
const text = formatPairWarnings(result) || JSON.stringify({ decision: result.decision ?? "allow", warnings: [] }, null, 2);
|
|
195
|
-
return { content: [{ type: "text", text }] };
|
|
19
|
+
return result(await operation());
|
|
196
20
|
}
|
|
197
|
-
catch (
|
|
198
|
-
|
|
21
|
+
catch (error) {
|
|
22
|
+
const value = error;
|
|
23
|
+
return { content: [{ type: 'text', text: `${value.code ?? 'ERROR'}: ${value.message ?? String(error)}` }], isError: true };
|
|
199
24
|
}
|
|
200
|
-
}
|
|
201
|
-
server.tool(
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
return { content: [{ type: "text", text: "No results found." }] };
|
|
234
|
-
const lines = [`Found ${results.length} result(s):\n`];
|
|
235
|
-
for (const e of results)
|
|
236
|
-
lines.push(fmtEntry(e));
|
|
237
|
-
return { content: [{ type: "text", text: lines.join("\n\n") }] };
|
|
238
|
-
}
|
|
239
|
-
catch (err) {
|
|
240
|
-
return { content: [{ type: "text", text: `Error: ${String(err)}` }], isError: true };
|
|
241
|
-
}
|
|
242
|
-
});
|
|
243
|
-
server.tool("search_trajectories", "Search compacted per-run decision trajectories: task title/description, decisions, " +
|
|
244
|
-
"alternatives, reasoning, and retrospective summaries/learnings. Use this for the WHY " +
|
|
245
|
-
"behind prior persona work.", {
|
|
246
|
-
query: z.string().describe("Search query for decisions, task text, personaId, projectId, or retrospectives."),
|
|
247
|
-
limit: z
|
|
248
|
-
.number()
|
|
249
|
-
.int()
|
|
250
|
-
.min(1)
|
|
251
|
-
.max(50)
|
|
252
|
-
.optional()
|
|
253
|
-
.default(10)
|
|
254
|
-
.describe("Maximum number of trajectory results to return. Default: 10."),
|
|
255
|
-
}, READ_ONLY, async ({ query, limit }) => {
|
|
256
|
-
try {
|
|
257
|
-
const hist = await getHist();
|
|
258
|
-
const results = hist.searchTrajectories(query, { limit });
|
|
259
|
-
if (results.length === 0) {
|
|
260
|
-
return { content: [{ type: "text", text: "No trajectories found." }] };
|
|
261
|
-
}
|
|
262
|
-
const lines = [`Found ${results.length} trajectory result(s):\n`];
|
|
263
|
-
for (const t of results)
|
|
264
|
-
lines.push(fmtTrajectory(t));
|
|
265
|
-
return { content: [{ type: "text", text: lines.join("\n\n") }] };
|
|
266
|
-
}
|
|
267
|
-
catch (err) {
|
|
268
|
-
return { content: [{ type: "text", text: `Error: ${String(err)}` }], isError: true };
|
|
269
|
-
}
|
|
270
|
-
});
|
|
271
|
-
server.tool("why_for_task", "Return the decisions and retrospective for the best-matching compacted trajectory. " +
|
|
272
|
-
"Use this when starting a task and you need the prior WHY: question, chosen option, " +
|
|
273
|
-
"reasoning, alternatives, summary, approach, learnings, and confidence.", {
|
|
274
|
-
query: z.string().describe("Task or decision query to match against compacted trajectories."),
|
|
275
|
-
}, READ_ONLY, async ({ query }) => {
|
|
276
|
-
try {
|
|
277
|
-
const hist = await getHist();
|
|
278
|
-
const trajectory = hist.whyForTask(query);
|
|
279
|
-
if (!trajectory) {
|
|
280
|
-
return { content: [{ type: "text", text: "No matching trajectory found." }] };
|
|
281
|
-
}
|
|
282
|
-
return { content: [{ type: "text", text: JSON.stringify(trajectory, null, 2) }] };
|
|
283
|
-
}
|
|
284
|
-
catch (err) {
|
|
285
|
-
return { content: [{ type: "text", text: `Error: ${String(err)}` }], isError: true };
|
|
286
|
-
}
|
|
287
|
-
});
|
|
288
|
-
server.tool("tag_session", "Tag a source-specific session so future history searches can filter by logical workstream. " +
|
|
289
|
-
"If source is omitted and multiple sources have the same session_id, all matching sessions are tagged.", {
|
|
290
|
-
session_id: z.string().describe("Session ID to tag."),
|
|
291
|
-
tag: z.string().min(1).describe("Tag name, for example release-plan or auth-refactor."),
|
|
292
|
-
source: SOURCE_SCHEMA.optional().describe("Optional source to disambiguate the session ID."),
|
|
293
|
-
color: z.string().optional().describe("Optional display color label."),
|
|
294
|
-
}, WRITE_LOCAL, async ({ session_id, tag, source, color }) => {
|
|
295
|
-
try {
|
|
296
|
-
const hist = await getHist();
|
|
297
|
-
const sessions = hist.tagSession(session_id, tag, { source, color });
|
|
298
|
-
if (sessions.length === 0) {
|
|
299
|
-
return { content: [{ type: "text", text: `No session found for ${session_id}.` }], isError: true };
|
|
300
|
-
}
|
|
301
|
-
return {
|
|
302
|
-
content: [
|
|
303
|
-
{
|
|
304
|
-
type: "text",
|
|
305
|
-
text: JSON.stringify({ tag, taggedSessions: sessions, dbPath: hist.dbPath }, null, 2),
|
|
306
|
-
},
|
|
307
|
-
],
|
|
308
|
-
};
|
|
309
|
-
}
|
|
310
|
-
catch (err) {
|
|
311
|
-
return { content: [{ type: "text", text: `Error: ${String(err)}` }], isError: true };
|
|
312
|
-
}
|
|
313
|
-
});
|
|
314
|
-
server.tool("untag_session", "Remove a tag from a session.", {
|
|
315
|
-
session_id: z.string().describe("Session ID to untag."),
|
|
316
|
-
tag: z.string().min(1).describe("Tag name to remove."),
|
|
317
|
-
source: SOURCE_SCHEMA.optional().describe("Optional source to disambiguate the session ID."),
|
|
318
|
-
}, WRITE_LOCAL, async ({ session_id, tag, source }) => {
|
|
319
|
-
try {
|
|
320
|
-
const hist = await getHist();
|
|
321
|
-
const removed = hist.untagSession(session_id, tag, { source });
|
|
322
|
-
return { content: [{ type: "text", text: JSON.stringify({ tag, removedAssignments: removed }, null, 2) }] };
|
|
323
|
-
}
|
|
324
|
-
catch (err) {
|
|
325
|
-
return { content: [{ type: "text", text: `Error: ${String(err)}` }], isError: true };
|
|
326
|
-
}
|
|
327
|
-
});
|
|
328
|
-
server.tool("list_tags", "List available session tags and optionally include the sessions assigned to each tag.", {
|
|
329
|
-
tag: z.string().optional().describe("Optional tag name to inspect."),
|
|
330
|
-
include_sessions: z.boolean().optional().default(false).describe("Include tagged session summaries."),
|
|
331
|
-
}, READ_ONLY, async ({ tag, include_sessions }) => {
|
|
332
|
-
try {
|
|
333
|
-
const hist = await getHist();
|
|
334
|
-
const tags = hist.listTags({ tag, includeSessions: include_sessions });
|
|
335
|
-
return { content: [{ type: "text", text: JSON.stringify(tags, null, 2) }] };
|
|
336
|
-
}
|
|
337
|
-
catch (err) {
|
|
338
|
-
return { content: [{ type: "text", text: `Error: ${String(err)}` }], isError: true };
|
|
339
|
-
}
|
|
340
|
-
});
|
|
341
|
-
server.tool("get_session", "Retrieve all prompts from a specific session ordered oldest to newest. " +
|
|
342
|
-
"Use this after search_history or recent_history to read the full conversation " +
|
|
343
|
-
"thread for a session_id. Returns all prompts with timestamps and includes the " +
|
|
344
|
-
"exact CLI command (claude --resume, codex resume, etc.) to continue that session.", {
|
|
345
|
-
session_id: z
|
|
346
|
-
.string()
|
|
347
|
-
.describe("Session ID to retrieve. Obtain from the session_id field in search_history or recent_history results."),
|
|
348
|
-
source: SOURCE_SCHEMA.optional().describe("Optional source to disambiguate the session ID."),
|
|
349
|
-
tag: z.string().optional().describe("Only return the session if it has this tag."),
|
|
350
|
-
}, READ_ONLY, async ({ session_id, source, tag }) => {
|
|
351
|
-
try {
|
|
352
|
-
const hist = await getHist();
|
|
353
|
-
const entries = hist.getSession(session_id, { source, tag });
|
|
354
|
-
if (entries.length === 0) {
|
|
355
|
-
return { content: [{ type: "text", text: `No entries found for session ${session_id}.` }] };
|
|
356
|
-
}
|
|
357
|
-
const first = entries[0];
|
|
358
|
-
const resume = resumeCommand(first);
|
|
359
|
-
const lines = [
|
|
360
|
-
`Session ${session_id} — ${entries.length} entries (${first.source}${first.project ? ", " + first.project : ""})`,
|
|
361
|
-
];
|
|
362
|
-
if (resume)
|
|
363
|
-
lines.push(`Resume: ${resume}`);
|
|
364
|
-
lines.push("");
|
|
365
|
-
for (const e of entries) {
|
|
366
|
-
const dt = new Date(e.timestampMs).toISOString().slice(0, 16).replace("T", " ");
|
|
367
|
-
lines.push(`[${dt}] #${e.id}\n${e.prompt}`);
|
|
368
|
-
}
|
|
369
|
-
return { content: [{ type: "text", text: lines.join("\n\n") }] };
|
|
370
|
-
}
|
|
371
|
-
catch (err) {
|
|
372
|
-
return { content: [{ type: "text", text: `Error: ${String(err)}` }], isError: true };
|
|
373
|
-
}
|
|
374
|
-
});
|
|
375
|
-
server.tool("get_context", "Get temporal context around a specific history entry: all prompts from the same " +
|
|
376
|
-
"session, plus prompts from other sessions within a configurable time window. " +
|
|
377
|
-
"Use this when you remember roughly when you worked on something but not the exact " +
|
|
378
|
-
"search terms — useful for reconstructing the full picture of a working session. " +
|
|
379
|
-
"Obtain an entry ID from search_history or recent_history.", {
|
|
380
|
-
id: z
|
|
381
|
-
.number()
|
|
382
|
-
.int()
|
|
383
|
-
.describe("History entry ID. Obtain from the id field in search_history or recent_history results."),
|
|
384
|
-
window_minutes: z
|
|
385
|
-
.number()
|
|
386
|
-
.int()
|
|
387
|
-
.min(0)
|
|
388
|
-
.max(60)
|
|
389
|
-
.optional()
|
|
390
|
-
.default(5)
|
|
391
|
-
.describe("Minutes before and after the entry to include from other sessions. Default: 5."),
|
|
392
|
-
}, READ_ONLY, async ({ id, window_minutes }) => {
|
|
393
|
-
try {
|
|
394
|
-
const hist = await getHist();
|
|
395
|
-
const entry = hist.getEntry(id);
|
|
396
|
-
if (!entry) {
|
|
397
|
-
return { content: [{ type: "text", text: `No entry with id ${id}.` }] };
|
|
398
|
-
}
|
|
399
|
-
const lines = [];
|
|
400
|
-
if (entry.sessionId) {
|
|
401
|
-
const sessionEntries = hist.getSession(entry.sessionId);
|
|
402
|
-
lines.push(`=== Session ${entry.sessionId} (${sessionEntries.length} entries) ===`);
|
|
403
|
-
for (const e of sessionEntries) {
|
|
404
|
-
const dt = new Date(e.timestampMs).toISOString().slice(0, 16).replace("T", " ");
|
|
405
|
-
const marker = e.id === id ? ">>>" : " ";
|
|
406
|
-
lines.push(`${marker} [${dt}] #${e.id} (${e.source}) ${e.prompt.slice(0, 150).replace(/\n/g, " ")}`);
|
|
407
|
-
}
|
|
408
|
-
}
|
|
409
|
-
const windowMs = window_minutes * 60 * 1000;
|
|
410
|
-
const nearby = hist
|
|
411
|
-
.getInTimeWindow(entry.timestampMs, windowMs)
|
|
412
|
-
.filter((e) => e.sessionId !== entry.sessionId && e.id !== id);
|
|
413
|
-
if (nearby.length > 0) {
|
|
414
|
-
lines.push(`\n=== Nearby (±${window_minutes}min, other sessions) ===`);
|
|
415
|
-
for (const e of nearby) {
|
|
416
|
-
const dt = new Date(e.timestampMs).toISOString().slice(0, 16).replace("T", " ");
|
|
417
|
-
lines.push(` [${dt}] #${e.id} (${e.source}) ${e.prompt.slice(0, 150).replace(/\n/g, " ")}`);
|
|
418
|
-
}
|
|
419
|
-
}
|
|
420
|
-
return {
|
|
421
|
-
content: [{ type: "text", text: lines.length > 0 ? lines.join("\n") : "No context found." }],
|
|
422
|
-
};
|
|
423
|
-
}
|
|
424
|
-
catch (err) {
|
|
425
|
-
return { content: [{ type: "text", text: `Error: ${String(err)}` }], isError: true };
|
|
426
|
-
}
|
|
427
|
-
});
|
|
428
|
-
server.tool("recent_history", "List the most recent AI coding agent history entries across all sources, newest first. " +
|
|
429
|
-
"Use this to see what was worked on recently, to get a history entry ID for use with " +
|
|
430
|
-
"get_context, or to browse history without a specific keyword in mind. " +
|
|
431
|
-
"Optionally filter by agent source or project path.", {
|
|
432
|
-
n: z
|
|
433
|
-
.number()
|
|
434
|
-
.int()
|
|
435
|
-
.min(1)
|
|
436
|
-
.max(100)
|
|
437
|
-
.optional()
|
|
438
|
-
.default(20)
|
|
439
|
-
.describe("Number of entries to return. Default: 20."),
|
|
440
|
-
source: SOURCE_SCHEMA
|
|
441
|
-
.optional()
|
|
442
|
-
.describe("Filter to a specific agent source. Omit for all sources."),
|
|
443
|
-
project: z
|
|
444
|
-
.string()
|
|
445
|
-
.optional()
|
|
446
|
-
.describe('Filter by project directory path (substring match). Example: "/my-app" or "work/api".'),
|
|
447
|
-
tag: z.string().optional().describe("Filter to sessions tagged with this tag."),
|
|
448
|
-
}, READ_ONLY, async ({ n, source, project, tag }) => {
|
|
449
|
-
try {
|
|
450
|
-
const hist = await getHist();
|
|
451
|
-
const entries = hist.recent({ limit: n, source, project, tag });
|
|
452
|
-
if (entries.length === 0)
|
|
453
|
-
return { content: [{ type: "text", text: "No history entries found." }] };
|
|
454
|
-
const lines = [`Most recent ${entries.length} entries:\n`];
|
|
455
|
-
for (const e of entries)
|
|
456
|
-
lines.push(fmtEntry(e, 150));
|
|
457
|
-
return { content: [{ type: "text", text: lines.join("\n\n") }] };
|
|
458
|
-
}
|
|
459
|
-
catch (err) {
|
|
460
|
-
return { content: [{ type: "text", text: `Error: ${String(err)}` }], isError: true };
|
|
461
|
-
}
|
|
462
|
-
});
|
|
463
|
-
server.tool("recent_entries", "List the most recent AI coding agent history entries across all sources, newest first. " +
|
|
464
|
-
"This is the contract alias for recent_history.", {
|
|
465
|
-
limit: z
|
|
466
|
-
.number()
|
|
467
|
-
.int()
|
|
468
|
-
.min(1)
|
|
469
|
-
.max(100)
|
|
470
|
-
.optional()
|
|
471
|
-
.default(20)
|
|
472
|
-
.describe("Number of entries to return. Default: 20."),
|
|
473
|
-
project: z
|
|
474
|
-
.string()
|
|
475
|
-
.optional()
|
|
476
|
-
.describe('Filter by project directory/path or projectId (exact match in the SDK).'),
|
|
477
|
-
tag: z.string().optional().describe("Filter to sessions tagged with this tag."),
|
|
478
|
-
}, READ_ONLY, async ({ limit, project, tag }) => {
|
|
479
|
-
try {
|
|
480
|
-
const hist = await getHist();
|
|
481
|
-
const entries = hist.recent({ limit, project, tag });
|
|
482
|
-
if (entries.length === 0)
|
|
483
|
-
return { content: [{ type: "text", text: "No history entries found." }] };
|
|
484
|
-
const lines = [`Most recent ${entries.length} entries:\n`];
|
|
485
|
-
for (const e of entries)
|
|
486
|
-
lines.push(fmtEntry(e, 150));
|
|
487
|
-
return { content: [{ type: "text", text: lines.join("\n\n") }] };
|
|
488
|
-
}
|
|
489
|
-
catch (err) {
|
|
490
|
-
return { content: [{ type: "text", text: `Error: ${String(err)}` }], isError: true };
|
|
491
|
-
}
|
|
492
|
-
});
|
|
493
|
-
server.tool("pack_evidence", "Search history and assemble a concise evidence bundle ready to paste into a new " +
|
|
494
|
-
"LLM context window. Returns numbered entries with timestamps, source, project path, " +
|
|
495
|
-
"truncated prompt text, and the exact resume command for each session. " +
|
|
496
|
-
"Use this before starting work on a task to pull in relevant context from past sessions " +
|
|
497
|
-
"without exceeding token limits. Adjust tokens_per_entry to control how much of each " +
|
|
498
|
-
"prompt is included.", {
|
|
499
|
-
query: z.string().describe("Search query to find relevant past sessions."),
|
|
500
|
-
limit: z
|
|
501
|
-
.number()
|
|
502
|
-
.int()
|
|
503
|
-
.min(1)
|
|
504
|
-
.max(50)
|
|
505
|
-
.optional()
|
|
506
|
-
.default(10)
|
|
507
|
-
.describe("Maximum number of entries in the bundle. Default: 10."),
|
|
508
|
-
tokens_per_entry: z
|
|
509
|
-
.number()
|
|
510
|
-
.int()
|
|
511
|
-
.min(50)
|
|
512
|
-
.max(2000)
|
|
513
|
-
.optional()
|
|
514
|
-
.default(200)
|
|
515
|
-
.describe("Approximate token budget per entry's prompt text (~4 chars/token). Default: 200."),
|
|
516
|
-
}, READ_ONLY, async ({ query, limit, tokens_per_entry }) => {
|
|
517
|
-
try {
|
|
518
|
-
const hist = await getHist();
|
|
519
|
-
const entries = hist.search(query, { limit });
|
|
520
|
-
if (entries.length === 0)
|
|
521
|
-
return { content: [{ type: "text", text: `No results for "${query}".` }] };
|
|
522
|
-
const charsPerEntry = tokens_per_entry * 4;
|
|
523
|
-
const now = new Date().toISOString().slice(0, 16).replace("T", " ");
|
|
524
|
-
const lines = [`=== ai-hist pack: "${query}" | ${now} | ${entries.length} entries ===\n`];
|
|
525
|
-
for (let i = 0; i < entries.length; i++) {
|
|
526
|
-
const e = entries[i];
|
|
527
|
-
const dt = new Date(e.timestampMs).toISOString().slice(0, 16).replace("T", " ");
|
|
528
|
-
const proj = e.project ? ` ${e.project}` : "";
|
|
529
|
-
const text = e.prompt.length > charsPerEntry
|
|
530
|
-
? e.prompt.slice(0, charsPerEntry).replace(/\n/g, " ") + "..."
|
|
531
|
-
: e.prompt.replace(/\n/g, " ");
|
|
532
|
-
const resume = resumeCommand(e);
|
|
533
|
-
let entry = `[${i + 1}/${entries.length}] #${e.id} ${dt} ${e.source}${proj}\n ${text}`;
|
|
534
|
-
if (resume)
|
|
535
|
-
entry += `\n Resume: ${resume}`;
|
|
536
|
-
lines.push(entry);
|
|
537
|
-
}
|
|
538
|
-
return { content: [{ type: "text", text: lines.join("\n\n") }] };
|
|
539
|
-
}
|
|
540
|
-
catch (err) {
|
|
541
|
-
return { content: [{ type: "text", text: `Error: ${String(err)}` }], isError: true };
|
|
542
|
-
}
|
|
543
|
-
});
|
|
544
|
-
server.tool("history_stats", "Return a summary of your AI coding agent history: total entry count broken down by " +
|
|
545
|
-
"source, the date range of recorded sessions, and the ten most-active projects. " +
|
|
546
|
-
"Use this to confirm that ai-hist sync has run successfully, to see which agents have " +
|
|
547
|
-
"the most history, or to get an overview before deciding where to search.", READ_ONLY, async () => {
|
|
548
|
-
try {
|
|
549
|
-
const hist = await getHist();
|
|
550
|
-
const s = hist.stats();
|
|
551
|
-
const lines = [`Total entries: ${s.total.toLocaleString()}\n`];
|
|
552
|
-
lines.push("By source:");
|
|
553
|
-
for (const [src, count] of Object.entries(s.bySource)) {
|
|
554
|
-
lines.push(` ${src}: ${(count ?? 0).toLocaleString()}`);
|
|
555
|
-
}
|
|
556
|
-
if (s.firstTimestampMs && s.lastTimestampMs) {
|
|
557
|
-
const first = new Date(s.firstTimestampMs).toISOString().slice(0, 10);
|
|
558
|
-
const last = new Date(s.lastTimestampMs).toISOString().slice(0, 10);
|
|
559
|
-
lines.push(`\nDate range: ${first} → ${last}`);
|
|
560
|
-
}
|
|
561
|
-
if (s.byProject.length > 0) {
|
|
562
|
-
lines.push("\nTop projects:");
|
|
563
|
-
for (const p of s.byProject) {
|
|
564
|
-
lines.push(` ${String(p.count).padStart(6)} ${p.project}`);
|
|
565
|
-
}
|
|
566
|
-
}
|
|
567
|
-
lines.push(`\nData source: ${hist.sourceKind} (${hist.dbPath})`);
|
|
568
|
-
if (hist.projectScope)
|
|
569
|
-
lines.push(`Project scope: ${hist.projectScope}`);
|
|
570
|
-
return { content: [{ type: "text", text: lines.join("\n") }] };
|
|
571
|
-
}
|
|
572
|
-
catch (err) {
|
|
573
|
-
return { content: [{ type: "text", text: `Error: ${String(err)}` }], isError: true };
|
|
574
|
-
}
|
|
575
|
-
});
|
|
576
|
-
server.tool("stats", "Contract alias for history_stats. Return total counts by source, date range, top projects, and data source.", READ_ONLY, async () => {
|
|
577
|
-
try {
|
|
578
|
-
const hist = await getHist();
|
|
579
|
-
const s = hist.stats();
|
|
580
|
-
return {
|
|
581
|
-
content: [
|
|
582
|
-
{
|
|
583
|
-
type: "text",
|
|
584
|
-
text: JSON.stringify({
|
|
585
|
-
total: s.total,
|
|
586
|
-
bySource: s.bySource,
|
|
587
|
-
byProject: s.byProject,
|
|
588
|
-
firstTimestampMs: s.firstTimestampMs,
|
|
589
|
-
lastTimestampMs: s.lastTimestampMs,
|
|
590
|
-
sourceKind: hist.sourceKind,
|
|
591
|
-
dbPath: hist.dbPath,
|
|
592
|
-
projectScope: hist.projectScope,
|
|
593
|
-
}, null, 2),
|
|
594
|
-
},
|
|
595
|
-
],
|
|
596
|
-
};
|
|
597
|
-
}
|
|
598
|
-
catch (err) {
|
|
599
|
-
return { content: [{ type: "text", text: `Error: ${String(err)}` }], isError: true };
|
|
600
|
-
}
|
|
601
|
-
});
|
|
602
|
-
server.tool("get_handoff", "Find active sessions for a repo/branch and generate a warm-start handoff brief " +
|
|
603
|
-
"so you can continue work in a different CLI (e.g. pick up a Claude session in Codex " +
|
|
604
|
-
"or vice versa). Returns ranked candidate sessions with: the original goal, files " +
|
|
605
|
-
"touched (extracted from prompts), last user prompt, last assistant state, the resume " +
|
|
606
|
-
"command for the original CLI, and a ready-to-paste warm-start command for the target CLI. " +
|
|
607
|
-
"Use this when switching between Claude Code and Codex mid-task due to quota or outage.", {
|
|
608
|
-
repo: z
|
|
609
|
-
.string()
|
|
610
|
-
.optional()
|
|
611
|
-
.describe("Project directory path to filter by (substring match against session cwd). " +
|
|
612
|
-
'Example: "/my-app", "work/api". Omit to search across all projects.'),
|
|
613
|
-
branch: z
|
|
614
|
-
.string()
|
|
615
|
-
.optional()
|
|
616
|
-
.describe("Git branch name to filter by (substring match). " +
|
|
617
|
-
'Example: "feat/auth", "main". Requires ai-hist sync to have captured gitBranch.'),
|
|
618
|
-
source: z
|
|
619
|
-
.enum(["claude", "codex"])
|
|
620
|
-
.optional()
|
|
621
|
-
.describe("Filter to sessions from a specific CLI. Omit to search both Claude Code and Codex."),
|
|
622
|
-
limit: z
|
|
623
|
-
.number()
|
|
624
|
-
.int()
|
|
625
|
-
.min(1)
|
|
626
|
-
.max(10)
|
|
627
|
-
.optional()
|
|
628
|
-
.default(3)
|
|
629
|
-
.describe("Maximum number of handoff candidates to return. Default: 3."),
|
|
630
|
-
}, READ_ONLY, async ({ repo, branch, source, limit }) => {
|
|
631
|
-
try {
|
|
632
|
-
const hist = await getHist();
|
|
633
|
-
const candidates = hist.getHandoff({ repo, branch, source, limit });
|
|
634
|
-
if (candidates.length === 0) {
|
|
635
|
-
return {
|
|
636
|
-
content: [
|
|
637
|
-
{
|
|
638
|
-
type: "text",
|
|
639
|
-
text: "No sessions found matching the given filters. " +
|
|
640
|
-
"Run `ai-hist sync` to populate session metadata, then try again.",
|
|
641
|
-
},
|
|
642
|
-
],
|
|
643
|
-
};
|
|
644
|
-
}
|
|
645
|
-
const lines = [`Found ${candidates.length} handoff candidate(s):\n`];
|
|
646
|
-
for (let i = 0; i < candidates.length; i++) {
|
|
647
|
-
const c = candidates[i];
|
|
648
|
-
let dt = "unknown";
|
|
649
|
-
if (c.lastActivityMs != null && Number.isFinite(c.lastActivityMs)) {
|
|
650
|
-
dt = new Date(c.lastActivityMs).toISOString().slice(0, 16).replace("T", " ");
|
|
651
|
-
}
|
|
652
|
-
lines.push(`[${i + 1}] ${c.source.toUpperCase()} session confidence: ${(c.confidence * 100).toFixed(0)}%`);
|
|
653
|
-
lines.push(` session_id : ${c.sessionId}`);
|
|
654
|
-
lines.push(` cwd : ${c.cwd ?? "(unknown)"}`);
|
|
655
|
-
lines.push(` branch : ${c.gitBranch ?? "(unknown)"}`);
|
|
656
|
-
lines.push(` last seen : ${dt} (${c.promptCount} prompts)`);
|
|
657
|
-
lines.push(` goal : ${c.goal.slice(0, 150)}`);
|
|
658
|
-
lines.push(` last state : ${c.lastState.slice(0, 150)}`);
|
|
659
|
-
if (c.filesTouched.length > 0) {
|
|
660
|
-
lines.push(` files : ${c.filesTouched.slice(0, 8).join(", ")}`);
|
|
661
|
-
}
|
|
662
|
-
if (c.lastAssistantText) {
|
|
663
|
-
lines.push(` assistant : ${c.lastAssistantText.slice(0, 200).replace(/\n/g, " ")}`);
|
|
664
|
-
}
|
|
665
|
-
if (c.resumeCommand) {
|
|
666
|
-
lines.push(` resume : ${c.resumeCommand}`);
|
|
667
|
-
}
|
|
668
|
-
lines.push(` warm-start : ${c.warmStartCommand}`);
|
|
669
|
-
}
|
|
670
|
-
return { content: [{ type: "text", text: lines.join("\n") }] };
|
|
671
|
-
}
|
|
672
|
-
catch (err) {
|
|
673
|
-
return { content: [{ type: "text", text: `Error: ${String(err)}` }], isError: true };
|
|
674
|
-
}
|
|
675
|
-
});
|
|
676
|
-
// ---------------------------------------------------------------------------
|
|
677
|
-
// Start
|
|
678
|
-
// ---------------------------------------------------------------------------
|
|
679
|
-
const transport = new StdioServerTransport();
|
|
680
|
-
await server.connect(transport);
|
|
25
|
+
}
|
|
26
|
+
server.tool('search_history', 'Search already-indexed RelayHistory prompts.', {
|
|
27
|
+
query: z.string(), source: SOURCE.optional(), project: z.string().optional(), tag: z.string().optional(),
|
|
28
|
+
limit: z.number().int().min(1).max(1000).optional().default(20),
|
|
29
|
+
}, READ, ({ query, source, project, tag, limit }) => call(() => search(query, { source, project, tag, limit })));
|
|
30
|
+
server.tool('recent_history', 'List recent already-indexed history.', {
|
|
31
|
+
source: SOURCE.optional(), project: z.string().optional(), tag: z.string().optional(),
|
|
32
|
+
n: z.number().int().min(1).max(1000).optional().default(20),
|
|
33
|
+
before_ms: z.number().int().optional(),
|
|
34
|
+
}, READ, ({ source, project, tag, n, before_ms }) => call(() => recent({ source, project, tag, limit: n, beforeMs: before_ms })));
|
|
35
|
+
server.tool('list_sessions', 'Cache-only indexed session catalog listing. This never discovers or syncs.', {
|
|
36
|
+
sources: z.array(CATALOG_SOURCE).optional(), limit: z.number().int().min(1).max(1000).optional().default(20),
|
|
37
|
+
before_ms: z.number().int().optional(),
|
|
38
|
+
after: z.object({ lastActivityMs: z.number().int().nullable().optional(), source: z.string(), sessionId: z.string() }).optional(),
|
|
39
|
+
}, READ, ({ sources, limit, before_ms, after }) => call(() => listSessionCatalogPage({
|
|
40
|
+
sources, limit, beforeMs: before_ms,
|
|
41
|
+
after: after ? { ...after, lastActivityMs: after.lastActivityMs ?? null } : undefined,
|
|
42
|
+
})));
|
|
43
|
+
server.tool('discover_sessions', 'Explicit shallow provider discovery. Updates only the session catalog.', {
|
|
44
|
+
sources: z.array(CATALOG_SOURCE).optional(), limit: z.number().int().min(1).max(10000).optional(),
|
|
45
|
+
}, WRITE, (args) => call(() => discoverSessions(args)));
|
|
46
|
+
server.tool('get_session', 'Get indexed prompts for one session.', {
|
|
47
|
+
session_id: z.string(), source: SOURCE.optional(), tag: z.string().optional(),
|
|
48
|
+
}, READ, ({ session_id, source, tag }) => call(() => getSession(session_id, { source, tag })));
|
|
49
|
+
server.tool('get_session_events', 'Get one bounded page of normalized events.', {
|
|
50
|
+
session_id: z.string(), source: SOURCE.optional(), limit: z.number().int().min(1).max(1000).optional().default(200),
|
|
51
|
+
after: z.object({ tsMs: z.number().int(), id: z.number().int() }).optional(),
|
|
52
|
+
}, READ, ({ session_id, source, limit, after }) => call(() => getSessionEventsPage(session_id, { source, limit, after })));
|
|
53
|
+
server.tool('history_stats', 'Statistics for already-indexed RelayHistory data.', {
|
|
54
|
+
tag: z.string().optional(),
|
|
55
|
+
}, READ, ({ tag }) => call(() => stats({ tag })));
|
|
56
|
+
server.tool('sync', 'Explicit full provider ingestion into RelayHistory.', {}, WRITE, () => call(() => sync()));
|
|
57
|
+
await server.connect(new StdioServerTransport());
|
|
681
58
|
//# sourceMappingURL=mcp-server.js.map
|