@akira-tl/forgerelay 0.5.6 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +43 -0
- package/README.md +7 -3
- package/capabilities/host-integration/GUIDE.md +15 -9
- package/dist/activity/host-turn-store.js +9 -7
- package/dist/activity/lifecycle.js +1 -1
- package/dist/activity/mcp-query-tools.js +110 -18
- package/dist/activity/query-service.js +4 -4
- package/dist/activity/ui/contract.js +2 -0
- package/dist/cli.js +47 -32
- package/dist/config.js +31 -3
- package/dist/db/migrations.js +30 -0
- package/dist/db/schema.js +2 -0
- package/dist/logger.js +6 -5
- package/dist/lsp/test-support/server-fixture.js +1 -1
- package/dist/mcp/server-instructions.js +7 -6
- package/dist/mcp-app-template.js +108 -12
- package/dist/oauth/public-url.js +14 -0
- package/dist/oauth/router.js +49 -0
- package/dist/process-sessions.js +2 -1
- package/dist/request-meta.js +4 -0
- package/dist/server.js +250 -84
- package/dist/skills.js +58 -1
- package/dist/ui/.vite/manifest.json +360 -312
- package/dist/ui/activity-panel-app.html +17 -0
- package/dist/ui/assets/activity-panel-app-CjZVvVNc.js +2 -0
- package/dist/ui/assets/angular-html-BkgDQl-G.js +1 -0
- package/dist/ui/assets/{angular-ts-BwPE8YDt.js → angular-ts-Bxh4-O1-.js} +1 -1
- package/dist/ui/assets/{apl-CGbONVvU.js → apl-BmYHKQ6m.js} +1 -1
- package/dist/ui/assets/{astro-DIAPy7cj.js → astro-C51vqDKq.js} +1 -1
- package/dist/ui/assets/{blade-Cgtd19Ky.js → blade-CdhVsmwq.js} +1 -1
- package/dist/ui/assets/c-IBzBpxAO.js +1 -0
- package/dist/ui/assets/chunk-EyZ2wyi3.js +1 -0
- package/dist/ui/assets/{cobol-G-7HLVKW.js → cobol-Cf6azoN_.js} +1 -1
- package/dist/ui/assets/{coffee-DQFyKxEB.js → coffee-vVdGzlmt.js} +1 -1
- package/dist/ui/assets/cpp-CVHeO-Jk.js +1 -0
- package/dist/ui/assets/{crystal-DuiWhEO3.js → crystal-5UZy6KIX.js} +1 -1
- package/dist/ui/assets/css-DEfus9J5.js +1 -0
- package/dist/ui/assets/{edge-COkalhFi.js → edge-CESgo4ia.js} +1 -1
- package/dist/ui/assets/{elixir-cdlzbYR1.js → elixir-9FKfw4cJ.js} +1 -1
- package/dist/ui/assets/{elm-ZwDYuYpb.js → elm-Dv7A0aLa.js} +1 -1
- package/dist/ui/assets/{erb-C8WG9TtC.js → erb-DHFmDvM0.js} +1 -1
- package/dist/ui/assets/{git-rebase-CMlDVJBT.js → git-rebase-utbU6CzV.js} +1 -1
- package/dist/ui/assets/{glimmer-js-DeLOQlol.js → glimmer-js-t2kiCkyB.js} +1 -1
- package/dist/ui/assets/{glimmer-ts-DTEM04TK.js → glimmer-ts-F0Jv5okV.js} +1 -1
- package/dist/ui/assets/glsl-_ooCjaeB.js +1 -0
- package/dist/ui/assets/graphql-B9LqP-cY.js +1 -0
- package/dist/ui/assets/{hack-C14sh6Lh.js → hack-8yjdbaSA.js} +1 -1
- package/dist/ui/assets/haml-BU5EoZBn.js +1 -0
- package/dist/ui/assets/{handlebars-eS-J0O-M.js → handlebars-BBFEg4Lo.js} +1 -1
- package/dist/ui/assets/{heavy-payload-BKB0QDOI.js → heavy-payload-vGgBRvNX.js} +1 -1
- package/dist/ui/assets/html-D-C6YZk4.js +1 -0
- package/dist/ui/assets/{html-derivative-BlVZU2id.js → html-derivative-DJAQcZyc.js} +1 -1
- package/dist/ui/assets/{http-BmbZzA5x.js → http-Dg5LSYRK.js} +1 -1
- package/dist/ui/assets/{hurl-Dnd_YpOD.js → hurl-Y5uYzMZR.js} +1 -1
- package/dist/ui/assets/java-ppHvycj5.js +1 -0
- package/dist/ui/assets/javascript-DDTjKLlQ.js +1 -0
- package/dist/ui/assets/{jinja-DTvkZdHA.js → jinja-Bt30h8Ik.js} +1 -1
- package/dist/ui/assets/{jison-DUJG-tmh.js → jison-D5uZ1Nyo.js} +1 -1
- package/dist/ui/assets/json-Cw91r8h1.js +1 -0
- package/dist/ui/assets/jsx-04X2FT_1.js +1 -0
- package/dist/ui/assets/{julia-P5B4pHFa.js → julia-76hRaaGz.js} +1 -1
- package/dist/ui/assets/{just-DYaoh7C6.js → just-CDE7W0vW.js} +1 -1
- package/dist/ui/assets/{latex-CI5lnkWV.js → latex-CVO2Byab.js} +1 -1
- package/dist/ui/assets/{liquid-D3LnAs_z.js → liquid-CA-AS3EX.js} +1 -1
- package/dist/ui/assets/lua-CiyBp52Y.js +1 -0
- package/dist/ui/assets/{marko-CRDrjqfS.js → marko-_BWrPfny.js} +1 -1
- package/dist/ui/assets/{mdc-ZzwgmJVn.js → mdc-PyJv85eS.js} +1 -1
- package/dist/ui/assets/{nginx-BAvFXGSV.js → nginx-DAWVw8yu.js} +1 -1
- package/dist/ui/assets/{nim-BHIFawe9.js → nim-CKE1dzpU.js} +1 -1
- package/dist/ui/assets/{perl-C_f5AnLa.js → perl-BBhWva19.js} +1 -1
- package/dist/ui/assets/{php-DDqpegL5.js → php-ChIeaAoF.js} +1 -1
- package/dist/ui/assets/{pug-CSaAZXyd.js → pug-CMgI5afi.js} +1 -1
- package/dist/ui/assets/{qml-CNHvRs_z.js → qml-CjxTkcFH.js} +1 -1
- package/dist/ui/assets/r-DfFga4si.js +1 -0
- package/dist/ui/assets/{razor-BDfAPy7I.js → razor-i4DbHr8p.js} +1 -1
- package/dist/ui/assets/regexp-iC9_nbSI.js +1 -0
- package/dist/ui/assets/{review-payload-D7GIdYDj.js → review-payload-4erWKckt.js} +1 -1
- package/dist/ui/assets/{rst-BzPP2gle.js → rst-oy_97Kl4.js} +1 -1
- package/dist/ui/assets/{ruby-CQjzokzX.js → ruby-C5zYZIyl.js} +1 -1
- package/dist/ui/assets/{sas-Bp37KGr7.js → sas-BnMEh2S3.js} +1 -1
- package/dist/ui/assets/{scrollbar-HUMBdLLK.js → scrollbar-CaOPzUJd.js} +6 -6
- package/dist/ui/assets/scss-BfZXsAWQ.js +1 -0
- package/dist/ui/assets/shellscript-BC2srNaB.js +1 -0
- package/dist/ui/assets/{shellsession-CNc62UhY.js → shellsession-BpE5o7_3.js} +1 -1
- package/dist/ui/assets/{soy-CF44SpBo.js → soy-COesmhTY.js} +1 -1
- package/dist/ui/assets/sql-Ccv_yeUm.js +1 -0
- package/dist/ui/assets/{stata-DVeMA68W.js → stata-CGQEuSZq.js} +1 -1
- package/dist/ui/assets/{surrealql-52sF96Yv.js → surrealql-Bz4JbU-9.js} +1 -1
- package/dist/ui/assets/{svelte-ydwT7UbF.js → svelte-1jM2ejXj.js} +1 -1
- package/dist/ui/assets/{templ-BXcoVoGh.js → templ-C-VwELcp.js} +1 -1
- package/dist/ui/assets/{tex-COsYFp7O.js → tex-t1rUiQCm.js} +1 -1
- package/dist/ui/assets/{ts-tags-CF--LE1s.js → ts-tags-Bewoa6pS.js} +1 -1
- package/dist/ui/assets/tsx-CTXzrBkN.js +1 -0
- package/dist/ui/assets/{twig-YoM-DiI5.js → twig-DVg-vELl.js} +1 -1
- package/dist/ui/assets/typescript--ElHoFkw.js +1 -0
- package/dist/ui/assets/{vue-html-CPdGGk4T.js → vue-html-BHtJ8geX.js} +1 -1
- package/dist/ui/assets/{vue-vine-B5bjgk0a.js → vue-vine-CNSGAWwc.js} +1 -1
- package/dist/ui/assets/{vue-B5LDEy4l.js → vue-wPQ7UlMy.js} +1 -1
- package/dist/ui/assets/workspace-app-CcrHAUIn.css +1 -0
- package/dist/ui/assets/workspace-app-DJmkPYJC.js +1 -0
- package/dist/ui/assets/workspace-app-DkAiSl_0.js +111 -0
- package/dist/ui/assets/workspace-app-QyauBrJX.js +5 -0
- package/dist/ui/assets/workspace-lifecycle-app-BIXEo53I.js +1 -0
- package/dist/ui/assets/xml-JnmX6vyS.js +1 -0
- package/dist/ui/assets/{xsl-wls_CEK_.js → xsl-3_vJ27aE.js} +1 -1
- package/dist/ui/assets/yaml-OAcZkP-w.js +1 -0
- package/dist/ui/workspace-app.html +5 -2
- package/dist/ui/workspace-lifecycle-app.html +18 -0
- package/dist/workspaces.js +11 -0
- package/docs/configuration.md +36 -4
- package/docs/debugging.md +75 -17
- package/docs/gotchas.md +15 -9
- package/docs/setup.md +24 -10
- package/package.json +4 -3
- package/scripts/debug/accept.mjs +79 -27
- package/scripts/debug/host/run.sh +180 -0
- package/scripts/debug/host/serve.mjs +64 -0
- package/scripts/debug/runtime.mjs +57 -2
- package/scripts/debug/runtime.test.mjs +74 -0
- package/scripts/debug/serve.mjs +4 -5
- package/scripts/dev-server.mjs +32 -3
- package/scripts/release/release-gate.test.mjs +43 -0
- package/scripts/release/release-version.test.mjs +107 -0
- package/scripts/release-parity.mjs +70 -46
- package/scripts/release-proof.mjs +2 -2
- package/scripts/release-proof.test.mjs +18 -0
- package/scripts/release-version.mjs +17 -8
- package/dist/ui/assets/angular-html-BT2Z6jee.js +0 -1
- package/dist/ui/assets/c-acnvZ8tm.js +0 -1
- package/dist/ui/assets/cpp-UyrmHXan.js +0 -1
- package/dist/ui/assets/css-D_WCbY6m.js +0 -1
- package/dist/ui/assets/glsl-IJV4SB-M.js +0 -1
- package/dist/ui/assets/graphql-PM-WDnoB.js +0 -1
- package/dist/ui/assets/haml-B_5nS6Kx.js +0 -1
- package/dist/ui/assets/html-C3TPjrC5.js +0 -1
- package/dist/ui/assets/java-ne9HdIVn.js +0 -1
- package/dist/ui/assets/javascript-DpHT58rr.js +0 -1
- package/dist/ui/assets/json-DJmnBdxW.js +0 -1
- package/dist/ui/assets/jsx-BclJnZnN.js +0 -1
- package/dist/ui/assets/lua-dZC7ZQIY.js +0 -1
- package/dist/ui/assets/r-CAfoQND2.js +0 -1
- package/dist/ui/assets/regexp-Mug5jltQ.js +0 -1
- package/dist/ui/assets/scss-C_Z1DYnw.js +0 -1
- package/dist/ui/assets/shellscript-CLfJpn3I.js +0 -1
- package/dist/ui/assets/sql-CYnzfiQ1.js +0 -1
- package/dist/ui/assets/tsx-DLBt2-2f.js +0 -1
- package/dist/ui/assets/typescript-Ce9JGkNI.js +0 -1
- package/dist/ui/assets/workspace-app-Cm9EX5x4.js +0 -117
- package/dist/ui/assets/workspace-app-DHvE2ojW.css +0 -1
- package/dist/ui/assets/xml-Cun8xb0T.js +0 -1
- package/dist/ui/assets/yaml-BxuXn9OL.js +0 -1
- package/scripts/release/tag-local-ci.mjs +0 -144
package/dist/db/migrations.js
CHANGED
|
@@ -59,6 +59,11 @@ const migrations = [
|
|
|
59
59
|
name: "bash-output-audit-columns",
|
|
60
60
|
up: migrateBashOutputAuditColumns,
|
|
61
61
|
},
|
|
62
|
+
{
|
|
63
|
+
version: 13,
|
|
64
|
+
name: "activity-host-turn-workspace",
|
|
65
|
+
up: migrateActivityHostTurnWorkspace,
|
|
66
|
+
},
|
|
62
67
|
];
|
|
63
68
|
export function migrateDatabase(sqlite) {
|
|
64
69
|
const migrate = sqlite.transaction(() => {
|
|
@@ -339,6 +344,31 @@ function migrateBashOutputAuditColumns(sqlite) {
|
|
|
339
344
|
addColumnIfMissing(sqlite, "bash_output_streams", "returned", "integer not null default 0");
|
|
340
345
|
addColumnIfMissing(sqlite, "bash_output_streams", "completion_claimed_at", "text");
|
|
341
346
|
}
|
|
347
|
+
function migrateActivityHostTurnWorkspace(sqlite) {
|
|
348
|
+
migrateActivityHostTurns(sqlite);
|
|
349
|
+
addColumnIfMissing(sqlite, "activity_host_turns", "workspace_id", "text");
|
|
350
|
+
if (tableExists(sqlite, "activity_audit_events")) {
|
|
351
|
+
sqlite.exec(`
|
|
352
|
+
update activity_host_turns
|
|
353
|
+
set workspace_id = (
|
|
354
|
+
select workspace_id
|
|
355
|
+
from activity_audit_events
|
|
356
|
+
where activity_audit_events.turn_id = activity_host_turns.turn_id
|
|
357
|
+
and workspace_id is not null
|
|
358
|
+
order by created_at asc
|
|
359
|
+
limit 1
|
|
360
|
+
)
|
|
361
|
+
where workspace_id is null;
|
|
362
|
+
`);
|
|
363
|
+
}
|
|
364
|
+
sqlite.exec(`
|
|
365
|
+
create index if not exists activity_host_turns_conversation_workspace_idx
|
|
366
|
+
on activity_host_turns(conversation_scope_id, workspace_id, created_at desc);
|
|
367
|
+
`);
|
|
368
|
+
}
|
|
369
|
+
function tableExists(sqlite, table) {
|
|
370
|
+
return Boolean(sqlite.prepare("select 1 from sqlite_master where type = 'table' and name = ?").get(table));
|
|
371
|
+
}
|
|
342
372
|
function addColumnIfMissing(sqlite, table, column, definition) {
|
|
343
373
|
const columns = sqlite.prepare(`pragma table_info(${table})`).all();
|
|
344
374
|
if (columns.some((existingColumn) => existingColumn.name === column))
|
package/dist/db/schema.js
CHANGED
|
@@ -100,9 +100,11 @@ export const activityAuditEvents = sqliteTable("activity_audit_events", {
|
|
|
100
100
|
export const activityHostTurns = sqliteTable("activity_host_turns", {
|
|
101
101
|
turnId: text("turn_id").primaryKey(),
|
|
102
102
|
conversationScopeId: text("conversation_scope_id"),
|
|
103
|
+
workspaceId: text("workspace_id"),
|
|
103
104
|
createdAt: text("created_at").notNull(),
|
|
104
105
|
}, (table) => [
|
|
105
106
|
index("activity_host_turns_conversation_idx").on(table.conversationScopeId, table.createdAt),
|
|
107
|
+
index("activity_host_turns_conversation_workspace_idx").on(table.conversationScopeId, table.workspaceId, table.createdAt),
|
|
106
108
|
index("activity_host_turns_created_idx").on(table.createdAt),
|
|
107
109
|
]);
|
|
108
110
|
export const bashOutputStreams = sqliteTable("bash_output_streams", {
|
package/dist/logger.js
CHANGED
|
@@ -59,10 +59,7 @@ export function transportSessionIdPrefix(transportSessionId) {
|
|
|
59
59
|
/** @deprecated Use transportSessionIdPrefix. */
|
|
60
60
|
export const sessionIdPrefix = transportSessionIdPrefix;
|
|
61
61
|
export function workspaceLogLabel(root, workspaceId) {
|
|
62
|
-
|
|
63
|
-
? `ws_${workspaceId.slice(3, 11)}`
|
|
64
|
-
: workspaceId.slice(0, 8);
|
|
65
|
-
return `${basename(root)}/${shortWorkspaceId}`;
|
|
62
|
+
return `${basename(root)}/${workspaceId}`;
|
|
66
63
|
}
|
|
67
64
|
export function commandPreview(command) {
|
|
68
65
|
const normalized = command.replace(/\s+/g, " ").trim();
|
|
@@ -181,7 +178,11 @@ function formatHttpMessage(entry, options) {
|
|
|
181
178
|
function formatMcpRequestMessage(entry) {
|
|
182
179
|
const method = stringField(entry.rpcMethod) ?? stringField(entry.httpMethod) ?? "request";
|
|
183
180
|
const target = stringField(entry.rpcTarget);
|
|
184
|
-
|
|
181
|
+
const metaKeys = Array.isArray(entry.rpcMetaKeys)
|
|
182
|
+
? entry.rpcMetaKeys.filter((value) => typeof value === "string" && value.length > 0)
|
|
183
|
+
: [];
|
|
184
|
+
const operation = target ? `mcp ${method} ${target}` : `mcp ${method}`;
|
|
185
|
+
return metaKeys.length > 0 ? `${operation} meta=[${metaKeys.join(",")}]` : operation;
|
|
185
186
|
}
|
|
186
187
|
function formatAppTemplateMessage(entry, options, failed) {
|
|
187
188
|
const requestedUri = stringField(entry.requestedUri) ?? "unknown";
|
|
@@ -43,7 +43,7 @@ export async function createCodeIntelligenceServerFixture(t, options = {}) {
|
|
|
43
43
|
const activityQueries = new ActivityQueryService(hostTurnStore, auditStore, bashOutputStore);
|
|
44
44
|
const processSessions = new ProcessManager({ outputAudit: bashOutputStore });
|
|
45
45
|
const activityLifecycle = new ActivityLifecycle(auditStore, {
|
|
46
|
-
turnIdForConversation: (conversationScopeId) => activityQueries.currentTurnId(conversationScopeId),
|
|
46
|
+
turnIdForConversation: (conversationScopeId, workspaceId) => activityQueries.currentTurnId(conversationScopeId, workspaceId),
|
|
47
47
|
});
|
|
48
48
|
const codeIntelligence = new CodeIntelligenceManager(config, options.codeIntelligenceOptions);
|
|
49
49
|
const server = createMcpServer(config, workspaces, createReviewCheckpointManager(), processSessions, [], [], codeIntelligence, activityLifecycle, bashOutputStore, activityQueries);
|
|
@@ -18,7 +18,7 @@ export function buildServerInstructions(config) {
|
|
|
18
18
|
}
|
|
19
19
|
export function buildToolDescriptions(config) {
|
|
20
20
|
const skillCapability = config.skillsEnabled
|
|
21
|
-
? "
|
|
21
|
+
? " Available skills are loaded through the virtual skills://<name> namespace; after loading a skill, files inside it may be read with skills://<name>/<relative-path>."
|
|
22
22
|
: "";
|
|
23
23
|
const shellSurface = config.toolMode === "codex"
|
|
24
24
|
? ""
|
|
@@ -37,16 +37,17 @@ export function buildToolDescriptions(config) {
|
|
|
37
37
|
function capabilityContractInstructions(config) {
|
|
38
38
|
const staleWorkspacePolicy = config.toolMode === "codex"
|
|
39
39
|
? ""
|
|
40
|
-
: ` If ${toolNames.openWorkspace} reports
|
|
41
|
-
const workspaceLifecycle = `Use ForgeRelay as a local coding workspace. Default to the user's existing checkout. Reuse
|
|
40
|
+
: ` If ${toolNames.openWorkspace} reports stale workspaces, let the user choose resume or ${toolNames.closeWorkspace}; never auto-close.`;
|
|
41
|
+
const workspaceLifecycle = `Use ForgeRelay as a local coding workspace. Default to the user's existing checkout. Reuse workspaceId from ${toolNames.openWorkspace}; change it only when asked.${staleWorkspacePolicy} Only open mode=\"worktree\" when the user explicitly asks for isolated or parallel Git work. ${toolNames.closeWorkspace} releases checkout workspaces or finalizes managed worktrees; managed close requires commitMessage.`;
|
|
42
|
+
const activityPanel = `Project-work order: ${toolNames.openWorkspace} if needed → activity_panel(workspaceId) once → work tools. activity_panel is the single ForgeRelay UI render tool: Workspace above Activity. A new workspaceId creates a new card. Never call activity_panel before needed ${toolNames.openWorkspace}.`;
|
|
42
43
|
const agents = `Follow instructions returned by ${toolNames.openWorkspace}. Read an availableAgentsFiles path before working under it.`;
|
|
43
44
|
const capabilityGuides = `For optional capabilities from ${toolNames.openWorkspace}, use ${toolNames.capability}; if unfamiliar, describe first and read its advertised capability guide with ${toolNames.read}.`;
|
|
44
45
|
const skills = config.skillsEnabled
|
|
45
|
-
? `
|
|
46
|
+
? `For a matching skill from ${toolNames.openWorkspace}, load ${toolNames.read}(path="skills://<name>") first. Skill paths stay internal; loaded skill files use skills://<name>/<relative-path>.`
|
|
46
47
|
: "";
|
|
47
48
|
const shellMutationPolicy = buildShellMutationPolicy();
|
|
48
49
|
const hooks = "When a ForgeRelay tool result reports Hook results, tell the user which meaningful hooks ran and whether they passed or blocked the operation. Do not claim the requested operation succeeded when a blocking hook prevented it.";
|
|
49
|
-
return joinInstructions(workspaceLifecycle, agents, capabilityGuides, skills, shellMutationPolicy, hooks);
|
|
50
|
+
return joinInstructions(workspaceLifecycle, activityPanel, agents, capabilityGuides, skills, shellMutationPolicy, hooks);
|
|
50
51
|
}
|
|
51
52
|
function selectedWorkflowInstructions(config) {
|
|
52
53
|
if (config.workflowInstructions === false)
|
|
@@ -60,7 +61,7 @@ function defaultWorkflowInstructions(config) {
|
|
|
60
61
|
return `Use ${toolNames.read} for direct file reads, ${toolNames.rename} and ${toolNames.delete} for direct path moves or removals, apply_patch for content modifications, exec_command for inspection, tests, builds, and other commands, and ${toolNames.writeStdin} to poll or interact with running processes.`;
|
|
61
62
|
}
|
|
62
63
|
const inspection = `Use ${toolNames.shell} with command-line tools such as grep, rg, find, ls, and tree for search and directory inspection.`;
|
|
63
|
-
return joinInstructions(inspection, `Prefer ${toolNames.edit} for targeted content modifications, ${toolNames.write} only for new files or complete rewrites, ${toolNames.rename} for path moves, ${toolNames.delete} for removals, and ${toolNames.shell} for tests, builds,
|
|
64
|
+
return joinInstructions(inspection, `Prefer ${toolNames.edit} for targeted content modifications, ${toolNames.write} only for new files or complete rewrites, ${toolNames.rename} for path moves, ${toolNames.delete} for removals, and ${toolNames.shell} for tests, builds, Git/package scripts, generators, formatters, and shell-suited commands. If ${toolNames.shell} returns a running processId, use action=\"process\" to poll/wait/interact/resize/interrupt it. When only waiting, set yieldTimeMs instead of short polling; reuse processId if still running. Otherwise keep working and consume its later completion notice.`);
|
|
64
65
|
}
|
|
65
66
|
function joinInstructions(...parts) {
|
|
66
67
|
return parts
|
package/dist/mcp-app-template.js
CHANGED
|
@@ -1,22 +1,88 @@
|
|
|
1
1
|
import { createHash } from "node:crypto";
|
|
2
2
|
import { readFileSync } from "node:fs";
|
|
3
3
|
export const WORKSPACE_APP_MANIFEST_ENTRY = "workspace-app.html";
|
|
4
|
+
export const WORKSPACE_LIFECYCLE_APP_MANIFEST_ENTRY = "workspace-lifecycle-app.html";
|
|
5
|
+
export const ACTIVITY_PANEL_APP_MANIFEST_ENTRY = "activity-panel-app.html";
|
|
6
|
+
// Host caches are keyed by the ui:// resource URI, so the revision must cover
|
|
7
|
+
// both bundle assets and the HTML/resource contract that serves them. Bump this
|
|
8
|
+
// when the MCP App HTML shell or resource-to-entry mapping changes.
|
|
9
|
+
export const MCP_APP_RESOURCE_TEMPLATE_REVISION = "4";
|
|
10
|
+
// Historical mixed-widget resource. Keep serving it so existing ChatGPT Web
|
|
11
|
+
// conversations can still render cards created before the UI split.
|
|
4
12
|
export const WORKSPACE_APP_LEGACY_URI = "ui://forgerelay/workspace-app.html";
|
|
5
13
|
export const WORKSPACE_APP_URI_TEMPLATE = "ui://forgerelay/workspace-app-{revision}.html";
|
|
14
|
+
export const WORKSPACE_LIFECYCLE_APP_LEGACY_URI = "ui://forgerelay/workspace-lifecycle-app.html";
|
|
15
|
+
export const WORKSPACE_LIFECYCLE_APP_URI_TEMPLATE = "ui://forgerelay/workspace-lifecycle-app-{revision}.html";
|
|
16
|
+
export const ACTIVITY_PANEL_APP_LEGACY_URI = "ui://forgerelay/activity-panel-app.html";
|
|
17
|
+
export const ACTIVITY_PANEL_APP_URI_TEMPLATE = "ui://forgerelay/activity-panel-app-{revision}.html";
|
|
6
18
|
export function workspaceAppUriForRevision(revision) {
|
|
7
19
|
return `ui://forgerelay/workspace-app-${encodeURIComponent(revision)}.html`;
|
|
8
20
|
}
|
|
9
|
-
export function
|
|
21
|
+
export function workspaceLifecycleAppUriForRevision(revision) {
|
|
22
|
+
return `ui://forgerelay/workspace-lifecycle-app-${encodeURIComponent(revision)}.html`;
|
|
23
|
+
}
|
|
24
|
+
export function activityPanelAppUriForRevision(revision) {
|
|
25
|
+
return `ui://forgerelay/activity-panel-app-${encodeURIComponent(revision)}.html`;
|
|
26
|
+
}
|
|
27
|
+
export function readMcpAppManifestEntry(manifestUrl, manifestEntry) {
|
|
10
28
|
const manifest = JSON.parse(readFileSync(manifestUrl, "utf8"));
|
|
11
|
-
const entry = manifest[
|
|
29
|
+
const entry = manifest[manifestEntry];
|
|
12
30
|
if (!entry?.file) {
|
|
13
|
-
throw new Error(`Missing ${
|
|
31
|
+
throw new Error(`Missing ${manifestEntry} in UI manifest.`);
|
|
14
32
|
}
|
|
15
|
-
|
|
33
|
+
const css = [];
|
|
34
|
+
const dependencies = [];
|
|
35
|
+
const seenCss = new Set();
|
|
36
|
+
const seenDependencies = new Set();
|
|
37
|
+
const visitedEntries = new Set();
|
|
38
|
+
const visit = (key, root = false) => {
|
|
39
|
+
if (visitedEntries.has(key))
|
|
40
|
+
return;
|
|
41
|
+
visitedEntries.add(key);
|
|
42
|
+
const current = manifest[key];
|
|
43
|
+
if (!current?.file) {
|
|
44
|
+
throw new Error(`Missing imported UI manifest entry ${key}.`);
|
|
45
|
+
}
|
|
46
|
+
if (!root && !seenDependencies.has(current.file)) {
|
|
47
|
+
dependencies.push(current.file);
|
|
48
|
+
seenDependencies.add(current.file);
|
|
49
|
+
}
|
|
50
|
+
for (const stylesheet of current.css ?? []) {
|
|
51
|
+
if (seenCss.has(stylesheet))
|
|
52
|
+
continue;
|
|
53
|
+
css.push(stylesheet);
|
|
54
|
+
seenCss.add(stylesheet);
|
|
55
|
+
}
|
|
56
|
+
for (const imported of current.imports ?? [])
|
|
57
|
+
visit(imported);
|
|
58
|
+
};
|
|
59
|
+
visit(manifestEntry, true);
|
|
60
|
+
return {
|
|
61
|
+
file: entry.file,
|
|
62
|
+
...(css.length > 0 ? { css } : {}),
|
|
63
|
+
...(dependencies.length > 0 ? { dependencies } : {}),
|
|
64
|
+
...(entry.isEntry !== undefined ? { isEntry: entry.isEntry } : {}),
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
export function readWorkspaceAppManifestEntry(manifestUrl) {
|
|
68
|
+
return readMcpAppManifestEntry(manifestUrl, WORKSPACE_APP_MANIFEST_ENTRY);
|
|
69
|
+
}
|
|
70
|
+
export function readWorkspaceLifecycleAppManifestEntry(manifestUrl) {
|
|
71
|
+
return readMcpAppManifestEntry(manifestUrl, WORKSPACE_LIFECYCLE_APP_MANIFEST_ENTRY);
|
|
72
|
+
}
|
|
73
|
+
export function readActivityPanelAppManifestEntry(manifestUrl) {
|
|
74
|
+
return readMcpAppManifestEntry(manifestUrl, ACTIVITY_PANEL_APP_MANIFEST_ENTRY);
|
|
16
75
|
}
|
|
17
|
-
export function workspaceAppBundleRevision(entry, buildDirectoryUrl) {
|
|
76
|
+
export function workspaceAppBundleRevision(entry, buildDirectoryUrl, resourceTemplateRevision = MCP_APP_RESOURCE_TEMPLATE_REVISION) {
|
|
18
77
|
const hash = createHash("sha256");
|
|
19
|
-
|
|
78
|
+
hash.update("resource-template\0");
|
|
79
|
+
hash.update(resourceTemplateRevision);
|
|
80
|
+
hash.update("\0");
|
|
81
|
+
const assetPaths = [
|
|
82
|
+
entry.file,
|
|
83
|
+
...(entry.dependencies ?? []),
|
|
84
|
+
...(entry.css ?? []),
|
|
85
|
+
];
|
|
20
86
|
for (const assetPath of assetPaths) {
|
|
21
87
|
hash.update(assetPath);
|
|
22
88
|
hash.update("\0");
|
|
@@ -25,21 +91,51 @@ export function workspaceAppBundleRevision(entry, buildDirectoryUrl) {
|
|
|
25
91
|
}
|
|
26
92
|
return hash.digest("hex").slice(0, 12);
|
|
27
93
|
}
|
|
28
|
-
|
|
94
|
+
function resolveAppIdentity(options) {
|
|
29
95
|
try {
|
|
30
|
-
const entry =
|
|
31
|
-
const revision = workspaceAppBundleRevision(entry, options.buildDirectoryUrl);
|
|
96
|
+
const entry = readMcpAppManifestEntry(options.manifestUrl, options.manifestEntry);
|
|
97
|
+
const revision = workspaceAppBundleRevision(entry, options.buildDirectoryUrl, options.resourceTemplateRevision);
|
|
32
98
|
return {
|
|
33
99
|
revision,
|
|
34
|
-
uri:
|
|
100
|
+
uri: options.uriForRevision(revision),
|
|
35
101
|
source: "bundle",
|
|
36
102
|
};
|
|
37
103
|
}
|
|
38
104
|
catch {
|
|
105
|
+
const revision = options.resourceTemplateRevision
|
|
106
|
+
? createHash("sha256")
|
|
107
|
+
.update("fallback\0")
|
|
108
|
+
.update(options.fallbackRevision)
|
|
109
|
+
.update("\0resource-template\0")
|
|
110
|
+
.update(options.resourceTemplateRevision)
|
|
111
|
+
.digest("hex")
|
|
112
|
+
.slice(0, 12)
|
|
113
|
+
: options.fallbackRevision;
|
|
39
114
|
return {
|
|
40
|
-
revision
|
|
41
|
-
uri:
|
|
115
|
+
revision,
|
|
116
|
+
uri: options.uriForRevision(revision),
|
|
42
117
|
source: "fallback",
|
|
43
118
|
};
|
|
44
119
|
}
|
|
45
120
|
}
|
|
121
|
+
export function resolveWorkspaceAppIdentity(options) {
|
|
122
|
+
return resolveAppIdentity({
|
|
123
|
+
...options,
|
|
124
|
+
manifestEntry: WORKSPACE_APP_MANIFEST_ENTRY,
|
|
125
|
+
uriForRevision: workspaceAppUriForRevision,
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
export function resolveWorkspaceLifecycleAppIdentity(options) {
|
|
129
|
+
return resolveAppIdentity({
|
|
130
|
+
...options,
|
|
131
|
+
manifestEntry: WORKSPACE_LIFECYCLE_APP_MANIFEST_ENTRY,
|
|
132
|
+
uriForRevision: workspaceLifecycleAppUriForRevision,
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
export function resolveActivityPanelAppIdentity(options) {
|
|
136
|
+
return resolveAppIdentity({
|
|
137
|
+
...options,
|
|
138
|
+
manifestEntry: ACTIVITY_PANEL_APP_MANIFEST_ENTRY,
|
|
139
|
+
uriForRevision: activityPanelAppUriForRevision,
|
|
140
|
+
});
|
|
141
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export function publicEndpointUrl(baseUrl, suffix) {
|
|
2
|
+
const url = new URL(baseUrl instanceof URL ? baseUrl.href : baseUrl);
|
|
3
|
+
const basePath = url.pathname.replace(/\/+$/, "");
|
|
4
|
+
const childPath = suffix.replace(/^\/+/, "");
|
|
5
|
+
url.pathname = `${basePath}/${childPath}` || "/";
|
|
6
|
+
url.search = "";
|
|
7
|
+
url.hash = "";
|
|
8
|
+
return url;
|
|
9
|
+
}
|
|
10
|
+
export function oauthAuthorizationServerMetadataPath(issuerUrl) {
|
|
11
|
+
const issuer = new URL(issuerUrl instanceof URL ? issuerUrl.href : issuerUrl);
|
|
12
|
+
const issuerPath = issuer.pathname === "/" ? "" : issuer.pathname.replace(/\/+$/, "");
|
|
13
|
+
return `/.well-known/oauth-authorization-server${issuerPath}`;
|
|
14
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import express from "express";
|
|
2
|
+
import { createOAuthMetadata, getOAuthProtectedResourceMetadataUrl, } from "@modelcontextprotocol/sdk/server/auth/router.js";
|
|
3
|
+
import { authorizationHandler } from "@modelcontextprotocol/sdk/server/auth/handlers/authorize.js";
|
|
4
|
+
import { tokenHandler } from "@modelcontextprotocol/sdk/server/auth/handlers/token.js";
|
|
5
|
+
import { clientRegistrationHandler } from "@modelcontextprotocol/sdk/server/auth/handlers/register.js";
|
|
6
|
+
import { revocationHandler } from "@modelcontextprotocol/sdk/server/auth/handlers/revoke.js";
|
|
7
|
+
import { metadataHandler } from "@modelcontextprotocol/sdk/server/auth/handlers/metadata.js";
|
|
8
|
+
import { oauthAuthorizationServerMetadataPath, publicEndpointUrl, } from "./public-url.js";
|
|
9
|
+
export function createForgeRelayAuthRouter(options) {
|
|
10
|
+
const { provider, issuerUrl, resourceServerUrl, scopesSupported, resourceName } = options;
|
|
11
|
+
const authorizationEndpoint = publicEndpointUrl(issuerUrl, "authorize");
|
|
12
|
+
const tokenEndpoint = publicEndpointUrl(issuerUrl, "token");
|
|
13
|
+
const registrationEndpoint = provider.clientsStore.registerClient
|
|
14
|
+
? publicEndpointUrl(issuerUrl, "register")
|
|
15
|
+
: undefined;
|
|
16
|
+
const revocationEndpoint = provider.revokeToken
|
|
17
|
+
? publicEndpointUrl(issuerUrl, "revoke")
|
|
18
|
+
: undefined;
|
|
19
|
+
const oauthMetadata = {
|
|
20
|
+
...createOAuthMetadata({
|
|
21
|
+
provider,
|
|
22
|
+
issuerUrl,
|
|
23
|
+
baseUrl: issuerUrl,
|
|
24
|
+
scopesSupported,
|
|
25
|
+
}),
|
|
26
|
+
authorization_endpoint: authorizationEndpoint.href,
|
|
27
|
+
token_endpoint: tokenEndpoint.href,
|
|
28
|
+
registration_endpoint: registrationEndpoint?.href,
|
|
29
|
+
revocation_endpoint: revocationEndpoint?.href,
|
|
30
|
+
};
|
|
31
|
+
const protectedResourceMetadata = {
|
|
32
|
+
resource: resourceServerUrl.href,
|
|
33
|
+
authorization_servers: [issuerUrl.href],
|
|
34
|
+
scopes_supported: scopesSupported,
|
|
35
|
+
resource_name: resourceName,
|
|
36
|
+
};
|
|
37
|
+
const router = express.Router();
|
|
38
|
+
router.use("/authorize", authorizationHandler({ provider }));
|
|
39
|
+
router.use("/token", tokenHandler({ provider }));
|
|
40
|
+
if (provider.clientsStore.registerClient) {
|
|
41
|
+
router.use("/register", clientRegistrationHandler({ clientsStore: provider.clientsStore }));
|
|
42
|
+
}
|
|
43
|
+
if (provider.revokeToken) {
|
|
44
|
+
router.use("/revoke", revocationHandler({ provider }));
|
|
45
|
+
}
|
|
46
|
+
router.use(oauthAuthorizationServerMetadataPath(issuerUrl), metadataHandler(oauthMetadata));
|
|
47
|
+
router.use(new URL(getOAuthProtectedResourceMetadataUrl(resourceServerUrl)).pathname, metadataHandler(protectedResourceMetadata));
|
|
48
|
+
return router;
|
|
49
|
+
}
|
package/dist/process-sessions.js
CHANGED
|
@@ -290,7 +290,8 @@ export class ProcessManager {
|
|
|
290
290
|
const writableChars = chars.replaceAll("\u0003", "");
|
|
291
291
|
if (writableChars && processEntry.running)
|
|
292
292
|
processEntry.process?.write(writableChars);
|
|
293
|
-
|
|
293
|
+
const explicitWaitRequested = input.yieldTimeMs !== undefined;
|
|
294
|
+
if ((explicitWaitRequested || interactionRequested || !processEntry.buffer.hasOutput()) && processEntry.running) {
|
|
294
295
|
const fallback = interactionRequested ? DEFAULT_INTERACTIVE_YIELD_MS : DEFAULT_POLL_YIELD_MS;
|
|
295
296
|
const maximum = interactionRequested ? MAX_COMMAND_YIELD_MS : MAX_POLL_YIELD_MS;
|
|
296
297
|
const yieldTimeMs = boundedInteger(input.yieldTimeMs, fallback, maximum);
|
package/dist/request-meta.js
CHANGED
|
@@ -7,3 +7,7 @@ function metadataString(meta, key) {
|
|
|
7
7
|
export function openAiConversationScopeId(meta) {
|
|
8
8
|
return metadataString(meta, "openai/session");
|
|
9
9
|
}
|
|
10
|
+
export function hostConversationScopeId(meta, transportSessionId, connectionScopeId) {
|
|
11
|
+
return openAiConversationScopeId(meta)
|
|
12
|
+
?? (transportSessionId ? `mcp-session:${transportSessionId}` : connectionScopeId);
|
|
13
|
+
}
|