wireal-run 0.1.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.
@@ -0,0 +1,2567 @@
1
+ // runner/mcp-stdio.ts
2
+ import { StdioServerTransport } from "@modelcontextprotocol/server/stdio";
3
+
4
+ // mcp/create-server.ts
5
+ import { McpServer } from "@modelcontextprotocol/server";
6
+ import * as z from "zod/v4";
7
+
8
+ // src/orb-settings.ts
9
+ function makeOrb(color) {
10
+ return { colors: [color, "#d6ecff", "#292139"], speed: 0.65, distortion: 0.75, swirl: 0.55, phase: 20 };
11
+ }
12
+ var defaultStatusOrbs = { proposed: makeOrb("#a78bfa"), todo: makeOrb("#9aacc4"), doing: makeOrb("#efb94b"), done: makeOrb("#45d797") };
13
+ var wholeMapOrb = { colors: ["#000000", "#ffffff", "#ffffff"], speed: 0.18, distortion: 0.28, swirl: 0.2, phase: 0 };
14
+ var orbPresets = [
15
+ { name: "Jade", orb: makeOrb("#45d797") },
16
+ { name: "Ocean", orb: { ...makeOrb("#408cff"), colors: ["#408cff", "#6bf1db", "#241a63"] } },
17
+ { name: "Amber", orb: { ...makeOrb("#ffb33e"), colors: ["#ffb33e", "#ffe8a2", "#bf4178"] } },
18
+ { name: "Rose", orb: { ...makeOrb("#ef6387"), colors: ["#ef6387", "#ffc8ad", "#673acc"] } }
19
+ ];
20
+ function validOrb(value) {
21
+ if (!value || typeof value !== "object") return false;
22
+ const v = value;
23
+ return Array.isArray(v.colors) && v.colors.length === 3 && v.colors.every((c) => /^#[a-f\d]{6}$/i.test(c)) && Number.isFinite(v.speed) && v.speed >= 0 && v.speed <= 2 && Number.isFinite(v.distortion) && v.distortion >= 0 && v.distortion <= 1 && Number.isFinite(v.swirl) && v.swirl >= 0 && v.swirl <= 1 && Number.isFinite(v.phase) && v.phase >= 0 && v.phase <= 100;
24
+ }
25
+
26
+ // src/domain.ts
27
+ var statuses = {
28
+ proposed: "Proposed",
29
+ todo: "To do",
30
+ doing: "Doing",
31
+ done: "Done"
32
+ };
33
+ function normalizeProjectPath(input) {
34
+ let path = input.trim().replace(/\/{2,}/g, "/");
35
+ while (path.startsWith("./") || path.startsWith("/"))
36
+ path = path.startsWith("./") ? path.slice(2) : path.slice(1);
37
+ return path.replace(/\/+$/g, "");
38
+ }
39
+ function projectPaths(project) {
40
+ return [
41
+ ...new Set((project.paths ?? []).map(normalizeProjectPath).filter(Boolean))
42
+ ];
43
+ }
44
+ var activityKinds = [
45
+ "change",
46
+ "discovery",
47
+ "decision",
48
+ "verification",
49
+ "blocker"
50
+ ];
51
+ var repositoryLayouts = ["monorepo", "multirepo"];
52
+ function workspaceKind(state) {
53
+ return state.map.kind ?? "coding";
54
+ }
55
+ var uid = () => crypto.randomUUID();
56
+ var taskReferenceId = (tasks = []) => {
57
+ const highest = tasks.reduce((top, task) => {
58
+ const value = Number(task.referenceId);
59
+ return Number.isSafeInteger(value) && value > top ? value : top;
60
+ }, 0);
61
+ return String(highest + 1);
62
+ };
63
+ var validReferenceId = (value) => typeof value === "string" && /^[1-9][0-9]{0,8}$/.test(value);
64
+ var event = (text, author = {
65
+ author: "Wireal AI",
66
+ authorType: "ai"
67
+ }) => ({
68
+ id: uid(),
69
+ text,
70
+ at: (/* @__PURE__ */ new Date()).toISOString(),
71
+ ...author
72
+ });
73
+ function canConnect(state, source, target) {
74
+ const a = state.tasks.find((t) => t.id === source), b = state.tasks.find((t) => t.id === target);
75
+ if (!a || !b || source === target) return false;
76
+ const edges = [
77
+ ...state.links,
78
+ ...state.tasks.filter((t) => t.parentId).map((t) => ({ source: t.parentId, target: t.id }))
79
+ ];
80
+ if (edges.some((e) => e.source === source && e.target === target))
81
+ return false;
82
+ const visited = /* @__PURE__ */ new Set();
83
+ const reaches = (id) => {
84
+ if (id === source) return true;
85
+ if (visited.has(id)) return false;
86
+ visited.add(id);
87
+ return edges.filter((e) => e.source === id).some((e) => reaches(e.target));
88
+ };
89
+ return !reaches(target);
90
+ }
91
+ function updateTask(state, id, patch, message, author) {
92
+ if (patch.name !== void 0 && !patch.name.trim())
93
+ throw new Error("A task needs a name.");
94
+ if (patch.projectIds && (!patch.projectIds.length || patch.projectIds.some((id2) => !state.projects.some((p) => p.id === id2))))
95
+ throw new Error("Choose at least one project.");
96
+ if (patch.commitUrls)
97
+ patch = {
98
+ ...patch,
99
+ commitUrls: [...new Set(patch.commitUrls.map(normalizeCommitUrl))]
100
+ };
101
+ return {
102
+ ...state,
103
+ tasks: state.tasks.map(
104
+ (t) => t.id === id ? {
105
+ ...t,
106
+ ...patch,
107
+ activity: message ? [event(message, author), ...t.activity] : t.activity
108
+ } : t
109
+ )
110
+ };
111
+ }
112
+ function removeTask(state, id) {
113
+ const removed = /* @__PURE__ */ new Set([id]);
114
+ let changed = true;
115
+ while (changed) {
116
+ changed = false;
117
+ for (const task of state.tasks)
118
+ if (task.parentId && removed.has(task.parentId) && !removed.has(task.id)) {
119
+ removed.add(task.id);
120
+ changed = true;
121
+ }
122
+ }
123
+ return {
124
+ ...state,
125
+ tasks: state.tasks.filter((t) => !removed.has(t.id)),
126
+ links: state.links.filter(
127
+ (e) => !removed.has(e.source) && !removed.has(e.target)
128
+ )
129
+ };
130
+ }
131
+ function launchOrder(state) {
132
+ const tasks = [...state.tasks];
133
+ const byPosition = (a, b) => a.position.x - b.position.x || a.position.y - b.position.y || Number(a.referenceId) - Number(b.referenceId);
134
+ const inbound = new Map(tasks.map((task) => [task.id, 0]));
135
+ const outgoing = new Map(tasks.map((task) => [task.id, []]));
136
+ const seen = /* @__PURE__ */ new Set();
137
+ const connections = [
138
+ ...state.links,
139
+ ...tasks.filter((task) => task.parentId).map((task) => ({ source: task.parentId, target: task.id }))
140
+ ];
141
+ for (const connection of connections) {
142
+ const key = `${connection.source}:${connection.target}`;
143
+ if (!inbound.has(connection.source) || !inbound.has(connection.target) || seen.has(key))
144
+ continue;
145
+ seen.add(key);
146
+ outgoing.get(connection.source).push(connection.target);
147
+ inbound.set(connection.target, inbound.get(connection.target) + 1);
148
+ }
149
+ const ready = tasks.filter((task) => inbound.get(task.id) === 0).sort(byPosition);
150
+ const order = /* @__PURE__ */ new Map();
151
+ while (ready.length) {
152
+ const task = ready.shift();
153
+ order.set(task.id, order.size + 1);
154
+ for (const target of outgoing.get(task.id) ?? []) {
155
+ const remaining = inbound.get(target) - 1;
156
+ inbound.set(target, remaining);
157
+ if (remaining === 0) {
158
+ ready.push(tasks.find((item) => item.id === target));
159
+ ready.sort(byPosition);
160
+ }
161
+ }
162
+ }
163
+ return order;
164
+ }
165
+ function normalizeUsage(value) {
166
+ if (!value || typeof value !== "object") return void 0;
167
+ const raw = value;
168
+ const usage = {};
169
+ for (const key of [
170
+ "costUsd",
171
+ "inputTokens",
172
+ "outputTokens",
173
+ "durationMs"
174
+ ])
175
+ if (Number.isFinite(raw[key])) usage[key] = raw[key];
176
+ if (typeof raw.model === "string" && raw.model.trim())
177
+ usage.model = raw.model;
178
+ const windows = normalizeWindows(raw.windows);
179
+ if (windows) usage.windows = windows;
180
+ return Object.keys(usage).length ? usage : void 0;
181
+ }
182
+ function normalizeWindows(value) {
183
+ if (!value || typeof value !== "object") return void 0;
184
+ const raw = value;
185
+ const windows = {};
186
+ for (const key of ["fiveHour", "sevenDay"]) {
187
+ const window = raw[key];
188
+ if (!window || typeof window !== "object") continue;
189
+ const clean = {};
190
+ for (const edge of ["start", "end"]) {
191
+ const percent = window[edge];
192
+ if (typeof percent === "number" && Number.isFinite(percent))
193
+ clean[edge] = percent;
194
+ }
195
+ if (Object.keys(clean).length) windows[key] = clean;
196
+ }
197
+ return Object.keys(windows).length ? windows : void 0;
198
+ }
199
+ function normalizeActivity(entry) {
200
+ const { id, text, at, author, authorType, authorId } = entry;
201
+ const clean = { id, text, at, author, authorType };
202
+ if (authorId !== void 0) clean.authorId = authorId;
203
+ if (entry.kind && activityKinds.includes(entry.kind)) clean.kind = entry.kind;
204
+ if (typeof entry.commit === "string" && /^[0-9a-f]{7,40}$/.test(entry.commit))
205
+ clean.commit = entry.commit;
206
+ const usage = normalizeUsage(entry.usage);
207
+ if (usage) clean.usage = usage;
208
+ return clean;
209
+ }
210
+ function parseWorkspace(raw) {
211
+ const parsed = JSON.parse(raw);
212
+ if (parsed?.version === 1 && Array.isArray(parsed.projects) && Array.isArray(parsed.tasks)) {
213
+ parsed.version = 2;
214
+ parsed.projects = parsed.projects.map((p) => ({
215
+ ...p,
216
+ repositoryUrl: ""
217
+ }));
218
+ parsed.tasks = parsed.tasks.map((t) => {
219
+ const { projectId, ...rest } = t;
220
+ return { ...rest, projectIds: [projectId], commitUrls: [] };
221
+ });
222
+ }
223
+ const value = parsed;
224
+ if (parsed?.version === 2 && Array.isArray(parsed.projects) && Array.isArray(parsed.tasks)) {
225
+ parsed.version = 3;
226
+ parsed.projects = parsed.projects.map(
227
+ (p) => {
228
+ const { category: _, ...project } = p;
229
+ return { ...project, orb: makeOrb(p.color) };
230
+ }
231
+ );
232
+ const names = [
233
+ .../* @__PURE__ */ new Set([
234
+ "Bug",
235
+ "Improvement",
236
+ "New feature",
237
+ ...parsed.tasks.flatMap((t) => t.labels)
238
+ ])
239
+ ];
240
+ parsed.labels = names.map((name, index) => ({
241
+ id: `label-${index}`,
242
+ name,
243
+ orb: makeOrb(["#ef6387", "#408cff", "#45d797", "#ffb33e"][index % 4])
244
+ }));
245
+ parsed.statusOrbs = structuredClone(defaultStatusOrbs);
246
+ }
247
+ if (parsed?.version === 3) {
248
+ parsed.version = 4;
249
+ parsed.map = { name: "Map", orb: structuredClone(wholeMapOrb) };
250
+ }
251
+ if (parsed?.version === 4 && Array.isArray(parsed.tasks)) {
252
+ parsed.version = 5;
253
+ parsed.tasks = parsed.tasks.map((task) => ({
254
+ ...task,
255
+ referenceId: task.referenceId
256
+ }));
257
+ }
258
+ if (parsed?.version === 5 && Array.isArray(parsed.tasks)) {
259
+ parsed.version = 6;
260
+ parsed.tasks = parsed.tasks.map((task) => ({
261
+ ...task,
262
+ objective: typeof task.objective === "string" ? task.objective : "",
263
+ activity: Array.isArray(task.activity) ? task.activity.map((item) => ({
264
+ ...item,
265
+ author: typeof item.author === "string" && item.author.trim() ? item.author : "Wireal AI",
266
+ authorType: item.authorType === "user" ? "user" : "ai"
267
+ })) : []
268
+ }));
269
+ }
270
+ if (parsed?.version === 6 && parsed.map) {
271
+ parsed.version = 7;
272
+ parsed.map = { ...parsed.map, repositoryUrl: "" };
273
+ }
274
+ if (parsed?.version === 7 && Array.isArray(parsed.tasks)) {
275
+ parsed.version = 8;
276
+ const ordered = [...parsed.tasks].sort(
277
+ (a, b) => a.position.x - b.position.x || a.position.y - b.position.y || String(a.referenceId).localeCompare(String(b.referenceId))
278
+ );
279
+ const numbers = new Map(
280
+ ordered.map((task, index) => [task.id, String(index + 1)])
281
+ );
282
+ parsed.tasks = parsed.tasks.map((task) => ({
283
+ ...task,
284
+ referenceId: numbers.get(task.id)
285
+ }));
286
+ }
287
+ if (parsed?.version === 8) {
288
+ parsed.version = 9;
289
+ parsed.statusOrbs = {
290
+ ...parsed.statusOrbs,
291
+ proposed: parsed.statusOrbs?.proposed ?? structuredClone(defaultStatusOrbs.proposed)
292
+ };
293
+ }
294
+ if (parsed?.statusOrbs && typeof parsed.statusOrbs === "object") {
295
+ for (const status of Object.keys(statuses))
296
+ parsed.statusOrbs[status] ??= structuredClone(defaultStatusOrbs[status]);
297
+ }
298
+ if (Array.isArray(parsed?.labels)) {
299
+ parsed.labels = parsed.labels.map((label) => ({
300
+ ...label,
301
+ color: typeof label.color === "string" && /^#[0-9a-f]{6}$/i.test(label.color) ? label.color : "#a1a1aa"
302
+ }));
303
+ }
304
+ const validString = (v) => typeof v === "string" && v.trim().length > 0;
305
+ if (value?.version !== 9 || !Array.isArray(value.projects) || !Array.isArray(value.tasks) || !Array.isArray(value.links))
306
+ throw new Error("Invalid Thread backup.");
307
+ if (!value.map || !validString(value.map.name) || typeof value.map.repositoryUrl !== "string" || value.map.kind !== void 0 && !["coding", "everyday"].includes(value.map.kind) || value.map.repositoryUrl && !isValidRepository(value.map.repositoryUrl) || value.map.repositoryLayout !== void 0 && !repositoryLayouts.includes(value.map.repositoryLayout) || !validOrb(value.map.orb))
308
+ throw new Error("Invalid workspace settings.");
309
+ if (!value.projects.every(
310
+ (p) => p && validString(p.id) && validString(p.name) && /^#[0-9a-f]{6}$/i.test(p.color) && typeof p.repositoryUrl === "string" && (p.paths === void 0 || Array.isArray(p.paths) && p.paths.length <= 50 && p.paths.every(
311
+ (path) => typeof path === "string" && path.trim().length >= 1 && path.trim().length <= 200 && !path.trim().split("/").includes("..")
312
+ )) && validOrb(p.orb) && (!p.repositoryUrl || isValidRepository(p.repositoryUrl))
313
+ ))
314
+ throw new Error("Invalid project data.");
315
+ if (!Array.isArray(value.labels) || !value.labels.every(
316
+ (l) => l && validString(l.id) && validString(l.name) && validOrb(l.orb) && typeof l.color === "string" && /^#[0-9a-f]{6}$/i.test(l.color)
317
+ ) || new Set(value.labels.map((l) => l.id)).size !== value.labels.length || new Set(value.labels.map((l) => l.name.toLowerCase())).size !== value.labels.length)
318
+ throw new Error("Invalid label data.");
319
+ if (!value.statusOrbs || !Object.keys(statuses).every((s) => validOrb(value.statusOrbs[s])))
320
+ throw new Error("Invalid status colors.");
321
+ const projects = new Set(value.projects.map((p) => p.id)), tasks = new Set(value.tasks.map((t) => t?.id));
322
+ if (projects.size !== value.projects.length || tasks.size !== value.tasks.length || new Set(value.tasks.map((task) => task.referenceId)).size !== value.tasks.length)
323
+ throw new Error("Duplicate IDs in backup.");
324
+ if (!value.tasks.every(
325
+ (t) => t && validString(t.id) && validReferenceId(t.referenceId) && validString(t.name) && typeof t.objective === "string" && Array.isArray(t.projectIds) && t.projectIds.length > 0 && new Set(t.projectIds).size === t.projectIds.length && t.projectIds.every((id) => projects.has(id)) && Array.isArray(t.commitUrls) && t.commitUrls.every(isValidCommit) && Object.hasOwn(statuses, t.status) && Array.isArray(t.labels) && t.labels.every(validString) && t.labels.every((name) => value.labels.some((l) => l.name === name)) && (t.parentId === null || tasks.has(t.parentId)) && Number.isFinite(t.position?.x) && Number.isFinite(t.position?.y) && Array.isArray(t.activity) && t.activity.every(
326
+ (a) => a && validString(a.id) && validString(a.text) && validString(a.author) && (a.authorType === "user" || a.authorType === "ai") && Number.isFinite(Date.parse(a.at))
327
+ )
328
+ ))
329
+ throw new Error("Invalid task data.");
330
+ value.tasks = value.tasks.map((task) => ({
331
+ ...task,
332
+ activity: task.activity.map(normalizeActivity)
333
+ }));
334
+ const graph = {
335
+ ...value,
336
+ links: [],
337
+ tasks: value.tasks.map((t) => ({ ...t, parentId: null }))
338
+ };
339
+ for (const t of value.tasks)
340
+ if (t.parentId) {
341
+ if (!canConnect(graph, t.parentId, t.id))
342
+ throw new Error("Invalid subtask hierarchy.");
343
+ graph.tasks = graph.tasks.map(
344
+ (n) => n.id === t.id ? { ...n, parentId: t.parentId } : n
345
+ );
346
+ }
347
+ const linkIds = /* @__PURE__ */ new Set();
348
+ for (const e of value.links) {
349
+ const legacy = e;
350
+ const legacyHandle = (handle) => handle === void 0 || typeof handle === "string" && ["left", "right", "top", "bottom"].includes(handle);
351
+ if (!e || !validString(e.id) || linkIds.has(e.id) || !legacyHandle(legacy.sourceHandle) || !legacyHandle(legacy.targetHandle) || !canConnect(graph, e.source, e.target))
352
+ throw new Error("Invalid task connections.");
353
+ linkIds.add(e.id);
354
+ const {
355
+ sourceHandle: _sourceHandle,
356
+ targetHandle: _targetHandle,
357
+ ...dependency
358
+ } = legacy;
359
+ graph.links.push(dependency);
360
+ }
361
+ value.links = graph.links;
362
+ value.projects = value.projects.map((project) => {
363
+ const legacy = project;
364
+ const { category: _category, ...clean } = legacy;
365
+ return clean;
366
+ });
367
+ return value;
368
+ }
369
+ function ensureLabels(state, names) {
370
+ const missing = [
371
+ ...new Set(names.map((n) => n.trim()).filter(Boolean))
372
+ ].filter(
373
+ (name) => !state.labels.some((l) => l.name.toLowerCase() === name.toLowerCase())
374
+ );
375
+ return {
376
+ ...state,
377
+ labels: [
378
+ ...state.labels,
379
+ ...missing.map((name) => ({
380
+ id: uid(),
381
+ name,
382
+ orb: makeOrb("#669df6"),
383
+ color: "#669df6"
384
+ }))
385
+ ]
386
+ };
387
+ }
388
+ function saveLabel(state, label) {
389
+ if (!label.name.trim() || state.labels.some(
390
+ (l) => l.id !== label.id && l.name.toLowerCase() === label.name.trim().toLowerCase()
391
+ ))
392
+ throw new Error("Use a unique label name.");
393
+ if (!validOrb(label.orb)) throw new Error("Invalid orb settings.");
394
+ if (label.color && !/^#[0-9a-f]{6}$/i.test(label.color))
395
+ throw new Error("Use a valid color.");
396
+ const previous = state.labels.find((l) => l.id === label.id);
397
+ const next = { ...label, name: label.name.trim() };
398
+ return {
399
+ ...state,
400
+ labels: previous ? state.labels.map((l) => l.id === label.id ? next : l) : [...state.labels, next],
401
+ tasks: previous ? state.tasks.map((t) => ({
402
+ ...t,
403
+ labels: t.labels.map(
404
+ (name) => name === previous.name ? next.name : name
405
+ )
406
+ })) : state.tasks
407
+ };
408
+ }
409
+ function deleteLabel(state, id) {
410
+ const label = state.labels.find((l) => l.id === id);
411
+ return {
412
+ ...state,
413
+ labels: state.labels.filter((l) => l.id !== id),
414
+ tasks: state.tasks.map((t) => ({
415
+ ...t,
416
+ labels: t.labels.filter((name) => name !== label?.name)
417
+ }))
418
+ };
419
+ }
420
+ function normalizeRepositoryUrl(input) {
421
+ if (!input.trim()) return "";
422
+ const url = new URL(input.trim());
423
+ const match = url.pathname.match(/^\/([a-zA-Z0-9-]+)\/([a-zA-Z0-9._-]+)\/?$/);
424
+ if (url.protocol !== "https:" || url.hostname !== "github.com" || url.port || url.username || url.password || !match || url.search || url.hash)
425
+ throw new Error(
426
+ "Use a GitHub repository URL: https://github.com/owner/repository"
427
+ );
428
+ return `https://github.com/${match[1]}/${match[2].replace(/\.git$/i, "")}`;
429
+ }
430
+ function repositoryLayout(state) {
431
+ return state.map.repositoryLayout ?? (state.map.repositoryUrl ? "monorepo" : "multirepo");
432
+ }
433
+ function projectRepository(state, project) {
434
+ if (workspaceKind(state) === "everyday") return "";
435
+ return repositoryLayout(state) === "monorepo" ? state.map.repositoryUrl || project.repositoryUrl : project.repositoryUrl;
436
+ }
437
+ function taskRepository(state, task) {
438
+ for (const id of task.projectIds) {
439
+ const project = state.projects.find((item) => item.id === id);
440
+ const repository = project ? projectRepository(state, project) : "";
441
+ if (repository) return repository;
442
+ }
443
+ return "";
444
+ }
445
+ function normalizeCommitUrl(input) {
446
+ const url = new URL(input.trim());
447
+ const match = url.pathname.match(
448
+ /^\/([a-zA-Z0-9-]+)\/([a-zA-Z0-9._-]+)\/commit\/([a-fA-F0-9]{40})\/?$/
449
+ );
450
+ if (url.protocol !== "https:" || url.hostname !== "github.com" || url.port || url.username || url.password || !match || url.search || url.hash)
451
+ throw new Error(
452
+ "Use an exact GitHub commit URL with the full 40-character SHA."
453
+ );
454
+ return `https://github.com/${match[1]}/${match[2]}/commit/${match[3].toLowerCase()}`;
455
+ }
456
+ function isValidRepository(input) {
457
+ try {
458
+ return typeof input === "string" && !!normalizeRepositoryUrl(input);
459
+ } catch {
460
+ return false;
461
+ }
462
+ }
463
+ function isValidCommit(input) {
464
+ try {
465
+ return typeof input === "string" && !!normalizeCommitUrl(input);
466
+ } catch {
467
+ return false;
468
+ }
469
+ }
470
+ function removeProject(state, id) {
471
+ const tasks = state.tasks.map((t) => ({ ...t, projectIds: t.projectIds.filter((p) => p !== id) })).filter((t) => t.projectIds.length);
472
+ const ids = new Set(tasks.map((t) => t.id));
473
+ return {
474
+ ...state,
475
+ projects: state.projects.filter((p) => p.id !== id),
476
+ tasks: tasks.map((t) => ({
477
+ ...t,
478
+ parentId: t.parentId && ids.has(t.parentId) ? t.parentId : null
479
+ })),
480
+ links: state.links.filter((e) => ids.has(e.source) && ids.has(e.target))
481
+ };
482
+ }
483
+
484
+ // src/agent-identity.ts
485
+ function agentBrand(name) {
486
+ if (/\bcodex\b/i.test(name)) return "codex";
487
+ if (/\bchatgpt\b/i.test(name)) return "chatgpt";
488
+ if (/\b(claude|clode|anthropic)\b/i.test(name)) return "claude";
489
+ return null;
490
+ }
491
+ var brandNames = {
492
+ chatgpt: "ChatGPT",
493
+ codex: "Codex",
494
+ claude: "Claude"
495
+ };
496
+ function agentQualifier(name) {
497
+ const match = /^\s*(?:claude|codex|chatgpt)\s*·\s*(.+?)\s*$/i.exec(name);
498
+ return match ? match[1].slice(0, 60) : "";
499
+ }
500
+ function agentDisplayName(name) {
501
+ const brand = agentBrand(name);
502
+ if (!brand) return name.trim().slice(0, 80) || "Agent";
503
+ const qualifier = agentQualifier(name);
504
+ return qualifier ? `${brandNames[brand]} \xB7 ${qualifier}` : brandNames[brand];
505
+ }
506
+
507
+ // mcp/identity.ts
508
+ var placeholders = /* @__PURE__ */ new Set([
509
+ "agent",
510
+ "mcp",
511
+ "mcp client",
512
+ "unknown",
513
+ "client"
514
+ ]);
515
+ function realName(candidate) {
516
+ const name = candidate?.trim();
517
+ return name && !placeholders.has(name.toLowerCase()) ? name : void 0;
518
+ }
519
+ function agentHandle(value) {
520
+ return (value ?? "").replace(/[^\p{L}\p{N} ._-]+/gu, " ").replace(/\s+/g, " ").trim().slice(0, 24).trim();
521
+ }
522
+ function qualified(name, handle) {
523
+ if (!handle || agentQualifier(name)) return name;
524
+ return `${name} \xB7 ${handle}`;
525
+ }
526
+ function mcpCallerName(identity) {
527
+ const clientInfoName = identity.clientInfoName?.trim();
528
+ const registeredClientName = identity.registeredClientName?.trim();
529
+ const userAgent = identity.userAgent?.trim();
530
+ const handle = agentHandle(identity.handle);
531
+ for (const candidate of [registeredClientName, clientInfoName, userAgent]) {
532
+ if (candidate && agentBrand(candidate))
533
+ return qualified(agentDisplayName(candidate), handle);
534
+ }
535
+ for (const candidate of [clientInfoName, registeredClientName]) {
536
+ const name = realName(candidate);
537
+ if (name) return qualified(agentDisplayName(name), handle);
538
+ }
539
+ return qualified(agentDisplayName(identity.fallback), handle);
540
+ }
541
+
542
+ // mcp/views.ts
543
+ function projectView(state, project) {
544
+ return {
545
+ ...project,
546
+ repositoryUrl: projectRepository(state, project),
547
+ folders: projectPaths(project)
548
+ };
549
+ }
550
+ function taskView(state, task, includeActivity = false, order = launchOrder(state)) {
551
+ const dependencyIds = state.links.filter((link) => link.target === task.id).map((link) => link.source);
552
+ const prerequisiteIds2 = /* @__PURE__ */ new Set([
553
+ ...dependencyIds,
554
+ ...task.parentId ? [task.parentId] : []
555
+ ]);
556
+ const unblockedIds = state.links.filter((link) => link.source === task.id).map((link) => link.target);
557
+ const view = {
558
+ id: task.id,
559
+ referenceId: task.referenceId,
560
+ name: task.name,
561
+ objective: task.objective,
562
+ status: task.status,
563
+ ...task.proposedBy ? { proposedBy: task.proposedBy } : {},
564
+ launchOrder: order.get(task.id) ?? null,
565
+ blocked: state.tasks.some(
566
+ (candidate) => prerequisiteIds2.has(candidate.id) && candidate.status !== "done"
567
+ ),
568
+ blockedBy: state.tasks.filter(
569
+ (candidate) => prerequisiteIds2.has(candidate.id) && candidate.status !== "done"
570
+ ).map((candidate) => ({
571
+ id: candidate.id,
572
+ referenceId: candidate.referenceId,
573
+ name: candidate.name,
574
+ status: candidate.status
575
+ })),
576
+ projects: state.projects.filter((project) => task.projectIds.includes(project.id)).map((project) => ({
577
+ id: project.id,
578
+ name: project.name,
579
+ repositoryUrl: projectRepository(state, project),
580
+ folders: projectPaths(project)
581
+ })),
582
+ labels: task.labels,
583
+ commitUrls: workspaceKind(state) === "everyday" ? [] : task.commitUrls,
584
+ parent: task.parentId ? state.tasks.find((candidate) => candidate.id === task.parentId)?.referenceId ?? task.parentId : null,
585
+ dependsOn: state.tasks.filter((candidate) => dependencyIds.includes(candidate.id)).map((candidate) => ({
586
+ id: candidate.id,
587
+ referenceId: candidate.referenceId,
588
+ name: candidate.name,
589
+ status: candidate.status
590
+ })),
591
+ unblocks: state.tasks.filter((candidate) => unblockedIds.includes(candidate.id)).map((candidate) => ({
592
+ id: candidate.id,
593
+ referenceId: candidate.referenceId,
594
+ name: candidate.name,
595
+ status: candidate.status
596
+ }))
597
+ };
598
+ return includeActivity ? { ...view, activity: task.activity.slice(0, 20) } : view;
599
+ }
600
+ function statusCounts(tasks) {
601
+ return {
602
+ proposed: tasks.filter((task) => task.status === "proposed").length,
603
+ todo: tasks.filter((task) => task.status === "todo").length,
604
+ doing: tasks.filter((task) => task.status === "doing").length,
605
+ done: tasks.filter((task) => task.status === "done").length
606
+ };
607
+ }
608
+
609
+ // mcp/workspace.ts
610
+ function lookup(values, identifier, kind) {
611
+ const needle = identifier.trim().toLowerCase();
612
+ const exact = values.filter(
613
+ (value) => value.id.toLowerCase() === needle || value.name.toLowerCase() === needle
614
+ );
615
+ if (exact.length === 1) return exact[0];
616
+ if (exact.length > 1) throw new Error(`Ambiguous ${kind}: ${identifier}`);
617
+ throw new Error(`Unknown ${kind}: ${identifier}`);
618
+ }
619
+ function findProject(state, identifier) {
620
+ return lookup(state.projects, identifier, "project");
621
+ }
622
+ function findLabel(state, identifier) {
623
+ return lookup(state.labels, identifier, "label");
624
+ }
625
+ function findTask(state, identifier) {
626
+ const needle = identifier.trim().toLowerCase();
627
+ const matches = state.tasks.filter(
628
+ (task) => task.id.toLowerCase() === needle || task.referenceId.toLowerCase() === needle || task.name.toLowerCase() === needle
629
+ );
630
+ if (matches.length === 1) return matches[0];
631
+ if (matches.length > 1) throw new Error(`Ambiguous task: ${identifier}`);
632
+ throw new Error(`Unknown task: ${identifier}`);
633
+ }
634
+ function canonicalLabels(state, names) {
635
+ return [
636
+ ...new Set(
637
+ names.map((name) => {
638
+ const label = state.labels.find(
639
+ (candidate) => candidate.name.toLowerCase() === name.trim().toLowerCase()
640
+ );
641
+ if (!label) throw new Error(`Unknown label: ${name}`);
642
+ return label.name;
643
+ })
644
+ )
645
+ ];
646
+ }
647
+ function appendAudit(task, text, agentName2) {
648
+ return {
649
+ ...task,
650
+ activity: [
651
+ event(text, { author: agentName2, authorType: "ai" }),
652
+ ...task.activity
653
+ ]
654
+ };
655
+ }
656
+ var columnWidth = 380;
657
+ var rowHeight = 300;
658
+ var gridColumns = 6;
659
+ function occupied(state, x, y) {
660
+ return state.tasks.some(
661
+ (task) => Math.abs(task.position.x - x) < columnWidth && Math.abs(task.position.y - y) < rowHeight
662
+ );
663
+ }
664
+ function placeTask(state, parent) {
665
+ if (parent) {
666
+ const x = parent.position.x + columnWidth;
667
+ let y = parent.position.y + rowHeight;
668
+ while (occupied(state, x, y)) y += rowHeight;
669
+ return { x, y };
670
+ }
671
+ if (!state.tasks.length) return { x: 0, y: 0 };
672
+ const originX = Math.min(...state.tasks.map((task) => task.position.x));
673
+ const originY = Math.min(...state.tasks.map((task) => task.position.y));
674
+ for (let row = 0; ; row++)
675
+ for (let column = 0; column < gridColumns; column++) {
676
+ const x = originX + column * columnWidth;
677
+ const y = originY + row * rowHeight;
678
+ if (!occupied(state, x, y)) return { x, y };
679
+ }
680
+ }
681
+ function createTask(state, input, agentName2) {
682
+ const name = input.name.trim();
683
+ if (!name) throw new Error("A task needs a name.");
684
+ if (!input.projects.length) throw new Error("Choose at least one project.");
685
+ const projectIds = [
686
+ ...new Set(input.projects.map((value) => findProject(state, value).id))
687
+ ];
688
+ const parent = input.parent ? findTask(state, input.parent) : void 0;
689
+ const labels = canonicalLabels(state, input.labels ?? []);
690
+ const position = placeTask(state, parent);
691
+ const task = {
692
+ id: uid(),
693
+ referenceId: taskReferenceId(state.tasks),
694
+ name,
695
+ projectIds,
696
+ commitUrls: [],
697
+ status: input.status ?? "todo",
698
+ objective: input.objective?.trim() ?? "",
699
+ labels,
700
+ parentId: parent?.id ?? null,
701
+ position,
702
+ activity: [
703
+ event("Task created through MCP", {
704
+ author: agentName2,
705
+ authorType: "ai"
706
+ })
707
+ ]
708
+ };
709
+ return { state: { ...state, tasks: [...state.tasks, task] }, task };
710
+ }
711
+ function patchTask(state, identifier, patch, agentName2) {
712
+ const task = findTask(state, identifier);
713
+ const resolved = Object.fromEntries(
714
+ Object.entries(patch).filter(([, value]) => value !== void 0)
715
+ );
716
+ if (patch.projectIds) {
717
+ resolved.projectIds = [
718
+ ...new Set(patch.projectIds.map((value) => findProject(state, value).id))
719
+ ];
720
+ }
721
+ if (patch.labels) resolved.labels = canonicalLabels(state, patch.labels);
722
+ let next = updateTask(state, task.id, resolved);
723
+ next = {
724
+ ...next,
725
+ tasks: next.tasks.map(
726
+ (candidate) => candidate.id === task.id ? appendAudit(candidate, "Task fields updated through MCP", agentName2) : candidate
727
+ )
728
+ };
729
+ return { state: next, task: findTask(next, task.id) };
730
+ }
731
+ function deleteTask(state, identifier) {
732
+ const task = findTask(state, identifier);
733
+ const next = removeTask(state, task.id);
734
+ const remaining = new Set(next.tasks.map((candidate) => candidate.id));
735
+ return {
736
+ state: next,
737
+ task,
738
+ removedTasks: state.tasks.filter(
739
+ (candidate) => !remaining.has(candidate.id)
740
+ )
741
+ };
742
+ }
743
+ function setTaskStatus(state, identifier, status, agentName2) {
744
+ const task = findTask(state, identifier);
745
+ if (task.status === status) return { state, task };
746
+ let next = updateTask(state, task.id, { status });
747
+ next = {
748
+ ...next,
749
+ tasks: next.tasks.map(
750
+ (candidate) => candidate.id === task.id ? appendAudit(
751
+ candidate,
752
+ `Status changed from ${task.status} to ${status}`,
753
+ agentName2
754
+ ) : candidate
755
+ )
756
+ };
757
+ return { state: next, task: findTask(next, task.id) };
758
+ }
759
+ function commitLinkFor(state, task, commit) {
760
+ if (!commit || !/^[0-9a-f]{40}$/i.test(commit)) return void 0;
761
+ if (workspaceKind(state) === "everyday") return void 0;
762
+ const project = state.projects.find(
763
+ (candidate) => task.projectIds.includes(candidate.id) && projectRepository(state, candidate)
764
+ );
765
+ if (!project) return void 0;
766
+ return normalizeCommitUrl(
767
+ `${projectRepository(state, project)}/commit/${commit.toLowerCase()}`
768
+ );
769
+ }
770
+ function withCommitLinked(task, commitUrl) {
771
+ if (!commitUrl || task.commitUrls.includes(commitUrl)) return task;
772
+ return { ...task, commitUrls: [...task.commitUrls, commitUrl] };
773
+ }
774
+ function addActivity(state, identifier, input, agentName2) {
775
+ const task = findTask(state, identifier);
776
+ const summary = input.summary.trim();
777
+ if (!summary) throw new Error("Activity summary is required.");
778
+ const entry = {
779
+ ...event(summary, { author: agentName2, authorType: "ai" }),
780
+ kind: input.kind,
781
+ ...input.commit !== void 0 ? { commit: input.commit } : {}
782
+ };
783
+ const commitUrl = commitLinkFor(state, task, input.commit);
784
+ const next = {
785
+ ...state,
786
+ tasks: state.tasks.map(
787
+ (candidate) => candidate.id === task.id ? withCommitLinked(
788
+ { ...candidate, activity: [entry, ...candidate.activity] },
789
+ commitUrl
790
+ ) : candidate
791
+ )
792
+ };
793
+ return {
794
+ state: next,
795
+ task: findTask(next, task.id),
796
+ ...commitUrl ? { commitUrl } : {}
797
+ };
798
+ }
799
+ function prerequisiteIds(state, task) {
800
+ return /* @__PURE__ */ new Set([
801
+ ...state.links.filter((link) => link.target === task.id).map((link) => link.source),
802
+ ...task.parentId ? [task.parentId] : []
803
+ ]);
804
+ }
805
+ function readyTasks(state) {
806
+ const order = launchOrder(state);
807
+ return state.tasks.filter(
808
+ (task) => task.status === "todo" && [...prerequisiteIds(state, task)].every(
809
+ (id) => state.tasks.find((candidate) => candidate.id === id)?.status === "done"
810
+ )
811
+ ).sort(
812
+ (left, right) => (order.get(left.id) ?? Number.MAX_SAFE_INTEGER) - (order.get(right.id) ?? Number.MAX_SAFE_INTEGER)
813
+ );
814
+ }
815
+ function taskBrief(state, identifier) {
816
+ const task = findTask(state, identifier);
817
+ const order = launchOrder(state);
818
+ const upstreamIds = prerequisiteIds(state, task);
819
+ return {
820
+ task: taskView(state, task, false, order),
821
+ repository: taskRepository(state, task),
822
+ projects: state.projects.filter((project) => task.projectIds.includes(project.id)).map((project) => ({
823
+ name: project.name,
824
+ repositoryUrl: projectRepository(state, project),
825
+ folders: projectPaths(project)
826
+ })),
827
+ upstream: state.tasks.filter((candidate) => upstreamIds.has(candidate.id)).sort(
828
+ (left, right) => (order.get(left.id) ?? Number.MAX_SAFE_INTEGER) - (order.get(right.id) ?? Number.MAX_SAFE_INTEGER)
829
+ ).map((candidate) => ({
830
+ referenceId: candidate.referenceId,
831
+ name: candidate.name,
832
+ status: candidate.status,
833
+ activity: candidate.activity.slice(0, 3).map((entry) => ({
834
+ text: entry.text,
835
+ author: entry.author,
836
+ kind: entry.kind,
837
+ commit: entry.commit
838
+ }))
839
+ }))
840
+ };
841
+ }
842
+ function proposeTask(state, input, agentName2) {
843
+ const source = findTask(state, input.from);
844
+ const name = input.name.trim();
845
+ if (!name) throw new Error("A task needs a name.");
846
+ const projectIdentifiers = input.projectIds ?? source.projectIds;
847
+ if (!projectIdentifiers.length)
848
+ throw new Error("Choose at least one project.");
849
+ const projectIds = [
850
+ ...new Set(
851
+ projectIdentifiers.map((identifier) => findProject(state, identifier).id)
852
+ )
853
+ ];
854
+ const proposedIds = new Set(
855
+ state.tasks.filter(
856
+ (task2) => task2.status === "proposed" && task2.proposedBy === agentName2
857
+ ).map((task2) => task2.id)
858
+ );
859
+ const proposalCount = state.links.filter(
860
+ (link) => link.source === source.id && proposedIds.has(link.target)
861
+ ).length;
862
+ if (proposalCount >= 3)
863
+ throw new Error(
864
+ "An agent can propose at most three tasks from one source."
865
+ );
866
+ const task = {
867
+ id: uid(),
868
+ referenceId: taskReferenceId(state.tasks),
869
+ name,
870
+ projectIds,
871
+ commitUrls: [],
872
+ status: "proposed",
873
+ proposedBy: agentName2,
874
+ objective: input.objective.trim(),
875
+ labels: [],
876
+ parentId: null,
877
+ position: { x: source.position.x, y: source.position.y + 270 },
878
+ activity: []
879
+ };
880
+ const next = {
881
+ ...state,
882
+ tasks: [
883
+ ...state.tasks.map(
884
+ (candidate) => candidate.id === source.id ? appendAudit(
885
+ candidate,
886
+ `Proposed ${task.referenceId} ${task.name}`,
887
+ agentName2
888
+ ) : candidate
889
+ ),
890
+ task
891
+ ],
892
+ links: [...state.links, { id: uid(), source: source.id, target: task.id }]
893
+ };
894
+ return { state: next, task };
895
+ }
896
+ function closeTask(state, input, agentName2) {
897
+ const task = findTask(state, input.task);
898
+ const summary = input.summary.trim();
899
+ if (!summary) throw new Error("A closing summary is required.");
900
+ const entry = {
901
+ ...event(summary, { author: agentName2, authorType: "ai" }),
902
+ ...input.outcome === "blocked" ? { kind: "blocker" } : {},
903
+ ...input.commit !== void 0 ? { commit: input.commit } : {},
904
+ ...input.usage !== void 0 ? { usage: input.usage } : {}
905
+ };
906
+ const status = input.outcome === "done" ? "done" : input.outcome === "abandoned" ? "todo" : task.status;
907
+ const commitUrl = commitLinkFor(state, task, input.commit);
908
+ const next = {
909
+ ...state,
910
+ tasks: state.tasks.map(
911
+ (candidate) => candidate.id === task.id ? withCommitLinked(
912
+ { ...candidate, status, activity: [entry, ...candidate.activity] },
913
+ commitUrl
914
+ ) : candidate
915
+ )
916
+ };
917
+ return {
918
+ state: next,
919
+ task: findTask(next, task.id),
920
+ ...commitUrl ? { commitUrl } : {}
921
+ };
922
+ }
923
+ function commitForProject(state, project, value) {
924
+ if (workspaceKind(state) === "everyday")
925
+ throw new Error("GitHub commits are only available in coding workspaces.");
926
+ const repositoryUrl = projectRepository(state, project);
927
+ if (!repositoryUrl) {
928
+ throw new Error(
929
+ `Project ${project.name} does not have a GitHub repository URL.`
930
+ );
931
+ }
932
+ const commitUrl = /^[a-fA-F0-9]{40}$/.test(value.trim()) ? normalizeCommitUrl(`${repositoryUrl}/commit/${value.trim()}`) : normalizeCommitUrl(value);
933
+ const expected = normalizeRepositoryUrl(repositoryUrl).toLowerCase();
934
+ const actual = commitUrl.slice(0, commitUrl.indexOf("/commit/")).toLowerCase();
935
+ if (actual !== expected) {
936
+ throw new Error(`Commit must belong to ${repositoryUrl}.`);
937
+ }
938
+ return commitUrl;
939
+ }
940
+ function linkCommit(state, taskIdentifier, projectIdentifier, commit, agentName2) {
941
+ const task = findTask(state, taskIdentifier);
942
+ const project = findProject(state, projectIdentifier);
943
+ if (!task.projectIds.includes(project.id)) {
944
+ throw new Error(`${task.name} does not belong to project ${project.name}.`);
945
+ }
946
+ const commitUrl = commitForProject(state, project, commit);
947
+ if (task.commitUrls.includes(commitUrl)) return { state, task, commitUrl };
948
+ let next = updateTask(state, task.id, {
949
+ commitUrls: [...task.commitUrls, commitUrl]
950
+ });
951
+ next = {
952
+ ...next,
953
+ tasks: next.tasks.map(
954
+ (candidate) => candidate.id === task.id ? appendAudit(
955
+ candidate,
956
+ `Linked GitHub commit ${commitUrl.split("/").at(-1)}`,
957
+ agentName2
958
+ ) : candidate
959
+ )
960
+ };
961
+ return { state: next, task: findTask(next, task.id), commitUrl };
962
+ }
963
+ function unlinkCommit(state, taskIdentifier, commit, agentName2) {
964
+ const task = findTask(state, taskIdentifier);
965
+ const needle = commit.trim().toLowerCase();
966
+ const commitUrl = task.commitUrls.find(
967
+ (url) => url.toLowerCase() === needle || url.split("/").at(-1)?.toLowerCase() === needle
968
+ );
969
+ if (!commitUrl) throw new Error("That commit is not linked to the task.");
970
+ let next = updateTask(state, task.id, {
971
+ commitUrls: task.commitUrls.filter((url) => url !== commitUrl)
972
+ });
973
+ next = {
974
+ ...next,
975
+ tasks: next.tasks.map(
976
+ (candidate) => candidate.id === task.id ? appendAudit(
977
+ candidate,
978
+ `Unlinked GitHub commit ${commitUrl.split("/").at(-1)}`,
979
+ agentName2
980
+ ) : candidate
981
+ )
982
+ };
983
+ return { state: next, task: findTask(next, task.id) };
984
+ }
985
+ function addDependency(state, taskIdentifier, dependencyIdentifier) {
986
+ const task = findTask(state, taskIdentifier);
987
+ const dependency = findTask(state, dependencyIdentifier);
988
+ if (!canConnect(state, dependency.id, task.id)) {
989
+ throw new Error(
990
+ "That dependency is duplicated, self-referential, or would create a cycle."
991
+ );
992
+ }
993
+ return {
994
+ state: {
995
+ ...state,
996
+ links: [
997
+ ...state.links,
998
+ { id: uid(), source: dependency.id, target: task.id }
999
+ ]
1000
+ },
1001
+ task,
1002
+ dependency
1003
+ };
1004
+ }
1005
+ function removeDependency(state, taskIdentifier, dependencyIdentifier) {
1006
+ const task = findTask(state, taskIdentifier);
1007
+ const dependency = findTask(state, dependencyIdentifier);
1008
+ const matches = state.links.filter(
1009
+ (link) => link.source === dependency.id && link.target === task.id
1010
+ );
1011
+ if (!matches.length) throw new Error("That dependency does not exist.");
1012
+ const ids = new Set(matches.map((link) => link.id));
1013
+ return {
1014
+ state: { ...state, links: state.links.filter((link) => !ids.has(link.id)) },
1015
+ task,
1016
+ dependency
1017
+ };
1018
+ }
1019
+ function createLabel(state, name) {
1020
+ const trimmed = name.trim();
1021
+ if (!trimmed) throw new Error("A label needs a name.");
1022
+ if (state.labels.some(
1023
+ (label) => label.name.toLowerCase() === trimmed.toLowerCase()
1024
+ )) {
1025
+ throw new Error(`Label already exists: ${trimmed}`);
1026
+ }
1027
+ return ensureLabels(state, [trimmed]);
1028
+ }
1029
+ function updateLabel(state, identifier, input) {
1030
+ const previous = findLabel(state, identifier);
1031
+ const name = input.name?.trim() ?? previous.name;
1032
+ if (!name) throw new Error("A label needs a name.");
1033
+ if (state.labels.some(
1034
+ (label2) => label2.id !== previous.id && label2.name.toLowerCase() === name.toLowerCase()
1035
+ )) {
1036
+ throw new Error(`Label already exists: ${name}`);
1037
+ }
1038
+ if (input.color && !/^#[0-9a-f]{6}$/i.test(input.color))
1039
+ throw new Error("Use a six-digit hex color.");
1040
+ const label = {
1041
+ ...previous,
1042
+ name,
1043
+ ...input.color ? { color: input.color, orb: makeOrb(input.color) } : {}
1044
+ };
1045
+ const next = saveLabel(state, label);
1046
+ return { state: next, label: findLabel(next, label.id) };
1047
+ }
1048
+ function deleteLabel2(state, identifier) {
1049
+ const label = findLabel(state, identifier);
1050
+ return { state: deleteLabel(state, label.id), label };
1051
+ }
1052
+ function createProject(state, input) {
1053
+ if (workspaceKind(state) === "everyday" && input.folders !== void 0)
1054
+ throw new Error("Project folders are only available in coding workspaces.");
1055
+ if (workspaceKind(state) === "everyday" && input.repositoryUrl)
1056
+ throw new Error("Everyday projects do not connect a GitHub repository.");
1057
+ const name = input.name.trim();
1058
+ if (!name) throw new Error("A project needs a name.");
1059
+ if (state.projects.some(
1060
+ (project2) => project2.name.toLowerCase() === name.toLowerCase()
1061
+ )) {
1062
+ throw new Error(`Project already exists: ${name}`);
1063
+ }
1064
+ const color = input.color ?? "#669df6";
1065
+ if (!/^#[0-9a-f]{6}$/i.test(color))
1066
+ throw new Error("Use a six-digit hex color.");
1067
+ const folders = normalizeProjectFolders(input.folders ?? []);
1068
+ const project = setProjectFolders(
1069
+ {
1070
+ id: uid(),
1071
+ name,
1072
+ color,
1073
+ orb: makeOrb(color),
1074
+ repositoryUrl: input.repositoryUrl ? normalizeRepositoryUrl(input.repositoryUrl) : ""
1075
+ },
1076
+ folders
1077
+ );
1078
+ return {
1079
+ state: { ...state, projects: [...state.projects, project] },
1080
+ project
1081
+ };
1082
+ }
1083
+ function updateProject(state, identifier, input) {
1084
+ if (workspaceKind(state) === "everyday" && input.folders !== void 0)
1085
+ throw new Error("Project folders are only available in coding workspaces.");
1086
+ if (workspaceKind(state) === "everyday" && input.repositoryUrl)
1087
+ throw new Error("Everyday projects do not connect a GitHub repository.");
1088
+ const previous = findProject(state, identifier);
1089
+ const name = input.name?.trim() ?? previous.name;
1090
+ if (!name) throw new Error("A project needs a name.");
1091
+ if (state.projects.some(
1092
+ (project2) => project2.id !== previous.id && project2.name.toLowerCase() === name.toLowerCase()
1093
+ )) {
1094
+ throw new Error(`Project already exists: ${name}`);
1095
+ }
1096
+ if (input.color && !/^#[0-9a-f]{6}$/i.test(input.color))
1097
+ throw new Error("Use a six-digit hex color.");
1098
+ let project = {
1099
+ ...previous,
1100
+ name,
1101
+ ...input.color ? { color: input.color, orb: makeOrb(input.color) } : {},
1102
+ ...input.repositoryUrl !== void 0 ? { repositoryUrl: normalizeRepositoryUrl(input.repositoryUrl) } : {}
1103
+ };
1104
+ if (input.folders !== void 0)
1105
+ project = setProjectFolders(
1106
+ project,
1107
+ normalizeProjectFolders(input.folders)
1108
+ );
1109
+ return {
1110
+ state: {
1111
+ ...state,
1112
+ projects: state.projects.map(
1113
+ (candidate) => candidate.id === project.id ? project : candidate
1114
+ )
1115
+ },
1116
+ project
1117
+ };
1118
+ }
1119
+ function deleteProject(state, identifier) {
1120
+ const project = findProject(state, identifier);
1121
+ const next = removeProject(state, project.id);
1122
+ const remaining = new Set(next.tasks.map((task) => task.id));
1123
+ return {
1124
+ state: next,
1125
+ project,
1126
+ removedTasks: state.tasks.filter((task) => !remaining.has(task.id))
1127
+ };
1128
+ }
1129
+ function configureProjectRepository(state, identifier, input) {
1130
+ if (workspaceKind(state) === "everyday")
1131
+ throw new Error(
1132
+ "Repository configuration is only available in coding workspaces."
1133
+ );
1134
+ const previous = findProject(state, identifier);
1135
+ const project = {
1136
+ ...previous,
1137
+ ...input.repositoryUrl !== void 0 ? { repositoryUrl: normalizeRepositoryUrl(input.repositoryUrl) } : {}
1138
+ };
1139
+ return {
1140
+ state: {
1141
+ ...state,
1142
+ projects: state.projects.map((p) => p.id === project.id ? project : p)
1143
+ },
1144
+ project
1145
+ };
1146
+ }
1147
+ function normalizeProjectFolders(values) {
1148
+ const folders = [];
1149
+ for (const value of values) {
1150
+ const folder = normalizeProjectPath(value);
1151
+ if (!folder) continue;
1152
+ if (folder.length > 200 || folder.split("/").includes(".."))
1153
+ throw new Error(
1154
+ "Folders are repository-relative paths of up to 200 characters."
1155
+ );
1156
+ if (!folders.includes(folder)) folders.push(folder);
1157
+ }
1158
+ return folders;
1159
+ }
1160
+ function setProjectFolders(project, folders) {
1161
+ if (folders.length > 50)
1162
+ throw new Error("A project can own at most 50 folders.");
1163
+ const { paths: _paths, ...withoutPaths } = project;
1164
+ return folders.length ? { ...withoutPaths, paths: folders } : withoutPaths;
1165
+ }
1166
+ function configureProjectFolders(state, identifier, input) {
1167
+ if (workspaceKind(state) === "everyday")
1168
+ throw new Error("Project folders are only available in coding workspaces.");
1169
+ const previous = findProject(state, identifier);
1170
+ const folders = normalizeProjectFolders(
1171
+ input.replace ?? previous.paths ?? []
1172
+ );
1173
+ for (const folder of normalizeProjectFolders(input.add ?? []))
1174
+ if (!folders.includes(folder)) folders.push(folder);
1175
+ const removed = new Set(normalizeProjectFolders(input.remove ?? []));
1176
+ const project = setProjectFolders(
1177
+ previous,
1178
+ folders.filter((folder) => !removed.has(folder))
1179
+ );
1180
+ return {
1181
+ state: {
1182
+ ...state,
1183
+ projects: state.projects.map(
1184
+ (candidate) => candidate.id === project.id ? project : candidate
1185
+ )
1186
+ },
1187
+ project
1188
+ };
1189
+ }
1190
+
1191
+ // mcp/create-server.ts
1192
+ var statusSchema = z.enum(["proposed", "todo", "doing", "done"]);
1193
+ var agentSchema = z.string().trim().max(200).optional().describe(
1194
+ "A short handle for this session so several agents in one workspace stay apart in the record, such as the first seven characters of $CLAUDE_CODE_SESSION_ID or the name of the worktree folder you are working in. Send the same handle on every call of this session."
1195
+ );
1196
+ function response(value) {
1197
+ return {
1198
+ content: [{ type: "text", text: JSON.stringify(value) }],
1199
+ structuredContent: value && typeof value === "object" ? value : { value }
1200
+ };
1201
+ }
1202
+ function failure(error) {
1203
+ return {
1204
+ content: [
1205
+ {
1206
+ type: "text",
1207
+ text: error instanceof Error ? error.message : String(error)
1208
+ }
1209
+ ],
1210
+ isError: true
1211
+ };
1212
+ }
1213
+ function run(handler, callerName) {
1214
+ return async (input) => {
1215
+ try {
1216
+ const handle = typeof input.agent === "string" ? input.agent : void 0;
1217
+ return await handler(input, callerName(handle));
1218
+ } catch (error) {
1219
+ return failure(error);
1220
+ }
1221
+ };
1222
+ }
1223
+ function createWirealServer(client) {
1224
+ const server2 = new McpServer(
1225
+ { name: "wireal", version: "0.2.0" },
1226
+ {
1227
+ instructions: "Wireal is the source of truth for tasks. Call set_active_workspace first when several workspaces exist. On every write pass agent with a short handle for this session, the first seven characters of $CLAUDE_CODE_SESSION_ID or the name of your worktree folder, and keep the same handle all session so several agents in one workspace stay apart. Before working on a task call get_recent_activity for it once. After a meaningful change, discovery, decision, verification or blocker call add_task_activity with a summary under 500 characters and the commit as the full 40-character SHA from git rev-parse HEAD, pushed or not. Check blocked and blockedBy before starting. In monorepos, project folders identify which repository paths belong to each project. When a runner started you for one task, do only that task. When you notice separate work, call propose_task instead of doing it. Finish with close_task."
1228
+ }
1229
+ );
1230
+ const callerName = (handle) => mcpCallerName({
1231
+ clientInfoName: server2.server.getClientVersion()?.name,
1232
+ registeredClientName: client.registeredClientName,
1233
+ userAgent: client.userAgent,
1234
+ handle,
1235
+ fallback: client.agentName
1236
+ });
1237
+ server2.registerTool(
1238
+ "list_workspaces",
1239
+ {
1240
+ title: "List workspaces",
1241
+ description: "List the signed-in user's Wireal workspaces and identify the active workspace.",
1242
+ inputSchema: z.object({}),
1243
+ annotations: { readOnlyHint: true }
1244
+ },
1245
+ run(async () => {
1246
+ const workspaces = await client.listWorkspaces();
1247
+ return response({
1248
+ count: workspaces.length,
1249
+ activeWorkspace: workspaces.find((workspace) => workspace.isActive) ?? null,
1250
+ workspaces
1251
+ });
1252
+ }, callerName)
1253
+ );
1254
+ server2.registerTool(
1255
+ "set_active_workspace",
1256
+ {
1257
+ title: "Set active workspace",
1258
+ description: "Switch subsequent Wireal tool calls to a workspace selected by ID or exact name.",
1259
+ inputSchema: z.object({
1260
+ workspace: z.string().min(1).describe("Workspace ID or exact workspace name")
1261
+ }),
1262
+ annotations: {
1263
+ readOnlyHint: false,
1264
+ destructiveHint: false,
1265
+ idempotentHint: true
1266
+ }
1267
+ },
1268
+ run(async ({ workspace }) => {
1269
+ const activeWorkspace = await client.setActiveWorkspace(workspace);
1270
+ return response({ activeWorkspace });
1271
+ }, callerName)
1272
+ );
1273
+ server2.registerTool(
1274
+ "workspace_summary",
1275
+ {
1276
+ title: "Workspace summary",
1277
+ description: "Summarize Wireal projects, labels, and task status counts.",
1278
+ inputSchema: z.object({}),
1279
+ annotations: { readOnlyHint: true }
1280
+ },
1281
+ run(async () => {
1282
+ const { workspace, revision } = await client.read();
1283
+ return response({
1284
+ revision,
1285
+ map: workspace.map.name,
1286
+ kind: workspaceKind(workspace),
1287
+ repositoryLayout: repositoryLayout(workspace),
1288
+ taskCount: workspace.tasks.length,
1289
+ launchOrderMeaning: "The sequence the board lays tasks out in: prerequisites first, then left to right and top to bottom. It says where a task sits, not what to pick up next \u2014 call ready_tasks for that.",
1290
+ statuses: statusCounts(workspace.tasks),
1291
+ projects: workspace.projects.map((project) => {
1292
+ const tasks = workspace.tasks.filter(
1293
+ (task) => task.projectIds.includes(project.id)
1294
+ );
1295
+ return {
1296
+ id: project.id,
1297
+ name: project.name,
1298
+ repositoryUrl: projectRepository(workspace, project),
1299
+ folders: projectPaths(project),
1300
+ taskCount: tasks.length,
1301
+ statuses: statusCounts(tasks)
1302
+ };
1303
+ }),
1304
+ labels: workspace.labels.map((label) => label.name)
1305
+ });
1306
+ }, callerName)
1307
+ );
1308
+ server2.registerTool(
1309
+ "list_projects",
1310
+ {
1311
+ title: "List projects",
1312
+ description: "List Wireal projects with repositories and per-status task counts.",
1313
+ inputSchema: z.object({}),
1314
+ annotations: { readOnlyHint: true }
1315
+ },
1316
+ run(async () => {
1317
+ const { workspace, revision } = await client.read();
1318
+ return response({
1319
+ revision,
1320
+ projects: workspace.projects.map((project) => {
1321
+ const tasks = workspace.tasks.filter(
1322
+ (task) => task.projectIds.includes(project.id)
1323
+ );
1324
+ return {
1325
+ ...projectView(workspace, project),
1326
+ taskCount: tasks.length,
1327
+ statuses: statusCounts(tasks)
1328
+ };
1329
+ })
1330
+ });
1331
+ }, callerName)
1332
+ );
1333
+ server2.registerTool(
1334
+ "list_tasks",
1335
+ {
1336
+ title: "List tasks",
1337
+ description: "List and search tasks in workspace launch order, optionally filtered by project and status.",
1338
+ inputSchema: z.object({
1339
+ project: z.string().min(1).optional().describe("Project ID or exact project name"),
1340
+ statuses: z.array(statusSchema).min(1).optional(),
1341
+ query: z.string().min(1).optional().describe("Case-insensitive name, reference, or objective search")
1342
+ }),
1343
+ annotations: { readOnlyHint: true }
1344
+ },
1345
+ run(async ({ project, statuses: statuses2, query }) => {
1346
+ const { workspace, revision } = await client.read();
1347
+ const selectedProject = project ? findProject(workspace, project) : void 0;
1348
+ const needle = query?.trim().toLowerCase();
1349
+ const order = launchOrder(workspace);
1350
+ const tasks = workspace.tasks.filter(
1351
+ (task) => (!selectedProject || task.projectIds.includes(selectedProject.id)) && (!statuses2 || statuses2.includes(task.status)) && (!needle || task.name.toLowerCase().includes(needle) || task.referenceId.toLowerCase().includes(needle) || task.objective.toLowerCase().includes(needle))
1352
+ ).sort(
1353
+ (left, right) => (order.get(left.id) ?? Number.MAX_SAFE_INTEGER) - (order.get(right.id) ?? Number.MAX_SAFE_INTEGER)
1354
+ );
1355
+ return response({
1356
+ revision,
1357
+ count: tasks.length,
1358
+ statuses: statusCounts(tasks),
1359
+ launchOrderMeaning: "The sequence the board lays tasks out in: prerequisites first, then left to right and top to bottom. It says where a task sits, not what to pick up next \u2014 call ready_tasks for that.",
1360
+ tasks: tasks.map((task) => {
1361
+ const view = taskView(workspace, task, false, order);
1362
+ return view.objective.length > 160 ? {
1363
+ ...view,
1364
+ objective: `${view.objective.slice(0, 160).trimEnd()}\u2026`
1365
+ } : view;
1366
+ })
1367
+ });
1368
+ }, callerName)
1369
+ );
1370
+ server2.registerTool(
1371
+ "get_task",
1372
+ {
1373
+ title: "Get task",
1374
+ description: "Read one task, including its workspace launch order, blockers, activity, projects, commits, parent, and dependencies.",
1375
+ inputSchema: z.object({
1376
+ task: z.string().min(1).describe(
1377
+ "Task ID, task number as shown in the app (for example 12), or exact task name"
1378
+ )
1379
+ }),
1380
+ annotations: { readOnlyHint: true }
1381
+ },
1382
+ run(async ({ task }) => {
1383
+ const { workspace, revision } = await client.read();
1384
+ return response({
1385
+ revision,
1386
+ task: taskView(workspace, findTask(workspace, task), true)
1387
+ });
1388
+ }, callerName)
1389
+ );
1390
+ server2.registerTool(
1391
+ "ready_tasks",
1392
+ {
1393
+ title: "Ready tasks",
1394
+ description: "List todo tasks whose parent and dependency prerequisites are done, in launch order.",
1395
+ inputSchema: z.object({}),
1396
+ annotations: { readOnlyHint: true }
1397
+ },
1398
+ run(async () => {
1399
+ const { workspace, revision } = await client.read();
1400
+ const order = launchOrder(workspace);
1401
+ const tasks = readyTasks(workspace);
1402
+ return response({
1403
+ revision,
1404
+ count: tasks.length,
1405
+ tasks: tasks.map((task) => taskView(workspace, task, false, order))
1406
+ });
1407
+ }, callerName)
1408
+ );
1409
+ server2.registerTool(
1410
+ "task_brief",
1411
+ {
1412
+ title: "Task brief",
1413
+ description: "Read one task with its repository, project folders, and token-lean upstream activity.",
1414
+ inputSchema: z.object({
1415
+ task: z.string().min(1).describe("Task ID, task number, or exact task name")
1416
+ }),
1417
+ annotations: { readOnlyHint: true }
1418
+ },
1419
+ run(async ({ task }) => {
1420
+ const { workspace, revision } = await client.read();
1421
+ return response({ revision, ...taskBrief(workspace, task) });
1422
+ }, callerName)
1423
+ );
1424
+ server2.registerTool(
1425
+ "propose_task",
1426
+ {
1427
+ title: "Propose task",
1428
+ description: "Propose separate follow-up work from the current task for a person to accept.",
1429
+ inputSchema: z.object({
1430
+ from: z.string().min(1).describe("Source task ID, number, or name"),
1431
+ name: z.string().trim().min(1),
1432
+ objective: z.string(),
1433
+ projectIds: z.array(z.string().min(1)).min(1).optional(),
1434
+ agent: agentSchema
1435
+ }),
1436
+ annotations: { readOnlyHint: false, destructiveHint: false }
1437
+ },
1438
+ run(async (input, agentName2) => {
1439
+ const result2 = await client.mutate((workspace2) => {
1440
+ const proposed = proposeTask(workspace2, input, agentName2);
1441
+ return { state: proposed.state, value: proposed.task.id };
1442
+ });
1443
+ const { workspace } = await client.read();
1444
+ return response({
1445
+ revision: result2.revision,
1446
+ task: taskView(workspace, findTask(workspace, result2.value), true)
1447
+ });
1448
+ }, callerName)
1449
+ );
1450
+ server2.registerTool(
1451
+ "create_task",
1452
+ {
1453
+ title: "Create task",
1454
+ description: "Create a task in one or more existing Wireal projects.",
1455
+ inputSchema: z.object({
1456
+ name: z.string().min(1),
1457
+ projects: z.array(z.string().min(1)).min(1).describe("Project IDs or exact names"),
1458
+ objective: z.string().optional(),
1459
+ status: statusSchema.optional(),
1460
+ labels: z.array(z.string().min(1)).optional().describe("Existing label names"),
1461
+ parent: z.string().min(1).optional().describe("Optional parent task"),
1462
+ agent: agentSchema
1463
+ }),
1464
+ annotations: { readOnlyHint: false, destructiveHint: false }
1465
+ },
1466
+ run(async (input, agentName2) => {
1467
+ const result2 = await client.mutate((workspace2) => {
1468
+ const created = createTask(workspace2, input, agentName2);
1469
+ return { state: created.state, value: created.task.id };
1470
+ });
1471
+ const { workspace } = await client.read();
1472
+ return response({
1473
+ revision: result2.revision,
1474
+ task: taskView(workspace, findTask(workspace, result2.value), true)
1475
+ });
1476
+ }, callerName)
1477
+ );
1478
+ server2.registerTool(
1479
+ "update_task",
1480
+ {
1481
+ title: "Update task",
1482
+ description: "Edit a task's name, project membership, or labels, but not status (use set_task_status) or objective (edited only by a person in the app; report findings with add_task_activity instead). A locked task rejects edits to its name, objective and parent until the lock is lifted in the app.",
1483
+ inputSchema: z.object({
1484
+ task: z.string().min(1),
1485
+ name: z.string().min(1).optional(),
1486
+ projects: z.array(z.string().min(1)).min(1).optional(),
1487
+ labels: z.array(z.string().min(1)).optional(),
1488
+ agent: agentSchema
1489
+ }).refine(
1490
+ ({ name, projects, labels }) => name !== void 0 || projects !== void 0 || labels !== void 0,
1491
+ { message: "Provide at least one field to update." }
1492
+ ),
1493
+ annotations: { readOnlyHint: false, destructiveHint: false }
1494
+ },
1495
+ run(async ({ task, name, projects, labels }, agentName2) => {
1496
+ const result2 = await client.mutate((workspace2) => {
1497
+ const changed = patchTask(
1498
+ workspace2,
1499
+ task,
1500
+ { name, projectIds: projects, labels },
1501
+ agentName2
1502
+ );
1503
+ return { state: changed.state, value: changed.task.id };
1504
+ });
1505
+ const { workspace } = await client.read();
1506
+ return response({
1507
+ revision: result2.revision,
1508
+ task: taskView(workspace, findTask(workspace, result2.value), true)
1509
+ });
1510
+ }, callerName)
1511
+ );
1512
+ server2.registerTool(
1513
+ "delete_task",
1514
+ {
1515
+ title: "Delete task",
1516
+ description: "Delete a task and its subtasks, removing every attached dependency link. A locked task cannot be deleted until the lock is lifted in the app.",
1517
+ inputSchema: z.object({
1518
+ task: z.string().min(1).describe(
1519
+ "Task ID, task number as shown in the app (for example 12), or exact task name"
1520
+ )
1521
+ }),
1522
+ annotations: { readOnlyHint: false, destructiveHint: true }
1523
+ },
1524
+ run(async ({ task }) => {
1525
+ const result2 = await client.mutate((workspace) => {
1526
+ const removed = deleteTask(workspace, task);
1527
+ return {
1528
+ state: removed.state,
1529
+ value: {
1530
+ task: {
1531
+ id: removed.task.id,
1532
+ referenceId: removed.task.referenceId,
1533
+ name: removed.task.name
1534
+ },
1535
+ removedTasks: removed.removedTasks.map((candidate) => ({
1536
+ id: candidate.id,
1537
+ referenceId: candidate.referenceId,
1538
+ name: candidate.name
1539
+ }))
1540
+ }
1541
+ };
1542
+ });
1543
+ return response({ revision: result2.revision, ...result2.value });
1544
+ }, callerName)
1545
+ );
1546
+ server2.registerTool(
1547
+ "set_task_status",
1548
+ {
1549
+ title: "Set task status",
1550
+ description: "Set a task status to proposed, todo, doing, or done and append an attributed audit activity.",
1551
+ inputSchema: z.object({
1552
+ task: z.string().min(1),
1553
+ status: statusSchema,
1554
+ agent: agentSchema
1555
+ }),
1556
+ annotations: { readOnlyHint: false, destructiveHint: false }
1557
+ },
1558
+ run(async ({ task, status }, agentName2) => {
1559
+ const result2 = await client.mutate((workspace2) => {
1560
+ const changed = setTaskStatus(workspace2, task, status, agentName2);
1561
+ return { state: changed.state, value: changed.task.id };
1562
+ });
1563
+ const { workspace } = await client.read();
1564
+ return response({
1565
+ revision: result2.revision,
1566
+ task: taskView(workspace, findTask(workspace, result2.value), true)
1567
+ });
1568
+ }, callerName)
1569
+ );
1570
+ server2.registerTool(
1571
+ "add_task_activity",
1572
+ {
1573
+ title: "Add task activity",
1574
+ description: "Append an AI-attributed activity entry to a task with a kind, a summary, and optionally the commit. A commit given as a full SHA is also linked to the task, which is what puts it on the commits canvas.",
1575
+ inputSchema: z.object({
1576
+ task: z.string().min(1),
1577
+ kind: z.enum(activityKinds),
1578
+ summary: z.string().trim().min(1).max(500),
1579
+ commit: z.string().regex(/^[0-9a-f]{40}$/).describe(
1580
+ "The full 40-character SHA from git rev-parse HEAD. An abbreviated SHA is rejected because only the full one can be linked to the task. The commit does not have to be pushed yet."
1581
+ ).optional(),
1582
+ agent: agentSchema
1583
+ }),
1584
+ annotations: { readOnlyHint: false, destructiveHint: false }
1585
+ },
1586
+ run(async ({ task, kind, summary, commit }, agentName2) => {
1587
+ const result2 = await client.mutate((workspace) => {
1588
+ const changed = addActivity(
1589
+ workspace,
1590
+ task,
1591
+ { kind, summary, commit },
1592
+ agentName2
1593
+ );
1594
+ return {
1595
+ state: changed.state,
1596
+ value: {
1597
+ taskId: changed.task.id,
1598
+ activityId: changed.task.activity[0].id,
1599
+ ...changed.commitUrl ? { commitUrl: changed.commitUrl } : {}
1600
+ }
1601
+ };
1602
+ });
1603
+ return response({ revision: result2.revision, ...result2.value });
1604
+ }, callerName)
1605
+ );
1606
+ server2.registerTool(
1607
+ "close_task",
1608
+ {
1609
+ title: "Close task",
1610
+ description: "Finish an agent run as done, blocked, or abandoned with one closing activity.",
1611
+ inputSchema: z.object({
1612
+ task: z.string().min(1),
1613
+ outcome: z.enum(["done", "blocked", "abandoned"]),
1614
+ summary: z.string().trim().min(1).max(500),
1615
+ commit: z.string().regex(/^[0-9a-f]{40}$/).describe(
1616
+ "The full 40-character SHA from git rev-parse HEAD. An abbreviated SHA is rejected because only the full one can be linked to the task. The commit does not have to be pushed yet."
1617
+ ).optional(),
1618
+ usage: z.object({
1619
+ costUsd: z.number().nonnegative().optional(),
1620
+ inputTokens: z.number().int().nonnegative().optional(),
1621
+ outputTokens: z.number().int().nonnegative().optional(),
1622
+ model: z.string().trim().min(1).optional(),
1623
+ durationMs: z.number().nonnegative().optional()
1624
+ }).optional(),
1625
+ agent: agentSchema
1626
+ }),
1627
+ annotations: { readOnlyHint: false, destructiveHint: false }
1628
+ },
1629
+ run(async (input, agentName2) => {
1630
+ const result2 = await client.mutate((workspace2) => {
1631
+ const closed = closeTask(workspace2, input, agentName2);
1632
+ return { state: closed.state, value: closed.task.id };
1633
+ });
1634
+ const { workspace } = await client.read();
1635
+ return response({
1636
+ revision: result2.revision,
1637
+ task: taskView(workspace, findTask(workspace, result2.value), true)
1638
+ });
1639
+ }, callerName)
1640
+ );
1641
+ server2.registerTool(
1642
+ "get_recent_activity",
1643
+ {
1644
+ title: "Get recent activity",
1645
+ description: "List the newest kind-tagged or user-written activity entries as plain text, grouped by task.",
1646
+ inputSchema: z.object({
1647
+ task: z.string().min(1).optional().describe(
1648
+ "Task ID, task number as shown in the app, or exact task name"
1649
+ ),
1650
+ limit: z.number().int().min(1).max(30).default(10)
1651
+ }),
1652
+ annotations: { readOnlyHint: true }
1653
+ },
1654
+ run(async ({ task, limit }) => {
1655
+ const { workspace } = await client.read();
1656
+ const tasks = task ? [findTask(workspace, task)] : workspace.tasks;
1657
+ const eligible = [];
1658
+ for (const candidate of tasks)
1659
+ for (const entry of candidate.activity)
1660
+ if (entry.kind !== void 0 || entry.authorType === "user")
1661
+ eligible.push({ task: candidate, entry });
1662
+ eligible.sort((a, b) => Date.parse(b.entry.at) - Date.parse(a.entry.at));
1663
+ const selected = eligible.slice(0, limit);
1664
+ const order = [];
1665
+ const groups = /* @__PURE__ */ new Map();
1666
+ for (const { task: owner, entry } of selected) {
1667
+ let group = groups.get(owner.id);
1668
+ if (!group) {
1669
+ group = { task: owner, entries: [] };
1670
+ groups.set(owner.id, group);
1671
+ order.push(owner.id);
1672
+ }
1673
+ group.entries.push(entry);
1674
+ }
1675
+ const blocks = order.map((taskId) => {
1676
+ const group = groups.get(taskId);
1677
+ const lines = task ? [] : [
1678
+ `#${group.task.referenceId} ${group.task.name} \xB7 ${group.task.status}`
1679
+ ];
1680
+ for (const entry of group.entries) {
1681
+ const at = new Date(entry.at).toISOString().slice(0, 16) + "Z";
1682
+ lines.push(
1683
+ `${entry.kind ?? "note"} \xB7 ${entry.author} \xB7 ${entry.commit ?? "uncommitted"} \xB7 ${at}`
1684
+ );
1685
+ lines.push(` ${entry.text}`);
1686
+ }
1687
+ return lines.join("\n");
1688
+ });
1689
+ return {
1690
+ content: [{ type: "text", text: blocks.join("\n\n") }]
1691
+ };
1692
+ }, callerName)
1693
+ );
1694
+ server2.registerTool(
1695
+ "link_github_commit",
1696
+ {
1697
+ title: "Link GitHub commit",
1698
+ description: "Link a full GitHub commit URL or 40-character SHA to a task, validating it against a project repository.",
1699
+ inputSchema: z.object({
1700
+ task: z.string().min(1),
1701
+ project: z.string().min(1),
1702
+ commit: z.string().min(1),
1703
+ agent: agentSchema
1704
+ }),
1705
+ annotations: { readOnlyHint: false, destructiveHint: false }
1706
+ },
1707
+ run(async ({ task, project, commit }, agentName2) => {
1708
+ const result2 = await client.mutate((workspace) => {
1709
+ const changed = linkCommit(workspace, task, project, commit, agentName2);
1710
+ return {
1711
+ state: changed.state,
1712
+ value: { taskId: changed.task.id, commitUrl: changed.commitUrl }
1713
+ };
1714
+ });
1715
+ return response({ revision: result2.revision, ...result2.value });
1716
+ }, callerName)
1717
+ );
1718
+ server2.registerTool(
1719
+ "unlink_github_commit",
1720
+ {
1721
+ title: "Unlink GitHub commit",
1722
+ description: "Remove a linked commit using its URL or full SHA.",
1723
+ inputSchema: z.object({
1724
+ task: z.string().min(1),
1725
+ commit: z.string().min(1),
1726
+ agent: agentSchema
1727
+ }),
1728
+ annotations: { readOnlyHint: false, destructiveHint: true }
1729
+ },
1730
+ run(async ({ task, commit }, agentName2) => {
1731
+ const result2 = await client.mutate((workspace) => {
1732
+ const changed = unlinkCommit(workspace, task, commit, agentName2);
1733
+ return { state: changed.state, value: changed.task.id };
1734
+ });
1735
+ return response({ revision: result2.revision, taskId: result2.value });
1736
+ }, callerName)
1737
+ );
1738
+ server2.registerTool(
1739
+ "add_task_dependency",
1740
+ {
1741
+ title: "Add task dependency",
1742
+ description: "Make a task depend on another task; duplicate links and cycles are rejected.",
1743
+ inputSchema: z.object({
1744
+ task: z.string().min(1).describe("The blocked task"),
1745
+ dependsOn: z.string().min(1).describe("The prerequisite task")
1746
+ }),
1747
+ annotations: { readOnlyHint: false, destructiveHint: false }
1748
+ },
1749
+ run(async ({ task, dependsOn }) => {
1750
+ const result2 = await client.mutate((workspace) => {
1751
+ const changed = addDependency(workspace, task, dependsOn);
1752
+ return {
1753
+ state: changed.state,
1754
+ value: {
1755
+ taskId: changed.task.id,
1756
+ dependencyId: changed.dependency.id
1757
+ }
1758
+ };
1759
+ });
1760
+ return response({ revision: result2.revision, ...result2.value });
1761
+ }, callerName)
1762
+ );
1763
+ server2.registerTool(
1764
+ "remove_task_dependency",
1765
+ {
1766
+ title: "Remove task dependency",
1767
+ description: "Remove a dependency link between two tasks.",
1768
+ inputSchema: z.object({
1769
+ task: z.string().min(1),
1770
+ dependsOn: z.string().min(1)
1771
+ }),
1772
+ annotations: { readOnlyHint: false, destructiveHint: true }
1773
+ },
1774
+ run(async ({ task, dependsOn }) => {
1775
+ const result2 = await client.mutate((workspace) => {
1776
+ const changed = removeDependency(workspace, task, dependsOn);
1777
+ return {
1778
+ state: changed.state,
1779
+ value: {
1780
+ taskId: changed.task.id,
1781
+ dependencyId: changed.dependency.id
1782
+ }
1783
+ };
1784
+ });
1785
+ return response({ revision: result2.revision, ...result2.value });
1786
+ }, callerName)
1787
+ );
1788
+ server2.registerTool(
1789
+ "create_label",
1790
+ {
1791
+ title: "Create label",
1792
+ description: "Create a reusable Wireal task label.",
1793
+ inputSchema: z.object({ name: z.string().min(1) }),
1794
+ annotations: { readOnlyHint: false, destructiveHint: false }
1795
+ },
1796
+ run(async ({ name }) => {
1797
+ const result2 = await client.mutate((workspace) => {
1798
+ const state = createLabel(workspace, name);
1799
+ const label = state.labels.find(
1800
+ (candidate) => candidate.name.toLowerCase() === name.trim().toLowerCase()
1801
+ );
1802
+ return { state, value: label };
1803
+ });
1804
+ return response({ revision: result2.revision, label: result2.value });
1805
+ }, callerName)
1806
+ );
1807
+ server2.registerTool(
1808
+ "update_label",
1809
+ {
1810
+ title: "Update label",
1811
+ description: "Rename a label everywhere it is used or change its six-digit hex color.",
1812
+ inputSchema: z.object({
1813
+ label: z.string().min(1).describe("Label ID or exact label name"),
1814
+ name: z.string().min(1).optional(),
1815
+ color: z.string().regex(/^#[0-9a-fA-F]{6}$/).optional()
1816
+ }).refine(
1817
+ ({ name, color }) => name !== void 0 || color !== void 0,
1818
+ {
1819
+ message: "Provide a name or color to update."
1820
+ }
1821
+ ),
1822
+ annotations: {
1823
+ readOnlyHint: false,
1824
+ destructiveHint: false,
1825
+ idempotentHint: true
1826
+ }
1827
+ },
1828
+ run(async ({ label, name, color }) => {
1829
+ const result2 = await client.mutate((workspace) => {
1830
+ const changed = updateLabel(workspace, label, { name, color });
1831
+ return { state: changed.state, value: changed.label };
1832
+ });
1833
+ return response({ revision: result2.revision, label: result2.value });
1834
+ }, callerName)
1835
+ );
1836
+ server2.registerTool(
1837
+ "delete_label",
1838
+ {
1839
+ title: "Delete label",
1840
+ description: "Delete a label and remove it from every task that uses it.",
1841
+ inputSchema: z.object({
1842
+ label: z.string().min(1).describe("Label ID or exact label name")
1843
+ }),
1844
+ annotations: { readOnlyHint: false, destructiveHint: true }
1845
+ },
1846
+ run(async ({ label }) => {
1847
+ const result2 = await client.mutate((workspace) => {
1848
+ const removed = deleteLabel2(workspace, label);
1849
+ return { state: removed.state, value: removed.label };
1850
+ });
1851
+ return response({ revision: result2.revision, label: result2.value });
1852
+ }, callerName)
1853
+ );
1854
+ server2.registerTool(
1855
+ "create_project",
1856
+ {
1857
+ title: "Create project",
1858
+ description: "Create a Wireal project, optionally connecting a GitHub repository and repository folders for coding projects.",
1859
+ inputSchema: z.object({
1860
+ name: z.string().min(1),
1861
+ repositoryUrl: z.string().url().optional(),
1862
+ folders: z.array(z.string()).optional(),
1863
+ color: z.string().regex(/^#[0-9a-fA-F]{6}$/).optional()
1864
+ }),
1865
+ annotations: { readOnlyHint: false, destructiveHint: false }
1866
+ },
1867
+ run(async (input) => {
1868
+ const result2 = await client.mutate((workspace) => {
1869
+ const changed = createProject(workspace, input);
1870
+ return {
1871
+ state: changed.state,
1872
+ value: projectView(changed.state, changed.project)
1873
+ };
1874
+ });
1875
+ return response({ revision: result2.revision, project: result2.value });
1876
+ }, callerName)
1877
+ );
1878
+ server2.registerTool(
1879
+ "update_project",
1880
+ {
1881
+ title: "Update project",
1882
+ description: "Update a project's name, color, GitHub repository, or repository folders.",
1883
+ inputSchema: z.object({
1884
+ project: z.string().min(1).describe("Project ID or exact name"),
1885
+ name: z.string().min(1).optional(),
1886
+ repositoryUrl: z.string().optional(),
1887
+ folders: z.array(z.string()).optional(),
1888
+ color: z.string().regex(/^#[0-9a-fA-F]{6}$/).optional()
1889
+ }).refine(
1890
+ ({ name, repositoryUrl, color, folders }) => name !== void 0 || repositoryUrl !== void 0 || color !== void 0 || folders !== void 0,
1891
+ { message: "Provide at least one field to update." }
1892
+ ),
1893
+ annotations: {
1894
+ readOnlyHint: false,
1895
+ destructiveHint: false,
1896
+ idempotentHint: true
1897
+ }
1898
+ },
1899
+ run(async ({ project, name, repositoryUrl, color, folders }) => {
1900
+ const result2 = await client.mutate((workspace) => {
1901
+ const changed = updateProject(workspace, project, {
1902
+ name,
1903
+ repositoryUrl,
1904
+ color,
1905
+ folders
1906
+ });
1907
+ return {
1908
+ state: changed.state,
1909
+ value: projectView(changed.state, changed.project)
1910
+ };
1911
+ });
1912
+ return response({ revision: result2.revision, project: result2.value });
1913
+ }, callerName)
1914
+ );
1915
+ server2.registerTool(
1916
+ "delete_project",
1917
+ {
1918
+ title: "Delete project",
1919
+ description: "Delete a project, keeping shared tasks in their other projects and deleting tasks that belong only to it.",
1920
+ inputSchema: z.object({
1921
+ project: z.string().min(1).describe("Project ID or exact name")
1922
+ }),
1923
+ annotations: { readOnlyHint: false, destructiveHint: true }
1924
+ },
1925
+ run(async ({ project }) => {
1926
+ const result2 = await client.mutate((workspace) => {
1927
+ const removed = deleteProject(workspace, project);
1928
+ return {
1929
+ state: removed.state,
1930
+ value: {
1931
+ project: { id: removed.project.id, name: removed.project.name },
1932
+ removedTasks: removed.removedTasks.map((task) => ({
1933
+ id: task.id,
1934
+ referenceId: task.referenceId,
1935
+ name: task.name
1936
+ }))
1937
+ }
1938
+ };
1939
+ });
1940
+ return response({ revision: result2.revision, ...result2.value });
1941
+ }, callerName)
1942
+ );
1943
+ server2.registerTool(
1944
+ "configure_project_folders",
1945
+ {
1946
+ title: "Configure project folders",
1947
+ description: "Configure repository-relative folders such as apps/web or services/api. In a monorepo, folders say which part of the shared repository belongs to the project, and commits touching those folders are attributed to it.",
1948
+ inputSchema: z.object({
1949
+ project: z.string().min(1).describe("Project ID or exact name"),
1950
+ add: z.array(z.string()).optional(),
1951
+ remove: z.array(z.string()).optional(),
1952
+ replace: z.array(z.string()).optional()
1953
+ }).refine(
1954
+ ({ add, remove, replace }) => add !== void 0 || remove !== void 0 || replace !== void 0,
1955
+ { message: "Provide add, remove, or replace." }
1956
+ ),
1957
+ annotations: {
1958
+ readOnlyHint: false,
1959
+ destructiveHint: false,
1960
+ idempotentHint: true
1961
+ }
1962
+ },
1963
+ run(async ({ project, add, remove, replace }) => {
1964
+ const result2 = await client.mutate((workspace) => {
1965
+ const changed = configureProjectFolders(workspace, project, {
1966
+ add,
1967
+ remove,
1968
+ replace
1969
+ });
1970
+ return {
1971
+ state: changed.state,
1972
+ value: projectView(changed.state, changed.project)
1973
+ };
1974
+ });
1975
+ return response({ revision: result2.revision, project: result2.value });
1976
+ }, callerName)
1977
+ );
1978
+ server2.registerTool(
1979
+ "configure_project_repository",
1980
+ {
1981
+ description: "Set a project's GitHub repository.",
1982
+ inputSchema: z.object({
1983
+ project: z.string().min(1),
1984
+ repositoryUrl: z.string()
1985
+ }),
1986
+ annotations: {
1987
+ readOnlyHint: false,
1988
+ destructiveHint: false,
1989
+ idempotentHint: true
1990
+ }
1991
+ },
1992
+ run(async ({ project, repositoryUrl }) => {
1993
+ const result2 = await client.mutate((workspace) => {
1994
+ const changed = configureProjectRepository(workspace, project, {
1995
+ repositoryUrl
1996
+ });
1997
+ return {
1998
+ state: changed.state,
1999
+ value: projectView(changed.state, changed.project)
2000
+ };
2001
+ });
2002
+ return response({ revision: result2.revision, project: result2.value });
2003
+ }, callerName)
2004
+ );
2005
+ return server2;
2006
+ }
2007
+
2008
+ // src/api-client.ts
2009
+ var ApiError = class extends Error {
2010
+ constructor(message, status = 0, errorCode) {
2011
+ super(message);
2012
+ this.status = status;
2013
+ this.errorCode = errorCode;
2014
+ this.name = "ApiError";
2015
+ this.code = String(status);
2016
+ }
2017
+ status;
2018
+ errorCode;
2019
+ code;
2020
+ };
2021
+ async function result(action) {
2022
+ try {
2023
+ return { data: await action(), error: null };
2024
+ } catch (error) {
2025
+ return {
2026
+ data: null,
2027
+ error: error instanceof ApiError ? error : new ApiError(
2028
+ error instanceof Error ? error.message : String(error)
2029
+ )
2030
+ };
2031
+ }
2032
+ }
2033
+ async function readResponse(response2) {
2034
+ const body = response2.status === 204 ? null : await response2.json().catch(() => null);
2035
+ if (!response2.ok)
2036
+ throw new ApiError(
2037
+ body?.error?.message ?? body?.error_description ?? body?.title ?? body?.message ?? `Request failed (${response2.status}).`,
2038
+ response2.status,
2039
+ typeof body?.error === "string" ? body.error : void 0
2040
+ );
2041
+ return body;
2042
+ }
2043
+ function jsonBody(value) {
2044
+ return {
2045
+ method: "POST",
2046
+ headers: { "Content-Type": "application/json" },
2047
+ body: JSON.stringify(value)
2048
+ };
2049
+ }
2050
+ var DataClient = class {
2051
+ constructor(request) {
2052
+ this.request = request;
2053
+ }
2054
+ request;
2055
+ from(table) {
2056
+ return new DataQuery(this.request, table);
2057
+ }
2058
+ async rpc(name, args = {}) {
2059
+ try {
2060
+ return await this.request(
2061
+ `/api/commands/${encodeURIComponent(name)}`,
2062
+ jsonBody(args)
2063
+ );
2064
+ } catch (error) {
2065
+ return result(() => Promise.reject(error));
2066
+ }
2067
+ }
2068
+ };
2069
+ var DataQuery = class {
2070
+ constructor(request, table) {
2071
+ this.request = request;
2072
+ this.query = {
2073
+ table,
2074
+ operation: "select",
2075
+ columns: "*",
2076
+ filters: [],
2077
+ any: [],
2078
+ order: [],
2079
+ offset: 0,
2080
+ limit: 1e3
2081
+ };
2082
+ }
2083
+ request;
2084
+ query;
2085
+ select(columns = "*", options) {
2086
+ this.query.columns = columns;
2087
+ this.query.count = options?.count === "exact";
2088
+ this.query.head = options?.head;
2089
+ return this;
2090
+ }
2091
+ update(values) {
2092
+ this.query.operation = "update";
2093
+ this.query.values = values;
2094
+ return this;
2095
+ }
2096
+ insert(values) {
2097
+ this.query.operation = "insert";
2098
+ this.query.values = values;
2099
+ return this;
2100
+ }
2101
+ delete() {
2102
+ this.query.operation = "delete";
2103
+ return this;
2104
+ }
2105
+ eq(field, value) {
2106
+ this.query.filters.push({ field, op: "eq", value });
2107
+ return this;
2108
+ }
2109
+ neq(field, value) {
2110
+ this.query.filters.push({ field, op: "neq", value });
2111
+ return this;
2112
+ }
2113
+ in(field, value) {
2114
+ this.query.filters.push({ field, op: "in", value });
2115
+ return this;
2116
+ }
2117
+ ilike(field, value) {
2118
+ this.query.filters.push({ field, op: "ilike", value });
2119
+ return this;
2120
+ }
2121
+ /** Converts the app's fixed OR patterns into structured filters, never SQL. */
2122
+ or(pattern) {
2123
+ const parts = pattern.match(/[^,]+\.in\.\([^)]*\)|[^,]+/g) ?? [];
2124
+ this.query.any.push(
2125
+ parts.map((part) => {
2126
+ const match = part.match(/^(\w+)\.(in|ilike|eq)\.(.*)$/);
2127
+ if (!match) throw new ApiError("Invalid query filter.");
2128
+ return {
2129
+ field: match[1],
2130
+ op: match[2],
2131
+ value: match[2] === "in" ? match[3].slice(1, -1).split(",") : match[3]
2132
+ };
2133
+ })
2134
+ );
2135
+ return this;
2136
+ }
2137
+ order(field, options) {
2138
+ this.query.order.push({ field, ascending: options?.ascending !== false });
2139
+ return this;
2140
+ }
2141
+ range(from, to) {
2142
+ this.query.offset = from;
2143
+ this.query.limit = to - from + 1;
2144
+ return this;
2145
+ }
2146
+ limit(limit) {
2147
+ this.query.limit = limit;
2148
+ return this;
2149
+ }
2150
+ async maybeSingle() {
2151
+ this.query.single = "optional";
2152
+ return this.execute();
2153
+ }
2154
+ async single() {
2155
+ this.query.single = "required";
2156
+ return this.execute();
2157
+ }
2158
+ async execute() {
2159
+ try {
2160
+ return await this.request("/api/query", jsonBody(this.query));
2161
+ } catch (error) {
2162
+ return result(() => Promise.reject(error));
2163
+ }
2164
+ }
2165
+ then(fulfilled, rejected) {
2166
+ return this.execute().then(fulfilled, rejected);
2167
+ }
2168
+ };
2169
+
2170
+ // mcp/client.ts
2171
+ var WirealClient = class {
2172
+ constructor(config) {
2173
+ this.config = config;
2174
+ if (!config.url)
2175
+ throw new Error(
2176
+ "Set WIREAL_API_URL or VITE_API_URL before starting MCP."
2177
+ );
2178
+ const url = new URL(config.url);
2179
+ if (url.protocol !== "https:" || url.pathname !== "/" || url.search || url.hash || url.username || url.password)
2180
+ throw new Error("The API URL must be an HTTPS origin.");
2181
+ this.agentName = config.agentName;
2182
+ this.workspaceId = config.workspaceId?.trim() || void 0;
2183
+ this.registeredClientName = config.registeredClientName;
2184
+ this.userAgent = config.userAgent;
2185
+ this.accessToken = config.accessToken ?? "";
2186
+ this.refreshToken = config.refreshToken ?? "";
2187
+ this.dataClient = new DataClient(async (path, init) => {
2188
+ await this.authenticate();
2189
+ const send = () => fetch(config.url.replace(/\/$/, "") + path, {
2190
+ ...init,
2191
+ headers: {
2192
+ ...Object.fromEntries(new Headers(init?.headers)),
2193
+ Authorization: "Bearer " + this.accessToken
2194
+ }
2195
+ });
2196
+ let response2 = await send();
2197
+ for (let attempt = 0; attempt < 2; attempt++) {
2198
+ if (response2.status !== 401 || !this.refreshToken) break;
2199
+ await this.refresh();
2200
+ response2 = await send();
2201
+ }
2202
+ return readResponse(response2);
2203
+ });
2204
+ }
2205
+ config;
2206
+ agentName;
2207
+ registeredClientName;
2208
+ userAgent;
2209
+ workspaceId;
2210
+ dataClient;
2211
+ accessToken;
2212
+ refreshToken;
2213
+ refreshing;
2214
+ async authenticate() {
2215
+ if (!this.accessToken && this.refreshToken) await this.refresh();
2216
+ if (!this.accessToken)
2217
+ throw new Error(
2218
+ "Wireal MCP is not authenticated. Reconnect your client to the hosted Wireal MCP endpoint."
2219
+ );
2220
+ }
2221
+ refresh() {
2222
+ this.refreshing ??= (async () => {
2223
+ if (this.config.refreshSession) {
2224
+ const session2 = await this.config.refreshSession();
2225
+ this.accessToken = session2.accessToken;
2226
+ this.refreshToken = session2.refreshToken;
2227
+ return;
2228
+ }
2229
+ if (!this.config.clientId)
2230
+ throw new Error(
2231
+ "MCP client registration is missing. Reconnect your client to the hosted Wireal MCP endpoint."
2232
+ );
2233
+ const body = new URLSearchParams({
2234
+ grant_type: "refresh_token",
2235
+ refresh_token: this.refreshToken,
2236
+ client_id: this.config.clientId,
2237
+ resource: this.config.resource ?? "https://mcp.wireal.co"
2238
+ });
2239
+ const session = await readResponse(
2240
+ await fetch(this.config.url.replace(/\/$/, "") + "/oauth/token", {
2241
+ method: "POST",
2242
+ body,
2243
+ signal: AbortSignal.timeout(15e3)
2244
+ })
2245
+ );
2246
+ this.accessToken = session.access_token;
2247
+ this.refreshToken = session.refresh_token;
2248
+ this.config.onSession?.(this.accessToken, this.refreshToken);
2249
+ })().finally(() => {
2250
+ this.refreshing = void 0;
2251
+ });
2252
+ return this.refreshing;
2253
+ }
2254
+ async readCurrent() {
2255
+ await this.authenticate();
2256
+ const rows = this.dataClient.from("workspaces").select("id,data,revision");
2257
+ const { data, error } = await (this.workspaceId ? rows.eq("id", this.workspaceId) : rows.eq("is_active", true)).limit(1).maybeSingle();
2258
+ if (error) throw error;
2259
+ if (!data)
2260
+ throw new Error(
2261
+ this.workspaceId ? `Workspace ${this.workspaceId} is not available to this user.` : "No Wireal cloud workspace exists for this user."
2262
+ );
2263
+ return {
2264
+ workspace: parseWorkspace(JSON.stringify(data.data)),
2265
+ revision: Number(data.revision),
2266
+ workspaceId: data.id
2267
+ };
2268
+ }
2269
+ async command(name, args) {
2270
+ await this.authenticate();
2271
+ const { data, error } = await this.dataClient.rpc(name, args);
2272
+ if (error) throw error;
2273
+ return data;
2274
+ }
2275
+ async read() {
2276
+ return this.readCurrent();
2277
+ }
2278
+ async listWorkspaces() {
2279
+ await this.authenticate();
2280
+ const { data, error } = await this.dataClient.from("workspaces").select("id,data,revision,is_active").order("created_at", { ascending: true });
2281
+ if (error) throw error;
2282
+ return (data ?? []).map((row) => {
2283
+ const workspace = parseWorkspace(JSON.stringify(row.data));
2284
+ return {
2285
+ id: row.id,
2286
+ name: workspace.map.name,
2287
+ kind: workspaceKind(workspace),
2288
+ repositoryUrl: workspaceKind(workspace) === "everyday" ? "" : workspace.map.repositoryUrl,
2289
+ revision: Number(row.revision),
2290
+ isActive: this.workspaceId ? row.id === this.workspaceId : row.is_active
2291
+ };
2292
+ });
2293
+ }
2294
+ async setActiveWorkspace(identifier) {
2295
+ if (this.workspaceId) {
2296
+ const pinned = await this.readCurrent();
2297
+ throw new Error(
2298
+ `This session is pinned to workspace ${pinned.workspace.map.name}.`
2299
+ );
2300
+ }
2301
+ const workspaces = await this.listWorkspaces();
2302
+ const needle = identifier.trim().toLowerCase();
2303
+ const matches = workspaces.filter(
2304
+ (workspace2) => workspace2.id.toLowerCase() === needle || workspace2.name.toLowerCase() === needle
2305
+ );
2306
+ if (matches.length > 1)
2307
+ throw new Error(`Ambiguous workspace: ${identifier}. Use its ID.`);
2308
+ const workspace = matches[0];
2309
+ if (!workspace) throw new Error(`Unknown workspace: ${identifier}`);
2310
+ if (workspace.isActive) return workspace;
2311
+ const { data, error } = await this.dataClient.rpc("set_active_workspace", {
2312
+ target_workspace_id: workspace.id
2313
+ });
2314
+ if (error) throw error;
2315
+ if (!data)
2316
+ throw new Error(`Workspace could not be activated: ${identifier}`);
2317
+ return { ...workspace, isActive: true };
2318
+ }
2319
+ async mutate(change) {
2320
+ for (let attempt = 0; attempt < 3; attempt += 1) {
2321
+ const current = await this.readCurrent();
2322
+ const changed = change(current.workspace);
2323
+ const validated = parseWorkspace(JSON.stringify(changed.state));
2324
+ if (JSON.stringify(validated) === JSON.stringify(current.workspace)) {
2325
+ return { value: changed.value, revision: current.revision };
2326
+ }
2327
+ const { data, error } = await this.dataClient.from("workspaces").update({ data: validated, updated_at: (/* @__PURE__ */ new Date()).toISOString() }).eq("id", current.workspaceId).eq("revision", current.revision).select("revision").maybeSingle();
2328
+ if (error) throw error;
2329
+ if (data)
2330
+ return { value: changed.value, revision: Number(data.revision) };
2331
+ }
2332
+ throw new Error("The workspace changed concurrently. Retry the operation.");
2333
+ }
2334
+ };
2335
+
2336
+ // runner/session.ts
2337
+ import {
2338
+ chmodSync,
2339
+ closeSync,
2340
+ mkdirSync,
2341
+ openSync,
2342
+ readFileSync,
2343
+ rmSync,
2344
+ statSync,
2345
+ writeFileSync
2346
+ } from "node:fs";
2347
+ import { homedir, hostname } from "node:os";
2348
+ import { dirname, join, posix, resolve, win32 } from "node:path";
2349
+ import { createInterface } from "node:readline/promises";
2350
+ function apiUrl(env = process.env) {
2351
+ return (env.WIREAL_API_URL || "https://api.wireal.co").replace(/\/$/, "");
2352
+ }
2353
+ function mcpResource(env = process.env) {
2354
+ return env.WIREAL_MCP_RESOURCE || "https://mcp.wireal.co";
2355
+ }
2356
+ function configDirectory(platform = process.platform, env = process.env, home = homedir()) {
2357
+ const under = platform === "win32" ? win32.join : posix.join;
2358
+ if (platform === "darwin")
2359
+ return under(home, "Library", "Application Support", "wireal");
2360
+ if (platform === "win32")
2361
+ return under(env.APPDATA || under(home, "AppData", "Roaming"), "wireal");
2362
+ return under(env.XDG_CONFIG_HOME || under(home, ".config"), "wireal");
2363
+ }
2364
+ function sessionPath(platform = process.platform, env = process.env, home = homedir()) {
2365
+ return (platform === "win32" ? win32.join : posix.join)(
2366
+ configDirectory(platform, env, home),
2367
+ "session.json"
2368
+ );
2369
+ }
2370
+ function readSession(path = sessionPath()) {
2371
+ try {
2372
+ const parsed = JSON.parse(readFileSync(path, "utf8"));
2373
+ if (!parsed.accessToken && !parsed.refreshToken) return null;
2374
+ return {
2375
+ apiUrl: parsed.apiUrl || apiUrl(),
2376
+ clientId: parsed.clientId ?? "",
2377
+ accessToken: parsed.accessToken ?? "",
2378
+ refreshToken: parsed.refreshToken ?? "",
2379
+ savedAt: parsed.savedAt ?? ""
2380
+ };
2381
+ } catch {
2382
+ return null;
2383
+ }
2384
+ }
2385
+ function writeSession(session, path = sessionPath()) {
2386
+ mkdirSync(dirname(path), { recursive: true, mode: 448 });
2387
+ writeFileSync(path, JSON.stringify(session, null, 2), { mode: 384 });
2388
+ try {
2389
+ chmodSync(path, 384);
2390
+ } catch {
2391
+ return;
2392
+ }
2393
+ }
2394
+ var lockStaleMs = 3e4;
2395
+ var lockBusyNote = "Another wireal-run on this machine is refreshing the saved session.";
2396
+ async function withSessionLock(work, path = sessionPath(), waitMs = 6e4) {
2397
+ const lock = `${path}.lock`;
2398
+ mkdirSync(dirname(path), { recursive: true, mode: 448 });
2399
+ const until = Date.now() + waitMs;
2400
+ let held;
2401
+ while (held === void 0 && Date.now() < until) {
2402
+ try {
2403
+ held = openSync(lock, "wx", 384);
2404
+ } catch {
2405
+ const stale = statSync(lock, { throwIfNoEntry: false });
2406
+ if (stale && Date.now() - stale.mtimeMs > lockStaleMs) {
2407
+ rmSync(lock, { force: true });
2408
+ continue;
2409
+ }
2410
+ await new Promise((wake) => setTimeout(wake, 40));
2411
+ }
2412
+ }
2413
+ if (held === void 0) throw new Error(lockBusyNote);
2414
+ try {
2415
+ return await work();
2416
+ } finally {
2417
+ closeSync(held);
2418
+ rmSync(lock, { force: true });
2419
+ }
2420
+ }
2421
+
2422
+ // runner/api.ts
2423
+ var refreshBefore = 6e4;
2424
+ var signedOutNote = "This machine's Wireal session was revoked. Run `wireal-run login` again.";
2425
+ function revoked(error) {
2426
+ return /expired or revoked|invalid_grant|invalid refresh token/i.test(
2427
+ error instanceof Error ? error.message : String(error)
2428
+ );
2429
+ }
2430
+ function tokenExpiry(token) {
2431
+ const part = token.split(".")[1];
2432
+ if (!part) return 0;
2433
+ try {
2434
+ const body = JSON.parse(
2435
+ Buffer.from(part, "base64url").toString("utf8")
2436
+ );
2437
+ return typeof body.exp === "number" && Number.isFinite(body.exp) ? body.exp * 1e3 : 0;
2438
+ } catch {
2439
+ return 0;
2440
+ }
2441
+ }
2442
+ var RunnerSession = class {
2443
+ constructor(session, path = sessionPath()) {
2444
+ this.session = session;
2445
+ this.path = path;
2446
+ this.origin = (session.apiUrl || apiUrl()).replace(/\/$/, "");
2447
+ this.accessToken = session.accessToken;
2448
+ this.refreshToken = session.refreshToken;
2449
+ this.expiresAt = tokenExpiry(session.accessToken);
2450
+ this.data = new DataClient(async (path2, init) => {
2451
+ await this.tokens().catch(() => void 0);
2452
+ const send = () => fetch(this.origin + path2, {
2453
+ ...init,
2454
+ headers: {
2455
+ ...Object.fromEntries(new Headers(init?.headers)),
2456
+ Authorization: "Bearer " + this.accessToken
2457
+ }
2458
+ });
2459
+ let response2 = await send();
2460
+ for (let attempt = 0; attempt < 2; attempt++) {
2461
+ if (response2.status !== 401 || !this.refreshToken) break;
2462
+ await this.refresh();
2463
+ response2 = await send();
2464
+ }
2465
+ return readResponse(response2);
2466
+ });
2467
+ }
2468
+ session;
2469
+ path;
2470
+ origin;
2471
+ data;
2472
+ accessToken;
2473
+ refreshToken;
2474
+ expiresAt;
2475
+ rotating;
2476
+ fresh(now = Date.now()) {
2477
+ if (!this.accessToken) return false;
2478
+ return this.expiresAt === 0 || this.expiresAt - now > refreshBefore;
2479
+ }
2480
+ tokens() {
2481
+ if (this.fresh())
2482
+ return Promise.resolve({
2483
+ accessToken: this.accessToken,
2484
+ refreshToken: this.refreshToken
2485
+ });
2486
+ return this.refresh();
2487
+ }
2488
+ adopt(stored, sent) {
2489
+ if (!stored?.refreshToken || stored.refreshToken === sent) return void 0;
2490
+ this.accessToken = stored.accessToken;
2491
+ this.refreshToken = stored.refreshToken;
2492
+ this.expiresAt = tokenExpiry(stored.accessToken);
2493
+ return { accessToken: this.accessToken, refreshToken: this.refreshToken };
2494
+ }
2495
+ refresh() {
2496
+ this.rotating ??= withSessionLock(async () => {
2497
+ const sent = this.refreshToken;
2498
+ const taken = this.adopt(readSession(this.path), sent);
2499
+ if (taken) return taken;
2500
+ let issued;
2501
+ try {
2502
+ issued = await readResponse(
2503
+ await fetch(this.origin + "/oauth/token", {
2504
+ method: "POST",
2505
+ body: new URLSearchParams({
2506
+ grant_type: "refresh_token",
2507
+ refresh_token: sent,
2508
+ client_id: this.session.clientId,
2509
+ resource: mcpResource()
2510
+ }),
2511
+ signal: AbortSignal.timeout(15e3)
2512
+ })
2513
+ );
2514
+ } catch (error) {
2515
+ const rotated = this.adopt(readSession(this.path), sent);
2516
+ if (rotated) return rotated;
2517
+ throw revoked(error) ? new Error(signedOutNote) : error;
2518
+ }
2519
+ this.accessToken = issued.access_token;
2520
+ this.refreshToken = issued.refresh_token;
2521
+ this.expiresAt = tokenExpiry(issued.access_token);
2522
+ writeSession(
2523
+ {
2524
+ ...this.session,
2525
+ accessToken: this.accessToken,
2526
+ refreshToken: this.refreshToken,
2527
+ savedAt: (/* @__PURE__ */ new Date()).toISOString()
2528
+ },
2529
+ this.path
2530
+ );
2531
+ return { accessToken: this.accessToken, refreshToken: this.refreshToken };
2532
+ }, this.path).finally(() => {
2533
+ this.rotating = void 0;
2534
+ });
2535
+ return this.rotating;
2536
+ }
2537
+ client(agentName2, workspaceId2) {
2538
+ return new WirealClient({
2539
+ url: this.origin,
2540
+ agentName: agentName2,
2541
+ registeredClientName: agentName2,
2542
+ ...workspaceId2 ? { workspaceId: workspaceId2 } : {},
2543
+ accessToken: this.accessToken,
2544
+ refreshToken: this.refreshToken,
2545
+ clientId: this.session.clientId,
2546
+ resource: mcpResource(),
2547
+ refreshSession: () => this.tokens()
2548
+ });
2549
+ }
2550
+ };
2551
+ function openSession(path = sessionPath()) {
2552
+ const session = readSession(path);
2553
+ if (!session)
2554
+ throw new Error("Not signed in. Run `wireal-run login` on this machine.");
2555
+ return new RunnerSession(session, path);
2556
+ }
2557
+
2558
+ // runner/board.ts
2559
+ function sessionClient(agentName2 = "Wireal runner", workspaceId2) {
2560
+ return openSession().client(agentName2, workspaceId2);
2561
+ }
2562
+
2563
+ // runner/mcp-stdio.ts
2564
+ var agentName = process.env.WIREAL_AGENT_NAME?.trim() || "Agent";
2565
+ var workspaceId = process.env.WIREAL_WORKSPACE_ID?.trim() || void 0;
2566
+ var server = createWirealServer(sessionClient(agentName, workspaceId));
2567
+ await server.connect(new StdioServerTransport());