@oh-my-pi/pi-coding-agent 17.1.7 → 17.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +127 -0
- package/dist/{CHANGELOG-5k4dq4g6.md → CHANGELOG-be1f2t8h.md} +127 -0
- package/dist/cli.js +9554 -12379
- package/dist/template-8vdv6xb4.js +1671 -0
- package/dist/template-f8wx9vfn.css +1355 -0
- package/dist/template-qat058wr.html +55 -0
- package/dist/tool-views.generated-jdfmzwmn.js +35 -0
- package/dist/types/advisor/advise-tool.d.ts +0 -7
- package/dist/types/advisor/runtime.d.ts +8 -11
- package/dist/types/advisor/transcript-recorder.d.ts +15 -0
- package/dist/types/async/job-manager.d.ts +11 -0
- package/dist/types/auto-thinking/classifier.d.ts +9 -2
- package/dist/types/cleanse/agent.d.ts +19 -0
- package/dist/types/cleanse/balance.d.ts +7 -0
- package/dist/types/cleanse/checkers.d.ts +13 -0
- package/dist/types/cleanse/index.d.ts +16 -0
- package/dist/types/cleanse/loop.d.ts +16 -0
- package/dist/types/cleanse/parsers.d.ts +13 -0
- package/dist/types/cleanse/progress.d.ts +14 -0
- package/dist/types/cleanse/types.d.ts +64 -0
- package/dist/types/cli/args.d.ts +2 -0
- package/dist/types/cli/session-picker.d.ts +14 -7
- package/dist/types/collab/guest.d.ts +21 -2
- package/dist/types/commands/cleanse.d.ts +23 -0
- package/dist/types/commands/launch.d.ts +6 -0
- package/dist/types/config/settings-schema.d.ts +63 -8
- package/dist/types/cursor-bridge-tools.d.ts +54 -0
- package/dist/types/cursor.d.ts +193 -8
- package/dist/types/edit/edit-clipboard.d.ts +19 -0
- package/dist/types/edit/hashline/diff.d.ts +9 -1
- package/dist/types/edit/index.d.ts +8 -1
- package/dist/types/edit/streaming.d.ts +8 -1
- package/dist/types/export/html/index.d.ts +2 -0
- package/dist/types/extensibility/extensions/runner.d.ts +19 -2
- package/dist/types/extensibility/extensions/types.d.ts +25 -1
- package/dist/types/extensibility/legacy-pi-ai-shim.d.ts +27 -1
- package/dist/types/extensibility/legacy-pi-coding-agent-shim.d.ts +2 -0
- package/dist/types/internal-urls/local-protocol.d.ts +3 -2
- package/dist/types/launch/terminal-output-worker-client.d.ts +5 -0
- package/dist/types/launch/terminal-output-worker-protocol.d.ts +14 -0
- package/dist/types/launch/terminal-output-worker.d.ts +1 -0
- package/dist/types/lsp/client.d.ts +2 -0
- package/dist/types/main.d.ts +2 -0
- package/dist/types/mcp/manager.d.ts +33 -4
- package/dist/types/modes/components/codex-reset-fireworks.d.ts +51 -0
- package/dist/types/modes/components/read-tool-group.d.ts +12 -0
- package/dist/types/modes/components/session-selector.d.ts +6 -0
- package/dist/types/modes/components/status-line/component.d.ts +23 -0
- package/dist/types/modes/components/tool-execution.d.ts +16 -7
- package/dist/types/modes/components/usage-row.d.ts +2 -0
- package/dist/types/modes/controllers/mcp-command-controller.d.ts +26 -1
- package/dist/types/modes/controllers/selector-controller.d.ts +2 -1
- package/dist/types/modes/interactive-mode.d.ts +8 -5
- package/dist/types/modes/rpc/rpc-client.d.ts +7 -0
- package/dist/types/modes/rpc/rpc-types.d.ts +16 -0
- package/dist/types/modes/theme/theme.d.ts +12 -1
- package/dist/types/modes/types.d.ts +2 -1
- package/dist/types/sdk.d.ts +1 -0
- package/dist/types/session/agent-session-events.d.ts +2 -0
- package/dist/types/session/agent-session-types.d.ts +36 -1
- package/dist/types/session/agent-session.d.ts +4 -2
- package/dist/types/session/async-job-delivery.d.ts +8 -0
- package/dist/types/session/claude-session-store.d.ts +13 -0
- package/dist/types/session/codex-session-store.d.ts +14 -0
- package/dist/types/session/credential-pin.d.ts +58 -0
- package/dist/types/session/foreign-session-import.d.ts +15 -0
- package/dist/types/session/foreign-session-jsonl.d.ts +9 -0
- package/dist/types/session/foreign-session-store.d.ts +23 -0
- package/dist/types/session/model-controls.d.ts +3 -3
- package/dist/types/session/session-advisors.d.ts +47 -2
- package/dist/types/session/session-entries.d.ts +19 -1
- package/dist/types/session/session-handoff.d.ts +4 -1
- package/dist/types/session/session-loader.d.ts +2 -0
- package/dist/types/session/session-maintenance.d.ts +1 -1
- package/dist/types/session/session-manager.d.ts +21 -0
- package/dist/types/session/session-tools.d.ts +25 -1
- package/dist/types/session/turn-recovery.d.ts +10 -4
- package/dist/types/system-prompt.d.ts +2 -1
- package/dist/types/task/executor.d.ts +11 -2
- package/dist/types/task/isolation-ownership.d.ts +34 -0
- package/dist/types/task/render.d.ts +7 -0
- package/dist/types/thinking.d.ts +18 -6
- package/dist/types/tools/acp-bridge.d.ts +28 -5
- package/dist/types/tools/browser/cmux/cmux-tab.d.ts +1 -2
- package/dist/types/tools/browser/tab-worker.d.ts +1 -4
- package/dist/types/tools/grep.d.ts +17 -1
- package/dist/types/tools/hub/launch.d.ts +5 -1
- package/dist/types/tools/index.d.ts +4 -1
- package/dist/types/tools/path-utils.d.ts +19 -0
- package/dist/types/tools/read.d.ts +2 -0
- package/dist/types/tools/xdev.d.ts +2 -1
- package/dist/types/tui/output-block.d.ts +5 -5
- package/dist/types/utils/changelog.d.ts +19 -0
- package/dist/types/utils/git.d.ts +10 -0
- package/dist/types/utils/jj.d.ts +11 -4
- package/dist/types/web/search/types.d.ts +2 -2
- package/package.json +16 -12
- package/scripts/bundle-dist.ts +3 -1
- package/src/advisor/advise-tool.ts +0 -11
- package/src/advisor/runtime.ts +138 -24
- package/src/advisor/transcript-recorder.ts +56 -0
- package/src/async/job-manager.ts +30 -7
- package/src/auto-thinking/classifier.ts +48 -10
- package/src/cleanse/agent.ts +226 -0
- package/src/cleanse/balance.ts +79 -0
- package/src/cleanse/checkers.ts +996 -0
- package/src/cleanse/index.ts +190 -0
- package/src/cleanse/loop.ts +51 -0
- package/src/cleanse/parsers.ts +726 -0
- package/src/cleanse/progress.ts +50 -0
- package/src/cleanse/prompts/assignment.md +47 -0
- package/src/cleanse/types.ts +72 -0
- package/src/cli/args.ts +6 -0
- package/src/cli/flag-tables.ts +2 -0
- package/src/cli/gallery-fixtures/fs.ts +25 -0
- package/src/cli/session-picker.ts +34 -17
- package/src/cli/ttsr-cli.ts +19 -1
- package/src/cli/update-cli.ts +3 -0
- package/src/cli/usage-cli.ts +29 -4
- package/src/cli/worktree-cli.ts +28 -11
- package/src/cli-commands.ts +1 -0
- package/src/cli.ts +11 -1
- package/src/collab/guest.ts +37 -4
- package/src/collab/host.ts +1 -0
- package/src/commands/cleanse.ts +45 -0
- package/src/commands/launch.ts +6 -0
- package/src/config/model-registry.ts +103 -25
- package/src/config/settings-schema.ts +65 -6
- package/src/config/settings.ts +25 -0
- package/src/cursor-bridge-tools.ts +81 -0
- package/src/cursor.ts +463 -12
- package/src/discovery/claude-plugins.ts +144 -34
- package/src/edit/edit-clipboard.ts +23 -0
- package/src/edit/hashline/diff.ts +49 -10
- package/src/edit/hashline/execute.ts +38 -6
- package/src/edit/hashline/filesystem.ts +27 -3
- package/src/edit/index.ts +12 -2
- package/src/edit/renderer.ts +4 -4
- package/src/edit/streaming.ts +17 -2
- package/src/export/html/index.ts +17 -10
- package/src/export/html/template.js +1 -1
- package/src/extensibility/extensions/runner.ts +70 -0
- package/src/extensibility/extensions/types.ts +30 -0
- package/src/extensibility/legacy-pi-ai-shim.ts +46 -1
- package/src/extensibility/legacy-pi-coding-agent-shim.ts +12 -13
- package/src/internal-urls/local-protocol.ts +3 -2
- package/src/internal-urls/mcp-protocol.ts +4 -1
- package/src/launch/broker.ts +14 -4
- package/src/launch/terminal-output-worker-client.ts +53 -0
- package/src/launch/terminal-output-worker-protocol.ts +11 -0
- package/src/launch/terminal-output-worker.ts +23 -0
- package/src/lsp/client.ts +18 -3
- package/src/lsp/index.ts +4 -0
- package/src/lsp/render.ts +1 -1
- package/src/main.ts +127 -56
- package/src/mcp/manager.ts +120 -18
- package/src/modes/acp/acp-agent.ts +27 -18
- package/src/modes/acp/acp-event-mapper.ts +38 -4
- package/src/modes/components/chat-transcript-builder.ts +30 -11
- package/src/modes/components/codex-reset-fireworks.ts +369 -0
- package/src/modes/components/read-tool-group.ts +159 -17
- package/src/modes/components/session-selector.ts +19 -3
- package/src/modes/components/settings-selector.ts +9 -1
- package/src/modes/components/status-line/component.ts +381 -54
- package/src/modes/components/tool-execution.ts +78 -31
- package/src/modes/components/usage-row.ts +10 -5
- package/src/modes/controllers/event-controller.ts +26 -4
- package/src/modes/controllers/mcp-command-controller.ts +68 -3
- package/src/modes/controllers/selector-controller.ts +94 -36
- package/src/modes/controllers/tan-command-controller.ts +14 -0
- package/src/modes/interactive-mode.ts +67 -64
- package/src/modes/prompt-action-autocomplete.ts +5 -3
- package/src/modes/rpc/rpc-client.ts +18 -0
- package/src/modes/rpc/rpc-mode.ts +16 -1
- package/src/modes/rpc/rpc-types.ts +11 -0
- package/src/modes/theme/theme.ts +151 -6
- package/src/modes/types.ts +2 -1
- package/src/modes/utils/ui-helpers.ts +38 -21
- package/src/prompts/goals/guided-goal-interview.md +41 -6
- package/src/prompts/system/auto-thinking-difficulty.md +4 -2
- package/src/prompts/system/plan-mode-active.md +2 -2
- package/src/prompts/system/system-prompt.md +3 -0
- package/src/prompts/system/vibe-mode-active.md +4 -1
- package/src/prompts/system/xdev-mount-notice.md +1 -1
- package/src/prompts/tools/bash.md +15 -17
- package/src/prompts/tools/browser.md +1 -0
- package/src/prompts/tools/checkpoint.md +1 -1
- package/src/prompts/tools/glob.md +7 -6
- package/src/prompts/tools/goal.md +1 -1
- package/src/prompts/tools/grep.md +7 -6
- package/src/sdk.ts +178 -15
- package/src/session/agent-session-events.ts +1 -0
- package/src/session/agent-session-types.ts +40 -1
- package/src/session/agent-session.ts +330 -179
- package/src/session/async-job-delivery.ts +8 -0
- package/src/session/claude-session-store.ts +426 -0
- package/src/session/codex-session-store.ts +673 -0
- package/src/session/credential-pin.ts +93 -0
- package/src/session/foreign-session-import.ts +52 -0
- package/src/session/foreign-session-jsonl.ts +29 -0
- package/src/session/foreign-session-store.ts +26 -0
- package/src/session/model-controls.ts +15 -7
- package/src/session/session-advisors.ts +142 -23
- package/src/session/session-entries.ts +21 -1
- package/src/session/session-handoff.ts +20 -3
- package/src/session/session-history-format.ts +31 -6
- package/src/session/session-loader.ts +25 -9
- package/src/session/session-maintenance.ts +59 -11
- package/src/session/session-manager.ts +61 -0
- package/src/session/session-tools.ts +107 -5
- package/src/session/settings-stream-fn.ts +1 -0
- package/src/session/turn-recovery.ts +97 -51
- package/src/slash-commands/builtin-registry.ts +136 -5
- package/src/system-prompt.ts +3 -2
- package/src/task/executor.ts +17 -4
- package/src/task/isolation-ownership.ts +106 -0
- package/src/task/render.ts +20 -4
- package/src/task/worktree.ts +8 -0
- package/src/thinking.ts +29 -10
- package/src/tools/acp-bridge.ts +52 -8
- package/src/tools/ask.ts +3 -3
- package/src/tools/ast-edit.ts +9 -2
- package/src/tools/browser/cmux/cmux-tab.ts +9 -14
- package/src/tools/browser/tab-worker.ts +12 -35
- package/src/tools/browser.ts +7 -2
- package/src/tools/checkpoint.ts +0 -13
- package/src/tools/gh-renderer.ts +3 -3
- package/src/tools/grep.ts +64 -8
- package/src/tools/hub/launch.ts +10 -15
- package/src/tools/index.ts +38 -5
- package/src/tools/path-utils.ts +88 -0
- package/src/tools/read.ts +3 -0
- package/src/tools/write.ts +44 -3
- package/src/tools/xdev.ts +39 -14
- package/src/tui/code-cell.ts +4 -4
- package/src/tui/output-block.ts +25 -8
- package/src/utils/changelog.ts +144 -4
- package/src/utils/git.ts +61 -14
- package/src/utils/jj.ts +19 -13
- package/src/utils/shell-snapshot-fn-env.sh +5 -2
- package/src/web/search/providers/codex.ts +34 -0
- package/src/web/search/render.ts +2 -2
- package/src/web/search/types.ts +1 -1
- package/dist/types/goals/guided-setup.d.ts +0 -30
- package/src/goals/guided-setup.ts +0 -171
- package/src/prompts/goals/guided-goal-system.md +0 -33
package/src/cursor.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { randomUUID } from "node:crypto";
|
|
2
2
|
import * as fs from "node:fs";
|
|
3
|
+
import * as path from "node:path";
|
|
3
4
|
import type {
|
|
4
5
|
AgentEvent,
|
|
5
6
|
AgentTool,
|
|
@@ -9,35 +10,91 @@ import type {
|
|
|
9
10
|
} from "@oh-my-pi/pi-agent-core";
|
|
10
11
|
import type {
|
|
11
12
|
CursorMcpCall,
|
|
13
|
+
CursorMcpResource,
|
|
14
|
+
CursorMcpResourceContent,
|
|
12
15
|
CursorShellStreamCallbacks,
|
|
13
16
|
CursorTodoSnapshot,
|
|
14
17
|
CursorExecHandlers as ICursorExecHandlers,
|
|
15
18
|
ToolResultMessage,
|
|
16
19
|
} from "@oh-my-pi/pi-ai";
|
|
20
|
+
import {
|
|
21
|
+
piEscapeRegexLiteral,
|
|
22
|
+
piGrepSkip,
|
|
23
|
+
piJoinPath,
|
|
24
|
+
piLimit,
|
|
25
|
+
piLsPath,
|
|
26
|
+
piReadPath,
|
|
27
|
+
piTimeout,
|
|
28
|
+
} from "@oh-my-pi/pi-ai/providers/cursor/exec-modern";
|
|
17
29
|
import { sanitizeText } from "@oh-my-pi/pi-utils";
|
|
18
|
-
import {
|
|
30
|
+
import type { MCPResourceReadResult } from "./mcp/types";
|
|
31
|
+
import type { ApprovalMode } from "./tools/approval";
|
|
32
|
+
import { resolveApproval } from "./tools/approval";
|
|
33
|
+
import { confineToWorkspace, resolveToCwd } from "./tools/path-utils";
|
|
19
34
|
import type { TodoPhase, TodoStatus } from "./tools/todo";
|
|
20
35
|
|
|
21
36
|
/** Phase used for Cursor-owned tasks with no local phase grouping. */
|
|
22
37
|
const CURSOR_TODO_PHASE = "Tasks";
|
|
23
38
|
|
|
39
|
+
/**
|
|
40
|
+
* A tool instance the bridge can run, matching the erased shape the session's
|
|
41
|
+
* tool registry stores. The concrete tools have narrower `execute` parameter
|
|
42
|
+
* types than the default `AgentTool`, which only unify through this alias.
|
|
43
|
+
*/
|
|
44
|
+
type CursorBridgeTool = AgentTool<any, any, any>;
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* The live MCP connections Cursor's resource frames are answered from.
|
|
48
|
+
*
|
|
49
|
+
* Named so every construction site can hand over the same adapter; a session
|
|
50
|
+
* and its advisors share one set of connections.
|
|
51
|
+
*/
|
|
52
|
+
export interface CursorMcpResourceAdapter {
|
|
53
|
+
serverNames(): string[];
|
|
54
|
+
getServerResources(
|
|
55
|
+
name: string,
|
|
56
|
+
): Promise<{ resources: { uri: string; name?: string; description?: string; mimeType?: string }[] } | undefined>;
|
|
57
|
+
readServerResource(name: string, uri: string): Promise<MCPResourceReadResult | undefined>;
|
|
58
|
+
}
|
|
59
|
+
|
|
24
60
|
interface CursorExecBridgeOptions {
|
|
25
61
|
cwd: string;
|
|
26
62
|
getCwd?: () => string;
|
|
27
63
|
tools: Map<string, AgentTool>;
|
|
28
64
|
/** Resolves execution overrides (mounted-device permission wrappers) before the canonical map. */
|
|
29
65
|
getExecutableTool?: (name: string) => AgentTool | undefined;
|
|
66
|
+
/**
|
|
67
|
+
* The `replace`-mode `edit` instance `pi_edit` must run, when the session
|
|
68
|
+
* granted `edit` at all.
|
|
69
|
+
*
|
|
70
|
+
* `PiEditExecArgs` is that mode's schema verbatim, and the session's own
|
|
71
|
+
* `edit` may be in any mode — `hashline` by default — whose schema rejects
|
|
72
|
+
* `old_text`/`new_text` outright. {@link tools} therefore cannot be trusted
|
|
73
|
+
* for this one frame: a session that starts on another provider keeps its
|
|
74
|
+
* configured instance in the map (only Cursor sessions move `edit` out), and
|
|
75
|
+
* switching to Cursor later does not rebuild the roster.
|
|
76
|
+
*/
|
|
77
|
+
getEditReplaceTool?: () => CursorBridgeTool | undefined;
|
|
30
78
|
getToolContext?: () => AgentToolContext | undefined;
|
|
31
79
|
emitEvent?: (event: AgentEvent) => void;
|
|
32
80
|
/**
|
|
33
|
-
* Whether
|
|
34
|
-
*
|
|
35
|
-
*
|
|
36
|
-
*
|
|
37
|
-
*
|
|
38
|
-
*
|
|
81
|
+
* Whether frames that mutate the filesystem WITHOUT running a registry tool
|
|
82
|
+
* may do so: the native `delete` frame, and a `read_mcp_resource` carrying
|
|
83
|
+
* `download_path`. Both write or remove workspace files directly instead of
|
|
84
|
+
* consulting {@link tools}, so a background read-only advisor could touch
|
|
85
|
+
* files it was never granted a mutating tool for (issue #5680 review).
|
|
86
|
+
*
|
|
87
|
+
* This is a grant, not a policy: it answers "did the session hand this
|
|
88
|
+
* channel a file-writing tool", which callers derive from their own roster
|
|
89
|
+
* before any bridge-specific rewriting. The primary Cursor session moves
|
|
90
|
+
* `edit` out of {@link tools} and serves it through
|
|
91
|
+
* {@link getEditReplaceTool}, so reading the map here would deny an
|
|
92
|
+
* edit-only session. Defaults to allowed
|
|
93
|
+
* to preserve the primary agent's behavior; callers with a restricted tool
|
|
94
|
+
* set (advisors) opt out. The user's approval policy is resolved separately,
|
|
95
|
+
* per call.
|
|
39
96
|
*/
|
|
40
|
-
|
|
97
|
+
allowDirectFileMutation?: boolean;
|
|
41
98
|
/**
|
|
42
99
|
* Mirror Cursor's server-owned todo list into local session state. Cursor
|
|
43
100
|
* resolves `update_todos` / `read_todos` remotely, so without this bridge
|
|
@@ -50,6 +107,99 @@ interface CursorExecBridgeOptions {
|
|
|
50
107
|
* Cursor emits no local `todo` toolResult, so nothing else records it.
|
|
51
108
|
*/
|
|
52
109
|
persistTodoPhases?: (phases: TodoPhase[]) => void;
|
|
110
|
+
/**
|
|
111
|
+
* Build a `grep` tool honoring a frame's own context width and match cap.
|
|
112
|
+
*
|
|
113
|
+
* The modern `pi_grep` frame carries both, and the shared `grep` instance
|
|
114
|
+
* is fixed to the session settings at construction — so without this the
|
|
115
|
+
* two fields are silently dropped. Callers that cannot supply it keep the
|
|
116
|
+
* shared instance and the session's defaults.
|
|
117
|
+
*
|
|
118
|
+
* The returned tool is executed as-is. Callers whose registry tools carry an
|
|
119
|
+
* approval wrapper MUST apply the same wrapper here, or a frame supplying
|
|
120
|
+
* either field silently escapes the approval gate that every other call
|
|
121
|
+
* goes through.
|
|
122
|
+
*/
|
|
123
|
+
createGrepTool?(options: { context?: number; totalMatchLimit?: number }): CursorBridgeTool | undefined;
|
|
124
|
+
/**
|
|
125
|
+
* The session's live MCP connections, for Cursor's resource frames.
|
|
126
|
+
*
|
|
127
|
+
* `list_mcp_resources` / `read_mcp_resource` ask what this client's servers
|
|
128
|
+
* advertise. Without this the bridge answers an empty catalog and
|
|
129
|
+
* `not_found`, hiding resources the session is in fact connected to.
|
|
130
|
+
*
|
|
131
|
+
* `getServerResources` is async because a server's catalog loads in the
|
|
132
|
+
* background after its tools register: a frame arriving in that window would
|
|
133
|
+
* otherwise read the not-yet-populated cache and report an empty catalog,
|
|
134
|
+
* which is indistinguishable from a server that advertises nothing.
|
|
135
|
+
*/
|
|
136
|
+
mcpResources?: CursorMcpResourceAdapter;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Write a downloaded resource without following a link at the target.
|
|
141
|
+
*
|
|
142
|
+
* The containment check and the write are separate syscalls, so a link planted
|
|
143
|
+
* at the target in between would redirect the bytes — the check cannot close
|
|
144
|
+
* that window on its own. `O_NOFOLLOW` decides it atomically for symlinks.
|
|
145
|
+
*
|
|
146
|
+
* A hard link needs a second check: it is a regular file that passes both the
|
|
147
|
+
* containment check and `O_NOFOLLOW` while sharing its inode with a file
|
|
148
|
+
* anywhere else on the volume, so truncating it overwrites that file too.
|
|
149
|
+
* `nlink > 1` on the OPEN handle is the test — statting the path first would
|
|
150
|
+
* reintroduce the race the open just closed. Same reasoning, and the same
|
|
151
|
+
* refusal, as `autolearn/managed-skills.ts`.
|
|
152
|
+
*
|
|
153
|
+
* Truncation therefore happens after that check rather than through `O_TRUNC`,
|
|
154
|
+
* which would have already destroyed the contents by the time it ran.
|
|
155
|
+
*
|
|
156
|
+
* Scope: `O_NOFOLLOW` applies to the FINAL component only. A parent directory
|
|
157
|
+
* swapped for an outward symlink between the check and this open is still
|
|
158
|
+
* followed; refusing that needs an `openat`/dirfd walk of every segment, which
|
|
159
|
+
* this does not attempt — an attacker who can rewrite the workspace tree
|
|
160
|
+
* mid-download is already inside the boundary this guard defends.
|
|
161
|
+
*
|
|
162
|
+
* Parent directories are created first, since the frame may name a path whose
|
|
163
|
+
* directories do not exist yet.
|
|
164
|
+
*
|
|
165
|
+
* `O_NONBLOCK` is what keeps the guard below reachable. A write-only open of a
|
|
166
|
+
* FIFO blocks until a reader arrives, so a `download_path` naming one would
|
|
167
|
+
* hang the turn forever WITHOUT the non-regular check ever running — the open
|
|
168
|
+
* itself never returns. Non-blocking turns that into `ENXIO` when no reader is
|
|
169
|
+
* attached, and hands back a descriptor the `isFile()` check refuses when one
|
|
170
|
+
* is. The flag has no effect on regular files, which is every legitimate
|
|
171
|
+
* target.
|
|
172
|
+
*/
|
|
173
|
+
async function writeWithoutFollowingLinks(absolutePath: string, payload: string | Buffer): Promise<void> {
|
|
174
|
+
await fs.promises.mkdir(path.dirname(absolutePath), { recursive: true });
|
|
175
|
+
const handle = await fs.promises
|
|
176
|
+
.open(
|
|
177
|
+
absolutePath,
|
|
178
|
+
fs.constants.O_WRONLY | fs.constants.O_CREAT | fs.constants.O_NOFOLLOW | fs.constants.O_NONBLOCK,
|
|
179
|
+
)
|
|
180
|
+
.catch((error: NodeJS.ErrnoException) => {
|
|
181
|
+
// A readerless FIFO. Reported as the refusal it is, rather than the
|
|
182
|
+
// bare "no such device or address" the errno spells out.
|
|
183
|
+
if (error.code === "ENXIO") {
|
|
184
|
+
throw new Error(`Refusing to download onto a special file: ${absolutePath}`);
|
|
185
|
+
}
|
|
186
|
+
throw error;
|
|
187
|
+
});
|
|
188
|
+
try {
|
|
189
|
+
const stat = await handle.stat();
|
|
190
|
+
if (!stat.isFile()) {
|
|
191
|
+
throw new Error(`Refusing to download onto a non-regular file: ${absolutePath}`);
|
|
192
|
+
}
|
|
193
|
+
if (stat.nlink > 1) {
|
|
194
|
+
throw new Error(
|
|
195
|
+
`Refusing to download onto a file with ${stat.nlink} hard links, which would overwrite its other names: ${absolutePath}`,
|
|
196
|
+
);
|
|
197
|
+
}
|
|
198
|
+
await handle.truncate(0);
|
|
199
|
+
await handle.writeFile(payload);
|
|
200
|
+
} finally {
|
|
201
|
+
await handle.close();
|
|
202
|
+
}
|
|
53
203
|
}
|
|
54
204
|
|
|
55
205
|
function createToolResultMessage(
|
|
@@ -81,8 +231,9 @@ async function executeTool(
|
|
|
81
231
|
toolName: string,
|
|
82
232
|
toolCallId: string,
|
|
83
233
|
args: Record<string, unknown>,
|
|
234
|
+
overrideTool?: CursorBridgeTool,
|
|
84
235
|
): Promise<ToolResultMessage> {
|
|
85
|
-
const tool = options.getExecutableTool?.(toolName) ?? options.tools.get(toolName);
|
|
236
|
+
const tool = overrideTool ?? options.getExecutableTool?.(toolName) ?? options.tools.get(toolName);
|
|
86
237
|
if (!tool) {
|
|
87
238
|
const result = buildToolErrorResult(`Tool "${toolName}" not available`);
|
|
88
239
|
return createToolResultMessage(toolCallId, toolName, result, true);
|
|
@@ -133,14 +284,49 @@ async function executeTool(
|
|
|
133
284
|
return createToolResultMessage(toolCallId, toolName, result, isError);
|
|
134
285
|
}
|
|
135
286
|
|
|
287
|
+
/**
|
|
288
|
+
* Resolve the user's policy for a frame that mutates the filesystem directly.
|
|
289
|
+
*
|
|
290
|
+
* The native `delete` and `read_mcp_resource` download frames both bypass the
|
|
291
|
+
* registry, so no approval wrapper sits in front of them. `write` is the tier
|
|
292
|
+
* a file creation or removal belongs to. Returns `null` when the call may
|
|
293
|
+
* proceed, or the refusal text to answer with.
|
|
294
|
+
*/
|
|
295
|
+
function refuseByWritePolicy(options: CursorExecBridgeOptions, toolName: string, pathArg: string): string | null {
|
|
296
|
+
const context = options.getToolContext?.();
|
|
297
|
+
const settings = context?.settings;
|
|
298
|
+
const approvalMode: ApprovalMode =
|
|
299
|
+
context?.autoApprove === true ? "yolo" : (settings?.get("tools.approvalMode") ?? "yolo");
|
|
300
|
+
const approval = resolveApproval(
|
|
301
|
+
{ name: toolName, approval: "write" },
|
|
302
|
+
{ path: pathArg },
|
|
303
|
+
approvalMode,
|
|
304
|
+
(settings?.get("tools.approval") ?? {}) as Record<string, unknown>,
|
|
305
|
+
);
|
|
306
|
+
if (approval.policy === "allow") return null;
|
|
307
|
+
return approval.policy === "deny"
|
|
308
|
+
? `Tool "${toolName}" is blocked by user policy.`
|
|
309
|
+
: `Tool "${toolName}" requires approval, which this channel cannot request.`;
|
|
310
|
+
}
|
|
311
|
+
|
|
136
312
|
async function executeDelete(options: CursorExecBridgeOptions, pathArg: string, toolCallId: string) {
|
|
137
313
|
const toolName = "delete";
|
|
138
314
|
|
|
139
|
-
if (options.
|
|
315
|
+
if (options.allowDirectFileMutation === false) {
|
|
140
316
|
const result = buildToolErrorResult(`Tool "${toolName}" not available`);
|
|
141
317
|
return createToolResultMessage(toolCallId, toolName, result, true);
|
|
142
318
|
}
|
|
143
319
|
|
|
320
|
+
// Unlike every other frame, this one mutates the filesystem directly instead
|
|
321
|
+
// of running a registry tool, so no approval wrapper sits in front of it.
|
|
322
|
+
// `allowDirectFileMutation` answers "was a mutating tool granted", which is a
|
|
323
|
+
// different question from "does the user's policy allow this call" — without
|
|
324
|
+
// this, a configured `deny` or an `always-ask` session still lost the file.
|
|
325
|
+
const refusal = refuseByWritePolicy(options, toolName, pathArg);
|
|
326
|
+
if (refusal) {
|
|
327
|
+
return createToolResultMessage(toolCallId, toolName, buildToolErrorResult(refusal), true);
|
|
328
|
+
}
|
|
329
|
+
|
|
144
330
|
options.emitEvent?.({ type: "tool_execution_start", toolCallId, toolName, args: { path: pathArg } });
|
|
145
331
|
|
|
146
332
|
const absolutePath = resolveToCwd(pathArg, options.getCwd?.() ?? options.cwd);
|
|
@@ -241,10 +427,21 @@ function buildTodoSyncResult(
|
|
|
241
427
|
export class CursorExecHandlers implements ICursorExecHandlers {
|
|
242
428
|
constructor(private options: CursorExecBridgeOptions) {}
|
|
243
429
|
|
|
430
|
+
/**
|
|
431
|
+
* Modern Cursor builds paginate the legacy `read` frame with
|
|
432
|
+
* `offset`/`limit`, exactly as `pi_read` does. Dropping them returns the
|
|
433
|
+
* whole file (or its own truncation) for every page, so a model walking a
|
|
434
|
+
* large file never advances. Composed with the same helper, so both frames
|
|
435
|
+
* translate a range identically.
|
|
436
|
+
*/
|
|
244
437
|
async read(args: Parameters<NonNullable<ICursorExecHandlers["read"]>>[0]) {
|
|
245
438
|
const toolCallId = decodeToolCallId(args.toolCallId);
|
|
246
|
-
const
|
|
247
|
-
|
|
439
|
+
const composed = piReadPath(args.path, args.offset, args.limit);
|
|
440
|
+
// A present `limit: 0` asks for zero lines; no selector expresses that.
|
|
441
|
+
if (composed === null) {
|
|
442
|
+
return createToolResultMessage(toolCallId, "read", { content: [{ type: "text", text: "" }] }, false);
|
|
443
|
+
}
|
|
444
|
+
return await executeTool(this.options, "read", toolCallId, { path: composed });
|
|
248
445
|
}
|
|
249
446
|
|
|
250
447
|
async ls(args: Parameters<NonNullable<ICursorExecHandlers["ls"]>>[0]) {
|
|
@@ -254,6 +451,12 @@ export class CursorExecHandlers implements ICursorExecHandlers {
|
|
|
254
451
|
return toolResultMessage;
|
|
255
452
|
}
|
|
256
453
|
|
|
454
|
+
/**
|
|
455
|
+
* Modern Cursor builds paginate this frame with `offset`. The local `grep`
|
|
456
|
+
* paginates by file through `skip`, which is the same unit its own
|
|
457
|
+
* "use skip=N for the next page" advice counts in — so an unforwarded
|
|
458
|
+
* offset re-runs the identical search and returns page one forever.
|
|
459
|
+
*/
|
|
257
460
|
async grep(args: Parameters<NonNullable<ICursorExecHandlers["grep"]>>[0]) {
|
|
258
461
|
const toolCallId = decodeToolCallId(args.toolCallId);
|
|
259
462
|
const searchPath = args.glob ? `${args.path || "."}/${args.glob}` : args.path || ".";
|
|
@@ -261,6 +464,7 @@ export class CursorExecHandlers implements ICursorExecHandlers {
|
|
|
261
464
|
pattern: args.pattern,
|
|
262
465
|
path: searchPath,
|
|
263
466
|
case: args.caseInsensitive === true ? false : undefined,
|
|
467
|
+
skip: piGrepSkip(args.offset),
|
|
264
468
|
});
|
|
265
469
|
return toolResultMessage;
|
|
266
470
|
}
|
|
@@ -401,6 +605,227 @@ export class CursorExecHandlers implements ICursorExecHandlers {
|
|
|
401
605
|
});
|
|
402
606
|
return toolResultMessage;
|
|
403
607
|
}
|
|
608
|
+
/**
|
|
609
|
+
* Modern Cursor CLI Pi tool frames (`ExecServerMessage` 45-51).
|
|
610
|
+
*
|
|
611
|
+
* These are a separate frame family from the legacy `read`/`shell`/... set,
|
|
612
|
+
* not aliases: different args, different result oneofs, and no `tool_call_id`
|
|
613
|
+
* (the provider mints one and passes it in `call.toolCallId`). Each maps onto
|
|
614
|
+
* the local tool with matching semantics, so the same approval, sandboxing
|
|
615
|
+
* and event plumbing applies as for a model-issued call.
|
|
616
|
+
*/
|
|
617
|
+
/**
|
|
618
|
+
* `offset`/`limit` are a 1-indexed start line plus a line count (verified
|
|
619
|
+
* against the reference `LocalPiReadExecutor`), which is exactly the local
|
|
620
|
+
* `read` tool's `:N+K` inline selector — the tool takes no range kwargs, so
|
|
621
|
+
* the range has to be composed onto the path or ranged reads silently
|
|
622
|
+
* return the whole file.
|
|
623
|
+
*/
|
|
624
|
+
async piRead(call: Parameters<NonNullable<ICursorExecHandlers["piRead"]>>[0]) {
|
|
625
|
+
const { path: readPath, offset, limit } = call.args;
|
|
626
|
+
const composed = piReadPath(readPath, offset, limit);
|
|
627
|
+
// A present `limit: 0` asks for zero lines. The reference slices an empty
|
|
628
|
+
// string for it; no `read` selector expresses that, so answer directly
|
|
629
|
+
// rather than falling back to a whole-file read.
|
|
630
|
+
if (composed === null) {
|
|
631
|
+
return createToolResultMessage(call.toolCallId, "read", { content: [{ type: "text", text: "" }] }, false);
|
|
632
|
+
}
|
|
633
|
+
return await executeTool(this.options, "read", call.toolCallId, { path: composed });
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
async piBash(call: Parameters<NonNullable<ICursorExecHandlers["piBash"]>>[0]) {
|
|
637
|
+
return await executeTool(this.options, "bash", call.toolCallId, {
|
|
638
|
+
command: call.args.command,
|
|
639
|
+
timeout: piTimeout(call.args.timeout),
|
|
640
|
+
});
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
/**
|
|
644
|
+
* `PiEditExecArgs` is the local `edit` tool's replace mode verbatim: a path
|
|
645
|
+
* plus `old_text`/`new_text` pairs. The tool's schema is snake_case, so the
|
|
646
|
+
* proto's camelCase accessors are mapped back on the way in.
|
|
647
|
+
*
|
|
648
|
+
* The replace-mode instance is requested explicitly rather than resolved
|
|
649
|
+
* from {@link CursorExecBridgeOptions.tools}: the registry's `edit` is in
|
|
650
|
+
* the session's configured mode, whose schema rejects these arguments.
|
|
651
|
+
*/
|
|
652
|
+
async piEdit(call: Parameters<NonNullable<ICursorExecHandlers["piEdit"]>>[0]) {
|
|
653
|
+
return await executeTool(
|
|
654
|
+
this.options,
|
|
655
|
+
"edit",
|
|
656
|
+
call.toolCallId,
|
|
657
|
+
{
|
|
658
|
+
path: call.args.path,
|
|
659
|
+
edits: call.args.edits.map(edit => ({ old_text: edit.oldText, new_text: edit.newText })),
|
|
660
|
+
},
|
|
661
|
+
this.options.getEditReplaceTool?.(),
|
|
662
|
+
);
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
async piWrite(call: Parameters<NonNullable<ICursorExecHandlers["piWrite"]>>[0]) {
|
|
666
|
+
return await executeTool(this.options, "write", call.toolCallId, {
|
|
667
|
+
path: call.args.path,
|
|
668
|
+
content: call.args.content,
|
|
669
|
+
});
|
|
670
|
+
}
|
|
671
|
+
|
|
672
|
+
/**
|
|
673
|
+
* `literal` makes the pattern a fixed string; the local tool is regex-only,
|
|
674
|
+
* so the pattern is escaped on the way in (same translation the legacy pi
|
|
675
|
+
* shim does).
|
|
676
|
+
*
|
|
677
|
+
* `context` and `limit` are not expressible in the model-facing schema —
|
|
678
|
+
* context width comes from settings fixed at tool construction — so the
|
|
679
|
+
* frame's values are honored by building a per-call `grep` through
|
|
680
|
+
* {@link CursorExecBridgeOptions.createGrepTool}. Both are `optional int32`,
|
|
681
|
+
* so a present `0` context means "no context lines", not "use the default".
|
|
682
|
+
* Without the factory the shared instance runs with session defaults.
|
|
683
|
+
*/
|
|
684
|
+
async piGrep(call: Parameters<NonNullable<ICursorExecHandlers["piGrep"]>>[0]) {
|
|
685
|
+
const { pattern, path, glob, ignoreCase, literal, context, limit } = call.args;
|
|
686
|
+
const scoped =
|
|
687
|
+
context !== undefined || limit !== undefined
|
|
688
|
+
? this.options.createGrepTool?.({ context, totalMatchLimit: piLimit(limit) })
|
|
689
|
+
: undefined;
|
|
690
|
+
// Same arg mapping as the legacy `grep` handler: the local tool takes one
|
|
691
|
+
// path spec, and its `case` flag is case-SENSITIVITY, the inverse of the
|
|
692
|
+
// frame's `ignore_case`.
|
|
693
|
+
return await executeTool(
|
|
694
|
+
this.options,
|
|
695
|
+
"grep",
|
|
696
|
+
call.toolCallId,
|
|
697
|
+
{
|
|
698
|
+
pattern: literal === true ? piEscapeRegexLiteral(pattern) : pattern,
|
|
699
|
+
path: glob ? piJoinPath(path, glob) : path || ".",
|
|
700
|
+
case: ignoreCase === true ? false : undefined,
|
|
701
|
+
},
|
|
702
|
+
scoped,
|
|
703
|
+
);
|
|
704
|
+
}
|
|
705
|
+
|
|
706
|
+
/**
|
|
707
|
+
* `pi_find` is a filename search, which is the local `glob` tool — not
|
|
708
|
+
* `grep`. Its `pattern` is a glob, joined onto `path` because `glob` takes a
|
|
709
|
+
* single combined path spec.
|
|
710
|
+
*
|
|
711
|
+
* `limit` is `optional int32`, so `0` is present rather than unset; the
|
|
712
|
+
* reference clamps it with `Math.max(1, limit ?? 1000)`, and an unset limit
|
|
713
|
+
* leaves the local tool's own default in place.
|
|
714
|
+
*/
|
|
715
|
+
async piFind(call: Parameters<NonNullable<ICursorExecHandlers["piFind"]>>[0]) {
|
|
716
|
+
const { pattern, path, limit } = call.args;
|
|
717
|
+
return await executeTool(this.options, "glob", call.toolCallId, {
|
|
718
|
+
path: piJoinPath(path, pattern),
|
|
719
|
+
limit: piLimit(limit),
|
|
720
|
+
});
|
|
721
|
+
}
|
|
722
|
+
|
|
723
|
+
/**
|
|
724
|
+
* Redirected to `read`, which lists directories — same as the legacy `ls`.
|
|
725
|
+
* The frame's entry `limit` is not mapped; see {@link piLsPath}.
|
|
726
|
+
*/
|
|
727
|
+
async piLs(call: Parameters<NonNullable<ICursorExecHandlers["piLs"]>>[0]) {
|
|
728
|
+
return await executeTool(this.options, "read", call.toolCallId, { path: piLsPath(call.args.path) });
|
|
729
|
+
}
|
|
730
|
+
|
|
731
|
+
/**
|
|
732
|
+
* The resources this client's MCP servers advertise.
|
|
733
|
+
*
|
|
734
|
+
* Cursor addresses a later read by `server`, so every entry carries the name
|
|
735
|
+
* it came from. An absent `server` filter means "all of them".
|
|
736
|
+
*/
|
|
737
|
+
async listMcpResources({ server }: { server?: string }): Promise<CursorMcpResource[]> {
|
|
738
|
+
const mcp = this.options.mcpResources;
|
|
739
|
+
if (!mcp) return [];
|
|
740
|
+
const names = server ? [server] : mcp.serverNames();
|
|
741
|
+
// Concurrently: each name may block on that server's first catalog load,
|
|
742
|
+
// and a slow server should not delay the rest of the listing.
|
|
743
|
+
const catalogs = await Promise.all(names.map(async name => [name, await mcp.getServerResources(name)] as const));
|
|
744
|
+
const listed: CursorMcpResource[] = [];
|
|
745
|
+
for (const [name, catalog] of catalogs) {
|
|
746
|
+
for (const resource of catalog?.resources ?? []) {
|
|
747
|
+
listed.push({
|
|
748
|
+
uri: resource.uri,
|
|
749
|
+
name: resource.name,
|
|
750
|
+
description: resource.description,
|
|
751
|
+
mimeType: resource.mimeType,
|
|
752
|
+
server: name,
|
|
753
|
+
});
|
|
754
|
+
}
|
|
755
|
+
}
|
|
756
|
+
return listed;
|
|
757
|
+
}
|
|
758
|
+
|
|
759
|
+
/**
|
|
760
|
+
* Read one resource, or `null` when the server or uri is unknown.
|
|
761
|
+
*
|
|
762
|
+
* MCP returns a list of content items; the wire carries exactly one text or
|
|
763
|
+
* blob. Text items are joined, since a multi-part text resource is one
|
|
764
|
+
* document; otherwise the first blob stands in. `blob` arrives base64 and
|
|
765
|
+
* the wire wants bytes.
|
|
766
|
+
*
|
|
767
|
+
* A `downloadPath` frame is a different contract: write the bytes to that
|
|
768
|
+
* workspace-relative path and answer with the path alone, so a large binary
|
|
769
|
+
* lands on disk instead of in the model's context. That makes it a workspace
|
|
770
|
+
* mutation reached without a registry tool, so it is gated exactly like the
|
|
771
|
+
* native `delete` frame — on the session actually granting a file-writing
|
|
772
|
+
* tool, and on the user's `write`-tier policy. The gate runs before the read
|
|
773
|
+
* so a refused download never fetches the resource either.
|
|
774
|
+
*/
|
|
775
|
+
async readMcpResource({
|
|
776
|
+
server,
|
|
777
|
+
uri,
|
|
778
|
+
downloadPath,
|
|
779
|
+
}: {
|
|
780
|
+
server: string;
|
|
781
|
+
uri: string;
|
|
782
|
+
downloadPath?: string;
|
|
783
|
+
}): Promise<CursorMcpResourceContent | null> {
|
|
784
|
+
if (downloadPath) {
|
|
785
|
+
if (this.options.allowDirectFileMutation === false) {
|
|
786
|
+
throw new Error('Tool "write" not available: this session cannot download resources to disk.');
|
|
787
|
+
}
|
|
788
|
+
const refusal = refuseByWritePolicy(this.options, "write", downloadPath);
|
|
789
|
+
if (refusal) throw new Error(refusal);
|
|
790
|
+
}
|
|
791
|
+
const mcp = this.options.mcpResources;
|
|
792
|
+
if (!mcp) return null;
|
|
793
|
+
const read = await mcp.readServerResource(server, uri);
|
|
794
|
+
if (!read) return null;
|
|
795
|
+
// The mime type must describe the bytes actually sent, not whatever item
|
|
796
|
+
// happened to be first: an image blob followed by a text note would
|
|
797
|
+
// otherwise label the text `image/png` and mislead the model about what
|
|
798
|
+
// it is holding. Each branch below takes the type from its own producer.
|
|
799
|
+
const textItems = read.contents.filter(item => item.text !== undefined);
|
|
800
|
+
const texts = textItems.map(item => item.text as string);
|
|
801
|
+
const blobItem = read.contents.find(item => item.blob !== undefined);
|
|
802
|
+
const blob = blobItem?.blob;
|
|
803
|
+
const textMimeType = textItems[0]?.mimeType;
|
|
804
|
+
const blobMimeType = blobItem?.mimeType;
|
|
805
|
+
|
|
806
|
+
if (downloadPath) {
|
|
807
|
+
// Text resources download as their own bytes; a blob decodes first.
|
|
808
|
+
const payload =
|
|
809
|
+
texts.length > 0 ? texts.join("\n") : blob !== undefined ? Buffer.from(blob, "base64") : undefined;
|
|
810
|
+
if (payload === undefined) return null;
|
|
811
|
+
// The path is workspace-relative BY CONTRACT, but it arrives from the
|
|
812
|
+
// server, and `resolveToCwd` deliberately honors absolute paths and
|
|
813
|
+
// `..` for user-authored tool input. Taking it at its word would let a
|
|
814
|
+
// frame write anywhere this process can reach, so confine it here
|
|
815
|
+
// rather than trusting the declaration.
|
|
816
|
+
const cwd = this.options.getCwd?.() ?? this.options.cwd;
|
|
817
|
+
const absolutePath = confineToWorkspace(downloadPath, cwd);
|
|
818
|
+
if (!absolutePath) throw new Error(`Refusing to download outside the workspace: ${downloadPath}`);
|
|
819
|
+
await writeWithoutFollowingLinks(absolutePath, payload);
|
|
820
|
+
// The path echoed back is the one the frame asked for; the model
|
|
821
|
+
// addresses it the same relative way.
|
|
822
|
+
return { uri, mimeType: texts.length > 0 ? textMimeType : blobMimeType, downloadPath };
|
|
823
|
+
}
|
|
824
|
+
|
|
825
|
+
if (texts.length > 0) return { uri, mimeType: textMimeType, text: texts.join("\n") };
|
|
826
|
+
if (blob === undefined) return null;
|
|
827
|
+
return { uri, mimeType: blobMimeType, blob: Buffer.from(blob, "base64") };
|
|
828
|
+
}
|
|
404
829
|
|
|
405
830
|
/**
|
|
406
831
|
* Settle a completed native Cursor todo call, mirroring its list when the
|
|
@@ -510,4 +935,30 @@ export class CursorExecHandlers implements ICursorExecHandlers {
|
|
|
510
935
|
const toolResultMessage = await executeTool(this.options, toolName, toolCallId, args);
|
|
511
936
|
return toolResultMessage;
|
|
512
937
|
}
|
|
938
|
+
|
|
939
|
+
/**
|
|
940
|
+
* Resolve an MCP call's approval without running it.
|
|
941
|
+
*
|
|
942
|
+
* Same resolution the wrapper applies at execution time, minus the
|
|
943
|
+
* execution: an unknown tool is not approvable, and a `prompt` is not an
|
|
944
|
+
* approval — the frame has no way to carry an interactive question, and the
|
|
945
|
+
* user is asked for real when the call itself arrives.
|
|
946
|
+
*/
|
|
947
|
+
async mcpApprovalPreflight(call: CursorMcpCall) {
|
|
948
|
+
const toolName = call.toolName || call.name;
|
|
949
|
+
const tool = this.options.getExecutableTool?.(toolName) ?? this.options.tools.get(toolName);
|
|
950
|
+
if (!tool) return false;
|
|
951
|
+
const context = this.options.getToolContext?.();
|
|
952
|
+
const settings = context?.settings;
|
|
953
|
+
const approvalMode: ApprovalMode =
|
|
954
|
+
context?.autoApprove === true ? "yolo" : (settings?.get("tools.approvalMode") ?? "yolo");
|
|
955
|
+
const args = Object.keys(call.args ?? {}).length > 0 ? call.args : decodeMcpArgs(call.rawArgs ?? {});
|
|
956
|
+
const approval = resolveApproval(
|
|
957
|
+
tool,
|
|
958
|
+
args,
|
|
959
|
+
approvalMode,
|
|
960
|
+
(settings?.get("tools.approval") ?? {}) as Record<string, unknown>,
|
|
961
|
+
);
|
|
962
|
+
return approval.policy === "allow";
|
|
963
|
+
}
|
|
513
964
|
}
|