rulesync 31.0.0 → 33.0.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 +23 -5
- package/dist/cli/index.js +23 -5
- package/dist/cli/index.js.map +1 -1
- package/dist/{import-ORYbMDJK.js → import-5-Uennl1.js} +451 -50
- package/dist/{import-ORYbMDJK.js.map → import-5-Uennl1.js.map} +1 -1
- package/dist/{import-CBvXTNp8.cjs → import-D_SV_C0k.cjs} +467 -66
- package/dist/index.cjs +1 -1
- package/dist/index.js +1 -1
- package/package.json +1 -1
|
@@ -453,36 +453,36 @@ const isFeatureValueEnabled = (value) => {
|
|
|
453
453
|
const parseCommaSeparatedList = (value) => value.split(",").map((s) => s.trim()).filter(Boolean);
|
|
454
454
|
//#endregion
|
|
455
455
|
//#region src/constants/rulesync-paths.ts
|
|
456
|
-
const { join: join$
|
|
456
|
+
const { join: join$417 } = node_path.posix;
|
|
457
457
|
const RULESYNC_CONFIG_RELATIVE_FILE_PATH = "rulesync.jsonc";
|
|
458
458
|
const RULESYNC_LOCAL_CONFIG_RELATIVE_FILE_PATH = "rulesync.local.jsonc";
|
|
459
459
|
const RULESYNC_USER_CONFIG_DIR_NAME = "rulesync";
|
|
460
460
|
const RULESYNC_XDG_CONFIG_HOME_DEFAULT_DIR_NAME = ".config";
|
|
461
461
|
const RULESYNC_RELATIVE_DIR_PATH = ".rulesync";
|
|
462
462
|
const RULES_FEATURE_SUBDIR = "rules";
|
|
463
|
-
const CURATED_RULES_FEATURE_SUBDIR = join$
|
|
463
|
+
const CURATED_RULES_FEATURE_SUBDIR = join$417(RULES_FEATURE_SUBDIR, ".curated");
|
|
464
464
|
const COMMANDS_FEATURE_SUBDIR = "commands";
|
|
465
465
|
const SUBAGENTS_FEATURE_SUBDIR = "subagents";
|
|
466
466
|
const CHECKS_FEATURE_SUBDIR = "checks";
|
|
467
467
|
const SKILLS_FEATURE_SUBDIR = "skills";
|
|
468
|
-
const CURATED_SKILLS_FEATURE_SUBDIR = join$
|
|
469
|
-
const RULESYNC_RULES_RELATIVE_DIR_PATH = join$
|
|
470
|
-
const RULESYNC_CURATED_RULES_RELATIVE_DIR_PATH = join$
|
|
471
|
-
const RULESYNC_COMMANDS_RELATIVE_DIR_PATH = join$
|
|
472
|
-
const RULESYNC_SUBAGENTS_RELATIVE_DIR_PATH = join$
|
|
473
|
-
const RULESYNC_CHECKS_RELATIVE_DIR_PATH = join$
|
|
474
|
-
const RULESYNC_MCP_RELATIVE_FILE_PATH = join$
|
|
475
|
-
const RULESYNC_HOOKS_RELATIVE_FILE_PATH = join$
|
|
476
|
-
const RULESYNC_PERMISSIONS_RELATIVE_FILE_PATH = join$
|
|
477
|
-
join$
|
|
478
|
-
const RULESYNC_HOOKS_LEGACY_RELATIVE_FILE_PATH = join$
|
|
479
|
-
const RULESYNC_PERMISSIONS_LEGACY_RELATIVE_FILE_PATH = join$
|
|
468
|
+
const CURATED_SKILLS_FEATURE_SUBDIR = join$417(SKILLS_FEATURE_SUBDIR, ".curated");
|
|
469
|
+
const RULESYNC_RULES_RELATIVE_DIR_PATH = join$417(RULESYNC_RELATIVE_DIR_PATH, RULES_FEATURE_SUBDIR);
|
|
470
|
+
const RULESYNC_CURATED_RULES_RELATIVE_DIR_PATH = join$417(RULESYNC_RELATIVE_DIR_PATH, CURATED_RULES_FEATURE_SUBDIR);
|
|
471
|
+
const RULESYNC_COMMANDS_RELATIVE_DIR_PATH = join$417(RULESYNC_RELATIVE_DIR_PATH, COMMANDS_FEATURE_SUBDIR);
|
|
472
|
+
const RULESYNC_SUBAGENTS_RELATIVE_DIR_PATH = join$417(RULESYNC_RELATIVE_DIR_PATH, SUBAGENTS_FEATURE_SUBDIR);
|
|
473
|
+
const RULESYNC_CHECKS_RELATIVE_DIR_PATH = join$417(RULESYNC_RELATIVE_DIR_PATH, CHECKS_FEATURE_SUBDIR);
|
|
474
|
+
const RULESYNC_MCP_RELATIVE_FILE_PATH = join$417(RULESYNC_RELATIVE_DIR_PATH, "mcp.jsonc");
|
|
475
|
+
const RULESYNC_HOOKS_RELATIVE_FILE_PATH = join$417(RULESYNC_RELATIVE_DIR_PATH, "hooks.jsonc");
|
|
476
|
+
const RULESYNC_PERMISSIONS_RELATIVE_FILE_PATH = join$417(RULESYNC_RELATIVE_DIR_PATH, "permissions.jsonc");
|
|
477
|
+
join$417(RULESYNC_RELATIVE_DIR_PATH, "mcp.json");
|
|
478
|
+
const RULESYNC_HOOKS_LEGACY_RELATIVE_FILE_PATH = join$417(RULESYNC_RELATIVE_DIR_PATH, "hooks.json");
|
|
479
|
+
const RULESYNC_PERMISSIONS_LEGACY_RELATIVE_FILE_PATH = join$417(RULESYNC_RELATIVE_DIR_PATH, "permissions.json");
|
|
480
480
|
const RULESYNC_AIIGNORE_FILE_NAME = ".aiignore";
|
|
481
|
-
const RULESYNC_AIIGNORE_RELATIVE_FILE_PATH = join$
|
|
481
|
+
const RULESYNC_AIIGNORE_RELATIVE_FILE_PATH = join$417(RULESYNC_RELATIVE_DIR_PATH, ".aiignore");
|
|
482
482
|
const RULESYNC_IGNORE_RELATIVE_FILE_PATH = ".rulesyncignore";
|
|
483
483
|
const RULESYNC_OVERVIEW_FILE_NAME = "overview.md";
|
|
484
|
-
const RULESYNC_SKILLS_RELATIVE_DIR_PATH = join$
|
|
485
|
-
const RULESYNC_CURATED_SKILLS_RELATIVE_DIR_PATH = join$
|
|
484
|
+
const RULESYNC_SKILLS_RELATIVE_DIR_PATH = join$417(RULESYNC_RELATIVE_DIR_PATH, SKILLS_FEATURE_SUBDIR);
|
|
485
|
+
const RULESYNC_CURATED_SKILLS_RELATIVE_DIR_PATH = join$417(RULESYNC_RELATIVE_DIR_PATH, CURATED_SKILLS_FEATURE_SUBDIR);
|
|
486
486
|
const RULESYNC_SOURCES_LOCK_RELATIVE_FILE_PATH = "rulesync.lock";
|
|
487
487
|
const RULESYNC_NPM_SOURCES_LOCK_RELATIVE_FILE_PATH = "rulesync-npm.lock.json";
|
|
488
488
|
const RULESYNC_MCP_FILE_NAME = "mcp.jsonc";
|
|
@@ -4845,13 +4845,11 @@ const PI_HOOK_EVENTS = [
|
|
|
4845
4845
|
/**
|
|
4846
4846
|
* Hook events supported by oh-my-pi (`omp`), Pi's fork, through the same
|
|
4847
4847
|
* generated TypeScript extension. oh-my-pi keeps Pi's names for every event
|
|
4848
|
-
* listed here
|
|
4849
|
-
*
|
|
4850
|
-
* `
|
|
4851
|
-
* result; it needs its own handler in the generator and is left for a
|
|
4852
|
-
* follow-up. See CANONICAL_TO_OMP_EVENT_NAMES.
|
|
4848
|
+
* listed here except `stop`: it has no `agent_before_settle`, so `stop` maps to
|
|
4849
|
+
* its own `session_stop` event instead. It has no `ui_prompt_start` either, so
|
|
4850
|
+
* `notification` is not mapped. See CANONICAL_TO_OMP_EVENT_NAMES.
|
|
4853
4851
|
*
|
|
4854
|
-
* @see https://github.com/can1357/oh-my-pi/blob/
|
|
4852
|
+
* @see https://github.com/can1357/oh-my-pi/blob/fa5ff4a3a977a28b93d025f4cf50f046e019f18c/docs/extensions.md
|
|
4855
4853
|
*/
|
|
4856
4854
|
const OMP_HOOK_EVENTS = [
|
|
4857
4855
|
"sessionStart",
|
|
@@ -4862,6 +4860,7 @@ const OMP_HOOK_EVENTS = [
|
|
|
4862
4860
|
"preModelInvocation",
|
|
4863
4861
|
"postModelInvocation",
|
|
4864
4862
|
"beforeSubmitPrompt",
|
|
4863
|
+
"stop",
|
|
4865
4864
|
"preCompact",
|
|
4866
4865
|
"postCompact"
|
|
4867
4866
|
];
|
|
@@ -6283,9 +6282,10 @@ const CANONICAL_TO_PI_EVENT_NAMES = {
|
|
|
6283
6282
|
/**
|
|
6284
6283
|
* Map canonical camelCase event names to oh-my-pi extension event names. They
|
|
6285
6284
|
* are Pi's names (see CANONICAL_TO_PI_EVENT_NAMES) for the events oh-my-pi
|
|
6286
|
-
* kept
|
|
6287
|
-
* budget, and fails a `tool_call`
|
|
6288
|
-
* `extensionHandlers.toolCallTimeoutMs`
|
|
6285
|
+
* kept, plus `session_stop` for `stop`. Note that oh-my-pi runs
|
|
6286
|
+
* `session_shutdown` handlers with a 2-second budget, and fails a `tool_call`
|
|
6287
|
+
* closed when its handler exceeds `extensionHandlers.toolCallTimeoutMs`
|
|
6288
|
+
* (30 seconds by default).
|
|
6289
6289
|
*
|
|
6290
6290
|
* @see https://github.com/can1357/oh-my-pi/blob/40e9368ef0458fd9073329cdff4174895f91bc6b/docs/extensions.md
|
|
6291
6291
|
*/
|
|
@@ -6298,6 +6298,7 @@ const CANONICAL_TO_OMP_EVENT_NAMES = {
|
|
|
6298
6298
|
preModelInvocation: "context",
|
|
6299
6299
|
postModelInvocation: "message_end",
|
|
6300
6300
|
beforeSubmitPrompt: "input",
|
|
6301
|
+
stop: "session_stop",
|
|
6301
6302
|
preCompact: "session_before_compact",
|
|
6302
6303
|
postCompact: "session_compact"
|
|
6303
6304
|
};
|
|
@@ -12955,10 +12956,16 @@ const SHARED_CONFIG_OWNERSHIP = {
|
|
|
12955
12956
|
[CODEWHALE_CONFIG_SHARED_FILE_KEY]: {
|
|
12956
12957
|
format: "toml",
|
|
12957
12958
|
invalidRootPolicy: "error",
|
|
12958
|
-
features: {
|
|
12959
|
-
|
|
12960
|
-
|
|
12961
|
-
|
|
12959
|
+
features: {
|
|
12960
|
+
hooks: {
|
|
12961
|
+
kind: "replace-owned-keys",
|
|
12962
|
+
ownedKeys: ["hooks"]
|
|
12963
|
+
},
|
|
12964
|
+
permissions: {
|
|
12965
|
+
kind: "replace-owned-keys",
|
|
12966
|
+
ownedKeys: ["allow_shell"]
|
|
12967
|
+
}
|
|
12968
|
+
}
|
|
12962
12969
|
},
|
|
12963
12970
|
".letta/settings.json": {
|
|
12964
12971
|
format: "json",
|
|
@@ -21759,22 +21766,29 @@ function mergeOpencodeV2Entries({ config, legacyKey, pluralKey, lower }) {
|
|
|
21759
21766
|
const entry = asOpencodeEntries(value);
|
|
21760
21767
|
const lowered = entry === null ? null : lower(entry);
|
|
21761
21768
|
if (Object.hasOwn(merged, name) || lowered === null) continue;
|
|
21762
|
-
|
|
21763
|
-
value: lowered,
|
|
21764
|
-
enumerable: true,
|
|
21765
|
-
configurable: true,
|
|
21766
|
-
writable: true
|
|
21767
|
-
});
|
|
21769
|
+
setOwnEntry(merged, name, lowered);
|
|
21768
21770
|
}
|
|
21769
21771
|
return Object.keys(merged).length > 0 ? merged : legacy;
|
|
21770
21772
|
}
|
|
21771
21773
|
/**
|
|
21774
|
+
* Sets `target[key]` as an own, enumerable property. A plain assignment to a
|
|
21775
|
+
* key named `__proto__` would set the prototype instead.
|
|
21776
|
+
*/
|
|
21777
|
+
function setOwnEntry(target, key, value) {
|
|
21778
|
+
Object.defineProperty(target, key, {
|
|
21779
|
+
value,
|
|
21780
|
+
enumerable: true,
|
|
21781
|
+
configurable: true,
|
|
21782
|
+
writable: true
|
|
21783
|
+
});
|
|
21784
|
+
}
|
|
21785
|
+
/**
|
|
21772
21786
|
* Returns the inline agent entries of an OpenCode config in the V1 `agent`
|
|
21773
21787
|
* shape. Since v1.18.24 OpenCode V1 also reads the V2 `agents` spelling and
|
|
21774
21788
|
* lowers it, so a layout that does so (`readsV2ConfigSpellings`) gets those
|
|
21775
21789
|
* entries too.
|
|
21776
21790
|
*
|
|
21777
|
-
* @see https://github.com/anomalyco/opencode/blob/v1.18.
|
|
21791
|
+
* @see https://github.com/anomalyco/opencode/blob/v1.18.35/packages/opencode/src/config/v2-compat.ts
|
|
21778
21792
|
*/
|
|
21779
21793
|
function getOpencodeAgentEntries({ config, layout = OPENCODE_LAYOUT }) {
|
|
21780
21794
|
if (!layout.readsV2ConfigSpellings) return asOpencodeEntries(config.agent);
|
|
@@ -21793,7 +21807,7 @@ function getOpencodeAgentEntries({ config, layout = OPENCODE_LAYOUT }) {
|
|
|
21793
21807
|
* Other fields are kept: V1 drops a V2 `subagent`, but rulesync reads it as
|
|
21794
21808
|
* `subtask` everywhere (as V2 does), so it is left for the command importer.
|
|
21795
21809
|
*
|
|
21796
|
-
* @see https://github.com/anomalyco/opencode/blob/v1.18.
|
|
21810
|
+
* @see https://github.com/anomalyco/opencode/blob/v1.18.35/packages/opencode/src/config/v2-compat.ts
|
|
21797
21811
|
*/
|
|
21798
21812
|
function getOpencodeCommandEntries({ config, layout = OPENCODE_LAYOUT }) {
|
|
21799
21813
|
if (!layout.readsV2ConfigSpellings) return asOpencodeEntries(config.command);
|
|
@@ -21816,7 +21830,7 @@ function getOpencodeCommandEntries({ config, layout = OPENCODE_LAYOUT }) {
|
|
|
21816
21830
|
* reads V2 spellings, a flat V2 `skills` array is accepted too: its entries
|
|
21817
21831
|
* that are not `http(s)://` URLs are the paths, as OpenCode V1 lowers them.
|
|
21818
21832
|
*
|
|
21819
|
-
* @see https://github.com/anomalyco/opencode/blob/v1.18.
|
|
21833
|
+
* @see https://github.com/anomalyco/opencode/blob/v1.18.35/packages/opencode/src/config/v2-compat.ts
|
|
21820
21834
|
*/
|
|
21821
21835
|
function getOpencodeSkillPaths({ config, layout = OPENCODE_LAYOUT }) {
|
|
21822
21836
|
if (Array.isArray(config.skills)) {
|
|
@@ -21826,6 +21840,160 @@ function getOpencodeSkillPaths({ config, layout = OPENCODE_LAYOUT }) {
|
|
|
21826
21840
|
const skills = asOpencodeEntries(config.skills);
|
|
21827
21841
|
return skills !== null && Array.isArray(skills.paths) ? skills.paths : [];
|
|
21828
21842
|
}
|
|
21843
|
+
/**
|
|
21844
|
+
* Whether a record is itself an MCP server entry rather than a V2 container
|
|
21845
|
+
* (`mcp.servers`, `mcp.timeout`): a server — or a server literally named
|
|
21846
|
+
* `type` / `enabled` — carries a scalar `type` or `enabled`. Mirrors OpenCode
|
|
21847
|
+
* V1's `isDirectServer`.
|
|
21848
|
+
*/
|
|
21849
|
+
function isDirectOpencodeMcpServer(value) {
|
|
21850
|
+
return ["type", "enabled"].some((key) => Object.hasOwn(value, key) && (value[key] === null || typeof value[key] !== "object" || Array.isArray(value[key])));
|
|
21851
|
+
}
|
|
21852
|
+
const OPENCODE_V2_TIMEOUT_KEYS = [
|
|
21853
|
+
"startup",
|
|
21854
|
+
"catalog",
|
|
21855
|
+
"execution"
|
|
21856
|
+
];
|
|
21857
|
+
/**
|
|
21858
|
+
* Whether an MCP server entry uses a V2-only per-server spelling, so that
|
|
21859
|
+
* OpenCode V1 lowers it: `disabled`, `codemode`, an object `timeout`, or a
|
|
21860
|
+
* snake_case `oauth` key.
|
|
21861
|
+
*/
|
|
21862
|
+
function isOpencodeV2NativeServer(value) {
|
|
21863
|
+
const oauth = value.oauth;
|
|
21864
|
+
return Object.hasOwn(value, "disabled") || Object.hasOwn(value, "codemode") || isRecord$1(value.timeout) || isRecord$1(oauth) && [
|
|
21865
|
+
"client_id",
|
|
21866
|
+
"client_secret",
|
|
21867
|
+
"callback_port",
|
|
21868
|
+
"redirect_uri"
|
|
21869
|
+
].some((key) => Object.hasOwn(oauth, key));
|
|
21870
|
+
}
|
|
21871
|
+
/**
|
|
21872
|
+
* Lowers a V2 timeout object (`{ startup, catalog, execution }`) to V1's single
|
|
21873
|
+
* number, the way OpenCode V1 does: only a `catalog` equal to `execution`, with
|
|
21874
|
+
* no `startup`, has a V1 equivalent. Anything else — including a value that is
|
|
21875
|
+
* not a positive integer — is `undefined` (dropped).
|
|
21876
|
+
*/
|
|
21877
|
+
function lowerOpencodeV2Timeout(value) {
|
|
21878
|
+
if (!hasPositiveIntegerTimeouts(value) || value.startup !== void 0 || typeof value.catalog !== "number") return;
|
|
21879
|
+
return value.catalog === value.execution ? value.catalog : void 0;
|
|
21880
|
+
}
|
|
21881
|
+
const OPENCODE_V2_OAUTH_KEYS = /* @__PURE__ */ new Map([
|
|
21882
|
+
["client_id", "clientId"],
|
|
21883
|
+
["client_secret", "clientSecret"],
|
|
21884
|
+
["callback_port", "callbackPort"],
|
|
21885
|
+
["redirect_uri", "redirectUri"]
|
|
21886
|
+
]);
|
|
21887
|
+
/**
|
|
21888
|
+
* V2 `oauth` keys that have no V1 spelling. OpenCode V1 drops them when it
|
|
21889
|
+
* lowers a server, so they are dropped (and reported) here too rather than
|
|
21890
|
+
* being written back as a snake_case key among camelCase ones.
|
|
21891
|
+
*/
|
|
21892
|
+
const OPENCODE_V2_UNSUPPORTED_OAUTH_KEYS = /* @__PURE__ */ new Set(["auth_server_metadata_url"]);
|
|
21893
|
+
function lowerOpencodeV2OAuth(oauth, path, onDropped) {
|
|
21894
|
+
const lowered = {};
|
|
21895
|
+
for (const [key, value] of Object.entries(oauth)) {
|
|
21896
|
+
if (OPENCODE_V2_UNSUPPORTED_OAUTH_KEYS.has(key)) {
|
|
21897
|
+
onDropped?.(`${path}.${key}`, "unsupported");
|
|
21898
|
+
continue;
|
|
21899
|
+
}
|
|
21900
|
+
setOwnEntry(lowered, OPENCODE_V2_OAUTH_KEYS.get(key) ?? key, value);
|
|
21901
|
+
}
|
|
21902
|
+
return lowered;
|
|
21903
|
+
}
|
|
21904
|
+
/**
|
|
21905
|
+
* Lowers one V2-spelled MCP server entry to V1's shape, as OpenCode V1 does:
|
|
21906
|
+
* `disabled` becomes `enabled` (an explicit `enabled` wins), an object
|
|
21907
|
+
* `timeout` becomes a number or is dropped, and snake_case `oauth` keys become
|
|
21908
|
+
* camelCase. `codemode` is kept: Rulesync passes it through where V1 drops it.
|
|
21909
|
+
*
|
|
21910
|
+
* Like V1, only an entry that is a local or remote server is lowered, and one
|
|
21911
|
+
* whose `enabled` or `disabled` is not a boolean is left as it is. Unlike V1,
|
|
21912
|
+
* such an entry is not dropped but passed on, so the schema rejects it loudly.
|
|
21913
|
+
*/
|
|
21914
|
+
function lowerOpencodeV2Server(value, path, onDropped) {
|
|
21915
|
+
if (!isRecord$1(value) || value.type !== "local" && value.type !== "remote" || Object.hasOwn(value, "enabled") && typeof value.enabled !== "boolean" || Object.hasOwn(value, "disabled") && typeof value.disabled !== "boolean") return value;
|
|
21916
|
+
const { disabled, timeout, ...rest } = value;
|
|
21917
|
+
const lowered = { ...rest };
|
|
21918
|
+
if (!Object.hasOwn(value, "enabled")) lowered.enabled = disabled !== true;
|
|
21919
|
+
else if (value.enabled === disabled) onDropped?.(`${path}.disabled`, "conflict");
|
|
21920
|
+
const lowTimeout = isRecord$1(timeout) ? lowerOpencodeV2Timeout(timeout) : timeout;
|
|
21921
|
+
if (lowTimeout !== void 0) lowered.timeout = lowTimeout;
|
|
21922
|
+
else if (isRecord$1(timeout) && Object.keys(timeout).length > 0) onDropped?.(`${path}.timeout`, "unsupported");
|
|
21923
|
+
if (isRecord$1(value.oauth)) lowered.oauth = lowerOpencodeV2OAuth(value.oauth, `${path}.oauth`, onDropped);
|
|
21924
|
+
return lowered;
|
|
21925
|
+
}
|
|
21926
|
+
function isPositiveInteger(value) {
|
|
21927
|
+
return typeof value === "number" && Number.isInteger(value) && value > 0;
|
|
21928
|
+
}
|
|
21929
|
+
/** Whether each V2 timeout value a record carries is a positive integer. */
|
|
21930
|
+
function hasPositiveIntegerTimeouts(value) {
|
|
21931
|
+
return OPENCODE_V2_TIMEOUT_KEYS.every((key) => value[key] === void 0 || isPositiveInteger(value[key]));
|
|
21932
|
+
}
|
|
21933
|
+
/**
|
|
21934
|
+
* Whether `mcp.timeout` is V2's global timeout object rather than a server
|
|
21935
|
+
* named `timeout`. As in OpenCode V1, each of its values must be a positive
|
|
21936
|
+
* integer; anything else is read as a server, which the schema then rejects.
|
|
21937
|
+
*/
|
|
21938
|
+
function isOpencodeV2GlobalTimeout(value) {
|
|
21939
|
+
return isRecord$1(value) && !isDirectOpencodeMcpServer(value) && (Object.keys(value).length === 0 || OPENCODE_V2_TIMEOUT_KEYS.some((key) => Object.hasOwn(value, key))) && hasPositiveIntegerTimeouts(value);
|
|
21940
|
+
}
|
|
21941
|
+
function getOpencodeV2McpEnvelope(mcp) {
|
|
21942
|
+
const nested = mcp.servers;
|
|
21943
|
+
return isRecord$1(nested) && !isDirectOpencodeMcpServer(nested) ? nested : null;
|
|
21944
|
+
}
|
|
21945
|
+
/**
|
|
21946
|
+
* Returns the paths of the V2-only containers in an OpenCode `mcp` value that
|
|
21947
|
+
* a write in V1's flat shape does not keep: a non-empty `mcp.servers`
|
|
21948
|
+
* envelope, and a non-empty global `mcp.timeout`.
|
|
21949
|
+
*/
|
|
21950
|
+
function findOpencodeV2McpContainers(mcp) {
|
|
21951
|
+
if (!isRecord$1(mcp)) return [];
|
|
21952
|
+
const envelope = getOpencodeV2McpEnvelope(mcp);
|
|
21953
|
+
return [...envelope !== null && Object.keys(envelope).length > 0 ? ["mcp.servers"] : [], ...isOpencodeV2GlobalTimeout(mcp.timeout) && Object.keys(mcp.timeout).length > 0 ? ["mcp.timeout"] : []];
|
|
21954
|
+
}
|
|
21955
|
+
/**
|
|
21956
|
+
* Lowers OpenCode V2 `mcp` spellings found in a V1 config to the flat V1 map,
|
|
21957
|
+
* the way OpenCode V1 itself does since v1.18.24 (`normalizeMcp`):
|
|
21958
|
+
*
|
|
21959
|
+
* - entries of the `mcp.servers` envelope are merged into the map, a flat entry
|
|
21960
|
+
* of the same name winning;
|
|
21961
|
+
* - V2-spelled server entries (`disabled`, an object `timeout`, snake_case
|
|
21962
|
+
* `oauth` keys) are lowered by {@link lowerOpencodeV2Server};
|
|
21963
|
+
* - the global `mcp.timeout` object is skipped. V1 moves it to
|
|
21964
|
+
* `experimental.mcp_timeout`, a default request timeout that has no
|
|
21965
|
+
* canonical counterpart, so it is not read as a server either.
|
|
21966
|
+
*
|
|
21967
|
+
* Unlike V1, an envelope entry that is not a valid server is not dropped: it
|
|
21968
|
+
* is passed on, so the schema rejects it loudly rather than it vanishing
|
|
21969
|
+
* without a word. Each value that is dropped — a non-empty global timeout, a
|
|
21970
|
+
* per-server timeout object or `oauth` key with no V1 equivalent, or an
|
|
21971
|
+
* envelope entry shadowed by a different flat entry — is reported through
|
|
21972
|
+
* `onDropped`.
|
|
21973
|
+
*
|
|
21974
|
+
* @see https://github.com/anomalyco/opencode/blob/v1.18.35/packages/opencode/src/config/v2-compat.ts
|
|
21975
|
+
* @see https://opencode.ai/v2/docs/mcp-servers/
|
|
21976
|
+
*/
|
|
21977
|
+
function lowerOpencodeV2Mcp(mcp, onDropped) {
|
|
21978
|
+
if (!isRecord$1(mcp)) return mcp;
|
|
21979
|
+
const envelope = getOpencodeV2McpEnvelope(mcp);
|
|
21980
|
+
const globalTimeout = isOpencodeV2GlobalTimeout(mcp.timeout);
|
|
21981
|
+
if (findOpencodeV2McpContainers(mcp).includes("mcp.timeout")) onDropped?.("mcp.timeout", "unsupported");
|
|
21982
|
+
const servers = {};
|
|
21983
|
+
for (const [name, value] of Object.entries(mcp)) {
|
|
21984
|
+
if (name === "servers" && envelope !== null || name === "timeout" && globalTimeout) continue;
|
|
21985
|
+
setOwnEntry(servers, name, isRecord$1(value) && isOpencodeV2NativeServer(value) ? lowerOpencodeV2Server(value, `mcp.${name}`, onDropped) : value);
|
|
21986
|
+
}
|
|
21987
|
+
for (const [name, value] of Object.entries(envelope ?? {})) {
|
|
21988
|
+
const path = `mcp.servers.${name}`;
|
|
21989
|
+
if (Object.hasOwn(servers, name)) {
|
|
21990
|
+
if (!(0, node_util.isDeepStrictEqual)(servers[name], value)) onDropped?.(path, "conflict");
|
|
21991
|
+
continue;
|
|
21992
|
+
}
|
|
21993
|
+
setOwnEntry(servers, name, lowerOpencodeV2Server(value, path, onDropped));
|
|
21994
|
+
}
|
|
21995
|
+
return servers;
|
|
21996
|
+
}
|
|
21829
21997
|
/** Matches a string whose entire value is an OpenCode `{file:...}` reference. */
|
|
21830
21998
|
const OPENCODE_FILE_TEMPLATE_PATTERN = /^\s*\{file:(.+?)\}\s*$/;
|
|
21831
21999
|
/**
|
|
@@ -32857,6 +33025,31 @@ const PI_PROMPT_BLOCKING_EVENT = "input";
|
|
|
32857
33025
|
* @see https://github.com/earendil-works/pi/blob/v0.87.1/packages/coding-agent/src/core/extensions/types.ts
|
|
32858
33026
|
*/
|
|
32859
33027
|
const PI_SETTLE_BLOCKING_EVENT = "agent_before_settle";
|
|
33028
|
+
/**
|
|
33029
|
+
* `session_stop` is oh-my-pi's stop surface (it has no `agent_before_settle`).
|
|
33030
|
+
* It fires when a main-session run is about to settle (never for subagents,
|
|
33031
|
+
* nor for a run the user aborted), and a handler returning
|
|
33032
|
+
* `{ continue: true, additionalContext }` queues one more turn with that text
|
|
33033
|
+
* as hidden model-visible context. As on Pi, only a stop command exiting with
|
|
33034
|
+
* code 2 asks for that, the feedback of every such command is combined, and
|
|
33035
|
+
* any other failure is rethrown for oh-my-pi to report. The generated handler
|
|
33036
|
+
* continues only when `event.stop_hook_active` is false, which oh-my-pi sets
|
|
33037
|
+
* after a continuation and clears once a stop pass lets the run settle, so the
|
|
33038
|
+
* agent is continued at most once per stop (the flag is shared by every
|
|
33039
|
+
* extension, so another extension's continuation also suppresses ours);
|
|
33040
|
+
* oh-my-pi additionally caps these
|
|
33041
|
+
* advisory continuations at 8 in a row. A run that ended on a provider error
|
|
33042
|
+
* (`stopReason: "error"`) is not continued, matching Pi's `completed`-only
|
|
33043
|
+
* gate. The advisory form is used rather than `{ decision: "block", reason }`,
|
|
33044
|
+
* which oh-my-pi keeps enforcing without that cap. oh-my-pi gives
|
|
33045
|
+
* `session_stop` handlers the 30-second extension handler budget: stop
|
|
33046
|
+
* commands running longer time the handler out, the run settles without a
|
|
33047
|
+
* continuation, and the command is not killed.
|
|
33048
|
+
*
|
|
33049
|
+
* @see https://github.com/can1357/oh-my-pi/blob/fa5ff4a3a977a28b93d025f4cf50f046e019f18c/docs/extensions.md
|
|
33050
|
+
* @see https://github.com/can1357/oh-my-pi/blob/fa5ff4a3a977a28b93d025f4cf50f046e019f18c/packages/coding-agent/src/session/agent-session.ts
|
|
33051
|
+
*/
|
|
33052
|
+
const OMP_SESSION_STOP_BLOCKING_EVENT = "session_stop";
|
|
32860
33053
|
const PI_EXTENSION_DIALECT = {
|
|
32861
33054
|
overrideKey: "pi",
|
|
32862
33055
|
typesPackage: "@earendil-works/pi-coding-agent",
|
|
@@ -32878,7 +33071,8 @@ const OMP_EXTENSION_DIALECT = {
|
|
|
32878
33071
|
const PI_BLOCKING_MODE_BY_EVENT = {
|
|
32879
33072
|
[PI_BLOCKING_EVENT]: "tool",
|
|
32880
33073
|
[PI_PROMPT_BLOCKING_EVENT]: "prompt",
|
|
32881
|
-
[PI_SETTLE_BLOCKING_EVENT]: "settle"
|
|
33074
|
+
[PI_SETTLE_BLOCKING_EVENT]: "settle",
|
|
33075
|
+
[OMP_SESSION_STOP_BLOCKING_EVENT]: "sessionStop"
|
|
32882
33076
|
};
|
|
32883
33077
|
/**
|
|
32884
33078
|
* The body emitted inside a blocking handler's `catch`, per gate. `tool_call`
|
|
@@ -32891,7 +33085,8 @@ function failureLinesByMode(dialect) {
|
|
|
32891
33085
|
none: [],
|
|
32892
33086
|
tool: ["return { block: true, reason: toBlockReason(error) };"],
|
|
32893
33087
|
prompt: ["reportPromptGateFailure(ctx, toBlockReason(error));", dialect.promptCancelLine],
|
|
32894
|
-
settle: ["stopHookFailures.push(error);"]
|
|
33088
|
+
settle: ["stopHookFailures.push(error);"],
|
|
33089
|
+
sessionStop: ["stopHookFailures.push(error);"]
|
|
32895
33090
|
};
|
|
32896
33091
|
}
|
|
32897
33092
|
/**
|
|
@@ -32983,6 +33178,12 @@ const PROMPT_GATE_HELPER_LINES = [
|
|
|
32983
33178
|
" console.error(reason);",
|
|
32984
33179
|
"}"
|
|
32985
33180
|
];
|
|
33181
|
+
/** Helper emitted alongside every stop gate (Pi's settle gate and oh-my-pi's `session_stop`). */
|
|
33182
|
+
const STOP_HOOK_BLOCK_HELPER_LINES = [
|
|
33183
|
+
"function isStopHookBlock(error: unknown): boolean {",
|
|
33184
|
+
" return (error as { code?: unknown } | null)?.code === 2;",
|
|
33185
|
+
"}"
|
|
33186
|
+
];
|
|
32986
33187
|
/**
|
|
32987
33188
|
* Helper emitted alongside the stop hook's settle gate. `event.entries` holds
|
|
32988
33189
|
* the drafts earlier extensions proposed at this boundary, so they are kept and
|
|
@@ -32993,10 +33194,6 @@ const PROMPT_GATE_HELPER_LINES = [
|
|
|
32993
33194
|
* appended message is committed, and Pi re-validates the final context itself.
|
|
32994
33195
|
*/
|
|
32995
33196
|
const STOP_HOOK_HELPER_LINES = [
|
|
32996
|
-
"function isStopHookBlock(error: unknown): boolean {",
|
|
32997
|
-
" return (error as { code?: unknown } | null)?.code === 2;",
|
|
32998
|
-
"}",
|
|
32999
|
-
"",
|
|
33000
33197
|
"function continueAfterStopHook(",
|
|
33001
33198
|
" event: AgentBeforeSettleEvent,",
|
|
33002
33199
|
" reason: string,",
|
|
@@ -33079,14 +33276,16 @@ function buildSubscriptionLines(handlerGroups, dialect) {
|
|
|
33079
33276
|
const blocking = PI_BLOCKING_MODE_BY_EVENT[piEvent] ?? "none";
|
|
33080
33277
|
const isPromptGate = blocking === "prompt";
|
|
33081
33278
|
const isSettleGate = blocking === "settle";
|
|
33279
|
+
const isSessionStopGate = blocking === "sessionStop";
|
|
33280
|
+
const collectsStopFailures = isSettleGate || isSessionStopGate;
|
|
33082
33281
|
const usesToolName = PI_TOOL_EVENTS.has(piEvent) && handlers.some((h) => h.matcher);
|
|
33083
33282
|
const usesErrorFlag = handlers.some((h) => h.onlyOnError);
|
|
33084
33283
|
const gatesOnAssistant = PI_ASSISTANT_MESSAGE_EVENTS.has(piEvent);
|
|
33085
|
-
const params = isPromptGate ? "event, ctx" : usesToolName || usesErrorFlag || gatesOnAssistant || isPromptGate ||
|
|
33284
|
+
const params = isPromptGate ? "event, ctx" : usesToolName || usesErrorFlag || gatesOnAssistant || isPromptGate || collectsStopFailures ? "event" : "";
|
|
33086
33285
|
lines.push(` pi.on(${JSON.stringify(piEvent)}, async (${params}) => {`);
|
|
33087
33286
|
if (gatesOnAssistant) lines.push(` if (event.message.role !== "assistant") return;`);
|
|
33088
33287
|
if (isPromptGate) lines.push(` if (event.source === "extension") ${dialect.promptPassLine}`);
|
|
33089
|
-
if (
|
|
33288
|
+
if (collectsStopFailures) lines.push(" const stopHookFailures: unknown[] = [];");
|
|
33090
33289
|
for (const handler of handlers) lines.push(...buildCommandLines({
|
|
33091
33290
|
handler,
|
|
33092
33291
|
usesToolName,
|
|
@@ -33102,6 +33301,17 @@ function buildSubscriptionLines(handlerGroups, dialect) {
|
|
|
33102
33301
|
lines.push(" }");
|
|
33103
33302
|
lines.push(" if (stopHookFailures.length > 0) throw stopHookFailures[0];");
|
|
33104
33303
|
}
|
|
33304
|
+
if (isSessionStopGate) {
|
|
33305
|
+
lines.push(" const blocks = stopHookFailures.filter(isStopHookBlock);");
|
|
33306
|
+
lines.push(" const endedOnError = (event.last_assistant_message as { stopReason?: unknown } | undefined)?.stopReason === \"error\";");
|
|
33307
|
+
lines.push(" if (blocks.length > 0 && !event.stop_hook_active && !endedOnError) {");
|
|
33308
|
+
lines.push(" return {");
|
|
33309
|
+
lines.push(" continue: true,");
|
|
33310
|
+
lines.push(" additionalContext: `Stop hook feedback:\\n${blocks.map(toBlockReason).join(\"\\n\\n\")}`,");
|
|
33311
|
+
lines.push(" };");
|
|
33312
|
+
lines.push(" }");
|
|
33313
|
+
lines.push(" if (stopHookFailures.length > 0) throw stopHookFailures[0];");
|
|
33314
|
+
}
|
|
33105
33315
|
lines.push(" });");
|
|
33106
33316
|
if (isSettleGate) {
|
|
33107
33317
|
lines.push(` pi.on("agent_settled", async () => {`);
|
|
@@ -33120,8 +33330,9 @@ function buildSubscriptionLines(handlerGroups, dialect) {
|
|
|
33120
33330
|
* `{ block: true, reason }`, and on `input` — Pi's prompt-submission gate —
|
|
33121
33331
|
* where a non-zero exit cancels the prompt with the dialect's cancel result
|
|
33122
33332
|
* (`{ action: "handled" }` for Pi, `{ handled: true }` for oh-my-pi), and
|
|
33123
|
-
* on `agent_before_settle
|
|
33124
|
-
* agent to continue once with the
|
|
33333
|
+
* on `agent_before_settle` (Pi) / `session_stop` (oh-my-pi), where a stop
|
|
33334
|
+
* command exiting with code 2 asks the agent to continue once with the
|
|
33335
|
+
* command's output as feedback.
|
|
33125
33336
|
* `postToolUse` and `postToolUseFailure` share Pi's `tool_result` event; the
|
|
33126
33337
|
* latter's commands run only when `event.isError` is set.
|
|
33127
33338
|
*
|
|
@@ -33142,7 +33353,8 @@ function generatePiExtensionCode({ config, supportedEvents, eventMap, dialect =
|
|
|
33142
33353
|
const subscriptionLines = buildSubscriptionLines(handlerGroups, dialect);
|
|
33143
33354
|
const hasPromptGate = Boolean(handlerGroups[PI_PROMPT_BLOCKING_EVENT]);
|
|
33144
33355
|
const hasSettleGate = Boolean(handlerGroups[PI_SETTLE_BLOCKING_EVENT]);
|
|
33145
|
-
const
|
|
33356
|
+
const hasSessionStopGate = Boolean(handlerGroups[OMP_SESSION_STOP_BLOCKING_EVENT]);
|
|
33357
|
+
const needsBlockReasonHelper = Boolean(handlerGroups[PI_BLOCKING_EVENT]) || hasPromptGate || hasSettleGate || hasSessionStopGate;
|
|
33146
33358
|
const lines = ["// Generated by rulesync. Do not edit manually."];
|
|
33147
33359
|
if (subscriptionLines.length === 0) {
|
|
33148
33360
|
lines.push("export default function () {}");
|
|
@@ -33169,6 +33381,10 @@ function generatePiExtensionCode({ config, supportedEvents, eventMap, dialect =
|
|
|
33169
33381
|
lines.push(...PROMPT_GATE_HELPER_LINES);
|
|
33170
33382
|
lines.push("");
|
|
33171
33383
|
}
|
|
33384
|
+
if (hasSettleGate || hasSessionStopGate) {
|
|
33385
|
+
lines.push(...STOP_HOOK_BLOCK_HELPER_LINES);
|
|
33386
|
+
lines.push("");
|
|
33387
|
+
}
|
|
33172
33388
|
if (hasSettleGate) {
|
|
33173
33389
|
lines.push(...STOP_HOOK_HELPER_LINES);
|
|
33174
33390
|
lines.push("");
|
|
@@ -45526,18 +45742,21 @@ var KiroMcp = class KiroMcp extends ToolMcp {
|
|
|
45526
45742
|
};
|
|
45527
45743
|
//#endregion
|
|
45528
45744
|
//#region src/features/mcp/opencode-mcp.ts
|
|
45745
|
+
const OpencodeMcpV2DisabledSchema = zod_mini.z.optional(zod_mini.z.boolean());
|
|
45529
45746
|
const OpencodeMcpLocalServerSchema = zod_mini.z.looseObject({
|
|
45530
45747
|
type: zod_mini.z.literal("local"),
|
|
45531
45748
|
command: zod_mini.z.array(zod_mini.z.string()),
|
|
45532
45749
|
environment: zod_mini.z.optional(zod_mini.z.record(zod_mini.z.string(), zod_mini.z.string())),
|
|
45533
45750
|
enabled: zod_mini.z._default(zod_mini.z.boolean(), true),
|
|
45751
|
+
disabled: OpencodeMcpV2DisabledSchema,
|
|
45534
45752
|
cwd: zod_mini.z.optional(zod_mini.z.string())
|
|
45535
45753
|
});
|
|
45536
45754
|
const OpencodeMcpRemoteServerSchema = zod_mini.z.looseObject({
|
|
45537
45755
|
type: zod_mini.z.literal("remote"),
|
|
45538
45756
|
url: zod_mini.z.string(),
|
|
45539
45757
|
headers: zod_mini.z.optional(zod_mini.z.record(zod_mini.z.string(), zod_mini.z.string())),
|
|
45540
|
-
enabled: zod_mini.z._default(zod_mini.z.boolean(), true)
|
|
45758
|
+
enabled: zod_mini.z._default(zod_mini.z.boolean(), true),
|
|
45759
|
+
disabled: OpencodeMcpV2DisabledSchema
|
|
45541
45760
|
});
|
|
45542
45761
|
const OPENCODE_PASSTHROUGH_SERVER_FIELDS = [
|
|
45543
45762
|
"timeout",
|
|
@@ -45549,7 +45768,7 @@ const OPENCODE_MCP_TRANSPORT_KEYS = [
|
|
|
45549
45768
|
...Object.keys(OpencodeMcpLocalServerSchema.def.shape),
|
|
45550
45769
|
...Object.keys(OpencodeMcpRemoteServerSchema.def.shape),
|
|
45551
45770
|
...OPENCODE_PASSTHROUGH_SERVER_FIELDS
|
|
45552
|
-
].filter((key) => key !== "enabled");
|
|
45771
|
+
].filter((key) => key !== "enabled" && key !== "disabled");
|
|
45553
45772
|
/**
|
|
45554
45773
|
* A bare toggle entry: `{"enabled": <bool>}` with no transport of its own,
|
|
45555
45774
|
* disabling a server another config layer defines. It is the third member of
|
|
@@ -45677,7 +45896,7 @@ const OPENCODE_TOGGLE_KEPT_KEYS = /* @__PURE__ */ new Set([
|
|
|
45677
45896
|
function warnAboutToggleDroppedKeys(serverName, serverConfig, logger) {
|
|
45678
45897
|
const dropped = Object.keys(serverConfig).filter((key) => !OPENCODE_TOGGLE_KEPT_KEYS.has(key));
|
|
45679
45898
|
if (dropped.length === 0) return;
|
|
45680
|
-
logger?.warn(`OpenCode MCP:
|
|
45899
|
+
logger?.warn(`OpenCode MCP: ${quoteValueForWarning(serverName)} declares no transport, so it is written as a toggle entry and ${dropped.toSorted().map(quoteValueForWarning).join(", ")} ${dropped.length === 1 ? "is" : "are"} dropped.`);
|
|
45681
45900
|
}
|
|
45682
45901
|
/**
|
|
45683
45902
|
* Convert standard MCP format to OpenCode native format
|
|
@@ -45757,9 +45976,35 @@ var OpencodeMcp = class extends ToolMcp {
|
|
|
45757
45976
|
/** Directory layout; OpenCode forks (MiMo Code) override it. */
|
|
45758
45977
|
static layout = OPENCODE_LAYOUT;
|
|
45759
45978
|
json;
|
|
45760
|
-
|
|
45979
|
+
/**
|
|
45980
|
+
* With a `logger`, the V2 values that lowering the content drops are warned
|
|
45981
|
+
* about (see {@link OpencodeMcp.lowerConfig}); `fromFile` passes one so the
|
|
45982
|
+
* warnings come from the lowering that builds the parsed view rather than
|
|
45983
|
+
* from an extra pass of their own. (`validate()`, which the parent
|
|
45984
|
+
* constructor calls before this view exists, lowers separately and silently.)
|
|
45985
|
+
*/
|
|
45986
|
+
constructor({ logger, ...params }) {
|
|
45761
45987
|
super(params);
|
|
45762
|
-
this.json = OpencodeConfigSchema.parse((0, jsonc_parser.parse)(this.fileContent || "{}"));
|
|
45988
|
+
this.json = OpencodeConfigSchema.parse(this.constructor.lowerConfig((0, jsonc_parser.parse)(this.fileContent || "{}"), logger));
|
|
45989
|
+
}
|
|
45990
|
+
/**
|
|
45991
|
+
* Lowers the OpenCode V2 `mcp` spellings of a parsed config for a layout
|
|
45992
|
+
* that reads them (see {@link lowerOpencodeV2Mcp}), so every reader — import,
|
|
45993
|
+
* the toggle lookup on generate, and the rules feature's `instructions`
|
|
45994
|
+
* write to the same file — accepts a V2-shaped `mcp`. Only the parsed view
|
|
45995
|
+
* is lowered; content written back keeps whatever `mcp` a feature that does
|
|
45996
|
+
* not own the key found there. With a `logger`, each V2 value the lowering
|
|
45997
|
+
* drops is warned about.
|
|
45998
|
+
*/
|
|
45999
|
+
static lowerConfig(json, logger) {
|
|
46000
|
+
if (!this.layout.readsV2ConfigSpellings || !isRecord$1(json) || json.mcp === void 0) return json;
|
|
46001
|
+
return {
|
|
46002
|
+
...json,
|
|
46003
|
+
mcp: lowerOpencodeV2Mcp(json.mcp, (path, reason) => {
|
|
46004
|
+
const quoted = quoteValueForWarning(path);
|
|
46005
|
+
logger?.warn(reason === "conflict" ? `OpenCode MCP: ${quoted} conflicts with a V1 value that takes precedence (the flat "mcp" entry of the same name, or "enabled"), so it is dropped on import.` : `OpenCode MCP: ${quoted} has no Rulesync equivalent, so it is dropped on import.`);
|
|
46006
|
+
})
|
|
46007
|
+
};
|
|
45763
46008
|
}
|
|
45764
46009
|
getJson() {
|
|
45765
46010
|
return this.json;
|
|
@@ -45776,7 +46021,7 @@ var OpencodeMcp = class extends ToolMcp {
|
|
|
45776
46021
|
relativeFilePath: this.layout.jsonFileName
|
|
45777
46022
|
};
|
|
45778
46023
|
}
|
|
45779
|
-
static async fromFile({ outputRoot = process.cwd(), validate = true, global = false }) {
|
|
46024
|
+
static async fromFile({ outputRoot = process.cwd(), validate = true, global = false, logger }) {
|
|
45780
46025
|
const basePaths = this.getSettablePaths({ global });
|
|
45781
46026
|
const jsonDir = (0, node_path.join)(outputRoot, basePaths.relativeDirPath);
|
|
45782
46027
|
let fileContent = null;
|
|
@@ -45798,7 +46043,8 @@ var OpencodeMcp = class extends ToolMcp {
|
|
|
45798
46043
|
relativeDirPath: basePaths.relativeDirPath,
|
|
45799
46044
|
relativeFilePath,
|
|
45800
46045
|
fileContent: JSON.stringify(newJson, null, 2),
|
|
45801
|
-
validate
|
|
46046
|
+
validate,
|
|
46047
|
+
logger
|
|
45802
46048
|
});
|
|
45803
46049
|
}
|
|
45804
46050
|
static async fromRulesyncMcp({ outputRoot = process.cwd(), rulesyncMcp, validate = true, global = false, logger }) {
|
|
@@ -45817,7 +46063,12 @@ var OpencodeMcp = class extends ToolMcp {
|
|
|
45817
46063
|
mcpServers: rulesyncMcp.getMcpServers(),
|
|
45818
46064
|
replacement: "{env:$1}"
|
|
45819
46065
|
});
|
|
45820
|
-
const
|
|
46066
|
+
const existingJson = (0, jsonc_parser.parse)(fileContent || "{}");
|
|
46067
|
+
const existingMcp = OpencodeConfigSchema.safeParse(this.lowerConfig(existingJson));
|
|
46068
|
+
if (this.layout.readsV2ConfigSpellings && isRecord$1(existingJson)) {
|
|
46069
|
+
const containers = findOpencodeV2McpContainers(existingJson.mcp);
|
|
46070
|
+
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.`);
|
|
46071
|
+
}
|
|
45821
46072
|
const { mcp: convertedMcp, tools: mcpTools } = convertToOpencodeFormat(transformedServers, (existingMcp.success ? existingMcp.data.mcp : void 0) ?? {}, logger);
|
|
45822
46073
|
return new this({
|
|
45823
46074
|
outputRoot,
|
|
@@ -45917,7 +46168,7 @@ var OpencodeMcp = class extends ToolMcp {
|
|
|
45917
46168
|
}
|
|
45918
46169
|
validate() {
|
|
45919
46170
|
const json = parseJsonc$6(this.fileContent || "{}");
|
|
45920
|
-
const result = OpencodeConfigSchema.safeParse(json);
|
|
46171
|
+
const result = OpencodeConfigSchema.safeParse(this.constructor.lowerConfig(json));
|
|
45921
46172
|
if (!result.success) return {
|
|
45922
46173
|
success: false,
|
|
45923
46174
|
error: result.error
|
|
@@ -55965,6 +56216,124 @@ function parseCodebuddySettings({ fileContent, relativePath }) {
|
|
|
55965
56216
|
}
|
|
55966
56217
|
}
|
|
55967
56218
|
//#endregion
|
|
56219
|
+
//#region src/features/permissions/codewhale-project-config.ts
|
|
56220
|
+
/**
|
|
56221
|
+
* The top-level key of the per-project `.codewhale/config.toml` overlay that
|
|
56222
|
+
* hides Codewhale's shell tools. The overlay only tightens: `false` disables
|
|
56223
|
+
* shell for the session, while `true` is ignored with a warning.
|
|
56224
|
+
* @see https://github.com/Hmbown/Codewhale/blob/fead51eeea0fb6038ea3e30923751b1a1c765b25/docs/CONFIGURATION.md#per-project-overlay-485
|
|
56225
|
+
* @see https://github.com/Hmbown/Codewhale/blob/fead51eeea0fb6038ea3e30923751b1a1c765b25/crates/tui/src/lib.rs
|
|
56226
|
+
*/
|
|
56227
|
+
const CODEWHALE_ALLOW_SHELL_KEY = "allow_shell";
|
|
56228
|
+
const CODEWHALE_PROJECT_CONFIG_PATHS = {
|
|
56229
|
+
relativeDirPath: CODEWHALE_DIR,
|
|
56230
|
+
relativeFilePath: CODEWHALE_CONFIG_FILE_NAME
|
|
56231
|
+
};
|
|
56232
|
+
/** The project `.codewhale/config.toml` overlay, as written by the permissions feature. */
|
|
56233
|
+
var CodewhaleProjectConfigFile = class extends ToolFile {
|
|
56234
|
+
validate() {
|
|
56235
|
+
return {
|
|
56236
|
+
success: true,
|
|
56237
|
+
error: null
|
|
56238
|
+
};
|
|
56239
|
+
}
|
|
56240
|
+
};
|
|
56241
|
+
/**
|
|
56242
|
+
* Whether the canonical config denies every shell command: a `deny` on the
|
|
56243
|
+
* catch-all `*` (or `**`) pattern of `bash` or of the all-tools `*` category.
|
|
56244
|
+
* Codewhale decides by action before specificity, so such a deny already
|
|
56245
|
+
* blocks every command in global scope too, whatever `allow` rules sit beside
|
|
56246
|
+
* it.
|
|
56247
|
+
*/
|
|
56248
|
+
function deniesEveryShellCommand(permission) {
|
|
56249
|
+
return collectShellCommandRules(permission).rules.some(({ pattern, action }) => action === "deny" && isShellCatchAll(pattern));
|
|
56250
|
+
}
|
|
56251
|
+
/** Whether a canonical shell pattern names every command. */
|
|
56252
|
+
function isShellCatchAll(pattern) {
|
|
56253
|
+
const trimmed = pattern.trim();
|
|
56254
|
+
return trimmed === "*" || trimmed === "**";
|
|
56255
|
+
}
|
|
56256
|
+
function projectConfigFilePath(outputRoot) {
|
|
56257
|
+
return (0, node_path.join)(outputRoot, CODEWHALE_PROJECT_CONFIG_PATHS.relativeDirPath, CODEWHALE_PROJECT_CONFIG_PATHS.relativeFilePath);
|
|
56258
|
+
}
|
|
56259
|
+
function parseProjectConfig({ fileContent, filePath }) {
|
|
56260
|
+
return parseSharedConfig({
|
|
56261
|
+
format: "toml",
|
|
56262
|
+
fileContent,
|
|
56263
|
+
filePath,
|
|
56264
|
+
invalidRootPolicy: "error"
|
|
56265
|
+
});
|
|
56266
|
+
}
|
|
56267
|
+
/**
|
|
56268
|
+
* Whether an overlay disables shell (`allow_shell = false`). A file that
|
|
56269
|
+
* cannot be read as TOML is reported and treated as not disabling shell.
|
|
56270
|
+
*/
|
|
56271
|
+
function disablesShell({ fileContent, filePath, logger }) {
|
|
56272
|
+
try {
|
|
56273
|
+
return parseProjectConfig({
|
|
56274
|
+
fileContent,
|
|
56275
|
+
filePath
|
|
56276
|
+
})[CODEWHALE_ALLOW_SHELL_KEY] === false;
|
|
56277
|
+
} catch (error) {
|
|
56278
|
+
logger?.warn(`Codewhale permissions: not reading "allow_shell" from ${filePath}, so a shell deny set there is not taken into account: ${formatError(error)}`);
|
|
56279
|
+
return false;
|
|
56280
|
+
}
|
|
56281
|
+
}
|
|
56282
|
+
/**
|
|
56283
|
+
* Whether the project overlay at `outputRoot` disables shell
|
|
56284
|
+
* (`allow_shell = false`), for import.
|
|
56285
|
+
*/
|
|
56286
|
+
async function readCodewhaleProjectShellDisabled({ outputRoot, logger }) {
|
|
56287
|
+
const filePath = projectConfigFilePath(outputRoot);
|
|
56288
|
+
const fileContent = await readFileContentOrNull(filePath);
|
|
56289
|
+
if (fileContent === null) return false;
|
|
56290
|
+
return disablesShell({
|
|
56291
|
+
fileContent,
|
|
56292
|
+
filePath,
|
|
56293
|
+
logger
|
|
56294
|
+
});
|
|
56295
|
+
}
|
|
56296
|
+
/**
|
|
56297
|
+
* The project `.codewhale/config.toml` overlay carrying `allow_shell = false`
|
|
56298
|
+
* when the canonical config denies every shell command, or `null` when the
|
|
56299
|
+
* file needs no change.
|
|
56300
|
+
*
|
|
56301
|
+
* Only `allow_shell` is written; every other overlay key (`model`,
|
|
56302
|
+
* `approval_policy`, ...) is kept, and an existing file that is not valid TOML
|
|
56303
|
+
* is an error only when it has to be written. An existing
|
|
56304
|
+
* `allow_shell = false` is never removed: the overlay cannot widen access, and
|
|
56305
|
+
* a hand-written value cannot be told apart from one rulesync wrote, so it is
|
|
56306
|
+
* reported instead.
|
|
56307
|
+
*/
|
|
56308
|
+
async function createCodewhaleProjectConfigFile({ outputRoot = process.cwd(), config, logger }) {
|
|
56309
|
+
const filePath = projectConfigFilePath(outputRoot);
|
|
56310
|
+
const existingContent = await readFileContentOrNull(filePath);
|
|
56311
|
+
if (!deniesEveryShellCommand(config.permission)) {
|
|
56312
|
+
if (existingContent !== null && disablesShell({
|
|
56313
|
+
fileContent: existingContent,
|
|
56314
|
+
filePath,
|
|
56315
|
+
logger
|
|
56316
|
+
})) logger?.warn(`Codewhale permissions: keeping "allow_shell = false" in ${filePath}; rulesync does not remove it. Delete it by hand to re-enable shell tools in this project, or add bash: { "*": "deny" } to the rulesync permissions to match it.`);
|
|
56317
|
+
return null;
|
|
56318
|
+
}
|
|
56319
|
+
if (parseProjectConfig({
|
|
56320
|
+
fileContent: existingContent ?? "",
|
|
56321
|
+
filePath
|
|
56322
|
+
})[CODEWHALE_ALLOW_SHELL_KEY] === false) return null;
|
|
56323
|
+
return new CodewhaleProjectConfigFile({
|
|
56324
|
+
outputRoot,
|
|
56325
|
+
...CODEWHALE_PROJECT_CONFIG_PATHS,
|
|
56326
|
+
fileContent: applySharedConfigPatch({
|
|
56327
|
+
fileKey: sharedConfigFileKey(CODEWHALE_PROJECT_CONFIG_PATHS),
|
|
56328
|
+
feature: "permissions",
|
|
56329
|
+
existingContent: existingContent ?? "",
|
|
56330
|
+
patch: { [CODEWHALE_ALLOW_SHELL_KEY]: false },
|
|
56331
|
+
filePath,
|
|
56332
|
+
logger
|
|
56333
|
+
})
|
|
56334
|
+
});
|
|
56335
|
+
}
|
|
56336
|
+
//#endregion
|
|
55968
56337
|
//#region src/features/permissions/codewhale-repo-law.ts
|
|
55969
56338
|
/**
|
|
55970
56339
|
* Project-scope Codewhale permissions: the enforced `protected_invariants` of
|
|
@@ -56115,7 +56484,8 @@ function collectRestrictions({ config, logger }) {
|
|
|
56115
56484
|
let skippedAllowCount = 0;
|
|
56116
56485
|
for (const [category, rules] of Object.entries(config.permission)) {
|
|
56117
56486
|
if (!REPO_LAW_CATEGORIES.includes(category)) {
|
|
56118
|
-
|
|
56487
|
+
const entries = Object.entries(rules);
|
|
56488
|
+
if ((category === "bash" && entries.some(([pattern, action]) => action === "deny" && isShellCatchAll(pattern)) ? entries.filter(([, action]) => action === "allow") : entries).length > 0) skippedCategories.push(category);
|
|
56119
56489
|
continue;
|
|
56120
56490
|
}
|
|
56121
56491
|
for (const [pattern, action] of Object.entries(rules)) {
|
|
@@ -56841,17 +57211,32 @@ async function warnOnShadowedConstitution({ outputRoot, logger }) {
|
|
|
56841
57211
|
* Project scope manages the enforced `protected_invariants` of the repo
|
|
56842
57212
|
* constitution `.codewhale/constitution.json` (see `codewhale-repo-law.ts`):
|
|
56843
57213
|
* the project `.codewhale/config.toml` overlay cannot carry rules, but repo
|
|
56844
|
-
* law can hold writes to protected paths.
|
|
57214
|
+
* law can hold writes to protected paths. A deny on every shell command also
|
|
57215
|
+
* sets `allow_shell = false` in that overlay (see
|
|
57216
|
+
* `codewhale-project-config.ts`), which import reads back here.
|
|
56845
57217
|
*
|
|
56846
57218
|
* @see https://github.com/Hmbown/Codewhale/blob/main/docs/CONFIGURATION.md
|
|
56847
57219
|
* @see https://github.com/Hmbown/Codewhale/blob/main/docs/AUTHORIZATION_ORDER.md
|
|
56848
57220
|
*/
|
|
56849
57221
|
var CodewhalePermissions = class CodewhalePermissions extends ToolPermissions {
|
|
56850
|
-
|
|
57222
|
+
/**
|
|
57223
|
+
* Whether the project `.codewhale/config.toml` overlay disables shell
|
|
57224
|
+
* (`allow_shell = false`); read on import in project scope only.
|
|
57225
|
+
*/
|
|
57226
|
+
projectShellDisabled;
|
|
57227
|
+
constructor({ projectShellDisabled = false, ...params }) {
|
|
56851
57228
|
super({
|
|
56852
57229
|
...params,
|
|
56853
57230
|
fileContent: params.fileContent ?? ""
|
|
56854
57231
|
});
|
|
57232
|
+
this.projectShellDisabled = projectShellDisabled;
|
|
57233
|
+
}
|
|
57234
|
+
/**
|
|
57235
|
+
* The project `.codewhale/config.toml` overlay, where a deny on every shell
|
|
57236
|
+
* command becomes `allow_shell = false` beside the user's other overlay keys.
|
|
57237
|
+
*/
|
|
57238
|
+
static getExtraSharedWritePaths(options) {
|
|
57239
|
+
return options?.global ? [] : [{ ...CODEWHALE_PROJECT_CONFIG_PATHS }];
|
|
56855
57240
|
}
|
|
56856
57241
|
/**
|
|
56857
57242
|
* Codewhale's approval card writes to the global file, and the constitution
|
|
@@ -56888,7 +57273,11 @@ var CodewhalePermissions = class CodewhalePermissions extends ToolPermissions {
|
|
|
56888
57273
|
relativeFilePath: paths.relativeFilePath,
|
|
56889
57274
|
fileContent,
|
|
56890
57275
|
validate,
|
|
56891
|
-
global
|
|
57276
|
+
global,
|
|
57277
|
+
projectShellDisabled: global ? false : await readCodewhaleProjectShellDisabled({
|
|
57278
|
+
outputRoot,
|
|
57279
|
+
logger
|
|
57280
|
+
})
|
|
56892
57281
|
});
|
|
56893
57282
|
}
|
|
56894
57283
|
static async fromRulesyncPermissions({ outputRoot = process.cwd(), rulesyncPermissions, logger, global = false }) {
|
|
@@ -56950,11 +57339,15 @@ var CodewhalePermissions = class CodewhalePermissions extends ToolPermissions {
|
|
|
56950
57339
|
toRulesyncPermissions() {
|
|
56951
57340
|
const filePath = (0, node_path.join)(this.getRelativeDirPath(), this.getRelativeFilePath());
|
|
56952
57341
|
if (!this.global) {
|
|
56953
|
-
const
|
|
57342
|
+
const permission = codewhaleConstitutionToCanonical(parseCodewhaleConstitution({
|
|
56954
57343
|
fileContent: this.getFileContent(),
|
|
56955
57344
|
filePath
|
|
56956
|
-
});
|
|
56957
|
-
|
|
57345
|
+
}));
|
|
57346
|
+
if (this.projectShellDisabled) permission.bash = {
|
|
57347
|
+
...permission.bash,
|
|
57348
|
+
"*": "deny"
|
|
57349
|
+
};
|
|
57350
|
+
return this.toRulesyncPermissionsDefault({ fileContent: JSON.stringify({ permission }, null, 2) });
|
|
56958
57351
|
}
|
|
56959
57352
|
const { permission, nativeRules } = codewhaleRulesToCanonical(partitionRuleRecords(parsePermissionsDocument({
|
|
56960
57353
|
fileContent: this.getFileContent(),
|
|
@@ -71883,6 +72276,14 @@ var PermissionsProcessor = class extends FeatureProcessor {
|
|
|
71883
72276
|
logger: this.logger,
|
|
71884
72277
|
global: this.global
|
|
71885
72278
|
});
|
|
72279
|
+
if (this.toolTarget === "codewhale" && !this.global) {
|
|
72280
|
+
const projectConfigFile = await createCodewhaleProjectConfigFile({
|
|
72281
|
+
outputRoot: this.outputRoot,
|
|
72282
|
+
config: effectivePermissions.getJson(),
|
|
72283
|
+
logger: this.logger
|
|
72284
|
+
});
|
|
72285
|
+
return projectConfigFile ? [toolPermissions, projectConfigFile] : [toolPermissions];
|
|
72286
|
+
}
|
|
71886
72287
|
if (this.toolTarget !== "codexcli") return [toolPermissions];
|
|
71887
72288
|
return [toolPermissions, createCodexcliBashRulesFile({
|
|
71888
72289
|
outputRoot: this.outputRoot,
|