@akira-tl/forgerelay 0.3.4 → 0.3.5

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 (94) hide show
  1. package/CHANGELOG.md +16 -0
  2. package/capabilities/artifacts-review/GUIDE.md +2 -4
  3. package/dist/artifact-tools.js +0 -128
  4. package/dist/capabilities.js +2 -5
  5. package/dist/hooks.js +12 -0
  6. package/dist/mcp/server-instructions.js +4 -9
  7. package/dist/review-checkpoints.js +4 -4
  8. package/dist/server.js +5 -263
  9. package/dist/ui/.vite/manifest.json +280 -280
  10. package/dist/ui/assets/{angular-html-CQmCUfkv.js → angular-html-BT2Z6jee.js} +1 -1
  11. package/dist/ui/assets/{angular-ts-BSbfxpMS.js → angular-ts-BwPE8YDt.js} +1 -1
  12. package/dist/ui/assets/{apl-CWCaH-E3.js → apl-CGbONVvU.js} +1 -1
  13. package/dist/ui/assets/{astro-R_p4G0xA.js → astro-DIAPy7cj.js} +1 -1
  14. package/dist/ui/assets/{blade-irHensDY.js → blade-Cgtd19Ky.js} +1 -1
  15. package/dist/ui/assets/{c-_XEQUoq6.js → c-acnvZ8tm.js} +1 -1
  16. package/dist/ui/assets/{cobol-Css6VQjJ.js → cobol-G-7HLVKW.js} +1 -1
  17. package/dist/ui/assets/{coffee-CpXUkyDm.js → coffee-DQFyKxEB.js} +1 -1
  18. package/dist/ui/assets/{cpp-BQHsPWwI.js → cpp-UyrmHXan.js} +1 -1
  19. package/dist/ui/assets/{crystal-Cbv8J9Fm.js → crystal-DuiWhEO3.js} +1 -1
  20. package/dist/ui/assets/{css-D8DkrS3U.js → css-D_WCbY6m.js} +1 -1
  21. package/dist/ui/assets/{edge-bBL_XDAG.js → edge-COkalhFi.js} +1 -1
  22. package/dist/ui/assets/{elixir-CQjsM9sA.js → elixir-cdlzbYR1.js} +1 -1
  23. package/dist/ui/assets/{elm-DLFl-jEZ.js → elm-ZwDYuYpb.js} +1 -1
  24. package/dist/ui/assets/{erb-BJjsdB_Q.js → erb-C8WG9TtC.js} +1 -1
  25. package/dist/ui/assets/{git-rebase-CqP5A2s0.js → git-rebase-CMlDVJBT.js} +1 -1
  26. package/dist/ui/assets/{glimmer-js-B5Hh_7xe.js → glimmer-js-DeLOQlol.js} +1 -1
  27. package/dist/ui/assets/{glimmer-ts-oaeSAC8s.js → glimmer-ts-DTEM04TK.js} +1 -1
  28. package/dist/ui/assets/{glsl-BylkOpcF.js → glsl-IJV4SB-M.js} +1 -1
  29. package/dist/ui/assets/{graphql-DsVRvP6S.js → graphql-PM-WDnoB.js} +1 -1
  30. package/dist/ui/assets/{hack-D4N5abT_.js → hack-C14sh6Lh.js} +1 -1
  31. package/dist/ui/assets/{haml-CIAJQIox.js → haml-B_5nS6Kx.js} +1 -1
  32. package/dist/ui/assets/{handlebars-DDEZyNXR.js → handlebars-eS-J0O-M.js} +1 -1
  33. package/dist/ui/assets/{heavy-payload-DafM8yjB.js → heavy-payload-BKB0QDOI.js} +1 -1
  34. package/dist/ui/assets/{html-zDy-3lvy.js → html-C3TPjrC5.js} +1 -1
  35. package/dist/ui/assets/{html-derivative-CZ91Y2d5.js → html-derivative-BlVZU2id.js} +1 -1
  36. package/dist/ui/assets/{http-ezaodZRf.js → http-BmbZzA5x.js} +1 -1
  37. package/dist/ui/assets/{hurl-BmL0o7TO.js → hurl-Dnd_YpOD.js} +1 -1
  38. package/dist/ui/assets/{java-Q2C0vwbH.js → java-ne9HdIVn.js} +1 -1
  39. package/dist/ui/assets/{javascript-UDhBkHE3.js → javascript-DpHT58rr.js} +1 -1
  40. package/dist/ui/assets/{jinja-BWRskcYS.js → jinja-DTvkZdHA.js} +1 -1
  41. package/dist/ui/assets/{jison-B18BY9Ds.js → jison-DUJG-tmh.js} +1 -1
  42. package/dist/ui/assets/{json-DNj7MpRr.js → json-DJmnBdxW.js} +1 -1
  43. package/dist/ui/assets/{jsx-CVpHJ6cr.js → jsx-BclJnZnN.js} +1 -1
  44. package/dist/ui/assets/{julia-CJBNBS9c.js → julia-P5B4pHFa.js} +1 -1
  45. package/dist/ui/assets/{just-Bmu2JCQT.js → just-DYaoh7C6.js} +1 -1
  46. package/dist/ui/assets/{latex-BB7F93KY.js → latex-CI5lnkWV.js} +1 -1
  47. package/dist/ui/assets/{liquid-Btnomc6E.js → liquid-D3LnAs_z.js} +1 -1
  48. package/dist/ui/assets/{lua-D0FYT9Vo.js → lua-dZC7ZQIY.js} +1 -1
  49. package/dist/ui/assets/{marko-MHLw0ZlK.js → marko-CRDrjqfS.js} +1 -1
  50. package/dist/ui/assets/{mdc-CSr-suGy.js → mdc-ZzwgmJVn.js} +1 -1
  51. package/dist/ui/assets/{nginx-ur1l6E-g.js → nginx-BAvFXGSV.js} +1 -1
  52. package/dist/ui/assets/{nim-B2k0IugA.js → nim-BHIFawe9.js} +1 -1
  53. package/dist/ui/assets/{perl-BqOHxFDn.js → perl-C_f5AnLa.js} +1 -1
  54. package/dist/ui/assets/{php-DzPK6O1_.js → php-DDqpegL5.js} +1 -1
  55. package/dist/ui/assets/{pug-D169OZkr.js → pug-CSaAZXyd.js} +1 -1
  56. package/dist/ui/assets/{qml-BtemJTC4.js → qml-CNHvRs_z.js} +1 -1
  57. package/dist/ui/assets/{r-BGVHu0bd.js → r-CAfoQND2.js} +1 -1
  58. package/dist/ui/assets/{razor-Diisjg1d.js → razor-BDfAPy7I.js} +1 -1
  59. package/dist/ui/assets/{regexp-B2F5vMZ6.js → regexp-Mug5jltQ.js} +1 -1
  60. package/dist/ui/assets/{review-payload-B-3I3Oj_.js → review-payload-D7GIdYDj.js} +1 -1
  61. package/dist/ui/assets/{rst-BxpeZwuT.js → rst-BzPP2gle.js} +1 -1
  62. package/dist/ui/assets/{ruby-DrCq4riN.js → ruby-CQjzokzX.js} +1 -1
  63. package/dist/ui/assets/{sas-CI_2W-1n.js → sas-Bp37KGr7.js} +1 -1
  64. package/dist/ui/assets/{scrollbar-DHsDF5w-.js → scrollbar-HUMBdLLK.js} +3 -3
  65. package/dist/ui/assets/{scss-BxtyHZ1p.js → scss-C_Z1DYnw.js} +1 -1
  66. package/dist/ui/assets/{shellscript-BY_YYrHT.js → shellscript-CLfJpn3I.js} +1 -1
  67. package/dist/ui/assets/{shellsession-C6uBrLmA.js → shellsession-CNc62UhY.js} +1 -1
  68. package/dist/ui/assets/{soy-BHM9gdPh.js → soy-CF44SpBo.js} +1 -1
  69. package/dist/ui/assets/{sql-XmhfEGz_.js → sql-CYnzfiQ1.js} +1 -1
  70. package/dist/ui/assets/{stata-BbHY7NEo.js → stata-DVeMA68W.js} +1 -1
  71. package/dist/ui/assets/{surrealql-CD49DZ3E.js → surrealql-52sF96Yv.js} +1 -1
  72. package/dist/ui/assets/{svelte-CbYsEYJY.js → svelte-ydwT7UbF.js} +1 -1
  73. package/dist/ui/assets/{templ-B5JoUxEB.js → templ-BXcoVoGh.js} +1 -1
  74. package/dist/ui/assets/{tex-DZcOfTZK.js → tex-COsYFp7O.js} +1 -1
  75. package/dist/ui/assets/{ts-tags-BeU-xqc0.js → ts-tags-CF--LE1s.js} +1 -1
  76. package/dist/ui/assets/{tsx-V7W1xGl3.js → tsx-DLBt2-2f.js} +1 -1
  77. package/dist/ui/assets/{twig-MIaJo5BU.js → twig-YoM-DiI5.js} +1 -1
  78. package/dist/ui/assets/{typescript-C0WESuVc.js → typescript-Ce9JGkNI.js} +1 -1
  79. package/dist/ui/assets/{vue-eJHwc9ra.js → vue-B5LDEy4l.js} +1 -1
  80. package/dist/ui/assets/{vue-html-CLmBujTU.js → vue-html-CPdGGk4T.js} +1 -1
  81. package/dist/ui/assets/{vue-vine-BP5kjIGb.js → vue-vine-B5bjgk0a.js} +1 -1
  82. package/dist/ui/assets/{workspace-app-C1qR5cAY.js → workspace-app-Cm9EX5x4.js} +3 -3
  83. package/dist/ui/assets/{xml-j7pmyP48.js → xml-Cun8xb0T.js} +1 -1
  84. package/dist/ui/assets/{xsl-5VFHbg6o.js → xsl-wls_CEK_.js} +1 -1
  85. package/dist/ui/assets/{yaml-DxFAt-KU.js → yaml-BxuXn9OL.js} +1 -1
  86. package/dist/ui/workspace-app.html +1 -1
  87. package/docs/artifact-exchange.md +4 -3
  88. package/docs/chatgpt-coding-workflow.md +5 -3
  89. package/docs/configuration.md +12 -10
  90. package/docs/debugging.md +2 -2
  91. package/docs/gotchas.md +3 -3
  92. package/docs/security.md +3 -2
  93. package/package.json +1 -1
  94. package/scripts/debug/accept.mjs +53 -12
package/CHANGELOG.md CHANGED
@@ -4,6 +4,22 @@ All notable ForgeRelay changes are documented here.
4
4
 
5
5
  ## [Unreleased]
6
6
 
7
+ ## [0.3.5] - 2026-08-10
8
+
9
+ ### Changed
10
+
11
+ - Regular `minimal` and `full` MCP modes now expose the same canonical 9-tool surface: `open_workspace`, `capability`, `close_workspace`, `read`, `write`, `edit`, `rename`, `delete`, and `bash`. Search and directory inspection use shell commands through `bash`; `full` remains only as a configuration-compatibility value.
12
+ - `review.changes` and `artifact.download` are now Capability Gateway-only workflows. Their dedicated top-level compatibility aliases have been removed, while review cards identify themselves as `capability` results with `capabilityName="review.changes"`.
13
+ - Capability fingerprints now report semantic runtime capabilities such as `review.changes` instead of implementation-shaped search/review tool names, and current Guides/docs/bootstrap instructions use the final Core Surface + Capability Gateway model.
14
+
15
+ ### Removed
16
+
17
+ - Removed the regular dedicated `grep`, `glob`, `ls`, aggregate-review, and native-artifact MCP adapters after their canonical workflows moved to `bash` or the Capability Gateway.
18
+
19
+ ### Fixed
20
+
21
+ - Successful `report: true` lifecycle Hook reports are now mirrored into the model-readable structured result as well as MCP text content, so Hosts that surface only structured tool results still expose the Hook outcome to the Agent.
22
+
7
23
  ## [0.3.4] - 2026-08-10
8
24
 
9
25
  ### Changed
@@ -12,8 +12,6 @@ Native artifact download 默认关闭,且当前安全发布实现主要面向
12
12
  2. 调用 `capability`,`name="artifact.download"`、`action="run"`,把 Host 原样提供的 native `file` 值放在 Gateway 顶层 `file`,并在 `arguments.path` 传 workspace-relative 目标路径;
13
13
  3. 成功后使用返回的 normalized relative path 配合普通 `read` / `rename` / `delete` / shell 等工具继续处理。
14
14
 
15
- 0.3.3 仍保留 `download_artifact` 作为迁移期 compatibility alias,但新的 Agent workflow 不应把它视为长期 public surface。
16
-
17
15
  `artifact.download` 会创建缺失的父目录,但拒绝覆盖已有目标。不要把 native file 值改造成:
18
16
 
19
17
  - 任意 URL 字符串;
@@ -35,10 +33,10 @@ Native artifact download 默认关闭,且当前安全发布实现主要面向
35
33
 
36
34
  Plain MCP Host 可以忽略这些 UI metadata;不要把 UI 是否显示当成文件操作是否成功的判据。
37
35
 
38
- 当 Capability catalog 广告 `review.changes` 时,本轮成功修改文件后,在最后一个相关 file mutation 之后、final response 之前通过 `capability` 调用一次,让用户看到聚合 diff;不要每改一个文件就调用一次。0.3.3 仍保留 `show_changes` compatibility alias,但它和 `review.changes` 共用同一套 checkpoint,不是两套 review 状态。
36
+ 当 Capability catalog 广告 `review.changes` 时,本轮成功修改文件后,在最后一个相关 file mutation 之后、final response 之前通过 `capability` 调用一次,让用户看到聚合 diff;不要每改一个文件就调用一次。
39
37
 
40
38
  `review.changes` 使用 Git-backed review checkpoint。它按 workspace 跟踪 open/baseline 状态,展示自上次 review checkpoint 以来的 coherent diff,并在成功 review 后推进 baseline。当前版本要求可用的 Git workspace;checkpoint 缺失或 root 不匹配时会明确失败或使用受控 fallback,而不是凭空重建历史。
41
39
 
42
40
  ## Capability ownership
43
41
 
44
- Artifact transfer 和 change review 都属于 ForgeRelay registered capability,而不是 Agent 自己的文件搬运协议。`tools/list` 只负责暴露稳定 Gateway 与当前兼容 aliases;真正可用的低频能力以当前 workspace 的 Capability catalog 为准。本指南提供流程和边界,不额外创造隐藏执行入口。
42
+ Artifact transfer 和 change review 都属于 ForgeRelay registered capability,而不是 Agent 自己的文件搬运协议。`tools/list` 只暴露稳定 `capability` Gateway;真正可用的低频能力以当前 workspace 的 Capability catalog 为准。本指南提供流程和边界,不额外创造隐藏执行入口。
@@ -2,18 +2,7 @@ import { createHash, randomUUID } from "node:crypto";
2
2
  import { constants as fsConstants } from "node:fs";
3
3
  import { link, lstat, mkdir, open, readdir, unlink, } from "node:fs/promises";
4
4
  import { isAbsolute, join, normalize, sep } from "node:path";
5
- import { registerAppTool } from "@modelcontextprotocol/ext-apps/server";
6
- import * as z from "zod/v4";
7
5
  import { ArtifactError } from "./artifact-error.js";
8
- import { runToolWithHooks } from "./hooks.js";
9
- import { describeIncomingArtifactValue, IncomingArtifactAdapterRegistry, } from "./incoming-artifacts.js";
10
- import { logEvent, workspaceLogLabel } from "./logger.js";
11
- const ARTIFACT_WRITE_ANNOTATIONS = {
12
- readOnlyHint: false,
13
- destructiveHint: false,
14
- idempotentHint: false,
15
- openWorldHint: true,
16
- };
17
6
  const NO_FOLLOW = fsConstants.O_NOFOLLOW ?? 0;
18
7
  const DIRECTORY_FLAGS = fsConstants.O_RDONLY | (fsConstants.O_DIRECTORY ?? 0) | NO_FOLLOW;
19
8
  const PARTIAL_PREFIX = ".forgerelay-download-";
@@ -21,64 +10,9 @@ const PARTIAL_SUFFIX = ".partial";
21
10
  const STALE_PARTIAL_AGE_MS = 24 * 60 * 60 * 1_000;
22
11
  const MAX_STALE_PARTIAL_CLEANUP = 32;
23
12
  const ARTIFACT_DOWNLOAD_PLATFORMS = new Set(["linux"]);
24
- const openAIFileReferenceInputSchema = z.strictObject({
25
- download_url: z.string(),
26
- file_id: z.string(),
27
- mime_type: z.string().nullable().optional(),
28
- file_name: z.string().nullable().optional(),
29
- name: z.string().nullable().optional(),
30
- size: z.number().int().nonnegative().nullable().optional(),
31
- });
32
13
  export function isArtifactDownloadSupportedPlatform(platform = process.platform) {
33
14
  return ARTIFACT_DOWNLOAD_PLATFORMS.has(platform);
34
15
  }
35
- export function registerArtifactTools(server, { config, workspaces, hooks, incomingArtifactAdapters = [], incomingArtifactRegistry, }) {
36
- const incomingRegistry = incomingArtifactRegistry
37
- ?? new IncomingArtifactAdapterRegistry(incomingArtifactAdapters);
38
- registerAppTool(server, "download_artifact", {
39
- title: "Download attached or generated file",
40
- description: "Stream one MCP-host-provided native file to a requested relative path inside an already-open workspace. Existing destinations, arbitrary URLs, absolute paths, traversal, symlinked parents, local source paths, and malformed file objects are rejected.",
41
- inputSchema: {
42
- file: openAIFileReferenceInputSchema.describe("Native file value authorized and supplied by the MCP host."),
43
- workspaceId: z.string().min(1).describe("Workspace identifier returned by open_workspace."),
44
- path: z.string().min(1).describe("Relative destination path inside the selected workspace. The destination must not already exist."),
45
- },
46
- outputSchema: {
47
- path: z.string(),
48
- },
49
- _meta: { "openai/fileParams": ["file"] },
50
- annotations: ARTIFACT_WRITE_ANNOTATIONS,
51
- }, async (input) => {
52
- const workspace = workspaces.getWorkspace(input.workspaceId);
53
- return runToolWithHooks(hooks, {
54
- tool: "download_artifact",
55
- invocation: {
56
- workspaceId: workspace.id,
57
- workspaceRoot: workspace.root,
58
- workspaceMode: workspace.mode,
59
- sourceRoot: workspace.sourceRoot,
60
- },
61
- payload: { path: input.path },
62
- changedPaths: (result) => [result.structuredContent.path],
63
- operation: () => executeArtifactTool(config, input, {
64
- workspace: workspaceLogLabel(workspace.root, workspace.id),
65
- }, async () => {
66
- const downloaded = await downloadIncomingArtifact({
67
- registry: incomingRegistry,
68
- workspaceId: workspace.id,
69
- workspaceRoot: workspace.root,
70
- maxFileBytes: config.artifactMaxFileBytes,
71
- file: input.file,
72
- path: input.path,
73
- });
74
- return {
75
- publicResult: { path: downloaded.path },
76
- logResult: downloaded,
77
- };
78
- }),
79
- });
80
- });
81
- }
82
16
  /**
83
17
  * Stream a trusted native file directly into one already-open workspace.
84
18
  *
@@ -159,53 +93,6 @@ export async function downloadIncomingArtifact({ registry, workspaceId, workspac
159
93
  await workspaceHandle?.close().catch(() => undefined);
160
94
  }
161
95
  }
162
- export function artifactToolLogFields(input) {
163
- return {
164
- fileProvided: input.file !== undefined,
165
- fileReferenceShape: describeIncomingArtifactValue(input.file),
166
- downloadUrlHostname: incomingFileDownloadHostname(input.file),
167
- workspaceId: input.workspaceId,
168
- path: input.path,
169
- };
170
- }
171
- async function executeArtifactTool(config, input, logContext, operation) {
172
- const startedAt = performance.now();
173
- try {
174
- const { publicResult, logResult } = await operation();
175
- if (config.logging.toolCalls) {
176
- logEvent(config.logging, "info", "artifact_tool_call", {
177
- tool: "download_artifact",
178
- ...artifactToolLogFields(input),
179
- ...logContext,
180
- path: logResult.path,
181
- size: logResult.size,
182
- sha256: logResult.sha256,
183
- success: true,
184
- durationMs: Math.round(performance.now() - startedAt),
185
- });
186
- }
187
- return artifactToolResponse(publicResult);
188
- }
189
- catch (error) {
190
- if (config.logging.toolCalls) {
191
- logEvent(config.logging, "warn", "artifact_tool_call", {
192
- tool: "download_artifact",
193
- ...artifactToolLogFields(input),
194
- ...logContext,
195
- success: false,
196
- errorCode: error instanceof ArtifactError ? error.code : "internal_error",
197
- durationMs: Math.round(performance.now() - startedAt),
198
- });
199
- }
200
- throw error;
201
- }
202
- }
203
- function artifactToolResponse(result) {
204
- return {
205
- content: [{ type: "text", text: JSON.stringify(result) }],
206
- structuredContent: result,
207
- };
208
- }
209
96
  async function openDirectoryNoFollow(path, code, message) {
210
97
  let handle;
211
98
  try {
@@ -356,21 +243,6 @@ async function writeAll(handle, buffer, position) {
356
243
  offset += bytesWritten;
357
244
  }
358
245
  }
359
- function incomingFileDownloadHostname(value) {
360
- if (typeof value !== "object" || value === null || Array.isArray(value)) {
361
- return undefined;
362
- }
363
- const rawUrl = value.download_url;
364
- if (typeof rawUrl !== "string")
365
- return undefined;
366
- try {
367
- const hostname = new URL(rawUrl).hostname.toLowerCase();
368
- return hostname.length > 0 && hostname.length <= 253 ? hostname : undefined;
369
- }
370
- catch {
371
- return undefined;
372
- }
373
- }
374
246
  function incomingStreamChunk(value) {
375
247
  if (Buffer.isBuffer(value))
376
248
  return value;
@@ -21,7 +21,7 @@ const CAPABILITY_GUIDE_DEFINITIONS = [
21
21
  {
22
22
  name: "artifacts-review",
23
23
  description: "Native artifact transfer and aggregate change review.",
24
- whenToRead: "Read for host-provided files or show_changes.",
24
+ whenToRead: "Read for host-provided files or aggregate change review.",
25
25
  enabled: (config) => config.artifactsEnabled || config.widgets === "changes",
26
26
  },
27
27
  {
@@ -76,9 +76,6 @@ export function buildCapabilityFingerprint(config, version, context = {}) {
76
76
  "hooks.lifecycle",
77
77
  "capability-guides.read",
78
78
  ];
79
- if (config.toolMode === "full") {
80
- capabilities.push("inspection.search-tools");
81
- }
82
79
  if (config.subagents) {
83
80
  capabilities.push("subagent.profiles");
84
81
  }
@@ -89,7 +86,7 @@ export function buildCapabilityFingerprint(config, version, context = {}) {
89
86
  capabilities.push("ui.mcp-app");
90
87
  }
91
88
  if (config.widgets === "changes") {
92
- capabilities.push("review.show-changes");
89
+ capabilities.push("review.changes");
93
90
  }
94
91
  return {
95
92
  version,
package/dist/hooks.js CHANGED
@@ -152,6 +152,10 @@ export function attachHookReports(result, executions) {
152
152
  const summary = formatVisibleHookReports(executions);
153
153
  if (!summary || !isRecord(result) || !Array.isArray(result.content))
154
154
  return result;
155
+ const structuredContent = isRecord(result.structuredContent) ? result.structuredContent : undefined;
156
+ const structuredResult = typeof structuredContent?.result === "string"
157
+ ? structuredContent.result
158
+ : undefined;
155
159
  return {
156
160
  ...result,
157
161
  content: [
@@ -161,6 +165,14 @@ export function attachHookReports(result, executions) {
161
165
  text: summary,
162
166
  },
163
167
  ],
168
+ ...(structuredContent && structuredResult !== undefined
169
+ ? {
170
+ structuredContent: {
171
+ ...structuredContent,
172
+ result: `${structuredResult}\n\n${summary}`,
173
+ },
174
+ }
175
+ : {}),
164
176
  };
165
177
  }
166
178
  function appendHookReportsToError(error, executions) {
@@ -6,9 +6,6 @@ export const toolNames = {
6
6
  edit: "edit",
7
7
  rename: "rename",
8
8
  delete: "delete",
9
- grep: "grep",
10
- glob: "glob",
11
- ls: "ls",
12
9
  shell: "bash",
13
10
  writeStdin: "write_stdin",
14
11
  capability: "capability",
@@ -23,9 +20,9 @@ export function buildToolDescriptions(config) {
23
20
  const skillCapability = config.skillsEnabled
24
21
  ? " Advertised skill paths may also be outside the workspace."
25
22
  : "";
26
- const shellSurface = config.toolMode === "minimal"
27
- ? ` In minimal tool mode, ${toolNames.grep}, ${toolNames.glob}, and ${toolNames.ls} are disabled, so shell commands may be used for equivalent search and directory inspection.`
28
- : "";
23
+ const shellSurface = config.toolMode === "codex"
24
+ ? ""
25
+ : " Use shell commands for search and directory inspection instead of dedicated MCP search tools.";
29
26
  return {
30
27
  read: `Read a file inside an open workspace or the OS temp directory. Instruction files and advertised capability guides returned by ${toolNames.openWorkspace} are also readable when applicable.${skillCapability} Only advertised entry files and files under already-loaded advertised directories are readable outside the normal roots. Call ${toolNames.openWorkspace} first and pass workspaceId.`,
31
28
  write: `Create or completely overwrite a file inside an open workspace or the OS temp directory. Workspace paths may be relative; OS temp paths may be absolute. Call ${toolNames.openWorkspace} first and pass workspaceId.`,
@@ -62,9 +59,7 @@ function defaultWorkflowInstructions(config) {
62
59
  if (config.toolMode === "codex") {
63
60
  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.`;
64
61
  }
65
- const inspection = config.toolMode === "full"
66
- ? `Prefer ${toolNames.read}, ${toolNames.grep}, ${toolNames.glob}, and ${toolNames.ls} for file inspection.`
67
- : `Use ${toolNames.shell} with command-line tools such as grep, rg, find, ls, and tree for search and directory inspection.`;
62
+ const inspection = `Use ${toolNames.shell} with command-line tools such as grep, rg, find, ls, and tree for search and directory inspection.`;
68
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.`);
69
64
  }
70
65
  function joinInstructions(...parts) {
@@ -39,19 +39,19 @@ export function createReviewCheckpointManager() {
39
39
  }
40
40
  assertWorkspaceRoot(state, workspaceId, root);
41
41
  if (!state?.gitRoot) {
42
- throw new Error(state?.diagnostic ?? "show_changes requires a Git workspace in this version.");
42
+ throw new Error(state?.diagnostic ?? "review.changes requires a Git workspace in this version.");
43
43
  }
44
44
  let effectiveSince = since;
45
45
  let usedWorkspaceOpenFallback = false;
46
46
  if (since === "last_shown" && !state.baselineRefAvailable) {
47
47
  if (!state.openRefAvailable) {
48
- throw new Error("Review checkpoints are missing; show_changes cannot reconstruct that history safely.");
48
+ throw new Error("Review checkpoints are missing; review.changes cannot reconstruct that history safely.");
49
49
  }
50
50
  effectiveSince = "workspace_open";
51
51
  usedWorkspaceOpenFallback = true;
52
52
  }
53
53
  else if (since === "workspace_open" && !state.openRefAvailable) {
54
- throw new Error("The workspace-open review checkpoint is missing; show_changes cannot reconstruct that history safely.");
54
+ throw new Error("The workspace-open review checkpoint is missing; review.changes cannot reconstruct that history safely.");
55
55
  }
56
56
  const baselineRef = effectiveSince === "workspace_open" ? state.openRef : state.baselineRef;
57
57
  const baseline = (await git(state.gitRoot, ["rev-parse", "--verify", `${baselineRef}^{commit}`])).stdout.trim();
@@ -98,7 +98,7 @@ async function initializeWorkspaceState(states, workspaceId, root) {
98
98
  try {
99
99
  const eligibility = await getGitEligibility(root);
100
100
  if (!eligibility.ok || !eligibility.gitRoot) {
101
- state.diagnostic = eligibility.message ?? "show_changes requires a Git workspace in this version.";
101
+ state.diagnostic = eligibility.message ?? "review.changes requires a Git workspace in this version.";
102
102
  return;
103
103
  }
104
104
  const [openCommit, baselineCommit] = await Promise.all([
package/dist/server.js CHANGED
@@ -17,7 +17,7 @@ import { applyPatch } from "./apply-patch.js";
17
17
  import { buildCapabilityFingerprint } from "./capabilities.js";
18
18
  import { CapabilityError, createCapabilityRegistry, } from "./capability-registry.js";
19
19
  import { deletePath, renamePath } from "./file-mutations.js";
20
- import { downloadIncomingArtifact, isArtifactDownloadSupportedPlatform, registerArtifactTools, } from "./artifact-tools.js";
20
+ import { downloadIncomingArtifact, isArtifactDownloadSupportedPlatform, } from "./artifact-tools.js";
21
21
  import { ArtifactError } from "./artifact-error.js";
22
22
  import { loadConfig } from "./config.js";
23
23
  import { attachHookReports, HookRunner, runToolWithHooks } from "./hooks.js";
@@ -25,7 +25,7 @@ import { checkHookConfiguration } from "./hook-cli.js";
25
25
  import { buildServerInstructions, buildShellMutationPolicy, buildToolDescriptions, toolNames, } from "./mcp/server-instructions.js";
26
26
  import { createOpenAIIncomingArtifactAdapter, IncomingArtifactAdapterRegistry, } from "./incoming-artifacts.js";
27
27
  import { logEvent, requestIp, requestPath, commandPreview, transportSessionIdPrefix, workspaceLogLabel, } from "./logger.js";
28
- import { editFileTool, findFilesTool, grepFilesTool, listDirectoryTool, readFileTool, writeFileTool, } from "./pi-tools.js";
28
+ import { editFileTool, readFileTool, writeFileTool, } from "./pi-tools.js";
29
29
  import { SingleUserOAuthProvider } from "./oauth-provider.js";
30
30
  import { McpTransportRegistry, } from "./mcp-sessions.js";
31
31
  import { ProcessManager, resolveProcessId, } from "./process-sessions.js";
@@ -67,7 +67,7 @@ function shouldAttachWidget(mode, kind) {
67
67
  case "off":
68
68
  return false;
69
69
  case "changes":
70
- return kind === "workspace" || kind === "show_changes" || kind === "capability";
70
+ return kind === "workspace" || kind === "capability";
71
71
  case "full":
72
72
  return true;
73
73
  }
@@ -711,7 +711,6 @@ export function createMcpServer(config, workspaces, reviewCheckpoints, processSe
711
711
  files: review.files,
712
712
  },
713
713
  card: {
714
- tool: "show_changes",
715
714
  summary: review.summary,
716
715
  files: review.files,
717
716
  payload: { patch: review.patch },
@@ -1120,9 +1119,10 @@ export function createMcpServer(config, workspaces, reviewCheckpoints, processSe
1120
1119
  ...(execution.card
1121
1120
  ? {
1122
1121
  _meta: {
1123
- tool: execution.card.tool ?? toolNames.capability,
1122
+ tool: toolNames.capability,
1124
1123
  card: {
1125
1124
  workspaceId,
1125
+ capabilityName: name,
1126
1126
  summary: execution.card.summary ?? {},
1127
1127
  files: execution.card.files,
1128
1128
  payload: execution.card.payload ?? {},
@@ -1716,255 +1716,6 @@ export function createMcpServer(config, workspaces, reviewCheckpoints, processSe
1716
1716
  });
1717
1717
  });
1718
1718
  }
1719
- if (config.widgets === "changes") {
1720
- registerAppTool(server, "show_changes", {
1721
- title: "Show changes",
1722
- description: "Show the changes made in this turn for an open workspace. Call this once after the final related file change and before your final response so the user can review the combined diff. Do not call it after each individual file change.",
1723
- inputSchema: {
1724
- workspaceId: z
1725
- .string()
1726
- .describe("Workspace identifier returned by open_workspace."),
1727
- },
1728
- outputSchema: resultOutputSchema(),
1729
- ...toolWidgetDescriptorMeta(config, "show_changes"),
1730
- annotations: { readOnlyHint: true },
1731
- }, async ({ workspaceId }, extra) => {
1732
- const workspace = workspaces.getWorkspace(workspaceId);
1733
- return runToolWithHooks(hooks, {
1734
- tool: "show_changes",
1735
- invocation: workspaceHookInvocation(workspace),
1736
- operation: async () => {
1737
- const startedAt = performance.now();
1738
- const review = await reviewWorkspaceChanges(reviewCheckpoints, workspace);
1739
- const content = [textBlock(review.result)];
1740
- logToolCall(config, {
1741
- tool: "show_changes",
1742
- ...workspaceLogContext(workspace, extra.sessionId),
1743
- success: true,
1744
- durationMs: Math.round(performance.now() - startedAt),
1745
- });
1746
- return {
1747
- content,
1748
- _meta: {
1749
- tool: "show_changes",
1750
- card: {
1751
- workspaceId,
1752
- summary: review.summary,
1753
- files: review.files,
1754
- payload: {
1755
- patch: review.patch,
1756
- },
1757
- },
1758
- },
1759
- structuredContent: {
1760
- result: contentText(content),
1761
- },
1762
- };
1763
- },
1764
- });
1765
- });
1766
- }
1767
- if (config.toolMode === "full") {
1768
- registerAppTool(server, toolNames.grep, {
1769
- title: "Grep",
1770
- description: "Search file contents inside an open workspace or the OS temp directory. Use this before broad reads when looking for symbols, text, or usage sites. Respects project ignore rules. Call open_workspace first and pass workspaceId.",
1771
- inputSchema: {
1772
- workspaceId: z
1773
- .string()
1774
- .describe("Workspace identifier returned by open_workspace."),
1775
- pattern: z.string().describe("Search pattern."),
1776
- path: z
1777
- .string()
1778
- .optional()
1779
- .describe("Optional path or glob scope relative to the workspace root, or an absolute path inside the OS temp directory."),
1780
- include: z.string().optional().describe("Optional include glob."),
1781
- },
1782
- outputSchema: resultOutputSchema(),
1783
- ...toolWidgetDescriptorMeta(config, "search"),
1784
- annotations: { readOnlyHint: true },
1785
- }, async ({ workspaceId, ...input }, extra) => {
1786
- const workspace = workspaces.getWorkspace(workspaceId);
1787
- return runToolWithHooks(hooks, {
1788
- tool: toolNames.grep,
1789
- invocation: workspaceHookInvocation(workspace),
1790
- payload: { pattern: input.pattern, path: input.path, include: input.include },
1791
- isFailure: toolResultIsError,
1792
- operation: async () => {
1793
- const startedAt = performance.now();
1794
- const response = await grepFilesTool(input, {
1795
- cwd: workspace.root,
1796
- root: workspace.root,
1797
- fileRoots: workspaces.fileToolRoots(workspace),
1798
- });
1799
- if (response.isError) {
1800
- logFailedToolResponse(config, {
1801
- tool: toolNames.grep,
1802
- ...workspaceLogContext(workspace, extra.sessionId),
1803
- path: input.path,
1804
- }, response.content, startedAt);
1805
- return response;
1806
- }
1807
- const summary = {
1808
- pattern: input.pattern,
1809
- scope: input.path ?? ".",
1810
- ...textSummary(response.content),
1811
- };
1812
- logToolCall(config, {
1813
- tool: toolNames.grep,
1814
- ...workspaceLogContext(workspace, extra.sessionId),
1815
- path: input.path,
1816
- success: true,
1817
- durationMs: Math.round(performance.now() - startedAt),
1818
- });
1819
- return {
1820
- ...response,
1821
- _meta: {
1822
- tool: toolNames.grep,
1823
- card: {
1824
- workspaceId,
1825
- path: input.path,
1826
- summary,
1827
- payload: { content: response.content },
1828
- },
1829
- },
1830
- structuredContent: {
1831
- result: contentText(response.content),
1832
- },
1833
- };
1834
- },
1835
- });
1836
- });
1837
- registerAppTool(server, toolNames.glob, {
1838
- title: "Glob",
1839
- description: "Find files by glob pattern inside an open workspace or the OS temp directory. Use this to discover filenames or narrow file sets before reading. Respects project ignore rules. Call open_workspace first and pass workspaceId.",
1840
- inputSchema: {
1841
- workspaceId: z
1842
- .string()
1843
- .describe("Workspace identifier returned by open_workspace."),
1844
- pattern: z.string().describe("File glob pattern."),
1845
- path: z
1846
- .string()
1847
- .optional()
1848
- .describe("Optional path scope relative to the workspace root, or an absolute path inside the OS temp directory."),
1849
- },
1850
- outputSchema: resultOutputSchema(),
1851
- ...toolWidgetDescriptorMeta(config, "search"),
1852
- annotations: { readOnlyHint: true },
1853
- }, async ({ workspaceId, ...input }, extra) => {
1854
- const workspace = workspaces.getWorkspace(workspaceId);
1855
- return runToolWithHooks(hooks, {
1856
- tool: toolNames.glob,
1857
- invocation: workspaceHookInvocation(workspace),
1858
- payload: { pattern: input.pattern, path: input.path },
1859
- isFailure: toolResultIsError,
1860
- operation: async () => {
1861
- const startedAt = performance.now();
1862
- const response = await findFilesTool(input, {
1863
- cwd: workspace.root,
1864
- root: workspace.root,
1865
- fileRoots: workspaces.fileToolRoots(workspace),
1866
- });
1867
- if (response.isError) {
1868
- logFailedToolResponse(config, {
1869
- tool: toolNames.glob,
1870
- ...workspaceLogContext(workspace, extra.sessionId),
1871
- path: input.path,
1872
- }, response.content, startedAt);
1873
- return response;
1874
- }
1875
- const summary = {
1876
- pattern: input.pattern,
1877
- scope: input.path ?? ".",
1878
- ...textSummary(response.content),
1879
- };
1880
- logToolCall(config, {
1881
- tool: toolNames.glob,
1882
- ...workspaceLogContext(workspace, extra.sessionId),
1883
- path: input.path,
1884
- success: true,
1885
- durationMs: Math.round(performance.now() - startedAt),
1886
- });
1887
- return {
1888
- ...response,
1889
- _meta: {
1890
- tool: toolNames.glob,
1891
- card: {
1892
- workspaceId,
1893
- path: input.path,
1894
- summary,
1895
- payload: { content: response.content },
1896
- },
1897
- },
1898
- structuredContent: {
1899
- result: contentText(response.content),
1900
- },
1901
- };
1902
- },
1903
- });
1904
- });
1905
- registerAppTool(server, toolNames.ls, {
1906
- title: "Ls",
1907
- description: "List a directory inside an open workspace or the OS temp directory. Use this for directory inspection before reading files. Call open_workspace first and pass workspaceId.",
1908
- inputSchema: {
1909
- workspaceId: z
1910
- .string()
1911
- .describe("Workspace identifier returned by open_workspace."),
1912
- path: z
1913
- .string()
1914
- .describe("Directory path to list, relative to the workspace root or absolute inside the OS temp directory."),
1915
- },
1916
- outputSchema: resultOutputSchema(),
1917
- ...toolWidgetDescriptorMeta(config, "directory"),
1918
- annotations: { readOnlyHint: true },
1919
- }, async ({ workspaceId, ...input }, extra) => {
1920
- const workspace = workspaces.getWorkspace(workspaceId);
1921
- return runToolWithHooks(hooks, {
1922
- tool: toolNames.ls,
1923
- invocation: workspaceHookInvocation(workspace),
1924
- payload: { path: input.path },
1925
- isFailure: toolResultIsError,
1926
- operation: async () => {
1927
- const startedAt = performance.now();
1928
- const response = await listDirectoryTool(input, {
1929
- cwd: workspace.root,
1930
- root: workspace.root,
1931
- fileRoots: workspaces.fileToolRoots(workspace),
1932
- });
1933
- if (response.isError) {
1934
- logFailedToolResponse(config, {
1935
- tool: toolNames.ls,
1936
- ...workspaceLogContext(workspace, extra.sessionId),
1937
- path: input.path,
1938
- }, response.content, startedAt);
1939
- return response;
1940
- }
1941
- const summary = textSummary(response.content);
1942
- logToolCall(config, {
1943
- tool: toolNames.ls,
1944
- ...workspaceLogContext(workspace, extra.sessionId),
1945
- path: input.path,
1946
- success: true,
1947
- durationMs: Math.round(performance.now() - startedAt),
1948
- });
1949
- return {
1950
- ...response,
1951
- _meta: {
1952
- tool: toolNames.ls,
1953
- card: {
1954
- workspaceId,
1955
- path: input.path,
1956
- summary,
1957
- payload: { content: response.content },
1958
- },
1959
- },
1960
- structuredContent: {
1961
- result: contentText(response.content),
1962
- },
1963
- };
1964
- },
1965
- });
1966
- });
1967
- }
1968
1719
  if (config.toolMode !== "codex") {
1969
1720
  registerAppTool(server, toolNames.shell, {
1970
1721
  title: "Bash",
@@ -2146,15 +1897,6 @@ export function createMcpServer(config, workspaces, reviewCheckpoints, processSe
2146
1897
  });
2147
1898
  }
2148
1899
  registerProcessTools(server, config, workspaces, processSessions, hooks);
2149
- if (config.artifactsEnabled && isArtifactDownloadSupportedPlatform()) {
2150
- registerArtifactTools(server, {
2151
- config,
2152
- workspaces,
2153
- hooks,
2154
- incomingArtifactAdapters,
2155
- incomingArtifactRegistry,
2156
- });
2157
- }
2158
1900
  return server;
2159
1901
  }
2160
1902
  export function createServer(config = loadConfig(), options = {}) {