@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,340 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
import type { DatabaseSync } from "node:sqlite";
|
|
3
|
+
|
|
4
|
+
import { z } from "zod";
|
|
5
|
+
|
|
6
|
+
import { ReviewInputError } from "./document.js";
|
|
7
|
+
|
|
8
|
+
const focusSchema = z.strictObject({
|
|
9
|
+
description: z.string().trim().min(1).max(160),
|
|
10
|
+
targetId: z.string().min(1).optional(),
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
/** Which page an agent last wrote on, so the app knows where its courier is. */
|
|
14
|
+
export type ActivitySurface = "document" | "lenses";
|
|
15
|
+
|
|
16
|
+
// One schema per action, so each agent tool states exactly what it needs.
|
|
17
|
+
export const activityBeginSchema = z.strictObject({
|
|
18
|
+
focus: focusSchema.nullable().optional(),
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
export const activityUpdateSchema = z.strictObject({
|
|
22
|
+
activityId: z.string().min(1),
|
|
23
|
+
focus: focusSchema.nullable().optional(),
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
export const activityEndSchema = z.strictObject({
|
|
27
|
+
activityId: z.string().min(1),
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
export const activitySchema = z.discriminatedUnion("action", [
|
|
31
|
+
activityBeginSchema.extend({ action: z.literal("begin") }),
|
|
32
|
+
activityUpdateSchema.extend({ action: z.literal("update") }),
|
|
33
|
+
activityEndSchema.extend({ action: z.literal("end") }),
|
|
34
|
+
]);
|
|
35
|
+
|
|
36
|
+
export type ActivityFocus = z.infer<typeof focusSchema>;
|
|
37
|
+
|
|
38
|
+
/** One agent working on a review. */
|
|
39
|
+
export interface ActivityPresence {
|
|
40
|
+
activityId: string;
|
|
41
|
+
/** The agent's color: the lowest slot free when it began, kept until it ends. */
|
|
42
|
+
slot: number;
|
|
43
|
+
focus?: ActivityFocus;
|
|
44
|
+
/** Where it last wrote; absent until its first attributed write. */
|
|
45
|
+
surface?: ActivitySurface;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export interface ActivitySnapshot {
|
|
49
|
+
/** Live presences. */
|
|
50
|
+
workingCount: number;
|
|
51
|
+
/** The latest expiry among live presences. */
|
|
52
|
+
expiresAt: number | null;
|
|
53
|
+
/** Each live presence, in the order they began. */
|
|
54
|
+
activities?: ActivityPresence[];
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// A presence lasts until end, or three minutes without an attributed write or
|
|
58
|
+
// an update, so a crashed agent stops showing within that long.
|
|
59
|
+
export const ACTIVITY_TTL_MS = 180_000;
|
|
60
|
+
|
|
61
|
+
export class ReviewActivity {
|
|
62
|
+
private readonly timers = new Map<string, ReturnType<typeof setTimeout>>();
|
|
63
|
+
private readonly listeners = new Set<(reviewId: string) => void>();
|
|
64
|
+
private readonly working = new Set<string>();
|
|
65
|
+
private readonly workingListeners = new Set<() => void>();
|
|
66
|
+
|
|
67
|
+
constructor(
|
|
68
|
+
private readonly db: DatabaseSync,
|
|
69
|
+
private readonly assertReview?: (reviewId: string) => void,
|
|
70
|
+
) {
|
|
71
|
+
// Presences replace the per-scope leases; both last minutes, so the old
|
|
72
|
+
// table is dropped rather than migrated.
|
|
73
|
+
db.exec(`CREATE TABLE IF NOT EXISTS authoring_presences(
|
|
74
|
+
activity_id TEXT PRIMARY KEY, review_id TEXT NOT NULL, slot INTEGER NOT NULL,
|
|
75
|
+
started_at INTEGER NOT NULL, expires_at INTEGER NOT NULL, focus TEXT, surface TEXT);
|
|
76
|
+
DROP TABLE IF EXISTS authoring_sessions;`);
|
|
77
|
+
|
|
78
|
+
for (const row of db
|
|
79
|
+
.prepare(
|
|
80
|
+
"SELECT DISTINCT review_id FROM authoring_presences WHERE expires_at>?",
|
|
81
|
+
)
|
|
82
|
+
.all(Date.now())) {
|
|
83
|
+
this.working.add(String(row.review_id));
|
|
84
|
+
this.scheduleExpiry(String(row.review_id));
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
subscribe(listener: (reviewId: string) => void) {
|
|
88
|
+
this.listeners.add(listener);
|
|
89
|
+
|
|
90
|
+
return () => {
|
|
91
|
+
this.listeners.delete(listener);
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
/** Fires only when a review starts or stops being authored, never on
|
|
95
|
+
* renewals or focus changes. */
|
|
96
|
+
subscribeWorking(listener: () => void) {
|
|
97
|
+
this.workingListeners.add(listener);
|
|
98
|
+
|
|
99
|
+
return () => {
|
|
100
|
+
this.workingListeners.delete(listener);
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
isWorking(reviewId: string) {
|
|
104
|
+
return this.working.has(reviewId);
|
|
105
|
+
}
|
|
106
|
+
private changed(reviewId: string) {
|
|
107
|
+
const working =
|
|
108
|
+
this.db
|
|
109
|
+
.prepare(
|
|
110
|
+
"SELECT 1 FROM authoring_presences WHERE review_id=? AND expires_at>? LIMIT 1",
|
|
111
|
+
)
|
|
112
|
+
.get(reviewId, Date.now()) !== undefined;
|
|
113
|
+
|
|
114
|
+
if (working !== this.working.has(reviewId)) {
|
|
115
|
+
if (working) this.working.add(reviewId);
|
|
116
|
+
else this.working.delete(reviewId);
|
|
117
|
+
|
|
118
|
+
for (const notify of this.workingListeners) notify();
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
for (const notify of this.listeners) notify(reviewId);
|
|
122
|
+
}
|
|
123
|
+
private live(reviewId: string) {
|
|
124
|
+
return this.db
|
|
125
|
+
.prepare(
|
|
126
|
+
"SELECT activity_id,slot,expires_at,focus,surface FROM authoring_presences WHERE review_id=? AND expires_at>? ORDER BY rowid",
|
|
127
|
+
)
|
|
128
|
+
.all(reviewId, Date.now());
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
read(reviewId: string): ActivitySnapshot {
|
|
132
|
+
const live = this.live(reviewId);
|
|
133
|
+
|
|
134
|
+
const snapshot: ActivitySnapshot = {
|
|
135
|
+
workingCount: live.length,
|
|
136
|
+
expiresAt: live.length
|
|
137
|
+
? Math.max(...live.map((row) => Number(row.expires_at)))
|
|
138
|
+
: null,
|
|
139
|
+
};
|
|
140
|
+
|
|
141
|
+
if (live.length)
|
|
142
|
+
snapshot.activities = live.map((row) => {
|
|
143
|
+
const presence: ActivityPresence = {
|
|
144
|
+
activityId: String(row.activity_id),
|
|
145
|
+
slot: Number(row.slot),
|
|
146
|
+
};
|
|
147
|
+
|
|
148
|
+
if (row.focus)
|
|
149
|
+
presence.focus = focusSchema.parse(JSON.parse(String(row.focus)));
|
|
150
|
+
|
|
151
|
+
if (row.surface === "document" || row.surface === "lenses")
|
|
152
|
+
presence.surface = row.surface;
|
|
153
|
+
|
|
154
|
+
return presence;
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
return snapshot;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/** Inside the caller's write transaction: the presence a write belongs to,
|
|
161
|
+
* renewed and moved to the write's surface. The named one if it is live;
|
|
162
|
+
* otherwise, with none named, the review's only presence. A write is never
|
|
163
|
+
* refused for this. Call `extended` once the transaction commits. */
|
|
164
|
+
attribute(
|
|
165
|
+
reviewId: string,
|
|
166
|
+
surface: ActivitySurface,
|
|
167
|
+
activityId?: string,
|
|
168
|
+
): string | undefined {
|
|
169
|
+
const live = this.live(reviewId);
|
|
170
|
+
|
|
171
|
+
const owner =
|
|
172
|
+
activityId === undefined
|
|
173
|
+
? live.length === 1
|
|
174
|
+
? String(live[0]!.activity_id)
|
|
175
|
+
: undefined
|
|
176
|
+
: live.some((row) => row.activity_id === activityId)
|
|
177
|
+
? activityId
|
|
178
|
+
: undefined;
|
|
179
|
+
|
|
180
|
+
if (owner === undefined) return undefined;
|
|
181
|
+
this.db
|
|
182
|
+
.prepare(
|
|
183
|
+
"UPDATE authoring_presences SET expires_at=?,surface=? WHERE activity_id=?",
|
|
184
|
+
)
|
|
185
|
+
.run(Date.now() + ACTIVITY_TTL_MS, surface, owner);
|
|
186
|
+
|
|
187
|
+
return owner;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/** Move the expiry timer after a committed `attribute`. */
|
|
191
|
+
extended(reviewId: string) {
|
|
192
|
+
this.scheduleExpiry(reviewId);
|
|
193
|
+
this.changed(reviewId);
|
|
194
|
+
}
|
|
195
|
+
// oxlint-disable-next-line anti-slop/no-unknown-parameters -- Activity boundary: activitySchema.parse below validates incoming JSON.
|
|
196
|
+
update(reviewId: string, value: unknown) {
|
|
197
|
+
const input = activitySchema.parse(value);
|
|
198
|
+
const now = Date.now();
|
|
199
|
+
|
|
200
|
+
let activityId: string;
|
|
201
|
+
|
|
202
|
+
this.db.exec("BEGIN IMMEDIATE");
|
|
203
|
+
|
|
204
|
+
try {
|
|
205
|
+
this.assertReview?.(reviewId);
|
|
206
|
+
|
|
207
|
+
if (input.action === "begin") {
|
|
208
|
+
activityId = randomUUID();
|
|
209
|
+
|
|
210
|
+
const taken = new Set(
|
|
211
|
+
this.live(reviewId).map((row) => Number(row.slot)),
|
|
212
|
+
);
|
|
213
|
+
|
|
214
|
+
let slot = 0;
|
|
215
|
+
|
|
216
|
+
while (taken.has(slot)) slot += 1;
|
|
217
|
+
this.db
|
|
218
|
+
.prepare(
|
|
219
|
+
"INSERT INTO authoring_presences(activity_id,review_id,slot,started_at,expires_at,focus) VALUES(?,?,?,?,?,?)",
|
|
220
|
+
)
|
|
221
|
+
.run(
|
|
222
|
+
activityId,
|
|
223
|
+
reviewId,
|
|
224
|
+
slot,
|
|
225
|
+
now,
|
|
226
|
+
now + ACTIVITY_TTL_MS,
|
|
227
|
+
input.focus ? JSON.stringify(input.focus) : null,
|
|
228
|
+
);
|
|
229
|
+
} else {
|
|
230
|
+
activityId = input.activityId;
|
|
231
|
+
|
|
232
|
+
if (input.action === "end") {
|
|
233
|
+
// Ending twice, or ending an expired presence, is harmless.
|
|
234
|
+
this.db
|
|
235
|
+
.prepare(
|
|
236
|
+
"DELETE FROM authoring_presences WHERE activity_id=? AND review_id=?",
|
|
237
|
+
)
|
|
238
|
+
.run(activityId, reviewId);
|
|
239
|
+
} else {
|
|
240
|
+
const renewed = this.db
|
|
241
|
+
.prepare(
|
|
242
|
+
`UPDATE authoring_presences SET expires_at=?${input.focus === undefined ? "" : ",focus=?"}
|
|
243
|
+
WHERE activity_id=? AND review_id=? AND expires_at>?`,
|
|
244
|
+
)
|
|
245
|
+
.run(
|
|
246
|
+
...[
|
|
247
|
+
now + ACTIVITY_TTL_MS,
|
|
248
|
+
...(input.focus === undefined
|
|
249
|
+
? []
|
|
250
|
+
: [
|
|
251
|
+
input.focus === null ? null : JSON.stringify(input.focus),
|
|
252
|
+
]),
|
|
253
|
+
activityId,
|
|
254
|
+
reviewId,
|
|
255
|
+
now,
|
|
256
|
+
],
|
|
257
|
+
);
|
|
258
|
+
|
|
259
|
+
if (renewed.changes === 0)
|
|
260
|
+
throw new ReviewInputError(
|
|
261
|
+
"This activity ended or expired. Begin a new one.",
|
|
262
|
+
409,
|
|
263
|
+
);
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
this.db.exec("COMMIT");
|
|
268
|
+
} catch (error) {
|
|
269
|
+
this.db.exec("ROLLBACK");
|
|
270
|
+
throw error;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
this.scheduleExpiry(reviewId);
|
|
274
|
+
|
|
275
|
+
this.changed(reviewId);
|
|
276
|
+
|
|
277
|
+
const result: ActivitySnapshot & { activityId?: string } =
|
|
278
|
+
this.read(reviewId);
|
|
279
|
+
|
|
280
|
+
if (input.action === "begin") result.activityId = activityId;
|
|
281
|
+
|
|
282
|
+
return result;
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
/** One timer per review, for its soonest-expiring presence. */
|
|
286
|
+
private scheduleExpiry(reviewId: string) {
|
|
287
|
+
clearTimeout(this.timers.get(reviewId));
|
|
288
|
+
this.timers.delete(reviewId);
|
|
289
|
+
|
|
290
|
+
const next = this.db
|
|
291
|
+
.prepare(
|
|
292
|
+
"SELECT MIN(expires_at) AS expires_at FROM authoring_presences WHERE review_id=? AND expires_at>?",
|
|
293
|
+
)
|
|
294
|
+
.get(reviewId, Date.now());
|
|
295
|
+
|
|
296
|
+
if (next?.expires_at === null || next?.expires_at === undefined) return;
|
|
297
|
+
|
|
298
|
+
const timer = setTimeout(
|
|
299
|
+
() => {
|
|
300
|
+
this.scheduleExpiry(reviewId);
|
|
301
|
+
this.changed(reviewId);
|
|
302
|
+
},
|
|
303
|
+
Math.max(1, Number(next.expires_at) - Date.now()),
|
|
304
|
+
);
|
|
305
|
+
|
|
306
|
+
timer.unref?.();
|
|
307
|
+
this.timers.set(reviewId, timer);
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
/** Called when another database connection commits session changes. */
|
|
311
|
+
refresh() {
|
|
312
|
+
const ids = new Set(this.timers.keys());
|
|
313
|
+
|
|
314
|
+
for (const row of this.db
|
|
315
|
+
.prepare(
|
|
316
|
+
"SELECT DISTINCT review_id FROM authoring_presences WHERE expires_at>?",
|
|
317
|
+
)
|
|
318
|
+
.all(Date.now()))
|
|
319
|
+
ids.add(String(row.review_id));
|
|
320
|
+
|
|
321
|
+
for (const id of ids) {
|
|
322
|
+
this.scheduleExpiry(id);
|
|
323
|
+
this.changed(id);
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
/** The store deletes the session atomically with its review before notifying. */
|
|
328
|
+
deleted(reviewId: string) {
|
|
329
|
+
clearTimeout(this.timers.get(reviewId));
|
|
330
|
+
this.timers.delete(reviewId);
|
|
331
|
+
this.changed(reviewId);
|
|
332
|
+
}
|
|
333
|
+
close() {
|
|
334
|
+
this.listeners.clear();
|
|
335
|
+
this.workingListeners.clear();
|
|
336
|
+
|
|
337
|
+
for (const timer of this.timers.values()) clearTimeout(timer);
|
|
338
|
+
this.timers.clear();
|
|
339
|
+
}
|
|
340
|
+
}
|
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
import type { Readable, Writable } from "node:stream";
|
|
2
|
+
|
|
3
|
+
import { traceMachineEnabled } from "@dev.fast/trace-core";
|
|
4
|
+
import {
|
|
5
|
+
REVIEW_INSTANCE_ENV,
|
|
6
|
+
selectReviewInstance,
|
|
7
|
+
} from "@review/desktop-discovery.js";
|
|
8
|
+
import { devReviewHome } from "@review/review-home-paths.js";
|
|
9
|
+
import {
|
|
10
|
+
type ReviewToolCall,
|
|
11
|
+
reviewSessionAgent,
|
|
12
|
+
} from "@review/review-telemetry.js";
|
|
13
|
+
|
|
14
|
+
import {
|
|
15
|
+
type AuthoringTool,
|
|
16
|
+
connectReviewInstance,
|
|
17
|
+
toolResultText,
|
|
18
|
+
} from "./agent-client.js";
|
|
19
|
+
import { type ReviewApiClient, ReviewApiError } from "./client.js";
|
|
20
|
+
import { callPublicTool, publicTool } from "./public-tools.js";
|
|
21
|
+
import { RECOVERY } from "./recovery.js";
|
|
22
|
+
import { REVIEW_AGENT_HEADER, REVIEW_VIA_HEADER } from "./request-origin.js";
|
|
23
|
+
import { toolFailure } from "./tool-failure.js";
|
|
24
|
+
|
|
25
|
+
interface AgentCliInput {
|
|
26
|
+
argv: string[];
|
|
27
|
+
env?: NodeJS.ProcessEnv;
|
|
28
|
+
stdin?: Readable;
|
|
29
|
+
stdout: Writable;
|
|
30
|
+
stderr: Writable;
|
|
31
|
+
/** Awaited on the api path: the process exits right after the call. */
|
|
32
|
+
onToolCall?: (call: ReviewToolCall) => Promise<void> | void;
|
|
33
|
+
/** Receives the failure so the parent CLI can classify its terminal event. */
|
|
34
|
+
onFailure?: (error: Error) => void;
|
|
35
|
+
/** Receives the release of each Desktop reached, for telemetry. */
|
|
36
|
+
onDesktop?: (appVersion: string | undefined) => void;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export const reviewAgentCliHelp =
|
|
40
|
+
"whiteboard api tools (one line per tool)\nwhiteboard api tools <tool-name> (its description and input schema)\nwhiteboard api <tool-name> '<json>'\nwhiteboard api <tool-name> - (read JSON from stdin)\nwhiteboard mcp (stdio MCP adapter; Whiteboard Desktop or whiteboard server start must be running)\nSelect headless state with DEV_REVIEW_SERVER_DIR or whiteboard --state-dir <path> api/mcp.\n";
|
|
41
|
+
|
|
42
|
+
export async function runReviewAgentCli(input: AgentCliInput): Promise<number> {
|
|
43
|
+
const env = input.env ?? process.env;
|
|
44
|
+
|
|
45
|
+
try {
|
|
46
|
+
const [mode, ...rest] = input.argv;
|
|
47
|
+
|
|
48
|
+
// --json requests raw data for session_get; other tools already return JSON.
|
|
49
|
+
const [name, json, ...extra] = rest.filter(
|
|
50
|
+
(argument) => argument !== "--json",
|
|
51
|
+
);
|
|
52
|
+
|
|
53
|
+
if (
|
|
54
|
+
rest.includes("--help") ||
|
|
55
|
+
rest.includes("-h") ||
|
|
56
|
+
(mode === "api" && !name)
|
|
57
|
+
) {
|
|
58
|
+
input.stdout.write(reviewAgentCliHelp);
|
|
59
|
+
|
|
60
|
+
return 0;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
if (extra.length || (mode === "mcp" && name))
|
|
64
|
+
throw new Error("Unexpected arguments. Use whiteboard api --help.");
|
|
65
|
+
|
|
66
|
+
const headers = {
|
|
67
|
+
[REVIEW_VIA_HEADER]: mode === "mcp" ? "mcp" : "api",
|
|
68
|
+
[REVIEW_AGENT_HEADER]: reviewSessionAgent(env),
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
if (mode === "mcp") {
|
|
72
|
+
const { serveReviewMcp } = await import("./mcp.js");
|
|
73
|
+
await serveReviewMcp(
|
|
74
|
+
async (key, agentKind) => {
|
|
75
|
+
const connected = await connectReviewInstance(
|
|
76
|
+
key ? { ...env, [REVIEW_INSTANCE_ENV]: key } : env,
|
|
77
|
+
agentKind
|
|
78
|
+
? { ...headers, [REVIEW_AGENT_HEADER]: agentKind }
|
|
79
|
+
: headers,
|
|
80
|
+
);
|
|
81
|
+
|
|
82
|
+
input.onDesktop?.(connected.instance?.appVersion);
|
|
83
|
+
|
|
84
|
+
return connected;
|
|
85
|
+
},
|
|
86
|
+
input.stdin ?? process.stdin,
|
|
87
|
+
input.stdout,
|
|
88
|
+
input.stderr,
|
|
89
|
+
await traceMachineEnabled({ env: input.env }),
|
|
90
|
+
env.DEV_REVIEW_SERVER_DIR?.trim()
|
|
91
|
+
? undefined
|
|
92
|
+
: async (problem) => {
|
|
93
|
+
const selection = await selectReviewInstance({ env });
|
|
94
|
+
|
|
95
|
+
return {
|
|
96
|
+
key: selection.key,
|
|
97
|
+
selectedBy: selection.source,
|
|
98
|
+
desktopAvailable: false,
|
|
99
|
+
running: selection.instances
|
|
100
|
+
.filter((instance) => instance.healthy)
|
|
101
|
+
.map((instance) => instance.key),
|
|
102
|
+
home: devReviewHome(env),
|
|
103
|
+
problem,
|
|
104
|
+
};
|
|
105
|
+
},
|
|
106
|
+
input.onToolCall,
|
|
107
|
+
reviewSessionAgent(env),
|
|
108
|
+
);
|
|
109
|
+
|
|
110
|
+
return 0;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
let client: ReviewApiClient;
|
|
114
|
+
let tools: AuthoringTool[];
|
|
115
|
+
|
|
116
|
+
try {
|
|
117
|
+
const connected = await connectReviewInstance(input.env, headers);
|
|
118
|
+
|
|
119
|
+
input.onDesktop?.(connected.instance?.appVersion);
|
|
120
|
+
client = connected.client;
|
|
121
|
+
tools = (await client.read<AuthoringTool[]>("/authoring")).map(
|
|
122
|
+
publicTool,
|
|
123
|
+
);
|
|
124
|
+
} catch (error) {
|
|
125
|
+
if (
|
|
126
|
+
name === "session_get_instructions" &&
|
|
127
|
+
!(error instanceof ReviewApiError)
|
|
128
|
+
) {
|
|
129
|
+
input.stderr.write(RECOVERY + "\n");
|
|
130
|
+
|
|
131
|
+
return 1;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
throw error;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
if (name === "tools") {
|
|
138
|
+
// The whole catalog is too long to read; a name prints one schema.
|
|
139
|
+
if (json) {
|
|
140
|
+
const tool = tools.find((tool) => tool.name === json);
|
|
141
|
+
|
|
142
|
+
if (!tool)
|
|
143
|
+
throw new Error(
|
|
144
|
+
`Unknown review tool: ${json}. Use whiteboard api tools.`,
|
|
145
|
+
);
|
|
146
|
+
input.stdout.write(JSON.stringify(tool, null, 2) + "\n");
|
|
147
|
+
|
|
148
|
+
return 0;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
for (const tool of tools)
|
|
152
|
+
input.stdout.write(
|
|
153
|
+
`${tool.name}: ${tool.description.split(/(?<=\.)\s/)[0]}\n`,
|
|
154
|
+
);
|
|
155
|
+
|
|
156
|
+
return 0;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
const tool = tools.find((tool) => tool.name === name);
|
|
160
|
+
|
|
161
|
+
if (!tool)
|
|
162
|
+
throw new Error(
|
|
163
|
+
`Unknown review tool: ${name}. Use whiteboard api tools.`,
|
|
164
|
+
);
|
|
165
|
+
let source = json ?? "{}";
|
|
166
|
+
|
|
167
|
+
if (source === "-") {
|
|
168
|
+
if (!input.stdin) throw new Error("No input stream supplied.");
|
|
169
|
+
source = "";
|
|
170
|
+
input.stdin.setEncoding("utf8");
|
|
171
|
+
|
|
172
|
+
for await (const chunk of input.stdin) source += chunk;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
const args = parseJsonText(source);
|
|
176
|
+
|
|
177
|
+
if (!isJsonObject(args))
|
|
178
|
+
throw new Error("Tool input must be a JSON object.");
|
|
179
|
+
|
|
180
|
+
if (name === "session_get" && rest.includes("--json")) args.format = "json";
|
|
181
|
+
const startedAt = Date.now();
|
|
182
|
+
let result: Awaited<ReturnType<typeof callPublicTool>>;
|
|
183
|
+
|
|
184
|
+
try {
|
|
185
|
+
result = await callPublicTool(client, tool, args);
|
|
186
|
+
} catch (error) {
|
|
187
|
+
await input.onToolCall?.({
|
|
188
|
+
tool: tool.name,
|
|
189
|
+
via: "api",
|
|
190
|
+
ok: false,
|
|
191
|
+
durationMs: Date.now() - startedAt,
|
|
192
|
+
...toolFailure(error, "call"),
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
throw error;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
await input.onToolCall?.({
|
|
199
|
+
tool: tool.name,
|
|
200
|
+
via: "api",
|
|
201
|
+
ok: true,
|
|
202
|
+
durationMs: Date.now() - startedAt,
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
const text = toolResultText(tool, result);
|
|
206
|
+
input.stdout.write(text.endsWith("\n") ? text : text + "\n");
|
|
207
|
+
|
|
208
|
+
return 0;
|
|
209
|
+
} catch (error) {
|
|
210
|
+
const failure = error instanceof Error ? error : new Error(String(error));
|
|
211
|
+
|
|
212
|
+
input.onFailure?.(failure);
|
|
213
|
+
input.stderr.write(failure.message + "\n");
|
|
214
|
+
|
|
215
|
+
return 1;
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
import { isJsonObject, parseJsonText } from "@dev.fast/json";
|