@akira-tl/forgerelay 0.8.4 → 0.8.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +30 -0
- package/README.md +10 -10
- package/dist/activity/audit-store.js +44 -6
- package/dist/activity/mcp-query-tools.js +53 -36
- package/dist/activity/query-service.js +41 -11
- package/dist/cli.js +16 -17
- package/dist/composite-activity.js +124 -33
- package/dist/config.js +8 -13
- package/dist/lsp/test-support/server-fixture.js +7 -7
- package/dist/process-sessions.js +2 -2
- package/dist/remote-auth.js +11 -1
- package/dist/remote-mcp-connection-pool.js +90 -0
- package/dist/remote-transport.js +28 -14
- package/dist/remote-workspace-relay.js +203 -45
- package/dist/server.js +134 -64
- package/dist/skills.js +1 -1
- package/dist/subagents/profiles.js +1 -2
- package/dist/subagents/providers/adapters/pi.js +2 -2
- package/dist/subagents/providers/availability.js +2 -2
- package/dist/subagents/providers/path.js +4 -4
- package/dist/ui/.vite/manifest.json +32 -32
- package/dist/ui/activity-panel-app.html +3 -3
- package/dist/ui/assets/{activity-panel-app-E1ju2dqI.js → activity-panel-app-CUAN6zyW.js} +1 -1
- package/dist/ui/assets/{heavy-payload-CeW-n9w5.js → heavy-payload-CgzrutLm.js} +1 -1
- package/dist/ui/assets/{review-payload-B9CO298v.js → review-payload-BrLbezbq.js} +1 -1
- package/dist/ui/assets/{scrollbar-C2twAENW.js → scrollbar-CbhpdW05.js} +1 -1
- package/dist/ui/assets/workspace-app-Bhj96tsR.js +1 -0
- package/dist/ui/assets/{workspace-app-CwbJnb_w.js → workspace-app-CxwJuZyS.js} +1 -1
- package/dist/ui/assets/{workspace-app-BztEvZIC.js → workspace-app-D6UR0AFl.js} +3 -3
- package/dist/ui/assets/workspace-app-ldjBmCJR.css +1 -0
- package/dist/ui/assets/workspace-lifecycle-app-Cqfhx9pV.js +1 -0
- package/dist/ui/workspace-app.html +4 -4
- package/dist/ui/workspace-lifecycle-app.html +4 -4
- package/dist/user-config.js +3 -19
- package/dist/workspace-presentation.js +69 -0
- package/docs/agent-profile-schema.md +4 -9
- package/docs/artifact-exchange.md +2 -1
- package/docs/chatgpt-coding-workflow.md +9 -9
- package/docs/configuration.md +20 -29
- package/docs/gotchas.md +10 -7
- package/docs/roadmap.md +6 -3
- package/docs/security.md +3 -2
- package/docs/setup.md +8 -5
- package/docs/versioning.md +1 -1
- package/package.json +6 -2
- package/scripts/debug/accept.mjs +7 -1
- package/scripts/debug/relay-accept.mjs +889 -0
- package/scripts/debug/runtime.mjs +0 -4
- package/scripts/debug/runtime.test.mjs +0 -2
- package/scripts/debug/traffic/run.sh +5 -0
- package/scripts/debug/traffic/traffic-audit.mjs +907 -0
- package/scripts/release/push-ready.mjs +124 -0
- package/scripts/release/push-ready.test.mjs +108 -0
- package/scripts/release/release-gate.test.mjs +1 -0
- package/scripts/release-proof.mjs +16 -1
- package/scripts/wiki/sync.mjs +246 -0
- package/dist/ui/assets/workspace-app-YnUST8IP.css +0 -1
- package/dist/ui/assets/workspace-app-rKuhdae8.js +0 -1
- package/dist/ui/assets/workspace-lifecycle-app-CEfMdudP.js +0 -1
|
@@ -2,7 +2,8 @@ import { randomBytes } from "node:crypto";
|
|
|
2
2
|
import { closeSync, existsSync, mkdirSync, openSync, readFileSync, renameSync, rmSync, statSync, writeFileSync, } from "node:fs";
|
|
3
3
|
import { join } from "node:path";
|
|
4
4
|
import { CallToolResultSchema } from "@modelcontextprotocol/sdk/types.js";
|
|
5
|
-
import { isRemoteMcpUnauthorized, refreshRemoteAuthentication,
|
|
5
|
+
import { isRemoteMcpUnauthorized, refreshRemoteAuthentication, } from "./remote-auth.js";
|
|
6
|
+
import { RemoteMcpConnectionPool } from "./remote-mcp-connection-pool.js";
|
|
6
7
|
import { withRemoteServiceEndpoint } from "./remote-transport.js";
|
|
7
8
|
import { loadForgeRelayFiles, writeForgeRelayRemote, } from "./user-config.js";
|
|
8
9
|
const ROUTE_LOCK_RETRY_MS = 10;
|
|
@@ -15,6 +16,7 @@ export class RemoteWorkspaceRelay {
|
|
|
15
16
|
authEnv;
|
|
16
17
|
routeStateDir;
|
|
17
18
|
routeStatePath;
|
|
19
|
+
mcpConnections = new RemoteMcpConnectionPool();
|
|
18
20
|
constructor(configDir, stateDir) {
|
|
19
21
|
this.authEnv = { FORGERELAY_CONFIG_DIR: configDir };
|
|
20
22
|
this.routeStateDir = stateDir;
|
|
@@ -27,20 +29,60 @@ export class RemoteWorkspaceRelay {
|
|
|
27
29
|
this.loadRoutes();
|
|
28
30
|
return this.routes.has(workspaceId);
|
|
29
31
|
}
|
|
30
|
-
|
|
32
|
+
async shutdown() {
|
|
33
|
+
await this.mcpConnections.closeAll();
|
|
34
|
+
}
|
|
35
|
+
async inspectWorkspace(gatewayWorkspaceId) {
|
|
31
36
|
const route = this.requireRoute(gatewayWorkspaceId);
|
|
32
37
|
const resolved = this.remoteByInstance(route.remoteInstanceId);
|
|
33
|
-
|
|
38
|
+
let result;
|
|
39
|
+
try {
|
|
40
|
+
result = await this.callRemoteTool(resolved.alias, resolved.remote, "open_workspace", {
|
|
41
|
+
action: "inspect",
|
|
42
|
+
workspaceId: route.remoteWorkspaceId,
|
|
43
|
+
});
|
|
44
|
+
assertRemoteToolSucceeded(resolved.alias, "open_workspace", result);
|
|
45
|
+
}
|
|
46
|
+
catch (error) {
|
|
47
|
+
throw sanitizedRemoteError(error, route.remoteWorkspaceId, gatewayWorkspaceId);
|
|
48
|
+
}
|
|
49
|
+
const structured = result.structuredContent;
|
|
50
|
+
const remoteInspection = structured?.inspection;
|
|
51
|
+
if (!remoteInspection || remoteInspection.kind !== "workspace") {
|
|
52
|
+
throw new Error(`Remote ForgeRelay ${resolved.alias} inspection did not return a Workspace projection.`);
|
|
53
|
+
}
|
|
54
|
+
const root = stringField(remoteInspection, "root", "Remote Workspace inspection");
|
|
55
|
+
const mode = remoteInspection.mode;
|
|
56
|
+
if (mode !== "checkout" && mode !== "worktree") {
|
|
57
|
+
throw new Error(`Remote ForgeRelay ${resolved.alias} inspection did not return a valid Workspace mode.`);
|
|
58
|
+
}
|
|
59
|
+
const projection = {
|
|
34
60
|
workspaceId: route.gatewayWorkspaceId,
|
|
35
61
|
kind: "workspace",
|
|
36
62
|
location: "relay",
|
|
37
|
-
root
|
|
63
|
+
root,
|
|
38
64
|
routeState: "known",
|
|
39
|
-
mode
|
|
40
|
-
...(route.sourceRoot ? { sourceRoot: route.sourceRoot } : {}),
|
|
65
|
+
mode,
|
|
41
66
|
relay: resolved.alias,
|
|
42
67
|
executionLocation: `remote:${resolved.alias}`,
|
|
43
68
|
};
|
|
69
|
+
copyStringField(remoteInspection, projection, "status");
|
|
70
|
+
const state = remoteInspection.state;
|
|
71
|
+
if (state === "active" || state === "stale" || state === "invalid" || state === "closed") {
|
|
72
|
+
projection.state = state;
|
|
73
|
+
}
|
|
74
|
+
copyStringField(remoteInspection, projection, "sourceRoot");
|
|
75
|
+
copyStringField(remoteInspection, projection, "branch");
|
|
76
|
+
copyStringField(remoteInspection, projection, "targetBranch");
|
|
77
|
+
copyBooleanField(remoteInspection, projection, "managed");
|
|
78
|
+
copyStringField(remoteInspection, projection, "createdAt");
|
|
79
|
+
copyStringField(remoteInspection, projection, "lastUsedAt");
|
|
80
|
+
copyNumberField(remoteInspection, projection, "idleMs");
|
|
81
|
+
copyBooleanField(remoteInspection, projection, "rootValid");
|
|
82
|
+
const taskSummary = safeTaskSummary(remoteInspection.taskSummary);
|
|
83
|
+
if (taskSummary)
|
|
84
|
+
projection.taskSummary = taskSummary;
|
|
85
|
+
return projection;
|
|
44
86
|
}
|
|
45
87
|
async openWorkspace(alias, input, conversationScopeId) {
|
|
46
88
|
const resolved = this.remoteByAlias(alias);
|
|
@@ -66,18 +108,15 @@ export class RemoteWorkspaceRelay {
|
|
|
66
108
|
if (mode !== "checkout" && mode !== "worktree") {
|
|
67
109
|
throw new Error("Remote open_workspace response did not include a valid workspace mode.");
|
|
68
110
|
}
|
|
69
|
-
const gatewayWorkspaceId = this.allocateGatewayWorkspaceId();
|
|
70
111
|
const sourceRoot = typeof structured?.sourceRoot === "string" ? structured.sourceRoot : undefined;
|
|
71
|
-
const route = {
|
|
72
|
-
gatewayWorkspaceId,
|
|
112
|
+
const route = this.findOrCreateRoute({
|
|
73
113
|
remoteInstanceId: resolved.remote.instanceId,
|
|
74
114
|
remoteWorkspaceId,
|
|
75
115
|
root,
|
|
76
116
|
mode,
|
|
77
117
|
...(sourceRoot ? { sourceRoot } : {}),
|
|
78
|
-
};
|
|
79
|
-
|
|
80
|
-
this.persistRoute(route);
|
|
118
|
+
});
|
|
119
|
+
const gatewayWorkspaceId = route.gatewayWorkspaceId;
|
|
81
120
|
const remapContext = (value) => replaceExactWorkspaceId(value, remoteWorkspaceId, gatewayWorkspaceId);
|
|
82
121
|
const remoteInstruction = typeof structured?.instruction === "string"
|
|
83
122
|
? String(remapContext(structured.instruction))
|
|
@@ -117,10 +156,24 @@ export class RemoteWorkspaceRelay {
|
|
|
117
156
|
};
|
|
118
157
|
}
|
|
119
158
|
async resumeWorkspace(gatewayWorkspaceId, context = "auto", conversationScopeId) {
|
|
159
|
+
const route = this.requireRoute(gatewayWorkspaceId);
|
|
120
160
|
const result = await this.callWorkspaceTool(gatewayWorkspaceId, "open_workspace", { context }, conversationScopeId);
|
|
121
161
|
if (result.isError === true) {
|
|
122
162
|
throw new Error(`Remote open_workspace failed: ${toolResultText(result)}`);
|
|
123
163
|
}
|
|
164
|
+
const structured = result.structuredContent;
|
|
165
|
+
const root = typeof structured?.root === "string" ? structured.root : route.root;
|
|
166
|
+
const mode = structured?.mode === "checkout" || structured?.mode === "worktree"
|
|
167
|
+
? structured.mode
|
|
168
|
+
: route.mode;
|
|
169
|
+
const sourceRoot = typeof structured?.sourceRoot === "string" ? structured.sourceRoot : route.sourceRoot;
|
|
170
|
+
this.findOrCreateRoute({
|
|
171
|
+
remoteInstanceId: route.remoteInstanceId,
|
|
172
|
+
remoteWorkspaceId: route.remoteWorkspaceId,
|
|
173
|
+
root,
|
|
174
|
+
mode,
|
|
175
|
+
...(sourceRoot ? { sourceRoot } : {}),
|
|
176
|
+
});
|
|
124
177
|
return result;
|
|
125
178
|
}
|
|
126
179
|
async read(gatewayWorkspaceId, input, conversationScopeId) {
|
|
@@ -187,11 +240,14 @@ export class RemoteWorkspaceRelay {
|
|
|
187
240
|
throw sanitizedRemoteError(error, route.remoteWorkspaceId, gatewayWorkspaceId);
|
|
188
241
|
}
|
|
189
242
|
}
|
|
243
|
+
async activityIndex(turnId, knownRevision, conversationScopeId) {
|
|
244
|
+
return this.callTurnTool(turnId, "activity_index", { turnId, ...(knownRevision !== undefined ? { knownRevision } : {}) }, conversationScopeId);
|
|
245
|
+
}
|
|
190
246
|
async activityDetail(turnId, activityId, conversationScopeId) {
|
|
191
247
|
return this.callTurnTool(turnId, "activity_detail", { turnId, activityId }, conversationScopeId);
|
|
192
248
|
}
|
|
193
|
-
async activityOutput(turnId, outputId, conversationScopeId) {
|
|
194
|
-
return this.callTurnTool(turnId, "activity_output", { turnId, outputId }, conversationScopeId);
|
|
249
|
+
async activityOutput(turnId, outputId, conversationScopeId, cursor) {
|
|
250
|
+
return this.callTurnTool(turnId, "activity_output", { turnId, outputId, ...(cursor !== undefined ? { cursor } : {}) }, conversationScopeId);
|
|
195
251
|
}
|
|
196
252
|
async callTurnTool(turnId, name, args, conversationScopeId) {
|
|
197
253
|
const gatewayWorkspaceId = this.turnRoutes.get(turnId);
|
|
@@ -221,14 +277,16 @@ export class RemoteWorkspaceRelay {
|
|
|
221
277
|
throw sanitizedRemoteError(error, route.remoteWorkspaceId, gatewayWorkspaceId);
|
|
222
278
|
}
|
|
223
279
|
}
|
|
224
|
-
async closeWorkspace(gatewayWorkspaceId,
|
|
280
|
+
async closeWorkspace(gatewayWorkspaceId, input = {}, conversationScopeId) {
|
|
225
281
|
const route = this.requireRoute(gatewayWorkspaceId);
|
|
226
282
|
const resolved = this.remoteByInstance(route.remoteInstanceId);
|
|
283
|
+
const action = input.action ?? "close";
|
|
227
284
|
let result;
|
|
228
285
|
try {
|
|
229
286
|
result = await this.callRemoteTool(resolved.alias, resolved.remote, "close_workspace", {
|
|
230
287
|
workspaceId: route.remoteWorkspaceId,
|
|
231
|
-
|
|
288
|
+
action,
|
|
289
|
+
...(input.commitMessage !== undefined ? { commitMessage: input.commitMessage } : {}),
|
|
232
290
|
}, conversationScopeId);
|
|
233
291
|
assertRemoteToolSucceeded(resolved.alias, "close_workspace", result);
|
|
234
292
|
}
|
|
@@ -236,21 +294,26 @@ export class RemoteWorkspaceRelay {
|
|
|
236
294
|
throw sanitizedRemoteError(error, route.remoteWorkspaceId, gatewayWorkspaceId);
|
|
237
295
|
}
|
|
238
296
|
const remoteStructured = result.structuredContent;
|
|
239
|
-
|
|
240
|
-
|
|
297
|
+
if (action === "delete") {
|
|
298
|
+
this.routes.delete(gatewayWorkspaceId);
|
|
299
|
+
this.deletePersistedRoute(gatewayWorkspaceId);
|
|
300
|
+
}
|
|
241
301
|
for (const [turnId, routedWorkspaceId] of this.turnRoutes) {
|
|
242
302
|
if (routedWorkspaceId === gatewayWorkspaceId)
|
|
243
303
|
this.turnRoutes.delete(turnId);
|
|
244
304
|
}
|
|
305
|
+
const actionText = action === "delete" ? "Deleted" : "Closed";
|
|
245
306
|
const text = route.mode === "worktree"
|
|
246
|
-
?
|
|
247
|
-
:
|
|
307
|
+
? `${actionText} relayed worktree workspace ${gatewayWorkspaceId} on remote ${resolved.alias}.`
|
|
308
|
+
: `${actionText} relayed checkout workspace ${gatewayWorkspaceId} on remote ${resolved.alias}.`;
|
|
248
309
|
const structuredContent = {
|
|
249
310
|
result: text,
|
|
250
311
|
workspaceId: gatewayWorkspaceId,
|
|
312
|
+
action,
|
|
251
313
|
mode: route.mode,
|
|
252
314
|
};
|
|
253
315
|
for (const field of [
|
|
316
|
+
"status",
|
|
254
317
|
"sourceRoot",
|
|
255
318
|
"branch",
|
|
256
319
|
"targetBranch",
|
|
@@ -270,6 +333,7 @@ export class RemoteWorkspaceRelay {
|
|
|
270
333
|
tool: "close_workspace",
|
|
271
334
|
card: {
|
|
272
335
|
workspaceId: gatewayWorkspaceId,
|
|
336
|
+
action,
|
|
273
337
|
mode: route.mode,
|
|
274
338
|
payload: { content: [{ type: "text", text }] },
|
|
275
339
|
},
|
|
@@ -292,10 +356,26 @@ export class RemoteWorkspaceRelay {
|
|
|
292
356
|
this.routes.set(workspaceId, route);
|
|
293
357
|
}
|
|
294
358
|
}
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
359
|
+
findOrCreateRoute(input) {
|
|
360
|
+
let selected;
|
|
361
|
+
mkdirSync(this.routeStateDir, { recursive: true });
|
|
362
|
+
this.withRouteFileLock(() => {
|
|
363
|
+
const routes = this.readRoutesFromDisk();
|
|
364
|
+
const existing = [...routes.values()]
|
|
365
|
+
.filter((route) => route.remoteInstanceId === input.remoteInstanceId &&
|
|
366
|
+
route.remoteWorkspaceId === input.remoteWorkspaceId)
|
|
367
|
+
.sort((left, right) => left.gatewayWorkspaceId.localeCompare(right.gatewayWorkspaceId))[0];
|
|
368
|
+
selected = {
|
|
369
|
+
gatewayWorkspaceId: existing?.gatewayWorkspaceId ?? this.allocateGatewayWorkspaceId(routes),
|
|
370
|
+
...input,
|
|
371
|
+
};
|
|
372
|
+
routes.set(selected.gatewayWorkspaceId, selected);
|
|
373
|
+
this.writeRoutesToDisk(routes);
|
|
298
374
|
});
|
|
375
|
+
if (!selected)
|
|
376
|
+
throw new Error("Failed to persist relayed Workspace route.");
|
|
377
|
+
this.routes.set(selected.gatewayWorkspaceId, selected);
|
|
378
|
+
return selected;
|
|
299
379
|
}
|
|
300
380
|
deletePersistedRoute(workspaceId) {
|
|
301
381
|
this.updatePersistedRoutes((routes) => {
|
|
@@ -392,42 +472,64 @@ export class RemoteWorkspaceRelay {
|
|
|
392
472
|
return { alias: entry[0], remote: entry[1] };
|
|
393
473
|
}
|
|
394
474
|
async callRemoteTool(alias, initialRemote, name, args, conversationScopeId) {
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
475
|
+
let remote = initialRemote;
|
|
476
|
+
let refreshed = false;
|
|
477
|
+
if (remote.accessTokenExpiresAt <= Math.floor(Date.now() / 1000)) {
|
|
478
|
+
remote = await withRemoteServiceEndpoint(remote.target, remote.sshRoute, (endpoint) => this.refreshRemote(alias, remote, endpoint));
|
|
479
|
+
refreshed = true;
|
|
480
|
+
}
|
|
481
|
+
let connection;
|
|
482
|
+
try {
|
|
483
|
+
connection = await this.mcpConnections.get(remote);
|
|
484
|
+
}
|
|
485
|
+
catch (error) {
|
|
486
|
+
if (!refreshed && isRemoteMcpUnauthorized(error)) {
|
|
487
|
+
remote = await withRemoteServiceEndpoint(remote.target, remote.sshRoute, (endpoint) => this.refreshRemote(alias, remote, endpoint));
|
|
400
488
|
refreshed = true;
|
|
489
|
+
connection = await this.mcpConnections.get(remote);
|
|
401
490
|
}
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
arguments: args,
|
|
405
|
-
...(conversationScopeId
|
|
406
|
-
? { _meta: { "openai/session": conversationScopeId } }
|
|
407
|
-
: {}),
|
|
408
|
-
})));
|
|
409
|
-
try {
|
|
410
|
-
return await invoke();
|
|
491
|
+
else {
|
|
492
|
+
throw new Error(`Remote ForgeRelay ${alias} request failed: ${errorMessage(error)}`, { cause: error });
|
|
411
493
|
}
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
494
|
+
}
|
|
495
|
+
const invoke = async (active) => CallToolResultSchema.parse(await active.client.callTool({
|
|
496
|
+
name,
|
|
497
|
+
arguments: args,
|
|
498
|
+
...(conversationScopeId
|
|
499
|
+
? { _meta: { "openai/session": conversationScopeId } }
|
|
500
|
+
: {}),
|
|
501
|
+
}));
|
|
502
|
+
try {
|
|
503
|
+
return await invoke(connection);
|
|
504
|
+
}
|
|
505
|
+
catch (error) {
|
|
506
|
+
if (!refreshed && isRemoteMcpUnauthorized(error)) {
|
|
507
|
+
remote = await this.refreshRemote(alias, remote, connection.endpoint);
|
|
508
|
+
refreshed = true;
|
|
509
|
+
await this.mcpConnections.invalidate(remote.instanceId, connection);
|
|
510
|
+
const refreshedConnection = await this.mcpConnections.get(remote);
|
|
511
|
+
try {
|
|
512
|
+
return await invoke(refreshedConnection);
|
|
513
|
+
}
|
|
514
|
+
catch (retryError) {
|
|
515
|
+
await this.mcpConnections.invalidate(remote.instanceId, refreshedConnection);
|
|
516
|
+
throw new Error(`Remote ForgeRelay ${alias} request failed: ${errorMessage(retryError)}`, { cause: retryError });
|
|
416
517
|
}
|
|
417
|
-
throw new Error(`Remote ForgeRelay ${alias} request failed: ${errorMessage(error)}`, { cause: error });
|
|
418
518
|
}
|
|
419
|
-
|
|
519
|
+
await this.mcpConnections.invalidate(remote.instanceId, connection);
|
|
520
|
+
throw new Error(`Remote ForgeRelay ${alias} request failed: ${errorMessage(error)}`, { cause: error });
|
|
521
|
+
}
|
|
420
522
|
}
|
|
421
523
|
async refreshRemote(alias, remote, endpoint) {
|
|
422
524
|
const refreshed = await refreshRemoteAuthentication(remote, endpoint);
|
|
423
525
|
writeForgeRelayRemote(alias, refreshed, this.authEnv);
|
|
424
526
|
return refreshed;
|
|
425
527
|
}
|
|
426
|
-
allocateGatewayWorkspaceId() {
|
|
528
|
+
allocateGatewayWorkspaceId(routes = this.routes) {
|
|
427
529
|
let workspaceId;
|
|
428
530
|
do {
|
|
429
531
|
workspaceId = `rws_${randomBytes(5).toString("hex")}`;
|
|
430
|
-
} while (
|
|
532
|
+
} while (routes.has(workspaceId));
|
|
431
533
|
return workspaceId;
|
|
432
534
|
}
|
|
433
535
|
}
|
|
@@ -443,6 +545,62 @@ function stringField(structured, field, label) {
|
|
|
443
545
|
}
|
|
444
546
|
return value;
|
|
445
547
|
}
|
|
548
|
+
function copyStringField(source, target, field) {
|
|
549
|
+
const value = source[field];
|
|
550
|
+
if (typeof value === "string")
|
|
551
|
+
target[field] = value;
|
|
552
|
+
}
|
|
553
|
+
function copyBooleanField(source, target, field) {
|
|
554
|
+
const value = source[field];
|
|
555
|
+
if (typeof value === "boolean")
|
|
556
|
+
target[field] = value;
|
|
557
|
+
}
|
|
558
|
+
function copyNumberField(source, target, field) {
|
|
559
|
+
const value = source[field];
|
|
560
|
+
if (typeof value === "number" && Number.isFinite(value) && value >= 0)
|
|
561
|
+
target[field] = value;
|
|
562
|
+
}
|
|
563
|
+
function safeTaskSummary(value) {
|
|
564
|
+
if (!value || typeof value !== "object")
|
|
565
|
+
return undefined;
|
|
566
|
+
const summary = value;
|
|
567
|
+
if (summary.level !== "summary" ||
|
|
568
|
+
summary.version !== 1 ||
|
|
569
|
+
typeof summary.revision !== "number" ||
|
|
570
|
+
!Number.isInteger(summary.revision) ||
|
|
571
|
+
summary.revision < 0 ||
|
|
572
|
+
!Array.isArray(summary.lists)) {
|
|
573
|
+
return undefined;
|
|
574
|
+
}
|
|
575
|
+
const lists = [];
|
|
576
|
+
for (const value of summary.lists) {
|
|
577
|
+
if (!value || typeof value !== "object")
|
|
578
|
+
return undefined;
|
|
579
|
+
const list = value;
|
|
580
|
+
if (typeof list.id !== "string" ||
|
|
581
|
+
typeof list.name !== "string" ||
|
|
582
|
+
(list.state !== "active" && list.state !== "archived") ||
|
|
583
|
+
typeof list.revision !== "number" || !Number.isInteger(list.revision) || list.revision <= 0 ||
|
|
584
|
+
typeof list.taskCount !== "number" || !Number.isInteger(list.taskCount) || list.taskCount < 0 ||
|
|
585
|
+
typeof list.unfinishedTaskCount !== "number" || !Number.isInteger(list.unfinishedTaskCount) || list.unfinishedTaskCount < 0) {
|
|
586
|
+
return undefined;
|
|
587
|
+
}
|
|
588
|
+
lists.push({
|
|
589
|
+
id: list.id,
|
|
590
|
+
name: list.name,
|
|
591
|
+
state: list.state,
|
|
592
|
+
revision: list.revision,
|
|
593
|
+
taskCount: list.taskCount,
|
|
594
|
+
unfinishedTaskCount: list.unfinishedTaskCount,
|
|
595
|
+
});
|
|
596
|
+
}
|
|
597
|
+
return {
|
|
598
|
+
level: "summary",
|
|
599
|
+
version: 1,
|
|
600
|
+
revision: summary.revision,
|
|
601
|
+
lists,
|
|
602
|
+
};
|
|
603
|
+
}
|
|
446
604
|
function toolResultText(result) {
|
|
447
605
|
return (result.content ?? [])
|
|
448
606
|
.filter((entry) => entry.type === "text")
|