evrex-mcp 0.7.0 → 0.8.1

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/dist/pretool.js CHANGED
@@ -49,7 +49,14 @@ var evrexApi = {
49
49
  // Evidence-only retrieval (BM25 + embedding), no LLM synthesis call — see
50
50
  // apps/backend/src/query/query.service.ts#search. Used by evrex_search,
51
51
  // which wants ranked hits fast, not a synthesized paragraph.
52
- search: (repoPath, text, filePaths) => post("/search", { repoPath, text, filePaths })
52
+ search: (repoPath, text, filePaths) => post("/search", { repoPath, text, filePaths }),
53
+ // Everything that happened in a repo, newest first, bounded by days — the
54
+ // same query the desktop Timeline screen makes. Sessions and commits
55
+ // interleaved, each with the handle evrex_expand takes.
56
+ feedback: (body) => post("/feedback", body),
57
+ timeline: (repoPath, days) => get(
58
+ `/timeline?repoPath=${encodeURIComponent(repoPath)}&days=${encodeURIComponent(String(days))}`
59
+ )
53
60
  };
54
61
 
55
62
  // src/hook-runtime.ts
@@ -91,6 +98,9 @@ function confidenceLabel(p) {
91
98
  if (p.status === "inferred") return ` ${Math.round((p.confidence ?? 0) * 100)}%`;
92
99
  return ` ${p.status}`;
93
100
  }
101
+ function handleFor(e) {
102
+ return e.kind === "commit" ? `commit:${e.refId.slice(0, 12)}` : `${e.kind}:${e.refId}`;
103
+ }
94
104
  function evidenceLabel(e) {
95
105
  return e.kind === "session" ? e.sourceKind ?? "session" : e.kind;
96
106
  }
@@ -125,14 +135,18 @@ function datedEvidence(items, root) {
125
135
  );
126
136
  }
127
137
 
128
- // src/pretool-state.ts
138
+ // src/hook-state.ts
129
139
  import { mkdirSync, readFileSync, renameSync, writeFileSync } from "node:fs";
130
140
  import { homedir } from "node:os";
131
141
  import { dirname, join } from "node:path";
132
142
  var MAX_SESSIONS = 40;
133
143
  var MAX_FILES = 200;
144
+ var MAX_SHOWN = 400;
134
145
  function statePath(home = homedir()) {
135
- return join(home, ".evrex", "pretool-state.json");
146
+ return join(home, ".evrex", "hook-state.json");
147
+ }
148
+ function recordKey(e) {
149
+ return `${e.kind}:${e.refId}`;
136
150
  }
137
151
  function readState(path) {
138
152
  try {
@@ -140,7 +154,16 @@ function readState(path) {
140
154
  if (!parsed || typeof parsed !== "object" || typeof parsed.sessions !== "object") {
141
155
  return { sessions: {} };
142
156
  }
143
- return { sessions: parsed.sessions ?? {} };
157
+ const sessions = {};
158
+ for (const [id, s] of Object.entries(parsed.sessions ?? {})) {
159
+ if (!s || typeof s !== "object") continue;
160
+ sessions[id] = {
161
+ at: typeof s.at === "string" ? s.at : (/* @__PURE__ */ new Date(0)).toISOString(),
162
+ files: Array.isArray(s.files) ? s.files.filter((f) => typeof f === "string") : [],
163
+ shown: Array.isArray(s.shown) ? s.shown.filter((f) => typeof f === "string") : []
164
+ };
165
+ }
166
+ return { sessions };
144
167
  } catch {
145
168
  return { sessions: {} };
146
169
  }
@@ -148,18 +171,35 @@ function readState(path) {
148
171
  function alreadySeen(state, sessionId, file) {
149
172
  return state.sessions[sessionId]?.files.includes(file) ?? false;
150
173
  }
151
- function remember(state, sessionId, file, now = /* @__PURE__ */ new Date()) {
152
- const existing = state.sessions[sessionId];
153
- const files = existing?.files.includes(file) ? existing.files : [...existing?.files ?? [], file].slice(-MAX_FILES);
154
- const sessions = {
155
- ...state.sessions,
156
- [sessionId]: { at: now.toISOString(), files }
157
- };
174
+ function shownIn(state, sessionId) {
175
+ return new Set(state.sessions[sessionId]?.shown ?? []);
176
+ }
177
+ function prune(sessions) {
158
178
  const ordered = Object.entries(sessions).sort(
159
179
  (a, b) => Date.parse(b[1].at) - Date.parse(a[1].at)
160
180
  );
161
181
  return { sessions: Object.fromEntries(ordered.slice(0, MAX_SESSIONS)) };
162
182
  }
183
+ function memoryOf(state, sessionId) {
184
+ const existing = state.sessions[sessionId];
185
+ return { at: existing?.at ?? "", files: existing?.files ?? [], shown: existing?.shown ?? [] };
186
+ }
187
+ function remember(state, sessionId, file, now = /* @__PURE__ */ new Date()) {
188
+ const m = memoryOf(state, sessionId);
189
+ const files = m.files.includes(file) ? m.files : [...m.files, file].slice(-MAX_FILES);
190
+ return prune({
191
+ ...state.sessions,
192
+ [sessionId]: { ...m, at: now.toISOString(), files }
193
+ });
194
+ }
195
+ function rememberShown(state, sessionId, keys, now = /* @__PURE__ */ new Date()) {
196
+ const m = memoryOf(state, sessionId);
197
+ const shown = [.../* @__PURE__ */ new Set([...m.shown, ...keys])].slice(-MAX_SHOWN);
198
+ return prune({
199
+ ...state.sessions,
200
+ [sessionId]: { ...m, at: now.toISOString(), shown }
201
+ });
202
+ }
163
203
  function writeState(path, state) {
164
204
  try {
165
205
  mkdirSync(dirname(path), { recursive: true });
@@ -212,7 +252,7 @@ function formatBlock(relPath, evidence, sessions) {
212
252
  parts.push(
213
253
  "EVIDENCE:\n" + evidence.map((e) => {
214
254
  const excerpt = e.excerpt.replace(/\s+/g, " ").trim().slice(0, MAX_EXCERPT);
215
- return `- [${evidenceLabel(e)} ${e.refId.slice(0, 8)}${confidenceLabel(e.provenance)}${dateLabel(e.at)}] ${excerpt}`;
255
+ return `- ${handleFor(e)} [${evidenceLabel(e)}${confidenceLabel(e.provenance)}${dateLabel(e.at)}] ${excerpt}`;
216
256
  }).join("\n")
217
257
  );
218
258
  parts.push(
@@ -256,13 +296,24 @@ async function main() {
256
296
  evrexApi.search(match.id, questionFor(relPath), [relPath]),
257
297
  left()
258
298
  );
259
- const evidence = datedEvidence(found?.evidence ?? [], match.root).filter(
299
+ const relevant = datedEvidence(found?.evidence ?? [], match.root).filter(
260
300
  (e) => (e.provenance?.confidence ?? 1) >= MIN_CONFIDENCE
261
301
  );
262
- if (sessionId) writeState(path, remember(state, sessionId, file));
263
- if (evidence.length === 0) return;
302
+ const shown = shownIn(state, sessionId);
303
+ const evidence = relevant.filter((e) => !shown.has(recordKey(e)));
304
+ if (sessionId) {
305
+ writeState(
306
+ path,
307
+ rememberShown(
308
+ remember(state, sessionId, file),
309
+ sessionId,
310
+ evidence.slice(0, MAX_ITEMS).map(recordKey)
311
+ )
312
+ );
313
+ }
314
+ if (relevant.length === 0) return;
264
315
  let sessions = [];
265
- const ids = [...new Set(evidence.filter((e) => e.kind === "session").map((e) => e.refId))].slice(
316
+ const ids = [...new Set(relevant.filter((e) => e.kind === "session").map((e) => e.refId))].slice(
266
317
  0,
267
318
  MAX_SESSIONS2
268
319
  );
package/dist/tickets.js CHANGED
@@ -63,7 +63,14 @@ var init_client = __esm({
63
63
  // Evidence-only retrieval (BM25 + embedding), no LLM synthesis call — see
64
64
  // apps/backend/src/query/query.service.ts#search. Used by evrex_search,
65
65
  // which wants ranked hits fast, not a synthesized paragraph.
66
- search: (repoPath, text, filePaths) => post("/search", { repoPath, text, filePaths })
66
+ search: (repoPath, text, filePaths) => post("/search", { repoPath, text, filePaths }),
67
+ // Everything that happened in a repo, newest first, bounded by days — the
68
+ // same query the desktop Timeline screen makes. Sessions and commits
69
+ // interleaved, each with the handle evrex_expand takes.
70
+ feedback: (body) => post("/feedback", body),
71
+ timeline: (repoPath, days) => get(
72
+ `/timeline?repoPath=${encodeURIComponent(repoPath)}&days=${encodeURIComponent(String(days))}`
73
+ )
67
74
  };
68
75
  }
69
76
  });
@@ -253,9 +260,22 @@ import { fileURLToPath } from "node:url";
253
260
  // ../../packages/ingest-core/src/types.ts
254
261
  var CONVERSATION_KINDS = [
255
262
  "claude-code",
263
+ // Xcode's coding assistant — the Claude Agent SDK embedded, writing the
264
+ // identical transcript format under ~/Library/Developer/Xcode. A separate
265
+ // kind because a conversation in an IDE panel is not a CLI session, and the
266
+ // Slack mislabel already taught this list what an absent entry costs.
267
+ "claude-xcode",
256
268
  "cursor",
257
269
  "codex",
258
270
  "gemini",
271
+ // OpenCode keeps its sessions in a SQLite store rather than files; the
272
+ // parser reads the store, so this kind arrives through the same importer
273
+ // as Cursor's.
274
+ "opencode",
275
+ // GitHub Copilot CLI writes an events file per session under ~/.copilot;
276
+ // the coding agent on github.com is a different thing and arrives as a
277
+ // reference through the Agent-Logs-Url trailer, not as this kind.
278
+ "copilot",
259
279
  "slack"
260
280
  ];
261
281
  var REFERENCE_KINDS = ["linear", "jira", "confluence"];
@@ -264,6 +284,9 @@ var SOURCE_KINDS = [
264
284
  ...REFERENCE_KINDS
265
285
  ];
266
286
 
287
+ // ../../packages/ingest-core/src/claude-sessions.ts
288
+ var MAX_TRANSCRIPT_BYTES = 256 * 1024 * 1024;
289
+
267
290
  // ../../packages/ingest-core/src/sanitize.ts
268
291
  var NUL = String.fromCharCode(0);
269
292
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "evrex-mcp",
3
- "version": "0.7.0",
3
+ "version": "0.8.1",
4
4
  "description": "MCP server that gives coding agents the recorded reasoning behind a repo: prior decisions, hard constraints, and approaches already rejected.",
5
5
  "keywords": [
6
6
  "mcp",
@@ -19,13 +19,15 @@
19
19
  "evrex-import": "dist/import.js",
20
20
  "evrex-tickets": "dist/tickets.js",
21
21
  "evrex-pretool": "dist/pretool.js",
22
- "evrex-continuity": "dist/continuity.js"
22
+ "evrex-continuity": "dist/continuity.js",
23
+ "evrex-account": "dist/account.js"
23
24
  },
24
25
  "files": [
25
26
  "dist/index.js",
26
27
  "dist/hook.js",
27
28
  "dist/pretool.js",
28
29
  "dist/continuity.js",
30
+ "dist/account.js",
29
31
  "dist/capture.js",
30
32
  "dist/import.js",
31
33
  "dist/tickets.js",