@sjawhar/claude-envoy 0.0.0 → 1.1.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/.claude-plugin/plugin.json +6 -0
- package/.mcp.json +11 -0
- package/.omp-plugin/plugin.json +6 -0
- package/AGENTS.md +343 -0
- package/README.md +639 -0
- package/bin/dispatch +6 -0
- package/bin/envoy-channel.ts +9 -0
- package/bin/envoy-send.ts +33 -0
- package/dist/THIRD_PARTY_NOTICES +816 -0
- package/dist/dispatch.js +27707 -0
- package/dist/envoy-channel.js +57695 -0
- package/dist/session-hook.js +26328 -0
- package/dist/session-title-hook.js +116 -0
- package/hooks/hooks.json +60 -0
- package/hooks/session-hook.ts +512 -0
- package/hooks/session-title-hook.ts +46 -0
- package/legion/.claude-plugin/plugin.json +4 -0
- package/legion/agents/deep-worker.md +68 -0
- package/legion/agents/oracle.md +35 -0
- package/legion/agents/plan-gap-analyst.md +65 -0
- package/legion/agents/plan-reviewer.md +104 -0
- package/legion/agents/thermonuclear-code-quality.md +30 -0
- package/legion/agents/thermonuclear-deep-review.md +30 -0
- package/legion/hooks/hooks.json +89 -0
- package/legion/skills/ce-simplify-code/LICENSE +21 -0
- package/legion/skills/ce-simplify-code/SKILL.md +66 -0
- package/legion/skills/ce-simplify-code/references/personas/code-quality-reviewer.md +17 -0
- package/legion/skills/ce-simplify-code/references/personas/code-reuse-reviewer.md +7 -0
- package/legion/skills/ce-simplify-code/references/personas/efficiency-reviewer.md +11 -0
- package/legion/skills/leading-a-work-stream/SKILL.md +96 -0
- package/legion/skills/leading-a-work-stream/references/daily-plan.md +8 -0
- package/legion/skills/leading-a-work-stream/references/decision-rights.md +6 -0
- package/legion/skills/leading-a-work-stream/references/direction-and-coaching.md +13 -0
- package/legion/skills/leading-a-work-stream/references/dispatch-upkeep.md +8 -0
- package/legion/skills/leading-a-work-stream/references/sources.md +9 -0
- package/legion/skills/leading-a-work-stream/references/speed-tick.md +44 -0
- package/legion/skills/leading-a-work-stream/references/user-quotes.md +7 -0
- package/legion/skills/legion-architect/SKILL.md +86 -0
- package/legion/skills/legion-architect/references/acceptance.md +208 -0
- package/legion/skills/legion-architect/references/decomposition.md +129 -0
- package/legion/skills/legion-architect/references/design-gate.md +206 -0
- package/legion/skills/legion-architect/references/merge-notices.md +91 -0
- package/legion/skills/legion-architect/references/wake-routing.md +41 -0
- package/legion/skills/legion-controller/SKILL.md +151 -0
- package/legion/skills/legion-controller/references/admission.md +22 -0
- package/legion/skills/legion-controller/references/backlog-stewardship.md +101 -0
- package/legion/skills/legion-controller/references/daily-report.md +75 -0
- package/legion/skills/legion-controller/references/escalation.md +82 -0
- package/legion/skills/legion-controller/references/finding-asks.md +181 -0
- package/legion/skills/legion-controller/references/parked-decisions.md +198 -0
- package/legion/skills/legion-controller/references/session.md +100 -0
- package/legion/skills/legion-controller/references/slack-mentions.md +154 -0
- package/legion/skills/legion-controller/references/triage.md +96 -0
- package/legion/skills/legion-controller/references/wakes.md +132 -0
- package/legion/skills/legion-oracle/SKILL.md +48 -0
- package/legion/skills/legion-retro/SKILL.md +112 -0
- package/legion/skills/legion-retro/references/learnings.md +60 -0
- package/legion/skills/legion-retro/references/pr-body-content.md +56 -0
- package/legion/skills/legion-worker/SKILL.md +69 -0
- package/legion/skills/legion-worker/references/cleanup-deletion.md +22 -0
- package/legion/skills/legion-worker/references/conflicts-and-rewrites.md +164 -0
- package/legion/skills/legion-worker/references/default-branch-check.md +53 -0
- package/legion/skills/legion-worker/references/findings.md +199 -0
- package/legion/skills/legion-worker/references/merge-gate.md +353 -0
- package/legion/skills/legion-worker/references/pr-body.md +218 -0
- package/legion/skills/legion-worker/references/review-threads.md +77 -0
- package/legion/skills/legion-worker/references/systematic-rename.md +19 -0
- package/legion/skills/legion-worker/references/tools.md +708 -0
- package/legion/skills/thermonuclear-code-quality/LICENSE +21 -0
- package/legion/skills/thermonuclear-code-quality/SKILL.md +60 -0
- package/legion/skills/thermonuclear-deep-review/LICENSE +21 -0
- package/legion/skills/thermonuclear-deep-review/SKILL.md +80 -0
- package/package.json +49 -1
- package/scripts/build-inputs.txt +84 -0
- package/scripts/build.ts +415 -0
- package/scripts/check-packed-skills.sh +50 -0
- package/scripts/pack-skills.sh +146 -0
- package/scripts/smoke-channel.sh +178 -0
- package/scripts/smoke-clear-rebind.sh +164 -0
- package/scripts/smoke-session-title.sh +245 -0
- package/scripts/verify-build.sh +88 -0
- package/skills/agent-secrets/SKILL.md +180 -0
- package/skills/asd-ste100/LICENSE +21 -0
- package/skills/asd-ste100/SKILL.md +140 -0
- package/skills/asd-ste100/examples/before-after.md +92 -0
- package/skills/asd-ste100/references/writing-rules.md +59 -0
- package/skills/dispatch/SKILL.md +89 -0
- package/skills/dispatch-first/SKILL.md +46 -0
- package/skills/dispatch-writing-specs/SKILL.md +80 -0
- package/skills/envoy/SKILL.md +82 -0
- package/skills/envoy/references/check-settlement.md +62 -0
- package/skills/envoy/references/deliveries.md +67 -0
- package/skills/envoy/references/legion.md +14 -0
- package/skills/envoy/references/roles.md +12 -0
- package/skills/envoy/references/sources.md +41 -0
- package/skills/outside-message/SKILL.md +111 -0
- package/skills/plain-words/SKILL.md +88 -0
- package/src/channel-forwarder.ts +262 -0
- package/src/claude-channel.ts +705 -0
- package/src/claude-session.ts +38 -0
- package/src/envoy-channel-server.ts +2115 -0
- package/src/envoy-client.ts +26 -0
- package/src/envoy-off.ts +62 -0
- package/src/legion-claim.ts +211 -0
- package/src/legion-inbox.ts +110 -0
- package/src/legion-phase-hook.ts +554 -0
- package/src/legion-tool.ts +69 -0
- package/src/replay-state.ts +97 -0
- package/src/send-arguments.ts +13 -0
- package/src/session-identity.ts +165 -0
- package/src/session-state.ts +142 -0
- package/src/session-title.ts +251 -0
- package/src/subscription-topics.ts +16 -0
|
@@ -0,0 +1,705 @@
|
|
|
1
|
+
import { spawnSync } from "node:child_process"
|
|
2
|
+
import { readdirSync, readFileSync, readlinkSync } from "node:fs"
|
|
3
|
+
import { homedir } from "node:os"
|
|
4
|
+
import { basename, join, resolve, sep } from "node:path"
|
|
5
|
+
import { z } from "zod"
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Whether Claude Code passes this server's channel notifications to the model. It says nothing
|
|
9
|
+
* about that to the server: the MCP handshake is the same with the channel on or off, and a
|
|
10
|
+
* notification sent while it is off is dropped without a word. So the server reads the `claude`
|
|
11
|
+
* process's own command line and applies the gate Claude Code applies to it (as of 2.1.301): the
|
|
12
|
+
* entry must be named by `--channels` or, in an interactive session,
|
|
13
|
+
* `--dangerously-load-development-channels`; a plugin entry must name the marketplace the plugin
|
|
14
|
+
* is installed from; and a `--channels` entry must be allowlisted, which for this plugin means
|
|
15
|
+
* `allowedChannelPlugins` in the managed settings Claude Code applies, since it is not on
|
|
16
|
+
* Anthropic's own list. Claude Code's account and provider checks are not repeated here, and
|
|
17
|
+
* neither is a channel an SDK host turns on after startup (`enableChannel`): the check runs once,
|
|
18
|
+
* so such a session advertises no mode and Dispatch refuses messages Claude Code would pass on.
|
|
19
|
+
*/
|
|
20
|
+
export type ChannelGate =
|
|
21
|
+
| { readonly loaded: true }
|
|
22
|
+
| { readonly loaded: false; readonly why: string }
|
|
23
|
+
|
|
24
|
+
/** One `plugin:<name>@<marketplace>` or `server:<name>` entry of either channel flag. */
|
|
25
|
+
export interface ChannelEntry {
|
|
26
|
+
readonly kind: "plugin" | "server"
|
|
27
|
+
readonly name: string
|
|
28
|
+
readonly marketplace?: string
|
|
29
|
+
readonly dev: boolean
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export interface AllowedChannelPlugin {
|
|
33
|
+
readonly marketplace: string
|
|
34
|
+
readonly plugin: string
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** The name `.mcp.json` gives this plugin's server. */
|
|
38
|
+
const PLUGIN_SERVER = "envoy"
|
|
39
|
+
|
|
40
|
+
/** The flags that name channels, and whether each is the development one. */
|
|
41
|
+
const CHANNEL_FLAGS: Readonly<Record<string, boolean>> = {
|
|
42
|
+
"--channels": false,
|
|
43
|
+
"--dangerously-load-development-channels": true,
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function parseEntry(value: string, dev: boolean): ChannelEntry | undefined {
|
|
47
|
+
if (value.startsWith("plugin:")) {
|
|
48
|
+
const spec = value.slice("plugin:".length)
|
|
49
|
+
const at = spec.indexOf("@")
|
|
50
|
+
if (at <= 0 || at === spec.length - 1) return undefined
|
|
51
|
+
return { kind: "plugin", name: spec.slice(0, at), marketplace: spec.slice(at + 1), dev }
|
|
52
|
+
}
|
|
53
|
+
if (value.startsWith("server:") && value.length > "server:".length) {
|
|
54
|
+
return { kind: "server", name: value.slice("server:".length), dev }
|
|
55
|
+
}
|
|
56
|
+
return undefined
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// Claude Code 2.1.301's own argument scanner: the options that take one value, the ones whose value
|
|
60
|
+
// is optional (taken when the next argument is not an option), and the ones that take every value
|
|
61
|
+
// up to the next option. Its print-mode test and its channel flags skip those values, and `--`
|
|
62
|
+
// ends the options.
|
|
63
|
+
const VALUE_OPTIONS: ReadonlySet<string> = new Set([
|
|
64
|
+
"--prefill",
|
|
65
|
+
"--prefill-b64",
|
|
66
|
+
"--deep-link-repo",
|
|
67
|
+
"--deep-link-last-fetch",
|
|
68
|
+
"--deep-link-cwd-b64",
|
|
69
|
+
"--handle-uri",
|
|
70
|
+
"--settings",
|
|
71
|
+
"--managed-settings",
|
|
72
|
+
"--setting-sources",
|
|
73
|
+
"--client-data-url",
|
|
74
|
+
"--watch-artifact",
|
|
75
|
+
"--watch-artifact-no-autoreact",
|
|
76
|
+
"--team-name",
|
|
77
|
+
"--agent-id",
|
|
78
|
+
"--agent-name",
|
|
79
|
+
"--agent-color",
|
|
80
|
+
"--parent-session-id",
|
|
81
|
+
"--agent-type",
|
|
82
|
+
"--model",
|
|
83
|
+
"--agent",
|
|
84
|
+
"--routine",
|
|
85
|
+
"--effort",
|
|
86
|
+
"--fast-mode",
|
|
87
|
+
"--permission-mode",
|
|
88
|
+
"--inherit-permission-mode",
|
|
89
|
+
"--proactivity",
|
|
90
|
+
"--debug-file",
|
|
91
|
+
"--system-prompt",
|
|
92
|
+
"--system-prompt-file",
|
|
93
|
+
"--append-system-prompt",
|
|
94
|
+
"--append-system-prompt-file",
|
|
95
|
+
"--system-prompt-snapshot",
|
|
96
|
+
"--append-subagent-system-prompt",
|
|
97
|
+
"--append-subagent-system-prompt-file",
|
|
98
|
+
"--plan-mode-instructions",
|
|
99
|
+
"--permission-prompt-tool",
|
|
100
|
+
"--permission-prompts",
|
|
101
|
+
"--json-schema",
|
|
102
|
+
"--fallback-model",
|
|
103
|
+
"--advisor",
|
|
104
|
+
"--agents",
|
|
105
|
+
"--name",
|
|
106
|
+
"-n",
|
|
107
|
+
"--allowedTools",
|
|
108
|
+
"--allowed-tools",
|
|
109
|
+
"--disallowedTools",
|
|
110
|
+
"--disallowed-tools",
|
|
111
|
+
"--tools",
|
|
112
|
+
"--add-dir",
|
|
113
|
+
"--mcp-config",
|
|
114
|
+
"--betas",
|
|
115
|
+
"--file",
|
|
116
|
+
"--channels",
|
|
117
|
+
"--dangerously-load-development-channels",
|
|
118
|
+
"--plugin-dir",
|
|
119
|
+
"--plugin-dir-no-mcp",
|
|
120
|
+
"--plugin-url",
|
|
121
|
+
"--remote-control-session-name-prefix",
|
|
122
|
+
"--exec",
|
|
123
|
+
"-m",
|
|
124
|
+
"--sdk-url",
|
|
125
|
+
"--thinking",
|
|
126
|
+
"--thinking-display",
|
|
127
|
+
"--max-thinking-tokens",
|
|
128
|
+
"--max-turns",
|
|
129
|
+
"--max-budget-usd",
|
|
130
|
+
"--task-budget",
|
|
131
|
+
"--autocompact",
|
|
132
|
+
"--rewind-files",
|
|
133
|
+
"--resume-session-at",
|
|
134
|
+
"--resume-drops-turn",
|
|
135
|
+
"--workload",
|
|
136
|
+
"--output-format",
|
|
137
|
+
"--input-format",
|
|
138
|
+
"--teammate-mode",
|
|
139
|
+
"--messaging-socket-path",
|
|
140
|
+
"--session-id",
|
|
141
|
+
"--environment",
|
|
142
|
+
"--pool",
|
|
143
|
+
"--ref",
|
|
144
|
+
"--on-branch",
|
|
145
|
+
"--correlation-id",
|
|
146
|
+
"--forward-home-settings",
|
|
147
|
+
"--project-config-root",
|
|
148
|
+
"--attach-serve",
|
|
149
|
+
])
|
|
150
|
+
const OPTIONAL_VALUE_OPTIONS: ReadonlySet<string> = new Set([
|
|
151
|
+
"-d",
|
|
152
|
+
"--debug",
|
|
153
|
+
"-r",
|
|
154
|
+
"--resume",
|
|
155
|
+
"--from-pr",
|
|
156
|
+
"-w",
|
|
157
|
+
"--worktree",
|
|
158
|
+
"--teleport",
|
|
159
|
+
"--cloud",
|
|
160
|
+
"--remote",
|
|
161
|
+
"--project",
|
|
162
|
+
"--remote-control",
|
|
163
|
+
"--rc",
|
|
164
|
+
"--prompt-suggestions",
|
|
165
|
+
])
|
|
166
|
+
const MULTI_VALUE_OPTIONS: ReadonlySet<string> = new Set([
|
|
167
|
+
"--allowedTools",
|
|
168
|
+
"--allowed-tools",
|
|
169
|
+
"--disallowedTools",
|
|
170
|
+
"--disallowed-tools",
|
|
171
|
+
"--tools",
|
|
172
|
+
"--add-dir",
|
|
173
|
+
"--mcp-config",
|
|
174
|
+
"--betas",
|
|
175
|
+
"--file",
|
|
176
|
+
"--channels",
|
|
177
|
+
"--dangerously-load-development-channels",
|
|
178
|
+
])
|
|
179
|
+
|
|
180
|
+
function isOption(argument: string): boolean {
|
|
181
|
+
return argument.length > 1 && argument.startsWith("-")
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/** The index of the last argument `argv[index]` consumes as its values, as Claude Code skips them. */
|
|
185
|
+
function lastValueIndex(argv: readonly string[], index: number): number {
|
|
186
|
+
const argument = argv[index] ?? ""
|
|
187
|
+
if (OPTIONAL_VALUE_OPTIONS.has(argument)) {
|
|
188
|
+
const next = argv[index + 1]
|
|
189
|
+
return next !== undefined && !isOption(next) ? index + 1 : index
|
|
190
|
+
}
|
|
191
|
+
if (!VALUE_OPTIONS.has(argument)) return index
|
|
192
|
+
let last = index + 1
|
|
193
|
+
if (MULTI_VALUE_OPTIONS.has(argument)) {
|
|
194
|
+
while (last + 1 < argv.length && !isOption(argv[last + 1] ?? "")) last++
|
|
195
|
+
}
|
|
196
|
+
return last
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/**
|
|
200
|
+
* Whether a command line asks for a non-interactive session, as Claude Code reads it: `-p`,
|
|
201
|
+
* `--print`, `--init-only` or `--sdk-url` as an option, not as another option's value, before `--`.
|
|
202
|
+
*/
|
|
203
|
+
export function asksForPrintMode(argv: readonly string[]): boolean {
|
|
204
|
+
for (let index = 1; index < argv.length; index++) {
|
|
205
|
+
const argument = argv[index] ?? ""
|
|
206
|
+
if (argument === "--") return false
|
|
207
|
+
if (
|
|
208
|
+
argument === "-p" ||
|
|
209
|
+
argument === "--print" ||
|
|
210
|
+
argument === "--init-only" ||
|
|
211
|
+
argument.startsWith("--sdk-url")
|
|
212
|
+
) {
|
|
213
|
+
return true
|
|
214
|
+
}
|
|
215
|
+
index = lastValueIndex(argv, index)
|
|
216
|
+
}
|
|
217
|
+
return false
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
/**
|
|
221
|
+
* The channel entries a `claude` command line names, as Claude Code reads them: each flag, before
|
|
222
|
+
* `--`, takes the values after it up to the next option, or one inline `=` value, and untagged values
|
|
223
|
+
* are dropped; every `--channels` entry comes before every development one, since Claude Code
|
|
224
|
+
* matches the first entry naming the server. Development channels load only in an interactive
|
|
225
|
+
* session, so they are dropped when `interactive` is false. A command line split on whitespace
|
|
226
|
+
* (`ps`, not `exact`) cannot tell an option from a word of a prompt, so none of that reading
|
|
227
|
+
* applies to it: its print mode is never read, `--` ends nothing and no value is skipped, since
|
|
228
|
+
* each could drop a loaded channel and refuse messages the session takes.
|
|
229
|
+
*/
|
|
230
|
+
export function claudeChannelEntries(
|
|
231
|
+
argv: readonly string[],
|
|
232
|
+
exact = true,
|
|
233
|
+
stdoutIsTerminal: boolean | undefined = undefined,
|
|
234
|
+
): ChannelEntry[] {
|
|
235
|
+
const interactive = !exact || (!asksForPrintMode(argv) && stdoutIsTerminal !== false)
|
|
236
|
+
const approved: ChannelEntry[] = []
|
|
237
|
+
const development: ChannelEntry[] = []
|
|
238
|
+
for (let index = 1; index < argv.length; index++) {
|
|
239
|
+
const argument = argv[index] ?? ""
|
|
240
|
+
if (exact && argument === "--") break
|
|
241
|
+
const equals = argument.indexOf("=")
|
|
242
|
+
const flag = equals === -1 ? argument : argument.slice(0, equals)
|
|
243
|
+
const dev = CHANNEL_FLAGS[flag]
|
|
244
|
+
if (dev === undefined) {
|
|
245
|
+
if (exact) index = lastValueIndex(argv, index)
|
|
246
|
+
continue
|
|
247
|
+
}
|
|
248
|
+
const values: string[] = []
|
|
249
|
+
if (equals !== -1) values.push(argument.slice(equals + 1))
|
|
250
|
+
else {
|
|
251
|
+
while (index + 1 < argv.length && !isOption(argv[index + 1] ?? "-")) {
|
|
252
|
+
values.push(argv[index + 1] ?? "")
|
|
253
|
+
index++
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
if (dev && !interactive) continue
|
|
257
|
+
for (const value of values) {
|
|
258
|
+
const entry = parseEntry(value, dev)
|
|
259
|
+
if (entry !== undefined) (dev ? development : approved).push(entry)
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
return [...approved, ...development]
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
/** A process's command line: its arguments, and whether they are exact (`/proc`) rather than
|
|
266
|
+
* `ps` output split on whitespace. */
|
|
267
|
+
export interface CommandLine {
|
|
268
|
+
readonly argv: readonly string[]
|
|
269
|
+
readonly exact: boolean
|
|
270
|
+
/** Whether the process's stdout is a terminal, when that can be read (`/proc`): Claude Code is
|
|
271
|
+
* non-interactive when it is not, and then ignores development channels. */
|
|
272
|
+
readonly stdoutIsTerminal?: boolean
|
|
273
|
+
/** The process's working directory, when it can be read (`/proc`), which a relative
|
|
274
|
+
* `--plugin-dir` value is relative to. */
|
|
275
|
+
readonly cwd?: string
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
/**
|
|
279
|
+
* Whether the `claude` process with `commandLine` loads this server's channel. An undefined
|
|
280
|
+
* command line could not be read, and is taken as loaded: that is what the server advertised
|
|
281
|
+
* before it could tell, and refusing every message on a guess would be worse.
|
|
282
|
+
*/
|
|
283
|
+
export function claudeChannelGate(input: {
|
|
284
|
+
readonly commandLine: CommandLine | undefined
|
|
285
|
+
/** This plugin's name and, when the install path says it, its marketplace; undefined when the
|
|
286
|
+
* server runs from a bare MCP config rather than as a plugin, where only `server:` entries
|
|
287
|
+
* apply (a plugin's server is `plugin:<name>:<server>`, which no `server:` entry names). */
|
|
288
|
+
readonly plugin: { readonly name: string; readonly marketplace: string | undefined } | undefined
|
|
289
|
+
/** The managed settings' `allowedChannelPlugins`, or undefined when they do not set it. */
|
|
290
|
+
readonly allowlist: readonly AllowedChannelPlugin[] | undefined
|
|
291
|
+
}): ChannelGate {
|
|
292
|
+
const { commandLine, plugin, allowlist } = input
|
|
293
|
+
if (commandLine === undefined) return { loaded: true }
|
|
294
|
+
const ours =
|
|
295
|
+
plugin === undefined
|
|
296
|
+
? "server:<name>"
|
|
297
|
+
: `plugin:${plugin.name}@${plugin.marketplace ?? "<marketplace>"}`
|
|
298
|
+
// A bare config's own server name is not known here, so any `server:` entry counts for it.
|
|
299
|
+
const entry = claudeChannelEntries(
|
|
300
|
+
commandLine.argv,
|
|
301
|
+
commandLine.exact,
|
|
302
|
+
commandLine.stdoutIsTerminal,
|
|
303
|
+
).find((candidate) =>
|
|
304
|
+
plugin === undefined
|
|
305
|
+
? candidate.kind === "server"
|
|
306
|
+
: // Claude Code names a plugin's server `plugin:<plugin>:<server>`, which a `server:` entry
|
|
307
|
+
// can name too.
|
|
308
|
+
(candidate.kind === "plugin" && candidate.name === plugin.name) ||
|
|
309
|
+
(candidate.kind === "server" &&
|
|
310
|
+
candidate.name === `plugin:${plugin.name}:${PLUGIN_SERVER}`),
|
|
311
|
+
)
|
|
312
|
+
if (entry === undefined) {
|
|
313
|
+
return {
|
|
314
|
+
loaded: false,
|
|
315
|
+
why: `claude was started without --channels ${ours} (or --dangerously-load-development-channels ${ours})`,
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
if (entry.kind === "server") {
|
|
319
|
+
return entry.dev
|
|
320
|
+
? { loaded: true }
|
|
321
|
+
: {
|
|
322
|
+
loaded: false,
|
|
323
|
+
why: `server:${entry.name} is not on Claude Code's approved channels allowlist (use --dangerously-load-development-channels server:${entry.name})`,
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
const asked = `plugin:${entry.name}@${entry.marketplace}`
|
|
327
|
+
// The install path holds the marketplace as Claude Code names its cache directory, with every
|
|
328
|
+
// character outside [A-Za-z0-9_-] made "-", while the flag holds it as written. So both are
|
|
329
|
+
// compared that way; a flag naming `legion-plugins` for a plugin from `legion.plugins` therefore
|
|
330
|
+
// counts as a match here, which Claude Code refuses, since the path no longer says which it was.
|
|
331
|
+
if (
|
|
332
|
+
plugin?.marketplace !== undefined &&
|
|
333
|
+
cacheDirectoryName(entry.marketplace ?? "") !== cacheDirectoryName(plugin.marketplace)
|
|
334
|
+
) {
|
|
335
|
+
return {
|
|
336
|
+
loaded: false,
|
|
337
|
+
why: `claude asked for ${asked} but this plugin is installed from ${plugin.marketplace}`,
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
if (entry.dev) return { loaded: true }
|
|
341
|
+
if (allowlist === undefined) {
|
|
342
|
+
return {
|
|
343
|
+
loaded: false,
|
|
344
|
+
why: `${asked} is not on Claude Code's approved channels allowlist (add it to allowedChannelPlugins in ${MANAGED_SETTINGS_HINT}; for one session use --dangerously-load-development-channels ${asked})`,
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
return allowlist.some(
|
|
348
|
+
(allowed) => allowed.marketplace === entry.marketplace && allowed.plugin === entry.name,
|
|
349
|
+
)
|
|
350
|
+
? { loaded: true }
|
|
351
|
+
: {
|
|
352
|
+
loaded: false,
|
|
353
|
+
why: `${asked} is not on Claude Code's approved channels allowlist (allowedChannelPlugins in ${MANAGED_SETTINGS_HINT})`,
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
/** Where an allowlist goes: the machine's managed-settings.json, unless the account receives
|
|
358
|
+
* server-managed settings, which then own `allowedChannelPlugins` (see `channelAllowlist`). */
|
|
359
|
+
const MANAGED_SETTINGS_HINT = `${join(managedSettingsDirectory(), "managed-settings.json")}, or in the server-managed settings when the account receives them`
|
|
360
|
+
|
|
361
|
+
/** A marketplace name as Claude Code writes it into its plugin cache path. */
|
|
362
|
+
function cacheDirectoryName(marketplace: string): string {
|
|
363
|
+
return marketplace.replace(/[^a-zA-Z0-9\-_]/g, "-")
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
/** The executable process `pid` runs, or undefined when it cannot be read. */
|
|
367
|
+
export function processExecutable(pid: number): string | undefined {
|
|
368
|
+
if (process.platform === "linux") {
|
|
369
|
+
try {
|
|
370
|
+
return readlinkSync(`/proc/${pid}/exe`)
|
|
371
|
+
} catch {
|
|
372
|
+
return undefined
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
if (process.platform === "win32") return undefined
|
|
376
|
+
const result = spawnSync("lsof", ["-a", "-p", String(pid), "-d", "txt", "-Fn"], {
|
|
377
|
+
encoding: "utf8",
|
|
378
|
+
timeout: 2_000,
|
|
379
|
+
})
|
|
380
|
+
const first = (result.stdout ?? "").split("\n").find((line) => line.startsWith("n"))
|
|
381
|
+
return first === undefined ? undefined : first.slice(1)
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
/** Whether process `pid`'s stdout is a terminal, from `/proc` on Linux; undefined elsewhere or
|
|
385
|
+
* when it cannot be read. */
|
|
386
|
+
function stdoutIsTerminal(pid: number): boolean | undefined {
|
|
387
|
+
if (process.platform !== "linux") return undefined
|
|
388
|
+
try {
|
|
389
|
+
const target = readlinkSync(`/proc/${pid}/fd/1`)
|
|
390
|
+
return target.startsWith("/dev/pts/") || /^\/dev\/tty[^S]/.test(target) || target === "/dev/tty"
|
|
391
|
+
} catch {
|
|
392
|
+
return undefined
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
/** Process `pid`'s working directory, from `/proc`; undefined when it cannot be read. */
|
|
397
|
+
function workingDirectory(pid: number): string | undefined {
|
|
398
|
+
try {
|
|
399
|
+
return readlinkSync(`/proc/${pid}/cwd`)
|
|
400
|
+
} catch {
|
|
401
|
+
return undefined
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
/**
|
|
406
|
+
* The command line of process `pid`, or undefined when it cannot be read: `/proc` on Linux
|
|
407
|
+
* (exact, with whether its stdout is a terminal), `ps` elsewhere (split on whitespace, which no
|
|
408
|
+
* channel entry contains), and nothing on Windows. Claude Code sets `process.title` to `claude`,
|
|
409
|
+
* which under Node overwrites the arguments `ps` and `/proc` show, so such a command line cannot
|
|
410
|
+
* be read either: on Linux it is the title followed by NUL padding over the old arguments, and
|
|
411
|
+
* elsewhere one word from a program whose name starts with `node` (`node-22`, `nodejs`). A native
|
|
412
|
+
* (Bun) build keeps its arguments, and its one-word command line means `claude` alone.
|
|
413
|
+
*/
|
|
414
|
+
export function processCommandLine(
|
|
415
|
+
pid: number,
|
|
416
|
+
executable: (pid: number) => string | undefined = processExecutable,
|
|
417
|
+
): CommandLine | undefined {
|
|
418
|
+
let commandLine: CommandLine | undefined
|
|
419
|
+
if (process.platform === "linux") {
|
|
420
|
+
try {
|
|
421
|
+
// Each argument ends in one NUL. A process that set its title (Node, for `process.title`)
|
|
422
|
+
// leaves the title alone, which holds no NUL, then NUL padding over its old arguments, so
|
|
423
|
+
// one piece before several empty ones is a title and its arguments are gone. Otherwise the
|
|
424
|
+
// empty pieces past the final NUL are blank arguments (`--name ""`), which keep their place,
|
|
425
|
+
// since dropping one would shift which word is a value.
|
|
426
|
+
const pieces = readFileSync(`/proc/${pid}/cmdline`, "utf8").split("\0")
|
|
427
|
+
let trailing = 0
|
|
428
|
+
while (pieces.length > 0 && pieces.at(-1) === "") {
|
|
429
|
+
pieces.pop()
|
|
430
|
+
trailing++
|
|
431
|
+
}
|
|
432
|
+
if (trailing > 1 && pieces.length === 1) return undefined
|
|
433
|
+
for (let blank = 1; blank < trailing; blank++) pieces.push("")
|
|
434
|
+
const argv = pieces
|
|
435
|
+
const terminal = stdoutIsTerminal(pid)
|
|
436
|
+
const cwd = workingDirectory(pid)
|
|
437
|
+
commandLine =
|
|
438
|
+
argv.length === 0
|
|
439
|
+
? undefined
|
|
440
|
+
: {
|
|
441
|
+
argv,
|
|
442
|
+
exact: true,
|
|
443
|
+
...(terminal === undefined ? {} : { stdoutIsTerminal: terminal }),
|
|
444
|
+
...(cwd === undefined ? {} : { cwd }),
|
|
445
|
+
}
|
|
446
|
+
} catch {
|
|
447
|
+
commandLine = undefined
|
|
448
|
+
}
|
|
449
|
+
} else if (process.platform !== "win32") {
|
|
450
|
+
const result = spawnSync("ps", ["-ww", "-o", "args=", "-p", String(pid)], {
|
|
451
|
+
encoding: "utf8",
|
|
452
|
+
timeout: 1_000,
|
|
453
|
+
})
|
|
454
|
+
const line = result.status === 0 ? result.stdout.trim() : ""
|
|
455
|
+
commandLine = line === "" ? undefined : { argv: line.split(/\s+/), exact: false }
|
|
456
|
+
}
|
|
457
|
+
if (commandLine === undefined || commandLine.argv.length > 1) return commandLine
|
|
458
|
+
return basename(executable(pid) ?? "").startsWith("node") ? undefined : commandLine
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
/** The parent of process `pid`, or undefined when it cannot be read. */
|
|
462
|
+
export function parentProcess(pid: number): number | undefined {
|
|
463
|
+
try {
|
|
464
|
+
if (process.platform === "linux") {
|
|
465
|
+
// The command name, in parentheses, may hold spaces; the parent is the second field after it.
|
|
466
|
+
const stat = readFileSync(`/proc/${pid}/stat`, "utf8")
|
|
467
|
+
const parent = Number(stat.slice(stat.lastIndexOf(")") + 2).split(" ")[1])
|
|
468
|
+
return Number.isInteger(parent) && parent > 0 ? parent : undefined
|
|
469
|
+
}
|
|
470
|
+
if (process.platform === "win32") return undefined
|
|
471
|
+
const result = spawnSync("ps", ["-o", "ppid=", "-p", String(pid)], {
|
|
472
|
+
encoding: "utf8",
|
|
473
|
+
timeout: 1_000,
|
|
474
|
+
})
|
|
475
|
+
const parent = Number((result.stdout ?? "").trim())
|
|
476
|
+
return result.status === 0 && Number.isInteger(parent) && parent > 0 ? parent : undefined
|
|
477
|
+
} catch {
|
|
478
|
+
return undefined
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
/** How many processes up from the server `claudeCommandLine` looks for Claude Code. */
|
|
483
|
+
const MAX_CLAUDE_DEPTH = 8
|
|
484
|
+
|
|
485
|
+
/**
|
|
486
|
+
* The command line of the Claude Code process that runs this server: the nearest of `pid` and its
|
|
487
|
+
* ancestors whose program is `claude`, since Claude Code may run the server through a shell prefix
|
|
488
|
+
* (`CLAUDE_CODE_SHELL_PREFIX`) or a version manager's `bun` shim. Undefined, read as "cannot tell",
|
|
489
|
+
* when no such process is found or a command line cannot be read.
|
|
490
|
+
*/
|
|
491
|
+
export function claudeCommandLine(
|
|
492
|
+
pid: number,
|
|
493
|
+
read: (pid: number) => CommandLine | undefined = processCommandLine,
|
|
494
|
+
parent: (pid: number) => number | undefined = parentProcess,
|
|
495
|
+
): CommandLine | undefined {
|
|
496
|
+
let current: number | undefined = pid
|
|
497
|
+
for (let depth = 0; current !== undefined && current > 1 && depth < MAX_CLAUDE_DEPTH; depth++) {
|
|
498
|
+
const commandLine = read(current)
|
|
499
|
+
if (commandLine === undefined) return undefined
|
|
500
|
+
if (basename(commandLine.argv[0] ?? "") === "claude") return commandLine
|
|
501
|
+
current = parent(current)
|
|
502
|
+
}
|
|
503
|
+
return undefined
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
/** The segments after `…/plugins/cache/` in a plugin install path, or undefined for any other path. */
|
|
507
|
+
function pluginCacheSegments(bundlePath: string): string[] | undefined {
|
|
508
|
+
const parts = bundlePath.split(sep)
|
|
509
|
+
for (let index = 0; index + 1 < parts.length; index++) {
|
|
510
|
+
if (parts[index] === "plugins" && parts[index + 1] === "cache") return parts.slice(index + 2)
|
|
511
|
+
}
|
|
512
|
+
return undefined
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
/**
|
|
516
|
+
* The marketplace a plugin install path names (`…/plugins/cache/<marketplace>/<plugin>/…`), or
|
|
517
|
+
* undefined for any other path: a source checkout, `--plugin-dir`, or a bare server config.
|
|
518
|
+
*/
|
|
519
|
+
export function installedMarketplace(bundlePath: string): string | undefined {
|
|
520
|
+
return pluginCacheSegments(bundlePath)?.[0]
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
/**
|
|
524
|
+
* The version a plugin install path names (`…/plugins/cache/<marketplace>/<plugin>/<version>/`,
|
|
525
|
+
* the published npm version for this plugin, whose marketplace entry is an `npm` source), or
|
|
526
|
+
* undefined for any other path.
|
|
527
|
+
*/
|
|
528
|
+
export function installedPluginVersion(bundlePath: string): string | undefined {
|
|
529
|
+
return pluginCacheSegments(bundlePath)?.[2]
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
/** The marketplace Claude Code names a plugin it loads with `--plugin-dir` under. */
|
|
533
|
+
export const INLINE_MARKETPLACE = "inline"
|
|
534
|
+
|
|
535
|
+
/**
|
|
536
|
+
* The marketplace this plugin, rooted at `pluginRoot`, is loaded from: `inline` when a
|
|
537
|
+
* `--plugin-dir` value on the `claude` command line names that directory, the plugin cache's
|
|
538
|
+
* marketplace when `bundlePath` lies in it, and undefined otherwise (a directory-source
|
|
539
|
+
* marketplace, say), which the gate does not check.
|
|
540
|
+
*/
|
|
541
|
+
export function loadedMarketplace(
|
|
542
|
+
pluginRoot: string,
|
|
543
|
+
bundlePath: string,
|
|
544
|
+
commandLine: { readonly argv: readonly string[]; readonly cwd: string } | undefined,
|
|
545
|
+
): string | undefined {
|
|
546
|
+
if (commandLine !== undefined) {
|
|
547
|
+
const { argv, cwd } = commandLine
|
|
548
|
+
const root = resolve(pluginRoot)
|
|
549
|
+
for (let index = 1; index < argv.length; index++) {
|
|
550
|
+
const argument = argv[index] ?? ""
|
|
551
|
+
if (argument === "--") break
|
|
552
|
+
const equals = argument.indexOf("=")
|
|
553
|
+
const flag = equals === -1 ? argument : argument.slice(0, equals)
|
|
554
|
+
if (flag === "--plugin-dir" || flag === "--plugin-dir-no-mcp") {
|
|
555
|
+
const value = equals === -1 ? argv[index + 1] : argument.slice(equals + 1)
|
|
556
|
+
if (value !== undefined && resolve(cwd, value) === root) return INLINE_MARKETPLACE
|
|
557
|
+
}
|
|
558
|
+
if (equals === -1) index = lastValueIndex(argv, index)
|
|
559
|
+
}
|
|
560
|
+
}
|
|
561
|
+
return installedMarketplace(bundlePath)
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
/** One `allowedChannelPlugins` entry as Claude Code accepts it: the documented object, or a
|
|
565
|
+
* `plugin@marketplace` string, which it also accepts with a warning. */
|
|
566
|
+
const AllowedChannelEntry = z.union([
|
|
567
|
+
z.object({ marketplace: z.string(), plugin: z.string() }),
|
|
568
|
+
z
|
|
569
|
+
.string()
|
|
570
|
+
.regex(/^[^@]+@[^@]+$/)
|
|
571
|
+
.transform((value) => {
|
|
572
|
+
const at = value.indexOf("@")
|
|
573
|
+
return { plugin: value.slice(0, at), marketplace: value.slice(at + 1) }
|
|
574
|
+
}),
|
|
575
|
+
])
|
|
576
|
+
|
|
577
|
+
/** An `allowedChannelPlugins` value's entries, each invalid one dropped on its own as Claude Code
|
|
578
|
+
* drops it; undefined when the value is not a list. */
|
|
579
|
+
function allowedEntries(value: unknown): readonly AllowedChannelPlugin[] | undefined {
|
|
580
|
+
if (!Array.isArray(value)) return undefined
|
|
581
|
+
return value.flatMap((entry: unknown) => {
|
|
582
|
+
const parsed = AllowedChannelEntry.safeParse(entry)
|
|
583
|
+
return parsed.success ? [parsed.data] : []
|
|
584
|
+
})
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
/** Claude Code's managed settings directory on this platform. */
|
|
588
|
+
export function managedSettingsDirectory(): string {
|
|
589
|
+
switch (process.platform) {
|
|
590
|
+
case "darwin":
|
|
591
|
+
return "/Library/Application Support/ClaudeCode"
|
|
592
|
+
case "win32":
|
|
593
|
+
return "C:\\Program Files\\ClaudeCode"
|
|
594
|
+
default:
|
|
595
|
+
return "/etc/claude-code"
|
|
596
|
+
}
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
interface ChannelSettings {
|
|
600
|
+
/** The file's `allowedChannelPlugins`, or undefined when it does not set one. */
|
|
601
|
+
readonly list: readonly AllowedChannelPlugin[] | undefined
|
|
602
|
+
/** The file's `managedSourcesBehavior`, when it sets one. */
|
|
603
|
+
readonly behavior: string | undefined
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
/** Keys that say how sources compose, which make no settings file a source on their own. */
|
|
607
|
+
const COMPOSITION_KEYS: ReadonlySet<string> = new Set([
|
|
608
|
+
"managedSourcesBehavior",
|
|
609
|
+
"wslInheritsWindowsSettings",
|
|
610
|
+
])
|
|
611
|
+
|
|
612
|
+
/** A settings file's channel settings, or undefined when the file is missing or is not a JSON
|
|
613
|
+
* object, and (with `asSource`) when it holds nothing but `COMPOSITION_KEYS`, as Claude Code does
|
|
614
|
+
* not count such a file as a source. */
|
|
615
|
+
function readChannelSettings(file: string, asSource = false): ChannelSettings | undefined {
|
|
616
|
+
try {
|
|
617
|
+
const value: unknown = JSON.parse(readFileSync(file, "utf8"))
|
|
618
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) return undefined
|
|
619
|
+
if (asSource && Object.keys(value).every((key) => COMPOSITION_KEYS.has(key))) return undefined
|
|
620
|
+
const record = value as Record<string, unknown>
|
|
621
|
+
const behavior = record["managedSourcesBehavior"]
|
|
622
|
+
return {
|
|
623
|
+
list: allowedEntries(record["allowedChannelPlugins"]),
|
|
624
|
+
behavior: typeof behavior === "string" ? behavior : undefined,
|
|
625
|
+
}
|
|
626
|
+
} catch {
|
|
627
|
+
return undefined
|
|
628
|
+
}
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
/**
|
|
632
|
+
* The `allowedChannelPlugins` of the managed settings Claude Code applies, read from files.
|
|
633
|
+
* Server-managed settings (cached as `remote-settings.json` in the config directory) outrank an
|
|
634
|
+
* MDM profile or the Windows registry, which outrank `managed-settings.json` with its
|
|
635
|
+
* `managed-settings.d` drop-ins (`.json` files whose names do not start with a dot). Under
|
|
636
|
+
* `managedSourcesBehavior` "first-wins", the default, the highest source present is the tier
|
|
637
|
+
* alone; under "merge", the highest source that sets the key owns it. Undefined when that source
|
|
638
|
+
* does not set the key, or none is present, which leaves Claude Code's own list. MDM and registry
|
|
639
|
+
* settings are not read; `ENVOY_CHANNEL_LOADED=1` covers an allowlist only they deliver.
|
|
640
|
+
*/
|
|
641
|
+
export function channelAllowlist(directories: {
|
|
642
|
+
readonly managedDirectory: string
|
|
643
|
+
readonly configDirectory: string
|
|
644
|
+
}): readonly AllowedChannelPlugin[] | undefined {
|
|
645
|
+
const remote = readChannelSettings(
|
|
646
|
+
join(directories.configDirectory, "remote-settings.json"),
|
|
647
|
+
true,
|
|
648
|
+
)
|
|
649
|
+
const dropIns = join(directories.managedDirectory, "managed-settings.d")
|
|
650
|
+
let dropInFiles: string[] = []
|
|
651
|
+
try {
|
|
652
|
+
dropInFiles = readdirSync(dropIns)
|
|
653
|
+
.filter((name) => name.endsWith(".json") && !name.startsWith("."))
|
|
654
|
+
.sort()
|
|
655
|
+
.map((name) => join(dropIns, name))
|
|
656
|
+
} catch {
|
|
657
|
+
dropInFiles = []
|
|
658
|
+
}
|
|
659
|
+
const files = [join(directories.managedDirectory, "managed-settings.json"), ...dropInFiles]
|
|
660
|
+
.map((file) => readChannelSettings(file))
|
|
661
|
+
.filter((settings) => settings !== undefined)
|
|
662
|
+
const lists = files.map((settings) => settings.list).filter((list) => list !== undefined)
|
|
663
|
+
const fileList = lists.length === 0 ? undefined : lists.flat()
|
|
664
|
+
const behavior = remote?.behavior ?? files.find((settings) => settings.behavior)?.behavior
|
|
665
|
+
if (remote === undefined) return fileList
|
|
666
|
+
return behavior === "merge" ? (remote.list ?? fileList) : remote.list
|
|
667
|
+
}
|
|
668
|
+
|
|
669
|
+
/**
|
|
670
|
+
* Whether this server's channel is loaded, for the `claude` process `claudePid` running the bundle
|
|
671
|
+
* at `bundlePath`: as a plugin when Claude Code set `CLAUDE_PLUGIN_ROOT`, else from a bare MCP
|
|
672
|
+
* config. `ENVOY_CHANNEL_LOADED=1` or `0` overrides the check.
|
|
673
|
+
*/
|
|
674
|
+
export function detectClaudeChannel(input: {
|
|
675
|
+
readonly claudePid: number
|
|
676
|
+
readonly bundlePath: string
|
|
677
|
+
readonly environment: NodeJS.ProcessEnv
|
|
678
|
+
}): ChannelGate {
|
|
679
|
+
const override = input.environment["ENVOY_CHANNEL_LOADED"]?.trim()
|
|
680
|
+
if (override === "1") return { loaded: true }
|
|
681
|
+
if (override === "0") return { loaded: false, why: "ENVOY_CHANNEL_LOADED=0" }
|
|
682
|
+
const configDirectory =
|
|
683
|
+
input.environment["CLAUDE_CONFIG_DIR"]?.trim() || join(homedir(), ".claude")
|
|
684
|
+
const commandLine = claudeCommandLine(input.claudePid)
|
|
685
|
+
const pluginRoot = input.environment["CLAUDE_PLUGIN_ROOT"]?.trim()
|
|
686
|
+
const plugin = pluginRoot
|
|
687
|
+
? {
|
|
688
|
+
name: "claude-envoy",
|
|
689
|
+
// A `--plugin-dir` value is relative to the `claude` process's directory, which only
|
|
690
|
+
// `/proc` gives, with the exact command line.
|
|
691
|
+
marketplace: loadedMarketplace(
|
|
692
|
+
pluginRoot,
|
|
693
|
+
input.bundlePath,
|
|
694
|
+
commandLine?.exact && commandLine.cwd !== undefined
|
|
695
|
+
? { argv: commandLine.argv, cwd: commandLine.cwd }
|
|
696
|
+
: undefined,
|
|
697
|
+
),
|
|
698
|
+
}
|
|
699
|
+
: undefined
|
|
700
|
+
return claudeChannelGate({
|
|
701
|
+
commandLine,
|
|
702
|
+
plugin,
|
|
703
|
+
allowlist: channelAllowlist({ managedDirectory: managedSettingsDirectory(), configDirectory }),
|
|
704
|
+
})
|
|
705
|
+
}
|