@dev.fast/whiteboard 0.0.0-stage → 0.2.1-preview.20261005.98
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/LICENSE +21 -0
- package/README.md +146 -2
- package/THIRD_PARTY_NOTICES.md +93 -0
- package/dist/account-alias-DOHN1RSH.js +973 -0
- package/dist/agent-cli-ChOq0ZuO.js +117 -0
- package/dist/agent-cli-DzWfZr6G.js +2 -0
- package/dist/agent-client-CPQo7iTI.js +305 -0
- package/dist/authoring-tools-Cr6Kpjsx.js +234 -0
- package/dist/build-info.json +1 -0
- package/dist/cli-hVgHcdsv.js +47 -0
- package/dist/cli-runner-D8y2_luU.js +5095 -0
- package/dist/cli.d.ts +1 -0
- package/dist/cli.js +128 -0
- package/dist/client-DFoh50W3.js +950 -0
- package/dist/desktop-discovery-DBO-PM2V.js +164 -0
- package/dist/error-message-OtiDonty.js +6 -0
- package/dist/fs-utils-BMPLt0cr.js +22 -0
- package/dist/fuzzy-match-BoAmcyak.js +30 -0
- package/dist/headless-host-BJxf4dUU.js +94 -0
- package/dist/input-error-OLgB_h31.js +11 -0
- package/dist/local-data-CxLk75rx.js +4491 -0
- package/dist/mcp-QeF8vkdf.js +140 -0
- package/dist/package-paths-B6-zxvIO.js +47 -0
- package/dist/process-error-telemetry-v9e5h6D7.js +3952 -0
- package/dist/profile-4ry2f3AM.js +76 -0
- package/dist/profile-Bh5FIrmf.js +2 -0
- package/dist/request-origin-D_4QIdoN.js +811 -0
- package/dist/review-agent-traces-DNJFYzc8.js +3866 -0
- package/dist/review-home-paths-6zZH-1c9.js +2560 -0
- package/dist/review-telemetry-DEFnQsQ2.js +1195 -0
- package/dist/runtime-CKiWhGrf.js +52 -0
- package/dist/runtime.d.ts +9 -0
- package/dist/runtime.js +2 -0
- package/dist/s3-SzLpeCG6.js +345 -0
- package/dist/s3-config-BgxkSoOy.js +2 -0
- package/dist/s3-config-CAxhO9u_.js +676 -0
- package/dist/server/desktop-host.d.ts +4 -0
- package/dist/server/desktop-host.js +1702 -0
- package/dist/server-discovery-Dbzn3w6Z.js +66 -0
- package/dist/sharing/index.d.ts +1977 -0
- package/dist/sharing/index.js +2 -0
- package/dist/src-CGP5ytbV.js +283 -0
- package/dist/src-CmdiBL20.js +3676 -0
- package/dist/src-DnwdaQ2r.js +865 -0
- package/dist/src-X9phtB2j.js +68 -0
- package/dist/stored-document-migration-DHeMpLMb.js +2671 -0
- package/dist/tool-failure-C8zB73HV.js +98 -0
- package/dist/tutorial-trace-DrBvkaeL.js +43 -0
- package/instructions/authoring.md +36 -0
- package/instructions/file-lenses.md +13 -0
- package/instructions/scratchpad.md +23 -0
- package/instructions/trace-archaeology.md +113 -0
- package/onboarding.md +8 -0
- package/package.json +106 -3
- package/src/agent-selection.ts +100 -0
- package/src/agent-session-ref.ts +80 -0
- package/src/ask/agents.ts +351 -0
- package/src/ask/checkout-files.ts +47 -0
- package/src/ask/file-refs.ts +111 -0
- package/src/ask/pi-mcp.ts +154 -0
- package/src/ask/protocol.ts +244 -0
- package/src/ask/thread-state.ts +350 -0
- package/src/ask/thread.ts +1400 -0
- package/src/ask/threads.ts +219 -0
- package/src/ask/watch.ts +79 -0
- package/src/cli-install.ts +1041 -0
- package/src/cli-runner.ts +1461 -0
- package/src/cli-runtime-info.ts +35 -0
- package/src/cli.ts +222 -0
- package/src/connect-prompts.ts +248 -0
- package/src/cursor-deeplink.ts +11 -0
- package/src/desktop-discovery.ts +355 -0
- package/src/diff-selection-migration.ts +63 -0
- package/src/embedded-posthog-key.ts +6 -0
- package/src/error-telemetry.ts +247 -0
- package/src/evidence.ts +14 -0
- package/src/exception-telemetry.ts +126 -0
- package/src/fixtures/blocks/call_stack_diff.json +27 -0
- package/src/fixtures/blocks/callout.json +11 -0
- package/src/fixtures/blocks/code.json +9 -0
- package/src/fixtures/blocks/code_peek.json +8 -0
- package/src/fixtures/blocks/database_lens.json +51 -0
- package/src/fixtures/blocks/divider.json +1 -0
- package/src/fixtures/blocks/fixtures.ts +33 -0
- package/src/fixtures/blocks/flow_diagram.json +36 -0
- package/src/fixtures/blocks/ids.ts +8 -0
- package/src/fixtures/blocks/image.json +9 -0
- package/src/fixtures/blocks/markdown.json +7 -0
- package/src/fixtures/blocks/section.json +11 -0
- package/src/fixtures/blocks/sequence.json +31 -0
- package/src/fixtures/blocks/software_map.json +7 -0
- package/src/fixtures/blocks/trace_quote.json +9 -0
- package/src/fixtures/blocks/tutorial.json +51 -0
- package/src/fs-utils.ts +32 -0
- package/src/fuzzy-match.ts +94 -0
- package/src/install.ts +29 -0
- package/src/legacy-skills.ts +133 -0
- package/src/lens-selection.ts +230 -0
- package/src/markdown-latex-math.ts +230 -0
- package/src/markdown.ts +81 -0
- package/src/package-paths.ts +53 -0
- package/src/posthog-capture-client.ts +610 -0
- package/src/review-api/README.md +295 -0
- package/src/review-api/activity.ts +340 -0
- package/src/review-api/agent-cli.ts +219 -0
- package/src/review-api/agent-client.ts +188 -0
- package/src/review-api/anchor-quotes.ts +88 -0
- package/src/review-api/ask-history.ts +242 -0
- package/src/review-api/authoring-tools.ts +211 -0
- package/src/review-api/blocks/call_stack_diff.ts +75 -0
- package/src/review-api/blocks/callout.ts +24 -0
- package/src/review-api/blocks/code.ts +9 -0
- package/src/review-api/blocks/code_peek.ts +13 -0
- package/src/review-api/blocks/database_lens.ts +150 -0
- package/src/review-api/blocks/definition.ts +40 -0
- package/src/review-api/blocks/divider.ts +6 -0
- package/src/review-api/blocks/flow_diagram.ts +111 -0
- package/src/review-api/blocks/image.ts +10 -0
- package/src/review-api/blocks/index.ts +90 -0
- package/src/review-api/blocks/markdown.ts +17 -0
- package/src/review-api/blocks/section.ts +24 -0
- package/src/review-api/blocks/sequence.ts +56 -0
- package/src/review-api/blocks/software_map.ts +9 -0
- package/src/review-api/blocks/trace_quote.ts +10 -0
- package/src/review-api/blocks/tutorial.ts +39 -0
- package/src/review-api/checkout-fs.ts +14 -0
- package/src/review-api/client.ts +1 -0
- package/src/review-api/comparison-coverage.ts +304 -0
- package/src/review-api/component-reference.ts +30 -0
- package/src/review-api/diff-lenses.ts +175 -0
- package/src/review-api/document-headings.ts +51 -0
- package/src/review-api/document-text.ts +200 -0
- package/src/review-api/document.ts +906 -0
- package/src/review-api/file-lenses.ts +100 -0
- package/src/review-api/http.ts +1877 -0
- package/src/review-api/image-decode.ts +29 -0
- package/src/review-api/input-error.ts +9 -0
- package/src/review-api/instructions.ts +89 -0
- package/src/review-api/lens-alignment.ts +52 -0
- package/src/review-api/local-data.ts +1837 -0
- package/src/review-api/map-input.ts +154 -0
- package/src/review-api/mcp-client-agent.ts +32 -0
- package/src/review-api/mcp.ts +240 -0
- package/src/review-api/origin.ts +43 -0
- package/src/review-api/profile.ts +146 -0
- package/src/review-api/public-tools.ts +102 -0
- package/src/review-api/pull-request.ts +389 -0
- package/src/review-api/read-schemas.ts +85 -0
- package/src/review-api/recovery.ts +2 -0
- package/src/review-api/request-origin.ts +33 -0
- package/src/review-api/review-progress.ts +382 -0
- package/src/review-api/status-tool.ts +10 -0
- package/src/review-api/store-schema.ts +22 -0
- package/src/review-api/store.ts +1647 -0
- package/src/review-api/tool-failure.ts +64 -0
- package/src/review-api/trace-schema.ts +13 -0
- package/src/review-api/traces.ts +119 -0
- package/src/review-api/unsupported-files.integration.ts +91 -0
- package/src/review-api/workspaces.ts +692 -0
- package/src/review-api/worktree-source.ts +170 -0
- package/src/review-api/worktree-structural.integration.ts +320 -0
- package/src/review-app-launcher.ts +432 -0
- package/src/review-app-picker.ts +168 -0
- package/src/review-app.ts +134 -0
- package/src/review-bundled-tools.ts +249 -0
- package/src/review-checkout-paths.ts +37 -0
- package/src/review-diff-files.ts +89 -0
- package/src/review-head-checkout.ts +300 -0
- package/src/review-home-paths.ts +73 -0
- package/src/review-info.ts +59 -0
- package/src/review-instances.ts +113 -0
- package/src/review-logger.ts +183 -0
- package/src/review-preferences.ts +88 -0
- package/src/review-prepare.ts +291 -0
- package/src/review-stack.ts +112 -0
- package/src/review-telemetry.ts +1066 -0
- package/src/runtime.ts +74 -0
- package/src/server/account-alias.ts +30 -0
- package/src/server/bounded-stream.ts +34 -0
- package/src/server/bug-report.ts +301 -0
- package/src/server/client-error-budget.ts +45 -0
- package/src/server/crash-report.ts +273 -0
- package/src/server/desktop-host-shutdown.ts +64 -0
- package/src/server/desktop-host.ts +190 -0
- package/src/server/desktop-server.ts +703 -0
- package/src/server/diffr-config.ts +509 -0
- package/src/server/diffr-languages.ts +97 -0
- package/src/server/global-verb-relay.ts +190 -0
- package/src/server/headless-host.ts +149 -0
- package/src/server/hono-http.ts +162 -0
- package/src/server/http-json.ts +24 -0
- package/src/server/json-review-reporting.ts +174 -0
- package/src/server/process-error-telemetry.ts +174 -0
- package/src/server/review-api-parsers.ts +90 -0
- package/src/server/review-lifecycle-telemetry.ts +73 -0
- package/src/server/review-open-watchdog.ts +46 -0
- package/src/server/review-server-core.ts +290 -0
- package/src/server/structural-comparisons.ts +153 -0
- package/src/server/structural-diff.ts +186 -0
- package/src/server/tutorial-service.ts +241 -0
- package/src/server/ui-telemetry.ts +198 -0
- package/src/server-discovery.ts +95 -0
- package/src/session-markers.ts +132 -0
- package/src/sharing/auth.ts +41 -0
- package/src/sharing/cli.ts +83 -0
- package/src/sharing/client.ts +334 -0
- package/src/sharing/export.ts +195 -0
- package/src/sharing/host.ts +316 -0
- package/src/sharing/import.ts +787 -0
- package/src/sharing/index.ts +16 -0
- package/src/sharing/repository.ts +145 -0
- package/src/sharing/routes.ts +34 -0
- package/src/slug.ts +23 -0
- package/src/software-map-diff-counts.ts +517 -0
- package/src/software-map-model.ts +1147 -0
- package/src/software-map-topology-diff.ts +260 -0
- package/src/source.ts +92 -0
- package/src/startup-trace.ts +232 -0
- package/src/stored-document-migration.ts +151 -0
- package/src/telemetry-clean-text.ts +257 -0
- package/src/telemetry-config.ts +298 -0
- package/src/telemetry-debug-sink.ts +38 -0
- package/src/telemetry.ts +13 -0
- package/src/trace-cli.ts +156 -0
- package/src/trace-storage-cli.ts +509 -0
- package/src/tutorial-conversation.ts +18 -0
- package/src/ui-telemetry-events.ts +765 -0
- package/src/unified-diff.ts +71 -0
- package/src/viewed-coverage.ts +259 -0
- package/src/windows-cli.ts +125 -0
- package/tutorial/document.json +275 -0
- package/tutorial/runtime-manifest.json +12 -0
- package/tutorial/sample-service/package.json +9 -0
- package/tutorial/sample-service/src/api/checkout-api.ts +17 -0
- package/tutorial/sample-service/src/app.ts +26 -0
- package/tutorial/sample-service/src/database/schema.ts +10 -0
- package/tutorial/sample-service/src/fulfillment/fulfillment-queue.ts +15 -0
- package/tutorial/sample-service/src/fulfillment/fulfillment-worker.ts +24 -0
- package/tutorial/sample-service/src/inventory/inventory-service.ts +11 -0
- package/tutorial/sample-service/src/orders/order-service.ts +34 -0
- package/tutorial/sample-service/src/orders/order.ts +21 -0
- package/tutorial/sample-service/src/orders/orders-repository.ts +24 -0
- package/tutorial/sample-service/src/payments/payment-gateway.ts +12 -0
- package/tutorial/sample-service/src/shipping/shipping-gateway.ts +15 -0
- package/tutorial/sample-service/tsconfig.json +12 -0
- package/tutorial/software-map.json +139 -0
- package/tutorial/trace.json +20 -0
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
SoftwareDataStoreFieldSchema,
|
|
3
|
+
SoftwareModelInput,
|
|
4
|
+
} from "@review/software-map-model.js";
|
|
5
|
+
import { z } from "zod";
|
|
6
|
+
|
|
7
|
+
// Decode the existing map authoring format at the HTTP boundary. The existing
|
|
8
|
+
// defineSoftwareMap normalizer remains responsible for relationships/coverage.
|
|
9
|
+
const label = z.string().trim().min(1);
|
|
10
|
+
|
|
11
|
+
const nonEmptyStringSchema = z
|
|
12
|
+
.string()
|
|
13
|
+
.refine((value) => value.trim().length > 0, "Must not be empty");
|
|
14
|
+
|
|
15
|
+
const optionalNonEmptyStringSchema = nonEmptyStringSchema.optional();
|
|
16
|
+
|
|
17
|
+
const softwareDataStoreForeignKeyRefSchema = z.union([
|
|
18
|
+
nonEmptyStringSchema,
|
|
19
|
+
z.strictObject({
|
|
20
|
+
store: optionalNonEmptyStringSchema,
|
|
21
|
+
table: nonEmptyStringSchema,
|
|
22
|
+
field: nonEmptyStringSchema,
|
|
23
|
+
label: optionalNonEmptyStringSchema,
|
|
24
|
+
cardinality: z.enum(["one-to-one", "many-to-one"]).optional(),
|
|
25
|
+
onDelete: optionalNonEmptyStringSchema,
|
|
26
|
+
onUpdate: optionalNonEmptyStringSchema,
|
|
27
|
+
}),
|
|
28
|
+
]);
|
|
29
|
+
|
|
30
|
+
const softwareDataStoreFieldSchema: z.ZodType<SoftwareDataStoreFieldSchema> =
|
|
31
|
+
z.lazy(() =>
|
|
32
|
+
z.record(
|
|
33
|
+
nonEmptyStringSchema,
|
|
34
|
+
z.union([
|
|
35
|
+
z.strictObject({
|
|
36
|
+
type: nonEmptyStringSchema,
|
|
37
|
+
example: z.unknown().optional(),
|
|
38
|
+
pk: z.boolean().optional(),
|
|
39
|
+
fk: softwareDataStoreForeignKeyRefSchema.optional(),
|
|
40
|
+
schema: softwareDataStoreFieldSchema.optional(),
|
|
41
|
+
}),
|
|
42
|
+
softwareDataStoreFieldSchema,
|
|
43
|
+
]),
|
|
44
|
+
),
|
|
45
|
+
);
|
|
46
|
+
|
|
47
|
+
const softwareDataStoreCollectionInputSchema = z.strictObject({
|
|
48
|
+
label: optionalNonEmptyStringSchema,
|
|
49
|
+
key: optionalNonEmptyStringSchema,
|
|
50
|
+
schema: softwareDataStoreFieldSchema,
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
const endpoint = label.describe(
|
|
54
|
+
'Existing element or data-store schema path. Resolution order: relative to the containing element, relative to its parent, then full path. "." means the containing element. At model root use full paths. Cross-level endpoints are allowed.',
|
|
55
|
+
);
|
|
56
|
+
|
|
57
|
+
const range = z.strictObject({
|
|
58
|
+
fromLine: z.number().int().positive(),
|
|
59
|
+
toLine: z.number().int().positive(),
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
const relationship = z.union([
|
|
63
|
+
z.strictObject({
|
|
64
|
+
kind: z.literal("call"),
|
|
65
|
+
from: endpoint,
|
|
66
|
+
to: endpoint,
|
|
67
|
+
label: z.string().optional(),
|
|
68
|
+
description: z.string().optional(),
|
|
69
|
+
nthCallSite: z.number().int().nonnegative().optional(),
|
|
70
|
+
}),
|
|
71
|
+
z.strictObject({
|
|
72
|
+
kind: z.literal("semantic"),
|
|
73
|
+
from: endpoint,
|
|
74
|
+
to: endpoint,
|
|
75
|
+
label: z.string().optional(),
|
|
76
|
+
description: z.string().optional(),
|
|
77
|
+
semanticKind: z.string().optional(),
|
|
78
|
+
sourceRanges: z.array(range).optional(),
|
|
79
|
+
}),
|
|
80
|
+
]);
|
|
81
|
+
|
|
82
|
+
const base = z.strictObject({
|
|
83
|
+
id: label
|
|
84
|
+
.optional()
|
|
85
|
+
.describe(
|
|
86
|
+
"Local ID, unique among siblings; defaults to the object key (or array index). Give array entries explicit IDs for stable paths. Nesting builds the full path: api under app becomes app.api. Do not repeat the parent path in the ID.",
|
|
87
|
+
),
|
|
88
|
+
label: label.optional(),
|
|
89
|
+
description: z.string().optional(),
|
|
90
|
+
changeStatus: z
|
|
91
|
+
.enum(["added", "removed", "modified", "unchanged"])
|
|
92
|
+
.optional(),
|
|
93
|
+
relationships: z
|
|
94
|
+
.array(relationship)
|
|
95
|
+
.optional()
|
|
96
|
+
.describe(
|
|
97
|
+
"Relationships scoped to this element. Endpoints may use relative names or full paths; they need not be direct children.",
|
|
98
|
+
),
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
const coverage = z.strictObject({
|
|
102
|
+
files: z
|
|
103
|
+
.array(
|
|
104
|
+
z.union([
|
|
105
|
+
label,
|
|
106
|
+
z.strictObject({ path: label, ranges: z.array(range).optional() }),
|
|
107
|
+
]),
|
|
108
|
+
)
|
|
109
|
+
.optional(),
|
|
110
|
+
globs: z.array(label).optional(),
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
const collection = <T extends z.ZodType>(schema: T) =>
|
|
114
|
+
z.union([z.record(label, schema), z.array(schema)]);
|
|
115
|
+
|
|
116
|
+
const code = base.extend({
|
|
117
|
+
sourceRanges: z.array(range.extend({ file: label })).optional(),
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
const component = base.extend({
|
|
121
|
+
coverage: coverage.optional(),
|
|
122
|
+
codeElements: collection(code).optional(),
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
const container = base.extend({
|
|
126
|
+
coverage: coverage.optional(),
|
|
127
|
+
components: collection(component).optional(),
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
const store = container.extend({
|
|
131
|
+
kind: z
|
|
132
|
+
.enum(["database", "objectStore", "bucket", "artifactStore", "fileStore"])
|
|
133
|
+
.optional(),
|
|
134
|
+
tables: z.record(label, softwareDataStoreCollectionInputSchema).optional(),
|
|
135
|
+
documents: z.record(label, softwareDataStoreCollectionInputSchema).optional(),
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
const system = base.extend({
|
|
139
|
+
coverage: coverage.optional(),
|
|
140
|
+
external: z.boolean().optional(),
|
|
141
|
+
containers: collection(container).optional(),
|
|
142
|
+
dataStores: collection(store).optional(),
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
export const mapInputSchema: z.ZodType<SoftwareModelInput> = z.strictObject({
|
|
146
|
+
people: collection(base).optional(),
|
|
147
|
+
systems: collection(system).optional(),
|
|
148
|
+
relationships: z
|
|
149
|
+
.array(relationship)
|
|
150
|
+
.optional()
|
|
151
|
+
.describe(
|
|
152
|
+
"Model-level relationships, including people and systems. Use full endpoint paths; external systems are systems with external: true.",
|
|
153
|
+
),
|
|
154
|
+
});
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import type { ReviewSessionAgent } from "@review/ui-telemetry-events.js";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* The agent behind an MCP client, from the name it sends in `initialize`.
|
|
5
|
+
* Names seen on 2026-10-02: Claude Code `claude-code`, Codex
|
|
6
|
+
* `codex-mcp-client`, the Cursor CLI `Cursor`, OpenCode `opencode`, Pi `pi`
|
|
7
|
+
* and Oh My Pi `omp-coding-agent`. Undefined for
|
|
8
|
+
* a client this does not recognize, so the caller can fall back to the
|
|
9
|
+
* session environment. Codex strips its `CODEX_*` variables from the MCP
|
|
10
|
+
* server's environment, so for Codex the handshake is the only signal.
|
|
11
|
+
*/
|
|
12
|
+
export function mcpClientAgent(
|
|
13
|
+
name: string | undefined,
|
|
14
|
+
): ReviewSessionAgent | undefined {
|
|
15
|
+
const normalized = name?.trim().toLowerCase();
|
|
16
|
+
|
|
17
|
+
if (!normalized) return undefined;
|
|
18
|
+
|
|
19
|
+
if (normalized.startsWith("codex")) return "codex";
|
|
20
|
+
|
|
21
|
+
if (normalized.startsWith("claude")) return "claude";
|
|
22
|
+
|
|
23
|
+
if (normalized.startsWith("cursor")) return "cursor";
|
|
24
|
+
|
|
25
|
+
if (normalized.startsWith("opencode")) return "opencode";
|
|
26
|
+
|
|
27
|
+
if (/^omp(?:$|[-_ ])/.test(normalized)) return "omp";
|
|
28
|
+
|
|
29
|
+
if (/^pi(?:$|[-_ ])/.test(normalized)) return "pi";
|
|
30
|
+
|
|
31
|
+
return undefined;
|
|
32
|
+
}
|
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
import type { Readable, Writable } from "node:stream";
|
|
2
|
+
|
|
3
|
+
import type { JsonValue } from "@dev.fast/json";
|
|
4
|
+
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
5
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
6
|
+
import {
|
|
7
|
+
CallToolRequestSchema,
|
|
8
|
+
ListToolsRequestSchema,
|
|
9
|
+
} from "@modelcontextprotocol/sdk/types.js";
|
|
10
|
+
import type { ReviewToolCall } from "@review/review-telemetry.js";
|
|
11
|
+
import type { ReviewSessionAgent } from "@review/ui-telemetry-events.js";
|
|
12
|
+
|
|
13
|
+
import {
|
|
14
|
+
type AuthoringTool,
|
|
15
|
+
type ConnectedReview,
|
|
16
|
+
toolResultText,
|
|
17
|
+
} from "./agent-client.js";
|
|
18
|
+
import { authoringTools } from "./authoring-tools.js";
|
|
19
|
+
import type { ReviewApiClient } from "./client.js";
|
|
20
|
+
import { ReviewApiError } from "./client.js";
|
|
21
|
+
import { ReviewInputError } from "./input-error.js";
|
|
22
|
+
import { mcpClientAgent } from "./mcp-client-agent.js";
|
|
23
|
+
import { callPublicTool, publicTool } from "./public-tools.js";
|
|
24
|
+
import { RECOVERY } from "./recovery.js";
|
|
25
|
+
import { REVIEW_STATUS_TOOL } from "./status-tool.js";
|
|
26
|
+
import { type ToolFailure, toolFailure } from "./tool-failure.js";
|
|
27
|
+
|
|
28
|
+
// Some hosts ignore tools/list_changed, so the agent itself has to reload.
|
|
29
|
+
const RELOAD_TOOLS =
|
|
30
|
+
"Whiteboard is running now, but this session listed Whiteboard's tools before it started, so you may see only session_get_instructions. Before authoring, reload the `whiteboard` MCP server's tools (reconnect it in your agent, or start a new agent session). If you can already see tools such as session_create, carry on.";
|
|
31
|
+
|
|
32
|
+
function mcpAuthoringGuidance(context: {
|
|
33
|
+
scratchpadAvailable: boolean;
|
|
34
|
+
traceEnabled: boolean;
|
|
35
|
+
}): string {
|
|
36
|
+
return [
|
|
37
|
+
"Whiteboard explains code in documents the user reads in Whiteboard Desktop. Call session_get_instructions before creating or editing a Whiteboard and follow it. Read session_capabilities before authoring; session_create opens the new review in Desktop when it is available, so call session_open only for an existing review, and generate software maps only when softwareMapEnabled is true. When the user asks for a Whiteboard or to use Whiteboard (for example to review a branch, a change or a pull request, or to explain a system in Whiteboard), author a Whiteboard with the default topic.",
|
|
38
|
+
...(context.scratchpadAvailable
|
|
39
|
+
? [
|
|
40
|
+
'When the user asks in conversation to be shown how code works or wants a diagram, without asking for a Whiteboard, call session_capabilities; if it reports scratchpadEnabled and desktopAvailable, draw on the Whiteboard scratchpad rather than answering only in chat, starting with session_get_instructions({topic:"scratchpad"}).',
|
|
41
|
+
]
|
|
42
|
+
: []),
|
|
43
|
+
...(context.traceEnabled
|
|
44
|
+
? [
|
|
45
|
+
'For why code exists, what an agent was thinking, or whether an agent solved something before, call session_get_instructions({topic:"trace-archaeology"}).',
|
|
46
|
+
]
|
|
47
|
+
: []),
|
|
48
|
+
"Never read or write Whiteboard files or SQL.",
|
|
49
|
+
].join(" ");
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export async function serveReviewMcp(
|
|
53
|
+
/** Connects to `key` once one is latched, else selects one, as `agentKind`. */
|
|
54
|
+
connect: (
|
|
55
|
+
key?: string,
|
|
56
|
+
agentKind?: ReviewSessionAgent,
|
|
57
|
+
) => Promise<ConnectedReview>,
|
|
58
|
+
stdin: Readable,
|
|
59
|
+
stdout: Writable,
|
|
60
|
+
stderr: Writable = process.stderr,
|
|
61
|
+
traceEnabled = false,
|
|
62
|
+
/** What whiteboard_status reports when the Desktop cannot be reached, and why. */
|
|
63
|
+
offlineStatus?: (problem: string) => Promise<JsonValue>,
|
|
64
|
+
onToolCall?: (call: ReviewToolCall) => Promise<void> | void,
|
|
65
|
+
/** The agent from the session environment, when the handshake names none. */
|
|
66
|
+
environmentAgent?: ReviewSessionAgent,
|
|
67
|
+
) {
|
|
68
|
+
const instructionsTool = {
|
|
69
|
+
...authoringTools(false, traceEnabled).find(
|
|
70
|
+
(tool) => tool.name === "review_get_instructions",
|
|
71
|
+
)!,
|
|
72
|
+
name: "session_get_instructions",
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
const server = new Server(
|
|
76
|
+
{ name: "whiteboard", version: "1.0.0" },
|
|
77
|
+
{
|
|
78
|
+
capabilities: { tools: { listChanged: true } },
|
|
79
|
+
},
|
|
80
|
+
);
|
|
81
|
+
|
|
82
|
+
// Hosts list tools once, right after initialize, often before Desktop is up.
|
|
83
|
+
// Answer from the last catalog (or none) instead of failing, and announce a
|
|
84
|
+
// changed list once the host can be reached.
|
|
85
|
+
let catalog: AuthoringTool[] = [];
|
|
86
|
+
let announceCatalog = false;
|
|
87
|
+
let listedWhileDown = false;
|
|
88
|
+
// One session follows one instance key, through that Desktop's restarts;
|
|
89
|
+
// it never hops to another key once others start.
|
|
90
|
+
let latched: string | undefined;
|
|
91
|
+
|
|
92
|
+
const agentKind = () =>
|
|
93
|
+
mcpClientAgent(server.getClientVersion()?.name) ?? environmentAgent;
|
|
94
|
+
|
|
95
|
+
const load = async (signal?: AbortSignal) => {
|
|
96
|
+
const { client, instance } = await connect(latched, agentKind());
|
|
97
|
+
latched ??= instance?.key;
|
|
98
|
+
|
|
99
|
+
catalog = (await client.read<AuthoringTool[]>("/authoring", signal)).map(
|
|
100
|
+
publicTool,
|
|
101
|
+
);
|
|
102
|
+
|
|
103
|
+
if (announceCatalog) {
|
|
104
|
+
announceCatalog = false;
|
|
105
|
+
void server.sendToolListChanged().catch(() => {});
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
return { client, tools: catalog };
|
|
109
|
+
};
|
|
110
|
+
|
|
111
|
+
server.setRequestHandler(ListToolsRequestSchema, async (_request, extra) => {
|
|
112
|
+
let tools = catalog;
|
|
113
|
+
|
|
114
|
+
try {
|
|
115
|
+
({ tools } = await load(extra.signal));
|
|
116
|
+
listedWhileDown = false;
|
|
117
|
+
} catch (error) {
|
|
118
|
+
announceCatalog = true;
|
|
119
|
+
listedWhileDown = true;
|
|
120
|
+
stderr.write(
|
|
121
|
+
`whiteboard mcp: ${error instanceof Error ? error.message : String(error)}\n`,
|
|
122
|
+
);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
const always = [instructionsTool, REVIEW_STATUS_TOOL].map(
|
|
126
|
+
(fallback) =>
|
|
127
|
+
tools.find((tool) => tool.name === fallback.name) ?? fallback,
|
|
128
|
+
);
|
|
129
|
+
|
|
130
|
+
return {
|
|
131
|
+
tools: [...always, ...tools.filter((tool) => !always.includes(tool))].map(
|
|
132
|
+
({ name, description, inputSchema }) => ({
|
|
133
|
+
name,
|
|
134
|
+
// Some clients prepend server instructions to every tool. Keep shared
|
|
135
|
+
// guidance on the discovery tool instead, including before Desktop starts.
|
|
136
|
+
description:
|
|
137
|
+
name === instructionsTool.name
|
|
138
|
+
? `${mcpAuthoringGuidance({ scratchpadAvailable: true, traceEnabled })}\n\n${description}`
|
|
139
|
+
: description,
|
|
140
|
+
inputSchema,
|
|
141
|
+
}),
|
|
142
|
+
),
|
|
143
|
+
};
|
|
144
|
+
});
|
|
145
|
+
server.setRequestHandler(CallToolRequestSchema, async (request, extra) => {
|
|
146
|
+
const startedAt = Date.now();
|
|
147
|
+
// The requested name is agent input; only a catalog name is reported.
|
|
148
|
+
let tool: AuthoringTool | undefined;
|
|
149
|
+
|
|
150
|
+
let phase: "connect" | "call" = "connect";
|
|
151
|
+
|
|
152
|
+
const report = (failure?: ToolFailure) =>
|
|
153
|
+
void onToolCall?.({
|
|
154
|
+
tool: tool?.name ?? "other",
|
|
155
|
+
via: "mcp",
|
|
156
|
+
ok: !failure,
|
|
157
|
+
durationMs: Date.now() - startedAt,
|
|
158
|
+
agentKind: agentKind(),
|
|
159
|
+
...failure,
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
try {
|
|
163
|
+
let client: ReviewApiClient;
|
|
164
|
+
let tools: AuthoringTool[];
|
|
165
|
+
|
|
166
|
+
try {
|
|
167
|
+
({ client, tools } = await load(extra.signal));
|
|
168
|
+
} catch (error) {
|
|
169
|
+
announceCatalog = true;
|
|
170
|
+
|
|
171
|
+
if (
|
|
172
|
+
request.params.name === instructionsTool.name &&
|
|
173
|
+
!(error instanceof ReviewApiError)
|
|
174
|
+
)
|
|
175
|
+
return { content: [{ type: "text" as const, text: RECOVERY }] };
|
|
176
|
+
|
|
177
|
+
if (request.params.name === REVIEW_STATUS_TOOL.name && offlineStatus)
|
|
178
|
+
return {
|
|
179
|
+
content: [
|
|
180
|
+
{
|
|
181
|
+
type: "text" as const,
|
|
182
|
+
text: JSON.stringify(
|
|
183
|
+
await offlineStatus(
|
|
184
|
+
error instanceof Error ? error.message : String(error),
|
|
185
|
+
),
|
|
186
|
+
),
|
|
187
|
+
},
|
|
188
|
+
],
|
|
189
|
+
};
|
|
190
|
+
|
|
191
|
+
throw error;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
phase = "call";
|
|
195
|
+
tool = tools.find((tool) => tool.name === request.params.name);
|
|
196
|
+
|
|
197
|
+
if (!tool)
|
|
198
|
+
throw new ReviewInputError(
|
|
199
|
+
`Unknown Whiteboard tool: ${request.params.name}`,
|
|
200
|
+
);
|
|
201
|
+
|
|
202
|
+
const result = await callPublicTool(
|
|
203
|
+
client,
|
|
204
|
+
tool,
|
|
205
|
+
request.params.arguments ?? {},
|
|
206
|
+
extra.signal,
|
|
207
|
+
);
|
|
208
|
+
|
|
209
|
+
const text = toolResultText(tool, result);
|
|
210
|
+
report();
|
|
211
|
+
|
|
212
|
+
return {
|
|
213
|
+
content: [
|
|
214
|
+
{
|
|
215
|
+
type: "text",
|
|
216
|
+
text:
|
|
217
|
+
listedWhileDown && tool.name === instructionsTool.name
|
|
218
|
+
? `${RELOAD_TOOLS}\n\n${text}`
|
|
219
|
+
: text,
|
|
220
|
+
},
|
|
221
|
+
],
|
|
222
|
+
};
|
|
223
|
+
} catch (error) {
|
|
224
|
+
report(toolFailure(error, phase));
|
|
225
|
+
|
|
226
|
+
return {
|
|
227
|
+
isError: true,
|
|
228
|
+
content: [
|
|
229
|
+
{
|
|
230
|
+
type: "text",
|
|
231
|
+
text: error instanceof Error ? error.message : String(error),
|
|
232
|
+
},
|
|
233
|
+
],
|
|
234
|
+
};
|
|
235
|
+
}
|
|
236
|
+
});
|
|
237
|
+
await server.connect(new StdioServerTransport(stdin, stdout));
|
|
238
|
+
|
|
239
|
+
return server;
|
|
240
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
|
|
3
|
+
import type { Snapshot } from "./store.js";
|
|
4
|
+
|
|
5
|
+
export const pullRequestUrl = z
|
|
6
|
+
.string()
|
|
7
|
+
.regex(
|
|
8
|
+
/^https:\/\/[a-z0-9.-]+\/[A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+\/pull\/[1-9]\d*$/,
|
|
9
|
+
"Use a canonical GitHub PR URL: https://github.com/owner/repository/pull/123, or the same path on a GitHub Enterprise host.",
|
|
10
|
+
)
|
|
11
|
+
.refine(
|
|
12
|
+
(url) => Number.isSafeInteger(Number(url.split("/").at(-1))),
|
|
13
|
+
"PR number is too large.",
|
|
14
|
+
);
|
|
15
|
+
|
|
16
|
+
/** One key per PR: GitHub owner and repository names are case-insensitive,
|
|
17
|
+
* and the URL pattern admits only ASCII, so lowercasing is canonical. */
|
|
18
|
+
export function pullRequestKey(url: string): string {
|
|
19
|
+
return url.toLowerCase();
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/** Omission preserves identity; null detaches it without changing import metadata. */
|
|
23
|
+
export function setPullRequest(
|
|
24
|
+
snapshot: Pick<Snapshot, "origin">,
|
|
25
|
+
url: string | null | undefined,
|
|
26
|
+
) {
|
|
27
|
+
if (url === undefined) return;
|
|
28
|
+
|
|
29
|
+
if (url === null) {
|
|
30
|
+
if (snapshot.origin) {
|
|
31
|
+
delete snapshot.origin.pullRequestUrl;
|
|
32
|
+
delete snapshot.origin.pullRequestNumber;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
snapshot.origin = {
|
|
39
|
+
...snapshot.origin,
|
|
40
|
+
pullRequestUrl: url,
|
|
41
|
+
pullRequestNumber: Number(url.split("/").at(-1)),
|
|
42
|
+
};
|
|
43
|
+
}
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
import { access, mkdir } from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { DatabaseSync } from "node:sqlite";
|
|
4
|
+
|
|
5
|
+
import { withFileLock } from "@dev.fast/trace-core";
|
|
6
|
+
|
|
7
|
+
import { openLocalReviewStore } from "./local-data.js";
|
|
8
|
+
import { initializeReviewStoreSchema } from "./store-schema.js";
|
|
9
|
+
|
|
10
|
+
const lockOptions = {
|
|
11
|
+
retryMs: 50,
|
|
12
|
+
timeoutMs: 10_000,
|
|
13
|
+
staleMs: Infinity,
|
|
14
|
+
unownedGraceMs: 1_000,
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
export async function openReviewProfile(
|
|
18
|
+
home: string,
|
|
19
|
+
options: { manageWorkspaces: boolean },
|
|
20
|
+
) {
|
|
21
|
+
await mkdir(home, { recursive: true, mode: 0o700 });
|
|
22
|
+
|
|
23
|
+
const outcome = await withFileLock(
|
|
24
|
+
path.join(home, ".review-profile-startup"),
|
|
25
|
+
lockOptions,
|
|
26
|
+
async () => {
|
|
27
|
+
for (const source of [
|
|
28
|
+
path.join(home, "review-server", "reviews.db"),
|
|
29
|
+
path.join(home, "reviews.db"),
|
|
30
|
+
])
|
|
31
|
+
await importHeadlessStore(home, source);
|
|
32
|
+
|
|
33
|
+
return openLocalReviewStore(path.join(home, "review-api.db"), options);
|
|
34
|
+
},
|
|
35
|
+
);
|
|
36
|
+
|
|
37
|
+
if (!outcome.acquired)
|
|
38
|
+
throw new Error(
|
|
39
|
+
"Another Review process is initializing this profile. Retry shortly.",
|
|
40
|
+
);
|
|
41
|
+
|
|
42
|
+
return outcome.result;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** Preserve preview headless IDs, history and resources; leave originals as backups. */
|
|
46
|
+
async function importHeadlessStore(home: string, source: string) {
|
|
47
|
+
try {
|
|
48
|
+
await access(source);
|
|
49
|
+
} catch (error) {
|
|
50
|
+
if (error instanceof Error && "code" in error && error.code === "ENOENT")
|
|
51
|
+
return;
|
|
52
|
+
throw error;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const database = new DatabaseSync(path.join(home, "review-api.db"), {
|
|
56
|
+
timeout: 5000,
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
try {
|
|
60
|
+
database.exec(
|
|
61
|
+
"PRAGMA foreign_keys=ON; CREATE TABLE IF NOT EXISTS headless_imports(path TEXT PRIMARY KEY)",
|
|
62
|
+
);
|
|
63
|
+
|
|
64
|
+
if (
|
|
65
|
+
database
|
|
66
|
+
.prepare("SELECT 1 FROM headless_imports WHERE path=?")
|
|
67
|
+
.get(source)
|
|
68
|
+
)
|
|
69
|
+
return;
|
|
70
|
+
|
|
71
|
+
initializeReviewStoreSchema(database);
|
|
72
|
+
|
|
73
|
+
const outcome = await withFileLock(
|
|
74
|
+
path.join(path.dirname(source), "server.lock"),
|
|
75
|
+
{ ...lockOptions, timeoutMs: 0 },
|
|
76
|
+
async () => {
|
|
77
|
+
database.prepare("ATTACH DATABASE ? AS headless").run(source);
|
|
78
|
+
database.exec("BEGIN IMMEDIATE");
|
|
79
|
+
|
|
80
|
+
try {
|
|
81
|
+
if (
|
|
82
|
+
database
|
|
83
|
+
.prepare(
|
|
84
|
+
"SELECT 1 FROM headless.reviews s JOIN reviews t ON s.id=t.id LIMIT 1",
|
|
85
|
+
)
|
|
86
|
+
.get()
|
|
87
|
+
)
|
|
88
|
+
throw new Error(
|
|
89
|
+
`Cannot merge ${source}: the shared store already contains one of its review IDs. Both databases are unchanged.`,
|
|
90
|
+
);
|
|
91
|
+
database.exec(`
|
|
92
|
+
INSERT INTO repositories SELECT s.* FROM headless.repositories s WHERE NOT EXISTS (SELECT 1 FROM repositories t WHERE t.path=s.path);
|
|
93
|
+
CREATE TEMP TABLE repository_ids AS SELECT s.id old_id,t.id new_id FROM headless.repositories s JOIN repositories t ON s.path=t.path;
|
|
94
|
+
`);
|
|
95
|
+
|
|
96
|
+
if (
|
|
97
|
+
database
|
|
98
|
+
.prepare(
|
|
99
|
+
`SELECT 1 FROM headless.versions s LEFT JOIN repository_ids r ON json_extract(s.snapshot,'$.pins.repositoryId')=r.old_id WHERE r.old_id IS NULL LIMIT 1`,
|
|
100
|
+
)
|
|
101
|
+
.get()
|
|
102
|
+
)
|
|
103
|
+
throw new Error(
|
|
104
|
+
`Cannot merge ${source}: a version refers to an unregistered repository. Both databases are unchanged.`,
|
|
105
|
+
);
|
|
106
|
+
|
|
107
|
+
if (
|
|
108
|
+
database
|
|
109
|
+
.prepare(`SELECT 1 FROM headless.resources s JOIN resources t ON s.id=t.id JOIN repository_ids r ON s.repository_id=r.old_id
|
|
110
|
+
WHERE t.repository_id!=r.new_id OR t.kind!=s.kind OR t.mime_type!=s.mime_type OR t.data IS NOT s.data LIMIT 1`)
|
|
111
|
+
.get()
|
|
112
|
+
)
|
|
113
|
+
throw new Error(
|
|
114
|
+
`Cannot merge ${source}: a resource ID has different content. Both databases are unchanged.`,
|
|
115
|
+
);
|
|
116
|
+
|
|
117
|
+
database.exec(`
|
|
118
|
+
INSERT OR IGNORE INTO resources SELECT s.id,r.new_id,s.kind,s.mime_type,s.data FROM headless.resources s JOIN repository_ids r ON s.repository_id=r.old_id;
|
|
119
|
+
INSERT INTO reviews SELECT * FROM headless.reviews;
|
|
120
|
+
INSERT INTO versions SELECT s.review_id,s.version,
|
|
121
|
+
CASE WHEN json_type(s.snapshot,'$.target')='object'
|
|
122
|
+
THEN json_set(s.snapshot,'$.pins.repositoryId',r.new_id,'$.target.repositoryId',r.new_id)
|
|
123
|
+
ELSE json_set(s.snapshot,'$.pins.repositoryId',r.new_id) END
|
|
124
|
+
FROM headless.versions s JOIN repository_ids r ON json_extract(s.snapshot,'$.pins.repositoryId')=r.old_id;
|
|
125
|
+
INSERT INTO review_attention SELECT * FROM headless.review_attention;
|
|
126
|
+
`);
|
|
127
|
+
|
|
128
|
+
database
|
|
129
|
+
.prepare("INSERT INTO headless_imports VALUES(?)")
|
|
130
|
+
.run(source);
|
|
131
|
+
database.exec("COMMIT");
|
|
132
|
+
} catch (error) {
|
|
133
|
+
database.exec("ROLLBACK");
|
|
134
|
+
throw error;
|
|
135
|
+
}
|
|
136
|
+
},
|
|
137
|
+
);
|
|
138
|
+
|
|
139
|
+
if (!outcome.acquired)
|
|
140
|
+
throw new Error(
|
|
141
|
+
`Stop the old headless server using ${path.dirname(source)}, then retry. Its reviews will be moved into the shared profile automatically.`,
|
|
142
|
+
);
|
|
143
|
+
} finally {
|
|
144
|
+
database.close();
|
|
145
|
+
}
|
|
146
|
+
}
|