@reposkein/mcp 0.3.0 → 0.5.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.
Files changed (117) hide show
  1. package/README.md +76 -1
  2. package/binary-digests.json +4 -4
  3. package/dist/SKILL.md +19 -0
  4. package/dist/cli/agentAdapters.d.ts +68 -0
  5. package/dist/cli/agentAdapters.js +223 -0
  6. package/dist/cli/agentAdapters.js.map +1 -0
  7. package/dist/cli/ansi.d.ts +24 -0
  8. package/dist/cli/ansi.js +40 -0
  9. package/dist/cli/ansi.js.map +1 -0
  10. package/dist/cli/doctor.d.ts +34 -2
  11. package/dist/cli/doctor.js +51 -6
  12. package/dist/cli/doctor.js.map +1 -1
  13. package/dist/cli/doctorFreshness.d.ts +43 -0
  14. package/dist/cli/doctorFreshness.js +111 -0
  15. package/dist/cli/doctorFreshness.js.map +1 -0
  16. package/dist/cli/doctorSummaries.d.ts +2 -0
  17. package/dist/cli/doctorSummaries.js +95 -0
  18. package/dist/cli/doctorSummaries.js.map +1 -0
  19. package/dist/cli/federatedDiscovery.d.ts +16 -0
  20. package/dist/cli/federatedDiscovery.js +75 -0
  21. package/dist/cli/federatedDiscovery.js.map +1 -0
  22. package/dist/cli/init.d.ts +69 -6
  23. package/dist/cli/init.js +194 -28
  24. package/dist/cli/init.js.map +1 -1
  25. package/dist/cli/sourceSlices.d.ts +17 -0
  26. package/dist/cli/sourceSlices.js +59 -0
  27. package/dist/cli/sourceSlices.js.map +1 -0
  28. package/dist/cli/stats.d.ts +29 -0
  29. package/dist/cli/stats.js +180 -0
  30. package/dist/cli/stats.js.map +1 -0
  31. package/dist/cli/view.d.ts +71 -3
  32. package/dist/cli/view.js +170 -14
  33. package/dist/cli/view.js.map +1 -1
  34. package/dist/index.d.ts +8 -48
  35. package/dist/index.js +94 -295
  36. package/dist/index.js.map +1 -1
  37. package/dist/indexer/fetchBinary.d.ts +14 -0
  38. package/dist/indexer/fetchBinary.js +81 -40
  39. package/dist/indexer/fetchBinary.js.map +1 -1
  40. package/dist/indexer/indexLock.d.ts +13 -0
  41. package/dist/indexer/indexLock.js +66 -0
  42. package/dist/indexer/indexLock.js.map +1 -0
  43. package/dist/indexer/runIndexer.d.ts +8 -2
  44. package/dist/indexer/runIndexer.js +20 -4
  45. package/dist/indexer/runIndexer.js.map +1 -1
  46. package/dist/serve/serve.d.ts +95 -0
  47. package/dist/serve/serve.js +581 -0
  48. package/dist/serve/serve.js.map +1 -0
  49. package/dist/serve/tokens.d.ts +49 -0
  50. package/dist/serve/tokens.js +108 -0
  51. package/dist/serve/tokens.js.map +1 -0
  52. package/dist/serve/watch.d.ts +73 -0
  53. package/dist/serve/watch.js +135 -0
  54. package/dist/serve/watch.js.map +1 -0
  55. package/dist/server/createMcpServer.d.ts +129 -0
  56. package/dist/server/createMcpServer.js +464 -0
  57. package/dist/server/createMcpServer.js.map +1 -0
  58. package/dist/store/JsonlGraphStore.d.ts +18 -4
  59. package/dist/store/JsonlGraphStore.js +61 -19
  60. package/dist/store/JsonlGraphStore.js.map +1 -1
  61. package/dist/store/decisions.d.ts +20 -2
  62. package/dist/store/decisions.js +35 -2
  63. package/dist/store/decisions.js.map +1 -1
  64. package/dist/store/indexedAt.d.ts +18 -0
  65. package/dist/store/indexedAt.js +56 -0
  66. package/dist/store/indexedAt.js.map +1 -0
  67. package/dist/store/instrumentTool.d.ts +40 -0
  68. package/dist/store/instrumentTool.js +82 -0
  69. package/dist/store/instrumentTool.js.map +1 -0
  70. package/dist/store/repoContextCache.d.ts +15 -0
  71. package/dist/store/repoContextCache.js +26 -0
  72. package/dist/store/repoContextCache.js.map +1 -0
  73. package/dist/store/repoSession.d.ts +63 -0
  74. package/dist/store/repoSession.js +123 -0
  75. package/dist/store/repoSession.js.map +1 -0
  76. package/dist/store/resolveRepoPath.d.ts +31 -0
  77. package/dist/store/resolveRepoPath.js +85 -0
  78. package/dist/store/resolveRepoPath.js.map +1 -0
  79. package/dist/store/sessionLog.d.ts +98 -0
  80. package/dist/store/sessionLog.js +216 -0
  81. package/dist/store/sessionLog.js.map +1 -0
  82. package/dist/store/sessionStats.d.ts +55 -0
  83. package/dist/store/sessionStats.js +140 -0
  84. package/dist/store/sessionStats.js.map +1 -0
  85. package/dist/store/sidecar.d.ts +39 -3
  86. package/dist/store/sidecar.js +89 -5
  87. package/dist/store/sidecar.js.map +1 -1
  88. package/dist/store/summaryShards.d.ts +117 -0
  89. package/dist/store/summaryShards.js +291 -0
  90. package/dist/store/summaryShards.js.map +1 -0
  91. package/dist/store/teamConfig.d.ts +17 -0
  92. package/dist/store/teamConfig.js +55 -0
  93. package/dist/store/teamConfig.js.map +1 -0
  94. package/dist/tools/indexerTools.d.ts +14 -2
  95. package/dist/tools/indexerTools.js +17 -5
  96. package/dist/tools/indexerTools.js.map +1 -1
  97. package/dist/tools/recordDecision.d.ts +3 -0
  98. package/dist/tools/recordDecision.js +7 -2
  99. package/dist/tools/recordDecision.js.map +1 -1
  100. package/dist/tools/writeSemanticSummary.d.ts +4 -1
  101. package/dist/tools/writeSemanticSummary.js +5 -2
  102. package/dist/tools/writeSemanticSummary.js.map +1 -1
  103. package/dist/viz/assets/{graph.worker-CC7BeE-8.js → graph.worker-DkH6-lMU.js} +1 -1
  104. package/dist/viz/assets/index-BnqIVYFE.css +1 -0
  105. package/dist/viz/assets/index-IhtszTp0.js +5 -0
  106. package/dist/viz/assets/jetbrains-mono-latin-400-normal-V6pRDFza.woff2 +0 -0
  107. package/dist/viz/assets/jetbrains-mono-latin-500-normal-BWZEU5yA.woff2 +0 -0
  108. package/dist/viz/assets/jetbrains-mono-latin-700-normal-BYuf6tUa.woff2 +0 -0
  109. package/dist/viz/assets/r3f-Bn9htB92.js +336 -0
  110. package/dist/viz/assets/tanstack-CSzx6NDA.js +62 -0
  111. package/dist/viz/assets/three-Cpu5RUEM.js +4116 -0
  112. package/dist/viz/index.html +5 -4
  113. package/package.json +5 -4
  114. package/dist/viz/assets/index-C_8UkiJ8.js +0 -9
  115. package/dist/viz/assets/r3f-Cku1OhB-.js +0 -277
  116. package/dist/viz/assets/tanstack-DyXcNRWk.js +0 -77
  117. package/dist/viz/assets/three-Z8XaLGmf.js +0 -3839
@@ -0,0 +1,108 @@
1
+ import { createHash, timingSafeEqual } from "node:crypto";
2
+ import { readConfigString } from "../store/teamConfig.js";
3
+ /** Minimum secret length we will accept. Short enough not to be annoying,
4
+ * long enough that a token typed by hand isn't guessable in an afternoon. */
5
+ const MIN_TOKEN_LENGTH = 16;
6
+ /** Parses a token list: `name:token[:write]` entries separated by commas,
7
+ * whitespace, or newlines.
8
+ *
9
+ * Chosen over JSON because it has to survive being pasted into a shell
10
+ * export, a systemd `Environment=` line, and a TOML string without escaping.
11
+ * Anything unparseable is DROPPED with a reason rather than silently
12
+ * becoming a valid credential — a malformed entry must never widen access.
13
+ * Capability is opt-in: only the literal third field `write` grants it. */
14
+ export function parseServeTokens(spec) {
15
+ const tokens = [];
16
+ const errors = [];
17
+ const seenNames = new Set();
18
+ const seenSecrets = new Set();
19
+ for (const rawEntry of spec.split(/[,\s]+/)) {
20
+ const entry = rawEntry.trim();
21
+ if (entry === "")
22
+ continue;
23
+ const parts = entry.split(":");
24
+ if (parts.length < 2 || parts.length > 3) {
25
+ errors.push(`ignored a token entry: expected "name:token" or "name:token:write"`);
26
+ continue;
27
+ }
28
+ const [name, token, cap] = parts;
29
+ if (!/^[A-Za-z0-9._-]{1,40}$/.test(name)) {
30
+ errors.push(`ignored a token entry: name must be 1-40 chars of [A-Za-z0-9._-] (got ${JSON.stringify(name)})`);
31
+ continue;
32
+ }
33
+ if (token.length < MIN_TOKEN_LENGTH) {
34
+ errors.push(`ignored token "${name}": secret shorter than ${MIN_TOKEN_LENGTH} characters`);
35
+ continue;
36
+ }
37
+ if (cap !== undefined && cap !== "write") {
38
+ errors.push(`ignored token "${name}": third field must be "write" (got ${JSON.stringify(cap)})`);
39
+ continue;
40
+ }
41
+ if (seenNames.has(name)) {
42
+ errors.push(`ignored a duplicate token name: "${name}"`);
43
+ continue;
44
+ }
45
+ if (seenSecrets.has(token)) {
46
+ errors.push(`ignored token "${name}": its secret is already used by another name`);
47
+ continue;
48
+ }
49
+ seenNames.add(name);
50
+ seenSecrets.add(token);
51
+ tokens.push({ name, token, write: cap === "write" });
52
+ }
53
+ return { tokens, errors };
54
+ }
55
+ /** Resolves the token list for `serve`: `REPOSKEIN_SERVE_TOKENS` wins over
56
+ * `[serve] tokens` in `.reposkein/config.toml`.
57
+ *
58
+ * Env-over-config because the config file is COMMITTED — an operator who
59
+ * puts real secrets there has leaked them to everyone with repo access, so
60
+ * the documented path is the env var and config is the convenience for a
61
+ * private deployment. Only one source is consulted (no union): merging them
62
+ * would make "why does this old token still work?" unanswerable. */
63
+ export function loadServeTokens(repoPath, env = process.env) {
64
+ const fromEnv = env.REPOSKEIN_SERVE_TOKENS?.trim();
65
+ if (fromEnv)
66
+ return { ...parseServeTokens(fromEnv), source: "env" };
67
+ const fromConfig = readConfigString(repoPath, "serve", "tokens");
68
+ if (fromConfig && fromConfig.trim()) {
69
+ return { ...parseServeTokens(fromConfig), source: "config" };
70
+ }
71
+ return { tokens: [], errors: [], source: "none" };
72
+ }
73
+ /** Extracts the credential from an `Authorization: Bearer <token>` header.
74
+ * Returns null for a missing, non-Bearer, or empty-value header. */
75
+ export function bearerFromAuthHeader(header) {
76
+ const value = Array.isArray(header) ? header[0] : header;
77
+ if (!value)
78
+ return null;
79
+ const m = /^Bearer[ \t]+(.+)$/i.exec(value.trim());
80
+ if (!m)
81
+ return null;
82
+ const token = m[1].trim();
83
+ return token === "" ? null : token;
84
+ }
85
+ /** Length-independent constant-time comparison: both sides are hashed to a
86
+ * fixed 32 bytes first, so `timingSafeEqual` never sees mismatched lengths
87
+ * (it throws on those, and the throw itself would leak the length). */
88
+ function secretEquals(a, b) {
89
+ const ha = createHash("sha256").update(a, "utf8").digest();
90
+ const hb = createHash("sha256").update(b, "utf8").digest();
91
+ return timingSafeEqual(ha, hb);
92
+ }
93
+ /** Finds the token a presented credential matches, or null.
94
+ *
95
+ * Scans the WHOLE list with no early exit so the response time doesn't
96
+ * reveal which position matched (or how many tokens the operator
97
+ * configured). */
98
+ export function matchServeToken(tokens, presented) {
99
+ if (presented === null)
100
+ return null;
101
+ let hit = null;
102
+ for (const t of tokens) {
103
+ if (secretEquals(t.token, presented))
104
+ hit = t;
105
+ }
106
+ return hit;
107
+ }
108
+ //# sourceMappingURL=tokens.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"tokens.js","sourceRoot":"","sources":["../../src/serve/tokens.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,73 @@
1
+ /** What one watch tick did. `unchanged` is the overwhelmingly common case and
2
+ * costs exactly one `git rev-parse` — the whole point of comparing against
3
+ * the indexed-at marker rather than re-indexing on a timer. `locked` means
4
+ * another indexer run (a write-capable tool call, or the previous tick) owns
5
+ * the index lock; the tick is skipped, not queued. */
6
+ export type WatchOutcome = "unchanged" | "reindexed" | "failed" | "no-head" | "locked";
7
+ export interface ReindexResult {
8
+ ok: boolean;
9
+ nodes?: number;
10
+ edges?: number;
11
+ error?: string;
12
+ }
13
+ export interface WatchDeps {
14
+ /** Current git HEAD of the served checkout, or null (not a repo / no HEAD). */
15
+ headSha: () => string | null;
16
+ /** The SHA the graph was last built from (`.reposkein/local/indexed-at`). */
17
+ indexedSha: () => string | null;
18
+ /** Rebuild the graph. MUST also refresh the indexed-at marker on success. */
19
+ reindex: () => Promise<ReindexResult>;
20
+ /** Where the one-line-per-reindex summary goes. MUST NOT be stdout in stdio
21
+ * mode; `serve` has no stdio transport, but stderr stays the convention. */
22
+ log: (message: string) => void;
23
+ /** Whether an indexer run is already in progress or queued. Injectable so
24
+ * the skip path is testable without spawning anything. */
25
+ indexBusy: () => boolean;
26
+ }
27
+ /** Reads HEAD of `repoPath`, or null. Never throws. */
28
+ export declare function gitHeadSha(repoPath: string): string | null;
29
+ /** One watch tick.
30
+ *
31
+ * Idempotence is structural, not statistical: the decision to do work is
32
+ * `HEAD !== indexed-at`, and the marker is written from HEAD by the index
33
+ * step itself. So a tick against an unchanged checkout does nothing at all
34
+ * (no indexer spawn, no log line), and a tick after `git pull` does exactly
35
+ * one re-index — including across a server restart, because the marker is on
36
+ * disk. If HEAD moves again DURING an index, the marker records the SHA that
37
+ * was current when the index finished and the next tick converges.
38
+ *
39
+ * Reusing REP-16's marker (rather than an in-memory "last seen" SHA) is what
40
+ * keeps `doctor --ci`'s `graph_stale` check agreeing with the watcher: both
41
+ * read the same file. */
42
+ export declare function watchTick(deps: WatchDeps): Promise<WatchOutcome>;
43
+ /** The default `reindex`: the same indexer invocation `reposkein-mcp index`
44
+ * uses, followed by the indexed-at marker write. */
45
+ export declare function makeDefaultReindex(repoPath: string, repoId: string): () => Promise<ReindexResult>;
46
+ export interface HeadWatcherOptions {
47
+ intervalMs: number;
48
+ log?: (message: string) => void;
49
+ /** Called after a successful re-index — `serve` uses it to rebuild the
50
+ * `/api/*` handler, whose node/edge counts and git metadata are computed
51
+ * once per handler. */
52
+ onReindexed?: () => void;
53
+ /** Test seam: replaces the git + indexer plumbing wholesale. */
54
+ deps?: Partial<WatchDeps>;
55
+ }
56
+ export interface HeadWatcher {
57
+ /** Runs one tick now (awaitable — used by the startup catch-up and tests). */
58
+ tick: () => Promise<WatchOutcome>;
59
+ stop: () => void;
60
+ }
61
+ /** Polls `git rev-parse HEAD` on an interval and re-indexes when it moves.
62
+ *
63
+ * A poll, not a filesystem watcher: the indexer's determinism guarantee is
64
+ * defined against committed state, and an fs watcher would fire on every
65
+ * editor save — re-indexing mid-edit, repeatedly, for a graph nobody asked
66
+ * for yet. A poll also covers the case that actually matters for a shared
67
+ * server (someone ran `git fetch && git reset --hard` in the checkout, or a
68
+ * deploy hook did) without requiring a hook to be installed at all.
69
+ *
70
+ * `intervalMs <= 0` disables polling entirely; `tick()` still works, so an
71
+ * operator can drive re-indexing from their own post-receive hook by
72
+ * restarting or by running `reposkein-mcp index`. */
73
+ export declare function startHeadWatcher(repoPath: string, repoId: string, opts: HeadWatcherOptions): HeadWatcher;
@@ -0,0 +1,135 @@
1
+ import { execFileSync } from "node:child_process";
2
+ import { readIndexedAtSha, writeIndexedAtMarker } from "../store/indexedAt.js";
3
+ import { ensureIndexerBinary } from "../indexer/fetchBinary.js";
4
+ import { spawnIndexer, parseJsonStats } from "../indexer/runIndexer.js";
5
+ import { isIndexLockHeld } from "../indexer/indexLock.js";
6
+ /** Reads HEAD of `repoPath`, or null. Never throws. */
7
+ export function gitHeadSha(repoPath) {
8
+ try {
9
+ const sha = execFileSync("git", ["-C", repoPath, "rev-parse", "HEAD"], {
10
+ encoding: "utf8",
11
+ stdio: ["ignore", "pipe", "ignore"],
12
+ }).trim();
13
+ return sha || null;
14
+ }
15
+ catch {
16
+ return null;
17
+ }
18
+ }
19
+ /** One watch tick.
20
+ *
21
+ * Idempotence is structural, not statistical: the decision to do work is
22
+ * `HEAD !== indexed-at`, and the marker is written from HEAD by the index
23
+ * step itself. So a tick against an unchanged checkout does nothing at all
24
+ * (no indexer spawn, no log line), and a tick after `git pull` does exactly
25
+ * one re-index — including across a server restart, because the marker is on
26
+ * disk. If HEAD moves again DURING an index, the marker records the SHA that
27
+ * was current when the index finished and the next tick converges.
28
+ *
29
+ * Reusing REP-16's marker (rather than an in-memory "last seen" SHA) is what
30
+ * keeps `doctor --ci`'s `graph_stale` check agreeing with the watcher: both
31
+ * read the same file. */
32
+ export async function watchTick(deps) {
33
+ const head = deps.headSha();
34
+ if (!head)
35
+ return "no-head";
36
+ if (head === deps.indexedSha())
37
+ return "unchanged";
38
+ // Someone else is indexing (a write tool call, or a still-running earlier
39
+ // tick). SKIP rather than queue: whatever they are building is being built
40
+ // from the same working tree, so by the time the lock frees, this tick's
41
+ // work is either done or the next tick will see it needs doing. Queueing
42
+ // here would fire a redundant full index behind every write tool call.
43
+ if (deps.indexBusy())
44
+ return "locked";
45
+ const started = Date.now();
46
+ const r = await deps.reindex();
47
+ const secs = ((Date.now() - started) / 1000).toFixed(1);
48
+ const short = head.slice(0, 12);
49
+ if (!r.ok) {
50
+ deps.log(`reposkein serve: reindex for HEAD ${short} FAILED after ${secs}s: ${r.error ?? "unknown error"} ` +
51
+ "(serving the previous graph; will retry on the next tick)");
52
+ return "failed";
53
+ }
54
+ deps.log(`reposkein serve: reindexed at HEAD ${short} in ${secs}s ` +
55
+ `(${r.nodes ?? "?"} nodes, ${r.edges ?? "?"} edges)`);
56
+ return "reindexed";
57
+ }
58
+ /** The default `reindex`: the same indexer invocation `reposkein-mcp index`
59
+ * uses, followed by the indexed-at marker write. */
60
+ export function makeDefaultReindex(repoPath, repoId) {
61
+ return async () => {
62
+ try {
63
+ const bin = await ensureIndexerBinary();
64
+ const r = await spawnIndexer(bin, ["index", "--json", "--repo-id", repoId, repoPath]);
65
+ if (r.code !== 0) {
66
+ return { ok: false, error: r.stderr.trim() || r.stdout.trim() || `exit ${r.code}` };
67
+ }
68
+ const stats = parseJsonStats(r.stdout);
69
+ writeIndexedAtMarker(repoPath);
70
+ return stats ? { ok: true, nodes: stats.nodes, edges: stats.edges } : { ok: true };
71
+ }
72
+ catch (e) {
73
+ return { ok: false, error: e instanceof Error ? e.message : String(e) };
74
+ }
75
+ };
76
+ }
77
+ /** Polls `git rev-parse HEAD` on an interval and re-indexes when it moves.
78
+ *
79
+ * A poll, not a filesystem watcher: the indexer's determinism guarantee is
80
+ * defined against committed state, and an fs watcher would fire on every
81
+ * editor save — re-indexing mid-edit, repeatedly, for a graph nobody asked
82
+ * for yet. A poll also covers the case that actually matters for a shared
83
+ * server (someone ran `git fetch && git reset --hard` in the checkout, or a
84
+ * deploy hook did) without requiring a hook to be installed at all.
85
+ *
86
+ * `intervalMs <= 0` disables polling entirely; `tick()` still works, so an
87
+ * operator can drive re-indexing from their own post-receive hook by
88
+ * restarting or by running `reposkein-mcp index`. */
89
+ export function startHeadWatcher(repoPath, repoId, opts) {
90
+ const log = opts.log ?? ((m) => void process.stderr.write(`${m}\n`));
91
+ const deps = {
92
+ headSha: opts.deps?.headSha ?? (() => gitHeadSha(repoPath)),
93
+ indexedSha: opts.deps?.indexedSha ?? (() => readIndexedAtSha(repoPath)),
94
+ reindex: opts.deps?.reindex ?? makeDefaultReindex(repoPath, repoId),
95
+ log: opts.deps?.log ?? log,
96
+ indexBusy: opts.deps?.indexBusy ?? isIndexLockHeld,
97
+ };
98
+ // Ticks never overlap: a slow index on a big repo must not stack up behind
99
+ // a fast interval, and two concurrent indexer runs on one checkout would
100
+ // race on the same output files.
101
+ let busy = false;
102
+ let stopped = false;
103
+ const tick = async () => {
104
+ if (stopped)
105
+ return "unchanged";
106
+ // A previous tick is still running (slow index on a big repo, fast
107
+ // interval). Same reasoning as the index-lock skip in `watchTick`.
108
+ if (busy)
109
+ return "locked";
110
+ busy = true;
111
+ try {
112
+ const outcome = await watchTick(deps);
113
+ if (outcome === "reindexed")
114
+ opts.onReindexed?.();
115
+ return outcome;
116
+ }
117
+ finally {
118
+ busy = false;
119
+ }
120
+ };
121
+ const timer = opts.intervalMs > 0
122
+ ? setInterval(() => {
123
+ void tick();
124
+ }, opts.intervalMs)
125
+ : null;
126
+ return {
127
+ tick,
128
+ stop: () => {
129
+ stopped = true;
130
+ if (timer)
131
+ clearInterval(timer);
132
+ },
133
+ };
134
+ }
135
+ //# sourceMappingURL=watch.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"watch.js","sourceRoot":"","sources":["../../src/serve/watch.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,129 @@
1
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
+ import { z } from "zod";
3
+ import type { GraphStore } from "../store/GraphStore.js";
4
+ import type { RepoResolution } from "../store/resolveRepoPath.js";
5
+ import { ensureGraph } from "../indexer/ensureGraph.js";
6
+ /** Selects the store backend.
7
+ * REPOSKEIN_STORE = "jsonl" | "neo4j" | "auto" (default "auto").
8
+ * - auto: JSONL if <repoPath>/.reposkein/nodes.jsonl exists, else Neo4j if
9
+ * NEO4J_PASSWORD is set, else Unconfigured.
10
+ * - jsonl: JSONL if available, else Unconfigured.
11
+ * - neo4j: Neo4j if configured, else Unconfigured.
12
+ *
13
+ * `agent` names the writer for sidecar selection (one summaries file per
14
+ * agent, see store/sidecar.ts); undefined keeps the REPOSKEIN_AGENT default. */
15
+ export declare function buildStore(repoPath: string | undefined, repoId: string | undefined, agent?: string): GraphStore;
16
+ /** Structured, actionable message for repo-scoped tool calls when no repo
17
+ * resolved (see `resolveRepoPath`). Names the discovered candidates when
18
+ * resolution failed due to workspace-mode ambiguity, and points at
19
+ * `list_repos` / `select_repo` so the agent can pick one instead of
20
+ * abandoning the tools. */
21
+ export declare function repoRequiredMessage(resolution: RepoResolution): string;
22
+ /** A repo resolved but has no committed `meta.json` (never indexed). */
23
+ export declare function repoUnindexedMessage(repoPath: string): string;
24
+ /** Whether a graph is available to READ at `repoPath` without building one.
25
+ *
26
+ * Mirrors `buildStore`'s backend choice exactly (same env, same order), so
27
+ * "we can serve reads" and "we picked a real store" can never disagree:
28
+ * explicit neo4j mode trusts the DB, otherwise the committed-derived JSONL
29
+ * decides, and `auto` falls back to Neo4j when a password is configured. */
30
+ export declare function graphAvailable(repoPath: string, env?: NodeJS.ProcessEnv): boolean;
31
+ /** A read-only connection resolved a repo whose derived graph isn't built.
32
+ *
33
+ * Building it means spawning the indexer and writing `.reposkein/*.jsonl` —
34
+ * a write, and the LAST write a read-only token could still trigger, since
35
+ * `ensureGraph` used to run unconditionally on first touch of any repo. So a
36
+ * read-only caller gets this instead: an explanation and the exact command
37
+ * for whoever does hold write access, with nothing changed on disk. */
38
+ export declare function readOnlyUnindexedMessage(repoPath: string): string;
39
+ /** Exported for schema tests. */
40
+ export declare const getContextProfileInputSchema: {
41
+ node_id: z.ZodOptional<z.ZodString>;
42
+ file_path: z.ZodOptional<z.ZodString>;
43
+ name: z.ZodOptional<z.ZodString>;
44
+ hops: z.ZodOptional<z.ZodNumber>;
45
+ federated: z.ZodOptional<z.ZodBoolean>;
46
+ };
47
+ /** Exported for schema tests. */
48
+ export declare const recordDecisionInputSchema: {
49
+ title: z.ZodString;
50
+ context: z.ZodString;
51
+ decision: z.ZodString;
52
+ consequences: z.ZodOptional<z.ZodString>;
53
+ alternatives: z.ZodOptional<z.ZodString>;
54
+ status: z.ZodOptional<z.ZodEnum<{
55
+ proposed: "proposed";
56
+ accepted: "accepted";
57
+ }>>;
58
+ anchor_node_ids: z.ZodOptional<z.ZodArray<z.ZodString>>;
59
+ anchor_paths: z.ZodOptional<z.ZodArray<z.ZodString>>;
60
+ supersedes: z.ZodOptional<z.ZodArray<z.ZodString>>;
61
+ };
62
+ export declare const setDecisionStatusInputSchema: {
63
+ decision_id: z.ZodString;
64
+ status: z.ZodEnum<{
65
+ accepted: "accepted";
66
+ rejected: "rejected";
67
+ deprecated: "deprecated";
68
+ }>;
69
+ };
70
+ export declare const reaffirmDecisionInputSchema: {
71
+ decision_id: z.ZodString;
72
+ };
73
+ export declare const listDecisionsInputSchema: {
74
+ status: z.ZodOptional<z.ZodEnum<{
75
+ proposed: "proposed";
76
+ accepted: "accepted";
77
+ rejected: "rejected";
78
+ deprecated: "deprecated";
79
+ superseded: "superseded";
80
+ }>>;
81
+ anchor: z.ZodOptional<z.ZodString>;
82
+ q: z.ZodOptional<z.ZodString>;
83
+ limit: z.ZodOptional<z.ZodNumber>;
84
+ };
85
+ export declare const getDecisionInputSchema: {
86
+ decision_id: z.ZodString;
87
+ };
88
+ /** The tools that MUTATE the repo (sidecar prose, decision records, the
89
+ * derived graph). Gated behind the `write` capability in `serve --http`
90
+ * (REP-17); always available over stdio, where the caller already owns the
91
+ * checkout.
92
+ *
93
+ * `select_repo` is deliberately NOT here: it mutates only this connection's
94
+ * own `RepoSession`, which since REP-17 is per-connection state, so a
95
+ * read-only caller pointing itself at a different discovered repo affects
96
+ * nobody else. */
97
+ export declare const WRITE_TOOLS: readonly ["write_semantic_summary", "record_decision", "set_decision_status", "reaffirm_decision", "reindex_file", "init_cpg_skeleton"];
98
+ export interface ToolCapabilities {
99
+ /** False = read-only connection: every WRITE_TOOLS call is refused. */
100
+ write: boolean;
101
+ }
102
+ export interface CreateMcpServerOptions {
103
+ /** Working directory repo discovery starts from. */
104
+ cwd: string;
105
+ /** REPOSKEIN_REPO_PATH, or undefined. */
106
+ envRepoPath: string | undefined;
107
+ /** Session-log id. Defaults to `resolveSessionId(process.env)` (the stdio
108
+ * behaviour: one id per server process). */
109
+ sessionId?: string;
110
+ /** Writer identity recorded as `summary_by`/`decided_by` and used to pick
111
+ * the per-agent sidecar file. Undefined keeps the historical
112
+ * `REPOSKEIN_AGENT ?? "agent"` fallback inside the tools. */
113
+ identity?: string;
114
+ /** Defaults to `{ write: true }` — the stdio behaviour. */
115
+ capabilities?: ToolCapabilities;
116
+ /** Test seam for the first-touch graph build. Defaults to the real
117
+ * `ensureGraph`. Injected in tests so the read-only build gate can be
118
+ * asserted (called / not called) without spawning an indexer. */
119
+ ensureGraph?: typeof ensureGraph;
120
+ }
121
+ /** Builds ONE MCP server with its own session state.
122
+ *
123
+ * Everything a caller can move — the active repo (`select_repo`), the
124
+ * per-repoPath store/context cache, the tool-call logger — lives in this
125
+ * closure, so two connections into the same process cannot see each other's
126
+ * selection. That is what makes `serve --http` (REP-17) safe; over stdio
127
+ * there is exactly one of these per process, which is byte-for-byte the
128
+ * behaviour that existed before the extraction. */
129
+ export declare function createMcpServer(opts: CreateMcpServerOptions): McpServer;