@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
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,36 @@ All notable ForgeRelay changes are documented here.
|
|
|
4
4
|
|
|
5
5
|
## [Unreleased]
|
|
6
6
|
|
|
7
|
+
## [0.8.6] - 2026-09-02
|
|
8
|
+
|
|
9
|
+
### Added
|
|
10
|
+
|
|
11
|
+
- Added `release:push-ready` as the canonical release-ready branch push, atomically advancing the remote main/release refs and synchronizing the local `main` only after divergence safety checks pass.
|
|
12
|
+
|
|
13
|
+
### Changed
|
|
14
|
+
|
|
15
|
+
- Retired rename-era DevSpace public compatibility adapters that are well beyond the migration window: `DEVSPACE_*` environment fallbacks, automatic `~/.devspace` config/state/worktree reuse, `.devspace/agents` profile discovery, and old DevSpace package-path special cases. Persisted storage/protocol identifiers remain compatible where renaming them would orphan existing state.
|
|
16
|
+
|
|
17
|
+
### Fixed
|
|
18
|
+
|
|
19
|
+
- Reduced Activity Panel wire amplification with state-only collapsed polling, lazy revisioned Activity indexes, cursor-based Bash output, adaptive polling, and persistent Relay connections.
|
|
20
|
+
- Restored the Workspace overview without reintroducing large repeated payloads by keeping Skills, instructions, and agent presentation metadata bounded.
|
|
21
|
+
|
|
22
|
+
## [0.8.5] - 2026-08-31
|
|
23
|
+
|
|
24
|
+
### Added
|
|
25
|
+
|
|
26
|
+
- Added source-controlled GitHub Wiki pages with validation and publishing tooling.
|
|
27
|
+
|
|
28
|
+
### Changed
|
|
29
|
+
|
|
30
|
+
- Relayed checkout and managed-worktree Workspaces now preserve stable Gateway identity and Execution-owned Task/lifecycle semantics across close, reopen, delete, inspection, and Composite member routing.
|
|
31
|
+
|
|
32
|
+
### Fixed
|
|
33
|
+
|
|
34
|
+
- Task reminders and relayed/Composite runtime state now survive independent MCP transport sessions instead of resetting or presenting stale session-local state.
|
|
35
|
+
- Managed-worktree Relay acceptance now tolerates Git-native LF/CRLF checkout normalization, keeping the release matrix portable across Linux, macOS, and Windows.
|
|
36
|
+
|
|
7
37
|
## [0.8.4] - 2026-08-31
|
|
8
38
|
|
|
9
39
|
### Added
|
package/README.md
CHANGED
|
@@ -255,7 +255,7 @@ See [Agent Profile Schema](docs/agent-profile-schema.md) for the profile format.
|
|
|
255
255
|
|
|
256
256
|
## Configuration and upgrades from DevSpace
|
|
257
257
|
|
|
258
|
-
|
|
258
|
+
Configuration uses the `FORGERELAY_*` prefix. For example:
|
|
259
259
|
|
|
260
260
|
```bash
|
|
261
261
|
FORGERELAY_ALLOWED_ROOTS="$HOME/projects" \
|
|
@@ -263,16 +263,14 @@ FORGERELAY_PUBLIC_BASE_URL="https://forge.example.com" \
|
|
|
263
263
|
forgerelay serve
|
|
264
264
|
```
|
|
265
265
|
|
|
266
|
-
|
|
267
|
-
|
|
266
|
+
The rename-era automatic compatibility window has ended. `DEVSPACE_*`
|
|
267
|
+
environment variables are ignored, ForgeRelay no longer auto-selects
|
|
268
|
+
`~/.devspace`, and project `.devspace/agents` profiles are not discovered.
|
|
268
269
|
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
Project-level `.devspace/agents` profiles are also still readable for migration
|
|
275
|
-
compatibility. New project configuration should use `.forgerelay`.
|
|
270
|
+
If an older installation still has data under DevSpace-named directories,
|
|
271
|
+
migrate it explicitly or point the canonical ForgeRelay settings at those paths
|
|
272
|
+
while moving the data. Persisted internal identifiers that would otherwise
|
|
273
|
+
orphan existing state remain compatible and are not product-facing configuration.
|
|
276
274
|
|
|
277
275
|
See [Configuration Reference](docs/configuration.md) for all supported options.
|
|
278
276
|
|
|
@@ -348,6 +346,7 @@ Trusted Publishing setup.
|
|
|
348
346
|
|
|
349
347
|
## Documentation
|
|
350
348
|
|
|
349
|
+
- [GitHub Wiki](https://github.com/Akira-TL/forgerelay/wiki) — 面向用户的使用指南、概念说明与故障排查入口。
|
|
351
350
|
- [Setup Guide](docs/setup.md)
|
|
352
351
|
- [Local Debugging and 7677 Acceptance](docs/debugging.md)
|
|
353
352
|
- [ChatGPT Coding Workflow](docs/chatgpt-coding-workflow.md)
|
|
@@ -358,6 +357,7 @@ Trusted Publishing setup.
|
|
|
358
357
|
- [Troubleshooting](docs/gotchas.md)
|
|
359
358
|
- [Roadmap](docs/roadmap.md)
|
|
360
359
|
- [Versioning and Release Management](docs/versioning.md)
|
|
360
|
+
- [GitHub Wiki Maintenance](docs/maintenance/wiki.md)
|
|
361
361
|
- [Changelog](CHANGELOG.md)
|
|
362
362
|
- [Attribution Notice](NOTICE.md)
|
|
363
363
|
|
|
@@ -67,13 +67,51 @@ export class ActivityAuditStore {
|
|
|
67
67
|
});
|
|
68
68
|
}
|
|
69
69
|
turnRevision(turnId) {
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
70
|
+
return this.turnState(turnId).revision;
|
|
71
|
+
}
|
|
72
|
+
turnState(turnId) {
|
|
73
|
+
const row = this.database.sqlite.prepare(`with turn_activities as (
|
|
74
|
+
select activity_id
|
|
75
|
+
from activity_audit_events
|
|
74
76
|
where event_type = 'started' and turn_id = ?
|
|
75
|
-
)
|
|
76
|
-
|
|
77
|
+
), turn_events as (
|
|
78
|
+
select event.rowid, event.activity_id, event.event_type
|
|
79
|
+
from activity_audit_events event
|
|
80
|
+
join turn_activities turn_activity on turn_activity.activity_id = event.activity_id
|
|
81
|
+
), latest_events as (
|
|
82
|
+
select event.event_type
|
|
83
|
+
from turn_events event
|
|
84
|
+
join (
|
|
85
|
+
select activity_id, max(rowid) as rowid
|
|
86
|
+
from turn_events
|
|
87
|
+
group by activity_id
|
|
88
|
+
) latest on latest.rowid = event.rowid
|
|
89
|
+
)
|
|
90
|
+
select
|
|
91
|
+
(select count(*) from turn_events) as revision,
|
|
92
|
+
count(*) as total,
|
|
93
|
+
coalesce(sum(case when event_type = 'started' then 1 else 0 end), 0) as working,
|
|
94
|
+
coalesce(sum(case when event_type in ('failed', 'blocked') then 1 else 0 end), 0) as error
|
|
95
|
+
from latest_events`).get(turnId);
|
|
96
|
+
return row;
|
|
97
|
+
}
|
|
98
|
+
activityIdsSinceTurnRevision(turnId, knownRevision) {
|
|
99
|
+
const offset = Math.max(0, Math.floor(knownRevision));
|
|
100
|
+
const rows = this.database.sqlite.prepare(`select activity_id, min(rowid) as first_rowid
|
|
101
|
+
from (
|
|
102
|
+
select event.rowid, event.activity_id
|
|
103
|
+
from activity_audit_events event
|
|
104
|
+
where event.activity_id in (
|
|
105
|
+
select activity_id
|
|
106
|
+
from activity_audit_events
|
|
107
|
+
where event_type = 'started' and turn_id = ?
|
|
108
|
+
)
|
|
109
|
+
order by event.rowid asc
|
|
110
|
+
limit -1 offset ?
|
|
111
|
+
)
|
|
112
|
+
group by activity_id
|
|
113
|
+
order by first_rowid asc`).all(turnId, offset);
|
|
114
|
+
return rows.map((row) => row.activity_id);
|
|
77
115
|
}
|
|
78
116
|
getActivity(activityId) {
|
|
79
117
|
const events = this.listEvents(activityId);
|
|
@@ -35,13 +35,15 @@ const activitySummarySchema = z.object({
|
|
|
35
35
|
error: z.number().int().nonnegative(),
|
|
36
36
|
}).optional(),
|
|
37
37
|
});
|
|
38
|
-
const
|
|
38
|
+
const stateOutputSchema = {
|
|
39
39
|
turnId: z.string(),
|
|
40
40
|
revision: z.number().int().nonnegative(),
|
|
41
41
|
changed: z.boolean(),
|
|
42
42
|
state: z.enum(["working", "done", "error"]),
|
|
43
|
+
};
|
|
44
|
+
const indexOutputSchema = {
|
|
45
|
+
...stateOutputSchema,
|
|
43
46
|
activities: z.array(activitySummarySchema),
|
|
44
|
-
[ACTIVITY_PANEL_WORKSPACE_META_KEY]: z.record(z.string(), z.unknown()).optional(),
|
|
45
47
|
};
|
|
46
48
|
export function registerActivityQueryTools(server, queries, connectionScopeId, panelMeta = {}, panelDefaultExpanded = false, logging, workspacePanelState, relay) {
|
|
47
49
|
const panelUi = typeof panelMeta.ui === "object" && panelMeta.ui !== null
|
|
@@ -53,7 +55,7 @@ export function registerActivityQueryTools(server, queries, connectionScopeId, p
|
|
|
53
55
|
inputSchema: {
|
|
54
56
|
workspaceId: z.string().min(1).describe("Workspace identifier returned by open_workspace for the project work in this Host Turn."),
|
|
55
57
|
},
|
|
56
|
-
outputSchema:
|
|
58
|
+
outputSchema: stateOutputSchema,
|
|
57
59
|
_meta: {
|
|
58
60
|
...panelMeta,
|
|
59
61
|
ui: {
|
|
@@ -80,10 +82,6 @@ export function registerActivityQueryTools(server, queries, connectionScopeId, p
|
|
|
80
82
|
[ACTIVITY_PANEL_DEFAULT_EXPANDED_META_KEY]: panelDefaultExpanded,
|
|
81
83
|
[ACTIVITY_PANEL_WORKSPACE_META_KEY]: workspace,
|
|
82
84
|
},
|
|
83
|
-
structuredContent: {
|
|
84
|
-
...(relayed.structuredContent ?? {}),
|
|
85
|
-
[ACTIVITY_PANEL_WORKSPACE_META_KEY]: workspace,
|
|
86
|
-
},
|
|
87
85
|
};
|
|
88
86
|
}
|
|
89
87
|
const snapshot = queries.beginTurn(conversationScopeId, workspaceId);
|
|
@@ -92,7 +90,6 @@ export function registerActivityQueryTools(server, queries, connectionScopeId, p
|
|
|
92
90
|
turnId: snapshot.turnId,
|
|
93
91
|
revision: snapshot.revision,
|
|
94
92
|
state: snapshot.state,
|
|
95
|
-
activities: snapshot.activities.length,
|
|
96
93
|
workspaceId,
|
|
97
94
|
transportSessionIdPrefix: transportSessionIdPrefix(extra.sessionId),
|
|
98
95
|
});
|
|
@@ -106,21 +103,18 @@ export function registerActivityQueryTools(server, queries, connectionScopeId, p
|
|
|
106
103
|
type: "text",
|
|
107
104
|
text: `Started ForgeRelay Panel Host Turn ${snapshot.turnId} for ${workspaceId}.`,
|
|
108
105
|
}],
|
|
109
|
-
structuredContent: {
|
|
110
|
-
...snapshot,
|
|
111
|
-
[ACTIVITY_PANEL_WORKSPACE_META_KEY]: workspace,
|
|
112
|
-
},
|
|
106
|
+
structuredContent: { ...snapshot },
|
|
113
107
|
};
|
|
114
108
|
});
|
|
115
109
|
registerAppTool(server, "activity_snapshot", {
|
|
116
110
|
title: "Read Activity snapshot",
|
|
117
|
-
description: "App-only
|
|
111
|
+
description: "App-only state source for one durable Host Turn. Returns only revision/change/state; Activity rows are fetched separately through activity_index when the Panel is expanded.",
|
|
118
112
|
inputSchema: {
|
|
119
113
|
turnId: z.string().optional().describe("Existing Host Turn identifier. Omit only for initial App bootstrap; ForgeRelay then resolves the current Host Turn from conversation metadata."),
|
|
120
114
|
workspaceId: z.string().min(1).optional().describe("Workspace identifier used by the App during initial bootstrap so ForgeRelay can restore the Workspace section."),
|
|
121
115
|
knownRevision: z.number().int().nonnegative().optional(),
|
|
122
116
|
},
|
|
123
|
-
outputSchema:
|
|
117
|
+
outputSchema: stateOutputSchema,
|
|
124
118
|
_meta: { ui: { visibility: ["app"] } },
|
|
125
119
|
annotations: READ_ONLY_ANNOTATIONS,
|
|
126
120
|
}, async ({ turnId, workspaceId, knownRevision }, extra) => {
|
|
@@ -139,10 +133,6 @@ export function registerActivityQueryTools(server, queries, connectionScopeId, p
|
|
|
139
133
|
[ACTIVITY_PANEL_DEFAULT_EXPANDED_META_KEY]: panelDefaultExpanded,
|
|
140
134
|
[ACTIVITY_PANEL_WORKSPACE_META_KEY]: workspace,
|
|
141
135
|
},
|
|
142
|
-
structuredContent: {
|
|
143
|
-
...(relayed.structuredContent ?? {}),
|
|
144
|
-
[ACTIVITY_PANEL_WORKSPACE_META_KEY]: workspace,
|
|
145
|
-
},
|
|
146
136
|
}
|
|
147
137
|
: relayed;
|
|
148
138
|
}
|
|
@@ -150,7 +140,7 @@ export function registerActivityQueryTools(server, queries, connectionScopeId, p
|
|
|
150
140
|
if (!resolvedTurnId) {
|
|
151
141
|
throw new Error("Activity snapshot bootstrap could not resolve the current Host Turn from conversation and workspace metadata.");
|
|
152
142
|
}
|
|
153
|
-
const snapshot = queries.
|
|
143
|
+
const snapshot = queries.state(resolvedTurnId, knownRevision);
|
|
154
144
|
if (logging) {
|
|
155
145
|
logEvent(logging, "debug", "activity_snapshot_call", {
|
|
156
146
|
turnId: resolvedTurnId,
|
|
@@ -159,7 +149,6 @@ export function registerActivityQueryTools(server, queries, connectionScopeId, p
|
|
|
159
149
|
revision: snapshot.revision,
|
|
160
150
|
changed: snapshot.changed,
|
|
161
151
|
state: snapshot.state,
|
|
162
|
-
activities: snapshot.activities.length,
|
|
163
152
|
transportSessionIdPrefix: transportSessionIdPrefix(extra.sessionId),
|
|
164
153
|
});
|
|
165
154
|
}
|
|
@@ -172,16 +161,40 @@ export function registerActivityQueryTools(server, queries, connectionScopeId, p
|
|
|
172
161
|
[ACTIVITY_PANEL_DEFAULT_EXPANDED_META_KEY]: panelDefaultExpanded,
|
|
173
162
|
...(workspace ? { [ACTIVITY_PANEL_WORKSPACE_META_KEY]: workspace } : {}),
|
|
174
163
|
},
|
|
175
|
-
content: [
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
164
|
+
content: [],
|
|
165
|
+
structuredContent: { ...snapshot },
|
|
166
|
+
};
|
|
167
|
+
});
|
|
168
|
+
registerAppTool(server, "activity_index", {
|
|
169
|
+
title: "Read Activity index",
|
|
170
|
+
description: "App-only lazy Activity-row source for an expanded Panel. Pass knownRevision to receive only rows changed since the previous index read.",
|
|
171
|
+
inputSchema: {
|
|
172
|
+
turnId: z.string(),
|
|
173
|
+
knownRevision: z.number().int().nonnegative().optional(),
|
|
174
|
+
},
|
|
175
|
+
outputSchema: indexOutputSchema,
|
|
176
|
+
_meta: { ui: { visibility: ["app"] } },
|
|
177
|
+
annotations: READ_ONLY_ANNOTATIONS,
|
|
178
|
+
}, async ({ turnId, knownRevision }, extra) => {
|
|
179
|
+
const conversationScopeId = hostConversationScopeId(extra._meta, extra.sessionId, connectionScopeId);
|
|
180
|
+
const relayed = await relay?.index(turnId, knownRevision, conversationScopeId);
|
|
181
|
+
if (relayed)
|
|
182
|
+
return relayed;
|
|
183
|
+
const index = queries.index(turnId, knownRevision);
|
|
184
|
+
if (logging) {
|
|
185
|
+
logEvent(logging, "debug", "activity_index_call", {
|
|
186
|
+
turnId,
|
|
187
|
+
knownRevision,
|
|
188
|
+
revision: index.revision,
|
|
189
|
+
changed: index.changed,
|
|
190
|
+
state: index.state,
|
|
191
|
+
activities: index.activities.length,
|
|
192
|
+
transportSessionIdPrefix: transportSessionIdPrefix(extra.sessionId),
|
|
193
|
+
});
|
|
194
|
+
}
|
|
195
|
+
return {
|
|
196
|
+
content: [],
|
|
197
|
+
structuredContent: { ...index },
|
|
185
198
|
};
|
|
186
199
|
});
|
|
187
200
|
registerAppTool(server, "activity_detail", {
|
|
@@ -216,16 +229,17 @@ export function registerActivityQueryTools(server, queries, connectionScopeId, p
|
|
|
216
229
|
});
|
|
217
230
|
}
|
|
218
231
|
return {
|
|
219
|
-
content: [
|
|
232
|
+
content: [],
|
|
220
233
|
structuredContent: { ...detail },
|
|
221
234
|
};
|
|
222
235
|
});
|
|
223
236
|
registerAppTool(server, "activity_output", {
|
|
224
237
|
title: "Read Bash output",
|
|
225
|
-
description: "App-only lazy data source for
|
|
238
|
+
description: "App-only lazy data source for durable Bash command/output by stable outputId. Pass the returned cursor on follow-up reads to receive only newly appended output.",
|
|
226
239
|
inputSchema: {
|
|
227
240
|
turnId: z.string(),
|
|
228
241
|
outputId: z.string(),
|
|
242
|
+
cursor: z.number().int().nonnegative().optional(),
|
|
229
243
|
},
|
|
230
244
|
outputSchema: {
|
|
231
245
|
outputId: z.string(),
|
|
@@ -233,6 +247,7 @@ export function registerActivityQueryTools(server, queries, connectionScopeId, p
|
|
|
233
247
|
processId: z.number().int().positive(),
|
|
234
248
|
command: z.string(),
|
|
235
249
|
output: z.string(),
|
|
250
|
+
cursor: z.number().int().nonnegative(),
|
|
236
251
|
status: z.enum(["running", "done", "failed"]),
|
|
237
252
|
exitCode: z.number().int().optional(),
|
|
238
253
|
signal: z.string().optional(),
|
|
@@ -242,16 +257,18 @@ export function registerActivityQueryTools(server, queries, connectionScopeId, p
|
|
|
242
257
|
},
|
|
243
258
|
_meta: { ui: { visibility: ["app"] } },
|
|
244
259
|
annotations: READ_ONLY_ANNOTATIONS,
|
|
245
|
-
}, async ({ turnId, outputId }, extra) => {
|
|
260
|
+
}, async ({ turnId, outputId, cursor }, extra) => {
|
|
246
261
|
const conversationScopeId = hostConversationScopeId(extra._meta, extra.sessionId, connectionScopeId);
|
|
247
|
-
const relayed = await relay?.output(turnId, outputId, conversationScopeId);
|
|
262
|
+
const relayed = await relay?.output(turnId, outputId, conversationScopeId, cursor);
|
|
248
263
|
if (relayed)
|
|
249
264
|
return relayed;
|
|
250
|
-
const output = queries.bashOutput(turnId, outputId);
|
|
265
|
+
const output = queries.bashOutput(turnId, outputId, cursor);
|
|
251
266
|
if (logging) {
|
|
252
267
|
logEvent(logging, "debug", "activity_output_call", {
|
|
253
268
|
turnId,
|
|
254
269
|
outputId,
|
|
270
|
+
cursor,
|
|
271
|
+
nextCursor: output.cursor,
|
|
255
272
|
activityId: output.activityId,
|
|
256
273
|
processId: output.processId,
|
|
257
274
|
status: output.status,
|
|
@@ -260,7 +277,7 @@ export function registerActivityQueryTools(server, queries, connectionScopeId, p
|
|
|
260
277
|
});
|
|
261
278
|
}
|
|
262
279
|
return {
|
|
263
|
-
content: [
|
|
280
|
+
content: [],
|
|
264
281
|
structuredContent: { ...output },
|
|
265
282
|
};
|
|
266
283
|
});
|
|
@@ -9,25 +9,50 @@ export class ActivityQueryService {
|
|
|
9
9
|
}
|
|
10
10
|
beginTurn(conversationScopeId, workspaceId) {
|
|
11
11
|
const turn = this.turns.begin(conversationScopeId, workspaceId);
|
|
12
|
-
return this.
|
|
12
|
+
return this.state(turn.turnId);
|
|
13
13
|
}
|
|
14
14
|
currentTurnId(conversationScopeId, workspaceId) {
|
|
15
15
|
return this.turns.current(conversationScopeId, workspaceId)?.turnId;
|
|
16
16
|
}
|
|
17
|
-
|
|
17
|
+
state(turnId, knownRevision) {
|
|
18
18
|
this.requireTurn(turnId);
|
|
19
|
-
const
|
|
20
|
-
const
|
|
21
|
-
|
|
22
|
-
|
|
19
|
+
const aggregate = this.audit.turnState(turnId);
|
|
20
|
+
const state = aggregate.total === 0 || aggregate.working > 0
|
|
21
|
+
? "working"
|
|
22
|
+
: aggregate.error > 0
|
|
23
|
+
? "error"
|
|
24
|
+
: "done";
|
|
23
25
|
return {
|
|
24
26
|
turnId,
|
|
25
|
-
revision,
|
|
26
|
-
changed,
|
|
27
|
+
revision: aggregate.revision,
|
|
28
|
+
changed: knownRevision === undefined || knownRevision !== aggregate.revision,
|
|
27
29
|
state,
|
|
28
|
-
activities: changed ? activities : [],
|
|
29
30
|
};
|
|
30
31
|
}
|
|
32
|
+
index(turnId, knownRevision) {
|
|
33
|
+
const state = this.state(turnId, knownRevision);
|
|
34
|
+
if (!state.changed)
|
|
35
|
+
return { ...state, activities: [] };
|
|
36
|
+
const activities = this.summaries(turnId);
|
|
37
|
+
if (knownRevision === undefined || knownRevision < 0 || knownRevision > state.revision) {
|
|
38
|
+
return { ...state, activities };
|
|
39
|
+
}
|
|
40
|
+
const changedIds = new Set(this.audit.activityIdsSinceTurnRevision(turnId, knownRevision));
|
|
41
|
+
if (changedIds.size === 0)
|
|
42
|
+
return { ...state, activities: [] };
|
|
43
|
+
for (const activity of activities) {
|
|
44
|
+
if (changedIds.has(activity.activityId) && activity.parentActivityId) {
|
|
45
|
+
changedIds.add(activity.parentActivityId);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
return {
|
|
49
|
+
...state,
|
|
50
|
+
activities: activities.filter((activity) => changedIds.has(activity.activityId)),
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
snapshot(turnId, knownRevision) {
|
|
54
|
+
return this.index(turnId, knownRevision);
|
|
55
|
+
}
|
|
31
56
|
detail(turnId, activityId) {
|
|
32
57
|
this.requireTurn(turnId);
|
|
33
58
|
const record = this.audit.getActivity(activityId);
|
|
@@ -46,7 +71,7 @@ export class ActivityQueryService {
|
|
|
46
71
|
...(record.error !== undefined ? { error: record.error } : {}),
|
|
47
72
|
};
|
|
48
73
|
}
|
|
49
|
-
bashOutput(turnId, outputId) {
|
|
74
|
+
bashOutput(turnId, outputId, cursor = 0) {
|
|
50
75
|
this.requireTurn(turnId);
|
|
51
76
|
const output = this.outputs.read(outputId);
|
|
52
77
|
if (!output)
|
|
@@ -56,12 +81,17 @@ export class ActivityQueryService {
|
|
|
56
81
|
if (!visible) {
|
|
57
82
|
throw new Error(`Bash output ${outputId} is not part of Host Turn ${turnId}.`);
|
|
58
83
|
}
|
|
84
|
+
const latestCursor = output.chunks.at(-1)?.sequence ?? 0;
|
|
59
85
|
return {
|
|
60
86
|
outputId: output.outputId,
|
|
61
87
|
activityId: output.activityId,
|
|
62
88
|
processId: output.processId,
|
|
63
89
|
command: output.command,
|
|
64
|
-
output: output.
|
|
90
|
+
output: output.chunks
|
|
91
|
+
.filter((chunk) => chunk.sequence > cursor)
|
|
92
|
+
.map((chunk) => chunk.data)
|
|
93
|
+
.join(""),
|
|
94
|
+
cursor: latestCursor,
|
|
65
95
|
status: output.status,
|
|
66
96
|
...(output.exitCode !== undefined ? { exitCode: output.exitCode } : {}),
|
|
67
97
|
...(output.signal !== undefined ? { signal: output.signal } : {}),
|
package/dist/cli.js
CHANGED
|
@@ -17,7 +17,7 @@ import { SubagentDeliveryMailbox } from "./subagents/sessions/delivery-mailbox.j
|
|
|
17
17
|
import { SubagentSessionManager } from "./subagents/sessions/manager.js";
|
|
18
18
|
import { formatSubagentProviderAvailabilitySummary } from "./subagents/providers/availability.js";
|
|
19
19
|
import { parseSubagentRunArgs } from "./subagents/cli-target.js";
|
|
20
|
-
import { ensureForgeRelayInstanceId, generateInstanceId, generateOwnerToken,
|
|
20
|
+
import { ensureForgeRelayInstanceId, generateInstanceId, generateOwnerToken, loadForgeRelayFiles, removeForgeRelayRemote, renameForgeRelayRemote, resolveSubagentsFlag, writeForgeRelayAuth, writeForgeRelayConfig, writeForgeRelayRemote, } from "./user-config.js";
|
|
21
21
|
import { expandHomePath } from "./roots.js";
|
|
22
22
|
import { shutdownHttpServer } from "./server-shutdown.js";
|
|
23
23
|
import { publicEndpointUrl } from "./oauth/public-url.js";
|
|
@@ -72,12 +72,12 @@ function normalizeCommand(command) {
|
|
|
72
72
|
throw new Error(`Unknown command: ${command}`);
|
|
73
73
|
}
|
|
74
74
|
async function ensureConfigured() {
|
|
75
|
-
const files =
|
|
75
|
+
const files = loadForgeRelayFiles();
|
|
76
76
|
if (files.configExists && files.authExists) {
|
|
77
77
|
ensureForgeRelayInstanceId();
|
|
78
78
|
return;
|
|
79
79
|
}
|
|
80
|
-
if (process.env.FORGERELAY_OAUTH_OWNER_TOKEN
|
|
80
|
+
if (process.env.FORGERELAY_OAUTH_OWNER_TOKEN) {
|
|
81
81
|
ensureForgeRelayInstanceId();
|
|
82
82
|
return;
|
|
83
83
|
}
|
|
@@ -94,7 +94,7 @@ async function ensureConfigured() {
|
|
|
94
94
|
await runInit({ force: false });
|
|
95
95
|
}
|
|
96
96
|
async function runInit({ force }) {
|
|
97
|
-
const files =
|
|
97
|
+
const files = loadForgeRelayFiles();
|
|
98
98
|
if (!force && files.configExists && files.authExists) {
|
|
99
99
|
prompts.log.info(`ForgeRelay is already configured at ${files.dir}`);
|
|
100
100
|
prompts.log.info("Run `forgerelay init --force` to update it.");
|
|
@@ -155,8 +155,8 @@ async function runInit({ force }) {
|
|
|
155
155
|
ownerToken: files.auth.ownerToken ?? generateOwnerToken(),
|
|
156
156
|
instanceId: files.auth.instanceId ?? generateInstanceId(),
|
|
157
157
|
};
|
|
158
|
-
const configPath =
|
|
159
|
-
const authPath =
|
|
158
|
+
const configPath = writeForgeRelayConfig(config);
|
|
159
|
+
const authPath = writeForgeRelayAuth(auth);
|
|
160
160
|
const lines = [
|
|
161
161
|
`Config: ${configPath}`,
|
|
162
162
|
`Auth: ${authPath}`,
|
|
@@ -285,8 +285,7 @@ async function resolveAuthOwnerToken(ownerToken) {
|
|
|
285
285
|
}
|
|
286
286
|
function localOwnerToken() {
|
|
287
287
|
const token = process.env.FORGERELAY_OAUTH_OWNER_TOKEN
|
|
288
|
-
??
|
|
289
|
-
?? loadDevspaceFiles().auth.ownerToken;
|
|
288
|
+
?? loadForgeRelayFiles().auth.ownerToken;
|
|
290
289
|
if (!token)
|
|
291
290
|
throw new Error("ForgeRelay owner token is not configured on this machine.");
|
|
292
291
|
return token;
|
|
@@ -302,7 +301,7 @@ async function runAuthCommand(args) {
|
|
|
302
301
|
if (subcommand === "list") {
|
|
303
302
|
if (rest.length > 0)
|
|
304
303
|
throw new Error("forgerelay auth list does not accept additional arguments.");
|
|
305
|
-
const remotes =
|
|
304
|
+
const remotes = loadForgeRelayFiles().auth.remotes ?? {};
|
|
306
305
|
if (Object.keys(remotes).length === 0) {
|
|
307
306
|
console.log("No remote ForgeRelay instances registered.");
|
|
308
307
|
return;
|
|
@@ -333,7 +332,7 @@ async function runAuthCommand(args) {
|
|
|
333
332
|
const [alias, ...extra] = rest;
|
|
334
333
|
if (!alias || extra.length > 0)
|
|
335
334
|
throw new Error("Usage: forgerelay auth test <alias>");
|
|
336
|
-
const files =
|
|
335
|
+
const files = loadForgeRelayFiles();
|
|
337
336
|
const storedRemote = files.auth.remotes?.[alias];
|
|
338
337
|
if (!storedRemote)
|
|
339
338
|
throw new Error(`Unknown remote alias: ${alias}`);
|
|
@@ -372,7 +371,7 @@ async function runAuthCommand(args) {
|
|
|
372
371
|
target,
|
|
373
372
|
...(parsed.sshRoute ? { sshRoute: parsed.sshRoute } : {}),
|
|
374
373
|
};
|
|
375
|
-
const files =
|
|
374
|
+
const files = loadForgeRelayFiles();
|
|
376
375
|
const existingAlias = Object.entries(files.auth.remotes ?? {}).find(([, record]) => record.instanceId === remote.instanceId)?.[0];
|
|
377
376
|
const defaultAlias = parsed.sshRoute
|
|
378
377
|
? defaultSshRouteAlias(parsed.sshRoute)
|
|
@@ -385,7 +384,7 @@ async function runAuthCommand(args) {
|
|
|
385
384
|
console.log(`Authenticated remote ${alias} (${remote.instanceId}).`);
|
|
386
385
|
}
|
|
387
386
|
async function runDoctor() {
|
|
388
|
-
const files =
|
|
387
|
+
const files = loadForgeRelayFiles();
|
|
389
388
|
console.log(`Config dir: ${files.dir}`);
|
|
390
389
|
console.log(`Config file: ${files.configExists ? files.configPath : "missing"}`);
|
|
391
390
|
console.log(`Auth file: ${files.authExists ? files.authPath : "missing"}`);
|
|
@@ -416,7 +415,7 @@ async function runDoctor() {
|
|
|
416
415
|
}
|
|
417
416
|
function runConfigCommand(args) {
|
|
418
417
|
const [subcommand, key, ...rest] = args;
|
|
419
|
-
const files =
|
|
418
|
+
const files = loadForgeRelayFiles();
|
|
420
419
|
if (!subcommand || subcommand === "get") {
|
|
421
420
|
console.log(JSON.stringify(files.config, null, 2));
|
|
422
421
|
return;
|
|
@@ -430,7 +429,7 @@ function runConfigCommand(args) {
|
|
|
430
429
|
const value = rest.join(" ").trim();
|
|
431
430
|
if (!value)
|
|
432
431
|
throw new Error("Missing publicBaseUrl value.");
|
|
433
|
-
|
|
432
|
+
writeForgeRelayConfig({
|
|
434
433
|
...files.config,
|
|
435
434
|
publicBaseUrl: normalizeOptionalPublicBaseUrl(value),
|
|
436
435
|
});
|
|
@@ -517,7 +516,7 @@ async function runAgentsRun(args) {
|
|
|
517
516
|
const started = existing
|
|
518
517
|
? manager.resume({ sessionId: existing.id, prompt: parsed.prompt })
|
|
519
518
|
: await manager.start({
|
|
520
|
-
workspaceId: process.env.FORGERELAY_WORKSPACE_ID
|
|
519
|
+
workspaceId: process.env.FORGERELAY_WORKSPACE_ID,
|
|
521
520
|
workspaceRoot,
|
|
522
521
|
target: parsed.target,
|
|
523
522
|
prompt: parsed.prompt,
|
|
@@ -609,11 +608,11 @@ function writeSubagentPromptFile(prompt) {
|
|
|
609
608
|
return filePath;
|
|
610
609
|
}
|
|
611
610
|
function resolveCurrentWorkspaceRoot() {
|
|
612
|
-
return resolve(process.env.FORGERELAY_WORKSPACE_ROOT ?? process.
|
|
611
|
+
return resolve(process.env.FORGERELAY_WORKSPACE_ROOT ?? process.cwd());
|
|
613
612
|
}
|
|
614
613
|
function resolveCurrentWorkspaceScope() {
|
|
615
614
|
return {
|
|
616
|
-
workspaceId: process.env.FORGERELAY_WORKSPACE_ID
|
|
615
|
+
workspaceId: process.env.FORGERELAY_WORKSPACE_ID,
|
|
617
616
|
workspaceRoot: resolveCurrentWorkspaceRoot(),
|
|
618
617
|
};
|
|
619
618
|
}
|