@reposkein/mcp 0.3.0 → 0.4.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/README.md +73 -1
- package/binary-digests.json +4 -4
- package/dist/SKILL.md +19 -0
- package/dist/cli/agentAdapters.d.ts +68 -0
- package/dist/cli/agentAdapters.js +223 -0
- package/dist/cli/agentAdapters.js.map +1 -0
- package/dist/cli/ansi.d.ts +24 -0
- package/dist/cli/ansi.js +40 -0
- package/dist/cli/ansi.js.map +1 -0
- package/dist/cli/doctor.d.ts +34 -2
- package/dist/cli/doctor.js +51 -6
- package/dist/cli/doctor.js.map +1 -1
- package/dist/cli/doctorFreshness.d.ts +43 -0
- package/dist/cli/doctorFreshness.js +111 -0
- package/dist/cli/doctorFreshness.js.map +1 -0
- package/dist/cli/doctorSummaries.d.ts +2 -0
- package/dist/cli/doctorSummaries.js +95 -0
- package/dist/cli/doctorSummaries.js.map +1 -0
- package/dist/cli/federatedDiscovery.d.ts +16 -0
- package/dist/cli/federatedDiscovery.js +75 -0
- package/dist/cli/federatedDiscovery.js.map +1 -0
- package/dist/cli/init.d.ts +69 -6
- package/dist/cli/init.js +194 -28
- package/dist/cli/init.js.map +1 -1
- package/dist/cli/sourceSlices.d.ts +17 -0
- package/dist/cli/sourceSlices.js +59 -0
- package/dist/cli/sourceSlices.js.map +1 -0
- package/dist/cli/stats.d.ts +29 -0
- package/dist/cli/stats.js +180 -0
- package/dist/cli/stats.js.map +1 -0
- package/dist/cli/view.d.ts +71 -3
- package/dist/cli/view.js +147 -12
- package/dist/cli/view.js.map +1 -1
- package/dist/index.d.ts +8 -0
- package/dist/index.js +286 -123
- package/dist/index.js.map +1 -1
- package/dist/indexer/fetchBinary.d.ts +14 -0
- package/dist/indexer/fetchBinary.js +81 -40
- package/dist/indexer/fetchBinary.js.map +1 -1
- package/dist/indexer/runIndexer.d.ts +0 -2
- package/dist/indexer/runIndexer.js +8 -4
- package/dist/indexer/runIndexer.js.map +1 -1
- package/dist/store/JsonlGraphStore.d.ts +13 -3
- package/dist/store/JsonlGraphStore.js +55 -17
- package/dist/store/JsonlGraphStore.js.map +1 -1
- package/dist/store/decisions.d.ts +20 -2
- package/dist/store/decisions.js +35 -2
- package/dist/store/decisions.js.map +1 -1
- package/dist/store/indexedAt.d.ts +18 -0
- package/dist/store/indexedAt.js +56 -0
- package/dist/store/indexedAt.js.map +1 -0
- package/dist/store/instrumentTool.d.ts +40 -0
- package/dist/store/instrumentTool.js +82 -0
- package/dist/store/instrumentTool.js.map +1 -0
- package/dist/store/repoContextCache.d.ts +15 -0
- package/dist/store/repoContextCache.js +26 -0
- package/dist/store/repoContextCache.js.map +1 -0
- package/dist/store/repoSession.d.ts +63 -0
- package/dist/store/repoSession.js +123 -0
- package/dist/store/repoSession.js.map +1 -0
- package/dist/store/resolveRepoPath.d.ts +31 -0
- package/dist/store/resolveRepoPath.js +85 -0
- package/dist/store/resolveRepoPath.js.map +1 -0
- package/dist/store/sessionLog.d.ts +98 -0
- package/dist/store/sessionLog.js +216 -0
- package/dist/store/sessionLog.js.map +1 -0
- package/dist/store/sessionStats.d.ts +55 -0
- package/dist/store/sessionStats.js +140 -0
- package/dist/store/sessionStats.js.map +1 -0
- package/dist/store/sidecar.d.ts +39 -3
- package/dist/store/sidecar.js +89 -5
- package/dist/store/sidecar.js.map +1 -1
- package/dist/store/summaryShards.d.ts +117 -0
- package/dist/store/summaryShards.js +291 -0
- package/dist/store/summaryShards.js.map +1 -0
- package/dist/store/teamConfig.d.ts +13 -0
- package/dist/store/teamConfig.js +49 -0
- package/dist/store/teamConfig.js.map +1 -0
- package/dist/tools/indexerTools.d.ts +14 -2
- package/dist/tools/indexerTools.js +17 -5
- package/dist/tools/indexerTools.js.map +1 -1
- package/dist/tools/recordDecision.js +6 -1
- package/dist/tools/recordDecision.js.map +1 -1
- package/dist/viz/assets/{graph.worker-CC7BeE-8.js → graph.worker-DkH6-lMU.js} +1 -1
- package/dist/viz/assets/index-CxqeGuRS.css +1 -0
- package/dist/viz/assets/index-DiNZyrXx.js +9 -0
- package/dist/viz/assets/jetbrains-mono-latin-400-normal-V6pRDFza.woff2 +0 -0
- package/dist/viz/assets/jetbrains-mono-latin-500-normal-BWZEU5yA.woff2 +0 -0
- package/dist/viz/assets/jetbrains-mono-latin-700-normal-BYuf6tUa.woff2 +0 -0
- package/dist/viz/assets/r3f-BZY3QR6_.js +336 -0
- package/dist/viz/assets/tanstack-CjXr-L-s.js +62 -0
- package/dist/viz/assets/three-Cpu5RUEM.js +4116 -0
- package/dist/viz/index.html +5 -4
- package/package.json +5 -4
- package/dist/viz/assets/index-C_8UkiJ8.js +0 -9
- package/dist/viz/assets/r3f-Cku1OhB-.js +0 -277
- package/dist/viz/assets/tanstack-DyXcNRWk.js +0 -77
- package/dist/viz/assets/three-Z8XaLGmf.js +0 -3839
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
/** One JSONL record per tool invocation, appended to the active repo's
|
|
2
|
+
* `.reposkein/local/sessions/<session-id>.jsonl` (REP-20). Never contains
|
|
3
|
+
* argument or result VALUES — only shapes/sizes — so it's safe to leave
|
|
4
|
+
* lying around on disk (though it's git-ignored regardless, see
|
|
5
|
+
* `.reposkein/.gitignore` written by the indexer: `local/`). */
|
|
6
|
+
export interface SessionLogRecord {
|
|
7
|
+
/** ISO-8601 timestamp of the call. */
|
|
8
|
+
ts: string;
|
|
9
|
+
/** MCP tool name, e.g. "get_context_profile". */
|
|
10
|
+
tool: string;
|
|
11
|
+
/** Argument KEY NAMES only, sorted — never values (may contain code/user text). */
|
|
12
|
+
argsShape: string[];
|
|
13
|
+
/** Byte size of the text content returned to the agent. */
|
|
14
|
+
resultBytes: number;
|
|
15
|
+
/** Node ids / file paths the call touched, when the result exposes them
|
|
16
|
+
* (best-effort — see `extractTouchedIds`). Omitted when none found. */
|
|
17
|
+
nodeIds?: string[];
|
|
18
|
+
/** False when the handler threw or returned `isError: true`. */
|
|
19
|
+
ok: boolean;
|
|
20
|
+
}
|
|
21
|
+
export interface ToolResultLike {
|
|
22
|
+
content?: Array<{
|
|
23
|
+
type: string;
|
|
24
|
+
text?: string;
|
|
25
|
+
}>;
|
|
26
|
+
isError?: boolean;
|
|
27
|
+
}
|
|
28
|
+
/** `<repoPath>/.reposkein/local/sessions` — where per-session JSONL logs
|
|
29
|
+
* live for this repo. Git-ignored via `.reposkein/.gitignore`'s `local/`
|
|
30
|
+
* entry (written by the indexer at index time). */
|
|
31
|
+
export declare function sessionsDir(repoPath: string): string;
|
|
32
|
+
export declare function sessionLogPath(repoPath: string, sessionId: string): string;
|
|
33
|
+
/** Session id = server process lifetime: start-timestamp + pid. Filesystem-
|
|
34
|
+
* safe (no colons) and lexicographically sortable = chronological. */
|
|
35
|
+
export declare function defaultSessionId(now?: Date, pid?: number): string;
|
|
36
|
+
/** Resolves this process's session id: `REPOSKEIN_SESSION_ID` override, else
|
|
37
|
+
* `defaultSessionId()`. Computed once per server process and reused for
|
|
38
|
+
* every call and every repo it touches during the connection's lifetime. */
|
|
39
|
+
export declare function resolveSessionId(env?: NodeJS.ProcessEnv, now?: Date, pid?: number): string;
|
|
40
|
+
/** Appends one record. Best-effort: a write failure (unwritable dir, full
|
|
41
|
+
* disk, permission error) is swallowed — logging must never fail or slow a
|
|
42
|
+
* tool call. Creates `local/sessions/` if needed. */
|
|
43
|
+
export declare function appendSessionLog(repoPath: string, sessionId: string, record: SessionLogRecord): void;
|
|
44
|
+
export interface PruneOptions {
|
|
45
|
+
/** Keep at most this many session files (newest first, by mtime). */
|
|
46
|
+
keep: number;
|
|
47
|
+
/** Drop any session file older than this many days, even if under `keep`. */
|
|
48
|
+
maxAgeDays: number;
|
|
49
|
+
}
|
|
50
|
+
export declare const DEFAULT_RETENTION: PruneOptions;
|
|
51
|
+
/** Prunes `local/sessions/` for one repo: a file survives only if it is both
|
|
52
|
+
* among the `keep` most-recently-modified files AND newer than
|
|
53
|
+
* `maxAgeDays`. Deleting one file's read/stat failure never aborts the
|
|
54
|
+
* sweep. Silently no-ops if the dir doesn't exist or isn't readable —
|
|
55
|
+
* pruning must never fail server startup. */
|
|
56
|
+
export declare function pruneSessions(repoPath: string, opts?: PruneOptions, now?: number): void;
|
|
57
|
+
/** Best-effort extraction of node ids / file paths a tool call touched, from
|
|
58
|
+
* its JSON-text result content — used for the "top queried nodes/files"
|
|
59
|
+
* stat. Generic (one implementation for all tools, not per-tool logic):
|
|
60
|
+
* parses each text block as JSON and walks it for a small allowlist of
|
|
61
|
+
* id-shaped keys (`node_id`, `id`, `decision_id`, `node_ids`,
|
|
62
|
+
* `anchor_node_ids`, `file_path`, `path`). Non-JSON or unparseable content
|
|
63
|
+
* contributes nothing — never throws. */
|
|
64
|
+
export declare function extractTouchedIds(result: ToolResultLike | undefined): string[];
|
|
65
|
+
/** Byte size of the text an agent would actually read from a tool result
|
|
66
|
+
* (sum of `content[].text`), the basis for "context tokens saved". Falls
|
|
67
|
+
* back to the full JSON envelope size only when there's no text content AT
|
|
68
|
+
* ALL (no `content` array, or no block of type "text") — a result whose
|
|
69
|
+
* text blocks exist but are empty strings correctly reports 0, it does not
|
|
70
|
+
* fall back to stringifying the envelope. */
|
|
71
|
+
export declare function resultByteSize(result: ToolResultLike | undefined): number;
|
|
72
|
+
/** Argument KEY NAMES only, sorted, never values. */
|
|
73
|
+
export declare function argsShapeOf(args: unknown): string[];
|
|
74
|
+
/** The single per-process instrumentation sink: one instance is created in
|
|
75
|
+
* `main()` and shared by the tool-dispatch wrapper for every registered
|
|
76
|
+
* tool (see `createToolLogger`/`withLog` in instrumentTool.ts) — the one
|
|
77
|
+
* place tool calls get logged, so individual tool handlers never contain
|
|
78
|
+
* logging code.
|
|
79
|
+
*
|
|
80
|
+
* Retention is enforced lazily, per repo, the first time this process logs
|
|
81
|
+
* a call for that repo — equivalent to "prune on server start" for the
|
|
82
|
+
* common single-repo case, and the only sound definition of "start" once a
|
|
83
|
+
* session can touch more than one repo (zero-config workspace mode). */
|
|
84
|
+
export declare class SessionLogger {
|
|
85
|
+
private readonly sessionId;
|
|
86
|
+
private readonly retention;
|
|
87
|
+
private readonly prunedRepos;
|
|
88
|
+
constructor(sessionId: string, retention?: PruneOptions);
|
|
89
|
+
getSessionId(): string;
|
|
90
|
+
log(repoPath: string, entry: Omit<SessionLogRecord, "ts">): void;
|
|
91
|
+
/** Same as `log`, but with `ts` supplied by the caller instead of stamped
|
|
92
|
+
* here — for the deferred-write path (see `instrumentTool.ts`), where the
|
|
93
|
+
* call's completion time is captured synchronously on the hot path and
|
|
94
|
+
* the actual write happens later (setImmediate); using that captured
|
|
95
|
+
* `ts` keeps the record's timestamp accurate to when the call finished,
|
|
96
|
+
* not to whenever the deferred write got scheduled. */
|
|
97
|
+
logAt(ts: string, repoPath: string, entry: Omit<SessionLogRecord, "ts">): void;
|
|
98
|
+
}
|
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
import { appendFileSync, existsSync, mkdirSync, readdirSync, statSync, unlinkSync } from "node:fs";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
/** `<repoPath>/.reposkein/local/sessions` — where per-session JSONL logs
|
|
4
|
+
* live for this repo. Git-ignored via `.reposkein/.gitignore`'s `local/`
|
|
5
|
+
* entry (written by the indexer at index time). */
|
|
6
|
+
export function sessionsDir(repoPath) {
|
|
7
|
+
return join(repoPath, ".reposkein", "local", "sessions");
|
|
8
|
+
}
|
|
9
|
+
export function sessionLogPath(repoPath, sessionId) {
|
|
10
|
+
return join(sessionsDir(repoPath), `${sessionId}.jsonl`);
|
|
11
|
+
}
|
|
12
|
+
function pad(n, len = 2) {
|
|
13
|
+
return String(n).padStart(len, "0");
|
|
14
|
+
}
|
|
15
|
+
/** Session id = server process lifetime: start-timestamp + pid. Filesystem-
|
|
16
|
+
* safe (no colons) and lexicographically sortable = chronological. */
|
|
17
|
+
export function defaultSessionId(now = new Date(), pid = process.pid) {
|
|
18
|
+
const stamp = `${now.getUTCFullYear()}${pad(now.getUTCMonth() + 1)}${pad(now.getUTCDate())}` +
|
|
19
|
+
`T${pad(now.getUTCHours())}${pad(now.getUTCMinutes())}${pad(now.getUTCSeconds())}Z`;
|
|
20
|
+
return `${stamp}-${pid}`;
|
|
21
|
+
}
|
|
22
|
+
/** Resolves this process's session id: `REPOSKEIN_SESSION_ID` override, else
|
|
23
|
+
* `defaultSessionId()`. Computed once per server process and reused for
|
|
24
|
+
* every call and every repo it touches during the connection's lifetime. */
|
|
25
|
+
export function resolveSessionId(env = process.env, now = new Date(), pid = process.pid) {
|
|
26
|
+
const override = env.REPOSKEIN_SESSION_ID?.trim();
|
|
27
|
+
return override ? override : defaultSessionId(now, pid);
|
|
28
|
+
}
|
|
29
|
+
/** Appends one record. Best-effort: a write failure (unwritable dir, full
|
|
30
|
+
* disk, permission error) is swallowed — logging must never fail or slow a
|
|
31
|
+
* tool call. Creates `local/sessions/` if needed. */
|
|
32
|
+
export function appendSessionLog(repoPath, sessionId, record) {
|
|
33
|
+
try {
|
|
34
|
+
mkdirSync(sessionsDir(repoPath), { recursive: true });
|
|
35
|
+
appendFileSync(sessionLogPath(repoPath, sessionId), JSON.stringify(record) + "\n", "utf8");
|
|
36
|
+
}
|
|
37
|
+
catch {
|
|
38
|
+
// best-effort — see doc comment
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
export const DEFAULT_RETENTION = { keep: 50, maxAgeDays: 30 };
|
|
42
|
+
/** Prunes `local/sessions/` for one repo: a file survives only if it is both
|
|
43
|
+
* among the `keep` most-recently-modified files AND newer than
|
|
44
|
+
* `maxAgeDays`. Deleting one file's read/stat failure never aborts the
|
|
45
|
+
* sweep. Silently no-ops if the dir doesn't exist or isn't readable —
|
|
46
|
+
* pruning must never fail server startup. */
|
|
47
|
+
export function pruneSessions(repoPath, opts = DEFAULT_RETENTION, now = Date.now()) {
|
|
48
|
+
try {
|
|
49
|
+
const dir = sessionsDir(repoPath);
|
|
50
|
+
if (!existsSync(dir))
|
|
51
|
+
return;
|
|
52
|
+
const files = readdirSync(dir).filter((f) => f.endsWith(".jsonl"));
|
|
53
|
+
const withStats = files
|
|
54
|
+
.map((f) => {
|
|
55
|
+
const path = join(dir, f);
|
|
56
|
+
try {
|
|
57
|
+
return { path, mtimeMs: statSync(path).mtimeMs };
|
|
58
|
+
}
|
|
59
|
+
catch {
|
|
60
|
+
return null;
|
|
61
|
+
}
|
|
62
|
+
})
|
|
63
|
+
.filter((x) => x !== null)
|
|
64
|
+
.sort((a, b) => b.mtimeMs - a.mtimeMs); // newest first
|
|
65
|
+
const maxAgeMs = opts.maxAgeDays * 24 * 60 * 60 * 1000;
|
|
66
|
+
withStats.forEach((entry, idx) => {
|
|
67
|
+
const tooOld = now - entry.mtimeMs > maxAgeMs;
|
|
68
|
+
const overCount = idx >= opts.keep;
|
|
69
|
+
if (tooOld || overCount) {
|
|
70
|
+
try {
|
|
71
|
+
unlinkSync(entry.path);
|
|
72
|
+
}
|
|
73
|
+
catch {
|
|
74
|
+
// best-effort
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
catch {
|
|
80
|
+
// best-effort — see doc comment
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
/** Keys whose STRING value is treated as an id/path a tool call touched. */
|
|
84
|
+
const ID_KEYS = new Set(["node_id", "id", "decision_id"]);
|
|
85
|
+
/** Keys whose ARRAY value holds ids/paths (each string element collected). */
|
|
86
|
+
const ID_ARRAY_KEYS = new Set(["node_ids", "anchor_node_ids"]);
|
|
87
|
+
/** Keys whose STRING value is a file path a tool call touched. */
|
|
88
|
+
const PATH_KEYS = new Set(["file_path", "path"]);
|
|
89
|
+
const MAX_TOUCHED_IDS = 25;
|
|
90
|
+
const MAX_WALK_DEPTH = 6;
|
|
91
|
+
function walkForIds(value, out, depth) {
|
|
92
|
+
if (out.size >= MAX_TOUCHED_IDS || depth > MAX_WALK_DEPTH || value == null)
|
|
93
|
+
return;
|
|
94
|
+
if (Array.isArray(value)) {
|
|
95
|
+
for (const v of value)
|
|
96
|
+
walkForIds(v, out, depth + 1);
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
if (typeof value !== "object")
|
|
100
|
+
return;
|
|
101
|
+
for (const [key, v] of Object.entries(value)) {
|
|
102
|
+
if (out.size >= MAX_TOUCHED_IDS)
|
|
103
|
+
return;
|
|
104
|
+
if ((ID_KEYS.has(key) || PATH_KEYS.has(key)) && typeof v === "string") {
|
|
105
|
+
out.add(v);
|
|
106
|
+
}
|
|
107
|
+
else if (ID_ARRAY_KEYS.has(key) && Array.isArray(v)) {
|
|
108
|
+
for (const item of v) {
|
|
109
|
+
if (out.size >= MAX_TOUCHED_IDS)
|
|
110
|
+
break;
|
|
111
|
+
if (typeof item === "string")
|
|
112
|
+
out.add(item);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
else {
|
|
116
|
+
walkForIds(v, out, depth + 1);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
/** Best-effort extraction of node ids / file paths a tool call touched, from
|
|
121
|
+
* its JSON-text result content — used for the "top queried nodes/files"
|
|
122
|
+
* stat. Generic (one implementation for all tools, not per-tool logic):
|
|
123
|
+
* parses each text block as JSON and walks it for a small allowlist of
|
|
124
|
+
* id-shaped keys (`node_id`, `id`, `decision_id`, `node_ids`,
|
|
125
|
+
* `anchor_node_ids`, `file_path`, `path`). Non-JSON or unparseable content
|
|
126
|
+
* contributes nothing — never throws. */
|
|
127
|
+
export function extractTouchedIds(result) {
|
|
128
|
+
if (!result)
|
|
129
|
+
return [];
|
|
130
|
+
const found = new Set();
|
|
131
|
+
for (const block of result.content ?? []) {
|
|
132
|
+
if (found.size >= MAX_TOUCHED_IDS)
|
|
133
|
+
break;
|
|
134
|
+
if (block.type !== "text" || typeof block.text !== "string")
|
|
135
|
+
continue;
|
|
136
|
+
let parsed;
|
|
137
|
+
try {
|
|
138
|
+
parsed = JSON.parse(block.text);
|
|
139
|
+
}
|
|
140
|
+
catch {
|
|
141
|
+
continue;
|
|
142
|
+
}
|
|
143
|
+
walkForIds(parsed, found, 0);
|
|
144
|
+
}
|
|
145
|
+
return [...found];
|
|
146
|
+
}
|
|
147
|
+
/** Byte size of the text an agent would actually read from a tool result
|
|
148
|
+
* (sum of `content[].text`), the basis for "context tokens saved". Falls
|
|
149
|
+
* back to the full JSON envelope size only when there's no text content AT
|
|
150
|
+
* ALL (no `content` array, or no block of type "text") — a result whose
|
|
151
|
+
* text blocks exist but are empty strings correctly reports 0, it does not
|
|
152
|
+
* fall back to stringifying the envelope. */
|
|
153
|
+
export function resultByteSize(result) {
|
|
154
|
+
if (!result)
|
|
155
|
+
return 0;
|
|
156
|
+
const blocks = result.content ?? [];
|
|
157
|
+
let bytes = 0;
|
|
158
|
+
let hasTextBlock = false;
|
|
159
|
+
for (const block of blocks) {
|
|
160
|
+
if (block.type === "text" && typeof block.text === "string") {
|
|
161
|
+
hasTextBlock = true;
|
|
162
|
+
bytes += Buffer.byteLength(block.text, "utf8");
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
return hasTextBlock ? bytes : Buffer.byteLength(JSON.stringify(result), "utf8");
|
|
166
|
+
}
|
|
167
|
+
/** Argument KEY NAMES only, sorted, never values. */
|
|
168
|
+
export function argsShapeOf(args) {
|
|
169
|
+
if (!args || typeof args !== "object")
|
|
170
|
+
return [];
|
|
171
|
+
return Object.keys(args).sort();
|
|
172
|
+
}
|
|
173
|
+
/** The single per-process instrumentation sink: one instance is created in
|
|
174
|
+
* `main()` and shared by the tool-dispatch wrapper for every registered
|
|
175
|
+
* tool (see `createToolLogger`/`withLog` in instrumentTool.ts) — the one
|
|
176
|
+
* place tool calls get logged, so individual tool handlers never contain
|
|
177
|
+
* logging code.
|
|
178
|
+
*
|
|
179
|
+
* Retention is enforced lazily, per repo, the first time this process logs
|
|
180
|
+
* a call for that repo — equivalent to "prune on server start" for the
|
|
181
|
+
* common single-repo case, and the only sound definition of "start" once a
|
|
182
|
+
* session can touch more than one repo (zero-config workspace mode). */
|
|
183
|
+
export class SessionLogger {
|
|
184
|
+
sessionId;
|
|
185
|
+
retention;
|
|
186
|
+
prunedRepos = new Set();
|
|
187
|
+
constructor(sessionId, retention = DEFAULT_RETENTION) {
|
|
188
|
+
this.sessionId = sessionId;
|
|
189
|
+
this.retention = retention;
|
|
190
|
+
}
|
|
191
|
+
getSessionId() {
|
|
192
|
+
return this.sessionId;
|
|
193
|
+
}
|
|
194
|
+
log(repoPath, entry) {
|
|
195
|
+
this.logAt(new Date().toISOString(), repoPath, entry);
|
|
196
|
+
}
|
|
197
|
+
/** Same as `log`, but with `ts` supplied by the caller instead of stamped
|
|
198
|
+
* here — for the deferred-write path (see `instrumentTool.ts`), where the
|
|
199
|
+
* call's completion time is captured synchronously on the hot path and
|
|
200
|
+
* the actual write happens later (setImmediate); using that captured
|
|
201
|
+
* `ts` keeps the record's timestamp accurate to when the call finished,
|
|
202
|
+
* not to whenever the deferred write got scheduled. */
|
|
203
|
+
logAt(ts, repoPath, entry) {
|
|
204
|
+
try {
|
|
205
|
+
if (!this.prunedRepos.has(repoPath)) {
|
|
206
|
+
this.prunedRepos.add(repoPath);
|
|
207
|
+
pruneSessions(repoPath, this.retention);
|
|
208
|
+
}
|
|
209
|
+
appendSessionLog(repoPath, this.sessionId, { ts, ...entry });
|
|
210
|
+
}
|
|
211
|
+
catch {
|
|
212
|
+
// never fail/slow the tool call — see appendSessionLog's own try/catch too
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
//# sourceMappingURL=sessionLog.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"sessionLog.js","sourceRoot":"","sources":["../../src/store/sessionLog.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { type SessionLogRecord } from "./sessionLog.js";
|
|
2
|
+
/** grep-baseline token-ratio multiplier used for the "estimated context
|
|
3
|
+
* tokens saved" stat. Sourced from `mcp/bench/README.md`'s Track 1 retrieval
|
|
4
|
+
* benchmark ("Mean token ratio on structural tasks: 8.4x (grep / RepoSkein,
|
|
5
|
+
* generous-to-grep)") — a measured, repo-local number, not invented here.
|
|
6
|
+
* Always LABEL output built from this as an estimate: it's one repo's
|
|
7
|
+
* 5-task mean, generalized to every call in a session. */
|
|
8
|
+
export declare const GREP_BASELINE_MULTIPLIER = 8.4;
|
|
9
|
+
export interface SessionSummary {
|
|
10
|
+
sessionId: string;
|
|
11
|
+
/** Absolute path to the session's .jsonl file. */
|
|
12
|
+
file: string;
|
|
13
|
+
recordCount: number;
|
|
14
|
+
callsByTool: Record<string, number>;
|
|
15
|
+
/** Top node ids / file paths touched, most-referenced first. */
|
|
16
|
+
topTouched: Array<{
|
|
17
|
+
id: string;
|
|
18
|
+
count: number;
|
|
19
|
+
}>;
|
|
20
|
+
/** Count of ADRs recorded this session (record_decision calls). */
|
|
21
|
+
decisionsWritten: number;
|
|
22
|
+
/** Count of semantic summaries written this session (write_semantic_summary calls). */
|
|
23
|
+
summariesWritten: number;
|
|
24
|
+
/** ms between the first and last record's `ts`. 0 for a single-record session. */
|
|
25
|
+
durationMs: number;
|
|
26
|
+
totalResultBytes: number;
|
|
27
|
+
/** GREP_BASELINE_MULTIPLIER * totalResultBytes / 4 (≈4 bytes/token), i.e.
|
|
28
|
+
* the estimated token cost an equivalent grep-based approach would have
|
|
29
|
+
* paid for what RepoSkein returned in `totalResultBytes`. Labeled an
|
|
30
|
+
* estimate everywhere it's rendered — see GREP_BASELINE_MULTIPLIER. */
|
|
31
|
+
estimatedGrepTokens: number;
|
|
32
|
+
estimatedRepoSkeinTokens: number;
|
|
33
|
+
estimatedTokensSaved: number;
|
|
34
|
+
failedCalls: number;
|
|
35
|
+
}
|
|
36
|
+
/** Reads and parses one session's JSONL file. Malformed lines are skipped
|
|
37
|
+
* (never thrown) — a session log is best-effort telemetry, not a source of
|
|
38
|
+
* truth that must be pristine. */
|
|
39
|
+
export declare function readSessionLog(file: string): SessionLogRecord[];
|
|
40
|
+
/** Every session file for a repo, sorted newest-first (by mtime). */
|
|
41
|
+
export declare function listSessionFiles(repoPath: string): Array<{
|
|
42
|
+
sessionId: string;
|
|
43
|
+
file: string;
|
|
44
|
+
mtimeMs: number;
|
|
45
|
+
}>;
|
|
46
|
+
/** Aggregates one session's records into the report shape rendered by
|
|
47
|
+
* `reposkein-mcp stats`. Pure function over already-parsed records so it's
|
|
48
|
+
* cheap to unit test independent of the filesystem. */
|
|
49
|
+
export declare function summarizeSession(sessionId: string, file: string, records: SessionLogRecord[]): SessionSummary;
|
|
50
|
+
/** Merges several sessions' records into one combined summary (`--all`) —
|
|
51
|
+
* same aggregation, over the union of records, with a synthetic id. */
|
|
52
|
+
export declare function summarizeSessions(sessions: Array<{
|
|
53
|
+
sessionId: string;
|
|
54
|
+
file: string;
|
|
55
|
+
}>): SessionSummary;
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
import { existsSync, readFileSync, readdirSync, statSync } from "node:fs";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { sessionsDir } from "./sessionLog.js";
|
|
4
|
+
/** grep-baseline token-ratio multiplier used for the "estimated context
|
|
5
|
+
* tokens saved" stat. Sourced from `mcp/bench/README.md`'s Track 1 retrieval
|
|
6
|
+
* benchmark ("Mean token ratio on structural tasks: 8.4x (grep / RepoSkein,
|
|
7
|
+
* generous-to-grep)") — a measured, repo-local number, not invented here.
|
|
8
|
+
* Always LABEL output built from this as an estimate: it's one repo's
|
|
9
|
+
* 5-task mean, generalized to every call in a session. */
|
|
10
|
+
export const GREP_BASELINE_MULTIPLIER = 8.4;
|
|
11
|
+
/** Reads and parses one session's JSONL file. Malformed lines are skipped
|
|
12
|
+
* (never thrown) — a session log is best-effort telemetry, not a source of
|
|
13
|
+
* truth that must be pristine. */
|
|
14
|
+
export function readSessionLog(file) {
|
|
15
|
+
if (!existsSync(file))
|
|
16
|
+
return [];
|
|
17
|
+
let text;
|
|
18
|
+
try {
|
|
19
|
+
text = readFileSync(file, "utf8");
|
|
20
|
+
}
|
|
21
|
+
catch {
|
|
22
|
+
return [];
|
|
23
|
+
}
|
|
24
|
+
const out = [];
|
|
25
|
+
for (const line of text.split("\n")) {
|
|
26
|
+
if (line.trim() === "")
|
|
27
|
+
continue;
|
|
28
|
+
try {
|
|
29
|
+
const rec = JSON.parse(line);
|
|
30
|
+
if (typeof rec.tool === "string" && typeof rec.ts === "string") {
|
|
31
|
+
out.push({
|
|
32
|
+
ts: rec.ts,
|
|
33
|
+
tool: rec.tool,
|
|
34
|
+
argsShape: Array.isArray(rec.argsShape) ? rec.argsShape : [],
|
|
35
|
+
resultBytes: typeof rec.resultBytes === "number" ? rec.resultBytes : 0,
|
|
36
|
+
...(Array.isArray(rec.nodeIds) && rec.nodeIds.length ? { nodeIds: rec.nodeIds } : {}),
|
|
37
|
+
ok: rec.ok !== false,
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
catch {
|
|
42
|
+
// skip malformed line
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
return out;
|
|
46
|
+
}
|
|
47
|
+
/** Every session file for a repo, sorted newest-first (by mtime). */
|
|
48
|
+
export function listSessionFiles(repoPath) {
|
|
49
|
+
const dir = sessionsDir(repoPath);
|
|
50
|
+
if (!existsSync(dir))
|
|
51
|
+
return [];
|
|
52
|
+
let names;
|
|
53
|
+
try {
|
|
54
|
+
names = readdirSync(dir).filter((f) => f.endsWith(".jsonl"));
|
|
55
|
+
}
|
|
56
|
+
catch {
|
|
57
|
+
return [];
|
|
58
|
+
}
|
|
59
|
+
return names
|
|
60
|
+
.map((name) => {
|
|
61
|
+
const file = join(dir, name);
|
|
62
|
+
let mtimeMs = 0;
|
|
63
|
+
try {
|
|
64
|
+
mtimeMs = statSync(file).mtimeMs;
|
|
65
|
+
}
|
|
66
|
+
catch {
|
|
67
|
+
/* keep 0 — still listed, just sorts last */
|
|
68
|
+
}
|
|
69
|
+
return { sessionId: name.replace(/\.jsonl$/, ""), file, mtimeMs };
|
|
70
|
+
})
|
|
71
|
+
.sort((a, b) => b.mtimeMs - a.mtimeMs);
|
|
72
|
+
}
|
|
73
|
+
/** Tools whose calls count as "summaries/ADRs written this session". */
|
|
74
|
+
const DECISION_TOOLS = new Set(["record_decision"]);
|
|
75
|
+
const SUMMARY_TOOLS = new Set(["write_semantic_summary"]);
|
|
76
|
+
/** Aggregates one session's records into the report shape rendered by
|
|
77
|
+
* `reposkein-mcp stats`. Pure function over already-parsed records so it's
|
|
78
|
+
* cheap to unit test independent of the filesystem. */
|
|
79
|
+
export function summarizeSession(sessionId, file, records) {
|
|
80
|
+
const callsByTool = {};
|
|
81
|
+
const touchedCounts = new Map();
|
|
82
|
+
let decisionsWritten = 0;
|
|
83
|
+
let summariesWritten = 0;
|
|
84
|
+
let totalResultBytes = 0;
|
|
85
|
+
let failedCalls = 0;
|
|
86
|
+
let minTs = Infinity;
|
|
87
|
+
let maxTs = -Infinity;
|
|
88
|
+
for (const rec of records) {
|
|
89
|
+
callsByTool[rec.tool] = (callsByTool[rec.tool] ?? 0) + 1;
|
|
90
|
+
totalResultBytes += rec.resultBytes;
|
|
91
|
+
if (!rec.ok)
|
|
92
|
+
failedCalls++;
|
|
93
|
+
if (DECISION_TOOLS.has(rec.tool))
|
|
94
|
+
decisionsWritten++;
|
|
95
|
+
if (SUMMARY_TOOLS.has(rec.tool))
|
|
96
|
+
summariesWritten++;
|
|
97
|
+
for (const id of rec.nodeIds ?? [])
|
|
98
|
+
touchedCounts.set(id, (touchedCounts.get(id) ?? 0) + 1);
|
|
99
|
+
const t = Date.parse(rec.ts);
|
|
100
|
+
if (!Number.isNaN(t)) {
|
|
101
|
+
if (t < minTs)
|
|
102
|
+
minTs = t;
|
|
103
|
+
if (t > maxTs)
|
|
104
|
+
maxTs = t;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
const topTouched = [...touchedCounts.entries()]
|
|
108
|
+
.sort((a, b) => b[1] - a[1] || a[0].localeCompare(b[0]))
|
|
109
|
+
.slice(0, 10)
|
|
110
|
+
.map(([id, count]) => ({ id, count }));
|
|
111
|
+
const durationMs = Number.isFinite(minTs) && Number.isFinite(maxTs) && maxTs > minTs ? maxTs - minTs : 0;
|
|
112
|
+
// ~4 bytes/token is the common rough estimator (also used by mcp/bench —
|
|
113
|
+
// see its token cost model) — deliberately simple and labeled an estimate.
|
|
114
|
+
const estimatedRepoSkeinTokens = Math.round(totalResultBytes / 4);
|
|
115
|
+
const estimatedGrepTokens = Math.round(estimatedRepoSkeinTokens * GREP_BASELINE_MULTIPLIER);
|
|
116
|
+
const estimatedTokensSaved = estimatedGrepTokens - estimatedRepoSkeinTokens;
|
|
117
|
+
return {
|
|
118
|
+
sessionId,
|
|
119
|
+
file,
|
|
120
|
+
recordCount: records.length,
|
|
121
|
+
callsByTool,
|
|
122
|
+
topTouched,
|
|
123
|
+
decisionsWritten,
|
|
124
|
+
summariesWritten,
|
|
125
|
+
durationMs,
|
|
126
|
+
totalResultBytes,
|
|
127
|
+
estimatedGrepTokens,
|
|
128
|
+
estimatedRepoSkeinTokens,
|
|
129
|
+
estimatedTokensSaved,
|
|
130
|
+
failedCalls,
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
/** Merges several sessions' records into one combined summary (`--all`) —
|
|
134
|
+
* same aggregation, over the union of records, with a synthetic id. */
|
|
135
|
+
export function summarizeSessions(sessions) {
|
|
136
|
+
const allRecords = sessions.flatMap((s) => readSessionLog(s.file));
|
|
137
|
+
const label = sessions.length === 1 ? sessions[0].sessionId : `${sessions.length} sessions`;
|
|
138
|
+
return summarizeSession(label, sessions.map((s) => s.file).join(","), allRecords);
|
|
139
|
+
}
|
|
140
|
+
//# sourceMappingURL=sessionStats.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"sessionStats.js","sourceRoot":"","sources":["../../src/store/sessionStats.ts"],"names":[],"mappings":"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"}
|
package/dist/store/sidecar.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
|
|
1
|
+
import { type SummaryAccumulator } from "./summaryShards.js";
|
|
2
|
+
/** A persisted summary record (git-ignored .reposkein/local/summaries-<agent>.jsonl). */
|
|
2
3
|
export interface SidecarSummary {
|
|
3
4
|
id: string;
|
|
4
5
|
semantic_summary: string;
|
|
@@ -7,10 +8,45 @@ export interface SidecarSummary {
|
|
|
7
8
|
summary_at: string;
|
|
8
9
|
summary_by: string;
|
|
9
10
|
}
|
|
10
|
-
|
|
11
|
+
/** Directory holding every agent's sidecar. Git-ignored in full. */
|
|
12
|
+
export declare function sidecarDir(repoPath: string): string;
|
|
13
|
+
/** Filesystem-safe slug for the writing agent.
|
|
14
|
+
*
|
|
15
|
+
* Untrusted input: REPOSKEIN_AGENT comes from whatever launched the server, so
|
|
16
|
+
* it is reduced to `[a-z0-9._-]` and capped. A value that reduces to nothing
|
|
17
|
+
* (or is absent) falls back to "agent", which is also the pre-multi-agent
|
|
18
|
+
* name — so a single-agent setup keeps one stable file. */
|
|
19
|
+
export declare function agentSlug(agent?: string | undefined): string;
|
|
20
|
+
/** THIS process's sidecar: one file per agent.
|
|
21
|
+
*
|
|
22
|
+
* A single shared `local/summaries.jsonl` meant two agents running against the
|
|
23
|
+
* same checkout silently overwrote each other — `upsertSidecar` rewrites the
|
|
24
|
+
* whole file, so the slower writer's prose vanished with no error anywhere.
|
|
25
|
+
* One file per writer removes the shared mutable state; the indexer folds
|
|
26
|
+
* every `local/summaries*.jsonl` into the committed shards. */
|
|
27
|
+
export declare function sidecarPath(repoPath: string, agent?: string): string;
|
|
28
|
+
/** Every agent's sidecar in this repo, sorted, so reads are deterministic and
|
|
29
|
+
* independent of directory order. Includes the pre-split
|
|
30
|
+
* `local/summaries.jsonl` an older client may still be writing. */
|
|
31
|
+
export declare function sidecarPaths(repoPath: string): string[];
|
|
11
32
|
/** Reads the sidecar into a map keyed by node id. Missing file → empty map.
|
|
12
33
|
* Best-effort: malformed lines / read errors are skipped. */
|
|
13
34
|
export declare function readSidecar(path: string): Map<string, SidecarSummary>;
|
|
35
|
+
/** Every agent's sidecar records, folded through the SAME rules the Rust
|
|
36
|
+
* indexer applies — `beats` within the sidecar set, losers preserved — so the
|
|
37
|
+
* server serves exactly the record the next `index` is going to write.
|
|
38
|
+
*
|
|
39
|
+
* This used to be a plain last-file-wins merge, which disagreed with the
|
|
40
|
+
* indexer whenever two agents summarised the same node, and dropped the loser
|
|
41
|
+
* with no trace. Callers should hand `conflicts` to `recordSummaryConflicts`. */
|
|
42
|
+
export declare function loadAllSidecars(repoPath: string): SummaryAccumulator;
|
|
43
|
+
/** Convenience view of `loadAllSidecars` for callers that only want the
|
|
44
|
+
* winning record per node id. Divergence losers are dropped on the floor
|
|
45
|
+
* here, so prefer `loadAllSidecars` anywhere they can be recorded. */
|
|
46
|
+
export declare function readAllSidecars(repoPath: string): Map<string, SidecarSummary>;
|
|
14
47
|
/** Upserts one record and rewrites the sidecar sorted by id (deterministic, no
|
|
15
|
-
* duplicate lines). Creates the local/ dir if needed. Best-effort.
|
|
48
|
+
* duplicate lines). Creates the local/ dir if needed. Best-effort.
|
|
49
|
+
*
|
|
50
|
+
* Rewriting the whole file is safe only because `path` belongs to one agent:
|
|
51
|
+
* see `sidecarPath`. */
|
|
16
52
|
export declare function upsertSidecar(path: string, rec: SidecarSummary): void;
|