@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,56 @@
|
|
|
1
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { execFileSync } from "node:child_process";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
/** Path to the file recording the git commit SHA that was HEAD the last time
|
|
5
|
+
* the mcp-side index invocation path (`reposkein-mcp index`, or `init`'s
|
|
6
|
+
* initial index) completed successfully. Lives under `.reposkein/local/` —
|
|
7
|
+
* per-machine scratch that's gitignored and never committed — so it never
|
|
8
|
+
* survives a `git clone`. That's deliberate: a fresh clone has no idea what
|
|
9
|
+
* commit (if any) its nonexistent graph was built from, which is exactly
|
|
10
|
+
* the state `doctor --ci`'s `graph_stale` check should fail on. */
|
|
11
|
+
export function indexedAtPath(repoPath) {
|
|
12
|
+
return join(repoPath, ".reposkein", "local", "indexed-at");
|
|
13
|
+
}
|
|
14
|
+
/** Records the current git HEAD as "the commit the graph was just built
|
|
15
|
+
* from". Best-effort and silent: not a git repo / no HEAD yet / a read-only
|
|
16
|
+
* filesystem all just skip the write — `graph_stale` degrades to "never
|
|
17
|
+
* recorded" (fails, conservatively) rather than the index step itself
|
|
18
|
+
* failing over a scratch-file write. */
|
|
19
|
+
export function writeIndexedAtMarker(repoPath) {
|
|
20
|
+
let sha;
|
|
21
|
+
try {
|
|
22
|
+
sha = execFileSync("git", ["-C", repoPath, "rev-parse", "HEAD"], {
|
|
23
|
+
encoding: "utf8",
|
|
24
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
25
|
+
}).trim();
|
|
26
|
+
}
|
|
27
|
+
catch {
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
if (!sha)
|
|
31
|
+
return;
|
|
32
|
+
try {
|
|
33
|
+
const path = indexedAtPath(repoPath);
|
|
34
|
+
mkdirSync(join(path, ".."), { recursive: true });
|
|
35
|
+
writeFileSync(path, sha + "\n", "utf8");
|
|
36
|
+
}
|
|
37
|
+
catch {
|
|
38
|
+
/* best-effort */
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
/** Reads the recorded indexed-at SHA, or null when never recorded — never
|
|
42
|
+
* indexed via the mcp-side path, or a fresh clone (local/ isn't committed,
|
|
43
|
+
* so it never carries over). */
|
|
44
|
+
export function readIndexedAtSha(repoPath) {
|
|
45
|
+
const path = indexedAtPath(repoPath);
|
|
46
|
+
if (!existsSync(path))
|
|
47
|
+
return null;
|
|
48
|
+
try {
|
|
49
|
+
const sha = readFileSync(path, "utf8").trim();
|
|
50
|
+
return sha || null;
|
|
51
|
+
}
|
|
52
|
+
catch {
|
|
53
|
+
return null;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
//# sourceMappingURL=indexedAt.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"indexedAt.js","sourceRoot":"","sources":["../../src/store/indexedAt.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,SAAS,EAAE,YAAY,EAAE,aAAa,EAAE,MAAM,SAAS,CAAC;AAC7E,OAAO,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAC;AAClD,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AAEjC;;;;;;oEAMoE;AACpE,MAAM,UAAU,aAAa,CAAC,QAAgB;IAC5C,OAAO,IAAI,CAAC,QAAQ,EAAE,YAAY,EAAE,OAAO,EAAE,YAAY,CAAC,CAAC;AAC7D,CAAC;AAED;;;;yCAIyC;AACzC,MAAM,UAAU,oBAAoB,CAAC,QAAgB;IACnD,IAAI,GAAW,CAAC;IAChB,IAAI,CAAC;QACH,GAAG,GAAG,YAAY,CAAC,KAAK,EAAE,CAAC,IAAI,EAAE,QAAQ,EAAE,WAAW,EAAE,MAAM,CAAC,EAAE;YAC/D,QAAQ,EAAE,MAAM;YAChB,KAAK,EAAE,CAAC,QAAQ,EAAE,MAAM,EAAE,QAAQ,CAAC;SACpC,CAAC,CAAC,IAAI,EAAE,CAAC;IACZ,CAAC;IAAC,MAAM,CAAC;QACP,OAAO;IACT,CAAC;IACD,IAAI,CAAC,GAAG;QAAE,OAAO;IACjB,IAAI,CAAC;QACH,MAAM,IAAI,GAAG,aAAa,CAAC,QAAQ,CAAC,CAAC;QACrC,SAAS,CAAC,IAAI,CAAC,IAAI,EAAE,IAAI,CAAC,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;QACjD,aAAa,CAAC,IAAI,EAAE,GAAG,GAAG,IAAI,EAAE,MAAM,CAAC,CAAC;IAC1C,CAAC;IAAC,MAAM,CAAC;QACP,iBAAiB;IACnB,CAAC;AACH,CAAC;AAED;;iCAEiC;AACjC,MAAM,UAAU,gBAAgB,CAAC,QAAgB;IAC/C,MAAM,IAAI,GAAG,aAAa,CAAC,QAAQ,CAAC,CAAC;IACrC,IAAI,CAAC,UAAU,CAAC,IAAI,CAAC;QAAE,OAAO,IAAI,CAAC;IACnC,IAAI,CAAC;QACH,MAAM,GAAG,GAAG,YAAY,CAAC,IAAI,EAAE,MAAM,CAAC,CAAC,IAAI,EAAE,CAAC;QAC9C,OAAO,GAAG,IAAI,IAAI,CAAC;IACrB,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,IAAI,CAAC;IACd,CAAC;AACH,CAAC"}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import type { RepoResolution } from "./resolveRepoPath.js";
|
|
2
|
+
import type { RepoSession } from "./repoSession.js";
|
|
3
|
+
import { type SessionLogger, type ToolResultLike } from "./sessionLog.js";
|
|
4
|
+
export interface ToolLogger {
|
|
5
|
+
/** `session.resolve()`, memoized for the lifetime of the CURRENT tool
|
|
6
|
+
* call (see `withLog`) — a handler that calls this (directly, or via
|
|
7
|
+
* `resolveActiveRepo`) and the logging wrapper both see the same
|
|
8
|
+
* filesystem walk, not two. Isolated per call via AsyncLocalStorage, so
|
|
9
|
+
* concurrent/overlapping tool calls never share a cached value. Falls
|
|
10
|
+
* back to a plain, uncached `session.resolve()` when called outside any
|
|
11
|
+
* `withLog`-wrapped call (e.g. server startup). */
|
|
12
|
+
cachedResolve(): RepoResolution;
|
|
13
|
+
/** Wraps a tool handler so its call is logged to the active repo's
|
|
14
|
+
* session log — see sessionLog.ts. `Args`/result type are inferred from
|
|
15
|
+
* the handler passed in, so call sites (`server.registerTool(name,
|
|
16
|
+
* config, withLog(name, async (args) => {...}))`) need no extra typing. */
|
|
17
|
+
withLog<Args, R extends ToolResultLike>(name: string, cb: (args: Args) => Promise<R>): (args: Args) => Promise<R>;
|
|
18
|
+
}
|
|
19
|
+
/** Builds the tool-call instrumentation for one MCP server connection: one
|
|
20
|
+
* `ToolLogger` shared across every `server.registerTool` call (REP-20).
|
|
21
|
+
*
|
|
22
|
+
* Two hot-path fixes from review round 1, both scoped to this module so
|
|
23
|
+
* they're covered without spinning up the whole server:
|
|
24
|
+
*
|
|
25
|
+
* 1. Single resolution per call — `cachedResolve()` memoizes
|
|
26
|
+
* `session.resolve()` per call (AsyncLocalStorage-scoped), so a
|
|
27
|
+
* handler's own resolution (via `resolveActiveRepo`, or a direct
|
|
28
|
+
* `cachedResolve()` call for the couple of tools that don't gate on a
|
|
29
|
+
* full repo context) and the logging wrapper's resolution are the same
|
|
30
|
+
* filesystem walk, not two.
|
|
31
|
+
* 2. Deferred write — the `finally` block below only captures cheap,
|
|
32
|
+
* already-computed references synchronously (tool name, argsShape, the
|
|
33
|
+
* resolved repo path, the result object BY REFERENCE, ok, ts) and
|
|
34
|
+
* returns immediately; JSON-parsing the result for `extractTouchedIds`
|
|
35
|
+
* and the actual file write happen in a `schedule()`-deferred callback
|
|
36
|
+
* (default `setImmediate`), which runs only after the response has
|
|
37
|
+
* already been handed back to the MCP transport. The deferred callback
|
|
38
|
+
* swallows every error itself — it must never surface as an unhandled
|
|
39
|
+
* exception/rejection. */
|
|
40
|
+
export declare function createToolLogger(session: RepoSession, sessionLogger: SessionLogger, schedule?: (fn: () => void) => void): ToolLogger;
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { AsyncLocalStorage } from "node:async_hooks";
|
|
2
|
+
import { argsShapeOf, extractTouchedIds, resultByteSize } from "./sessionLog.js";
|
|
3
|
+
/** Builds the tool-call instrumentation for one MCP server connection: one
|
|
4
|
+
* `ToolLogger` shared across every `server.registerTool` call (REP-20).
|
|
5
|
+
*
|
|
6
|
+
* Two hot-path fixes from review round 1, both scoped to this module so
|
|
7
|
+
* they're covered without spinning up the whole server:
|
|
8
|
+
*
|
|
9
|
+
* 1. Single resolution per call — `cachedResolve()` memoizes
|
|
10
|
+
* `session.resolve()` per call (AsyncLocalStorage-scoped), so a
|
|
11
|
+
* handler's own resolution (via `resolveActiveRepo`, or a direct
|
|
12
|
+
* `cachedResolve()` call for the couple of tools that don't gate on a
|
|
13
|
+
* full repo context) and the logging wrapper's resolution are the same
|
|
14
|
+
* filesystem walk, not two.
|
|
15
|
+
* 2. Deferred write — the `finally` block below only captures cheap,
|
|
16
|
+
* already-computed references synchronously (tool name, argsShape, the
|
|
17
|
+
* resolved repo path, the result object BY REFERENCE, ok, ts) and
|
|
18
|
+
* returns immediately; JSON-parsing the result for `extractTouchedIds`
|
|
19
|
+
* and the actual file write happen in a `schedule()`-deferred callback
|
|
20
|
+
* (default `setImmediate`), which runs only after the response has
|
|
21
|
+
* already been handed back to the MCP transport. The deferred callback
|
|
22
|
+
* swallows every error itself — it must never surface as an unhandled
|
|
23
|
+
* exception/rejection. */
|
|
24
|
+
export function createToolLogger(session, sessionLogger, schedule = setImmediate) {
|
|
25
|
+
const als = new AsyncLocalStorage();
|
|
26
|
+
function cachedResolve() {
|
|
27
|
+
const store = als.getStore();
|
|
28
|
+
if (!store)
|
|
29
|
+
return session.resolve();
|
|
30
|
+
if (!store.value)
|
|
31
|
+
store.value = session.resolve();
|
|
32
|
+
return store.value;
|
|
33
|
+
}
|
|
34
|
+
function deferredLog(ts, repoPath, tool, argsShape, ok, result) {
|
|
35
|
+
schedule(() => {
|
|
36
|
+
try {
|
|
37
|
+
const resultBytes = resultByteSize(result);
|
|
38
|
+
const nodeIds = extractTouchedIds(result);
|
|
39
|
+
sessionLogger.logAt(ts, repoPath, {
|
|
40
|
+
tool,
|
|
41
|
+
argsShape,
|
|
42
|
+
resultBytes,
|
|
43
|
+
...(nodeIds.length ? { nodeIds } : {}),
|
|
44
|
+
ok,
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
catch {
|
|
48
|
+
// deferred logging must never surface as an unhandled exception
|
|
49
|
+
}
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
function withLog(name, cb) {
|
|
53
|
+
return (args) => als.run({}, async () => {
|
|
54
|
+
let result;
|
|
55
|
+
let threw = false;
|
|
56
|
+
try {
|
|
57
|
+
result = await cb(args);
|
|
58
|
+
return result;
|
|
59
|
+
}
|
|
60
|
+
catch (err) {
|
|
61
|
+
threw = true;
|
|
62
|
+
throw err;
|
|
63
|
+
}
|
|
64
|
+
finally {
|
|
65
|
+
// Cheap, synchronous capture only — no JSON.parse, no fs calls on
|
|
66
|
+
// the hot path. `result` is captured BY REFERENCE; it isn't read
|
|
67
|
+
// until the deferred callback runs.
|
|
68
|
+
try {
|
|
69
|
+
const resolution = cachedResolve();
|
|
70
|
+
if (resolution.repoPath) {
|
|
71
|
+
deferredLog(new Date().toISOString(), resolution.repoPath, name, argsShapeOf(args), !threw && result?.isError !== true, result);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
catch {
|
|
75
|
+
// instrumentation must never affect the tool call
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
return { cachedResolve, withLog };
|
|
81
|
+
}
|
|
82
|
+
//# sourceMappingURL=instrumentTool.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"instrumentTool.js","sourceRoot":"","sources":["../../src/store/instrumentTool.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,iBAAiB,EAAE,MAAM,kBAAkB,CAAC;AAGrD,OAAO,EAAE,WAAW,EAAE,iBAAiB,EAAE,cAAc,EAA2C,MAAM,iBAAiB,CAAC;AAsB1H;;;;;;;;;;;;;;;;;;;;8BAoB8B;AAC9B,MAAM,UAAU,gBAAgB,CAC9B,OAAoB,EACpB,aAA4B,EAC5B,WAAqC,YAAY;IAEjD,MAAM,GAAG,GAAG,IAAI,iBAAiB,EAAa,CAAC;IAE/C,SAAS,aAAa;QACpB,MAAM,KAAK,GAAG,GAAG,CAAC,QAAQ,EAAE,CAAC;QAC7B,IAAI,CAAC,KAAK;YAAE,OAAO,OAAO,CAAC,OAAO,EAAE,CAAC;QACrC,IAAI,CAAC,KAAK,CAAC,KAAK;YAAE,KAAK,CAAC,KAAK,GAAG,OAAO,CAAC,OAAO,EAAE,CAAC;QAClD,OAAO,KAAK,CAAC,KAAK,CAAC;IACrB,CAAC;IAED,SAAS,WAAW,CAClB,EAAU,EACV,QAAgB,EAChB,IAAY,EACZ,SAAmB,EACnB,EAAW,EACX,MAAkC;QAElC,QAAQ,CAAC,GAAG,EAAE;YACZ,IAAI,CAAC;gBACH,MAAM,WAAW,GAAG,cAAc,CAAC,MAAM,CAAC,CAAC;gBAC3C,MAAM,OAAO,GAAG,iBAAiB,CAAC,MAAM,CAAC,CAAC;gBAC1C,aAAa,CAAC,KAAK,CAAC,EAAE,EAAE,QAAQ,EAAE;oBAChC,IAAI;oBACJ,SAAS;oBACT,WAAW;oBACX,GAAG,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,OAAO,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;oBACtC,EAAE;iBACH,CAAC,CAAC;YACL,CAAC;YAAC,MAAM,CAAC;gBACP,gEAAgE;YAClE,CAAC;QACH,CAAC,CAAC,CAAC;IACL,CAAC;IAED,SAAS,OAAO,CACd,IAAY,EACZ,EAA8B;QAE9B,OAAO,CAAC,IAAU,EAAc,EAAE,CAChC,GAAG,CAAC,GAAG,CAAC,EAAE,EAAE,KAAK,IAAI,EAAE;YACrB,IAAI,MAAqB,CAAC;YAC1B,IAAI,KAAK,GAAG,KAAK,CAAC;YAClB,IAAI,CAAC;gBACH,MAAM,GAAG,MAAM,EAAE,CAAC,IAAI,CAAC,CAAC;gBACxB,OAAO,MAAM,CAAC;YAChB,CAAC;YAAC,OAAO,GAAG,EAAE,CAAC;gBACb,KAAK,GAAG,IAAI,CAAC;gBACb,MAAM,GAAG,CAAC;YACZ,CAAC;oBAAS,CAAC;gBACT,kEAAkE;gBAClE,iEAAiE;gBACjE,oCAAoC;gBACpC,IAAI,CAAC;oBACH,MAAM,UAAU,GAAG,aAAa,EAAE,CAAC;oBACnC,IAAI,UAAU,CAAC,QAAQ,EAAE,CAAC;wBACxB,WAAW,CACT,IAAI,IAAI,EAAE,CAAC,WAAW,EAAE,EACxB,UAAU,CAAC,QAAQ,EACnB,IAAI,EACJ,WAAW,CAAC,IAAI,CAAC,EACjB,CAAC,KAAK,IAAK,MAAqC,EAAE,OAAO,KAAK,IAAI,EAClE,MAAM,CACP,CAAC;oBACJ,CAAC;gBACH,CAAC;gBAAC,MAAM,CAAC;oBACP,kDAAkD;gBACpD,CAAC;YACH,CAAC;QACH,CAAC,CAAC,CAAC;IACP,CAAC;IAED,OAAO,EAAE,aAAa,EAAE,OAAO,EAAE,CAAC;AACpC,CAAC"}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/** A minimal memoizing cache with a caching decision left to the caller —
|
|
2
|
+
* extracted so "never cache a failed/partial result" is one small, directly
|
|
3
|
+
* testable primitive rather than inline logic in mcp/src/index.ts.
|
|
4
|
+
*
|
|
5
|
+
* Context: index.ts's per-repoPath {repoId, store} cache used to cache
|
|
6
|
+
* every resolution unconditionally, including a repo whose `.reposkein/`
|
|
7
|
+
* exists but isn't indexed yet (no meta.json — `repoId` resolves to
|
|
8
|
+
* `undefined`). That froze a session on the failure forever: an agent that
|
|
9
|
+
* fixed the repo out-of-band (ran `reposkein-mcp index` in another
|
|
10
|
+
* terminal) stayed stuck until the server restarted, which defeats
|
|
11
|
+
* `select_repo`'s whole "no restart needed" point. `shouldCache` lets the
|
|
12
|
+
* caller keep re-attempting `compute` for exactly the states worth
|
|
13
|
+
* retrying, while still caching (and not repeatedly re-doing) real work
|
|
14
|
+
* for a successful resolution. */
|
|
15
|
+
export declare function makeCache<T>(compute: (key: string) => Promise<T>, shouldCache: (value: T) => boolean): (key: string) => Promise<T>;
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/** A minimal memoizing cache with a caching decision left to the caller —
|
|
2
|
+
* extracted so "never cache a failed/partial result" is one small, directly
|
|
3
|
+
* testable primitive rather than inline logic in mcp/src/index.ts.
|
|
4
|
+
*
|
|
5
|
+
* Context: index.ts's per-repoPath {repoId, store} cache used to cache
|
|
6
|
+
* every resolution unconditionally, including a repo whose `.reposkein/`
|
|
7
|
+
* exists but isn't indexed yet (no meta.json — `repoId` resolves to
|
|
8
|
+
* `undefined`). That froze a session on the failure forever: an agent that
|
|
9
|
+
* fixed the repo out-of-band (ran `reposkein-mcp index` in another
|
|
10
|
+
* terminal) stayed stuck until the server restarted, which defeats
|
|
11
|
+
* `select_repo`'s whole "no restart needed" point. `shouldCache` lets the
|
|
12
|
+
* caller keep re-attempting `compute` for exactly the states worth
|
|
13
|
+
* retrying, while still caching (and not repeatedly re-doing) real work
|
|
14
|
+
* for a successful resolution. */
|
|
15
|
+
export function makeCache(compute, shouldCache) {
|
|
16
|
+
const cache = new Map();
|
|
17
|
+
return async (key) => {
|
|
18
|
+
if (cache.has(key))
|
|
19
|
+
return cache.get(key);
|
|
20
|
+
const value = await compute(key);
|
|
21
|
+
if (shouldCache(value))
|
|
22
|
+
cache.set(key, value);
|
|
23
|
+
return value;
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
//# sourceMappingURL=repoContextCache.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"repoContextCache.js","sourceRoot":"","sources":["../../src/store/repoContextCache.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;mCAamC;AACnC,MAAM,UAAU,SAAS,CACvB,OAAoC,EACpC,WAAkC;IAElC,MAAM,KAAK,GAAG,IAAI,GAAG,EAAa,CAAC;IACnC,OAAO,KAAK,EAAE,GAAW,EAAc,EAAE;QACvC,IAAI,KAAK,CAAC,GAAG,CAAC,GAAG,CAAC;YAAE,OAAO,KAAK,CAAC,GAAG,CAAC,GAAG,CAAM,CAAC;QAC/C,MAAM,KAAK,GAAG,MAAM,OAAO,CAAC,GAAG,CAAC,CAAC;QACjC,IAAI,WAAW,CAAC,KAAK,CAAC;YAAE,KAAK,CAAC,GAAG,CAAC,GAAG,EAAE,KAAK,CAAC,CAAC;QAC9C,OAAO,KAAK,CAAC;IACf,CAAC,CAAC;AACJ,CAAC"}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { type RepoResolution } from "./resolveRepoPath.js";
|
|
2
|
+
export interface RepoInfo {
|
|
3
|
+
path: string;
|
|
4
|
+
/** From <path>/.reposkein/meta.json, when present. */
|
|
5
|
+
repo_id?: string;
|
|
6
|
+
/** Display name: repo_id if meta.json resolved one, else the directory's
|
|
7
|
+
* basename — always present so `select_repo` has something to match on
|
|
8
|
+
* even before the repo has been indexed (no meta.json yet). */
|
|
9
|
+
name: string;
|
|
10
|
+
/** Cheap line counts (not full parses) — omitted where the file is absent. */
|
|
11
|
+
nodes?: number;
|
|
12
|
+
edges?: number;
|
|
13
|
+
}
|
|
14
|
+
export declare function describeRepo(path: string): RepoInfo;
|
|
15
|
+
/** Every repo `list_repos` should report. Deliberately broader than
|
|
16
|
+
* `resolveRepoPath`'s own short-circuiting precedence: discovery always
|
|
17
|
+
* walks the filesystem (nearest ancestor with `.reposkein/`, else
|
|
18
|
+
* subdirectories) so a workspace's sibling repos are visible — and thus
|
|
19
|
+
* selectable via `select_repo` — even in a session where REPOSKEIN_REPO_PATH
|
|
20
|
+
* happens to pin one of them. That env pin is unioned in (deduped) so it's
|
|
21
|
+
* never missing from the list, but it does not suppress the others; only
|
|
22
|
+
* `resolveRepoPath`'s actual *default* pick (used by `resolve()` below)
|
|
23
|
+
* gives it priority over the walk. Sorted for deterministic output. */
|
|
24
|
+
export declare function discoverRepoPaths(opts: {
|
|
25
|
+
cwd: string;
|
|
26
|
+
envRepoPath?: string;
|
|
27
|
+
}): string[];
|
|
28
|
+
/** Tracks the MCP session's active repo across tool calls.
|
|
29
|
+
*
|
|
30
|
+
* Resolution precedence: `select_repo` (session-only; sticky for the rest of
|
|
31
|
+
* the connection) > REPOSKEIN_REPO_PATH > walk-up > walk-down. `select_repo`
|
|
32
|
+
* is deliberately allowed to override REPOSKEIN_REPO_PATH once called — it's
|
|
33
|
+
* the more specific, more recent signal, and it's how a workspace-mode
|
|
34
|
+
* session (multiple sibling repos, walk-down ambiguous) picks one to work
|
|
35
|
+
* with instead of restarting the server with a different env var. */
|
|
36
|
+
export declare class RepoSession {
|
|
37
|
+
private readonly cwd;
|
|
38
|
+
private readonly envRepoPath;
|
|
39
|
+
private selectedPath;
|
|
40
|
+
constructor(opts: {
|
|
41
|
+
cwd: string;
|
|
42
|
+
envRepoPath: string | undefined;
|
|
43
|
+
});
|
|
44
|
+
/** Current resolution, honoring an active `select_repo` pick first. */
|
|
45
|
+
resolve(): RepoResolution;
|
|
46
|
+
/** Every candidate repo for this session — see `discoverRepoPaths`. Each
|
|
47
|
+
* entry is flagged `selected` for whichever one `resolve()` currently
|
|
48
|
+
* returns, so an agent can see the effect of a prior `select_repo` call. */
|
|
49
|
+
list(): (RepoInfo & {
|
|
50
|
+
selected: boolean;
|
|
51
|
+
})[];
|
|
52
|
+
/** Sets the session-active repo by path, by its meta.json `repo_id`, or by
|
|
53
|
+
* display `name` (as returned by `list()`/`list_repos`) — never an
|
|
54
|
+
* arbitrary filesystem path, so this can't be used to point outside what
|
|
55
|
+
* resolution actually discovered. */
|
|
56
|
+
select(pathOrName: string): {
|
|
57
|
+
ok: true;
|
|
58
|
+
repo: RepoInfo;
|
|
59
|
+
} | {
|
|
60
|
+
ok: false;
|
|
61
|
+
error: string;
|
|
62
|
+
};
|
|
63
|
+
}
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
2
|
+
import { basename, join } from "node:path";
|
|
3
|
+
import { resolveRepoPath, walkUp, walkDown } from "./resolveRepoPath.js";
|
|
4
|
+
function countLines(text) {
|
|
5
|
+
let n = 0;
|
|
6
|
+
for (const line of text.split("\n"))
|
|
7
|
+
if (line.trim())
|
|
8
|
+
n++;
|
|
9
|
+
return n;
|
|
10
|
+
}
|
|
11
|
+
function readMetaRepoId(path) {
|
|
12
|
+
try {
|
|
13
|
+
const meta = JSON.parse(readFileSync(join(path, ".reposkein", "meta.json"), "utf8"));
|
|
14
|
+
return typeof meta.repo_id === "string" ? meta.repo_id : undefined;
|
|
15
|
+
}
|
|
16
|
+
catch {
|
|
17
|
+
return undefined;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
/** Line counts for nodes.jsonl / edges.jsonl if they exist — the same cheap
|
|
21
|
+
* "count non-empty lines" approach `cli/doctor.ts` uses, not a full parse. */
|
|
22
|
+
function cheapCounts(path) {
|
|
23
|
+
const out = {};
|
|
24
|
+
const nodesFile = join(path, ".reposkein", "nodes.jsonl");
|
|
25
|
+
const edgesFile = join(path, ".reposkein", "edges.jsonl");
|
|
26
|
+
try {
|
|
27
|
+
if (existsSync(nodesFile))
|
|
28
|
+
out.nodes = countLines(readFileSync(nodesFile, "utf8"));
|
|
29
|
+
}
|
|
30
|
+
catch {
|
|
31
|
+
/* unreadable — omit rather than fail list_repos over one repo */
|
|
32
|
+
}
|
|
33
|
+
try {
|
|
34
|
+
if (existsSync(edgesFile))
|
|
35
|
+
out.edges = countLines(readFileSync(edgesFile, "utf8"));
|
|
36
|
+
}
|
|
37
|
+
catch {
|
|
38
|
+
/* unreadable — omit */
|
|
39
|
+
}
|
|
40
|
+
return out;
|
|
41
|
+
}
|
|
42
|
+
export function describeRepo(path) {
|
|
43
|
+
const repo_id = readMetaRepoId(path);
|
|
44
|
+
return {
|
|
45
|
+
path,
|
|
46
|
+
...(repo_id ? { repo_id } : {}),
|
|
47
|
+
name: repo_id ?? basename(path),
|
|
48
|
+
...cheapCounts(path),
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
/** Every repo `list_repos` should report. Deliberately broader than
|
|
52
|
+
* `resolveRepoPath`'s own short-circuiting precedence: discovery always
|
|
53
|
+
* walks the filesystem (nearest ancestor with `.reposkein/`, else
|
|
54
|
+
* subdirectories) so a workspace's sibling repos are visible — and thus
|
|
55
|
+
* selectable via `select_repo` — even in a session where REPOSKEIN_REPO_PATH
|
|
56
|
+
* happens to pin one of them. That env pin is unioned in (deduped) so it's
|
|
57
|
+
* never missing from the list, but it does not suppress the others; only
|
|
58
|
+
* `resolveRepoPath`'s actual *default* pick (used by `resolve()` below)
|
|
59
|
+
* gives it priority over the walk. Sorted for deterministic output. */
|
|
60
|
+
export function discoverRepoPaths(opts) {
|
|
61
|
+
const up = walkUp(opts.cwd);
|
|
62
|
+
const physical = up ? [up] : walkDown(opts.cwd);
|
|
63
|
+
if (opts.envRepoPath && !physical.includes(opts.envRepoPath)) {
|
|
64
|
+
return [...physical, opts.envRepoPath].sort();
|
|
65
|
+
}
|
|
66
|
+
return physical;
|
|
67
|
+
}
|
|
68
|
+
/** Tracks the MCP session's active repo across tool calls.
|
|
69
|
+
*
|
|
70
|
+
* Resolution precedence: `select_repo` (session-only; sticky for the rest of
|
|
71
|
+
* the connection) > REPOSKEIN_REPO_PATH > walk-up > walk-down. `select_repo`
|
|
72
|
+
* is deliberately allowed to override REPOSKEIN_REPO_PATH once called — it's
|
|
73
|
+
* the more specific, more recent signal, and it's how a workspace-mode
|
|
74
|
+
* session (multiple sibling repos, walk-down ambiguous) picks one to work
|
|
75
|
+
* with instead of restarting the server with a different env var. */
|
|
76
|
+
export class RepoSession {
|
|
77
|
+
cwd;
|
|
78
|
+
envRepoPath;
|
|
79
|
+
selectedPath;
|
|
80
|
+
constructor(opts) {
|
|
81
|
+
this.cwd = opts.cwd;
|
|
82
|
+
this.envRepoPath = opts.envRepoPath;
|
|
83
|
+
}
|
|
84
|
+
/** Current resolution, honoring an active `select_repo` pick first. */
|
|
85
|
+
resolve() {
|
|
86
|
+
if (this.selectedPath)
|
|
87
|
+
return { repoPath: this.selectedPath, source: "explicit" };
|
|
88
|
+
return resolveRepoPath({ cwd: this.cwd, envRepoPath: this.envRepoPath });
|
|
89
|
+
}
|
|
90
|
+
/** Every candidate repo for this session — see `discoverRepoPaths`. Each
|
|
91
|
+
* entry is flagged `selected` for whichever one `resolve()` currently
|
|
92
|
+
* returns, so an agent can see the effect of a prior `select_repo` call. */
|
|
93
|
+
list() {
|
|
94
|
+
const active = this.resolve().repoPath;
|
|
95
|
+
return discoverRepoPaths({ cwd: this.cwd, envRepoPath: this.envRepoPath }).map((path) => ({
|
|
96
|
+
...describeRepo(path),
|
|
97
|
+
selected: path === active,
|
|
98
|
+
}));
|
|
99
|
+
}
|
|
100
|
+
/** Sets the session-active repo by path, by its meta.json `repo_id`, or by
|
|
101
|
+
* display `name` (as returned by `list()`/`list_repos`) — never an
|
|
102
|
+
* arbitrary filesystem path, so this can't be used to point outside what
|
|
103
|
+
* resolution actually discovered. */
|
|
104
|
+
select(pathOrName) {
|
|
105
|
+
const candidates = discoverRepoPaths({ cwd: this.cwd, envRepoPath: this.envRepoPath }).map(describeRepo);
|
|
106
|
+
const hit = candidates.find((c) => c.path === pathOrName) ??
|
|
107
|
+
candidates.find((c) => c.repo_id === pathOrName) ??
|
|
108
|
+
candidates.find((c) => c.name === pathOrName);
|
|
109
|
+
if (!hit) {
|
|
110
|
+
return {
|
|
111
|
+
ok: false,
|
|
112
|
+
error: `No repo matching "${pathOrName}" among the discovered candidates` +
|
|
113
|
+
(candidates.length > 0
|
|
114
|
+
? `: ${candidates.map((c) => c.path).join(", ")}.`
|
|
115
|
+
: " (none found).") +
|
|
116
|
+
" Call list_repos first and pass one of its `path` or `name` values.",
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
this.selectedPath = hit.path;
|
|
120
|
+
return { ok: true, repo: hit };
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
//# sourceMappingURL=repoSession.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"repoSession.js","sourceRoot":"","sources":["../../src/store/repoSession.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
export type RepoResolutionSource = "explicit" | "env" | "walk-up" | "walk-down" | "none";
|
|
2
|
+
export interface RepoResolution {
|
|
3
|
+
/** Resolved repo root, or undefined if none could be determined. */
|
|
4
|
+
repoPath: string | undefined;
|
|
5
|
+
/** How repoPath was determined ("none" when nothing resolved). */
|
|
6
|
+
source: RepoResolutionSource;
|
|
7
|
+
/** Populated only when source is "none" because walk-down found 2+ hits
|
|
8
|
+
* (workspace mode, ambiguous) — name them so the caller can pick one. */
|
|
9
|
+
candidates?: string[];
|
|
10
|
+
}
|
|
11
|
+
/** Nearest ancestor of `cwd` (inclusive) containing `.reposkein/`. */
|
|
12
|
+
export declare function walkUp(cwd: string): string | undefined;
|
|
13
|
+
/** Scans descendants of `cwd` (bounded to `maxDepth` levels, skipping
|
|
14
|
+
* build/VCS/tool directories) for directories containing `.reposkein/`.
|
|
15
|
+
* Does not recurse further into a hit — nested repos are a graph-federation
|
|
16
|
+
* concern (see store/federation.ts), not a workspace-candidate one here.
|
|
17
|
+
* Returns hits sorted for deterministic ambiguity-error output. */
|
|
18
|
+
export declare function walkDown(cwd: string, maxDepth?: number): string[];
|
|
19
|
+
/** Zero-config repo resolution.
|
|
20
|
+
*
|
|
21
|
+
* Precedence: explicit tool arg > REPOSKEIN_REPO_PATH > walk-up > walk-down.
|
|
22
|
+
* - Walk-up: nearest ancestor of `cwd` (inclusive) containing `.reposkein/`.
|
|
23
|
+
* - Walk-down (workspace mode, only tried when no ancestor has one): scans
|
|
24
|
+
* `cwd`'s children/grandchildren for `.reposkein/` dirs. One hit auto-
|
|
25
|
+
* selects it. Zero or 2+ hits leave `repoPath` undefined ("none"); 2+
|
|
26
|
+
* populates `candidates` so the caller can build an actionable error. */
|
|
27
|
+
export declare function resolveRepoPath(opts: {
|
|
28
|
+
cwd: string;
|
|
29
|
+
envRepoPath?: string;
|
|
30
|
+
explicit?: string;
|
|
31
|
+
}): RepoResolution;
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import { readdirSync, statSync } from "node:fs";
|
|
2
|
+
import { dirname, join, resolve as resolvePath } from "node:path";
|
|
3
|
+
const REPOSKEIN_DIR = ".reposkein";
|
|
4
|
+
/** Directories never treated as repo candidates while walking down: build
|
|
5
|
+
* output, VCS internals, other worktrees/tool state — never real repo roots. */
|
|
6
|
+
const SKIP_DIR_NAMES = new Set(["node_modules", ".git", "target", "dist", ".worktrees", ".claude"]);
|
|
7
|
+
function hasReposkeinDir(dir) {
|
|
8
|
+
try {
|
|
9
|
+
return statSync(join(dir, REPOSKEIN_DIR)).isDirectory();
|
|
10
|
+
}
|
|
11
|
+
catch {
|
|
12
|
+
return false;
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
/** Nearest ancestor of `cwd` (inclusive) containing `.reposkein/`. */
|
|
16
|
+
export function walkUp(cwd) {
|
|
17
|
+
let dir = resolvePath(cwd);
|
|
18
|
+
for (;;) {
|
|
19
|
+
if (hasReposkeinDir(dir))
|
|
20
|
+
return dir;
|
|
21
|
+
const parent = dirname(dir);
|
|
22
|
+
if (parent === dir)
|
|
23
|
+
return undefined; // reached filesystem root
|
|
24
|
+
dir = parent;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
/** Scans descendants of `cwd` (bounded to `maxDepth` levels, skipping
|
|
28
|
+
* build/VCS/tool directories) for directories containing `.reposkein/`.
|
|
29
|
+
* Does not recurse further into a hit — nested repos are a graph-federation
|
|
30
|
+
* concern (see store/federation.ts), not a workspace-candidate one here.
|
|
31
|
+
* Returns hits sorted for deterministic ambiguity-error output. */
|
|
32
|
+
export function walkDown(cwd, maxDepth = 2) {
|
|
33
|
+
const root = resolvePath(cwd);
|
|
34
|
+
const hits = [];
|
|
35
|
+
function recurse(dir, depth) {
|
|
36
|
+
let entries;
|
|
37
|
+
try {
|
|
38
|
+
entries = readdirSync(dir, { withFileTypes: true });
|
|
39
|
+
}
|
|
40
|
+
catch {
|
|
41
|
+
return; // unreadable dir (permissions, race) — skip, don't fail resolution
|
|
42
|
+
}
|
|
43
|
+
for (const entry of entries) {
|
|
44
|
+
if (!entry.isDirectory())
|
|
45
|
+
continue;
|
|
46
|
+
if (entry.name === REPOSKEIN_DIR)
|
|
47
|
+
continue;
|
|
48
|
+
if (SKIP_DIR_NAMES.has(entry.name))
|
|
49
|
+
continue;
|
|
50
|
+
const full = join(dir, entry.name);
|
|
51
|
+
if (hasReposkeinDir(full)) {
|
|
52
|
+
hits.push(full);
|
|
53
|
+
continue;
|
|
54
|
+
}
|
|
55
|
+
if (depth < maxDepth)
|
|
56
|
+
recurse(full, depth + 1);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
recurse(root, 1);
|
|
60
|
+
return hits.sort();
|
|
61
|
+
}
|
|
62
|
+
/** Zero-config repo resolution.
|
|
63
|
+
*
|
|
64
|
+
* Precedence: explicit tool arg > REPOSKEIN_REPO_PATH > walk-up > walk-down.
|
|
65
|
+
* - Walk-up: nearest ancestor of `cwd` (inclusive) containing `.reposkein/`.
|
|
66
|
+
* - Walk-down (workspace mode, only tried when no ancestor has one): scans
|
|
67
|
+
* `cwd`'s children/grandchildren for `.reposkein/` dirs. One hit auto-
|
|
68
|
+
* selects it. Zero or 2+ hits leave `repoPath` undefined ("none"); 2+
|
|
69
|
+
* populates `candidates` so the caller can build an actionable error. */
|
|
70
|
+
export function resolveRepoPath(opts) {
|
|
71
|
+
if (opts.explicit)
|
|
72
|
+
return { repoPath: opts.explicit, source: "explicit" };
|
|
73
|
+
if (opts.envRepoPath)
|
|
74
|
+
return { repoPath: opts.envRepoPath, source: "env" };
|
|
75
|
+
const up = walkUp(opts.cwd);
|
|
76
|
+
if (up)
|
|
77
|
+
return { repoPath: up, source: "walk-up" };
|
|
78
|
+
const down = walkDown(opts.cwd);
|
|
79
|
+
if (down.length === 1)
|
|
80
|
+
return { repoPath: down[0], source: "walk-down" };
|
|
81
|
+
if (down.length > 1)
|
|
82
|
+
return { repoPath: undefined, source: "none", candidates: down };
|
|
83
|
+
return { repoPath: undefined, source: "none" };
|
|
84
|
+
}
|
|
85
|
+
//# sourceMappingURL=resolveRepoPath.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"resolveRepoPath.js","sourceRoot":"","sources":["../../src/store/resolveRepoPath.ts"],"names":[],"mappings":"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"}
|