@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.
Files changed (152) hide show
  1. package/CHANGELOG.md +43 -0
  2. package/README.md +7 -3
  3. package/capabilities/host-integration/GUIDE.md +15 -9
  4. package/dist/activity/host-turn-store.js +9 -7
  5. package/dist/activity/lifecycle.js +1 -1
  6. package/dist/activity/mcp-query-tools.js +110 -18
  7. package/dist/activity/query-service.js +4 -4
  8. package/dist/activity/ui/contract.js +2 -0
  9. package/dist/cli.js +47 -32
  10. package/dist/config.js +31 -3
  11. package/dist/db/migrations.js +30 -0
  12. package/dist/db/schema.js +2 -0
  13. package/dist/logger.js +6 -5
  14. package/dist/lsp/test-support/server-fixture.js +1 -1
  15. package/dist/mcp/server-instructions.js +7 -6
  16. package/dist/mcp-app-template.js +108 -12
  17. package/dist/oauth/public-url.js +14 -0
  18. package/dist/oauth/router.js +49 -0
  19. package/dist/process-sessions.js +2 -1
  20. package/dist/request-meta.js +4 -0
  21. package/dist/server.js +250 -84
  22. package/dist/skills.js +58 -1
  23. package/dist/ui/.vite/manifest.json +360 -312
  24. package/dist/ui/activity-panel-app.html +17 -0
  25. package/dist/ui/assets/activity-panel-app-CjZVvVNc.js +2 -0
  26. package/dist/ui/assets/angular-html-BkgDQl-G.js +1 -0
  27. package/dist/ui/assets/{angular-ts-BwPE8YDt.js → angular-ts-Bxh4-O1-.js} +1 -1
  28. package/dist/ui/assets/{apl-CGbONVvU.js → apl-BmYHKQ6m.js} +1 -1
  29. package/dist/ui/assets/{astro-DIAPy7cj.js → astro-C51vqDKq.js} +1 -1
  30. package/dist/ui/assets/{blade-Cgtd19Ky.js → blade-CdhVsmwq.js} +1 -1
  31. package/dist/ui/assets/c-IBzBpxAO.js +1 -0
  32. package/dist/ui/assets/chunk-EyZ2wyi3.js +1 -0
  33. package/dist/ui/assets/{cobol-G-7HLVKW.js → cobol-Cf6azoN_.js} +1 -1
  34. package/dist/ui/assets/{coffee-DQFyKxEB.js → coffee-vVdGzlmt.js} +1 -1
  35. package/dist/ui/assets/cpp-CVHeO-Jk.js +1 -0
  36. package/dist/ui/assets/{crystal-DuiWhEO3.js → crystal-5UZy6KIX.js} +1 -1
  37. package/dist/ui/assets/css-DEfus9J5.js +1 -0
  38. package/dist/ui/assets/{edge-COkalhFi.js → edge-CESgo4ia.js} +1 -1
  39. package/dist/ui/assets/{elixir-cdlzbYR1.js → elixir-9FKfw4cJ.js} +1 -1
  40. package/dist/ui/assets/{elm-ZwDYuYpb.js → elm-Dv7A0aLa.js} +1 -1
  41. package/dist/ui/assets/{erb-C8WG9TtC.js → erb-DHFmDvM0.js} +1 -1
  42. package/dist/ui/assets/{git-rebase-CMlDVJBT.js → git-rebase-utbU6CzV.js} +1 -1
  43. package/dist/ui/assets/{glimmer-js-DeLOQlol.js → glimmer-js-t2kiCkyB.js} +1 -1
  44. package/dist/ui/assets/{glimmer-ts-DTEM04TK.js → glimmer-ts-F0Jv5okV.js} +1 -1
  45. package/dist/ui/assets/glsl-_ooCjaeB.js +1 -0
  46. package/dist/ui/assets/graphql-B9LqP-cY.js +1 -0
  47. package/dist/ui/assets/{hack-C14sh6Lh.js → hack-8yjdbaSA.js} +1 -1
  48. package/dist/ui/assets/haml-BU5EoZBn.js +1 -0
  49. package/dist/ui/assets/{handlebars-eS-J0O-M.js → handlebars-BBFEg4Lo.js} +1 -1
  50. package/dist/ui/assets/{heavy-payload-BKB0QDOI.js → heavy-payload-vGgBRvNX.js} +1 -1
  51. package/dist/ui/assets/html-D-C6YZk4.js +1 -0
  52. package/dist/ui/assets/{html-derivative-BlVZU2id.js → html-derivative-DJAQcZyc.js} +1 -1
  53. package/dist/ui/assets/{http-BmbZzA5x.js → http-Dg5LSYRK.js} +1 -1
  54. package/dist/ui/assets/{hurl-Dnd_YpOD.js → hurl-Y5uYzMZR.js} +1 -1
  55. package/dist/ui/assets/java-ppHvycj5.js +1 -0
  56. package/dist/ui/assets/javascript-DDTjKLlQ.js +1 -0
  57. package/dist/ui/assets/{jinja-DTvkZdHA.js → jinja-Bt30h8Ik.js} +1 -1
  58. package/dist/ui/assets/{jison-DUJG-tmh.js → jison-D5uZ1Nyo.js} +1 -1
  59. package/dist/ui/assets/json-Cw91r8h1.js +1 -0
  60. package/dist/ui/assets/jsx-04X2FT_1.js +1 -0
  61. package/dist/ui/assets/{julia-P5B4pHFa.js → julia-76hRaaGz.js} +1 -1
  62. package/dist/ui/assets/{just-DYaoh7C6.js → just-CDE7W0vW.js} +1 -1
  63. package/dist/ui/assets/{latex-CI5lnkWV.js → latex-CVO2Byab.js} +1 -1
  64. package/dist/ui/assets/{liquid-D3LnAs_z.js → liquid-CA-AS3EX.js} +1 -1
  65. package/dist/ui/assets/lua-CiyBp52Y.js +1 -0
  66. package/dist/ui/assets/{marko-CRDrjqfS.js → marko-_BWrPfny.js} +1 -1
  67. package/dist/ui/assets/{mdc-ZzwgmJVn.js → mdc-PyJv85eS.js} +1 -1
  68. package/dist/ui/assets/{nginx-BAvFXGSV.js → nginx-DAWVw8yu.js} +1 -1
  69. package/dist/ui/assets/{nim-BHIFawe9.js → nim-CKE1dzpU.js} +1 -1
  70. package/dist/ui/assets/{perl-C_f5AnLa.js → perl-BBhWva19.js} +1 -1
  71. package/dist/ui/assets/{php-DDqpegL5.js → php-ChIeaAoF.js} +1 -1
  72. package/dist/ui/assets/{pug-CSaAZXyd.js → pug-CMgI5afi.js} +1 -1
  73. package/dist/ui/assets/{qml-CNHvRs_z.js → qml-CjxTkcFH.js} +1 -1
  74. package/dist/ui/assets/r-DfFga4si.js +1 -0
  75. package/dist/ui/assets/{razor-BDfAPy7I.js → razor-i4DbHr8p.js} +1 -1
  76. package/dist/ui/assets/regexp-iC9_nbSI.js +1 -0
  77. package/dist/ui/assets/{review-payload-D7GIdYDj.js → review-payload-4erWKckt.js} +1 -1
  78. package/dist/ui/assets/{rst-BzPP2gle.js → rst-oy_97Kl4.js} +1 -1
  79. package/dist/ui/assets/{ruby-CQjzokzX.js → ruby-C5zYZIyl.js} +1 -1
  80. package/dist/ui/assets/{sas-Bp37KGr7.js → sas-BnMEh2S3.js} +1 -1
  81. package/dist/ui/assets/{scrollbar-HUMBdLLK.js → scrollbar-CaOPzUJd.js} +6 -6
  82. package/dist/ui/assets/scss-BfZXsAWQ.js +1 -0
  83. package/dist/ui/assets/shellscript-BC2srNaB.js +1 -0
  84. package/dist/ui/assets/{shellsession-CNc62UhY.js → shellsession-BpE5o7_3.js} +1 -1
  85. package/dist/ui/assets/{soy-CF44SpBo.js → soy-COesmhTY.js} +1 -1
  86. package/dist/ui/assets/sql-Ccv_yeUm.js +1 -0
  87. package/dist/ui/assets/{stata-DVeMA68W.js → stata-CGQEuSZq.js} +1 -1
  88. package/dist/ui/assets/{surrealql-52sF96Yv.js → surrealql-Bz4JbU-9.js} +1 -1
  89. package/dist/ui/assets/{svelte-ydwT7UbF.js → svelte-1jM2ejXj.js} +1 -1
  90. package/dist/ui/assets/{templ-BXcoVoGh.js → templ-C-VwELcp.js} +1 -1
  91. package/dist/ui/assets/{tex-COsYFp7O.js → tex-t1rUiQCm.js} +1 -1
  92. package/dist/ui/assets/{ts-tags-CF--LE1s.js → ts-tags-Bewoa6pS.js} +1 -1
  93. package/dist/ui/assets/tsx-CTXzrBkN.js +1 -0
  94. package/dist/ui/assets/{twig-YoM-DiI5.js → twig-DVg-vELl.js} +1 -1
  95. package/dist/ui/assets/typescript--ElHoFkw.js +1 -0
  96. package/dist/ui/assets/{vue-html-CPdGGk4T.js → vue-html-BHtJ8geX.js} +1 -1
  97. package/dist/ui/assets/{vue-vine-B5bjgk0a.js → vue-vine-CNSGAWwc.js} +1 -1
  98. package/dist/ui/assets/{vue-B5LDEy4l.js → vue-wPQ7UlMy.js} +1 -1
  99. package/dist/ui/assets/workspace-app-CcrHAUIn.css +1 -0
  100. package/dist/ui/assets/workspace-app-DJmkPYJC.js +1 -0
  101. package/dist/ui/assets/workspace-app-DkAiSl_0.js +111 -0
  102. package/dist/ui/assets/workspace-app-QyauBrJX.js +5 -0
  103. package/dist/ui/assets/workspace-lifecycle-app-BIXEo53I.js +1 -0
  104. package/dist/ui/assets/xml-JnmX6vyS.js +1 -0
  105. package/dist/ui/assets/{xsl-wls_CEK_.js → xsl-3_vJ27aE.js} +1 -1
  106. package/dist/ui/assets/yaml-OAcZkP-w.js +1 -0
  107. package/dist/ui/workspace-app.html +5 -2
  108. package/dist/ui/workspace-lifecycle-app.html +18 -0
  109. package/dist/workspaces.js +11 -0
  110. package/docs/configuration.md +36 -4
  111. package/docs/debugging.md +75 -17
  112. package/docs/gotchas.md +15 -9
  113. package/docs/setup.md +24 -10
  114. package/package.json +4 -3
  115. package/scripts/debug/accept.mjs +79 -27
  116. package/scripts/debug/host/run.sh +180 -0
  117. package/scripts/debug/host/serve.mjs +64 -0
  118. package/scripts/debug/runtime.mjs +57 -2
  119. package/scripts/debug/runtime.test.mjs +74 -0
  120. package/scripts/debug/serve.mjs +4 -5
  121. package/scripts/dev-server.mjs +32 -3
  122. package/scripts/release/release-gate.test.mjs +43 -0
  123. package/scripts/release/release-version.test.mjs +107 -0
  124. package/scripts/release-parity.mjs +70 -46
  125. package/scripts/release-proof.mjs +2 -2
  126. package/scripts/release-proof.test.mjs +18 -0
  127. package/scripts/release-version.mjs +17 -8
  128. package/dist/ui/assets/angular-html-BT2Z6jee.js +0 -1
  129. package/dist/ui/assets/c-acnvZ8tm.js +0 -1
  130. package/dist/ui/assets/cpp-UyrmHXan.js +0 -1
  131. package/dist/ui/assets/css-D_WCbY6m.js +0 -1
  132. package/dist/ui/assets/glsl-IJV4SB-M.js +0 -1
  133. package/dist/ui/assets/graphql-PM-WDnoB.js +0 -1
  134. package/dist/ui/assets/haml-B_5nS6Kx.js +0 -1
  135. package/dist/ui/assets/html-C3TPjrC5.js +0 -1
  136. package/dist/ui/assets/java-ne9HdIVn.js +0 -1
  137. package/dist/ui/assets/javascript-DpHT58rr.js +0 -1
  138. package/dist/ui/assets/json-DJmnBdxW.js +0 -1
  139. package/dist/ui/assets/jsx-BclJnZnN.js +0 -1
  140. package/dist/ui/assets/lua-dZC7ZQIY.js +0 -1
  141. package/dist/ui/assets/r-CAfoQND2.js +0 -1
  142. package/dist/ui/assets/regexp-Mug5jltQ.js +0 -1
  143. package/dist/ui/assets/scss-C_Z1DYnw.js +0 -1
  144. package/dist/ui/assets/shellscript-CLfJpn3I.js +0 -1
  145. package/dist/ui/assets/sql-CYnzfiQ1.js +0 -1
  146. package/dist/ui/assets/tsx-DLBt2-2f.js +0 -1
  147. package/dist/ui/assets/typescript-Ce9JGkNI.js +0 -1
  148. package/dist/ui/assets/workspace-app-Cm9EX5x4.js +0 -117
  149. package/dist/ui/assets/workspace-app-DHvE2ojW.css +0 -1
  150. package/dist/ui/assets/xml-Cun8xb0T.js +0 -1
  151. package/dist/ui/assets/yaml-BxuXn9OL.js +0 -1
  152. package/scripts/release/tag-local-ci.mjs +0 -144
@@ -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
- const shortWorkspaceId = workspaceId.startsWith("ws_")
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
- return target ? `mcp ${method} ${target}` : `mcp ${method}`;
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
- ? " Advertised skill paths may also be outside the workspace."
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 logical workspaces idle for more than two days, let the user choose whether to resume or close them with ${toolNames.closeWorkspace}; never close them automatically.`;
41
- const workspaceLifecycle = `Use ForgeRelay as a local coding workspace. Default to the user's existing checkout. Reuse the workspaceId from ${toolNames.openWorkspace}; resume or create another logical workspace only when the user asks.${staleWorkspacePolicy} Only open mode=\"worktree\" when the user explicitly asks for isolated or parallel Git work. ${toolNames.closeWorkspace} releases checkout-backed workspaces or safely finalizes managed-worktree-backed ones; managed close requires commitMessage. Read the managed-worktrees capability guide for advanced failure semantics.`;
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
- ? `When a task matches an available skill from ${toolNames.openWorkspace}, read its advertised path before proceeding. Outside normal file roots, ${toolNames.read} permits only advertised entry files and files under already-loaded advertised directories.`
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, git inspection, package scripts, generators, formatters, and commands that are better executed by the shell. If ${toolNames.shell} returns a running process with a processId, call ${toolNames.shell} again with action=\"process\" when you need to poll, wait, interact, resize, or interrupt it; otherwise you may continue other work and consume its completion notice from a later tool result.`);
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
@@ -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 readWorkspaceAppManifestEntry(manifestUrl) {
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[WORKSPACE_APP_MANIFEST_ENTRY];
29
+ const entry = manifest[manifestEntry];
12
30
  if (!entry?.file) {
13
- throw new Error(`Missing ${WORKSPACE_APP_MANIFEST_ENTRY} in UI manifest.`);
31
+ throw new Error(`Missing ${manifestEntry} in UI manifest.`);
14
32
  }
15
- return entry;
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
- const assetPaths = [entry.file, ...(entry.css ?? [])];
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
- export function resolveWorkspaceAppIdentity(options) {
94
+ function resolveAppIdentity(options) {
29
95
  try {
30
- const entry = readWorkspaceAppManifestEntry(options.manifestUrl);
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: workspaceAppUriForRevision(revision),
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: options.fallbackRevision,
41
- uri: workspaceAppUriForRevision(options.fallbackRevision),
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
+ }
@@ -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
- if ((interactionRequested || !processEntry.buffer.hasOutput()) && processEntry.running) {
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);
@@ -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
+ }