@rockhopper-co/mcp-server 0.7.0 → 0.10.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 (52) hide show
  1. package/CHANGELOG.md +68 -0
  2. package/README.md +31 -0
  3. package/dist/api-client.d.ts +83 -3
  4. package/dist/api-client.d.ts.map +1 -1
  5. package/dist/api-client.js +171 -10
  6. package/dist/api-client.js.map +1 -1
  7. package/dist/auth/device-grant-client.d.ts.map +1 -1
  8. package/dist/auth/device-grant-client.js +3 -1
  9. package/dist/auth/device-grant-client.js.map +1 -1
  10. package/dist/cli.js +39 -1
  11. package/dist/cli.js.map +1 -1
  12. package/dist/correlation.d.ts +9 -0
  13. package/dist/correlation.d.ts.map +1 -0
  14. package/dist/correlation.js +26 -0
  15. package/dist/correlation.js.map +1 -0
  16. package/dist/logger.d.ts +42 -0
  17. package/dist/logger.d.ts.map +1 -0
  18. package/dist/logger.js +159 -0
  19. package/dist/logger.js.map +1 -0
  20. package/dist/not-ready.d.ts +95 -0
  21. package/dist/not-ready.d.ts.map +1 -0
  22. package/dist/not-ready.js +151 -0
  23. package/dist/not-ready.js.map +1 -0
  24. package/dist/prompts/index.d.ts.map +1 -1
  25. package/dist/prompts/index.js +8 -0
  26. package/dist/prompts/index.js.map +1 -1
  27. package/dist/resources/changes.d.ts.map +1 -1
  28. package/dist/resources/changes.js +6 -0
  29. package/dist/resources/changes.js.map +1 -1
  30. package/dist/resources/orchestration-guide.md +23 -3
  31. package/dist/resources/teams.d.ts.map +1 -1
  32. package/dist/resources/teams.js +7 -1
  33. package/dist/resources/teams.js.map +1 -1
  34. package/dist/server.d.ts.map +1 -1
  35. package/dist/server.js +40 -1
  36. package/dist/server.js.map +1 -1
  37. package/dist/tools/get-cell-history.d.ts.map +1 -1
  38. package/dist/tools/get-cell-history.js +28 -4
  39. package/dist/tools/get-cell-history.js.map +1 -1
  40. package/dist/tools/search.d.ts.map +1 -1
  41. package/dist/tools/search.js +13 -1
  42. package/dist/tools/search.js.map +1 -1
  43. package/dist/tools/write-reviews.d.ts.map +1 -1
  44. package/dist/tools/write-reviews.js +58 -4
  45. package/dist/tools/write-reviews.js.map +1 -1
  46. package/dist/types.d.ts +58 -0
  47. package/dist/types.d.ts.map +1 -1
  48. package/dist/zod-schemas.d.ts +30 -54
  49. package/dist/zod-schemas.d.ts.map +1 -1
  50. package/dist/zod-schemas.js +24 -0
  51. package/dist/zod-schemas.js.map +1 -1
  52. package/package.json +22 -10
package/dist/cli.js CHANGED
@@ -2,8 +2,25 @@
2
2
  import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
3
3
  import { ApiClient } from './api-client.js';
4
4
  import { AuthResolutionError, resolveAuth, } from './auth/resolve-auth.js';
5
+ import { flushLoggerSync, initLogger, log, serviceVersion, } from './logger.js';
5
6
  import { createServer } from './server.js';
6
7
  const ROCKHOPPER_API_URL = process.env.ROCKHOPPER_API_URL || 'https://api.rockhopper.co';
8
+ // KI-225: start the local rotating diagnostic logfile before anything else,
9
+ // so startup-path failures (auth, preflight, crashes) are captured. Never
10
+ // throws and never writes to stdout — falls back to a no-op logger.
11
+ await initLogger();
12
+ // Capture client-side crashes that would otherwise vanish (the backend can't
13
+ // see them). Registering these handlers suppresses Node's default crash, so
14
+ // uncaughtException re-exits 1 to PRESERVE the existing exit behavior; the
15
+ // fatal line is flushed to disk synchronously first.
16
+ process.on('uncaughtException', (err) => {
17
+ log.fatal({ event: 'uncaught_exception', err }, 'uncaught_exception');
18
+ flushLoggerSync();
19
+ process.exit(1);
20
+ });
21
+ process.on('unhandledRejection', (reason) => {
22
+ log.error({ event: 'unhandled_rejection', err: reason }, 'unhandled_rejection');
23
+ });
7
24
  // ENG-1444: auth resolution order is
8
25
  // 1. ROCKHOPPER_TOKEN env var (Personal Access Token — headless / CI)
9
26
  // 2. Stored OAuth bundle in the OS keychain (prior device-grant flow)
@@ -17,6 +34,8 @@ try {
17
34
  }
18
35
  catch (err) {
19
36
  if (err instanceof AuthResolutionError) {
37
+ // KI-225: local auth failure before any API call (bad token / sign-in).
38
+ log.warn({ event: 'auth_failed', reason: err.code }, 'auth_failed');
20
39
  if (err.code === 'pat_malformed') {
21
40
  console.error(`Error: ${err.message}`);
22
41
  console.error('Tokens start with "rh_pat_". Check that the full token was copied correctly.');
@@ -34,16 +53,33 @@ catch (err) {
34
53
  }
35
54
  process.exit(1);
36
55
  }
56
+ // Every catch branch above ends in `process.exit(1)`, so `resolved` is always
57
+ // assigned past this point. TS 6.0's stricter control-flow analysis can't prove
58
+ // that across the implicit-typed `let`, so narrow it explicitly here.
59
+ if (!resolved) {
60
+ process.exit(1);
61
+ }
37
62
  const apiClient = new ApiClient({
38
63
  baseUrl: ROCKHOPPER_API_URL,
39
64
  token: resolved.accessToken,
40
65
  });
41
66
  try {
42
- await apiClient.getMe();
67
+ const me = await apiClient.getMe();
68
+ // ENG-1756 (plan §9 decision 15): the PAT owner IS the human driving this
69
+ // local agent — reuse the preflight to declare them, so every write carries
70
+ // `X-Driving-Human` and the backend's anonymous-agent-write admission
71
+ // never fires for this client. Best-effort: while unset, the backend
72
+ // resolves the PAT owner server-side (same human).
73
+ apiClient.setDrivingHuman(me?.msId ?? me?.googleId ?? null);
43
74
  }
44
75
  catch (err) {
45
76
  const msg = err instanceof Error ? err.message : String(err);
46
77
  if (msg.includes('401') || msg.includes('403')) {
78
+ // KI-225: preflight token rejection — the local auth-fail signal.
79
+ log.warn({
80
+ event: 'auth_failed',
81
+ reason: resolved.source === 'pat' ? 'pat_invalid' : 'oauth_invalid',
82
+ }, 'auth_failed');
47
83
  if (resolved.source === 'pat') {
48
84
  console.error('Error: ROCKHOPPER_TOKEN is invalid or expired.\n' +
49
85
  'Create a new Personal Access Token in Rockhopper Settings and set it as ROCKHOPPER_TOKEN.');
@@ -60,6 +96,8 @@ catch (err) {
60
96
  process.exit(1);
61
97
  }
62
98
  const server = createServer(apiClient);
99
+ // KI-225: one line per launch — anchors a session in the file.
100
+ log.info({ event: 'mcp_server_start', version: serviceVersion }, 'mcp_server_start');
63
101
  const transport = new StdioServerTransport();
64
102
  await server.connect(transport);
65
103
  //# sourceMappingURL=cli.js.map
package/dist/cli.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"cli.js","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"cli.js","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,9 @@
1
+ /**
2
+ * Runs `fn` inside an AsyncLocalStorage scope carrying `id`. Reads via
3
+ * {@link getCorrelationId} anywhere in the (sync or async) continuation
4
+ * return the same id. Mints a fresh UUID v4 when `id` is omitted.
5
+ */
6
+ export declare const runWithCorrelationId: <T>(fn: () => T, id?: string) => T;
7
+ /** Current correlation id, or `undefined` when called outside a scope. */
8
+ export declare const getCorrelationId: () => string | undefined;
9
+ //# sourceMappingURL=correlation.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"correlation.d.ts","sourceRoot":"","sources":["../src/correlation.ts"],"names":[],"mappings":"AAmBA;;;;GAIG;AACH,eAAO,MAAM,oBAAoB,GAAI,CAAC,EACpC,IAAI,MAAM,CAAC,EACX,KAAI,MAAqB,KACxB,CAAoB,CAAC;AAExB,0EAA0E;AAC1E,eAAO,MAAM,gBAAgB,QAAO,MAAM,GAAG,SAA2B,CAAC"}
@@ -0,0 +1,26 @@
1
+ import { AsyncLocalStorage } from 'node:async_hooks';
2
+ import { randomUUID } from 'node:crypto';
3
+ /**
4
+ * Phase 1.1 / KI-226 — correlation id propagation.
5
+ *
6
+ * Per-tool-call scope: a single correlation id covers every outbound
7
+ * Rockhopper API call a tool invocation makes (including multi-call
8
+ * fan-outs like search). The id rides the `X-Correlation-Id` header out of
9
+ * {@link ApiClient.request}, so an mcp-server-originated request is
10
+ * traceable as a group in backend logs.
11
+ *
12
+ * The id is a non-sensitive UUID v4 — never co-log the bearer token with it.
13
+ * `randomUUID` comes from `node:crypto` (NOT the global `crypto`) because the
14
+ * package supports Node 18, where global `crypto.randomUUID` is not
15
+ * guaranteed.
16
+ */
17
+ const als = new AsyncLocalStorage();
18
+ /**
19
+ * Runs `fn` inside an AsyncLocalStorage scope carrying `id`. Reads via
20
+ * {@link getCorrelationId} anywhere in the (sync or async) continuation
21
+ * return the same id. Mints a fresh UUID v4 when `id` is omitted.
22
+ */
23
+ export const runWithCorrelationId = (fn, id = randomUUID()) => als.run(id, fn);
24
+ /** Current correlation id, or `undefined` when called outside a scope. */
25
+ export const getCorrelationId = () => als.getStore();
26
+ //# sourceMappingURL=correlation.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"correlation.js","sourceRoot":"","sources":["../src/correlation.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,iBAAiB,EAAE,MAAM,kBAAkB,CAAC;AACrD,OAAO,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AAEzC;;;;;;;;;;;;;GAaG;AACH,MAAM,GAAG,GAAG,IAAI,iBAAiB,EAAU,CAAC;AAE5C;;;;GAIG;AACH,MAAM,CAAC,MAAM,oBAAoB,GAAG,CAClC,EAAW,EACX,KAAa,UAAU,EAAE,EACtB,EAAE,CAAC,GAAG,CAAC,GAAG,CAAC,EAAE,EAAE,EAAE,CAAC,CAAC;AAExB,0EAA0E;AAC1E,MAAM,CAAC,MAAM,gBAAgB,GAAG,GAAuB,EAAE,CAAC,GAAG,CAAC,QAAQ,EAAE,CAAC"}
@@ -0,0 +1,42 @@
1
+ import pino from 'pino';
2
+ import pinoRoll from 'pino-roll';
3
+ /** The 5 log methods we expose. A real `pino.Logger` satisfies this. */
4
+ export interface DiagnosticLogger {
5
+ debug: pino.LogFn;
6
+ info: pino.LogFn;
7
+ warn: pino.LogFn;
8
+ error: pino.LogFn;
9
+ fatal: pino.LogFn;
10
+ }
11
+ export declare const serviceVersion: string;
12
+ /**
13
+ * Builds a file-only diagnostic logger. NEVER throws and NEVER writes to
14
+ * stdout — on opt-out or any failure it returns a no-op logger. Exposed
15
+ * (vs. only the {@link log} singleton) so tests can drive an isolated
16
+ * instance pointed at a tmpdir.
17
+ */
18
+ export declare function createDiagnosticLogger(opts?: {
19
+ dir?: string;
20
+ disable?: boolean;
21
+ level?: string;
22
+ }): Promise<{
23
+ logger: DiagnosticLogger;
24
+ flush: () => Promise<void>;
25
+ destination?: Awaited<ReturnType<typeof pinoRoll>>;
26
+ }>;
27
+ /**
28
+ * Process-wide singleton. Starts as a no-op (so importing this module —
29
+ * including from the library entry / mcp-gateway — has no side effect and
30
+ * opens no file) and is swapped to the real file logger by
31
+ * {@link initLogger}, which the stdio CLI calls at startup.
32
+ */
33
+ export declare let log: DiagnosticLogger;
34
+ /** Idempotently constructs the singleton from env config. Never rejects. */
35
+ export declare function initLogger(): Promise<void>;
36
+ /**
37
+ * Best-effort synchronous flush — for the crash path, where the last
38
+ * `fatal` line must hit disk before {@link process.exit}. No-op when the
39
+ * logger is disabled / not yet ready.
40
+ */
41
+ export declare function flushLoggerSync(): void;
42
+ //# sourceMappingURL=logger.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"logger.d.ts","sourceRoot":"","sources":["../src/logger.ts"],"names":[],"mappings":"AA2BA,OAAO,IAAI,MAAM,MAAM,CAAC;AACxB,OAAO,QAAQ,MAAM,WAAW,CAAC;AAGjC,wEAAwE;AACxE,MAAM,WAAW,gBAAgB;IAC/B,KAAK,EAAE,IAAI,CAAC,KAAK,CAAC;IAClB,IAAI,EAAE,IAAI,CAAC,KAAK,CAAC;IACjB,IAAI,EAAE,IAAI,CAAC,KAAK,CAAC;IACjB,KAAK,EAAE,IAAI,CAAC,KAAK,CAAC;IAClB,KAAK,EAAE,IAAI,CAAC,KAAK,CAAC;CACnB;AA0BD,eAAO,MAAM,cAAc,QAAkB,CAAC;AAuB9C;;;;;GAKG;AACH,wBAAsB,sBAAsB,CAC1C,IAAI,GAAE;IAAE,GAAG,CAAC,EAAE,MAAM,CAAC;IAAC,OAAO,CAAC,EAAE,OAAO,CAAC;IAAC,KAAK,CAAC,EAAE,MAAM,CAAA;CAAO,GAC7D,OAAO,CAAC;IACT,MAAM,EAAE,gBAAgB,CAAC;IACzB,KAAK,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;IAC3B,WAAW,CAAC,EAAE,OAAO,CAAC,UAAU,CAAC,OAAO,QAAQ,CAAC,CAAC,CAAC;CACpD,CAAC,CAmDD;AAED;;;;;GAKG;AACH,eAAO,IAAI,GAAG,EAAE,gBAA6B,CAAC;AAK9C,4EAA4E;AAC5E,wBAAsB,UAAU,IAAI,OAAO,CAAC,IAAI,CAAC,CAQhD;AAED;;;;GAIG;AACH,wBAAgB,eAAe,IAAI,IAAI,CAMtC"}
package/dist/logger.js ADDED
@@ -0,0 +1,159 @@
1
+ /**
2
+ * Phase 1.5 / KI-225 — local, rotating diagnostic logfile.
3
+ *
4
+ * Why this exists: the backend already logs everything that *reaches* the
5
+ * API. The unique value of a client-side log is the failures that NEVER
6
+ * reach the backend (network-unreachable, local auth rejection, schema
7
+ * drift, uncaught crashes) plus a local request-latency view. The file is
8
+ * the customer's to keep and hand to support — there is NO remote
9
+ * transmission.
10
+ *
11
+ * 🚨 HARD CONSTRAINT — stdout is the MCP stdio transport. Logs MUST go to
12
+ * the FILE ONLY, never stdout. We therefore:
13
+ * - build pino against a {@link pino-roll} Sonic-boom file destination
14
+ * (NOT pino's default stdout destination, NOT a worker transport), and
15
+ * - fall back to a NO-OP logger on ANY construction failure — never
16
+ * crash the server, never write to stdout.
17
+ *
18
+ * Redaction (SOC2-adjacent, runs on customer machines): callers must only
19
+ * ever pass safe fields — event, method, URL pathname (no query/token),
20
+ * status, durationMs, tool name, correlationId, version, error
21
+ * type/message. Never tokens, Authorization headers, request/response
22
+ * bodies, tool arguments, file contents, or cell data.
23
+ */
24
+ import fs from 'node:fs';
25
+ import { createRequire } from 'node:module';
26
+ import os from 'node:os';
27
+ import path from 'node:path';
28
+ import pino from 'pino';
29
+ import pinoRoll from 'pino-roll';
30
+ import { getCorrelationId } from './correlation.js';
31
+ const SERVICE_NAME = 'mcp-server';
32
+ const LOG_FILE_BASENAME = 'mcp-server.log';
33
+ const LOG_MAX_SIZE = '5m';
34
+ const LOG_FILE_COUNT = 5;
35
+ const DEFAULT_LOG_DIR = path.join(os.homedir(), '.rockhopper', 'mcp-server');
36
+ const VALID_LEVELS = new Set([
37
+ 'fatal',
38
+ 'error',
39
+ 'warn',
40
+ 'info',
41
+ 'debug',
42
+ 'trace',
43
+ 'silent',
44
+ ]);
45
+ /** Package version stamped into every line's `base`. Best-effort read. */
46
+ const requireJson = createRequire(import.meta.url);
47
+ let resolvedVersion = '0.0.0';
48
+ try {
49
+ const pkg = requireJson('../package.json');
50
+ if (pkg.version)
51
+ resolvedVersion = pkg.version;
52
+ }
53
+ catch {
54
+ // package.json unreadable — keep the sentinel; never crash over a version.
55
+ }
56
+ export const serviceVersion = resolvedVersion;
57
+ /** Shared no-op so disabled/failed logging is a single cheap call site. */
58
+ const noopFn = () => { };
59
+ const noopLogger = {
60
+ debug: noopFn,
61
+ info: noopFn,
62
+ warn: noopFn,
63
+ error: noopFn,
64
+ fatal: noopFn,
65
+ };
66
+ function isTruthyEnv(value) {
67
+ if (!value)
68
+ return false;
69
+ const v = value.trim().toLowerCase();
70
+ return v !== '' && v !== '0' && v !== 'false' && v !== 'no';
71
+ }
72
+ function normalizeLevel(level) {
73
+ const v = level?.trim().toLowerCase();
74
+ return v && VALID_LEVELS.has(v) ? v : 'info';
75
+ }
76
+ /**
77
+ * Builds a file-only diagnostic logger. NEVER throws and NEVER writes to
78
+ * stdout — on opt-out or any failure it returns a no-op logger. Exposed
79
+ * (vs. only the {@link log} singleton) so tests can drive an isolated
80
+ * instance pointed at a tmpdir.
81
+ */
82
+ export async function createDiagnosticLogger(opts = {}) {
83
+ const disable = opts.disable ?? isTruthyEnv(process.env.ROCKHOPPER_MCP_LOG_DISABLE);
84
+ if (disable) {
85
+ return { logger: noopLogger, flush: async () => { } };
86
+ }
87
+ try {
88
+ const envDir = process.env.ROCKHOPPER_MCP_LOG_DIR?.trim();
89
+ const dir = opts.dir ?? (envDir && envDir.length ? envDir : DEFAULT_LOG_DIR);
90
+ fs.mkdirSync(dir, { recursive: true });
91
+ const destination = await pinoRoll({
92
+ file: path.join(dir, LOG_FILE_BASENAME),
93
+ size: LOG_MAX_SIZE,
94
+ limit: { count: LOG_FILE_COUNT },
95
+ mkdir: true,
96
+ });
97
+ // A logging IO error must never crash the server (or escape as an
98
+ // unhandled 'error' event). Swallow it — diagnostics are best-effort.
99
+ destination.on('error', () => { });
100
+ const logger = pino({
101
+ level: normalizeLevel(opts.level ?? process.env.ROCKHOPPER_MCP_LOG_LEVEL),
102
+ base: { service: SERVICE_NAME, version: serviceVersion },
103
+ serializers: { err: pino.stdSerializers.err },
104
+ // Every line auto-carries the per-tool-call correlationId (Phase 1.1
105
+ // ALS scope) when one is active — no call site has to thread it.
106
+ mixin() {
107
+ const id = getCorrelationId();
108
+ return id ? { correlationId: id } : {};
109
+ },
110
+ }, destination);
111
+ const flush = () => new Promise((resolve) => {
112
+ try {
113
+ destination.flush(() => resolve());
114
+ }
115
+ catch {
116
+ resolve();
117
+ }
118
+ });
119
+ return { logger, flush, destination };
120
+ }
121
+ catch {
122
+ // fs / pino-roll / pino failure — degrade to no-op, never to stdout.
123
+ return { logger: noopLogger, flush: async () => { } };
124
+ }
125
+ }
126
+ /**
127
+ * Process-wide singleton. Starts as a no-op (so importing this module —
128
+ * including from the library entry / mcp-gateway — has no side effect and
129
+ * opens no file) and is swapped to the real file logger by
130
+ * {@link initLogger}, which the stdio CLI calls at startup.
131
+ */
132
+ export let log = noopLogger;
133
+ let activeDestination;
134
+ let initPromise;
135
+ /** Idempotently constructs the singleton from env config. Never rejects. */
136
+ export async function initLogger() {
137
+ if (initPromise)
138
+ return initPromise;
139
+ initPromise = (async () => {
140
+ const built = await createDiagnosticLogger();
141
+ log = built.logger;
142
+ activeDestination = built.destination;
143
+ })();
144
+ return initPromise;
145
+ }
146
+ /**
147
+ * Best-effort synchronous flush — for the crash path, where the last
148
+ * `fatal` line must hit disk before {@link process.exit}. No-op when the
149
+ * logger is disabled / not yet ready.
150
+ */
151
+ export function flushLoggerSync() {
152
+ try {
153
+ activeDestination?.flushSync();
154
+ }
155
+ catch {
156
+ // Stream not ready or already closed — nothing more we can do pre-exit.
157
+ }
158
+ }
159
+ //# sourceMappingURL=logger.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"logger.js","sourceRoot":"","sources":["../src/logger.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,95 @@
1
+ /**
2
+ * Plan 02 ruling 5 (David, 2026-08-04) — STRICT no-partial, on the machine
3
+ * surfaces.
4
+ *
5
+ * The rule David landed is "nothing serves change history until it is
6
+ * complete", and he chose STRICT specifically because THIS server is a
7
+ * consumer: rows leave here for Claude Desktop / Cursor, which get no banner,
8
+ * no colour and no human in the loop, and will narrate whatever arrives as
9
+ * fact. An empty list is the dangerous answer — "there are no changes" is a
10
+ * factual claim, and it is the claim an assistant makes when handed zero rows.
11
+ *
12
+ * So a not-ready answer here is a REFUSAL, not a value: `isError: true`, a
13
+ * shouting marker, an explicit "this is not an empty result", and a JSON
14
+ * object an assistant can branch on. A resource or prompt THROWS for the same
15
+ * reason a tool cannot return rows — a protocol error cannot be summarised as
16
+ * data.
17
+ *
18
+ * Vocabulary is deliberately the backend's, not a second one: the reasons
19
+ * mirror the SP02 `ParsedOutputNotReadyReason` family
20
+ * (`backend/src/common/parsed-output/parsed-output-not-ready.error.ts`), and
21
+ * `isNotReady` matches both the typed error and a wrapper carrying it as
22
+ * `cause`, exactly like the backend's `isParsedOutputNotReady`.
23
+ */
24
+ /** Why a change-history answer is being refused. */
25
+ export type NotReadyReason =
26
+ /** A commit-diff fold is still rewriting this file's change-log window. */
27
+ 'change_history_incomplete'
28
+ /** The backend answered 429/503: parsed outputs are still being produced. */
29
+ | 'still_producing'
30
+ /**
31
+ * The completeness probe itself could not answer. Fail CLOSED: under STRICT
32
+ * an unknown completeness state is not permission to serve rows. This is the
33
+ * one reason that is NOT a statement about the file.
34
+ */
35
+ | 'completeness_unknown';
36
+ /** Grep marker; also the first token of every refusal an assistant sees. */
37
+ export declare const NOT_READY_MARKER = "CHANGE_HISTORY_NOT_READY";
38
+ /** Poll hint when the backend gave none — mirrors the backend's own default. */
39
+ export declare const DEFAULT_RETRY_AFTER_SECONDS = 15;
40
+ export declare class ChangeHistoryNotReadyError extends Error {
41
+ /** Structural marker, so a wrapper can be recognised without importing. */
42
+ readonly notReady: true;
43
+ readonly reason: NotReadyReason;
44
+ readonly retryAfterSeconds: number;
45
+ readonly fileMsId: string | null;
46
+ constructor(ctx: {
47
+ reason: NotReadyReason;
48
+ retryAfterSeconds?: number | null;
49
+ fileMsId?: string | null;
50
+ detail?: string;
51
+ });
52
+ }
53
+ /** Matches the typed error AND any error carrying one as `cause`. */
54
+ export declare function isNotReady(err: unknown): err is ChangeHistoryNotReadyError;
55
+ export interface NotReadyToolResult {
56
+ /** The SDK's `CallToolResult` carries an index signature; mirror it so this
57
+ * shape is assignable to a tool handler's return type. */
58
+ [key: string]: unknown;
59
+ content: Array<{
60
+ type: 'text';
61
+ text: string;
62
+ }>;
63
+ isError: true;
64
+ }
65
+ /**
66
+ * The tool answer for a refusal. Three defences, because one is not enough
67
+ * against a model that wants to be helpful: `isError`, prose that names the
68
+ * wrong inference explicitly, and a JSON object to branch on.
69
+ */
70
+ export declare function notReadyToolResult(err: unknown): NotReadyToolResult;
71
+ /** The completeness probe this module needs from the API client. */
72
+ export interface CompletenessProbe {
73
+ getFoldStatus(fileMsId: string): Promise<{
74
+ foldPending: boolean;
75
+ foldTargetVersionId: number | null;
76
+ }>;
77
+ }
78
+ /**
79
+ * Throws {@link ChangeHistoryNotReadyError} unless the file's change history is
80
+ * complete.
81
+ *
82
+ * `foldPending` is the backend's own authoritative queue read
83
+ * (`GET /file-versions/file/:fileMsId/fold-status`, KI-1399) — while it is
84
+ * true a commit-diff fold is queued, retrying or running, and the change-log
85
+ * window is mid-rewrite. That is precisely the incomplete state, and after
86
+ * plan 02's write-path decoupling (David Q3, 2026-08-03: save the version row
87
+ * first, defer the fold) it is the NORMAL state immediately after any write.
88
+ *
89
+ * A probe that cannot answer refuses. The backend's own probe fails OPEN to
90
+ * not-pending on purpose — a UI lock that can hang is worse than a stale row —
91
+ * but that trade does not transfer here: for a machine consumer a wrong
92
+ * "complete" is a fabricated fact, so the client-side default is the opposite.
93
+ */
94
+ export declare function assertChangeHistoryComplete(api: CompletenessProbe, fileMsId: string): Promise<void>;
95
+ //# sourceMappingURL=not-ready.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"not-ready.d.ts","sourceRoot":"","sources":["../src/not-ready.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;GAsBG;AAEH,oDAAoD;AACpD,MAAM,MAAM,cAAc;AACxB,2EAA2E;AACzE,2BAA2B;AAC7B,6EAA6E;GAC3E,iBAAiB;AACnB;;;;GAIG;GACD,sBAAsB,CAAC;AAE3B,4EAA4E;AAC5E,eAAO,MAAM,gBAAgB,6BAA6B,CAAC;AAE3D,gFAAgF;AAChF,eAAO,MAAM,2BAA2B,KAAK,CAAC;AAE9C,qBAAa,0BAA2B,SAAQ,KAAK;IACnD,2EAA2E;IAC3E,QAAQ,CAAC,QAAQ,EAAG,IAAI,CAAU;IAClC,QAAQ,CAAC,MAAM,EAAE,cAAc,CAAC;IAChC,QAAQ,CAAC,iBAAiB,EAAE,MAAM,CAAC;IACnC,QAAQ,CAAC,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAC;gBAErB,GAAG,EAAE;QACf,MAAM,EAAE,cAAc,CAAC;QACvB,iBAAiB,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;QAClC,QAAQ,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;QACzB,MAAM,CAAC,EAAE,MAAM,CAAC;KACjB;CAeF;AAED,qEAAqE;AACrE,wBAAgB,UAAU,CAAC,GAAG,EAAE,OAAO,GAAG,GAAG,IAAI,0BAA0B,CAI1E;AAyBD,MAAM,WAAW,kBAAkB;IACjC;8DAC0D;IAC1D,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC;IACvB,OAAO,EAAE,KAAK,CAAC;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;IAC/C,OAAO,EAAE,IAAI,CAAC;CACf;AAED;;;;GAIG;AACH,wBAAgB,kBAAkB,CAAC,GAAG,EAAE,OAAO,GAAG,kBAAkB,CAwBnE;AAED,oEAAoE;AACpE,MAAM,WAAW,iBAAiB;IAChC,aAAa,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC;QACvC,WAAW,EAAE,OAAO,CAAC;QACrB,mBAAmB,EAAE,MAAM,GAAG,IAAI,CAAC;KACpC,CAAC,CAAC;CACJ;AAED;;;;;;;;;;;;;;;GAeG;AACH,wBAAsB,2BAA2B,CAC/C,GAAG,EAAE,iBAAiB,EACtB,QAAQ,EAAE,MAAM,GACf,OAAO,CAAC,IAAI,CAAC,CA4Bf"}
@@ -0,0 +1,151 @@
1
+ /**
2
+ * Plan 02 ruling 5 (David, 2026-08-04) — STRICT no-partial, on the machine
3
+ * surfaces.
4
+ *
5
+ * The rule David landed is "nothing serves change history until it is
6
+ * complete", and he chose STRICT specifically because THIS server is a
7
+ * consumer: rows leave here for Claude Desktop / Cursor, which get no banner,
8
+ * no colour and no human in the loop, and will narrate whatever arrives as
9
+ * fact. An empty list is the dangerous answer — "there are no changes" is a
10
+ * factual claim, and it is the claim an assistant makes when handed zero rows.
11
+ *
12
+ * So a not-ready answer here is a REFUSAL, not a value: `isError: true`, a
13
+ * shouting marker, an explicit "this is not an empty result", and a JSON
14
+ * object an assistant can branch on. A resource or prompt THROWS for the same
15
+ * reason a tool cannot return rows — a protocol error cannot be summarised as
16
+ * data.
17
+ *
18
+ * Vocabulary is deliberately the backend's, not a second one: the reasons
19
+ * mirror the SP02 `ParsedOutputNotReadyReason` family
20
+ * (`backend/src/common/parsed-output/parsed-output-not-ready.error.ts`), and
21
+ * `isNotReady` matches both the typed error and a wrapper carrying it as
22
+ * `cause`, exactly like the backend's `isParsedOutputNotReady`.
23
+ */
24
+ /** Grep marker; also the first token of every refusal an assistant sees. */
25
+ export const NOT_READY_MARKER = 'CHANGE_HISTORY_NOT_READY';
26
+ /** Poll hint when the backend gave none — mirrors the backend's own default. */
27
+ export const DEFAULT_RETRY_AFTER_SECONDS = 15;
28
+ export class ChangeHistoryNotReadyError extends Error {
29
+ /** Structural marker, so a wrapper can be recognised without importing. */
30
+ notReady = true;
31
+ reason;
32
+ retryAfterSeconds;
33
+ fileMsId;
34
+ constructor(ctx) {
35
+ super(`${NOT_READY_MARKER}: reason=${ctx.reason} ` +
36
+ `retryAfterSeconds=${ctx.retryAfterSeconds ?? DEFAULT_RETRY_AFTER_SECONDS}` +
37
+ (ctx.detail ? ` — ${ctx.detail}` : '') +
38
+ ' — Rockhopper has not finished computing this change history. ' +
39
+ 'This is NOT an empty result: no rows can be served yet, and nothing ' +
40
+ 'may be inferred about whether the file changed.');
41
+ this.name = 'ChangeHistoryNotReadyError';
42
+ this.reason = ctx.reason;
43
+ this.retryAfterSeconds =
44
+ ctx.retryAfterSeconds ?? DEFAULT_RETRY_AFTER_SECONDS;
45
+ this.fileMsId = ctx.fileMsId ?? null;
46
+ }
47
+ }
48
+ /** Matches the typed error AND any error carrying one as `cause`. */
49
+ export function isNotReady(err) {
50
+ if (err instanceof ChangeHistoryNotReadyError)
51
+ return true;
52
+ const cause = err?.cause;
53
+ return cause instanceof ChangeHistoryNotReadyError;
54
+ }
55
+ /**
56
+ * HTTP statuses that answer the question DEFINITIVELY. A permanently-refused
57
+ * request is not "still producing", and the two must not share an answer: a
58
+ * probe that 404s (no such file) or 403s (no access) has told us something
59
+ * true, and dressing it up as a capacity signal would send an assistant into a
60
+ * retry loop against a wall. Matched structurally (`err.status`) so this module
61
+ * stays free of an import cycle with the API client.
62
+ */
63
+ const DEFINITIVE_HTTP_STATUSES = new Set([400, 401, 403, 404, 405, 410, 422]);
64
+ function isDefinitiveRejection(err) {
65
+ const status = err?.status;
66
+ return typeof status === 'number' && DEFINITIVE_HTTP_STATUSES.has(status);
67
+ }
68
+ /** Unwrap to the typed error (accepts the `cause` shape). */
69
+ function unwrap(err) {
70
+ if (err instanceof ChangeHistoryNotReadyError)
71
+ return err;
72
+ const cause = err.cause;
73
+ if (cause instanceof ChangeHistoryNotReadyError)
74
+ return cause;
75
+ return new ChangeHistoryNotReadyError({ reason: 'completeness_unknown' });
76
+ }
77
+ /**
78
+ * The tool answer for a refusal. Three defences, because one is not enough
79
+ * against a model that wants to be helpful: `isError`, prose that names the
80
+ * wrong inference explicitly, and a JSON object to branch on.
81
+ */
82
+ export function notReadyToolResult(err) {
83
+ const e = unwrap(err);
84
+ const payload = {
85
+ status: 'not_ready',
86
+ reason: e.reason,
87
+ retryAfterSeconds: e.retryAfterSeconds,
88
+ fileMsId: e.fileMsId,
89
+ };
90
+ return {
91
+ content: [
92
+ {
93
+ type: 'text',
94
+ text: `${NOT_READY_MARKER} — this is NOT a result and NOT an empty result.\n` +
95
+ `Rockhopper has not finished computing this file's change history, ` +
96
+ `so no rows can be served.\n` +
97
+ `Do NOT say there are no changes, that nothing changed, or that the ` +
98
+ `history is empty — none of that is known.\n` +
99
+ `Retry in ${e.retryAfterSeconds} seconds.\n` +
100
+ JSON.stringify(payload),
101
+ },
102
+ ],
103
+ isError: true,
104
+ };
105
+ }
106
+ /**
107
+ * Throws {@link ChangeHistoryNotReadyError} unless the file's change history is
108
+ * complete.
109
+ *
110
+ * `foldPending` is the backend's own authoritative queue read
111
+ * (`GET /file-versions/file/:fileMsId/fold-status`, KI-1399) — while it is
112
+ * true a commit-diff fold is queued, retrying or running, and the change-log
113
+ * window is mid-rewrite. That is precisely the incomplete state, and after
114
+ * plan 02's write-path decoupling (David Q3, 2026-08-03: save the version row
115
+ * first, defer the fold) it is the NORMAL state immediately after any write.
116
+ *
117
+ * A probe that cannot answer refuses. The backend's own probe fails OPEN to
118
+ * not-pending on purpose — a UI lock that can hang is worse than a stale row —
119
+ * but that trade does not transfer here: for a machine consumer a wrong
120
+ * "complete" is a fabricated fact, so the client-side default is the opposite.
121
+ */
122
+ export async function assertChangeHistoryComplete(api, fileMsId) {
123
+ let status;
124
+ try {
125
+ status = await api.getFoldStatus(fileMsId);
126
+ }
127
+ catch (err) {
128
+ if (isNotReady(err))
129
+ throw unwrap(err);
130
+ // A definitive rejection is the caller's real answer — let it through so
131
+ // the tool reports "not found" / "no access" instead of "retry in 15s".
132
+ if (isDefinitiveRejection(err))
133
+ throw err;
134
+ throw new ChangeHistoryNotReadyError({
135
+ reason: 'completeness_unknown',
136
+ fileMsId,
137
+ detail: `fold-status probe failed: ${err instanceof Error ? err.message : String(err)}`,
138
+ });
139
+ }
140
+ if (status.foldPending) {
141
+ throw new ChangeHistoryNotReadyError({
142
+ reason: 'change_history_incomplete',
143
+ fileMsId,
144
+ detail: `a commit-diff fold is still pending` +
145
+ (status.foldTargetVersionId == null
146
+ ? ''
147
+ : ` for version ${status.foldTargetVersionId}`),
148
+ });
149
+ }
150
+ }
151
+ //# sourceMappingURL=not-ready.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"not-ready.js","sourceRoot":"","sources":["../src/not-ready.ts"],"names":[],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/prompts/index.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,yCAAyC,CAAC;AAEzE,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,kBAAkB,CAAC;AAElD,wBAAgB,eAAe,CAAC,MAAM,EAAE,SAAS,EAAE,GAAG,EAAE,SAAS,GAAG,IAAI,CAuNvE"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/prompts/index.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,yCAAyC,CAAC;AAEzE,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,kBAAkB,CAAC;AAGlD,wBAAgB,eAAe,CAAC,MAAM,EAAE,SAAS,EAAE,GAAG,EAAE,SAAS,GAAG,IAAI,CAgOvE"}
@@ -1,4 +1,5 @@
1
1
  import { z } from 'zod';
2
+ import { assertChangeHistoryComplete } from '../not-ready.js';
2
3
  export function registerPrompts(server, api) {
3
4
  server.registerPrompt('summarize-file-changes', {
4
5
  title: 'Summarize Recent Changes',
@@ -7,6 +8,10 @@ export function registerPrompts(server, api) {
7
8
  fileMsId: z.string().describe('Platform ID of the enrolled file'),
8
9
  },
9
10
  }, async ({ fileMsId }) => {
11
+ // Plan 02 ruling 5 (STRICT) — a prompt is the highest-risk surface: its
12
+ // whole product is a model narrating these rows as fact. Refuse before
13
+ // assembling anything; a prompt has no error channel but its own throw.
14
+ await assertChangeHistoryComplete(api, fileMsId);
10
15
  const [file, versions, changesPage] = await Promise.all([
11
16
  api.getEnrolledFile(fileMsId),
12
17
  api.getFileVersions(fileMsId),
@@ -118,6 +123,9 @@ export function registerPrompts(server, api) {
118
123
  fileMsId: z.string().describe('Platform ID of the enrolled file'),
119
124
  },
120
125
  }, async ({ fileMsId }) => {
126
+ // Plan 02 ruling 5 (STRICT) — this prompt reports a change COUNT, which
127
+ // is the same factual claim in one number instead of many rows.
128
+ await assertChangeHistoryComplete(api, fileMsId);
121
129
  const [file, versions, comments, changesPage] = await Promise.all([
122
130
  api.getEnrolledFile(fileMsId),
123
131
  api.getFileVersions(fileMsId),