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
|
@@ -23474,6 +23474,62 @@ function findOpencodeV2McpContainers(mcp) {
|
|
|
23474
23474
|
return [...envelope !== null && Object.keys(envelope).length > 0 ? ["mcp.servers"] : [], ...isOpencodeV2GlobalTimeout(mcp.timeout) && Object.keys(mcp.timeout).length > 0 ? ["mcp.timeout"] : []];
|
|
23475
23475
|
}
|
|
23476
23476
|
/**
|
|
23477
|
+
* Returns the non-empty V2 global `mcp.timeout` object of an OpenCode `mcp`
|
|
23478
|
+
* value, or `undefined` when there is none.
|
|
23479
|
+
*/
|
|
23480
|
+
function getOpencodeV2GlobalMcpTimeout(mcp) {
|
|
23481
|
+
return isRecord$1(mcp) && findOpencodeV2McpContainers(mcp).includes("mcp.timeout") ? mcp.timeout : void 0;
|
|
23482
|
+
}
|
|
23483
|
+
/**
|
|
23484
|
+
* Returns the raw entry an OpenCode `mcp` value holds for a server: the one
|
|
23485
|
+
* under the V2 `mcp.servers` envelope, or else the flat V1 entry.
|
|
23486
|
+
*/
|
|
23487
|
+
function getOpencodeMcpServerEntry(mcp, name) {
|
|
23488
|
+
if (!isRecord$1(mcp)) return;
|
|
23489
|
+
const envelope = getOpencodeV2McpEnvelope(mcp);
|
|
23490
|
+
if (envelope !== null && Object.hasOwn(envelope, name)) return envelope[name];
|
|
23491
|
+
return Object.hasOwn(mcp, name) ? mcp[name] : void 0;
|
|
23492
|
+
}
|
|
23493
|
+
const OPENCODE_V1_TO_V2_OAUTH_KEYS = new Map([...OPENCODE_V2_OAUTH_KEYS].map(([v2, v1]) => [v1, v2]));
|
|
23494
|
+
/**
|
|
23495
|
+
* Raises one V1-shaped OpenCode MCP server (`local` or `remote`) to the V2
|
|
23496
|
+
* spelling — the inverse of {@link lowerOpencodeV2Server}:
|
|
23497
|
+
*
|
|
23498
|
+
* - `enabled` is dropped and `disabled: true` written only for a disabled server;
|
|
23499
|
+
* - a number `timeout` becomes `{ catalog, execution }`, and camelCase `oauth`
|
|
23500
|
+
* keys become snake_case.
|
|
23501
|
+
*
|
|
23502
|
+
* Values Rulesync's canonical form cannot hold are taken from the entry the
|
|
23503
|
+
* file already has for the server (`existing`): its `timeout` object and its
|
|
23504
|
+
* `oauth` object are kept verbatim whenever lowering them gives back exactly
|
|
23505
|
+
* what the canonical server carries, so a V2 file survives import and
|
|
23506
|
+
* generate unchanged. When the canonical `timeout` differs, the existing
|
|
23507
|
+
* `startup` value is still kept.
|
|
23508
|
+
*/
|
|
23509
|
+
function raiseOpencodeServerToV2(server, existing) {
|
|
23510
|
+
const { enabled, timeout, oauth, ...rest } = server;
|
|
23511
|
+
const raised = { ...rest };
|
|
23512
|
+
if (enabled === false) raised.disabled = true;
|
|
23513
|
+
const existingTimeout = isRecord$1(existing) && isRecord$1(existing.timeout) ? existing.timeout : null;
|
|
23514
|
+
if (existingTimeout !== null && lowerOpencodeV2Timeout(existingTimeout) === timeout) raised.timeout = existingTimeout;
|
|
23515
|
+
else if (typeof timeout === "number") {
|
|
23516
|
+
const startup = existingTimeout?.startup;
|
|
23517
|
+
raised.timeout = {
|
|
23518
|
+
...startup !== void 0 && { startup },
|
|
23519
|
+
catalog: timeout,
|
|
23520
|
+
execution: timeout
|
|
23521
|
+
};
|
|
23522
|
+
} else if (timeout !== void 0) raised.timeout = timeout;
|
|
23523
|
+
const existingOauth = isRecord$1(existing) && isRecord$1(existing.oauth) ? existing.oauth : null;
|
|
23524
|
+
if (existingOauth !== null && isRecord$1(oauth) && isDeepStrictEqual(lowerOpencodeV2OAuth(existingOauth, ""), oauth)) raised.oauth = existingOauth;
|
|
23525
|
+
else if (isRecord$1(oauth)) {
|
|
23526
|
+
const snakeOauth = {};
|
|
23527
|
+
for (const [key, value] of Object.entries(oauth)) setOwnEntry(snakeOauth, OPENCODE_V1_TO_V2_OAUTH_KEYS.get(key) ?? key, value);
|
|
23528
|
+
raised.oauth = snakeOauth;
|
|
23529
|
+
} else if (oauth !== void 0) raised.oauth = oauth;
|
|
23530
|
+
return raised;
|
|
23531
|
+
}
|
|
23532
|
+
/**
|
|
23477
23533
|
* Lowers OpenCode V2 `mcp` spellings found in a V1 config to the flat V1 map,
|
|
23478
23534
|
* the way OpenCode V1 itself does since v1.18.24 (`normalizeMcp`):
|
|
23479
23535
|
*
|
|
@@ -33935,6 +33991,69 @@ var JunieHooks = class JunieHooks extends ToolHooks {
|
|
|
33935
33991
|
}
|
|
33936
33992
|
};
|
|
33937
33993
|
//#endregion
|
|
33994
|
+
//#region src/features/hooks/hook-block-reason.ts
|
|
33995
|
+
/**
|
|
33996
|
+
* Source lines of a `sanitizeBlockReason(raw)` helper, emitted into generated
|
|
33997
|
+
* hook plugins that hand a hook command's output to the agent as the reason a
|
|
33998
|
+
* call was blocked (the Pi / oh-my-pi extension and the OpenCode-style plugin).
|
|
33999
|
+
* It returns the sanitized, length-capped text, or `""` when nothing survives
|
|
34000
|
+
* so the caller can substitute its own fallback.
|
|
34001
|
+
*
|
|
34002
|
+
* The text is sanitized because a hook command's output can relay third-party
|
|
34003
|
+
* content (linter output, matched file lines) into a terminal or, in RPC mode,
|
|
34004
|
+
* into an external client. Escape sequences, C1 and other control characters,
|
|
34005
|
+
* and format characters such as bidirectional overrides and zero-width joiners
|
|
34006
|
+
* are stripped so the reason cannot repaint or reorder what the user is shown,
|
|
34007
|
+
* and carriage returns are folded into newlines so it cannot overwrite an
|
|
34008
|
+
* already printed line. The format-character pass is a superset of the bidi
|
|
34009
|
+
* controls listed in `CONTROL_CHARACTERS_PATTERN`
|
|
34010
|
+
* (`src/utils/control-characters.ts`): a blocked-hook reason is short
|
|
34011
|
+
* diagnostic text, so dropping every `Cf` character is safer than enumerating
|
|
34012
|
+
* the harmful ones and missing `U+061C`, `U+FEFF`, or a future addition.
|
|
34013
|
+
*
|
|
34014
|
+
* Sanitization runs before truncation so a reason that begins with a progress
|
|
34015
|
+
* banner is still reported by its content. The raw text is only sliced to a
|
|
34016
|
+
* generous backstop first: a hook command may emit a large amount of output,
|
|
34017
|
+
* and the slice bounds the work even if a later edit reintroduces a pattern
|
|
34018
|
+
* that is not linear. A slice that actually fired is reported as truncated, so
|
|
34019
|
+
* the text is never silently cut.
|
|
34020
|
+
*
|
|
34021
|
+
* Both slices cut on UTF-16 code units, so each drops a trailing lone high
|
|
34022
|
+
* surrogate: the reason may travel as JSON, and half a surrogate pair is not
|
|
34023
|
+
* text a strict consumer has to accept.
|
|
34024
|
+
*
|
|
34025
|
+
* `typed` emits TypeScript parameter and return annotations (for the Pi
|
|
34026
|
+
* extension, a `.ts` module); without it the lines are plain JavaScript.
|
|
34027
|
+
*/
|
|
34028
|
+
function buildBlockReasonSanitizerLines({ typed }) {
|
|
34029
|
+
const str = typed ? ": string" : "";
|
|
34030
|
+
return [
|
|
34031
|
+
"const MAX_BLOCK_REASON_LENGTH = 2000;",
|
|
34032
|
+
"const MAX_SCANNED_REASON_LENGTH = 128_000;",
|
|
34033
|
+
"",
|
|
34034
|
+
`function dropTrailingLoneSurrogate(text${str})${str} {`,
|
|
34035
|
+
" return text.replace(/[\\ud800-\\udbff]$/, \"\");",
|
|
34036
|
+
"}",
|
|
34037
|
+
"",
|
|
34038
|
+
`function sanitizeBlockReason(raw${str})${str} {`,
|
|
34039
|
+
" const scannedTruncated = raw.length > MAX_SCANNED_REASON_LENGTH;",
|
|
34040
|
+
" const scanned = scannedTruncated",
|
|
34041
|
+
" ? dropTrailingLoneSurrogate(raw.slice(0, MAX_SCANNED_REASON_LENGTH))",
|
|
34042
|
+
" : raw;",
|
|
34043
|
+
" const sanitized = scanned",
|
|
34044
|
+
" .replace(/\\r\\n?/g, \"\\n\")",
|
|
34045
|
+
" .replace(/\\u001b\\[[0-9;?]*[\\u0020-\\u002f]*[\\u0040-\\u007e]/g, \"\")",
|
|
34046
|
+
" .replace(/\\u001b\\][^\\u0007\\u001b]{0,256}(?:\\u0007|\\u001b\\\\)/g, \"\")",
|
|
34047
|
+
" .replace(/[\\u0000-\\u0008\\u000b\\u000c\\u000e-\\u001f\\u007f-\\u009f]/g, \"\")",
|
|
34048
|
+
" .replace(/[\\p{Cf}\\p{Zl}\\p{Zp}]/gu, \"\")",
|
|
34049
|
+
" .trim();",
|
|
34050
|
+
" if (!sanitized) return \"\";",
|
|
34051
|
+
" if (sanitized.length <= MAX_BLOCK_REASON_LENGTH && !scannedTruncated) return sanitized;",
|
|
34052
|
+
" return `${dropTrailingLoneSurrogate(sanitized.slice(0, MAX_BLOCK_REASON_LENGTH))}...`;",
|
|
34053
|
+
"}"
|
|
34054
|
+
];
|
|
34055
|
+
}
|
|
34056
|
+
//#endregion
|
|
33938
34057
|
//#region src/features/hooks/opencode-style-generator.ts
|
|
33939
34058
|
/**
|
|
33940
34059
|
* Tool events emitted as named `(input, ...)` hooks rather than through the
|
|
@@ -34005,6 +34124,87 @@ const SHELL_EVENT_TOOL_GATES = {
|
|
|
34005
34124
|
tool: "bash"
|
|
34006
34125
|
}
|
|
34007
34126
|
};
|
|
34127
|
+
/**
|
|
34128
|
+
* Canonical events whose command receives a Claude Code-style JSON payload on
|
|
34129
|
+
* stdin and whose result can deny the call, mapped to the `hook_event_name`
|
|
34130
|
+
* the payload carries. Only `preToolUse` (`tool.execute.before`) takes part:
|
|
34131
|
+
* it is the one named hook where throwing aborts the call — OpenCode rejects
|
|
34132
|
+
* the tool call and returns the error message to the model (the `.env`
|
|
34133
|
+
* protection example in the plugins doc).
|
|
34134
|
+
*
|
|
34135
|
+
* The payload mirrors Claude Code's `PreToolUse` input: `hook_event_name`,
|
|
34136
|
+
* `session_id` (`input.sessionID`), `tool_use_id` (`input.callID`),
|
|
34137
|
+
* `tool_name` (the raw OpenCode tool name — `bash`, `edit`, `<server>_<tool>`
|
|
34138
|
+
* for MCP — the same subject a `matcher` is tested against), `tool_input`
|
|
34139
|
+
* (`output.args`) and `cwd` (the plugin context's `directory`, falling back to
|
|
34140
|
+
* `process.cwd()`). It is serialized lazily, on the first handler whose
|
|
34141
|
+
* matcher matches, so a tool call no hook applies to pays nothing for it.
|
|
34142
|
+
*
|
|
34143
|
+
* Bun Shell has no stdin option on a command and rejects redirections on a
|
|
34144
|
+
* subshell, so the payload is piped in by the shell's builtin `echo`
|
|
34145
|
+
* (`echo ${payload} | <command>`); Bun escapes the interpolated value, so the
|
|
34146
|
+
* payload never reaches the shell as syntax. The pipe feeds the first
|
|
34147
|
+
* pipeline of the command — a compound `a && b` or `a; b` gives the payload
|
|
34148
|
+
* to `a` only, so multi-step guards belong in a script.
|
|
34149
|
+
*
|
|
34150
|
+
* @see https://opencode.ai/docs/plugins/
|
|
34151
|
+
* @see https://bun.sh/docs/runtime/shell
|
|
34152
|
+
* @see https://code.claude.com/docs/en/hooks
|
|
34153
|
+
*/
|
|
34154
|
+
const STDIN_PAYLOAD_EVENTS = { preToolUse: "PreToolUse" };
|
|
34155
|
+
/**
|
|
34156
|
+
* Name of the helper the generated plugin defines when a `preToolUse` handler
|
|
34157
|
+
* is present. It interprets the command's result like Claude Code does for a
|
|
34158
|
+
* `PreToolUse` hook, throwing (which aborts the OpenCode tool call) when the
|
|
34159
|
+
* hook denies it:
|
|
34160
|
+
* - exit code 2 → deny, with the command's stderr as the reason;
|
|
34161
|
+
* - exit code 0 with JSON stdout whose `hookSpecificOutput.permissionDecision`
|
|
34162
|
+
* is `"deny"` → deny, with `permissionDecisionReason` as the reason;
|
|
34163
|
+
* - exit code 0 otherwise → allow;
|
|
34164
|
+
* - any other non-zero exit → deny too, with stderr or the exit code.
|
|
34165
|
+
*
|
|
34166
|
+
* Every reason goes through the `sanitizeBlockReason` helper shared with the
|
|
34167
|
+
* Pi extension (`hook-block-reason.ts`): control and format characters are
|
|
34168
|
+
* stripped and the text is capped at 2000 characters before the model sees it.
|
|
34169
|
+
*
|
|
34170
|
+
* The other non-zero exit case is deliberate. Claude Code treats that as a
|
|
34171
|
+
* non-blocking error, but before this payload contract existed the plugin
|
|
34172
|
+
* ran the command through Bun Shell's default throwing mode, so every
|
|
34173
|
+
* non-zero exit aborted the call. Keeping that preserves guards that block
|
|
34174
|
+
* with `exit 1`, rather than silently failing them open.
|
|
34175
|
+
*/
|
|
34176
|
+
const PRE_TOOL_USE_RESULT_HELPER = "__rulesyncEnforcePreToolUseResult";
|
|
34177
|
+
const PRE_TOOL_USE_RESULT_HELPER_LINES = [
|
|
34178
|
+
...buildBlockReasonSanitizerLines({ typed: false }),
|
|
34179
|
+
"",
|
|
34180
|
+
`const ${PRE_TOOL_USE_RESULT_HELPER} = (result) => {`,
|
|
34181
|
+
" const stderr = result.stderr.toString();",
|
|
34182
|
+
" if (result.exitCode === 0) {",
|
|
34183
|
+
" let parsed;",
|
|
34184
|
+
" try {",
|
|
34185
|
+
" parsed = JSON.parse(result.stdout.toString());",
|
|
34186
|
+
" } catch {",
|
|
34187
|
+
" return;",
|
|
34188
|
+
" }",
|
|
34189
|
+
" const specific = parsed && typeof parsed === \"object\" ? parsed.hookSpecificOutput : undefined;",
|
|
34190
|
+
" if (specific && typeof specific === \"object\" && specific.permissionDecision === \"deny\") {",
|
|
34191
|
+
" throw new Error(",
|
|
34192
|
+
" (typeof specific.permissionDecisionReason === \"string\" &&",
|
|
34193
|
+
" sanitizeBlockReason(specific.permissionDecisionReason)) ||",
|
|
34194
|
+
" \"Blocked by a preToolUse hook.\",",
|
|
34195
|
+
" );",
|
|
34196
|
+
" }",
|
|
34197
|
+
" return;",
|
|
34198
|
+
" }",
|
|
34199
|
+
" if (result.exitCode === 2) {",
|
|
34200
|
+
" throw new Error(sanitizeBlockReason(stderr) || \"Blocked by a preToolUse hook.\");",
|
|
34201
|
+
" }",
|
|
34202
|
+
" throw new Error(",
|
|
34203
|
+
" sanitizeBlockReason(stderr) ||",
|
|
34204
|
+
" \"preToolUse hook command failed with exit code \" + result.exitCode + \".\",",
|
|
34205
|
+
" );",
|
|
34206
|
+
"};"
|
|
34207
|
+
];
|
|
34008
34208
|
function escapeForTemplateLiteral(command) {
|
|
34009
34209
|
return command.replace(/\\/g, "\\\\").replace(/`/g, "\\`").replace(/\$\{/g, "\\${");
|
|
34010
34210
|
}
|
|
@@ -34019,6 +34219,17 @@ function validateAndSanitizeMatcher(matcher) {
|
|
|
34019
34219
|
}
|
|
34020
34220
|
return sanitized.replace(/\\/g, "\\\\").replace(/"/g, "\\\"");
|
|
34021
34221
|
}
|
|
34222
|
+
/** The per-canonical-event gates and payload flag every handler of it carries. */
|
|
34223
|
+
function handlerExtrasFor(canonicalEvent) {
|
|
34224
|
+
const shellGate = SHELL_EVENT_TOOL_GATES[canonicalEvent];
|
|
34225
|
+
const propertyGate = GENERIC_EVENT_PROPERTY_GATES[canonicalEvent];
|
|
34226
|
+
const stdinEvent = shellGate ? void 0 : STDIN_PAYLOAD_EVENTS[canonicalEvent];
|
|
34227
|
+
return {
|
|
34228
|
+
...shellGate ? { toolGate: shellGate.tool } : {},
|
|
34229
|
+
...propertyGate ? { propertyGate } : {},
|
|
34230
|
+
...stdinEvent ? { stdinEvent } : {}
|
|
34231
|
+
};
|
|
34232
|
+
}
|
|
34022
34233
|
/**
|
|
34023
34234
|
* Group the effective hooks into named (tool.execute.*) and generic event
|
|
34024
34235
|
* handler groups, keyed by tool event name. Mutates the supplied groups.
|
|
@@ -34026,7 +34237,7 @@ function validateAndSanitizeMatcher(matcher) {
|
|
|
34026
34237
|
function collectOpencodeStyleHandlers({ effectiveHooks, eventMap, namedEventHandlers, genericEventHandlers }) {
|
|
34027
34238
|
for (const [canonicalEvent, definitions] of Object.entries(effectiveHooks)) {
|
|
34028
34239
|
const shellGate = SHELL_EVENT_TOOL_GATES[canonicalEvent];
|
|
34029
|
-
const
|
|
34240
|
+
const extras = handlerExtrasFor(canonicalEvent);
|
|
34030
34241
|
const toolEvent = shellGate?.toolEvent ?? eventMap[canonicalEvent];
|
|
34031
34242
|
if (!toolEvent) continue;
|
|
34032
34243
|
const matcherSupported = !shellGate && Object.hasOwn(NAMED_HOOK_MATCHER_SUBJECTS, toolEvent) && NAMED_HOOK_MATCHER_SUBJECTS[toolEvent] !== null;
|
|
@@ -34038,8 +34249,7 @@ function collectOpencodeStyleHandlers({ effectiveHooks, eventMap, namedEventHand
|
|
|
34038
34249
|
handlers.push({
|
|
34039
34250
|
command: def.command,
|
|
34040
34251
|
matcher: def.matcher ? def.matcher : void 0,
|
|
34041
|
-
...
|
|
34042
|
-
...propertyGate ? { propertyGate } : {}
|
|
34252
|
+
...extras
|
|
34043
34253
|
});
|
|
34044
34254
|
}
|
|
34045
34255
|
if (handlers.length > 0) {
|
|
@@ -34072,46 +34282,79 @@ function buildGenericEventBodyLines(genericEventHandlers) {
|
|
|
34072
34282
|
bodyLines.push(" },");
|
|
34073
34283
|
return bodyLines;
|
|
34074
34284
|
}
|
|
34285
|
+
function stdinPayloadVariable(stdinEvent) {
|
|
34286
|
+
return `__rulesync${stdinEvent}Input`;
|
|
34287
|
+
}
|
|
34288
|
+
/**
|
|
34289
|
+
* The statement that runs a handler's command. A handler with a stdin payload
|
|
34290
|
+
* pipes the payload in and hands the result to the deny-aware helper; every
|
|
34291
|
+
* other handler keeps Bun Shell's default mode, where a non-zero exit throws.
|
|
34292
|
+
*/
|
|
34293
|
+
function buildRunStatement(handler) {
|
|
34294
|
+
const escapedCommand = escapeForTemplateLiteral(handler.command);
|
|
34295
|
+
if (handler.stdinEvent) {
|
|
34296
|
+
const payload = stdinPayloadVariable(handler.stdinEvent);
|
|
34297
|
+
return `${PRE_TOOL_USE_RESULT_HELPER}(await $\`echo \${${payload}()} | ${escapedCommand}\`.nothrow().quiet());`;
|
|
34298
|
+
}
|
|
34299
|
+
return `await $\`${escapedCommand}\`;`;
|
|
34300
|
+
}
|
|
34075
34301
|
/** Emit the named (`tool.execute.*`) handler blocks. */
|
|
34076
34302
|
function buildNamedEventBodyLines(namedEventHandlers) {
|
|
34077
34303
|
const bodyLines = [];
|
|
34078
34304
|
for (const [eventName, handlers] of Object.entries(namedEventHandlers)) {
|
|
34079
34305
|
const matcherSubject = NAMED_HOOK_MATCHER_SUBJECTS[eventName] ?? null;
|
|
34080
|
-
|
|
34306
|
+
const stdinEvents = [...new Set(handlers.flatMap((handler) => handler.stdinEvent ? [handler.stdinEvent] : []))];
|
|
34307
|
+
bodyLines.push(` "${eventName}": async (${stdinEvents.length > 0 ? "input, output" : "input"}) => {`);
|
|
34308
|
+
for (const stdinEvent of stdinEvents) {
|
|
34309
|
+
const payload = stdinPayloadVariable(stdinEvent);
|
|
34310
|
+
bodyLines.push(` let ${payload}Cache;`);
|
|
34311
|
+
bodyLines.push(` const ${payload} = () =>`);
|
|
34312
|
+
bodyLines.push(` (${payload}Cache ??= JSON.stringify({`);
|
|
34313
|
+
bodyLines.push(` hook_event_name: "${stdinEvent}",`);
|
|
34314
|
+
bodyLines.push(" session_id: input.sessionID,");
|
|
34315
|
+
bodyLines.push(" tool_use_id: input.callID,");
|
|
34316
|
+
bodyLines.push(" tool_name: input.tool,");
|
|
34317
|
+
bodyLines.push(" tool_input: output?.args,");
|
|
34318
|
+
bodyLines.push(" cwd: directory ?? process.cwd(),");
|
|
34319
|
+
bodyLines.push(" }));");
|
|
34320
|
+
}
|
|
34081
34321
|
for (const handler of handlers) {
|
|
34082
|
-
const
|
|
34322
|
+
const runStatement = buildRunStatement(handler);
|
|
34083
34323
|
if (handler.toolGate) {
|
|
34084
34324
|
bodyLines.push(` if (input.tool === "${handler.toolGate}") {`);
|
|
34085
|
-
bodyLines.push(`
|
|
34325
|
+
bodyLines.push(` ${runStatement}`);
|
|
34086
34326
|
bodyLines.push(" }");
|
|
34087
34327
|
} else if (handler.matcher && matcherSubject !== null) {
|
|
34088
34328
|
const safeMatcher = validateAndSanitizeMatcher(handler.matcher);
|
|
34089
34329
|
bodyLines.push(" {");
|
|
34090
34330
|
bodyLines.push(` const __re = new RegExp("${safeMatcher}");`);
|
|
34091
34331
|
bodyLines.push(` if (__re.test(${matcherSubject})) {`);
|
|
34092
|
-
bodyLines.push(`
|
|
34332
|
+
bodyLines.push(` ${runStatement}`);
|
|
34093
34333
|
bodyLines.push(" }");
|
|
34094
34334
|
bodyLines.push(" }");
|
|
34095
|
-
} else bodyLines.push(`
|
|
34335
|
+
} else bodyLines.push(` ${runStatement}`);
|
|
34096
34336
|
}
|
|
34097
34337
|
bodyLines.push(" },");
|
|
34098
34338
|
}
|
|
34099
34339
|
return bodyLines;
|
|
34100
34340
|
}
|
|
34101
34341
|
/** Wrap the handler body lines in the requested export shape. */
|
|
34102
|
-
function wrapInExportShape({ bodyLines, exportStyle }) {
|
|
34342
|
+
function wrapInExportShape({ preambleLines, bodyLines, exportStyle }) {
|
|
34343
|
+
const pluginContextPattern = preambleLines.length > 0 ? "{ $, directory }" : "{ $ }";
|
|
34103
34344
|
const lines = [];
|
|
34104
34345
|
if (exportStyle === "default") {
|
|
34105
34346
|
lines.push("export default {");
|
|
34106
34347
|
lines.push(" id: \"rulesync-hooks\",");
|
|
34107
|
-
lines.push(
|
|
34348
|
+
lines.push(` server: async (${pluginContextPattern}) => {`);
|
|
34349
|
+
for (const line of preambleLines) lines.push(line === "" ? "" : ` ${line}`);
|
|
34108
34350
|
lines.push(" return {");
|
|
34109
34351
|
for (const line of bodyLines) lines.push(line === "" ? "" : ` ${line}`);
|
|
34110
34352
|
lines.push(" };");
|
|
34111
34353
|
lines.push(" },");
|
|
34112
34354
|
lines.push("};");
|
|
34113
34355
|
} else {
|
|
34114
|
-
lines.push(
|
|
34356
|
+
lines.push(`export const RulesyncHooksPlugin = async (${pluginContextPattern}) => {`);
|
|
34357
|
+
for (const line of preambleLines) lines.push(line === "" ? "" : ` ${line}`);
|
|
34115
34358
|
lines.push(" return {");
|
|
34116
34359
|
lines.push(...bodyLines);
|
|
34117
34360
|
lines.push(" };");
|
|
@@ -34136,8 +34379,10 @@ function generateOpencodeStylePluginCode(config, supportedEvents, toolConfigKey,
|
|
|
34136
34379
|
namedEventHandlers,
|
|
34137
34380
|
genericEventHandlers
|
|
34138
34381
|
});
|
|
34382
|
+
const bodyLines = [...buildGenericEventBodyLines(genericEventHandlers), ...buildNamedEventBodyLines(namedEventHandlers)];
|
|
34139
34383
|
return wrapInExportShape({
|
|
34140
|
-
|
|
34384
|
+
preambleLines: Object.values(namedEventHandlers).some((handlers) => handlers.some((handler) => handler.stdinEvent !== void 0)) ? PRE_TOOL_USE_RESULT_HELPER_LINES : [],
|
|
34385
|
+
bodyLines,
|
|
34141
34386
|
exportStyle
|
|
34142
34387
|
}).join("\n");
|
|
34143
34388
|
}
|
|
@@ -35282,36 +35527,12 @@ function failureLinesByMode(dialect) {
|
|
|
35282
35527
|
* `PI_SETTLE_BLOCKING_EVENT`): such a failure is rethrown for Pi to report
|
|
35283
35528
|
* instead of continuing the agent.
|
|
35284
35529
|
*
|
|
35285
|
-
* The chosen text
|
|
35286
|
-
*
|
|
35287
|
-
*
|
|
35288
|
-
* characters, and format characters such as bidirectional overrides and
|
|
35289
|
-
* zero-width joiners are stripped so the reason cannot repaint or reorder what
|
|
35290
|
-
* the user is shown, and carriage returns are folded into newlines so it cannot
|
|
35291
|
-
* overwrite an already printed line. The format-character pass is a superset of
|
|
35292
|
-
* the bidi controls listed in `CONTROL_CHARACTERS_PATTERN`
|
|
35293
|
-
* (`src/utils/control-characters.ts`): a blocked-hook reason is short
|
|
35294
|
-
* diagnostic text, so dropping every `Cf` character is safer than enumerating
|
|
35295
|
-
* the harmful ones and missing `U+061C`, `U+FEFF`, or a future addition.
|
|
35296
|
-
*
|
|
35297
|
-
* Sanitization runs before truncation so a reason that begins with a progress
|
|
35298
|
-
* banner is still reported by its content. The raw text is only sliced to a
|
|
35299
|
-
* generous backstop first: a hook command may emit up to `exec`'s full
|
|
35300
|
-
* `maxBuffer`, and the slice bounds the work even if a later edit reintroduces
|
|
35301
|
-
* a pattern that is not linear. A slice that actually fired is reported as
|
|
35302
|
-
* truncated, so the text is never silently cut.
|
|
35303
|
-
*
|
|
35304
|
-
* Both slices cut on UTF-16 code units, so each drops a trailing lone high
|
|
35305
|
-
* surrogate: the reason travels to an RPC client as JSON, and half a surrogate
|
|
35306
|
-
* pair is not text a strict consumer has to accept.
|
|
35530
|
+
* The chosen text goes through `sanitizeBlockReason`, shared with the
|
|
35531
|
+
* OpenCode-style plugin (see `hook-block-reason.ts`): control and format
|
|
35532
|
+
* characters are stripped and the result is capped at 2000 characters.
|
|
35307
35533
|
*/
|
|
35308
35534
|
const BLOCK_REASON_HELPER_LINES = [
|
|
35309
|
-
|
|
35310
|
-
"const MAX_SCANNED_REASON_LENGTH = 128_000;",
|
|
35311
|
-
"",
|
|
35312
|
-
"function dropTrailingLoneSurrogate(text: string): string {",
|
|
35313
|
-
" return text.replace(/[\\ud800-\\udbff]$/, \"\");",
|
|
35314
|
-
"}",
|
|
35535
|
+
...buildBlockReasonSanitizerLines({ typed: true }),
|
|
35315
35536
|
"",
|
|
35316
35537
|
"function toBlockReason(error: unknown): string {",
|
|
35317
35538
|
" const result = error as { stdout?: unknown; stderr?: unknown; code?: unknown } | null;",
|
|
@@ -35325,20 +35546,7 @@ const BLOCK_REASON_HELPER_LINES = [
|
|
|
35325
35546
|
" : error instanceof Error",
|
|
35326
35547
|
" ? error.message",
|
|
35327
35548
|
" : String(error));",
|
|
35328
|
-
"
|
|
35329
|
-
" const scanned = scannedTruncated",
|
|
35330
|
-
" ? dropTrailingLoneSurrogate(raw.slice(0, MAX_SCANNED_REASON_LENGTH))",
|
|
35331
|
-
" : raw;",
|
|
35332
|
-
" const sanitized = scanned",
|
|
35333
|
-
" .replace(/\\r\\n?/g, \"\\n\")",
|
|
35334
|
-
" .replace(/\\u001b\\[[0-9;?]*[\\u0020-\\u002f]*[\\u0040-\\u007e]/g, \"\")",
|
|
35335
|
-
" .replace(/\\u001b\\][^\\u0007\\u001b]{0,256}(?:\\u0007|\\u001b\\\\)/g, \"\")",
|
|
35336
|
-
" .replace(/[\\u0000-\\u0008\\u000b\\u000c\\u000e-\\u001f\\u007f-\\u009f]/g, \"\")",
|
|
35337
|
-
" .replace(/[\\p{Cf}\\p{Zl}\\p{Zp}]/gu, \"\")",
|
|
35338
|
-
" .trim();",
|
|
35339
|
-
" if (!sanitized) return \"Hook command failed.\";",
|
|
35340
|
-
" if (sanitized.length <= MAX_BLOCK_REASON_LENGTH && !scannedTruncated) return sanitized;",
|
|
35341
|
-
" return `${dropTrailingLoneSurrogate(sanitized.slice(0, MAX_BLOCK_REASON_LENGTH))}...`;",
|
|
35549
|
+
" return sanitizeBlockReason(raw) || \"Hook command failed.\";",
|
|
35342
35550
|
"}"
|
|
35343
35551
|
];
|
|
35344
35552
|
/**
|
|
@@ -48957,12 +49165,18 @@ function warnAboutToggleDroppedKeys(serverName, serverConfig, logger) {
|
|
|
48957
49165
|
* - enabledTools/disabledTools -> top-level tools map (with server name prefix)
|
|
48958
49166
|
* - OpenCode-supported extras (timeout, oauth, protocol, codemode) -> passed through verbatim
|
|
48959
49167
|
*/
|
|
48960
|
-
function convertServerToOpencodeFormat(serverName, serverConfig, existingEntry, logger) {
|
|
49168
|
+
function convertServerToOpencodeFormat(serverName, serverConfig, existingEntry, configShape, logger) {
|
|
48961
49169
|
const serverRecord = serverConfig;
|
|
48962
49170
|
const passthrough = {};
|
|
48963
49171
|
for (const key of OPENCODE_PASSTHROUGH_SERVER_FIELDS) if (serverRecord[key] !== void 0) passthrough[key] = serverRecord[key];
|
|
48964
49172
|
const enabled = serverConfig.disabled !== void 0 ? !serverConfig.disabled : true;
|
|
48965
49173
|
if (declaresNoTransport(serverConfig)) {
|
|
49174
|
+
if (configShape === "v2") return warnAndSkipMcpServer({
|
|
49175
|
+
toolName: "OpenCode",
|
|
49176
|
+
serverName,
|
|
49177
|
+
reason: "no transport, and the OpenCode V2 shape (\"configShape\": \"v2\") has no toggle-only entry",
|
|
49178
|
+
logger
|
|
49179
|
+
});
|
|
48966
49180
|
if (serverConfig.disabled === void 0) {
|
|
48967
49181
|
if (existingEntry !== void 0 && !isOpencodeTransportServer(existingEntry)) {
|
|
48968
49182
|
warnAboutToggleDroppedKeys(serverName, serverConfig, logger);
|
|
@@ -49010,11 +49224,11 @@ function convertServerToOpencodeFormat(serverName, serverConfig, existingEntry,
|
|
|
49010
49224
|
...serverConfig.cwd && { cwd: serverConfig.cwd }
|
|
49011
49225
|
};
|
|
49012
49226
|
}
|
|
49013
|
-
function convertToOpencodeFormat(mcpServers, existingMcp, logger) {
|
|
49227
|
+
function convertToOpencodeFormat(mcpServers, existingMcp, configShape, logger) {
|
|
49014
49228
|
const tools = {};
|
|
49015
49229
|
return {
|
|
49016
49230
|
mcp: Object.fromEntries(Object.entries(mcpServers).map(([serverName, serverConfig]) => {
|
|
49017
|
-
const converted = convertServerToOpencodeFormat(serverName, serverConfig, existingMcp[serverName], logger);
|
|
49231
|
+
const converted = convertServerToOpencodeFormat(serverName, serverConfig, existingMcp[serverName], configShape, logger);
|
|
49018
49232
|
if (serverConfig.enabledTools) for (const tool of serverConfig.enabledTools) tools[`${serverName}_${tool}`] = true;
|
|
49019
49233
|
if (serverConfig.disabledTools) for (const tool of serverConfig.disabledTools) tools[`${serverName}_${tool}`] = false;
|
|
49020
49234
|
return converted === null ? null : [serverName, converted];
|
|
@@ -49022,6 +49236,7 @@ function convertToOpencodeFormat(mcpServers, existingMcp, logger) {
|
|
|
49022
49236
|
tools
|
|
49023
49237
|
};
|
|
49024
49238
|
}
|
|
49239
|
+
const OpencodeMcpFeatureOptionsSchema = z.looseObject({ configShape: z.optional(z.enum(["v1", "v2"])) });
|
|
49025
49240
|
var OpencodeMcp = class extends ToolMcp {
|
|
49026
49241
|
/** Directory layout; OpenCode forks (MiMo Code) override it. */
|
|
49027
49242
|
static layout = OPENCODE_LAYOUT;
|
|
@@ -49060,6 +49275,19 @@ var OpencodeMcp = class extends ToolMcp {
|
|
|
49060
49275
|
return this.json;
|
|
49061
49276
|
}
|
|
49062
49277
|
/**
|
|
49278
|
+
* Reads the `configShape` option of the target's `mcp` feature options
|
|
49279
|
+
* (`targets.opencode.mcp.configShape`). Only a layout that reads the V2
|
|
49280
|
+
* spellings back can be given the V2 shape.
|
|
49281
|
+
*/
|
|
49282
|
+
static resolveConfigShape(options) {
|
|
49283
|
+
if (!options) return "v1";
|
|
49284
|
+
const parsed = OpencodeMcpFeatureOptionsSchema.safeParse(options);
|
|
49285
|
+
if (!parsed.success) throw new Error(`Invalid options for the ${this.layout.toolTarget} mcp feature: \`configShape\` must be either "v1" or "v2".`);
|
|
49286
|
+
const configShape = parsed.data.configShape ?? "v1";
|
|
49287
|
+
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.`);
|
|
49288
|
+
return configShape;
|
|
49289
|
+
}
|
|
49290
|
+
/**
|
|
49063
49291
|
* opencode.json may contain other settings, so it should not be deleted.
|
|
49064
49292
|
*/
|
|
49065
49293
|
isDeletable() {
|
|
@@ -49103,7 +49331,8 @@ var OpencodeMcp = class extends ToolMcp {
|
|
|
49103
49331
|
logger
|
|
49104
49332
|
});
|
|
49105
49333
|
}
|
|
49106
|
-
static async fromRulesyncMcp({ outputRoot = process.cwd(), rulesyncMcp, validate = true, global = false, logger }) {
|
|
49334
|
+
static async fromRulesyncMcp({ outputRoot = process.cwd(), rulesyncMcp, validate = true, global = false, logger, featureOptions }) {
|
|
49335
|
+
const configShape = this.resolveConfigShape(featureOptions);
|
|
49107
49336
|
const basePaths = this.getSettablePaths({ global });
|
|
49108
49337
|
const jsonDir = join(outputRoot, basePaths.relativeDirPath);
|
|
49109
49338
|
let fileContent = null;
|
|
@@ -49121,16 +49350,21 @@ var OpencodeMcp = class extends ToolMcp {
|
|
|
49121
49350
|
});
|
|
49122
49351
|
const existingJson = parse$1(fileContent || "{}");
|
|
49123
49352
|
const existingMcp = OpencodeConfigSchema.safeParse(this.lowerConfig(existingJson));
|
|
49124
|
-
|
|
49125
|
-
|
|
49353
|
+
const existingRawMcp = isRecord$1(existingJson) ? existingJson.mcp : void 0;
|
|
49354
|
+
if (configShape === "v1" && this.layout.readsV2ConfigSpellings) {
|
|
49355
|
+
const containers = findOpencodeV2McpContainers(existingRawMcp);
|
|
49126
49356
|
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.`);
|
|
49127
49357
|
}
|
|
49128
|
-
const { mcp: convertedMcp, tools: mcpTools } = convertToOpencodeFormat(transformedServers, (existingMcp.success ? existingMcp.data.mcp : void 0) ?? {}, logger);
|
|
49129
|
-
const
|
|
49358
|
+
const { mcp: convertedMcp, tools: mcpTools } = convertToOpencodeFormat(transformedServers, (existingMcp.success ? existingMcp.data.mcp : void 0) ?? {}, configShape, logger);
|
|
49359
|
+
const generatedServers = undefinedWhenEmpty(convertedMcp);
|
|
49360
|
+
const generatedMcp = configShape === "v2" ? this.toV2Mcp({
|
|
49361
|
+
servers: convertedMcp,
|
|
49362
|
+
existingRawMcp
|
|
49363
|
+
}) : generatedServers;
|
|
49130
49364
|
const generatedTools = Object.keys(mcpTools).length > 0 ? mcpTools : void 0;
|
|
49131
49365
|
warnOpencodeDotDirOverrides({
|
|
49132
49366
|
generated: {
|
|
49133
|
-
mcp:
|
|
49367
|
+
mcp: generatedServers,
|
|
49134
49368
|
tools: generatedTools
|
|
49135
49369
|
},
|
|
49136
49370
|
overlays: (await readOpencodeDotDirConfigLayers({
|
|
@@ -49166,6 +49400,28 @@ var OpencodeMcp = class extends ToolMcp {
|
|
|
49166
49400
|
});
|
|
49167
49401
|
}
|
|
49168
49402
|
/**
|
|
49403
|
+
* Builds the V2 `mcp` value: each server raised to the V2 spelling under the
|
|
49404
|
+
* `mcp.servers` envelope, keeping what the existing file has that Rulesync
|
|
49405
|
+
* cannot express (see {@link raiseOpencodeServerToV2}), plus the existing
|
|
49406
|
+
* global `mcp.timeout`, which has no canonical counterpart.
|
|
49407
|
+
*/
|
|
49408
|
+
static toV2Mcp({ servers, existingRawMcp }) {
|
|
49409
|
+
const v2Servers = {};
|
|
49410
|
+
for (const [name, server] of Object.entries(servers)) Object.defineProperty(v2Servers, name, {
|
|
49411
|
+
value: raiseOpencodeServerToV2(server, getOpencodeMcpServerEntry(existingRawMcp, name)),
|
|
49412
|
+
enumerable: true,
|
|
49413
|
+
configurable: true,
|
|
49414
|
+
writable: true
|
|
49415
|
+
});
|
|
49416
|
+
const globalTimeout = getOpencodeV2GlobalMcpTimeout(existingRawMcp);
|
|
49417
|
+
const hasServers = Object.keys(v2Servers).length > 0;
|
|
49418
|
+
if (!hasServers && globalTimeout === void 0) return;
|
|
49419
|
+
return {
|
|
49420
|
+
...globalTimeout !== void 0 && { timeout: globalTimeout },
|
|
49421
|
+
...hasServers && { servers: v2Servers }
|
|
49422
|
+
};
|
|
49423
|
+
}
|
|
49424
|
+
/**
|
|
49169
49425
|
* Register additional instruction file paths into the shared opencode config
|
|
49170
49426
|
* (`opencode.json` / `opencode.jsonc`) under the `instructions` key.
|
|
49171
49427
|
*
|
|
@@ -54885,7 +55141,8 @@ var McpProcessor = class extends FeatureProcessor {
|
|
|
54885
55141
|
toolTarget;
|
|
54886
55142
|
global;
|
|
54887
55143
|
getFactory;
|
|
54888
|
-
|
|
55144
|
+
featureOptions;
|
|
55145
|
+
constructor({ outputRoot = process.cwd(), inputRoots, toolTarget, global = false, getFactory = defaultGetFactory$3, dryRun = false, logger, featureOptions }) {
|
|
54889
55146
|
super({
|
|
54890
55147
|
outputRoot,
|
|
54891
55148
|
inputRoots,
|
|
@@ -54897,6 +55154,7 @@ var McpProcessor = class extends FeatureProcessor {
|
|
|
54897
55154
|
this.toolTarget = result.data;
|
|
54898
55155
|
this.global = global;
|
|
54899
55156
|
this.getFactory = getFactory;
|
|
55157
|
+
this.featureOptions = featureOptions;
|
|
54900
55158
|
}
|
|
54901
55159
|
/**
|
|
54902
55160
|
* Implementation of abstract method from FeatureProcessor
|
|
@@ -54978,7 +55236,8 @@ var McpProcessor = class extends FeatureProcessor {
|
|
|
54978
55236
|
outputRoot: this.outputRoot,
|
|
54979
55237
|
rulesyncMcp: filteredRulesyncMcp,
|
|
54980
55238
|
global: this.global,
|
|
54981
|
-
logger: this.logger
|
|
55239
|
+
logger: this.logger,
|
|
55240
|
+
featureOptions: this.featureOptions
|
|
54982
55241
|
});
|
|
54983
55242
|
}));
|
|
54984
55243
|
const auxiliaryFiles = await factory.class.getAuxiliaryFiles?.({
|
|
@@ -57759,12 +58018,14 @@ var CodewhaleSkill = class CodewhaleSkill extends ToolSkill {
|
|
|
57759
58018
|
const CodexCliSkillFrontmatterSchema = z.looseObject({
|
|
57760
58019
|
name: z.string(),
|
|
57761
58020
|
description: z.string(),
|
|
57762
|
-
metadata: z.optional(z.looseObject({ "short-description": z.optional(z.string()) }))
|
|
58021
|
+
metadata: z.optional(z.looseObject({ "short-description": z.optional(z.string()) })),
|
|
58022
|
+
"disable-model-invocation": z.optional(z.boolean())
|
|
57763
58023
|
});
|
|
57764
58024
|
/**
|
|
57765
58025
|
* Relative path (within a skill directory) of the Codex `agents/openai.yaml` sidecar.
|
|
57766
58026
|
* Codex CLI reads UI metadata, invocation policy, and tool dependencies from this file;
|
|
57767
|
-
* `SKILL.md` frontmatter only
|
|
58027
|
+
* from `SKILL.md` frontmatter it reads only `name`, `description` and
|
|
58028
|
+
* `metadata.short-description`, ignoring any other key.
|
|
57768
58029
|
* @see https://developers.openai.com/codex/skills.md
|
|
57769
58030
|
*/
|
|
57770
58031
|
const CODEX_OPENAI_YAML_RELATIVE_PATH = CODEXCLI_OPENAI_YAML_RELATIVE_PATH;
|
|
@@ -57854,6 +58115,40 @@ function openaiYamlToCodexcliSection(parsed) {
|
|
|
57854
58115
|
return Object.keys(section).length > 0 ? section : void 0;
|
|
57855
58116
|
}
|
|
57856
58117
|
/**
|
|
58118
|
+
* Build the `SKILL.md` `metadata` map: the root-level rulesync `metadata` with
|
|
58119
|
+
* the legacy `codexcli.short-description` merged in. The tool-specific value wins
|
|
58120
|
+
* over a root `metadata.short-description`, as a target section overrides the
|
|
58121
|
+
* root default everywhere else. Values are stringified because the Agent Skills
|
|
58122
|
+
* spec types `metadata` as a string-to-string map, and `.agents/skills/` is read
|
|
58123
|
+
* by other spec clients too; Codex itself deserializes `short-description` as a
|
|
58124
|
+
* string.
|
|
58125
|
+
* @see https://agentskills.io/specification
|
|
58126
|
+
*/
|
|
58127
|
+
function buildSkillMetadata({ rootMetadata, shortDescription }) {
|
|
58128
|
+
const merged = {
|
|
58129
|
+
...rootMetadata,
|
|
58130
|
+
...shortDescription !== void 0 && { "short-description": shortDescription }
|
|
58131
|
+
};
|
|
58132
|
+
return Object.keys(merged).length > 0 ? toStringMetadata(merged) : void 0;
|
|
58133
|
+
}
|
|
58134
|
+
/**
|
|
58135
|
+
* Drop `policy.allow_implicit_invocation: false` from an imported sidecar section
|
|
58136
|
+
* when the `SKILL.md` frontmatter already carries `disable-model-invocation: true`,
|
|
58137
|
+
* since the root flag regenerates exactly that value. Keeping both would store the
|
|
58138
|
+
* same setting twice in `.rulesync/skills/`.
|
|
58139
|
+
*/
|
|
58140
|
+
function dropPolicyImpliedByRootFlag(section, disableModelInvocation) {
|
|
58141
|
+
const policy = section?.policy;
|
|
58142
|
+
if (!section || disableModelInvocation !== true || policy === null || typeof policy !== "object" || Array.isArray(policy) || policy.allow_implicit_invocation !== false) return section;
|
|
58143
|
+
const { allow_implicit_invocation: _implied, ...restPolicy } = policy;
|
|
58144
|
+
const { policy: _policy, ...restSection } = section;
|
|
58145
|
+
const result = {
|
|
58146
|
+
...restSection,
|
|
58147
|
+
...Object.keys(restPolicy).length > 0 && { policy: restPolicy }
|
|
58148
|
+
};
|
|
58149
|
+
return Object.keys(result).length > 0 ? result : void 0;
|
|
58150
|
+
}
|
|
58151
|
+
/**
|
|
57857
58152
|
* Represents a Codex CLI skill directory.
|
|
57858
58153
|
* Codex CLI supports skills in both project mode (under $CWD/.agents/skills)
|
|
57859
58154
|
* and global mode (under ~/.agents/skills — Rulesync resolves the home
|
|
@@ -57904,16 +58199,20 @@ var CodexCliSkill = class CodexCliSkill extends ToolSkill {
|
|
|
57904
58199
|
}
|
|
57905
58200
|
toRulesyncSkill() {
|
|
57906
58201
|
const frontmatter = this.getFrontmatter();
|
|
58202
|
+
const disableModelInvocation = frontmatter["disable-model-invocation"];
|
|
57907
58203
|
const { parsed, rest } = extractOpenaiYamlFile(this.getOtherFiles());
|
|
57908
|
-
const openaiSection = openaiYamlToCodexcliSection(parsed);
|
|
58204
|
+
const openaiSection = dropPolicyImpliedByRootFlag(openaiYamlToCodexcliSection(parsed), disableModelInvocation);
|
|
58205
|
+
const { "short-description": shortDescription, ...rootMetadata } = frontmatter.metadata ?? {};
|
|
57909
58206
|
const codexcliSection = {
|
|
57910
|
-
...
|
|
58207
|
+
...shortDescription !== void 0 && { "short-description": shortDescription },
|
|
57911
58208
|
...openaiSection
|
|
57912
58209
|
};
|
|
57913
58210
|
const rulesyncFrontmatter = {
|
|
57914
58211
|
name: frontmatter.name,
|
|
57915
58212
|
description: frontmatter.description,
|
|
57916
58213
|
targets: ["*"],
|
|
58214
|
+
...disableModelInvocation !== void 0 && { "disable-model-invocation": disableModelInvocation },
|
|
58215
|
+
...Object.keys(rootMetadata).length > 0 && { metadata: rootMetadata },
|
|
57917
58216
|
...Object.keys(codexcliSection).length > 0 && { codexcli: codexcliSection }
|
|
57918
58217
|
};
|
|
57919
58218
|
return new RulesyncSkill({
|
|
@@ -57930,10 +58229,16 @@ var CodexCliSkill = class CodexCliSkill extends ToolSkill {
|
|
|
57930
58229
|
static fromRulesyncSkill({ outputRoot = process.cwd(), rulesyncSkill, validate = true, global = false }) {
|
|
57931
58230
|
const settablePaths = CodexCliSkill.getSettablePaths({ global });
|
|
57932
58231
|
const rulesyncFrontmatter = rulesyncSkill.getFrontmatter();
|
|
58232
|
+
const metadata = buildSkillMetadata({
|
|
58233
|
+
rootMetadata: rulesyncFrontmatter.metadata,
|
|
58234
|
+
shortDescription: rulesyncFrontmatter.codexcli?.["short-description"]
|
|
58235
|
+
});
|
|
58236
|
+
const disableModelInvocation = rulesyncFrontmatter["disable-model-invocation"];
|
|
57933
58237
|
const codexFrontmatter = {
|
|
57934
58238
|
name: rulesyncFrontmatter.name,
|
|
57935
58239
|
description: rulesyncFrontmatter.description,
|
|
57936
|
-
...
|
|
58240
|
+
...disableModelInvocation !== void 0 && { "disable-model-invocation": disableModelInvocation },
|
|
58241
|
+
...metadata !== void 0 && { metadata }
|
|
57937
58242
|
};
|
|
57938
58243
|
const target = toPosixPath(CODEX_OPENAI_YAML_RELATIVE_PATH);
|
|
57939
58244
|
const baseOtherFiles = rulesyncSkill.getOtherFiles().filter((file) => toPosixPath(file.relativeFilePathToDirPath) !== target);
|
|
@@ -67421,6 +67726,16 @@ var CopilotSubagent = class CopilotSubagent extends ToolSubagent {
|
|
|
67421
67726
|
* (`preferred` lets a `subagents` settings override or the `/subagents`
|
|
67422
67727
|
* picker replace the model; `required` locks dispatch to the authored
|
|
67423
67728
|
* list). CLI v1.0.81+; the shared reference page still lists `model` only.
|
|
67729
|
+
* - `model` may also be a list of models in preference order. The Copilot
|
|
67730
|
+
* CLI changelog for v1.0.83 (2026-09-04) says "Custom agents can list
|
|
67731
|
+
* several models in `model`, tried in order until one is available to
|
|
67732
|
+
* you"; the CLI command reference and the shared configuration reference
|
|
67733
|
+
* still document `model` as a string, so older CLI versions may not read
|
|
67734
|
+
* the list form (a string is the safe form there). The VS Code Copilot
|
|
67735
|
+
* harness documents the same array spelling. Preserve a list as authored
|
|
67736
|
+
* instead of rewriting it to the separate `models` field.
|
|
67737
|
+
* https://github.com/github/copilot-cli/blob/main/changelog.md#1083---2026-09-04
|
|
67738
|
+
* https://github.com/microsoft/vscode-docs/blob/main/docs/agents/reference/custom-agent-file-reference.md
|
|
67424
67739
|
*
|
|
67425
67740
|
* `looseObject` is used so that future additional fields are passed through
|
|
67426
67741
|
* unchanged (consistent with the rest of rulesync's frontmatter schemas).
|
|
@@ -67430,7 +67745,7 @@ const CopilotCliSubagentFrontmatterSchema = z.looseObject({
|
|
|
67430
67745
|
name: z.optional(z.string()),
|
|
67431
67746
|
target: z.optional(z.string()),
|
|
67432
67747
|
tools: z.optional(z.union([z.string(), z.array(z.string())])),
|
|
67433
|
-
model: z.optional(z.string()),
|
|
67748
|
+
model: z.optional(z.union([z.string(), z.array(z.string())])),
|
|
67434
67749
|
models: z.optional(z.array(z.string())),
|
|
67435
67750
|
modelPolicy: z.optional(z.enum(["preferred", "required"])),
|
|
67436
67751
|
"disable-model-invocation": z.optional(z.boolean()),
|
|
@@ -107219,7 +107534,8 @@ function buildMcpStrategy(ctx) {
|
|
|
107219
107534
|
toolTarget,
|
|
107220
107535
|
global,
|
|
107221
107536
|
dryRun,
|
|
107222
|
-
logger
|
|
107537
|
+
logger,
|
|
107538
|
+
featureOptions: config.getFeatureOptions(toolTarget, "mcp")
|
|
107223
107539
|
}),
|
|
107224
107540
|
loadSource: (p) => p.loadToolFiles(),
|
|
107225
107541
|
toRulesync: (p, files) => p.convertToolFilesToRulesyncFiles(files),
|
|
@@ -108949,7 +109265,8 @@ async function generateMcpCore(params) {
|
|
|
108949
109265
|
toolTarget,
|
|
108950
109266
|
global: config.getGlobal(),
|
|
108951
109267
|
dryRun: config.isPreviewMode(),
|
|
108952
|
-
logger
|
|
109268
|
+
logger,
|
|
109269
|
+
featureOptions: config.getFeatureOptions(toolTarget, "mcp")
|
|
108953
109270
|
});
|
|
108954
109271
|
const result = await processFeatureWithRulesyncFiles({
|
|
108955
109272
|
config,
|
|
@@ -109675,4 +109992,4 @@ async function importChecksCore(params) {
|
|
|
109675
109992
|
//#endregion
|
|
109676
109993
|
export { RulesyncIgnore as $, stripHiddenCharacters as $n, readFileContent as $t, caseFoldIdentity as A, RULESYNC_CURATED_SKILLS_RELATIVE_DIR_PATH as An, ErrorCodes as At, QWENCODE_DIR as B, RULESYNC_RULES_RELATIVE_DIR_PATH as Bn, ensureDir as Bt, COMMANDCODE_DIR as C, MAX_FILE_SIZE as Cn, WarningCollectingLogger as Ct, ChecksProcessor as D, RULESYNC_CONFIG_RELATIVE_FILE_PATH as Dn, resetRunWarningState as Dt, CODEBUDDY_LOCAL_RULE_FILE_NAME as E, RULESYNC_COMMANDS_RELATIVE_DIR_PATH as En, withFallbackLoggerTarget as Et, CLAUDECODE_DIR as F, RULESYNC_MCP_SCHEMA_URL as Fn, assertTreeContainsNoSymlinks as Ft, RulesyncSubagent as G, RULESYNC_XDG_CONFIG_HOME_DEFAULT_DIR_NAME as Gn, isFileNotFoundError as Gt, AUGMENTCODE_DIR as H, RULESYNC_SOURCES_LOCK_RELATIVE_FILE_PATH as Hn, findFilesByGlobs as Ht, CLAUDECODE_LOCAL_RULE_FILE_NAME as I, RULESYNC_NPM_SOURCES_LOCK_RELATIVE_FILE_PATH as In, assertWritablePathInsideRoot as It, RulesyncSkillFrontmatterSchema as J, hasDeceptiveHiddenCharacters as Jn, listDirectoryEntryNames as Jt, RulesyncSubagentFrontmatterSchema as K, formatError as Kn, isFileSystemError as Kt, CLAUDECODE_MEMORIES_DIR_NAME as L, RULESYNC_PERMISSIONS_RELATIVE_FILE_PATH as Ln, checkPathTraversal as Lt, CODEXCLI_BASH_RULES_FILE_NAME as M, RULESYNC_IGNORE_RELATIVE_FILE_PATH as Mn, applyFileMode as Mt, CODEXCLI_DIR as N, RULESYNC_LOCAL_CONFIG_RELATIVE_FILE_PATH as Nn, assertDirectoryIfExists as Nt, FACTORYDROID_DIR as O, RULESYNC_CONFIG_SCHEMA_URL as On, withWarnOnceScope as Ot, CLAUDECODE_AGENT_MEMORY_LOCAL_DIR_NAME as P, RULESYNC_MCP_RELATIVE_FILE_PATH as Pn, assertPathResolvesInsideRoot as Pt, RulesyncMcp as Q, stripControlCharactersKeepingLineFeeds as Qn, posixRelativePathEscapesRoot as Qt, CLAUDECODE_SETTINGS_LOCAL_FILE_NAME as R, RULESYNC_PERMISSIONS_SCHEMA_URL as Rn, createTempDirectory as Rt, LETTACODE_SETTINGS_LOCAL_FILE_NAME as S, FEATURE_SOURCE_TREE_ENTRIES as Sn, JsonLogger as St, CODEBUDDY_DIR as T, RULESYNC_CHECKS_RELATIVE_DIR_PATH as Tn, warnOnConflictingFlags as Tt, AUGMENTCODE_SETTINGS_LOCAL_FILE_NAME as U, RULESYNC_SUBAGENTS_RELATIVE_DIR_PATH as Un, getFileSizeAndExecutableBit as Ut, QWENCODE_LOCAL_RULE_FILE_NAME as V, RULESYNC_SKILLS_RELATIVE_DIR_PATH as Vn, fileExists as Vt, getLocalSkillDirNames as W, RULESYNC_USER_CONFIG_DIR_NAME as Wn, getHomeDirectory as Wt, RulesyncRuleFrontmatterSchema as X, quoteForLog as Xn, listSubdirectoryNames as Xt, RulesyncRule as Y, hasEnclosingMarkOutsideKeycap as Yn, listFilePathsRecursively as Yt, RulesyncPermissions as Z, stripControlCharacters as Zn, pathEscapesRoot as Zt, HooksProcessor as _, ToolTargetSchema as _n, GITIGNORE_DESTINATION_KEY as _t, convertFromTool as a, removeTempDirectory as an, RulesyncCommandFrontmatterSchema as at, REASONIX_LOCAL_RULE_FILE_NAME as b, DEPRECATED_FEATURE_REPLACEMENTS as bn, findControlCharacter as bt, parseCommaSeparatedList as c, runWithDirectoryRollback as cn, stringifyFrontmatter as ct, shortenToWidth as d, writeFileBuffer as dn, SKILL_FILE_NAME as dt, readFileContentOrNull as en, RulesyncHooks as et, RulesProcessor as f, writeFileContent as fn, ConfigResolver as ft, IgnoreProcessor as g, PACKAGING_TOOL_TARGETS as gn, ConfigFileSchema as gt, McpProcessor as h, ALL_TOOL_TARGETS_WITH_WILDCARD as hn, CONFLICTING_TARGET_PAIRS as ht, inspectInputRoots as i, removeFileStrict as in, RulesyncCommand as it, groupSpellingsByCaseFoldedIdentity as j, RULESYNC_HOOKS_RELATIVE_FILE_PATH as jn, PathOutsideRootError as jt, FACTORYDROID_SETTINGS_LOCAL_FILE_NAME as k, RULESYNC_CURATED_RULES_RELATIVE_DIR_PATH as kn, CLIError as kt, ELLIPSIS_WIDTH as l, splitPathSegments as ln, loadYaml as lt, SkillsProcessor as m, ALL_TOOL_TARGETS as mn, resolveEffectiveInputRoots as mt, formatSourceLoadFailure as n, removeDirectoryStrict as nn, resolveRulesyncSourceWritePath as nt, isPackagingToolTarget as o, resolvePath as on, RulesyncCheck as ot, SubagentsProcessor as p, mapWithConcurrency as pn, mergeInputRootConfigs as pt, RulesyncSkill as q, truncateText as qn, isSymlink as qt, generate as r, removeFile as rn, parseJsonc as rt, getProcessorRegistryEntry as s, resolvedRelativePath as sn, RulesyncCheckFrontmatterSchema as st, importFromTool as t, removeDirectory as tn, getRulesyncSourceCandidates as tt, displayWidthOf as u, toPosixPath as un, SHARED_USER_MANAGED_CONFIG_PATHS as ut, CRUSH_LOCAL_RULE_FILE_NAME as v, ALL_FEATURES as vn, SourceEntrySchema as vt, COMMANDCODE_SETTINGS_LOCAL_FILE_NAME as w, RULESYNC_AIIGNORE_RELATIVE_FILE_PATH as wn, fallbackLogger as wt, LETTACODE_DIR as x, CURATED_RULES_FEATURE_SUBDIR as xn, ConsoleLogger as xt, CommandsProcessor as y, ALL_FEATURES_WITH_WILDCARD as yn, assertTargetsFeaturesExclusive as yt, CLAUDECODE_SKILLS_DIR_PATH as z, RULESYNC_RELATIVE_DIR_PATH as zn, directoryExists as zt };
|
|
109677
109994
|
|
|
109678
|
-
//# sourceMappingURL=import-
|
|
109995
|
+
//# sourceMappingURL=import-D7cJEvia.js.map
|