@extenshi/mcp 0.22.1 → 0.24.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -124,6 +124,9 @@ stdio or CLI. See the [development workflow](https://docs.extenshi.io/developers
124
124
  | `generate_landing_page` | Static landing-page (homepage) HTML with no JavaScript — same generator as the cabinet Page generator; returns `{html, bytes, warnings, nextSteps}`. Host it on any HTTPS origin and register the URL with `upsert_hosted_page` (feeds `HOMEPAGE_URL`) | Free (no key) |
125
125
  | `scan_extension` | Pre-publish security scan of a local artifact (.zip/.crx/.xpi), with live progress | 1 scan |
126
126
  | `publish_extension` | Publish to Chrome/Firefox/Edge with your own store credentials (fully local) | Free |
127
+ | `get_build_map` | The author's build map of `projectDir` (`npx @extenshi/cli map`): progress, steps waiting on the author, steps that can start now, every step's current task, and the variants offered with the author's answer. Local server only | Free (no key) |
128
+ | `report_build_step` | Report on a build map step (start, progress, done with hashed file proof, stop), or record the author's answer from the chat (`choice` / `redo`). Local server only | Free (no key) |
129
+ | `request_build_review` | Offer the author 1–8 variants to choose from on the map (icon concepts, screenshots, listing copy), each a preview page or image in the project; returns the link to the step. Local server only | Free (no key) |
127
130
 
128
131
  ### Identifying an extension by its store id
129
132
 
@@ -0,0 +1,128 @@
1
+ /**
2
+ * What an agent does on the build map (plan 2026-11-03-extension-build-map
3
+ * §6.3–6.4, phase 2): report a step, offer the author variants to choose from,
4
+ * record the author's answer from the chat, and wait for the author's answer.
5
+ *
6
+ * The CLI (`extenshi map step|review|choose|wait`) and the stdio MCP server
7
+ * (`report_build_step`, `request_build_review`, `get_build_map`) both call
8
+ * these. Every write goes through updateMapState, so it lands in the one map of
9
+ * the project from whichever worktree the agent works in, and an open page shows
10
+ * it within a second. The MCP server ships a byte-identical copy of this file
11
+ * next to its copy of build-map-store.ts; build-map-store-sync.test.ts there
12
+ * fails on any drift. Edit this file, then copy it over.
13
+ *
14
+ * Files an agent names are resolved here. Evidence files are hashed, so the
15
+ * journal entry is `checked`. A variant's preview and image must be files of
16
+ * the project, in the main checkout or any of its worktrees, or https
17
+ * addresses. The map server serves those files only for a variant that names
18
+ * them (reviewFileOnDisk), so naming a file elsewhere is refused up front.
19
+ */
20
+ import { type BuildEvidenceWhere, type BuildMapView, type BuildNodeRecord, type BuildReview, type BuildStepReport } from '@extenshi/contracts';
21
+ import { type MapClient, type RunningServer } from './build-map-store.js';
22
+ /** The file types a variant's preview or image may be, with the type the map server answers with. */
23
+ export declare const REVIEW_FILE_TYPES: Readonly<Record<string, string>>;
24
+ export declare const REVIEW_FILE_MAX_BYTES: number;
25
+ /**
26
+ * Where a variant's preview or image is, as the state file keeps it: an https
27
+ * address as given; a file relative to `home` when it is in the main checkout,
28
+ * absolute when it is in another checkout of the project. Throws for anything
29
+ * else, with what to do instead.
30
+ */
31
+ export declare function resolveReviewFile(home: string, from: string, value: string): Promise<string>;
32
+ /**
33
+ * The file behind a variant's preview or image, if the map may serve it: it
34
+ * is a file of a checkout of the project, of a type in REVIEW_FILE_TYPES, and
35
+ * not too big. Checked again at every request, since files change.
36
+ */
37
+ export declare function reviewFileOnDisk(home: string, stored: string): Promise<{
38
+ file: string;
39
+ type: string;
40
+ bytes: number;
41
+ } | undefined>;
42
+ /** Evidence files hashed: relative to `home` when inside it, absolute otherwise. Throws for a file that is not there. */
43
+ export declare function evidenceFiles(home: string, from: string, files: readonly string[]): Promise<BuildEvidenceWhere[]>;
44
+ /**
45
+ * The map as the running map server derives it: with the project facts and
46
+ * the bound Dojo project. Undefined when no server runs or it does not answer.
47
+ * `client` tells the server which program asks (probeServer); `server` says
48
+ * which release and graph revision the map runs.
49
+ */
50
+ export declare function serverSnapshot(home: string, client?: MapClient): Promise<{
51
+ url: string;
52
+ server: RunningServer;
53
+ snapshot: {
54
+ view: BuildMapView;
55
+ } & Record<string, unknown>;
56
+ } | undefined>;
57
+ /**
58
+ * What an agent should know about the release the author's map runs, in one
59
+ * sentence: the map draws older steps than the agent knows (switch it), newer
60
+ * ones (update the agent's own tool), or a newer release is out. Undefined
61
+ * when the two agree and nothing newer is known.
62
+ */
63
+ export declare function mapUpdateNote(server: Pick<RunningServer, 'version' | 'graph' | 'available' | 'updating'>, client: MapClient): string | undefined;
64
+ /** The address that opens one step on the map. */
65
+ export declare const stepUrl: (url: string, node: string) => string;
66
+ export type StepInput = Omit<BuildStepReport, 'at' | 'where' | 'checked'> & {
67
+ /** Files that show the task done: hashed, so the entry is checked. */
68
+ files?: readonly string[];
69
+ urls?: readonly string[];
70
+ dojo?: readonly string[];
71
+ command?: string;
72
+ };
73
+ /**
74
+ * Report on a step. `view` checks that the step's inputs are done; without it
75
+ * (no project facts) nothing is refused for that. The entry is `checked` when
76
+ * every proof is a file this call hashed.
77
+ */
78
+ export declare function reportStep(home: string, from: string, input: StepInput, view?: BuildMapView): Promise<{
79
+ record: BuildNodeRecord | undefined;
80
+ checked: boolean;
81
+ }>;
82
+ export type VariantInput = {
83
+ label: string;
84
+ preview: string;
85
+ image?: string;
86
+ };
87
+ /** Offer the author variants to choose from; files are resolved against `from`. */
88
+ export declare function openReview(home: string, from: string, input: {
89
+ node: string;
90
+ agent: string;
91
+ variants: readonly VariantInput[];
92
+ }, view?: BuildMapView): Promise<BuildReview>;
93
+ /** Record the author's answer from the chat: the variant they picked, or what to change. */
94
+ export declare function answerReview(home: string, input: {
95
+ node: string;
96
+ agent: string;
97
+ choice?: string;
98
+ redo?: string;
99
+ round?: number;
100
+ }, view?: BuildMapView): Promise<BuildReview>;
101
+ export type ReviewAnswer = {
102
+ node: string;
103
+ round: number;
104
+ status: BuildReview['status'];
105
+ /** The variant picked. */
106
+ choice?: {
107
+ id: string;
108
+ label: string;
109
+ preview: string;
110
+ image?: string;
111
+ };
112
+ /** What to change, in the author's words. */
113
+ feedback?: string;
114
+ via?: 'map' | 'chat';
115
+ };
116
+ export declare function reviewAnswer(node: string, review: BuildReview): ReviewAnswer;
117
+ /**
118
+ * Wait until the author answers the review of `node`, reading the state file
119
+ * every `pollMs` (no map server needed). Resolves at once when it is answered
120
+ * already; with `timeoutMs`, resolves with the review still waiting once that
121
+ * passes.
122
+ */
123
+ export declare function waitForReview(home: string, node: string, options?: {
124
+ timeoutMs?: number;
125
+ pollMs?: number;
126
+ signal?: AbortSignal;
127
+ }): Promise<ReviewAnswer>;
128
+ //# sourceMappingURL=build-map-agent.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"build-map-agent.d.ts","sourceRoot":"","sources":["../src/build-map-agent.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,270 @@
1
+ /**
2
+ * What an agent does on the build map (plan 2026-11-03-extension-build-map
3
+ * §6.3–6.4, phase 2): report a step, offer the author variants to choose from,
4
+ * record the author's answer from the chat, and wait for the author's answer.
5
+ *
6
+ * The CLI (`extenshi map step|review|choose|wait`) and the stdio MCP server
7
+ * (`report_build_step`, `request_build_review`, `get_build_map`) both call
8
+ * these. Every write goes through updateMapState, so it lands in the one map of
9
+ * the project from whichever worktree the agent works in, and an open page shows
10
+ * it within a second. The MCP server ships a byte-identical copy of this file
11
+ * next to its copy of build-map-store.ts; build-map-store-sync.test.ts there
12
+ * fails on any drift. Edit this file, then copy it over.
13
+ *
14
+ * Files an agent names are resolved here. Evidence files are hashed, so the
15
+ * journal entry is `checked`. A variant's preview and image must be files of
16
+ * the project, in the main checkout or any of its worktrees, or https
17
+ * addresses. The map server serves those files only for a variant that names
18
+ * them (reviewFileOnDisk), so naming a file elsewhere is refused up front.
19
+ */
20
+ import { createHash } from 'node:crypto';
21
+ import { createReadStream } from 'node:fs';
22
+ import { realpath, stat } from 'node:fs/promises';
23
+ import path from 'node:path';
24
+ import { answerBuildReview, buildReviewLocationValid, openBuildReview, reportBuildStep, } from '@extenshi/contracts';
25
+ import { mapSkew, probeServer, projectCheckouts, readMapState, updateMapState, } from './build-map-store.js';
26
+ /** The file types a variant's preview or image may be, with the type the map server answers with. */
27
+ export const REVIEW_FILE_TYPES = {
28
+ '.html': 'text/html; charset=utf-8',
29
+ '.htm': 'text/html; charset=utf-8',
30
+ '.svg': 'image/svg+xml',
31
+ '.png': 'image/png',
32
+ '.jpg': 'image/jpeg',
33
+ '.jpeg': 'image/jpeg',
34
+ '.webp': 'image/webp',
35
+ '.gif': 'image/gif',
36
+ };
37
+ export const REVIEW_FILE_MAX_BYTES = 20 * 1024 * 1024;
38
+ /** Evidence files bigger than this are refused rather than hashed for minutes. */
39
+ const EVIDENCE_FILE_MAX_BYTES = 64 * 1024 * 1024;
40
+ const isHttps = (value) => /^https:\/\//i.test(value);
41
+ const hasScheme = (value) => /^[a-z][a-z0-9+.-]*:\/\//i.test(value);
42
+ function inside(root, file) {
43
+ const relative = path.relative(root, file);
44
+ return relative !== '' && !relative.startsWith('..') && !path.isAbsolute(relative);
45
+ }
46
+ /** The project directory in every checkout of the repository, resolved: the map home first. */
47
+ async function checkoutRoots(home) {
48
+ const roots = [];
49
+ for (const dir of await projectCheckouts(home)) {
50
+ try {
51
+ const real = await realpath(dir);
52
+ if (!roots.includes(real))
53
+ roots.push(real);
54
+ }
55
+ catch {
56
+ // A worktree removed since git listed it.
57
+ }
58
+ }
59
+ return roots.length ? roots : [home];
60
+ }
61
+ async function sha256Of(file) {
62
+ const hash = createHash('sha256');
63
+ for await (const chunk of createReadStream(file))
64
+ hash.update(chunk);
65
+ return hash.digest('hex');
66
+ }
67
+ /**
68
+ * Where a variant's preview or image is, as the state file keeps it: an https
69
+ * address as given; a file relative to `home` when it is in the main checkout,
70
+ * absolute when it is in another checkout of the project. Throws for anything
71
+ * else, with what to do instead.
72
+ */
73
+ export async function resolveReviewFile(home, from, value) {
74
+ const given = value.trim();
75
+ if (hasScheme(given)) {
76
+ if (isHttps(given) && buildReviewLocationValid(given))
77
+ return given;
78
+ throw new Error(`${given}: a preview is a file of the project or an https address.`);
79
+ }
80
+ let real;
81
+ try {
82
+ real = await realpath(path.resolve(from, given));
83
+ }
84
+ catch {
85
+ throw new Error(`${given}: no such file.`);
86
+ }
87
+ const type = REVIEW_FILE_TYPES[path.extname(real).toLowerCase()];
88
+ if (!type)
89
+ throw new Error(`${given}: a preview is an HTML page or an image (${Object.keys(REVIEW_FILE_TYPES).join(', ')}).`);
90
+ const info = await stat(real);
91
+ if (!info.isFile())
92
+ throw new Error(`${given}: not a file.`);
93
+ if (info.size > REVIEW_FILE_MAX_BYTES)
94
+ throw new Error(`${given}: larger than ${REVIEW_FILE_MAX_BYTES / 1024 / 1024} MB.`);
95
+ if (inside(home, real))
96
+ return path.relative(home, real);
97
+ // Asking git for the other checkouts costs a process or two: only for a file outside the main one.
98
+ if ((await checkoutRoots(home)).some((root) => inside(root, real)))
99
+ return real;
100
+ throw new Error(`${given}: outside the project, so the map will not serve it. Write the preview inside the project (any of its git worktrees works).`);
101
+ }
102
+ /**
103
+ * The file behind a variant's preview or image, if the map may serve it: it
104
+ * is a file of a checkout of the project, of a type in REVIEW_FILE_TYPES, and
105
+ * not too big. Checked again at every request, since files change.
106
+ */
107
+ export async function reviewFileOnDisk(home, stored) {
108
+ if (!buildReviewLocationValid(stored) || hasScheme(stored))
109
+ return undefined;
110
+ try {
111
+ const real = await realpath(path.isAbsolute(stored) ? stored : path.join(home, stored));
112
+ const type = REVIEW_FILE_TYPES[path.extname(real).toLowerCase()];
113
+ if (!type)
114
+ return undefined;
115
+ const info = await stat(real);
116
+ if (!info.isFile() || info.size > REVIEW_FILE_MAX_BYTES)
117
+ return undefined;
118
+ if (!inside(home, real) && !(await checkoutRoots(home)).some((root) => inside(root, real)))
119
+ return undefined;
120
+ return { file: real, type, bytes: info.size };
121
+ }
122
+ catch {
123
+ return undefined;
124
+ }
125
+ }
126
+ /** Evidence files hashed: relative to `home` when inside it, absolute otherwise. Throws for a file that is not there. */
127
+ export async function evidenceFiles(home, from, files) {
128
+ const where = [];
129
+ for (const given of files) {
130
+ const absolute = path.resolve(from, given);
131
+ let info;
132
+ try {
133
+ info = await stat(absolute);
134
+ }
135
+ catch {
136
+ throw new Error(`${given}: no such file.`);
137
+ }
138
+ if (!info.isFile())
139
+ throw new Error(`${given}: not a file.`);
140
+ if (info.size > EVIDENCE_FILE_MAX_BYTES)
141
+ throw new Error(`${given}: larger than ${EVIDENCE_FILE_MAX_BYTES / 1024 / 1024} MB; name a smaller file as proof.`);
142
+ const real = await realpath(absolute);
143
+ where.push({ file: inside(home, real) ? path.relative(home, real) : real, sha256: await sha256Of(real) });
144
+ }
145
+ return where;
146
+ }
147
+ /**
148
+ * The map as the running map server derives it: with the project facts and
149
+ * the bound Dojo project. Undefined when no server runs or it does not answer.
150
+ * `client` tells the server which program asks (probeServer); `server` says
151
+ * which release and graph revision the map runs.
152
+ */
153
+ export async function serverSnapshot(home, client) {
154
+ const server = await probeServer(home, client);
155
+ if (!server)
156
+ return undefined;
157
+ try {
158
+ const response = await fetch(`http://127.0.0.1:${server.port}/api/map`, {
159
+ signal: AbortSignal.timeout(3_000),
160
+ });
161
+ if (!response.ok)
162
+ return undefined;
163
+ const snapshot = (await response.json());
164
+ return snapshot.view
165
+ ? { url: server.url, server, snapshot: snapshot }
166
+ : undefined;
167
+ }
168
+ catch {
169
+ return undefined;
170
+ }
171
+ }
172
+ /**
173
+ * What an agent should know about the release the author's map runs, in one
174
+ * sentence: the map draws older steps than the agent knows (switch it), newer
175
+ * ones (update the agent's own tool), or a newer release is out. Undefined
176
+ * when the two agree and nothing newer is known.
177
+ */
178
+ export function mapUpdateNote(server, client) {
179
+ const tool = client.kind === 'cli' ? 'this CLI' : 'this MCP server';
180
+ const switchIt = client.kind === 'cli' ? '`extenshi map update`' : '`npx @extenshi/cli@latest map update` in the project';
181
+ if (server.updating)
182
+ return `The map is restarting on @extenshi/cli ${server.updating}; the author's page reloads by itself.`;
183
+ const skew = mapSkew(server, client);
184
+ if (skew === 'map_older' && client.graph > server.graph)
185
+ return `The author's map draws an older set of steps (revision ${server.graph}) than ${tool} knows (revision ${client.graph}). Run ${switchIt} to restart it on the newer steps when no choice waits on the author; their progress stays.`;
186
+ if (skew === 'map_older')
187
+ return `The author's map runs @extenshi/cli ${server.version}, older than this CLI (${client.version}). Run ${switchIt} to restart it on this release; the author's progress stays.`;
188
+ if (skew === 'client_older')
189
+ return client.kind === 'cli'
190
+ ? `The author's map draws newer steps (revision ${server.graph}) than this CLI (revision ${client.graph}). Run the latest CLI: npx @extenshi/cli@latest map …`
191
+ : `The author's map draws newer steps (revision ${server.graph}) than this MCP server (revision ${client.graph}). Restart the MCP server on the latest @extenshi/mcp so its checks match the map.`;
192
+ if (server.available)
193
+ return `@extenshi/cli ${server.available} is out; the author's map runs ${server.version}. The author can press Update on the map, or run \`npx @extenshi/cli@latest map update\` in the project.`;
194
+ return undefined;
195
+ }
196
+ /** The address that opens one step on the map. */
197
+ export const stepUrl = (url, node) => `${url.replace(/#.*$/, '')}#node=${node}`;
198
+ /**
199
+ * Report on a step. `view` checks that the step's inputs are done; without it
200
+ * (no project facts) nothing is refused for that. The entry is `checked` when
201
+ * every proof is a file this call hashed.
202
+ */
203
+ export async function reportStep(home, from, input, view) {
204
+ const hashed = await evidenceFiles(home, from, input.files ?? []);
205
+ const where = [
206
+ ...hashed,
207
+ ...(input.urls ?? []).map((url) => ({ url })),
208
+ ...(input.dojo ?? []).map((dojo) => ({ dojo })),
209
+ ...(input.command ? [{ command: input.command }] : []),
210
+ ];
211
+ const checked = hashed.length > 0 && hashed.length === where.length;
212
+ const { files: _files, urls: _urls, dojo: _dojo, command: _command, ...report } = input;
213
+ const state = await updateMapState(home, (current) => reportBuildStep(current, view, { ...report, where, checked, at: new Date().toISOString() }));
214
+ return { record: state.nodes[input.node], checked };
215
+ }
216
+ /** Offer the author variants to choose from; files are resolved against `from`. */
217
+ export async function openReview(home, from, input, view) {
218
+ const variants = [];
219
+ for (const variant of input.variants)
220
+ variants.push({
221
+ label: variant.label,
222
+ preview: await resolveReviewFile(home, from, variant.preview),
223
+ ...(variant.image ? { image: await resolveReviewFile(home, from, variant.image) } : {}),
224
+ });
225
+ const state = await updateMapState(home, (current) => openBuildReview(current, view, {
226
+ node: input.node,
227
+ agent: input.agent,
228
+ variants,
229
+ at: new Date().toISOString(),
230
+ }));
231
+ return state.reviews[input.node];
232
+ }
233
+ /** Record the author's answer from the chat: the variant they picked, or what to change. */
234
+ export async function answerReview(home, input, view) {
235
+ const state = await updateMapState(home, (current) => answerBuildReview(current, view, { ...input, via: 'chat', at: new Date().toISOString() }));
236
+ return state.reviews[input.node];
237
+ }
238
+ export function reviewAnswer(node, review) {
239
+ const choice = review.status === 'picked' ? review.variants.find((v) => v.id === review.choice) : undefined;
240
+ return {
241
+ node,
242
+ round: review.round,
243
+ status: review.status,
244
+ ...(choice ? { choice } : {}),
245
+ ...(review.status === 'redo' && review.feedback ? { feedback: review.feedback } : {}),
246
+ ...(review.via && review.status !== 'waiting' ? { via: review.via } : {}),
247
+ };
248
+ }
249
+ /**
250
+ * Wait until the author answers the review of `node`, reading the state file
251
+ * every `pollMs` (no map server needed). Resolves at once when it is answered
252
+ * already; with `timeoutMs`, resolves with the review still waiting once that
253
+ * passes.
254
+ */
255
+ export async function waitForReview(home, node, options = {}) {
256
+ const pollMs = options.pollMs ?? 1_000;
257
+ const deadline = options.timeoutMs ? Date.now() + options.timeoutMs : Number.POSITIVE_INFINITY;
258
+ for (;;) {
259
+ const read = await readMapState(home);
260
+ if (read.problem)
261
+ throw new Error(`Cannot read the build map: ${read.problem}`);
262
+ const review = read.state.reviews[node];
263
+ if (!review)
264
+ throw new Error(`No review is open on "${node}": open one with the variants first.`);
265
+ if (review.status !== 'waiting' || Date.now() >= deadline || options.signal?.aborted)
266
+ return reviewAnswer(node, review);
267
+ await new Promise((resolve) => setTimeout(resolve, Math.min(pollMs, Math.max(0, deadline - Date.now()))));
268
+ }
269
+ }
270
+ //# sourceMappingURL=build-map-agent.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"build-map-agent.js","sourceRoot":"","sources":["../src/build-map-agent.ts"],"names":[],"mappings":"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"}
@@ -32,7 +32,38 @@ export declare const MAP_STATE_FILE = "build-map.json";
32
32
  export declare const MAP_SERVER_FILE = "build-map.server.json";
33
33
  export declare const MAP_LOG_FILE = "build-map.log";
34
34
  /** Bumped when the page/server protocol changes: a newer CLI replaces an older server instead of reusing it. */
35
- export declare const MAP_PROTOCOL = 1;
35
+ export declare const MAP_PROTOCOL = 7;
36
+ /** The record token, from the owner-only record file: authorizes /api/shutdown and a client's hello. */
37
+ export declare const MAP_TOKEN_HEADER = "x-extenshi-map-token";
38
+ /** Who asks: `cli/<version> graph=<revision>` or `mcp/<version> graph=<revision>` (see mapClientHeader). */
39
+ export declare const MAP_CLIENT_HEADER = "x-extenshi-map-client";
40
+ /**
41
+ * A program that talks to the map server on an agent's behalf: the CLI or the
42
+ * stdio MCP server, its package version, and the graph revision it was built
43
+ * with (BUILD_GRAPH_REVISION of its @extenshi/contracts).
44
+ */
45
+ export type MapClient = {
46
+ kind: 'cli' | 'mcp';
47
+ version: string;
48
+ graph: number;
49
+ };
50
+ /** This program as a map client: the graph revision is the one it was built with. */
51
+ export declare const mapClient: (kind: MapClient["kind"], version: string) => MapClient;
52
+ export declare const mapClientHeader: (client: MapClient) => string;
53
+ export declare function parseMapClient(value: unknown): MapClient | undefined;
54
+ /** Order of two `x.y.z` versions (a pre-release sorts before its release); undefined when either is not one. */
55
+ export declare function compareVersions(a: string, b: string): number | undefined;
56
+ /**
57
+ * How the running map and a client differ. `map_older`: the map draws an older
58
+ * graph than the client knows, or — for the CLI, the program that serves the
59
+ * map — it runs an older CLI release; `client_older`: the client knows an older
60
+ * graph than the map draws. A server from before graph revisions counts as
61
+ * revision 0.
62
+ */
63
+ export declare function mapSkew(server: {
64
+ version: string;
65
+ graph?: number;
66
+ }, client: MapClient): 'map_older' | 'client_older' | undefined;
36
67
  export type MapHome = {
37
68
  /** Where the map's files live: this project directory in the main checkout. */
38
69
  home: string;
@@ -90,6 +121,8 @@ export type MapServerRecord = {
90
121
  token: string;
91
122
  protocol: number;
92
123
  version: string;
124
+ /** The graph revision the server draws (absent before protocol 7). */
125
+ graph?: number;
93
126
  home: string;
94
127
  startedAt: string;
95
128
  };
@@ -99,19 +132,28 @@ export type MapHealth = {
99
132
  protocol: number;
100
133
  version: string;
101
134
  clients: number;
135
+ /** The graph revision the server draws (absent before protocol 7). */
136
+ graph?: number;
137
+ /** A newer @extenshi/cli on npm that the map can restart on. */
138
+ available?: string;
139
+ /** The version the map is restarting on right now. */
140
+ updating?: string;
102
141
  };
103
142
  export declare function readServerRecord(home: string): Promise<MapServerRecord | undefined>;
104
- export declare function fetchHealth(port: number, timeoutMs?: number): Promise<MapHealth | undefined>;
105
- export type RunningServer = MapServerRecord & {
143
+ export declare function fetchHealth(port: number, timeoutMs?: number, headers?: Record<string, string>): Promise<MapHealth | undefined>;
144
+ export type RunningServer = MapServerRecord & Pick<MapHealth, 'available' | 'updating'> & {
106
145
  clients: number;
146
+ graph: number;
107
147
  };
108
148
  /**
109
149
  * The server recorded for `home`, if it is really up: same machine, process
110
150
  * alive, and its health answer carries the recorded instance id and home. A
111
151
  * record left by a crash, by another machine (a synced folder) or a reused pid
112
- * reads as no server.
152
+ * reads as no server. With `client`, the probe also tells the server which
153
+ * program asks and which graph it knows, so the map can offer an update when
154
+ * an agent works with newer steps than the map draws.
113
155
  */
114
- export declare function probeServer(home: string): Promise<RunningServer | undefined>;
156
+ export declare function probeServer(home: string, client?: MapClient): Promise<RunningServer | undefined>;
115
157
  export declare const serverLockPath: (home: string) => string;
116
158
  /**
117
159
  * Mark the server record stopped, but only if it still names `instance` —
@@ -1 +1 @@
1
- {"version":3,"file":"build-map-store.d.ts","sourceRoot":"","sources":["../src/build-map-store.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2BG;AAOH,OAAO,EAAE,KAAK,aAAa,EAA8C,MAAM,qBAAqB,CAAA;AAEpG,eAAO,MAAM,OAAO,cAAc,CAAA;AAClC,eAAO,MAAM,cAAc,mBAAmB,CAAA;AAC9C,eAAO,MAAM,eAAe,0BAA0B,CAAA;AACtD,eAAO,MAAM,YAAY,kBAAkB,CAAA;AAI3C,gHAAgH;AAChH,eAAO,MAAM,YAAY,IAAI,CAAA;AAE7B,MAAM,MAAM,OAAO,GAAG;IACrB,+EAA+E;IAC/E,IAAI,EAAE,MAAM,CAAA;IACZ,wCAAwC;IACxC,GAAG,EAAE,MAAM,CAAA;IACX,qFAAqF;IACrF,QAAQ,EAAE,OAAO,CAAA;CACjB,CAAA;AAED,eAAO,MAAM,OAAO,GAAI,MAAM,MAAM,EAAE,MAAM,MAAM,WAAmC,CAAA;AAkBrF;;;;GAIG;AACH,wBAAsB,cAAc,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CAkBlE;AAED,gHAAgH;AAChH,wBAAsB,gBAAgB,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,EAAE,CAAC,CAetE;AAED,wBAAgB,QAAQ,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAQ7C;AA4ED;;;;;;;GAOG;AACH,wBAAsB,WAAW,CAAC,CAAC,EAClC,OAAO,EAAE,MAAM,EACf,IAAI,EAAE,MAAM,OAAO,CAAC,CAAC,CAAC,EACtB,OAAO,GAAE;IAAE,SAAS,CAAC,EAAE,MAAM,CAAC;IAAC,OAAO,CAAC,EAAE,MAAM,CAAC;IAAC,IAAI,CAAC,EAAE,MAAM,CAAA;CAAO,GACnE,OAAO,CAAC,CAAC,CAAC,CAqCZ;AAED,uFAAuF;AACvF,wBAAsB,eAAe,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,OAAO,EAAE,IAAI,SAAQ,GAAG,OAAO,CAAC,IAAI,CAAC,CAe/F;AAED,MAAM,MAAM,SAAS,GAAG;IAAE,KAAK,EAAE,aAAa,CAAC;IAAC,MAAM,EAAE,OAAO,CAAC;IAAC,OAAO,CAAC,EAAE,MAAM,CAAA;CAAE,CAAA;AAEnF,mGAAmG;AACnG,wBAAsB,YAAY,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,SAAS,CAAC,CAkBnE;AAED;;;;GAIG;AACH,wBAAsB,cAAc,CACnC,IAAI,EAAE,MAAM,EACZ,MAAM,EAAE,CAAC,KAAK,EAAE,aAAa,KAAK,aAAa,GAAG,SAAS,GACzD,OAAO,CAAC,aAAa,CAAC,CAYxB;AAED,MAAM,MAAM,eAAe,GAAG;IAC7B,GAAG,EAAE,MAAM,CAAA;IACX,IAAI,EAAE,MAAM,CAAA;IACZ,IAAI,EAAE,MAAM,CAAA;IACZ,GAAG,EAAE,MAAM,CAAA;IACX,4FAA4F;IAC5F,QAAQ,EAAE,MAAM,CAAA;IAChB,wEAAwE;IACxE,KAAK,EAAE,MAAM,CAAA;IACb,QAAQ,EAAE,MAAM,CAAA;IAChB,OAAO,EAAE,MAAM,CAAA;IACf,IAAI,EAAE,MAAM,CAAA;IACZ,SAAS,EAAE,MAAM,CAAA;CACjB,CAAA;AAED,MAAM,MAAM,SAAS,GAAG;IAAE,QAAQ,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAC;IAAC,QAAQ,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,MAAM,CAAA;CAAE,CAAA;AAE9G,wBAAsB,gBAAgB,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,eAAe,GAAG,SAAS,CAAC,CAQzF;AAED,wBAAsB,WAAW,CAAC,IAAI,EAAE,MAAM,EAAE,SAAS,SAAQ,GAAG,OAAO,CAAC,SAAS,GAAG,SAAS,CAAC,CAUjG;AAED,MAAM,MAAM,aAAa,GAAG,eAAe,GAAG;IAAE,OAAO,EAAE,MAAM,CAAA;CAAE,CAAA;AAEjE;;;;;GAKG;AACH,wBAAsB,WAAW,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,aAAa,GAAG,SAAS,CAAC,CAMlF;AAED,eAAO,MAAM,cAAc,GAAI,MAAM,MAAM,WAA+B,CAAA;AAE1E;;;;;GAKG;AACH,wBAAsB,kBAAkB,CAAC,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAQtF"}
1
+ {"version":3,"file":"build-map-store.d.ts","sourceRoot":"","sources":["../src/build-map-store.ts"],"names":[],"mappings":"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"}
@@ -31,7 +31,7 @@ import { randomUUID } from 'node:crypto';
31
31
  import { mkdir, open, readFile, realpath, rename, rm, stat, writeFile } from 'node:fs/promises';
32
32
  import os from 'node:os';
33
33
  import path from 'node:path';
34
- import { emptyBuildMapState, normalizeBuildMapState } from '@extenshi/contracts';
34
+ import { BUILD_GRAPH_REVISION, emptyBuildMapState, normalizeBuildMapState, } from '@extenshi/contracts';
35
35
  export const MAP_DIR = '.extenshi';
36
36
  export const MAP_STATE_FILE = 'build-map.json';
37
37
  export const MAP_SERVER_FILE = 'build-map.server.json';
@@ -39,7 +39,63 @@ export const MAP_LOG_FILE = 'build-map.log';
39
39
  const STATE_LOCK = 'build-map.lock';
40
40
  const SERVER_LOCK = 'build-map.server.lock';
41
41
  /** Bumped when the page/server protocol changes: a newer CLI replaces an older server instead of reusing it. */
42
- export const MAP_PROTOCOL = 1;
42
+ export const MAP_PROTOCOL = 7;
43
+ /** The record token, from the owner-only record file: authorizes /api/shutdown and a client's hello. */
44
+ export const MAP_TOKEN_HEADER = 'x-extenshi-map-token';
45
+ /** Who asks: `cli/<version> graph=<revision>` or `mcp/<version> graph=<revision>` (see mapClientHeader). */
46
+ export const MAP_CLIENT_HEADER = 'x-extenshi-map-client';
47
+ /** This program as a map client: the graph revision is the one it was built with. */
48
+ export const mapClient = (kind, version) => ({
49
+ kind,
50
+ version,
51
+ graph: BUILD_GRAPH_REVISION,
52
+ });
53
+ export const mapClientHeader = (client) => `${client.kind}/${client.version} graph=${client.graph}`;
54
+ export function parseMapClient(value) {
55
+ if (typeof value !== 'string' || value.length > 80)
56
+ return undefined;
57
+ const match = /^(cli|mcp)\/([0-9A-Za-z.+-]{1,40}) graph=(\d{1,6})$/.exec(value);
58
+ if (!match)
59
+ return undefined;
60
+ return { kind: match[1], version: match[2], graph: Number(match[3]) };
61
+ }
62
+ /** Order of two `x.y.z` versions (a pre-release sorts before its release); undefined when either is not one. */
63
+ export function compareVersions(a, b) {
64
+ const parse = (v) => /^(\d+)\.(\d+)\.(\d+)(?:-([0-9A-Za-z.-]+))?(?:\+[0-9A-Za-z.-]+)?$/.exec(v);
65
+ const x = parse(a);
66
+ const y = parse(b);
67
+ if (!x || !y)
68
+ return undefined;
69
+ for (let i = 1; i <= 3; i++) {
70
+ const d = Number(x[i]) - Number(y[i]);
71
+ if (d)
72
+ return Math.sign(d);
73
+ }
74
+ if (x[4] === y[4])
75
+ return 0;
76
+ if (!x[4])
77
+ return 1;
78
+ if (!y[4])
79
+ return -1;
80
+ return x[4] < y[4] ? -1 : 1;
81
+ }
82
+ /**
83
+ * How the running map and a client differ. `map_older`: the map draws an older
84
+ * graph than the client knows, or — for the CLI, the program that serves the
85
+ * map — it runs an older CLI release; `client_older`: the client knows an older
86
+ * graph than the map draws. A server from before graph revisions counts as
87
+ * revision 0.
88
+ */
89
+ export function mapSkew(server, client) {
90
+ const graph = server.graph ?? 0;
91
+ if (client.graph > graph)
92
+ return 'map_older';
93
+ if (client.graph < graph)
94
+ return 'client_older';
95
+ if (client.kind === 'cli' && (compareVersions(client.version, server.version) ?? 0) > 0)
96
+ return 'map_older';
97
+ return undefined;
98
+ }
43
99
  export const mapPath = (home, file) => path.join(home, MAP_DIR, file);
44
100
  function git(cwd, args) {
45
101
  return new Promise((resolve) => {
@@ -303,10 +359,11 @@ export async function readServerRecord(home) {
303
359
  return undefined;
304
360
  }
305
361
  }
306
- export async function fetchHealth(port, timeoutMs = 1_500) {
362
+ export async function fetchHealth(port, timeoutMs = 1_500, headers) {
307
363
  try {
308
364
  const response = await fetch(`http://127.0.0.1:${port}/api/health`, {
309
365
  signal: AbortSignal.timeout(timeoutMs),
366
+ ...(headers ? { headers } : {}),
310
367
  });
311
368
  if (!response.ok)
312
369
  return undefined;
@@ -320,16 +377,27 @@ export async function fetchHealth(port, timeoutMs = 1_500) {
320
377
  * The server recorded for `home`, if it is really up: same machine, process
321
378
  * alive, and its health answer carries the recorded instance id and home. A
322
379
  * record left by a crash, by another machine (a synced folder) or a reused pid
323
- * reads as no server.
380
+ * reads as no server. With `client`, the probe also tells the server which
381
+ * program asks and which graph it knows, so the map can offer an update when
382
+ * an agent works with newer steps than the map draws.
324
383
  */
325
- export async function probeServer(home) {
384
+ export async function probeServer(home, client) {
326
385
  const record = await readServerRecord(home);
327
386
  if (!record || record.host !== os.hostname() || !pidAlive(record.pid))
328
387
  return undefined;
329
- const health = await fetchHealth(record.port);
388
+ const health = await fetchHealth(record.port, 1_500, client && record.token
389
+ ? { [MAP_TOKEN_HEADER]: record.token, [MAP_CLIENT_HEADER]: mapClientHeader(client) }
390
+ : undefined);
330
391
  if (!health || health.instance !== record.instance || health.home !== home)
331
392
  return undefined;
332
- return { ...record, clients: health.clients };
393
+ return {
394
+ ...record,
395
+ version: health.version ?? record.version,
396
+ clients: health.clients,
397
+ graph: health.graph ?? 0,
398
+ ...(health.available ? { available: health.available } : {}),
399
+ ...(health.updating ? { updating: health.updating } : {}),
400
+ };
333
401
  }
334
402
  export const serverLockPath = (home) => mapPath(home, SERVER_LOCK);
335
403
  /**