rulesync 40.1.0 → 40.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/dist/cli/index.cjs +14 -14
- package/dist/cli/index.js +14 -14
- package/dist/cli/index.js.map +1 -1
- package/dist/{import-CZq4buOw.cjs → import-D-Be9rwv.cjs} +390 -73
- package/dist/{import-BmBQ8sWz.js → import-D7cJEvia.js} +391 -74
- package/dist/{import-BmBQ8sWz.js.map → import-D7cJEvia.js.map} +1 -1
- package/dist/index.cjs +1 -1
- package/dist/index.d.cts.map +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/package.json +1 -1
|
@@ -23496,6 +23496,62 @@ function findOpencodeV2McpContainers(mcp) {
|
|
|
23496
23496
|
return [...envelope !== null && Object.keys(envelope).length > 0 ? ["mcp.servers"] : [], ...isOpencodeV2GlobalTimeout(mcp.timeout) && Object.keys(mcp.timeout).length > 0 ? ["mcp.timeout"] : []];
|
|
23497
23497
|
}
|
|
23498
23498
|
/**
|
|
23499
|
+
* Returns the non-empty V2 global `mcp.timeout` object of an OpenCode `mcp`
|
|
23500
|
+
* value, or `undefined` when there is none.
|
|
23501
|
+
*/
|
|
23502
|
+
function getOpencodeV2GlobalMcpTimeout(mcp) {
|
|
23503
|
+
return isRecord$1(mcp) && findOpencodeV2McpContainers(mcp).includes("mcp.timeout") ? mcp.timeout : void 0;
|
|
23504
|
+
}
|
|
23505
|
+
/**
|
|
23506
|
+
* Returns the raw entry an OpenCode `mcp` value holds for a server: the one
|
|
23507
|
+
* under the V2 `mcp.servers` envelope, or else the flat V1 entry.
|
|
23508
|
+
*/
|
|
23509
|
+
function getOpencodeMcpServerEntry(mcp, name) {
|
|
23510
|
+
if (!isRecord$1(mcp)) return;
|
|
23511
|
+
const envelope = getOpencodeV2McpEnvelope(mcp);
|
|
23512
|
+
if (envelope !== null && Object.hasOwn(envelope, name)) return envelope[name];
|
|
23513
|
+
return Object.hasOwn(mcp, name) ? mcp[name] : void 0;
|
|
23514
|
+
}
|
|
23515
|
+
const OPENCODE_V1_TO_V2_OAUTH_KEYS = new Map([...OPENCODE_V2_OAUTH_KEYS].map(([v2, v1]) => [v1, v2]));
|
|
23516
|
+
/**
|
|
23517
|
+
* Raises one V1-shaped OpenCode MCP server (`local` or `remote`) to the V2
|
|
23518
|
+
* spelling — the inverse of {@link lowerOpencodeV2Server}:
|
|
23519
|
+
*
|
|
23520
|
+
* - `enabled` is dropped and `disabled: true` written only for a disabled server;
|
|
23521
|
+
* - a number `timeout` becomes `{ catalog, execution }`, and camelCase `oauth`
|
|
23522
|
+
* keys become snake_case.
|
|
23523
|
+
*
|
|
23524
|
+
* Values Rulesync's canonical form cannot hold are taken from the entry the
|
|
23525
|
+
* file already has for the server (`existing`): its `timeout` object and its
|
|
23526
|
+
* `oauth` object are kept verbatim whenever lowering them gives back exactly
|
|
23527
|
+
* what the canonical server carries, so a V2 file survives import and
|
|
23528
|
+
* generate unchanged. When the canonical `timeout` differs, the existing
|
|
23529
|
+
* `startup` value is still kept.
|
|
23530
|
+
*/
|
|
23531
|
+
function raiseOpencodeServerToV2(server, existing) {
|
|
23532
|
+
const { enabled, timeout, oauth, ...rest } = server;
|
|
23533
|
+
const raised = { ...rest };
|
|
23534
|
+
if (enabled === false) raised.disabled = true;
|
|
23535
|
+
const existingTimeout = isRecord$1(existing) && isRecord$1(existing.timeout) ? existing.timeout : null;
|
|
23536
|
+
if (existingTimeout !== null && lowerOpencodeV2Timeout(existingTimeout) === timeout) raised.timeout = existingTimeout;
|
|
23537
|
+
else if (typeof timeout === "number") {
|
|
23538
|
+
const startup = existingTimeout?.startup;
|
|
23539
|
+
raised.timeout = {
|
|
23540
|
+
...startup !== void 0 && { startup },
|
|
23541
|
+
catalog: timeout,
|
|
23542
|
+
execution: timeout
|
|
23543
|
+
};
|
|
23544
|
+
} else if (timeout !== void 0) raised.timeout = timeout;
|
|
23545
|
+
const existingOauth = isRecord$1(existing) && isRecord$1(existing.oauth) ? existing.oauth : null;
|
|
23546
|
+
if (existingOauth !== null && isRecord$1(oauth) && (0, node_util.isDeepStrictEqual)(lowerOpencodeV2OAuth(existingOauth, ""), oauth)) raised.oauth = existingOauth;
|
|
23547
|
+
else if (isRecord$1(oauth)) {
|
|
23548
|
+
const snakeOauth = {};
|
|
23549
|
+
for (const [key, value] of Object.entries(oauth)) setOwnEntry(snakeOauth, OPENCODE_V1_TO_V2_OAUTH_KEYS.get(key) ?? key, value);
|
|
23550
|
+
raised.oauth = snakeOauth;
|
|
23551
|
+
} else if (oauth !== void 0) raised.oauth = oauth;
|
|
23552
|
+
return raised;
|
|
23553
|
+
}
|
|
23554
|
+
/**
|
|
23499
23555
|
* Lowers OpenCode V2 `mcp` spellings found in a V1 config to the flat V1 map,
|
|
23500
23556
|
* the way OpenCode V1 itself does since v1.18.24 (`normalizeMcp`):
|
|
23501
23557
|
*
|
|
@@ -33957,6 +34013,69 @@ var JunieHooks = class JunieHooks extends ToolHooks {
|
|
|
33957
34013
|
}
|
|
33958
34014
|
};
|
|
33959
34015
|
//#endregion
|
|
34016
|
+
//#region src/features/hooks/hook-block-reason.ts
|
|
34017
|
+
/**
|
|
34018
|
+
* Source lines of a `sanitizeBlockReason(raw)` helper, emitted into generated
|
|
34019
|
+
* hook plugins that hand a hook command's output to the agent as the reason a
|
|
34020
|
+
* call was blocked (the Pi / oh-my-pi extension and the OpenCode-style plugin).
|
|
34021
|
+
* It returns the sanitized, length-capped text, or `""` when nothing survives
|
|
34022
|
+
* so the caller can substitute its own fallback.
|
|
34023
|
+
*
|
|
34024
|
+
* The text is sanitized because a hook command's output can relay third-party
|
|
34025
|
+
* content (linter output, matched file lines) into a terminal or, in RPC mode,
|
|
34026
|
+
* into an external client. Escape sequences, C1 and other control characters,
|
|
34027
|
+
* and format characters such as bidirectional overrides and zero-width joiners
|
|
34028
|
+
* are stripped so the reason cannot repaint or reorder what the user is shown,
|
|
34029
|
+
* and carriage returns are folded into newlines so it cannot overwrite an
|
|
34030
|
+
* already printed line. The format-character pass is a superset of the bidi
|
|
34031
|
+
* controls listed in `CONTROL_CHARACTERS_PATTERN`
|
|
34032
|
+
* (`src/utils/control-characters.ts`): a blocked-hook reason is short
|
|
34033
|
+
* diagnostic text, so dropping every `Cf` character is safer than enumerating
|
|
34034
|
+
* the harmful ones and missing `U+061C`, `U+FEFF`, or a future addition.
|
|
34035
|
+
*
|
|
34036
|
+
* Sanitization runs before truncation so a reason that begins with a progress
|
|
34037
|
+
* banner is still reported by its content. The raw text is only sliced to a
|
|
34038
|
+
* generous backstop first: a hook command may emit a large amount of output,
|
|
34039
|
+
* and the slice bounds the work even if a later edit reintroduces a pattern
|
|
34040
|
+
* that is not linear. A slice that actually fired is reported as truncated, so
|
|
34041
|
+
* the text is never silently cut.
|
|
34042
|
+
*
|
|
34043
|
+
* Both slices cut on UTF-16 code units, so each drops a trailing lone high
|
|
34044
|
+
* surrogate: the reason may travel as JSON, and half a surrogate pair is not
|
|
34045
|
+
* text a strict consumer has to accept.
|
|
34046
|
+
*
|
|
34047
|
+
* `typed` emits TypeScript parameter and return annotations (for the Pi
|
|
34048
|
+
* extension, a `.ts` module); without it the lines are plain JavaScript.
|
|
34049
|
+
*/
|
|
34050
|
+
function buildBlockReasonSanitizerLines({ typed }) {
|
|
34051
|
+
const str = typed ? ": string" : "";
|
|
34052
|
+
return [
|
|
34053
|
+
"const MAX_BLOCK_REASON_LENGTH = 2000;",
|
|
34054
|
+
"const MAX_SCANNED_REASON_LENGTH = 128_000;",
|
|
34055
|
+
"",
|
|
34056
|
+
`function dropTrailingLoneSurrogate(text${str})${str} {`,
|
|
34057
|
+
" return text.replace(/[\\ud800-\\udbff]$/, \"\");",
|
|
34058
|
+
"}",
|
|
34059
|
+
"",
|
|
34060
|
+
`function sanitizeBlockReason(raw${str})${str} {`,
|
|
34061
|
+
" const scannedTruncated = raw.length > MAX_SCANNED_REASON_LENGTH;",
|
|
34062
|
+
" const scanned = scannedTruncated",
|
|
34063
|
+
" ? dropTrailingLoneSurrogate(raw.slice(0, MAX_SCANNED_REASON_LENGTH))",
|
|
34064
|
+
" : raw;",
|
|
34065
|
+
" const sanitized = scanned",
|
|
34066
|
+
" .replace(/\\r\\n?/g, \"\\n\")",
|
|
34067
|
+
" .replace(/\\u001b\\[[0-9;?]*[\\u0020-\\u002f]*[\\u0040-\\u007e]/g, \"\")",
|
|
34068
|
+
" .replace(/\\u001b\\][^\\u0007\\u001b]{0,256}(?:\\u0007|\\u001b\\\\)/g, \"\")",
|
|
34069
|
+
" .replace(/[\\u0000-\\u0008\\u000b\\u000c\\u000e-\\u001f\\u007f-\\u009f]/g, \"\")",
|
|
34070
|
+
" .replace(/[\\p{Cf}\\p{Zl}\\p{Zp}]/gu, \"\")",
|
|
34071
|
+
" .trim();",
|
|
34072
|
+
" if (!sanitized) return \"\";",
|
|
34073
|
+
" if (sanitized.length <= MAX_BLOCK_REASON_LENGTH && !scannedTruncated) return sanitized;",
|
|
34074
|
+
" return `${dropTrailingLoneSurrogate(sanitized.slice(0, MAX_BLOCK_REASON_LENGTH))}...`;",
|
|
34075
|
+
"}"
|
|
34076
|
+
];
|
|
34077
|
+
}
|
|
34078
|
+
//#endregion
|
|
33960
34079
|
//#region src/features/hooks/opencode-style-generator.ts
|
|
33961
34080
|
/**
|
|
33962
34081
|
* Tool events emitted as named `(input, ...)` hooks rather than through the
|
|
@@ -34027,6 +34146,87 @@ const SHELL_EVENT_TOOL_GATES = {
|
|
|
34027
34146
|
tool: "bash"
|
|
34028
34147
|
}
|
|
34029
34148
|
};
|
|
34149
|
+
/**
|
|
34150
|
+
* Canonical events whose command receives a Claude Code-style JSON payload on
|
|
34151
|
+
* stdin and whose result can deny the call, mapped to the `hook_event_name`
|
|
34152
|
+
* the payload carries. Only `preToolUse` (`tool.execute.before`) takes part:
|
|
34153
|
+
* it is the one named hook where throwing aborts the call — OpenCode rejects
|
|
34154
|
+
* the tool call and returns the error message to the model (the `.env`
|
|
34155
|
+
* protection example in the plugins doc).
|
|
34156
|
+
*
|
|
34157
|
+
* The payload mirrors Claude Code's `PreToolUse` input: `hook_event_name`,
|
|
34158
|
+
* `session_id` (`input.sessionID`), `tool_use_id` (`input.callID`),
|
|
34159
|
+
* `tool_name` (the raw OpenCode tool name — `bash`, `edit`, `<server>_<tool>`
|
|
34160
|
+
* for MCP — the same subject a `matcher` is tested against), `tool_input`
|
|
34161
|
+
* (`output.args`) and `cwd` (the plugin context's `directory`, falling back to
|
|
34162
|
+
* `process.cwd()`). It is serialized lazily, on the first handler whose
|
|
34163
|
+
* matcher matches, so a tool call no hook applies to pays nothing for it.
|
|
34164
|
+
*
|
|
34165
|
+
* Bun Shell has no stdin option on a command and rejects redirections on a
|
|
34166
|
+
* subshell, so the payload is piped in by the shell's builtin `echo`
|
|
34167
|
+
* (`echo ${payload} | <command>`); Bun escapes the interpolated value, so the
|
|
34168
|
+
* payload never reaches the shell as syntax. The pipe feeds the first
|
|
34169
|
+
* pipeline of the command — a compound `a && b` or `a; b` gives the payload
|
|
34170
|
+
* to `a` only, so multi-step guards belong in a script.
|
|
34171
|
+
*
|
|
34172
|
+
* @see https://opencode.ai/docs/plugins/
|
|
34173
|
+
* @see https://bun.sh/docs/runtime/shell
|
|
34174
|
+
* @see https://code.claude.com/docs/en/hooks
|
|
34175
|
+
*/
|
|
34176
|
+
const STDIN_PAYLOAD_EVENTS = { preToolUse: "PreToolUse" };
|
|
34177
|
+
/**
|
|
34178
|
+
* Name of the helper the generated plugin defines when a `preToolUse` handler
|
|
34179
|
+
* is present. It interprets the command's result like Claude Code does for a
|
|
34180
|
+
* `PreToolUse` hook, throwing (which aborts the OpenCode tool call) when the
|
|
34181
|
+
* hook denies it:
|
|
34182
|
+
* - exit code 2 → deny, with the command's stderr as the reason;
|
|
34183
|
+
* - exit code 0 with JSON stdout whose `hookSpecificOutput.permissionDecision`
|
|
34184
|
+
* is `"deny"` → deny, with `permissionDecisionReason` as the reason;
|
|
34185
|
+
* - exit code 0 otherwise → allow;
|
|
34186
|
+
* - any other non-zero exit → deny too, with stderr or the exit code.
|
|
34187
|
+
*
|
|
34188
|
+
* Every reason goes through the `sanitizeBlockReason` helper shared with the
|
|
34189
|
+
* Pi extension (`hook-block-reason.ts`): control and format characters are
|
|
34190
|
+
* stripped and the text is capped at 2000 characters before the model sees it.
|
|
34191
|
+
*
|
|
34192
|
+
* The other non-zero exit case is deliberate. Claude Code treats that as a
|
|
34193
|
+
* non-blocking error, but before this payload contract existed the plugin
|
|
34194
|
+
* ran the command through Bun Shell's default throwing mode, so every
|
|
34195
|
+
* non-zero exit aborted the call. Keeping that preserves guards that block
|
|
34196
|
+
* with `exit 1`, rather than silently failing them open.
|
|
34197
|
+
*/
|
|
34198
|
+
const PRE_TOOL_USE_RESULT_HELPER = "__rulesyncEnforcePreToolUseResult";
|
|
34199
|
+
const PRE_TOOL_USE_RESULT_HELPER_LINES = [
|
|
34200
|
+
...buildBlockReasonSanitizerLines({ typed: false }),
|
|
34201
|
+
"",
|
|
34202
|
+
`const ${PRE_TOOL_USE_RESULT_HELPER} = (result) => {`,
|
|
34203
|
+
" const stderr = result.stderr.toString();",
|
|
34204
|
+
" if (result.exitCode === 0) {",
|
|
34205
|
+
" let parsed;",
|
|
34206
|
+
" try {",
|
|
34207
|
+
" parsed = JSON.parse(result.stdout.toString());",
|
|
34208
|
+
" } catch {",
|
|
34209
|
+
" return;",
|
|
34210
|
+
" }",
|
|
34211
|
+
" const specific = parsed && typeof parsed === \"object\" ? parsed.hookSpecificOutput : undefined;",
|
|
34212
|
+
" if (specific && typeof specific === \"object\" && specific.permissionDecision === \"deny\") {",
|
|
34213
|
+
" throw new Error(",
|
|
34214
|
+
" (typeof specific.permissionDecisionReason === \"string\" &&",
|
|
34215
|
+
" sanitizeBlockReason(specific.permissionDecisionReason)) ||",
|
|
34216
|
+
" \"Blocked by a preToolUse hook.\",",
|
|
34217
|
+
" );",
|
|
34218
|
+
" }",
|
|
34219
|
+
" return;",
|
|
34220
|
+
" }",
|
|
34221
|
+
" if (result.exitCode === 2) {",
|
|
34222
|
+
" throw new Error(sanitizeBlockReason(stderr) || \"Blocked by a preToolUse hook.\");",
|
|
34223
|
+
" }",
|
|
34224
|
+
" throw new Error(",
|
|
34225
|
+
" sanitizeBlockReason(stderr) ||",
|
|
34226
|
+
" \"preToolUse hook command failed with exit code \" + result.exitCode + \".\",",
|
|
34227
|
+
" );",
|
|
34228
|
+
"};"
|
|
34229
|
+
];
|
|
34030
34230
|
function escapeForTemplateLiteral(command) {
|
|
34031
34231
|
return command.replace(/\\/g, "\\\\").replace(/`/g, "\\`").replace(/\$\{/g, "\\${");
|
|
34032
34232
|
}
|
|
@@ -34041,6 +34241,17 @@ function validateAndSanitizeMatcher(matcher) {
|
|
|
34041
34241
|
}
|
|
34042
34242
|
return sanitized.replace(/\\/g, "\\\\").replace(/"/g, "\\\"");
|
|
34043
34243
|
}
|
|
34244
|
+
/** The per-canonical-event gates and payload flag every handler of it carries. */
|
|
34245
|
+
function handlerExtrasFor(canonicalEvent) {
|
|
34246
|
+
const shellGate = SHELL_EVENT_TOOL_GATES[canonicalEvent];
|
|
34247
|
+
const propertyGate = GENERIC_EVENT_PROPERTY_GATES[canonicalEvent];
|
|
34248
|
+
const stdinEvent = shellGate ? void 0 : STDIN_PAYLOAD_EVENTS[canonicalEvent];
|
|
34249
|
+
return {
|
|
34250
|
+
...shellGate ? { toolGate: shellGate.tool } : {},
|
|
34251
|
+
...propertyGate ? { propertyGate } : {},
|
|
34252
|
+
...stdinEvent ? { stdinEvent } : {}
|
|
34253
|
+
};
|
|
34254
|
+
}
|
|
34044
34255
|
/**
|
|
34045
34256
|
* Group the effective hooks into named (tool.execute.*) and generic event
|
|
34046
34257
|
* handler groups, keyed by tool event name. Mutates the supplied groups.
|
|
@@ -34048,7 +34259,7 @@ function validateAndSanitizeMatcher(matcher) {
|
|
|
34048
34259
|
function collectOpencodeStyleHandlers({ effectiveHooks, eventMap, namedEventHandlers, genericEventHandlers }) {
|
|
34049
34260
|
for (const [canonicalEvent, definitions] of Object.entries(effectiveHooks)) {
|
|
34050
34261
|
const shellGate = SHELL_EVENT_TOOL_GATES[canonicalEvent];
|
|
34051
|
-
const
|
|
34262
|
+
const extras = handlerExtrasFor(canonicalEvent);
|
|
34052
34263
|
const toolEvent = shellGate?.toolEvent ?? eventMap[canonicalEvent];
|
|
34053
34264
|
if (!toolEvent) continue;
|
|
34054
34265
|
const matcherSupported = !shellGate && Object.hasOwn(NAMED_HOOK_MATCHER_SUBJECTS, toolEvent) && NAMED_HOOK_MATCHER_SUBJECTS[toolEvent] !== null;
|
|
@@ -34060,8 +34271,7 @@ function collectOpencodeStyleHandlers({ effectiveHooks, eventMap, namedEventHand
|
|
|
34060
34271
|
handlers.push({
|
|
34061
34272
|
command: def.command,
|
|
34062
34273
|
matcher: def.matcher ? def.matcher : void 0,
|
|
34063
|
-
...
|
|
34064
|
-
...propertyGate ? { propertyGate } : {}
|
|
34274
|
+
...extras
|
|
34065
34275
|
});
|
|
34066
34276
|
}
|
|
34067
34277
|
if (handlers.length > 0) {
|
|
@@ -34094,46 +34304,79 @@ function buildGenericEventBodyLines(genericEventHandlers) {
|
|
|
34094
34304
|
bodyLines.push(" },");
|
|
34095
34305
|
return bodyLines;
|
|
34096
34306
|
}
|
|
34307
|
+
function stdinPayloadVariable(stdinEvent) {
|
|
34308
|
+
return `__rulesync${stdinEvent}Input`;
|
|
34309
|
+
}
|
|
34310
|
+
/**
|
|
34311
|
+
* The statement that runs a handler's command. A handler with a stdin payload
|
|
34312
|
+
* pipes the payload in and hands the result to the deny-aware helper; every
|
|
34313
|
+
* other handler keeps Bun Shell's default mode, where a non-zero exit throws.
|
|
34314
|
+
*/
|
|
34315
|
+
function buildRunStatement(handler) {
|
|
34316
|
+
const escapedCommand = escapeForTemplateLiteral(handler.command);
|
|
34317
|
+
if (handler.stdinEvent) {
|
|
34318
|
+
const payload = stdinPayloadVariable(handler.stdinEvent);
|
|
34319
|
+
return `${PRE_TOOL_USE_RESULT_HELPER}(await $\`echo \${${payload}()} | ${escapedCommand}\`.nothrow().quiet());`;
|
|
34320
|
+
}
|
|
34321
|
+
return `await $\`${escapedCommand}\`;`;
|
|
34322
|
+
}
|
|
34097
34323
|
/** Emit the named (`tool.execute.*`) handler blocks. */
|
|
34098
34324
|
function buildNamedEventBodyLines(namedEventHandlers) {
|
|
34099
34325
|
const bodyLines = [];
|
|
34100
34326
|
for (const [eventName, handlers] of Object.entries(namedEventHandlers)) {
|
|
34101
34327
|
const matcherSubject = NAMED_HOOK_MATCHER_SUBJECTS[eventName] ?? null;
|
|
34102
|
-
|
|
34328
|
+
const stdinEvents = [...new Set(handlers.flatMap((handler) => handler.stdinEvent ? [handler.stdinEvent] : []))];
|
|
34329
|
+
bodyLines.push(` "${eventName}": async (${stdinEvents.length > 0 ? "input, output" : "input"}) => {`);
|
|
34330
|
+
for (const stdinEvent of stdinEvents) {
|
|
34331
|
+
const payload = stdinPayloadVariable(stdinEvent);
|
|
34332
|
+
bodyLines.push(` let ${payload}Cache;`);
|
|
34333
|
+
bodyLines.push(` const ${payload} = () =>`);
|
|
34334
|
+
bodyLines.push(` (${payload}Cache ??= JSON.stringify({`);
|
|
34335
|
+
bodyLines.push(` hook_event_name: "${stdinEvent}",`);
|
|
34336
|
+
bodyLines.push(" session_id: input.sessionID,");
|
|
34337
|
+
bodyLines.push(" tool_use_id: input.callID,");
|
|
34338
|
+
bodyLines.push(" tool_name: input.tool,");
|
|
34339
|
+
bodyLines.push(" tool_input: output?.args,");
|
|
34340
|
+
bodyLines.push(" cwd: directory ?? process.cwd(),");
|
|
34341
|
+
bodyLines.push(" }));");
|
|
34342
|
+
}
|
|
34103
34343
|
for (const handler of handlers) {
|
|
34104
|
-
const
|
|
34344
|
+
const runStatement = buildRunStatement(handler);
|
|
34105
34345
|
if (handler.toolGate) {
|
|
34106
34346
|
bodyLines.push(` if (input.tool === "${handler.toolGate}") {`);
|
|
34107
|
-
bodyLines.push(`
|
|
34347
|
+
bodyLines.push(` ${runStatement}`);
|
|
34108
34348
|
bodyLines.push(" }");
|
|
34109
34349
|
} else if (handler.matcher && matcherSubject !== null) {
|
|
34110
34350
|
const safeMatcher = validateAndSanitizeMatcher(handler.matcher);
|
|
34111
34351
|
bodyLines.push(" {");
|
|
34112
34352
|
bodyLines.push(` const __re = new RegExp("${safeMatcher}");`);
|
|
34113
34353
|
bodyLines.push(` if (__re.test(${matcherSubject})) {`);
|
|
34114
|
-
bodyLines.push(`
|
|
34354
|
+
bodyLines.push(` ${runStatement}`);
|
|
34115
34355
|
bodyLines.push(" }");
|
|
34116
34356
|
bodyLines.push(" }");
|
|
34117
|
-
} else bodyLines.push(`
|
|
34357
|
+
} else bodyLines.push(` ${runStatement}`);
|
|
34118
34358
|
}
|
|
34119
34359
|
bodyLines.push(" },");
|
|
34120
34360
|
}
|
|
34121
34361
|
return bodyLines;
|
|
34122
34362
|
}
|
|
34123
34363
|
/** Wrap the handler body lines in the requested export shape. */
|
|
34124
|
-
function wrapInExportShape({ bodyLines, exportStyle }) {
|
|
34364
|
+
function wrapInExportShape({ preambleLines, bodyLines, exportStyle }) {
|
|
34365
|
+
const pluginContextPattern = preambleLines.length > 0 ? "{ $, directory }" : "{ $ }";
|
|
34125
34366
|
const lines = [];
|
|
34126
34367
|
if (exportStyle === "default") {
|
|
34127
34368
|
lines.push("export default {");
|
|
34128
34369
|
lines.push(" id: \"rulesync-hooks\",");
|
|
34129
|
-
lines.push(
|
|
34370
|
+
lines.push(` server: async (${pluginContextPattern}) => {`);
|
|
34371
|
+
for (const line of preambleLines) lines.push(line === "" ? "" : ` ${line}`);
|
|
34130
34372
|
lines.push(" return {");
|
|
34131
34373
|
for (const line of bodyLines) lines.push(line === "" ? "" : ` ${line}`);
|
|
34132
34374
|
lines.push(" };");
|
|
34133
34375
|
lines.push(" },");
|
|
34134
34376
|
lines.push("};");
|
|
34135
34377
|
} else {
|
|
34136
|
-
lines.push(
|
|
34378
|
+
lines.push(`export const RulesyncHooksPlugin = async (${pluginContextPattern}) => {`);
|
|
34379
|
+
for (const line of preambleLines) lines.push(line === "" ? "" : ` ${line}`);
|
|
34137
34380
|
lines.push(" return {");
|
|
34138
34381
|
lines.push(...bodyLines);
|
|
34139
34382
|
lines.push(" };");
|
|
@@ -34158,8 +34401,10 @@ function generateOpencodeStylePluginCode(config, supportedEvents, toolConfigKey,
|
|
|
34158
34401
|
namedEventHandlers,
|
|
34159
34402
|
genericEventHandlers
|
|
34160
34403
|
});
|
|
34404
|
+
const bodyLines = [...buildGenericEventBodyLines(genericEventHandlers), ...buildNamedEventBodyLines(namedEventHandlers)];
|
|
34161
34405
|
return wrapInExportShape({
|
|
34162
|
-
|
|
34406
|
+
preambleLines: Object.values(namedEventHandlers).some((handlers) => handlers.some((handler) => handler.stdinEvent !== void 0)) ? PRE_TOOL_USE_RESULT_HELPER_LINES : [],
|
|
34407
|
+
bodyLines,
|
|
34163
34408
|
exportStyle
|
|
34164
34409
|
}).join("\n");
|
|
34165
34410
|
}
|
|
@@ -35304,36 +35549,12 @@ function failureLinesByMode(dialect) {
|
|
|
35304
35549
|
* `PI_SETTLE_BLOCKING_EVENT`): such a failure is rethrown for Pi to report
|
|
35305
35550
|
* instead of continuing the agent.
|
|
35306
35551
|
*
|
|
35307
|
-
* The chosen text
|
|
35308
|
-
*
|
|
35309
|
-
*
|
|
35310
|
-
* characters, and format characters such as bidirectional overrides and
|
|
35311
|
-
* zero-width joiners are stripped so the reason cannot repaint or reorder what
|
|
35312
|
-
* the user is shown, and carriage returns are folded into newlines so it cannot
|
|
35313
|
-
* overwrite an already printed line. The format-character pass is a superset of
|
|
35314
|
-
* the bidi controls listed in `CONTROL_CHARACTERS_PATTERN`
|
|
35315
|
-
* (`src/utils/control-characters.ts`): a blocked-hook reason is short
|
|
35316
|
-
* diagnostic text, so dropping every `Cf` character is safer than enumerating
|
|
35317
|
-
* the harmful ones and missing `U+061C`, `U+FEFF`, or a future addition.
|
|
35318
|
-
*
|
|
35319
|
-
* Sanitization runs before truncation so a reason that begins with a progress
|
|
35320
|
-
* banner is still reported by its content. The raw text is only sliced to a
|
|
35321
|
-
* generous backstop first: a hook command may emit up to `exec`'s full
|
|
35322
|
-
* `maxBuffer`, and the slice bounds the work even if a later edit reintroduces
|
|
35323
|
-
* a pattern that is not linear. A slice that actually fired is reported as
|
|
35324
|
-
* truncated, so the text is never silently cut.
|
|
35325
|
-
*
|
|
35326
|
-
* Both slices cut on UTF-16 code units, so each drops a trailing lone high
|
|
35327
|
-
* surrogate: the reason travels to an RPC client as JSON, and half a surrogate
|
|
35328
|
-
* pair is not text a strict consumer has to accept.
|
|
35552
|
+
* The chosen text goes through `sanitizeBlockReason`, shared with the
|
|
35553
|
+
* OpenCode-style plugin (see `hook-block-reason.ts`): control and format
|
|
35554
|
+
* characters are stripped and the result is capped at 2000 characters.
|
|
35329
35555
|
*/
|
|
35330
35556
|
const BLOCK_REASON_HELPER_LINES = [
|
|
35331
|
-
|
|
35332
|
-
"const MAX_SCANNED_REASON_LENGTH = 128_000;",
|
|
35333
|
-
"",
|
|
35334
|
-
"function dropTrailingLoneSurrogate(text: string): string {",
|
|
35335
|
-
" return text.replace(/[\\ud800-\\udbff]$/, \"\");",
|
|
35336
|
-
"}",
|
|
35557
|
+
...buildBlockReasonSanitizerLines({ typed: true }),
|
|
35337
35558
|
"",
|
|
35338
35559
|
"function toBlockReason(error: unknown): string {",
|
|
35339
35560
|
" const result = error as { stdout?: unknown; stderr?: unknown; code?: unknown } | null;",
|
|
@@ -35347,20 +35568,7 @@ const BLOCK_REASON_HELPER_LINES = [
|
|
|
35347
35568
|
" : error instanceof Error",
|
|
35348
35569
|
" ? error.message",
|
|
35349
35570
|
" : String(error));",
|
|
35350
|
-
"
|
|
35351
|
-
" const scanned = scannedTruncated",
|
|
35352
|
-
" ? dropTrailingLoneSurrogate(raw.slice(0, MAX_SCANNED_REASON_LENGTH))",
|
|
35353
|
-
" : raw;",
|
|
35354
|
-
" const sanitized = scanned",
|
|
35355
|
-
" .replace(/\\r\\n?/g, \"\\n\")",
|
|
35356
|
-
" .replace(/\\u001b\\[[0-9;?]*[\\u0020-\\u002f]*[\\u0040-\\u007e]/g, \"\")",
|
|
35357
|
-
" .replace(/\\u001b\\][^\\u0007\\u001b]{0,256}(?:\\u0007|\\u001b\\\\)/g, \"\")",
|
|
35358
|
-
" .replace(/[\\u0000-\\u0008\\u000b\\u000c\\u000e-\\u001f\\u007f-\\u009f]/g, \"\")",
|
|
35359
|
-
" .replace(/[\\p{Cf}\\p{Zl}\\p{Zp}]/gu, \"\")",
|
|
35360
|
-
" .trim();",
|
|
35361
|
-
" if (!sanitized) return \"Hook command failed.\";",
|
|
35362
|
-
" if (sanitized.length <= MAX_BLOCK_REASON_LENGTH && !scannedTruncated) return sanitized;",
|
|
35363
|
-
" return `${dropTrailingLoneSurrogate(sanitized.slice(0, MAX_BLOCK_REASON_LENGTH))}...`;",
|
|
35571
|
+
" return sanitizeBlockReason(raw) || \"Hook command failed.\";",
|
|
35364
35572
|
"}"
|
|
35365
35573
|
];
|
|
35366
35574
|
/**
|
|
@@ -48979,12 +49187,18 @@ function warnAboutToggleDroppedKeys(serverName, serverConfig, logger) {
|
|
|
48979
49187
|
* - enabledTools/disabledTools -> top-level tools map (with server name prefix)
|
|
48980
49188
|
* - OpenCode-supported extras (timeout, oauth, protocol, codemode) -> passed through verbatim
|
|
48981
49189
|
*/
|
|
48982
|
-
function convertServerToOpencodeFormat(serverName, serverConfig, existingEntry, logger) {
|
|
49190
|
+
function convertServerToOpencodeFormat(serverName, serverConfig, existingEntry, configShape, logger) {
|
|
48983
49191
|
const serverRecord = serverConfig;
|
|
48984
49192
|
const passthrough = {};
|
|
48985
49193
|
for (const key of OPENCODE_PASSTHROUGH_SERVER_FIELDS) if (serverRecord[key] !== void 0) passthrough[key] = serverRecord[key];
|
|
48986
49194
|
const enabled = serverConfig.disabled !== void 0 ? !serverConfig.disabled : true;
|
|
48987
49195
|
if (declaresNoTransport(serverConfig)) {
|
|
49196
|
+
if (configShape === "v2") return warnAndSkipMcpServer({
|
|
49197
|
+
toolName: "OpenCode",
|
|
49198
|
+
serverName,
|
|
49199
|
+
reason: "no transport, and the OpenCode V2 shape (\"configShape\": \"v2\") has no toggle-only entry",
|
|
49200
|
+
logger
|
|
49201
|
+
});
|
|
48988
49202
|
if (serverConfig.disabled === void 0) {
|
|
48989
49203
|
if (existingEntry !== void 0 && !isOpencodeTransportServer(existingEntry)) {
|
|
48990
49204
|
warnAboutToggleDroppedKeys(serverName, serverConfig, logger);
|
|
@@ -49032,11 +49246,11 @@ function convertServerToOpencodeFormat(serverName, serverConfig, existingEntry,
|
|
|
49032
49246
|
...serverConfig.cwd && { cwd: serverConfig.cwd }
|
|
49033
49247
|
};
|
|
49034
49248
|
}
|
|
49035
|
-
function convertToOpencodeFormat(mcpServers, existingMcp, logger) {
|
|
49249
|
+
function convertToOpencodeFormat(mcpServers, existingMcp, configShape, logger) {
|
|
49036
49250
|
const tools = {};
|
|
49037
49251
|
return {
|
|
49038
49252
|
mcp: Object.fromEntries(Object.entries(mcpServers).map(([serverName, serverConfig]) => {
|
|
49039
|
-
const converted = convertServerToOpencodeFormat(serverName, serverConfig, existingMcp[serverName], logger);
|
|
49253
|
+
const converted = convertServerToOpencodeFormat(serverName, serverConfig, existingMcp[serverName], configShape, logger);
|
|
49040
49254
|
if (serverConfig.enabledTools) for (const tool of serverConfig.enabledTools) tools[`${serverName}_${tool}`] = true;
|
|
49041
49255
|
if (serverConfig.disabledTools) for (const tool of serverConfig.disabledTools) tools[`${serverName}_${tool}`] = false;
|
|
49042
49256
|
return converted === null ? null : [serverName, converted];
|
|
@@ -49044,6 +49258,7 @@ function convertToOpencodeFormat(mcpServers, existingMcp, logger) {
|
|
|
49044
49258
|
tools
|
|
49045
49259
|
};
|
|
49046
49260
|
}
|
|
49261
|
+
const OpencodeMcpFeatureOptionsSchema = zod_mini.z.looseObject({ configShape: zod_mini.z.optional(zod_mini.z.enum(["v1", "v2"])) });
|
|
49047
49262
|
var OpencodeMcp = class extends ToolMcp {
|
|
49048
49263
|
/** Directory layout; OpenCode forks (MiMo Code) override it. */
|
|
49049
49264
|
static layout = OPENCODE_LAYOUT;
|
|
@@ -49082,6 +49297,19 @@ var OpencodeMcp = class extends ToolMcp {
|
|
|
49082
49297
|
return this.json;
|
|
49083
49298
|
}
|
|
49084
49299
|
/**
|
|
49300
|
+
* Reads the `configShape` option of the target's `mcp` feature options
|
|
49301
|
+
* (`targets.opencode.mcp.configShape`). Only a layout that reads the V2
|
|
49302
|
+
* spellings back can be given the V2 shape.
|
|
49303
|
+
*/
|
|
49304
|
+
static resolveConfigShape(options) {
|
|
49305
|
+
if (!options) return "v1";
|
|
49306
|
+
const parsed = OpencodeMcpFeatureOptionsSchema.safeParse(options);
|
|
49307
|
+
if (!parsed.success) throw new Error(`Invalid options for the ${this.layout.toolTarget} mcp feature: \`configShape\` must be either "v1" or "v2".`);
|
|
49308
|
+
const configShape = parsed.data.configShape ?? "v1";
|
|
49309
|
+
if (configShape === "v2" && !this.layout.readsV2ConfigSpellings) throw new Error(`Invalid options for the ${this.layout.toolTarget} mcp feature: \`configShape: "v2"\` is supported only for OpenCode.`);
|
|
49310
|
+
return configShape;
|
|
49311
|
+
}
|
|
49312
|
+
/**
|
|
49085
49313
|
* opencode.json may contain other settings, so it should not be deleted.
|
|
49086
49314
|
*/
|
|
49087
49315
|
isDeletable() {
|
|
@@ -49125,7 +49353,8 @@ var OpencodeMcp = class extends ToolMcp {
|
|
|
49125
49353
|
logger
|
|
49126
49354
|
});
|
|
49127
49355
|
}
|
|
49128
|
-
static async fromRulesyncMcp({ outputRoot = process.cwd(), rulesyncMcp, validate = true, global = false, logger }) {
|
|
49356
|
+
static async fromRulesyncMcp({ outputRoot = process.cwd(), rulesyncMcp, validate = true, global = false, logger, featureOptions }) {
|
|
49357
|
+
const configShape = this.resolveConfigShape(featureOptions);
|
|
49129
49358
|
const basePaths = this.getSettablePaths({ global });
|
|
49130
49359
|
const jsonDir = (0, node_path.join)(outputRoot, basePaths.relativeDirPath);
|
|
49131
49360
|
let fileContent = null;
|
|
@@ -49143,16 +49372,21 @@ var OpencodeMcp = class extends ToolMcp {
|
|
|
49143
49372
|
});
|
|
49144
49373
|
const existingJson = (0, jsonc_parser.parse)(fileContent || "{}");
|
|
49145
49374
|
const existingMcp = OpencodeConfigSchema.safeParse(this.lowerConfig(existingJson));
|
|
49146
|
-
|
|
49147
|
-
|
|
49375
|
+
const existingRawMcp = isRecord$1(existingJson) ? existingJson.mcp : void 0;
|
|
49376
|
+
if (configShape === "v1" && this.layout.readsV2ConfigSpellings) {
|
|
49377
|
+
const containers = findOpencodeV2McpContainers(existingRawMcp);
|
|
49148
49378
|
if (containers.length > 0) logger?.warn(`OpenCode MCP: the whole "mcp" key is rewritten in the flat V1 shape, so ${containers.map(quoteValueForWarning).join(" and ")} ${containers.length === 1 ? "is" : "are"} not kept.`);
|
|
49149
49379
|
}
|
|
49150
|
-
const { mcp: convertedMcp, tools: mcpTools } = convertToOpencodeFormat(transformedServers, (existingMcp.success ? existingMcp.data.mcp : void 0) ?? {}, logger);
|
|
49151
|
-
const
|
|
49380
|
+
const { mcp: convertedMcp, tools: mcpTools } = convertToOpencodeFormat(transformedServers, (existingMcp.success ? existingMcp.data.mcp : void 0) ?? {}, configShape, logger);
|
|
49381
|
+
const generatedServers = undefinedWhenEmpty(convertedMcp);
|
|
49382
|
+
const generatedMcp = configShape === "v2" ? this.toV2Mcp({
|
|
49383
|
+
servers: convertedMcp,
|
|
49384
|
+
existingRawMcp
|
|
49385
|
+
}) : generatedServers;
|
|
49152
49386
|
const generatedTools = Object.keys(mcpTools).length > 0 ? mcpTools : void 0;
|
|
49153
49387
|
warnOpencodeDotDirOverrides({
|
|
49154
49388
|
generated: {
|
|
49155
|
-
mcp:
|
|
49389
|
+
mcp: generatedServers,
|
|
49156
49390
|
tools: generatedTools
|
|
49157
49391
|
},
|
|
49158
49392
|
overlays: (await readOpencodeDotDirConfigLayers({
|
|
@@ -49188,6 +49422,28 @@ var OpencodeMcp = class extends ToolMcp {
|
|
|
49188
49422
|
});
|
|
49189
49423
|
}
|
|
49190
49424
|
/**
|
|
49425
|
+
* Builds the V2 `mcp` value: each server raised to the V2 spelling under the
|
|
49426
|
+
* `mcp.servers` envelope, keeping what the existing file has that Rulesync
|
|
49427
|
+
* cannot express (see {@link raiseOpencodeServerToV2}), plus the existing
|
|
49428
|
+
* global `mcp.timeout`, which has no canonical counterpart.
|
|
49429
|
+
*/
|
|
49430
|
+
static toV2Mcp({ servers, existingRawMcp }) {
|
|
49431
|
+
const v2Servers = {};
|
|
49432
|
+
for (const [name, server] of Object.entries(servers)) Object.defineProperty(v2Servers, name, {
|
|
49433
|
+
value: raiseOpencodeServerToV2(server, getOpencodeMcpServerEntry(existingRawMcp, name)),
|
|
49434
|
+
enumerable: true,
|
|
49435
|
+
configurable: true,
|
|
49436
|
+
writable: true
|
|
49437
|
+
});
|
|
49438
|
+
const globalTimeout = getOpencodeV2GlobalMcpTimeout(existingRawMcp);
|
|
49439
|
+
const hasServers = Object.keys(v2Servers).length > 0;
|
|
49440
|
+
if (!hasServers && globalTimeout === void 0) return;
|
|
49441
|
+
return {
|
|
49442
|
+
...globalTimeout !== void 0 && { timeout: globalTimeout },
|
|
49443
|
+
...hasServers && { servers: v2Servers }
|
|
49444
|
+
};
|
|
49445
|
+
}
|
|
49446
|
+
/**
|
|
49191
49447
|
* Register additional instruction file paths into the shared opencode config
|
|
49192
49448
|
* (`opencode.json` / `opencode.jsonc`) under the `instructions` key.
|
|
49193
49449
|
*
|
|
@@ -54907,7 +55163,8 @@ var McpProcessor = class extends FeatureProcessor {
|
|
|
54907
55163
|
toolTarget;
|
|
54908
55164
|
global;
|
|
54909
55165
|
getFactory;
|
|
54910
|
-
|
|
55166
|
+
featureOptions;
|
|
55167
|
+
constructor({ outputRoot = process.cwd(), inputRoots, toolTarget, global = false, getFactory = defaultGetFactory$3, dryRun = false, logger, featureOptions }) {
|
|
54911
55168
|
super({
|
|
54912
55169
|
outputRoot,
|
|
54913
55170
|
inputRoots,
|
|
@@ -54919,6 +55176,7 @@ var McpProcessor = class extends FeatureProcessor {
|
|
|
54919
55176
|
this.toolTarget = result.data;
|
|
54920
55177
|
this.global = global;
|
|
54921
55178
|
this.getFactory = getFactory;
|
|
55179
|
+
this.featureOptions = featureOptions;
|
|
54922
55180
|
}
|
|
54923
55181
|
/**
|
|
54924
55182
|
* Implementation of abstract method from FeatureProcessor
|
|
@@ -55000,7 +55258,8 @@ var McpProcessor = class extends FeatureProcessor {
|
|
|
55000
55258
|
outputRoot: this.outputRoot,
|
|
55001
55259
|
rulesyncMcp: filteredRulesyncMcp,
|
|
55002
55260
|
global: this.global,
|
|
55003
|
-
logger: this.logger
|
|
55261
|
+
logger: this.logger,
|
|
55262
|
+
featureOptions: this.featureOptions
|
|
55004
55263
|
});
|
|
55005
55264
|
}));
|
|
55006
55265
|
const auxiliaryFiles = await factory.class.getAuxiliaryFiles?.({
|
|
@@ -57781,12 +58040,14 @@ var CodewhaleSkill = class CodewhaleSkill extends ToolSkill {
|
|
|
57781
58040
|
const CodexCliSkillFrontmatterSchema = zod_mini.z.looseObject({
|
|
57782
58041
|
name: zod_mini.z.string(),
|
|
57783
58042
|
description: zod_mini.z.string(),
|
|
57784
|
-
metadata: zod_mini.z.optional(zod_mini.z.looseObject({ "short-description": zod_mini.z.optional(zod_mini.z.string()) }))
|
|
58043
|
+
metadata: zod_mini.z.optional(zod_mini.z.looseObject({ "short-description": zod_mini.z.optional(zod_mini.z.string()) })),
|
|
58044
|
+
"disable-model-invocation": zod_mini.z.optional(zod_mini.z.boolean())
|
|
57785
58045
|
});
|
|
57786
58046
|
/**
|
|
57787
58047
|
* Relative path (within a skill directory) of the Codex `agents/openai.yaml` sidecar.
|
|
57788
58048
|
* Codex CLI reads UI metadata, invocation policy, and tool dependencies from this file;
|
|
57789
|
-
* `SKILL.md` frontmatter only
|
|
58049
|
+
* from `SKILL.md` frontmatter it reads only `name`, `description` and
|
|
58050
|
+
* `metadata.short-description`, ignoring any other key.
|
|
57790
58051
|
* @see https://developers.openai.com/codex/skills.md
|
|
57791
58052
|
*/
|
|
57792
58053
|
const CODEX_OPENAI_YAML_RELATIVE_PATH = CODEXCLI_OPENAI_YAML_RELATIVE_PATH;
|
|
@@ -57876,6 +58137,40 @@ function openaiYamlToCodexcliSection(parsed) {
|
|
|
57876
58137
|
return Object.keys(section).length > 0 ? section : void 0;
|
|
57877
58138
|
}
|
|
57878
58139
|
/**
|
|
58140
|
+
* Build the `SKILL.md` `metadata` map: the root-level rulesync `metadata` with
|
|
58141
|
+
* the legacy `codexcli.short-description` merged in. The tool-specific value wins
|
|
58142
|
+
* over a root `metadata.short-description`, as a target section overrides the
|
|
58143
|
+
* root default everywhere else. Values are stringified because the Agent Skills
|
|
58144
|
+
* spec types `metadata` as a string-to-string map, and `.agents/skills/` is read
|
|
58145
|
+
* by other spec clients too; Codex itself deserializes `short-description` as a
|
|
58146
|
+
* string.
|
|
58147
|
+
* @see https://agentskills.io/specification
|
|
58148
|
+
*/
|
|
58149
|
+
function buildSkillMetadata({ rootMetadata, shortDescription }) {
|
|
58150
|
+
const merged = {
|
|
58151
|
+
...rootMetadata,
|
|
58152
|
+
...shortDescription !== void 0 && { "short-description": shortDescription }
|
|
58153
|
+
};
|
|
58154
|
+
return Object.keys(merged).length > 0 ? toStringMetadata(merged) : void 0;
|
|
58155
|
+
}
|
|
58156
|
+
/**
|
|
58157
|
+
* Drop `policy.allow_implicit_invocation: false` from an imported sidecar section
|
|
58158
|
+
* when the `SKILL.md` frontmatter already carries `disable-model-invocation: true`,
|
|
58159
|
+
* since the root flag regenerates exactly that value. Keeping both would store the
|
|
58160
|
+
* same setting twice in `.rulesync/skills/`.
|
|
58161
|
+
*/
|
|
58162
|
+
function dropPolicyImpliedByRootFlag(section, disableModelInvocation) {
|
|
58163
|
+
const policy = section?.policy;
|
|
58164
|
+
if (!section || disableModelInvocation !== true || policy === null || typeof policy !== "object" || Array.isArray(policy) || policy.allow_implicit_invocation !== false) return section;
|
|
58165
|
+
const { allow_implicit_invocation: _implied, ...restPolicy } = policy;
|
|
58166
|
+
const { policy: _policy, ...restSection } = section;
|
|
58167
|
+
const result = {
|
|
58168
|
+
...restSection,
|
|
58169
|
+
...Object.keys(restPolicy).length > 0 && { policy: restPolicy }
|
|
58170
|
+
};
|
|
58171
|
+
return Object.keys(result).length > 0 ? result : void 0;
|
|
58172
|
+
}
|
|
58173
|
+
/**
|
|
57879
58174
|
* Represents a Codex CLI skill directory.
|
|
57880
58175
|
* Codex CLI supports skills in both project mode (under $CWD/.agents/skills)
|
|
57881
58176
|
* and global mode (under ~/.agents/skills — Rulesync resolves the home
|
|
@@ -57926,16 +58221,20 @@ var CodexCliSkill = class CodexCliSkill extends ToolSkill {
|
|
|
57926
58221
|
}
|
|
57927
58222
|
toRulesyncSkill() {
|
|
57928
58223
|
const frontmatter = this.getFrontmatter();
|
|
58224
|
+
const disableModelInvocation = frontmatter["disable-model-invocation"];
|
|
57929
58225
|
const { parsed, rest } = extractOpenaiYamlFile(this.getOtherFiles());
|
|
57930
|
-
const openaiSection = openaiYamlToCodexcliSection(parsed);
|
|
58226
|
+
const openaiSection = dropPolicyImpliedByRootFlag(openaiYamlToCodexcliSection(parsed), disableModelInvocation);
|
|
58227
|
+
const { "short-description": shortDescription, ...rootMetadata } = frontmatter.metadata ?? {};
|
|
57931
58228
|
const codexcliSection = {
|
|
57932
|
-
...
|
|
58229
|
+
...shortDescription !== void 0 && { "short-description": shortDescription },
|
|
57933
58230
|
...openaiSection
|
|
57934
58231
|
};
|
|
57935
58232
|
const rulesyncFrontmatter = {
|
|
57936
58233
|
name: frontmatter.name,
|
|
57937
58234
|
description: frontmatter.description,
|
|
57938
58235
|
targets: ["*"],
|
|
58236
|
+
...disableModelInvocation !== void 0 && { "disable-model-invocation": disableModelInvocation },
|
|
58237
|
+
...Object.keys(rootMetadata).length > 0 && { metadata: rootMetadata },
|
|
57939
58238
|
...Object.keys(codexcliSection).length > 0 && { codexcli: codexcliSection }
|
|
57940
58239
|
};
|
|
57941
58240
|
return new RulesyncSkill({
|
|
@@ -57952,10 +58251,16 @@ var CodexCliSkill = class CodexCliSkill extends ToolSkill {
|
|
|
57952
58251
|
static fromRulesyncSkill({ outputRoot = process.cwd(), rulesyncSkill, validate = true, global = false }) {
|
|
57953
58252
|
const settablePaths = CodexCliSkill.getSettablePaths({ global });
|
|
57954
58253
|
const rulesyncFrontmatter = rulesyncSkill.getFrontmatter();
|
|
58254
|
+
const metadata = buildSkillMetadata({
|
|
58255
|
+
rootMetadata: rulesyncFrontmatter.metadata,
|
|
58256
|
+
shortDescription: rulesyncFrontmatter.codexcli?.["short-description"]
|
|
58257
|
+
});
|
|
58258
|
+
const disableModelInvocation = rulesyncFrontmatter["disable-model-invocation"];
|
|
57955
58259
|
const codexFrontmatter = {
|
|
57956
58260
|
name: rulesyncFrontmatter.name,
|
|
57957
58261
|
description: rulesyncFrontmatter.description,
|
|
57958
|
-
...
|
|
58262
|
+
...disableModelInvocation !== void 0 && { "disable-model-invocation": disableModelInvocation },
|
|
58263
|
+
...metadata !== void 0 && { metadata }
|
|
57959
58264
|
};
|
|
57960
58265
|
const target = toPosixPath(CODEX_OPENAI_YAML_RELATIVE_PATH);
|
|
57961
58266
|
const baseOtherFiles = rulesyncSkill.getOtherFiles().filter((file) => toPosixPath(file.relativeFilePathToDirPath) !== target);
|
|
@@ -67443,6 +67748,16 @@ var CopilotSubagent = class CopilotSubagent extends ToolSubagent {
|
|
|
67443
67748
|
* (`preferred` lets a `subagents` settings override or the `/subagents`
|
|
67444
67749
|
* picker replace the model; `required` locks dispatch to the authored
|
|
67445
67750
|
* list). CLI v1.0.81+; the shared reference page still lists `model` only.
|
|
67751
|
+
* - `model` may also be a list of models in preference order. The Copilot
|
|
67752
|
+
* CLI changelog for v1.0.83 (2026-09-04) says "Custom agents can list
|
|
67753
|
+
* several models in `model`, tried in order until one is available to
|
|
67754
|
+
* you"; the CLI command reference and the shared configuration reference
|
|
67755
|
+
* still document `model` as a string, so older CLI versions may not read
|
|
67756
|
+
* the list form (a string is the safe form there). The VS Code Copilot
|
|
67757
|
+
* harness documents the same array spelling. Preserve a list as authored
|
|
67758
|
+
* instead of rewriting it to the separate `models` field.
|
|
67759
|
+
* https://github.com/github/copilot-cli/blob/main/changelog.md#1083---2026-09-04
|
|
67760
|
+
* https://github.com/microsoft/vscode-docs/blob/main/docs/agents/reference/custom-agent-file-reference.md
|
|
67446
67761
|
*
|
|
67447
67762
|
* `looseObject` is used so that future additional fields are passed through
|
|
67448
67763
|
* unchanged (consistent with the rest of rulesync's frontmatter schemas).
|
|
@@ -67452,7 +67767,7 @@ const CopilotCliSubagentFrontmatterSchema = zod_mini.z.looseObject({
|
|
|
67452
67767
|
name: zod_mini.z.optional(zod_mini.z.string()),
|
|
67453
67768
|
target: zod_mini.z.optional(zod_mini.z.string()),
|
|
67454
67769
|
tools: zod_mini.z.optional(zod_mini.z.union([zod_mini.z.string(), zod_mini.z.array(zod_mini.z.string())])),
|
|
67455
|
-
model: zod_mini.z.optional(zod_mini.z.string()),
|
|
67770
|
+
model: zod_mini.z.optional(zod_mini.z.union([zod_mini.z.string(), zod_mini.z.array(zod_mini.z.string())])),
|
|
67456
67771
|
models: zod_mini.z.optional(zod_mini.z.array(zod_mini.z.string())),
|
|
67457
67772
|
modelPolicy: zod_mini.z.optional(zod_mini.z.enum(["preferred", "required"])),
|
|
67458
67773
|
"disable-model-invocation": zod_mini.z.optional(zod_mini.z.boolean()),
|
|
@@ -107241,7 +107556,8 @@ function buildMcpStrategy(ctx) {
|
|
|
107241
107556
|
toolTarget,
|
|
107242
107557
|
global,
|
|
107243
107558
|
dryRun,
|
|
107244
|
-
logger
|
|
107559
|
+
logger,
|
|
107560
|
+
featureOptions: config.getFeatureOptions(toolTarget, "mcp")
|
|
107245
107561
|
}),
|
|
107246
107562
|
loadSource: (p) => p.loadToolFiles(),
|
|
107247
107563
|
toRulesync: (p, files) => p.convertToolFilesToRulesyncFiles(files),
|
|
@@ -108971,7 +109287,8 @@ async function generateMcpCore(params) {
|
|
|
108971
109287
|
toolTarget,
|
|
108972
109288
|
global: config.getGlobal(),
|
|
108973
109289
|
dryRun: config.isPreviewMode(),
|
|
108974
|
-
logger
|
|
109290
|
+
logger,
|
|
109291
|
+
featureOptions: config.getFeatureOptions(toolTarget, "mcp")
|
|
108975
109292
|
});
|
|
108976
109293
|
const result = await processFeatureWithRulesyncFiles({
|
|
108977
109294
|
config,
|