@akira-tl/forgerelay 0.1.1 → 0.2.1

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/dist/server.js CHANGED
@@ -1,6 +1,7 @@
1
1
  import { randomUUID } from "node:crypto";
2
2
  import { readFileSync } from "node:fs";
3
3
  import { access, realpath } from "node:fs/promises";
4
+ import { tmpdir } from "node:os";
4
5
  import { fileURLToPath } from "node:url";
5
6
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
6
7
  import { createMcpExpressApp } from "@modelcontextprotocol/sdk/server/express.js";
@@ -15,9 +16,10 @@ import * as z from "zod/v4";
15
16
  import { applyPatch } from "./apply-patch.js";
16
17
  import { isArtifactDownloadSupportedPlatform, registerArtifactTools, } from "./artifact-tools.js";
17
18
  import { loadConfig } from "./config.js";
19
+ import { attachHookReports, HookRunner, runToolWithHooks } from "./hooks.js";
18
20
  import { buildServerInstructions, buildToolDescriptions, toolNames, } from "./mcp/server-instructions.js";
19
21
  import { createOpenAIIncomingArtifactAdapter, } from "./incoming-artifacts.js";
20
- import { logEvent, requestIp, requestPath, commandPreview, sessionIdPrefix, } from "./logger.js";
22
+ import { logEvent, requestIp, requestPath, commandPreview, sessionIdPrefix, workspaceLogLabel, } from "./logger.js";
21
23
  import { editFileTool, findFilesTool, grepFilesTool, listDirectoryTool, readFileTool, runShellTool, writeFileTool, } from "./pi-tools.js";
22
24
  import { SingleUserOAuthProvider } from "./oauth-provider.js";
23
25
  import { McpSessionRegistry, } from "./mcp-sessions.js";
@@ -33,6 +35,7 @@ import { formatLocalAgentProviderAvailabilitySummary, getLocalAgentProviderAvail
33
35
  // MCP clients can reconnect without closing the previous transport. Bound stale
34
36
  // session retention so abandoned MCP servers do not accumulate for the life of the process.
35
37
  const MCP_SESSION_IDLE_TIMEOUT_MS = 24 * 60 * 60 * 1_000;
38
+ const FORGERELAY_VERSION = readForgeRelayVersion();
36
39
  const MCP_SESSION_CLEANUP_INTERVAL_MS = 5 * 60 * 1_000;
37
40
  const WORKSPACE_APP_URI = "ui://forgerelay/workspace-app.html";
38
41
  const WORKSPACE_APP_MANIFEST_ENTRY = "workspace-app.html";
@@ -76,6 +79,13 @@ function toolWidgetDescriptorMeta(config, kind) {
76
79
  },
77
80
  };
78
81
  }
82
+ function workspaceLogContext(workspace, sessionId) {
83
+ return {
84
+ workspaceId: workspace.id,
85
+ workspace: workspaceLogLabel(workspace.root, workspace.id),
86
+ session: sessionIdPrefix(sessionId),
87
+ };
88
+ }
79
89
  function formatVisibleAgent(agent) {
80
90
  const model = agent.model ? `, model ${agent.model}` : "";
81
91
  const thinking = agent.thinking ? `, thinking ${agent.thinking}` : "";
@@ -313,6 +323,13 @@ function processOutputSchema() {
313
323
  outputTruncated: z.boolean(),
314
324
  });
315
325
  }
326
+ function readForgeRelayVersion() {
327
+ const packageJson = JSON.parse(readFileSync(new URL("../package.json", import.meta.url), "utf8"));
328
+ if (typeof packageJson.version !== "string" || packageJson.version.length === 0) {
329
+ throw new Error("Unable to read ForgeRelay package version.");
330
+ }
331
+ return packageJson.version;
332
+ }
316
333
  function processToolResponse(tool, workspaceId, snapshot, summary) {
317
334
  const result = processResult(snapshot);
318
335
  const content = [textBlock(result)];
@@ -338,7 +355,18 @@ function processToolResponse(tool, workspaceId, snapshot, summary) {
338
355
  },
339
356
  };
340
357
  }
341
- function registerCodexProcessTools(server, config, workspaces, processSessions) {
358
+ function workspaceHookInvocation(workspace) {
359
+ return {
360
+ workspaceId: workspace.id,
361
+ workspaceRoot: workspace.root,
362
+ workspaceMode: workspace.mode,
363
+ sourceRoot: workspace.sourceRoot,
364
+ };
365
+ }
366
+ function toolResultIsError(result) {
367
+ return typeof result === "object" && result !== null && result.isError === true;
368
+ }
369
+ function registerCodexProcessTools(server, config, workspaces, processSessions, hooks) {
342
370
  registerAppTool(server, "exec_command", {
343
371
  title: "Execute command",
344
372
  description: "Run a command inside an open workspace. Returns its result when it exits during the yield window, otherwise returns a sessionId for write_stdin. Use this for file inspection, tests, builds, package scripts, and long-running processes. Call open_workspace first and pass workspaceId.",
@@ -373,36 +401,46 @@ function registerCodexProcessTools(server, config, workspaces, processSessions)
373
401
  outputSchema: processOutputSchema(),
374
402
  ...toolWidgetDescriptorMeta(config, "shell"),
375
403
  annotations: SHELL_TOOL_ANNOTATIONS,
376
- }, async ({ workspaceId, cmd, tty, columns, rows, workingDirectory, yieldTimeMs, maxOutputTokens }) => {
377
- const startedAt = performance.now();
404
+ }, async ({ workspaceId, cmd, tty, columns, rows, workingDirectory, yieldTimeMs, maxOutputTokens }, extra) => {
378
405
  const workspace = workspaces.getWorkspace(workspaceId);
379
- const cwd = workspaces.resolveWorkingDirectory(workspace, workingDirectory);
380
- const snapshot = await processSessions.start({
381
- workspaceId,
382
- command: cmd,
383
- cwd,
384
- workspaceRoot: workspace.root,
385
- tty,
386
- columns,
387
- rows,
388
- yieldTimeMs,
389
- maxOutputTokens,
390
- });
391
- logToolCall(config, {
406
+ return runToolWithHooks(hooks, {
392
407
  tool: "exec_command",
393
- workspaceId,
394
- workingDirectory: workingDirectory ?? ".",
395
- command: cmd,
396
- commandLength: cmd.length,
397
- success: true,
398
- durationMs: Math.round(performance.now() - startedAt),
399
- });
400
- return processToolResponse("exec_command", workspaceId, snapshot, {
401
- command: cmd,
402
- workingDirectory: workingDirectory ?? ".",
403
- running: snapshot.running,
404
- exitCode: snapshot.exitCode,
405
- wallTimeMs: snapshot.wallTimeMs,
408
+ invocation: workspaceHookInvocation(workspace),
409
+ payload: { command: cmd, workingDirectory: workingDirectory ?? "." },
410
+ operation: async () => {
411
+ const startedAt = performance.now();
412
+ const cwd = workspaces.resolveWorkingDirectory(workspace, workingDirectory);
413
+ const snapshot = await processSessions.start({
414
+ workspaceId,
415
+ command: cmd,
416
+ cwd,
417
+ workspaceRoot: workspace.root,
418
+ tty,
419
+ columns,
420
+ rows,
421
+ yieldTimeMs,
422
+ maxOutputTokens,
423
+ });
424
+ logToolCall(config, {
425
+ tool: "exec_command",
426
+ ...workspaceLogContext(workspace, extra.sessionId),
427
+ workingDirectory: workingDirectory ?? ".",
428
+ command: cmd,
429
+ commandLength: cmd.length,
430
+ exitCode: snapshot.exitCode,
431
+ running: snapshot.running,
432
+ processSessionId: snapshot.sessionId,
433
+ success: snapshot.running || snapshot.exitCode === 0,
434
+ durationMs: Math.round(performance.now() - startedAt),
435
+ });
436
+ return processToolResponse("exec_command", workspaceId, snapshot, {
437
+ command: cmd,
438
+ workingDirectory: workingDirectory ?? ".",
439
+ running: snapshot.running,
440
+ exitCode: snapshot.exitCode,
441
+ wallTimeMs: snapshot.wallTimeMs,
442
+ });
443
+ },
406
444
  });
407
445
  });
408
446
  registerAppTool(server, "write_stdin", {
@@ -432,39 +470,55 @@ function registerCodexProcessTools(server, config, workspaces, processSessions)
432
470
  outputSchema: processOutputSchema(),
433
471
  ...toolWidgetDescriptorMeta(config, "shell"),
434
472
  annotations: SHELL_TOOL_ANNOTATIONS,
435
- }, async ({ workspaceId, sessionId, chars, columns, rows, yieldTimeMs, maxOutputTokens }) => {
436
- const startedAt = performance.now();
437
- workspaces.getWorkspace(workspaceId);
438
- const snapshot = await processSessions.write({
439
- workspaceId,
440
- sessionId,
441
- chars,
442
- columns,
443
- rows,
444
- yieldTimeMs,
445
- maxOutputTokens,
446
- });
447
- logToolCall(config, {
473
+ }, async ({ workspaceId, sessionId, chars, columns, rows, yieldTimeMs, maxOutputTokens }, extra) => {
474
+ const workspace = workspaces.getWorkspace(workspaceId);
475
+ return runToolWithHooks(hooks, {
448
476
  tool: "write_stdin",
449
- workspaceId,
450
- success: true,
451
- durationMs: Math.round(performance.now() - startedAt),
452
- });
453
- return processToolResponse("write_stdin", workspaceId, snapshot, {
454
- sessionId,
455
- charactersWritten: chars?.length ?? 0,
456
- running: snapshot.running,
457
- exitCode: snapshot.exitCode,
458
- wallTimeMs: snapshot.wallTimeMs,
477
+ invocation: workspaceHookInvocation(workspace),
478
+ payload: {
479
+ sessionId,
480
+ charactersWritten: chars?.length ?? 0,
481
+ columns,
482
+ rows,
483
+ },
484
+ operation: async () => {
485
+ const startedAt = performance.now();
486
+ const snapshot = await processSessions.write({
487
+ workspaceId,
488
+ sessionId,
489
+ chars,
490
+ columns,
491
+ rows,
492
+ yieldTimeMs,
493
+ maxOutputTokens,
494
+ });
495
+ logToolCall(config, {
496
+ tool: "write_stdin",
497
+ ...workspaceLogContext(workspace, extra.sessionId),
498
+ exitCode: snapshot.exitCode,
499
+ running: snapshot.running,
500
+ processSessionId: snapshot.sessionId,
501
+ success: snapshot.running || snapshot.exitCode === 0,
502
+ durationMs: Math.round(performance.now() - startedAt),
503
+ });
504
+ return processToolResponse("write_stdin", workspaceId, snapshot, {
505
+ sessionId,
506
+ charactersWritten: chars?.length ?? 0,
507
+ running: snapshot.running,
508
+ exitCode: snapshot.exitCode,
509
+ wallTimeMs: snapshot.wallTimeMs,
510
+ });
511
+ },
459
512
  });
460
513
  });
461
514
  }
462
515
  export function createMcpServer(config, workspaces, reviewCheckpoints, processSessions, localAgentProviders, incomingArtifactAdapters) {
463
516
  const toolDescriptions = buildToolDescriptions(config);
517
+ const hooks = new HookRunner(config.hooks, config.logging);
464
518
  const server = new McpServer({
465
519
  name: "forgerelay",
466
520
  title: "ForgeRelay",
467
- version: "0.1.0",
521
+ version: FORGERELAY_VERSION,
468
522
  description: "Secure local coding workspace for MCP clients. Provides workspace-scoped file, search, edit, write, and shell tools.",
469
523
  }, {
470
524
  instructions: buildServerInstructions(config, {
@@ -557,9 +611,9 @@ export function createMcpServer(config, workspaces, reviewCheckpoints, processSe
557
611
  idempotentHint: false,
558
612
  openWorldHint: false,
559
613
  },
560
- }, async ({ path, mode, baseRef, newWorktree }, { _meta }) => {
614
+ }, async ({ path, mode, baseRef, newWorktree }, { _meta, sessionId }) => {
561
615
  const startedAt = performance.now();
562
- const { workspace, agentsFiles, availableAgentsFiles, workspaceReused, includeBootstrapContext, } = await workspaces.openWorkspace({ path, mode, baseRef, newWorktree }, { conversationScopeId: openAiConversationScopeId(_meta) });
616
+ const { workspace, agentsFiles, availableAgentsFiles, hookReports, workspaceReused, includeBootstrapContext, } = await workspaces.openWorkspace({ path, mode, baseRef, newWorktree }, { conversationScopeId: openAiConversationScopeId(_meta) });
563
617
  const knownWorktrees = await workspaces.listKnownWorktrees(workspace);
564
618
  if (config.widgets === "changes") {
565
619
  await reviewCheckpoints.initializeWorkspace({
@@ -652,12 +706,12 @@ export function createMcpServer(config, workspaces, reviewCheckpoints, processSe
652
706
  ];
653
707
  logToolCall(config, {
654
708
  tool: "open_workspace",
655
- workspaceId: workspace.id,
709
+ ...workspaceLogContext(workspace, sessionId),
656
710
  path: workspace.root,
657
711
  success: true,
658
712
  durationMs: Math.round(performance.now() - startedAt),
659
713
  });
660
- return {
714
+ return attachHookReports({
661
715
  content: resultContent,
662
716
  _meta: {
663
717
  tool: "open_workspace",
@@ -706,7 +760,7 @@ export function createMcpServer(config, workspaces, reviewCheckpoints, processSe
706
760
  : {}),
707
761
  instruction,
708
762
  },
709
- };
763
+ }, hookReports);
710
764
  });
711
765
  registerAppTool(server, toolNames.closeWorktree, {
712
766
  title: "Close worktree",
@@ -732,39 +786,48 @@ export function createMcpServer(config, workspaces, reviewCheckpoints, processSe
732
786
  }),
733
787
  _meta: {},
734
788
  annotations: WRITE_TOOL_ANNOTATIONS,
735
- }, async ({ workspaceId, commitMessage }) => {
736
- const startedAt = performance.now();
737
- const closed = await workspaces.closeWorktree(workspaceId, commitMessage);
738
- const result = [
739
- `Closed managed worktree ${workspaceId}.`,
740
- `Merged ${closed.branch} into ${closed.targetBranch} by fast-forward.`,
741
- `Source checkout: ${closed.sourceRoot}`,
742
- `Commit: ${closed.commitSha}`,
743
- closed.cleanupWarning
744
- ? `Cleanup warning: ${closed.cleanupWarning}`
745
- : "The worktree directory and managed branch were removed.",
746
- ].join("\n");
747
- logToolCall(config, {
789
+ }, async ({ workspaceId, commitMessage }, extra) => {
790
+ const workspace = workspaces.getWorkspace(workspaceId);
791
+ return runToolWithHooks(hooks, {
748
792
  tool: toolNames.closeWorktree,
749
- workspaceId,
750
- path: closed.sourceRoot,
751
- success: true,
752
- durationMs: Math.round(performance.now() - startedAt),
753
- });
754
- return {
755
- content: [{ type: "text", text: result }],
756
- structuredContent: {
757
- result,
758
- workspaceId,
759
- sourceRoot: closed.sourceRoot,
760
- branch: closed.branch,
761
- targetBranch: closed.targetBranch,
762
- commitSha: closed.commitSha,
763
- mergedSha: closed.mergedSha,
764
- committed: closed.committed,
765
- cleanupWarning: closed.cleanupWarning,
793
+ invocation: workspaceHookInvocation(workspace),
794
+ payload: { commitMessage },
795
+ afterCwd: (response) => response.structuredContent.sourceRoot,
796
+ operation: async () => {
797
+ const startedAt = performance.now();
798
+ const closed = await workspaces.closeWorktree(workspaceId, commitMessage);
799
+ const result = [
800
+ `Closed managed worktree ${workspaceId}.`,
801
+ `Merged ${closed.branch} into ${closed.targetBranch} by fast-forward.`,
802
+ `Source checkout: ${closed.sourceRoot}`,
803
+ `Commit: ${closed.commitSha}`,
804
+ closed.cleanupWarning
805
+ ? `Cleanup warning: ${closed.cleanupWarning}`
806
+ : "The worktree directory and managed branch were removed.",
807
+ ].join("\n");
808
+ logToolCall(config, {
809
+ tool: toolNames.closeWorktree,
810
+ ...workspaceLogContext(workspace, extra.sessionId),
811
+ path: closed.sourceRoot,
812
+ success: true,
813
+ durationMs: Math.round(performance.now() - startedAt),
814
+ });
815
+ return attachHookReports({
816
+ content: [{ type: "text", text: result }],
817
+ structuredContent: {
818
+ result,
819
+ workspaceId,
820
+ sourceRoot: closed.sourceRoot,
821
+ branch: closed.branch,
822
+ targetBranch: closed.targetBranch,
823
+ commitSha: closed.commitSha,
824
+ mergedSha: closed.mergedSha,
825
+ committed: closed.committed,
826
+ cleanupWarning: closed.cleanupWarning,
827
+ },
828
+ }, closed.hookReports);
766
829
  },
767
- };
830
+ });
768
831
  });
769
832
  registerAppTool(server, toolNames.read, {
770
833
  title: "Read file",
@@ -776,8 +839,8 @@ export function createMcpServer(config, workspaces, reviewCheckpoints, processSe
776
839
  path: z
777
840
  .string()
778
841
  .describe(config.skillsEnabled
779
- ? "File path to read, relative to the workspace root. May also be an advertised skill path from open_workspace skills."
780
- : "File path to read, relative to the workspace root."),
842
+ ? "File path to read, relative to the workspace root or absolute inside the OS temp directory. May also be an advertised skill path from open_workspace skills."
843
+ : "File path to read, relative to the workspace root or absolute inside the OS temp directory."),
781
844
  offset: z
782
845
  .number()
783
846
  .int()
@@ -794,51 +857,59 @@ export function createMcpServer(config, workspaces, reviewCheckpoints, processSe
794
857
  outputSchema: resultOutputSchema(),
795
858
  ...toolWidgetDescriptorMeta(config, "read"),
796
859
  annotations: { readOnlyHint: true },
797
- }, async ({ workspaceId, ...input }) => {
798
- const startedAt = performance.now();
860
+ }, async ({ workspaceId, ...input }, extra) => {
799
861
  const workspace = workspaces.getWorkspace(workspaceId);
800
- const readPath = workspaces.resolveReadPath(workspace, input.path);
801
- const response = await readFileTool({ ...input, path: readPath.absolutePath }, {
802
- cwd: workspace.root,
803
- root: workspace.root,
804
- readRoots: readPath.readRoots,
805
- });
806
- if (response.isError) {
807
- logFailedToolResponse(config, {
808
- tool: toolNames.read,
809
- workspaceId,
810
- path: input.path,
811
- }, response.content, startedAt);
812
- return response;
813
- }
814
- workspaces.markReadPathLoaded(workspace, readPath);
815
- const summary = {
816
- ...textSummary(response.content),
817
- offset: input.offset ?? 1,
818
- limited: input.limit !== undefined,
819
- };
820
- logToolCall(config, {
862
+ return runToolWithHooks(hooks, {
821
863
  tool: toolNames.read,
822
- workspaceId,
823
- path: input.path,
824
- success: true,
825
- durationMs: Math.round(performance.now() - startedAt),
826
- });
827
- return {
828
- ...response,
829
- _meta: {
830
- tool: toolNames.read,
831
- card: {
832
- workspaceId,
864
+ invocation: workspaceHookInvocation(workspace),
865
+ payload: { path: input.path, offset: input.offset, limit: input.limit },
866
+ isFailure: toolResultIsError,
867
+ operation: async () => {
868
+ const startedAt = performance.now();
869
+ const readPath = workspaces.resolveReadPath(workspace, input.path);
870
+ const response = await readFileTool({ ...input, path: readPath.absolutePath }, {
871
+ cwd: workspace.root,
872
+ root: workspace.root,
873
+ readRoots: readPath.readRoots,
874
+ });
875
+ if (response.isError) {
876
+ logFailedToolResponse(config, {
877
+ tool: toolNames.read,
878
+ ...workspaceLogContext(workspace, extra.sessionId),
879
+ path: input.path,
880
+ }, response.content, startedAt);
881
+ return response;
882
+ }
883
+ workspaces.markReadPathLoaded(workspace, readPath);
884
+ const summary = {
885
+ ...textSummary(response.content),
886
+ offset: input.offset ?? 1,
887
+ limited: input.limit !== undefined,
888
+ };
889
+ logToolCall(config, {
890
+ tool: toolNames.read,
891
+ ...workspaceLogContext(workspace, extra.sessionId),
833
892
  path: input.path,
834
- summary,
835
- payload: { content: response.content },
836
- },
837
- },
838
- structuredContent: {
839
- result: contentText(response.content),
893
+ success: true,
894
+ durationMs: Math.round(performance.now() - startedAt),
895
+ });
896
+ return {
897
+ ...response,
898
+ _meta: {
899
+ tool: toolNames.read,
900
+ card: {
901
+ workspaceId,
902
+ path: input.path,
903
+ summary,
904
+ payload: { content: response.content },
905
+ },
906
+ },
907
+ structuredContent: {
908
+ result: contentText(response.content),
909
+ },
910
+ };
840
911
  },
841
- };
912
+ });
842
913
  });
843
914
  if (config.toolMode !== "codex") {
844
915
  registerAppTool(server, toolNames.write, {
@@ -850,60 +921,69 @@ export function createMcpServer(config, workspaces, reviewCheckpoints, processSe
850
921
  .describe("Workspace identifier returned by open_workspace."),
851
922
  path: z
852
923
  .string()
853
- .describe("File path to write, relative to the workspace root."),
924
+ .describe("File path to write, relative to the workspace root or absolute inside the OS temp directory."),
854
925
  content: z.string().describe("Complete new file content."),
855
926
  },
856
927
  outputSchema: resultOutputSchema(),
857
928
  ...toolWidgetDescriptorMeta(config, "write"),
858
929
  annotations: WRITE_TOOL_ANNOTATIONS,
859
- }, async ({ workspaceId, ...input }) => {
860
- const startedAt = performance.now();
930
+ }, async ({ workspaceId, ...input }, extra) => {
861
931
  const workspace = workspaces.getWorkspace(workspaceId);
862
- workspaces.resolvePath(workspace, input.path);
863
- const response = await writeFileTool(input, {
864
- cwd: workspace.root,
865
- root: workspace.root,
866
- });
867
- if (response.isError) {
868
- logFailedToolResponse(config, {
869
- tool: toolNames.write,
870
- workspaceId,
871
- path: input.path,
872
- }, response.content, startedAt);
873
- return response;
874
- }
875
- const patch = newFilePatch(input.path, input.content);
876
- const stats = countDiffStats(patch);
877
- const summary = {
878
- ...stats,
879
- lines: contentLineCount(input.content),
880
- characters: input.content.length,
881
- };
882
- logToolCall(config, {
932
+ return runToolWithHooks(hooks, {
883
933
  tool: toolNames.write,
884
- workspaceId,
885
- path: input.path,
886
- success: true,
887
- durationMs: Math.round(performance.now() - startedAt),
888
- });
889
- return {
890
- ...response,
891
- _meta: {
892
- tool: toolNames.write,
893
- card: {
894
- workspaceId,
934
+ invocation: workspaceHookInvocation(workspace),
935
+ payload: { path: input.path },
936
+ isFailure: toolResultIsError,
937
+ changedPaths: (result) => toolResultIsError(result) ? [] : [input.path],
938
+ operation: async () => {
939
+ const startedAt = performance.now();
940
+ const response = await writeFileTool(input, {
941
+ cwd: workspace.root,
942
+ root: workspace.root,
943
+ fileRoots: workspaces.fileToolRoots(workspace),
944
+ });
945
+ if (response.isError) {
946
+ logFailedToolResponse(config, {
947
+ tool: toolNames.write,
948
+ ...workspaceLogContext(workspace, extra.sessionId),
949
+ path: input.path,
950
+ }, response.content, startedAt);
951
+ return response;
952
+ }
953
+ const patch = newFilePatch(input.path, input.content);
954
+ const stats = countDiffStats(patch);
955
+ const summary = {
956
+ ...stats,
957
+ lines: contentLineCount(input.content),
958
+ characters: input.content.length,
959
+ };
960
+ logToolCall(config, {
961
+ tool: toolNames.write,
962
+ ...workspaceLogContext(workspace, extra.sessionId),
895
963
  path: input.path,
896
- summary,
897
- payload: {
898
- content: response.content,
899
- patch,
964
+ success: true,
965
+ durationMs: Math.round(performance.now() - startedAt),
966
+ });
967
+ return {
968
+ ...response,
969
+ _meta: {
970
+ tool: toolNames.write,
971
+ card: {
972
+ workspaceId,
973
+ path: input.path,
974
+ summary,
975
+ payload: {
976
+ content: response.content,
977
+ patch,
978
+ },
979
+ },
900
980
  },
901
- },
902
- },
903
- structuredContent: {
904
- result: contentText(response.content),
981
+ structuredContent: {
982
+ result: contentText(response.content),
983
+ },
984
+ };
905
985
  },
906
- };
986
+ });
907
987
  });
908
988
  registerAppTool(server, toolNames.edit, {
909
989
  title: "Edit file",
@@ -914,7 +994,7 @@ export function createMcpServer(config, workspaces, reviewCheckpoints, processSe
914
994
  .describe("Workspace identifier returned by open_workspace."),
915
995
  path: z
916
996
  .string()
917
- .describe("File path to edit, relative to the workspace root."),
997
+ .describe("File path to edit, relative to the workspace root or absolute inside the OS temp directory."),
918
998
  edits: z
919
999
  .array(z.object({
920
1000
  oldText: z
@@ -929,55 +1009,64 @@ export function createMcpServer(config, workspaces, reviewCheckpoints, processSe
929
1009
  }),
930
1010
  ...toolWidgetDescriptorMeta(config, "edit"),
931
1011
  annotations: EDIT_TOOL_ANNOTATIONS,
932
- }, async ({ workspaceId, ...input }) => {
933
- const startedAt = performance.now();
1012
+ }, async ({ workspaceId, ...input }, extra) => {
934
1013
  const workspace = workspaces.getWorkspace(workspaceId);
935
- workspaces.resolvePath(workspace, input.path);
936
- const response = await editFileTool(input, {
937
- cwd: workspace.root,
938
- root: workspace.root,
939
- });
940
- if (response.isError) {
941
- logFailedToolResponse(config, {
942
- tool: toolNames.edit,
943
- workspaceId,
944
- path: input.path,
945
- }, response.content, startedAt);
946
- return response;
947
- }
948
- const stats = countDiffStats(response.details?.patch ?? response.details?.diff);
949
- const summary = {
950
- ...stats,
951
- editCount: input.edits.length,
952
- };
953
- const editResultText = `Edited ${input.path} (+${stats.additions} -${stats.removals}).`;
954
- const editContent = [textBlock(editResultText)];
955
- logToolCall(config, {
1014
+ return runToolWithHooks(hooks, {
956
1015
  tool: toolNames.edit,
957
- workspaceId,
958
- path: input.path,
959
- success: true,
960
- durationMs: Math.round(performance.now() - startedAt),
961
- });
962
- return {
963
- content: editContent,
964
- _meta: {
965
- tool: toolNames.edit,
966
- card: {
967
- workspaceId,
1016
+ invocation: workspaceHookInvocation(workspace),
1017
+ payload: { path: input.path, editCount: input.edits.length },
1018
+ isFailure: toolResultIsError,
1019
+ changedPaths: (result) => toolResultIsError(result) ? [] : [input.path],
1020
+ operation: async () => {
1021
+ const startedAt = performance.now();
1022
+ const response = await editFileTool(input, {
1023
+ cwd: workspace.root,
1024
+ root: workspace.root,
1025
+ fileRoots: workspaces.fileToolRoots(workspace),
1026
+ });
1027
+ if (response.isError) {
1028
+ logFailedToolResponse(config, {
1029
+ tool: toolNames.edit,
1030
+ ...workspaceLogContext(workspace, extra.sessionId),
1031
+ path: input.path,
1032
+ }, response.content, startedAt);
1033
+ return response;
1034
+ }
1035
+ const stats = countDiffStats(response.details?.patch ?? response.details?.diff);
1036
+ const summary = {
1037
+ ...stats,
1038
+ editCount: input.edits.length,
1039
+ };
1040
+ const editResultText = `Edited ${input.path} (+${stats.additions} -${stats.removals}).`;
1041
+ const editContent = [textBlock(editResultText)];
1042
+ logToolCall(config, {
1043
+ tool: toolNames.edit,
1044
+ ...workspaceLogContext(workspace, extra.sessionId),
968
1045
  path: input.path,
969
- summary,
970
- payload: {
971
- diff: response.details?.diff,
972
- patch: response.details?.patch,
1046
+ success: true,
1047
+ durationMs: Math.round(performance.now() - startedAt),
1048
+ });
1049
+ return {
1050
+ content: editContent,
1051
+ _meta: {
1052
+ tool: toolNames.edit,
1053
+ card: {
1054
+ workspaceId,
1055
+ path: input.path,
1056
+ summary,
1057
+ payload: {
1058
+ diff: response.details?.diff,
1059
+ patch: response.details?.patch,
1060
+ },
1061
+ },
973
1062
  },
974
- },
975
- },
976
- structuredContent: {
977
- status: "applied",
978
- result: contentText(editContent),
1063
+ structuredContent: {
1064
+ status: "applied",
1065
+ result: contentText(editContent),
1066
+ },
1067
+ };
979
1068
  },
980
- };
1069
+ });
981
1070
  });
982
1071
  }
983
1072
  if (config.toolMode === "codex") {
@@ -1003,45 +1092,55 @@ export function createMcpServer(config, workspaces, reviewCheckpoints, processSe
1003
1092
  }),
1004
1093
  ...toolWidgetDescriptorMeta(config, "edit"),
1005
1094
  annotations: EDIT_TOOL_ANNOTATIONS,
1006
- }, async ({ workspaceId, patch }) => {
1007
- const startedAt = performance.now();
1095
+ }, async ({ workspaceId, patch }, extra) => {
1008
1096
  const workspace = workspaces.getWorkspace(workspaceId);
1009
- const applied = await applyPatch(workspace.root, patch);
1010
- const paths = applied.files.map((file) => file.path).join(", ");
1011
- const result = `Applied patch to ${applied.files.length} file(s): ${paths}`;
1012
- const content = [textBlock(result)];
1013
- const displayPath = applied.files.length === 1
1014
- ? applied.files[0]?.path
1015
- : `${applied.files.length} files`;
1016
- logToolCall(config, {
1097
+ return runToolWithHooks(hooks, {
1017
1098
  tool: "apply_patch",
1018
- workspaceId,
1019
- success: true,
1020
- durationMs: Math.round(performance.now() - startedAt),
1021
- });
1022
- return {
1023
- content,
1024
- _meta: {
1025
- tool: "apply_patch",
1026
- card: {
1027
- workspaceId,
1099
+ invocation: workspaceHookInvocation(workspace),
1100
+ payload: { patchBytes: Buffer.byteLength(patch) },
1101
+ changedPaths: (response) => Array.from(new Set(response.structuredContent.files.flatMap((file) => [file.previousPath, file.path])
1102
+ .filter((path) => Boolean(path)))),
1103
+ operation: async () => {
1104
+ const startedAt = performance.now();
1105
+ const applied = await applyPatch(workspace.root, patch, [tmpdir()]);
1106
+ const paths = applied.files.map((file) => file.path).join(", ");
1107
+ const result = `Applied patch to ${applied.files.length} file(s): ${paths}`;
1108
+ const content = [textBlock(result)];
1109
+ const displayPath = applied.files.length === 1
1110
+ ? applied.files[0]?.path
1111
+ : `${applied.files.length} files`;
1112
+ logToolCall(config, {
1113
+ tool: "apply_patch",
1114
+ ...workspaceLogContext(workspace, extra.sessionId),
1028
1115
  path: displayPath,
1029
- summary: {
1030
- files: applied.files.length,
1116
+ success: true,
1117
+ durationMs: Math.round(performance.now() - startedAt),
1118
+ });
1119
+ return {
1120
+ content,
1121
+ _meta: {
1122
+ tool: "apply_patch",
1123
+ card: {
1124
+ workspaceId,
1125
+ path: displayPath,
1126
+ summary: {
1127
+ files: applied.files.length,
1128
+ additions: applied.additions,
1129
+ removals: applied.removals,
1130
+ },
1131
+ files: applied.files,
1132
+ payload: { patch: applied.patch },
1133
+ },
1134
+ },
1135
+ structuredContent: {
1136
+ result,
1031
1137
  additions: applied.additions,
1032
1138
  removals: applied.removals,
1139
+ files: applied.files,
1033
1140
  },
1034
- files: applied.files,
1035
- payload: { patch: applied.patch },
1036
- },
1037
- },
1038
- structuredContent: {
1039
- result,
1040
- additions: applied.additions,
1041
- removals: applied.removals,
1042
- files: applied.files,
1141
+ };
1043
1142
  },
1044
- };
1143
+ });
1045
1144
  });
1046
1145
  }
1047
1146
  if (config.widgets === "changes") {
@@ -1056,44 +1155,50 @@ export function createMcpServer(config, workspaces, reviewCheckpoints, processSe
1056
1155
  outputSchema: resultOutputSchema(),
1057
1156
  ...toolWidgetDescriptorMeta(config, "show_changes"),
1058
1157
  annotations: { readOnlyHint: true },
1059
- }, async ({ workspaceId }) => {
1060
- const startedAt = performance.now();
1158
+ }, async ({ workspaceId }, extra) => {
1061
1159
  const workspace = workspaces.getWorkspace(workspaceId);
1062
- const review = await reviewCheckpoints.reviewChanges({
1063
- workspaceId,
1064
- root: workspace.root,
1065
- markReviewed: true,
1066
- });
1067
- const content = [textBlock(review.result)];
1068
- logToolCall(config, {
1160
+ return runToolWithHooks(hooks, {
1069
1161
  tool: "show_changes",
1070
- workspaceId,
1071
- success: true,
1072
- durationMs: Math.round(performance.now() - startedAt),
1073
- });
1074
- return {
1075
- content,
1076
- _meta: {
1077
- tool: "show_changes",
1078
- card: {
1162
+ invocation: workspaceHookInvocation(workspace),
1163
+ operation: async () => {
1164
+ const startedAt = performance.now();
1165
+ const review = await reviewCheckpoints.reviewChanges({
1079
1166
  workspaceId,
1080
- summary: review.summary,
1081
- files: review.files,
1082
- payload: {
1083
- patch: review.patch,
1167
+ root: workspace.root,
1168
+ markReviewed: true,
1169
+ });
1170
+ const content = [textBlock(review.result)];
1171
+ logToolCall(config, {
1172
+ tool: "show_changes",
1173
+ ...workspaceLogContext(workspace, extra.sessionId),
1174
+ success: true,
1175
+ durationMs: Math.round(performance.now() - startedAt),
1176
+ });
1177
+ return {
1178
+ content,
1179
+ _meta: {
1180
+ tool: "show_changes",
1181
+ card: {
1182
+ workspaceId,
1183
+ summary: review.summary,
1184
+ files: review.files,
1185
+ payload: {
1186
+ patch: review.patch,
1187
+ },
1188
+ },
1084
1189
  },
1085
- },
1086
- },
1087
- structuredContent: {
1088
- result: contentText(content),
1190
+ structuredContent: {
1191
+ result: contentText(content),
1192
+ },
1193
+ };
1089
1194
  },
1090
- };
1195
+ });
1091
1196
  });
1092
1197
  }
1093
1198
  if (config.toolMode === "full") {
1094
1199
  registerAppTool(server, toolNames.grep, {
1095
1200
  title: "Grep",
1096
- description: "Search file contents inside an open workspace. Use this before broad reads when looking for symbols, text, or usage sites. Respects project ignore rules. Call open_workspace first and pass workspaceId.",
1201
+ 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.",
1097
1202
  inputSchema: {
1098
1203
  workspaceId: z
1099
1204
  .string()
@@ -1102,60 +1207,67 @@ export function createMcpServer(config, workspaces, reviewCheckpoints, processSe
1102
1207
  path: z
1103
1208
  .string()
1104
1209
  .optional()
1105
- .describe("Optional path or glob scope relative to the workspace root."),
1210
+ .describe("Optional path or glob scope relative to the workspace root, or an absolute path inside the OS temp directory."),
1106
1211
  include: z.string().optional().describe("Optional include glob."),
1107
1212
  },
1108
1213
  outputSchema: resultOutputSchema(),
1109
1214
  ...toolWidgetDescriptorMeta(config, "search"),
1110
1215
  annotations: { readOnlyHint: true },
1111
- }, async ({ workspaceId, ...input }) => {
1112
- const startedAt = performance.now();
1216
+ }, async ({ workspaceId, ...input }, extra) => {
1113
1217
  const workspace = workspaces.getWorkspace(workspaceId);
1114
- if (input.path)
1115
- workspaces.resolvePath(workspace, input.path);
1116
- const response = await grepFilesTool(input, {
1117
- cwd: workspace.root,
1118
- root: workspace.root,
1119
- });
1120
- if (response.isError) {
1121
- logFailedToolResponse(config, {
1122
- tool: toolNames.grep,
1123
- workspaceId,
1124
- path: input.path,
1125
- }, response.content, startedAt);
1126
- return response;
1127
- }
1128
- const summary = {
1129
- pattern: input.pattern,
1130
- scope: input.path ?? ".",
1131
- ...textSummary(response.content),
1132
- };
1133
- logToolCall(config, {
1218
+ return runToolWithHooks(hooks, {
1134
1219
  tool: toolNames.grep,
1135
- workspaceId,
1136
- path: input.path,
1137
- success: true,
1138
- durationMs: Math.round(performance.now() - startedAt),
1139
- });
1140
- return {
1141
- ...response,
1142
- _meta: {
1143
- tool: toolNames.grep,
1144
- card: {
1145
- workspaceId,
1220
+ invocation: workspaceHookInvocation(workspace),
1221
+ payload: { pattern: input.pattern, path: input.path, include: input.include },
1222
+ isFailure: toolResultIsError,
1223
+ operation: async () => {
1224
+ const startedAt = performance.now();
1225
+ const response = await grepFilesTool(input, {
1226
+ cwd: workspace.root,
1227
+ root: workspace.root,
1228
+ fileRoots: workspaces.fileToolRoots(workspace),
1229
+ });
1230
+ if (response.isError) {
1231
+ logFailedToolResponse(config, {
1232
+ tool: toolNames.grep,
1233
+ ...workspaceLogContext(workspace, extra.sessionId),
1234
+ path: input.path,
1235
+ }, response.content, startedAt);
1236
+ return response;
1237
+ }
1238
+ const summary = {
1239
+ pattern: input.pattern,
1240
+ scope: input.path ?? ".",
1241
+ ...textSummary(response.content),
1242
+ };
1243
+ logToolCall(config, {
1244
+ tool: toolNames.grep,
1245
+ ...workspaceLogContext(workspace, extra.sessionId),
1146
1246
  path: input.path,
1147
- summary,
1148
- payload: { content: response.content },
1149
- },
1150
- },
1151
- structuredContent: {
1152
- result: contentText(response.content),
1247
+ success: true,
1248
+ durationMs: Math.round(performance.now() - startedAt),
1249
+ });
1250
+ return {
1251
+ ...response,
1252
+ _meta: {
1253
+ tool: toolNames.grep,
1254
+ card: {
1255
+ workspaceId,
1256
+ path: input.path,
1257
+ summary,
1258
+ payload: { content: response.content },
1259
+ },
1260
+ },
1261
+ structuredContent: {
1262
+ result: contentText(response.content),
1263
+ },
1264
+ };
1153
1265
  },
1154
- };
1266
+ });
1155
1267
  });
1156
1268
  registerAppTool(server, toolNames.glob, {
1157
1269
  title: "Glob",
1158
- description: "Find files by glob pattern inside an open workspace. Use this to discover filenames or narrow file sets before reading. Respects project ignore rules. Call open_workspace first and pass workspaceId.",
1270
+ 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.",
1159
1271
  inputSchema: {
1160
1272
  workspaceId: z
1161
1273
  .string()
@@ -1164,109 +1276,124 @@ export function createMcpServer(config, workspaces, reviewCheckpoints, processSe
1164
1276
  path: z
1165
1277
  .string()
1166
1278
  .optional()
1167
- .describe("Optional path scope relative to the workspace root."),
1279
+ .describe("Optional path scope relative to the workspace root, or an absolute path inside the OS temp directory."),
1168
1280
  },
1169
1281
  outputSchema: resultOutputSchema(),
1170
1282
  ...toolWidgetDescriptorMeta(config, "search"),
1171
1283
  annotations: { readOnlyHint: true },
1172
- }, async ({ workspaceId, ...input }) => {
1173
- const startedAt = performance.now();
1284
+ }, async ({ workspaceId, ...input }, extra) => {
1174
1285
  const workspace = workspaces.getWorkspace(workspaceId);
1175
- if (input.path)
1176
- workspaces.resolvePath(workspace, input.path);
1177
- const response = await findFilesTool(input, {
1178
- cwd: workspace.root,
1179
- root: workspace.root,
1180
- });
1181
- if (response.isError) {
1182
- logFailedToolResponse(config, {
1183
- tool: toolNames.glob,
1184
- workspaceId,
1185
- path: input.path,
1186
- }, response.content, startedAt);
1187
- return response;
1188
- }
1189
- const summary = {
1190
- pattern: input.pattern,
1191
- scope: input.path ?? ".",
1192
- ...textSummary(response.content),
1193
- };
1194
- logToolCall(config, {
1286
+ return runToolWithHooks(hooks, {
1195
1287
  tool: toolNames.glob,
1196
- workspaceId,
1197
- path: input.path,
1198
- success: true,
1199
- durationMs: Math.round(performance.now() - startedAt),
1200
- });
1201
- return {
1202
- ...response,
1203
- _meta: {
1204
- tool: toolNames.glob,
1205
- card: {
1206
- workspaceId,
1288
+ invocation: workspaceHookInvocation(workspace),
1289
+ payload: { pattern: input.pattern, path: input.path },
1290
+ isFailure: toolResultIsError,
1291
+ operation: async () => {
1292
+ const startedAt = performance.now();
1293
+ const response = await findFilesTool(input, {
1294
+ cwd: workspace.root,
1295
+ root: workspace.root,
1296
+ fileRoots: workspaces.fileToolRoots(workspace),
1297
+ });
1298
+ if (response.isError) {
1299
+ logFailedToolResponse(config, {
1300
+ tool: toolNames.glob,
1301
+ ...workspaceLogContext(workspace, extra.sessionId),
1302
+ path: input.path,
1303
+ }, response.content, startedAt);
1304
+ return response;
1305
+ }
1306
+ const summary = {
1307
+ pattern: input.pattern,
1308
+ scope: input.path ?? ".",
1309
+ ...textSummary(response.content),
1310
+ };
1311
+ logToolCall(config, {
1312
+ tool: toolNames.glob,
1313
+ ...workspaceLogContext(workspace, extra.sessionId),
1207
1314
  path: input.path,
1208
- summary,
1209
- payload: { content: response.content },
1210
- },
1211
- },
1212
- structuredContent: {
1213
- result: contentText(response.content),
1315
+ success: true,
1316
+ durationMs: Math.round(performance.now() - startedAt),
1317
+ });
1318
+ return {
1319
+ ...response,
1320
+ _meta: {
1321
+ tool: toolNames.glob,
1322
+ card: {
1323
+ workspaceId,
1324
+ path: input.path,
1325
+ summary,
1326
+ payload: { content: response.content },
1327
+ },
1328
+ },
1329
+ structuredContent: {
1330
+ result: contentText(response.content),
1331
+ },
1332
+ };
1214
1333
  },
1215
- };
1334
+ });
1216
1335
  });
1217
1336
  registerAppTool(server, toolNames.ls, {
1218
1337
  title: "Ls",
1219
- description: "List a directory inside an open workspace. Use this for directory inspection before reading files. Call open_workspace first and pass workspaceId.",
1338
+ 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.",
1220
1339
  inputSchema: {
1221
1340
  workspaceId: z
1222
1341
  .string()
1223
1342
  .describe("Workspace identifier returned by open_workspace."),
1224
1343
  path: z
1225
1344
  .string()
1226
- .describe("Directory path to list, relative to the workspace root."),
1345
+ .describe("Directory path to list, relative to the workspace root or absolute inside the OS temp directory."),
1227
1346
  },
1228
1347
  outputSchema: resultOutputSchema(),
1229
1348
  ...toolWidgetDescriptorMeta(config, "directory"),
1230
1349
  annotations: { readOnlyHint: true },
1231
- }, async ({ workspaceId, ...input }) => {
1232
- const startedAt = performance.now();
1350
+ }, async ({ workspaceId, ...input }, extra) => {
1233
1351
  const workspace = workspaces.getWorkspace(workspaceId);
1234
- workspaces.resolvePath(workspace, input.path);
1235
- const response = await listDirectoryTool(input, {
1236
- cwd: workspace.root,
1237
- root: workspace.root,
1238
- });
1239
- if (response.isError) {
1240
- logFailedToolResponse(config, {
1241
- tool: toolNames.ls,
1242
- workspaceId,
1243
- path: input.path,
1244
- }, response.content, startedAt);
1245
- return response;
1246
- }
1247
- const summary = textSummary(response.content);
1248
- logToolCall(config, {
1352
+ return runToolWithHooks(hooks, {
1249
1353
  tool: toolNames.ls,
1250
- workspaceId,
1251
- path: input.path,
1252
- success: true,
1253
- durationMs: Math.round(performance.now() - startedAt),
1254
- });
1255
- return {
1256
- ...response,
1257
- _meta: {
1258
- tool: toolNames.ls,
1259
- card: {
1260
- workspaceId,
1354
+ invocation: workspaceHookInvocation(workspace),
1355
+ payload: { path: input.path },
1356
+ isFailure: toolResultIsError,
1357
+ operation: async () => {
1358
+ const startedAt = performance.now();
1359
+ const response = await listDirectoryTool(input, {
1360
+ cwd: workspace.root,
1361
+ root: workspace.root,
1362
+ fileRoots: workspaces.fileToolRoots(workspace),
1363
+ });
1364
+ if (response.isError) {
1365
+ logFailedToolResponse(config, {
1366
+ tool: toolNames.ls,
1367
+ ...workspaceLogContext(workspace, extra.sessionId),
1368
+ path: input.path,
1369
+ }, response.content, startedAt);
1370
+ return response;
1371
+ }
1372
+ const summary = textSummary(response.content);
1373
+ logToolCall(config, {
1374
+ tool: toolNames.ls,
1375
+ ...workspaceLogContext(workspace, extra.sessionId),
1261
1376
  path: input.path,
1262
- summary,
1263
- payload: { content: response.content },
1264
- },
1265
- },
1266
- structuredContent: {
1267
- result: contentText(response.content),
1377
+ success: true,
1378
+ durationMs: Math.round(performance.now() - startedAt),
1379
+ });
1380
+ return {
1381
+ ...response,
1382
+ _meta: {
1383
+ tool: toolNames.ls,
1384
+ card: {
1385
+ workspaceId,
1386
+ path: input.path,
1387
+ summary,
1388
+ payload: { content: response.content },
1389
+ },
1390
+ },
1391
+ structuredContent: {
1392
+ result: contentText(response.content),
1393
+ },
1394
+ };
1268
1395
  },
1269
- };
1396
+ });
1270
1397
  });
1271
1398
  }
1272
1399
  if (config.toolMode !== "codex") {
@@ -1294,62 +1421,75 @@ export function createMcpServer(config, workspaces, reviewCheckpoints, processSe
1294
1421
  outputSchema: resultOutputSchema(),
1295
1422
  ...toolWidgetDescriptorMeta(config, "shell"),
1296
1423
  annotations: SHELL_TOOL_ANNOTATIONS,
1297
- }, async ({ workspaceId, workingDirectory, ...input }) => {
1298
- const startedAt = performance.now();
1424
+ }, async ({ workspaceId, workingDirectory, ...input }, extra) => {
1299
1425
  const workspace = workspaces.getWorkspace(workspaceId);
1300
- const cwd = workspaces.resolveWorkingDirectory(workspace, workingDirectory);
1301
- const response = await runShellTool(input, {
1302
- cwd,
1303
- root: workspace.root,
1304
- });
1305
- if (response.isError) {
1306
- logFailedToolResponse(config, {
1307
- tool: toolNames.shell,
1308
- workspaceId,
1309
- workingDirectory: workingDirectory ?? ".",
1310
- command: input.command,
1311
- commandLength: input.command.length,
1312
- }, response.content, startedAt);
1313
- return response;
1314
- }
1315
- const summary = {
1316
- command: input.command,
1317
- workingDirectory: workingDirectory ?? ".",
1318
- ...textSummary(response.content),
1319
- };
1320
- logToolCall(config, {
1426
+ return runToolWithHooks(hooks, {
1321
1427
  tool: toolNames.shell,
1322
- workspaceId,
1323
- workingDirectory: workingDirectory ?? ".",
1324
- command: input.command,
1325
- commandLength: input.command.length,
1326
- success: true,
1327
- durationMs: Math.round(performance.now() - startedAt),
1328
- });
1329
- return {
1330
- ...response,
1331
- _meta: {
1332
- tool: toolNames.shell,
1333
- card: {
1334
- workspaceId,
1335
- path: workingDirectory,
1336
- summary,
1337
- payload: { content: response.content },
1338
- },
1428
+ invocation: workspaceHookInvocation(workspace),
1429
+ payload: {
1430
+ command: input.command,
1431
+ workingDirectory: workingDirectory ?? ".",
1432
+ timeoutSeconds: input.timeout,
1339
1433
  },
1340
- structuredContent: {
1341
- result: contentText(response.content),
1434
+ isFailure: toolResultIsError,
1435
+ operation: async () => {
1436
+ const startedAt = performance.now();
1437
+ const cwd = workspaces.resolveWorkingDirectory(workspace, workingDirectory);
1438
+ const response = await runShellTool(input, {
1439
+ cwd,
1440
+ root: workspace.root,
1441
+ });
1442
+ if (response.isError) {
1443
+ logFailedToolResponse(config, {
1444
+ tool: toolNames.shell,
1445
+ ...workspaceLogContext(workspace, extra.sessionId),
1446
+ workingDirectory: workingDirectory ?? ".",
1447
+ command: input.command,
1448
+ commandLength: input.command.length,
1449
+ }, response.content, startedAt);
1450
+ return response;
1451
+ }
1452
+ const summary = {
1453
+ command: input.command,
1454
+ workingDirectory: workingDirectory ?? ".",
1455
+ ...textSummary(response.content),
1456
+ };
1457
+ logToolCall(config, {
1458
+ tool: toolNames.shell,
1459
+ ...workspaceLogContext(workspace, extra.sessionId),
1460
+ workingDirectory: workingDirectory ?? ".",
1461
+ command: input.command,
1462
+ commandLength: input.command.length,
1463
+ success: true,
1464
+ durationMs: Math.round(performance.now() - startedAt),
1465
+ });
1466
+ return {
1467
+ ...response,
1468
+ _meta: {
1469
+ tool: toolNames.shell,
1470
+ card: {
1471
+ workspaceId,
1472
+ path: workingDirectory,
1473
+ summary,
1474
+ payload: { content: response.content },
1475
+ },
1476
+ },
1477
+ structuredContent: {
1478
+ result: contentText(response.content),
1479
+ },
1480
+ };
1342
1481
  },
1343
- };
1482
+ });
1344
1483
  });
1345
1484
  }
1346
1485
  if (config.toolMode === "codex") {
1347
- registerCodexProcessTools(server, config, workspaces, processSessions);
1486
+ registerCodexProcessTools(server, config, workspaces, processSessions, hooks);
1348
1487
  }
1349
1488
  if (config.artifactsEnabled && isArtifactDownloadSupportedPlatform()) {
1350
1489
  registerArtifactTools(server, {
1351
1490
  config,
1352
1491
  workspaces,
1492
+ hooks,
1353
1493
  incomingArtifactAdapters,
1354
1494
  });
1355
1495
  }