rulesync 16.36.0 → 16.38.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/README.md +1 -1
- package/dist/cli/index.cjs +4 -4
- package/dist/cli/index.js +4 -4
- package/dist/cli/index.js.map +1 -1
- package/dist/{import-D_iukHSy.js → import-BiyWNVZY.js} +757 -90
- package/dist/import-BiyWNVZY.js.map +1 -0
- package/dist/{import-D_daTMgR.cjs → import-DKBaTqsY.cjs} +773 -106
- package/dist/index.cjs +1 -1
- package/dist/index.d.cts +5 -1
- package/dist/index.d.cts.map +1 -1
- package/dist/index.d.ts +5 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/package.json +1 -1
- package/dist/import-D_iukHSy.js.map +0 -1
|
@@ -444,36 +444,36 @@ const isFeatureValueEnabled = (value) => {
|
|
|
444
444
|
const parseCommaSeparatedList = (value) => value.split(",").map((s) => s.trim()).filter(Boolean);
|
|
445
445
|
//#endregion
|
|
446
446
|
//#region src/constants/rulesync-paths.ts
|
|
447
|
-
const { join: join$
|
|
447
|
+
const { join: join$351 } = node_path.posix;
|
|
448
448
|
const RULESYNC_CONFIG_RELATIVE_FILE_PATH = "rulesync.jsonc";
|
|
449
449
|
const RULESYNC_LOCAL_CONFIG_RELATIVE_FILE_PATH = "rulesync.local.jsonc";
|
|
450
450
|
const RULESYNC_USER_CONFIG_DIR_NAME = "rulesync";
|
|
451
451
|
const RULESYNC_XDG_CONFIG_HOME_DEFAULT_DIR_NAME = ".config";
|
|
452
452
|
const RULESYNC_RELATIVE_DIR_PATH = ".rulesync";
|
|
453
453
|
const RULES_FEATURE_SUBDIR = "rules";
|
|
454
|
-
const CURATED_RULES_FEATURE_SUBDIR = join$
|
|
454
|
+
const CURATED_RULES_FEATURE_SUBDIR = join$351(RULES_FEATURE_SUBDIR, ".curated");
|
|
455
455
|
const COMMANDS_FEATURE_SUBDIR = "commands";
|
|
456
456
|
const SUBAGENTS_FEATURE_SUBDIR = "subagents";
|
|
457
457
|
const CHECKS_FEATURE_SUBDIR = "checks";
|
|
458
458
|
const SKILLS_FEATURE_SUBDIR = "skills";
|
|
459
|
-
const CURATED_SKILLS_FEATURE_SUBDIR = join$
|
|
460
|
-
const RULESYNC_RULES_RELATIVE_DIR_PATH = join$
|
|
461
|
-
const RULESYNC_CURATED_RULES_RELATIVE_DIR_PATH = join$
|
|
462
|
-
const RULESYNC_COMMANDS_RELATIVE_DIR_PATH = join$
|
|
463
|
-
const RULESYNC_SUBAGENTS_RELATIVE_DIR_PATH = join$
|
|
464
|
-
const RULESYNC_CHECKS_RELATIVE_DIR_PATH = join$
|
|
465
|
-
const RULESYNC_MCP_RELATIVE_FILE_PATH = join$
|
|
466
|
-
const RULESYNC_HOOKS_RELATIVE_FILE_PATH = join$
|
|
467
|
-
const RULESYNC_PERMISSIONS_RELATIVE_FILE_PATH = join$
|
|
468
|
-
join$
|
|
469
|
-
const RULESYNC_HOOKS_LEGACY_RELATIVE_FILE_PATH = join$
|
|
470
|
-
const RULESYNC_PERMISSIONS_LEGACY_RELATIVE_FILE_PATH = join$
|
|
459
|
+
const CURATED_SKILLS_FEATURE_SUBDIR = join$351(SKILLS_FEATURE_SUBDIR, ".curated");
|
|
460
|
+
const RULESYNC_RULES_RELATIVE_DIR_PATH = join$351(RULESYNC_RELATIVE_DIR_PATH, RULES_FEATURE_SUBDIR);
|
|
461
|
+
const RULESYNC_CURATED_RULES_RELATIVE_DIR_PATH = join$351(RULESYNC_RELATIVE_DIR_PATH, CURATED_RULES_FEATURE_SUBDIR);
|
|
462
|
+
const RULESYNC_COMMANDS_RELATIVE_DIR_PATH = join$351(RULESYNC_RELATIVE_DIR_PATH, COMMANDS_FEATURE_SUBDIR);
|
|
463
|
+
const RULESYNC_SUBAGENTS_RELATIVE_DIR_PATH = join$351(RULESYNC_RELATIVE_DIR_PATH, SUBAGENTS_FEATURE_SUBDIR);
|
|
464
|
+
const RULESYNC_CHECKS_RELATIVE_DIR_PATH = join$351(RULESYNC_RELATIVE_DIR_PATH, CHECKS_FEATURE_SUBDIR);
|
|
465
|
+
const RULESYNC_MCP_RELATIVE_FILE_PATH = join$351(RULESYNC_RELATIVE_DIR_PATH, "mcp.jsonc");
|
|
466
|
+
const RULESYNC_HOOKS_RELATIVE_FILE_PATH = join$351(RULESYNC_RELATIVE_DIR_PATH, "hooks.jsonc");
|
|
467
|
+
const RULESYNC_PERMISSIONS_RELATIVE_FILE_PATH = join$351(RULESYNC_RELATIVE_DIR_PATH, "permissions.jsonc");
|
|
468
|
+
join$351(RULESYNC_RELATIVE_DIR_PATH, "mcp.json");
|
|
469
|
+
const RULESYNC_HOOKS_LEGACY_RELATIVE_FILE_PATH = join$351(RULESYNC_RELATIVE_DIR_PATH, "hooks.json");
|
|
470
|
+
const RULESYNC_PERMISSIONS_LEGACY_RELATIVE_FILE_PATH = join$351(RULESYNC_RELATIVE_DIR_PATH, "permissions.json");
|
|
471
471
|
const RULESYNC_AIIGNORE_FILE_NAME = ".aiignore";
|
|
472
|
-
const RULESYNC_AIIGNORE_RELATIVE_FILE_PATH = join$
|
|
472
|
+
const RULESYNC_AIIGNORE_RELATIVE_FILE_PATH = join$351(RULESYNC_RELATIVE_DIR_PATH, ".aiignore");
|
|
473
473
|
const RULESYNC_IGNORE_RELATIVE_FILE_PATH = ".rulesyncignore";
|
|
474
474
|
const RULESYNC_OVERVIEW_FILE_NAME = "overview.md";
|
|
475
|
-
const RULESYNC_SKILLS_RELATIVE_DIR_PATH = join$
|
|
476
|
-
const RULESYNC_CURATED_SKILLS_RELATIVE_DIR_PATH = join$
|
|
475
|
+
const RULESYNC_SKILLS_RELATIVE_DIR_PATH = join$351(RULESYNC_RELATIVE_DIR_PATH, SKILLS_FEATURE_SUBDIR);
|
|
476
|
+
const RULESYNC_CURATED_SKILLS_RELATIVE_DIR_PATH = join$351(RULESYNC_RELATIVE_DIR_PATH, CURATED_SKILLS_FEATURE_SUBDIR);
|
|
477
477
|
const RULESYNC_SOURCES_LOCK_RELATIVE_FILE_PATH = "rulesync.lock";
|
|
478
478
|
const RULESYNC_NPM_SOURCES_LOCK_RELATIVE_FILE_PATH = "rulesync-npm.lock.json";
|
|
479
479
|
const RULESYNC_MCP_FILE_NAME = "mcp.jsonc";
|
|
@@ -616,6 +616,7 @@ const mcpProcessorToolTargetTuple = [
|
|
|
616
616
|
"junie",
|
|
617
617
|
"musecode",
|
|
618
618
|
"opencode",
|
|
619
|
+
"pool",
|
|
619
620
|
"qwencode",
|
|
620
621
|
"reasonix",
|
|
621
622
|
"roo",
|
|
@@ -3329,6 +3330,8 @@ const SHARED_USER_MANAGED_CONFIG_PATHS = [
|
|
|
3329
3330
|
".grok/config.toml",
|
|
3330
3331
|
".rovodev/config.yml",
|
|
3331
3332
|
".rovodev/mcp.json",
|
|
3333
|
+
".poolside/settings.yaml",
|
|
3334
|
+
".config/poolside/settings.yaml",
|
|
3332
3335
|
".tabnine/agent/settings.json",
|
|
3333
3336
|
".vibe/config.toml",
|
|
3334
3337
|
".zcode/config.json",
|
|
@@ -4378,7 +4381,7 @@ const PI_HOOK_EVENTS = [
|
|
|
4378
4381
|
* adapter. Amp's `agent.start` / `agent.end` events describe the main agent
|
|
4379
4382
|
* turn, not a subagent lifecycle, so they map to `beforeSubmitPrompt` / `stop`.
|
|
4380
4383
|
*
|
|
4381
|
-
* @see https://ampcode.com/
|
|
4384
|
+
* @see https://ampcode.com/docs/plugin-api
|
|
4382
4385
|
*/
|
|
4383
4386
|
const AMP_HOOK_EVENTS = [
|
|
4384
4387
|
"sessionStart",
|
|
@@ -6223,6 +6226,7 @@ const McpServerSchema = zod_mini.z.looseObject({
|
|
|
6223
6226
|
crushOauthClientSecret: zod_mini.z.optional(zod_mini.z.string()),
|
|
6224
6227
|
crushOauthCallbackPort: zod_mini.z.optional(zod_mini.z.int()),
|
|
6225
6228
|
crushSessionless: zod_mini.z.optional(zod_mini.z.boolean()),
|
|
6229
|
+
poolAllow: zod_mini.z.optional(zod_mini.z.array(zod_mini.z.string())),
|
|
6226
6230
|
headers: zod_mini.z.optional(zod_mini.z.record(zod_mini.z.string(), zod_mini.z.string())),
|
|
6227
6231
|
/**
|
|
6228
6232
|
* The canonical per-server tool allowlist.
|
|
@@ -6654,6 +6658,7 @@ var RulesyncMcp = class RulesyncMcp extends RulesyncFile {
|
|
|
6654
6658
|
"crushOauthClientSecret",
|
|
6655
6659
|
"crushOauthCallbackPort",
|
|
6656
6660
|
"crushSessionless",
|
|
6661
|
+
"poolAllow",
|
|
6657
6662
|
"oauth_client_id",
|
|
6658
6663
|
"oauth_client_secret",
|
|
6659
6664
|
"oauth_callback_port",
|
|
@@ -8341,7 +8346,7 @@ const PERMISSION_OVERRIDE_KEY_ALIASES = {
|
|
|
8341
8346
|
* them; treating a segment that carries one as static could name a directory
|
|
8342
8347
|
* that no matched file actually lives under.
|
|
8343
8348
|
*/
|
|
8344
|
-
const GLOB_METACHARACTERS = /[*?[\]{}()!]/;
|
|
8349
|
+
const GLOB_METACHARACTERS$1 = /[*?[\]{}()!]/;
|
|
8345
8350
|
/**
|
|
8346
8351
|
* The directory every file matched by `glob` lives under, or `undefined` when
|
|
8347
8352
|
* the pattern pins down no such directory.
|
|
@@ -8374,7 +8379,7 @@ function getGlobStaticPrefix(glob) {
|
|
|
8374
8379
|
const lastIndex = segments.length - 1;
|
|
8375
8380
|
const prefix = [];
|
|
8376
8381
|
for (const [index, segment] of segments.entries()) {
|
|
8377
|
-
if (segment === "" || GLOB_METACHARACTERS.test(segment)) break;
|
|
8382
|
+
if (segment === "" || GLOB_METACHARACTERS$1.test(segment)) break;
|
|
8378
8383
|
if (segment === ".") continue;
|
|
8379
8384
|
if (index === lastIndex) break;
|
|
8380
8385
|
prefix.push(segment);
|
|
@@ -8445,6 +8450,7 @@ const RulesyncRuleFrontmatterSchema = zod_mini.z.object({
|
|
|
8445
8450
|
trigger: zod_mini.z.optional(zod_mini.z.string()),
|
|
8446
8451
|
globs: zod_mini.z.optional(zod_mini.z.array(zod_mini.z.string()))
|
|
8447
8452
|
})),
|
|
8453
|
+
aiassistant: zod_mini.z.optional(zod_mini.z.looseObject({ apply: zod_mini.z.optional(zod_mini.z.string()) })),
|
|
8448
8454
|
devin: zod_mini.z.optional(zod_mini.z.looseObject({
|
|
8449
8455
|
trigger: zod_mini.z.optional(zod_mini.z.string()),
|
|
8450
8456
|
globs: zod_mini.z.optional(zod_mini.z.array(zod_mini.z.string())),
|
|
@@ -9669,6 +9675,8 @@ const RulesyncSkillFrontmatterSchema = zod_mini.z.looseObject({
|
|
|
9669
9675
|
paths: zod_mini.z.optional(zod_mini.z.union([zod_mini.z.string(), zod_mini.z.array(zod_mini.z.string())])),
|
|
9670
9676
|
"disable-model-invocation": zod_mini.z.optional(zod_mini.z.boolean()),
|
|
9671
9677
|
"user-invocable": zod_mini.z.optional(zod_mini.z.boolean()),
|
|
9678
|
+
icon: zod_mini.z.optional(zod_mini.z.string()),
|
|
9679
|
+
color: zod_mini.z.optional(zod_mini.z.string()),
|
|
9672
9680
|
metadata: zod_mini.z.optional(zod_mini.z.looseObject({}))
|
|
9673
9681
|
})),
|
|
9674
9682
|
factorydroid: zod_mini.z.optional(zod_mini.z.looseObject({
|
|
@@ -12946,6 +12954,8 @@ const KIMI_CODE_HOME_CONFIG_SHARED_FILE_KEY = "config.toml";
|
|
|
12946
12954
|
const REASONIX_PROJECT_CONFIG_SHARED_FILE_KEY = "reasonix.toml";
|
|
12947
12955
|
const REASONIX_GLOBAL_CONFIG_SHARED_FILE_KEY = ".reasonix/config.toml";
|
|
12948
12956
|
const ROVODEV_CONFIG_SHARED_FILE_KEY = ".rovodev/config.yml";
|
|
12957
|
+
const POOL_PROJECT_SETTINGS_SHARED_FILE_KEY = ".poolside/settings.yaml";
|
|
12958
|
+
const POOL_GLOBAL_SETTINGS_SHARED_FILE_KEY = ".config/poolside/settings.yaml";
|
|
12949
12959
|
/**
|
|
12950
12960
|
* Build the `SHARED_CONFIG_OWNERSHIP` lookup key from a tool's settable paths.
|
|
12951
12961
|
* Mirrors `sharedFileKey` in `src/lib/shared-file-derive.ts` (kept separate so
|
|
@@ -13027,6 +13037,20 @@ const KIMI_CODE_CONFIG_DECLARATION = {
|
|
|
13027
13037
|
}
|
|
13028
13038
|
};
|
|
13029
13039
|
/**
|
|
13040
|
+
* Pool settings (`.poolside/settings.yaml` / `~/.config/poolside/settings.yaml`):
|
|
13041
|
+
* the user's primary Pool settings file, so an unparseable root fails closed
|
|
13042
|
+
* rather than being replaced with generated output. `mcp_servers` is owned as
|
|
13043
|
+
* a whole key because the MCP writer emits the complete server map.
|
|
13044
|
+
*/
|
|
13045
|
+
const POOL_SETTINGS_DECLARATION = {
|
|
13046
|
+
format: "yaml",
|
|
13047
|
+
invalidRootPolicy: "error",
|
|
13048
|
+
features: { mcp: {
|
|
13049
|
+
kind: "replace-owned-keys",
|
|
13050
|
+
ownedKeys: ["mcp_servers"]
|
|
13051
|
+
} }
|
|
13052
|
+
};
|
|
13053
|
+
/**
|
|
13030
13054
|
* ZCode's settings file, which also carries model/theme/permission keys
|
|
13031
13055
|
* rulesync does not own. Both copies are the user's primary ZCode config, so
|
|
13032
13056
|
* every writer refuses to read-modify-write a file it could not parse rather
|
|
@@ -13131,6 +13155,8 @@ const SHARED_CONFIG_OWNERSHIP = {
|
|
|
13131
13155
|
[HERMES_CONFIG_SHARED_FILE_KEY]: HERMES_CONFIG_DECLARATION,
|
|
13132
13156
|
[HERMES_WIN32_CONFIG_SHARED_FILE_KEY]: HERMES_CONFIG_DECLARATION,
|
|
13133
13157
|
[HERMES_HOME_CONFIG_SHARED_FILE_KEY]: HERMES_CONFIG_DECLARATION,
|
|
13158
|
+
[POOL_PROJECT_SETTINGS_SHARED_FILE_KEY]: POOL_SETTINGS_DECLARATION,
|
|
13159
|
+
[POOL_GLOBAL_SETTINGS_SHARED_FILE_KEY]: POOL_SETTINGS_DECLARATION,
|
|
13134
13160
|
[TAKT_CONFIG_SHARED_FILE_KEY]: {
|
|
13135
13161
|
format: "yaml",
|
|
13136
13162
|
invalidRootPolicy: "error",
|
|
@@ -23683,14 +23709,23 @@ var ContinueHooks = class extends SettingsJsonHooks {
|
|
|
23683
23709
|
* portable `command` field is written, which upstream copies to both when
|
|
23684
23710
|
* neither is present. Note the cloud agent runs hooks in a Linux sandbox and
|
|
23685
23711
|
* honors only `bash` and `command` — a `powershell` entry is ignored there.
|
|
23712
|
+
* VS Code reads the same file and maps `bash` to `osx` / `linux` and
|
|
23713
|
+
* `powershell` to `windows`; it also documents its own per-OS overrides
|
|
23714
|
+
* (`windows`, `linux`, `osx`) next to `command`, which are typed here so an
|
|
23715
|
+
* entry that uses them instead of a shell field is not mistaken for one
|
|
23716
|
+
* without a command.
|
|
23686
23717
|
*
|
|
23687
23718
|
* @see https://docs.github.com/en/copilot/reference/hooks-reference
|
|
23719
|
+
* @see https://code.visualstudio.com/docs/agents/reference/hooks-reference
|
|
23688
23720
|
*/
|
|
23689
23721
|
const CopilotHookEntrySchema = zod_mini.z.looseObject({
|
|
23690
23722
|
type: zod_mini.z.string(),
|
|
23691
23723
|
bash: zod_mini.z.optional(zod_mini.z.string()),
|
|
23692
23724
|
powershell: zod_mini.z.optional(zod_mini.z.string()),
|
|
23693
23725
|
command: zod_mini.z.optional(zod_mini.z.string()),
|
|
23726
|
+
windows: zod_mini.z.optional(zod_mini.z.string()),
|
|
23727
|
+
linux: zod_mini.z.optional(zod_mini.z.string()),
|
|
23728
|
+
osx: zod_mini.z.optional(zod_mini.z.string()),
|
|
23694
23729
|
cwd: zod_mini.z.optional(zod_mini.z.string()),
|
|
23695
23730
|
env: zod_mini.z.optional(zod_mini.z.record(zod_mini.z.string(), zod_mini.z.string())),
|
|
23696
23731
|
timeoutSec: zod_mini.z.optional(zod_mini.z.number()),
|
|
@@ -23781,17 +23816,30 @@ function resolveImportCommand$1(entry, logger) {
|
|
|
23781
23816
|
*
|
|
23782
23817
|
* Generate re-emits any non-canonical key verbatim through `rest`, so a key
|
|
23783
23818
|
* dropped here does not survive an import → generate round trip. `cwd` is a
|
|
23784
|
-
* documented Copilot hook field and was previously lost that way
|
|
23819
|
+
* documented Copilot hook field and was previously lost that way, as were the
|
|
23820
|
+
* VS Code per-OS overrides `windows` / `linux` / `osx`: an entry authored with
|
|
23821
|
+
* only those (no `command`) imported as a hook with no command at all and was
|
|
23822
|
+
* regenerated empty.
|
|
23785
23823
|
*
|
|
23786
23824
|
* @see https://docs.github.com/en/copilot/reference/hooks-reference
|
|
23787
23825
|
* @see https://docs.github.com/en/copilot/concepts/agents/coding-agent/about-hooks
|
|
23826
|
+
* @see https://code.visualstudio.com/docs/agents/reference/hooks-reference
|
|
23788
23827
|
*/
|
|
23789
23828
|
function importPassthrough$1(entry) {
|
|
23790
23829
|
const passthrough = {};
|
|
23791
23830
|
if (entry.cwd !== void 0) passthrough.cwd = entry.cwd;
|
|
23792
23831
|
if (entry.env !== void 0) passthrough.env = entry.env;
|
|
23832
|
+
for (const key of OS_OVERRIDE_KEYS) if (entry[key] !== void 0) passthrough[key] = entry[key];
|
|
23793
23833
|
return passthrough;
|
|
23794
23834
|
}
|
|
23835
|
+
const OS_OVERRIDE_KEYS = [
|
|
23836
|
+
"windows",
|
|
23837
|
+
"linux",
|
|
23838
|
+
"osx"
|
|
23839
|
+
];
|
|
23840
|
+
function hasOsOverride(passthrough) {
|
|
23841
|
+
return OS_OVERRIDE_KEYS.some((key) => passthrough[key] !== void 0);
|
|
23842
|
+
}
|
|
23795
23843
|
/**
|
|
23796
23844
|
* Extract hooks from Copilot hooks JSON into canonical format.
|
|
23797
23845
|
* Copilot format: { version: 1, hooks: { eventName: [...hookEntries] } }
|
|
@@ -23812,12 +23860,14 @@ function copilotHooksToCanonical(copilotHooks, logger) {
|
|
|
23812
23860
|
const entry = parseResult.data;
|
|
23813
23861
|
const { command, shell } = resolveImportCommand$1(entry, logger);
|
|
23814
23862
|
const timeout = entry.timeoutSec ?? entry.timeout;
|
|
23863
|
+
const passthrough = importPassthrough$1(entry);
|
|
23864
|
+
if (command === void 0 && hasOsOverride(passthrough)) logger?.warn(`Copilot hook on '${copilotEventName}' has only VS Code per-OS overrides (windows/linux/osx) and no portable command; it is preserved for copilot, but other targets will generate it without a command until a \`command\` is added.`);
|
|
23815
23865
|
defs.push({
|
|
23816
23866
|
type: "command",
|
|
23817
23867
|
...command !== void 0 && { command },
|
|
23818
23868
|
...shell !== void 0 && { shell },
|
|
23819
23869
|
...timeout !== void 0 && { timeout },
|
|
23820
|
-
...
|
|
23870
|
+
...passthrough
|
|
23821
23871
|
});
|
|
23822
23872
|
}
|
|
23823
23873
|
if (defs.length > 0) canonical[eventName] = defs;
|
|
@@ -23980,6 +24030,8 @@ const CopilotCliHookEntrySchema = zod_mini.z.looseObject({
|
|
|
23980
24030
|
bash: zod_mini.z.optional(zod_mini.z.string()),
|
|
23981
24031
|
powershell: zod_mini.z.optional(zod_mini.z.string()),
|
|
23982
24032
|
command: zod_mini.z.optional(zod_mini.z.string()),
|
|
24033
|
+
exec: zod_mini.z.optional(zod_mini.z.string()),
|
|
24034
|
+
args: zod_mini.z.optional(zod_mini.z.array(zod_mini.z.string())),
|
|
23983
24035
|
prompt: zod_mini.z.optional(zod_mini.z.string()),
|
|
23984
24036
|
url: zod_mini.z.optional(zod_mini.z.string()),
|
|
23985
24037
|
headers: zod_mini.z.optional(zod_mini.z.record(zod_mini.z.string(), zod_mini.z.string())),
|
|
@@ -24009,6 +24061,32 @@ function resolveExportMatcherPart({ matcher, matcherSupported, eventName, logger
|
|
|
24009
24061
|
return {};
|
|
24010
24062
|
}
|
|
24011
24063
|
/**
|
|
24064
|
+
* Pick the command fields of an exported `command` entry.
|
|
24065
|
+
*
|
|
24066
|
+
* A canonical `args` list (an empty one counts) selects the CLI's shell-free
|
|
24067
|
+
* form: `command` becomes `exec` and the list is written as `args`, so the
|
|
24068
|
+
* executable is spawned directly. The `shell` selector is meaningless there
|
|
24069
|
+
* and is dropped with a warning. Otherwise the canonical `shell` selector
|
|
24070
|
+
* picks the shell-specific field; without it the portable `command` field is
|
|
24071
|
+
* written, which upstream copies to both `bash` and `powershell`. Keying off
|
|
24072
|
+
* `shell` rather than `process.platform` keeps the generated file identical
|
|
24073
|
+
* on every machine.
|
|
24074
|
+
*/
|
|
24075
|
+
function resolveExportCommandPart({ def, eventName, logger }) {
|
|
24076
|
+
if (Array.isArray(def.args)) {
|
|
24077
|
+
if (def.shell !== void 0) logger?.warn(`Copilot CLI runs an \`exec\` hook without a shell; dropping shell "${def.shell}" on '${eventName}'.`);
|
|
24078
|
+
return compact({
|
|
24079
|
+
exec: def.command,
|
|
24080
|
+
args: def.args,
|
|
24081
|
+
env: def.env
|
|
24082
|
+
});
|
|
24083
|
+
}
|
|
24084
|
+
return compact({
|
|
24085
|
+
[def.shell ?? "command"]: def.command,
|
|
24086
|
+
env: def.env
|
|
24087
|
+
});
|
|
24088
|
+
}
|
|
24089
|
+
/**
|
|
24012
24090
|
* Build the exported entries for a single canonical event. Returns an empty
|
|
24013
24091
|
* array when no entries are emitted (e.g. all prompt hooks were skipped).
|
|
24014
24092
|
*/
|
|
@@ -24048,19 +24126,17 @@ function buildCopilotCliEntriesForEvent({ eventName, definitions, canonicalSchem
|
|
|
24048
24126
|
...timeoutPart,
|
|
24049
24127
|
...rest
|
|
24050
24128
|
});
|
|
24051
|
-
else if (hookType === "command") {
|
|
24052
|
-
|
|
24053
|
-
|
|
24054
|
-
|
|
24055
|
-
|
|
24056
|
-
|
|
24057
|
-
|
|
24058
|
-
|
|
24059
|
-
|
|
24060
|
-
|
|
24061
|
-
|
|
24062
|
-
});
|
|
24063
|
-
}
|
|
24129
|
+
else if (hookType === "command") entries.push({
|
|
24130
|
+
type: "command",
|
|
24131
|
+
...matcherPart,
|
|
24132
|
+
...resolveExportCommandPart({
|
|
24133
|
+
def,
|
|
24134
|
+
eventName,
|
|
24135
|
+
logger
|
|
24136
|
+
}),
|
|
24137
|
+
...timeoutPart,
|
|
24138
|
+
...rest
|
|
24139
|
+
});
|
|
24064
24140
|
}
|
|
24065
24141
|
return entries;
|
|
24066
24142
|
}
|
|
@@ -24100,6 +24176,10 @@ function importPassthrough(entry) {
|
|
|
24100
24176
|
/**
|
|
24101
24177
|
* Resolve the canonical command and its `shell` selector from an imported entry.
|
|
24102
24178
|
*
|
|
24179
|
+
* The CLI-only `exec` form maps to canonical `command` + `args`, the exec form
|
|
24180
|
+
* shared with AugmentCode and Claude Code, so it no longer imports as a hook
|
|
24181
|
+
* with no command at all.
|
|
24182
|
+
*
|
|
24103
24183
|
* A shell-specific field carries its `shell` through so re-export writes the
|
|
24104
24184
|
* same field back. An entry using only the portable `command` field leaves
|
|
24105
24185
|
* `shell` unset, which re-export renders as the portable field again.
|
|
@@ -24113,6 +24193,13 @@ function importPassthrough(entry) {
|
|
|
24113
24193
|
function resolveImportCommand(entry, logger) {
|
|
24114
24194
|
const hasBash = typeof entry.bash === "string";
|
|
24115
24195
|
const hasPowershell = typeof entry.powershell === "string";
|
|
24196
|
+
if (typeof entry.exec === "string") {
|
|
24197
|
+
if (hasBash || hasPowershell || typeof entry.command === "string") logger?.warn("Copilot CLI hook has both exec and a shell command; using exec and ignoring the shell fields, as the hooks reference says not to combine them.");
|
|
24198
|
+
return {
|
|
24199
|
+
command: entry.exec,
|
|
24200
|
+
args: entry.args ?? []
|
|
24201
|
+
};
|
|
24202
|
+
}
|
|
24116
24203
|
if (hasBash && hasPowershell) {
|
|
24117
24204
|
logger?.warn("Copilot CLI hook has both bash and powershell commands; using bash and ignoring powershell, so the imported config does not depend on the machine the import ran on.");
|
|
24118
24205
|
return {
|
|
@@ -24162,11 +24249,12 @@ function copilotCliHooksToCanonical(rawHooks, logger) {
|
|
|
24162
24249
|
...passthrough
|
|
24163
24250
|
});
|
|
24164
24251
|
else {
|
|
24165
|
-
const { command, shell } = resolveImportCommand(entry, logger);
|
|
24252
|
+
const { command, shell, args } = resolveImportCommand(entry, logger);
|
|
24166
24253
|
defs.push({
|
|
24167
24254
|
type: "command",
|
|
24168
24255
|
...command !== void 0 && { command },
|
|
24169
24256
|
...shell !== void 0 && { shell },
|
|
24257
|
+
...args !== void 0 && { args },
|
|
24170
24258
|
...matcherPart,
|
|
24171
24259
|
...timeoutPart,
|
|
24172
24260
|
...passthrough
|
|
@@ -24716,6 +24804,7 @@ var CursorHooks = class CursorHooks extends ToolHooks {
|
|
|
24716
24804
|
...def.loop_limit !== void 0 && { loop_limit: def.loop_limit },
|
|
24717
24805
|
...def.matcher !== void 0 && def.matcher !== null && { matcher: def.matcher },
|
|
24718
24806
|
...def.prompt !== void 0 && def.prompt !== null && { prompt: def.prompt },
|
|
24807
|
+
...def.type === "prompt" && def.model !== void 0 && def.model !== null && { model: def.model },
|
|
24719
24808
|
...def.failClosed !== void 0 && def.failClosed !== null && { failClosed: def.failClosed }
|
|
24720
24809
|
}));
|
|
24721
24810
|
if (mappedDefs.length > 0) mappedHooks[cursorEventName] = mappedDefs;
|
|
@@ -37053,6 +37142,363 @@ var OpencodeMcp = class OpencodeMcp extends ToolMcp {
|
|
|
37053
37142
|
}
|
|
37054
37143
|
};
|
|
37055
37144
|
//#endregion
|
|
37145
|
+
//#region src/constants/pool-paths.ts
|
|
37146
|
+
/**
|
|
37147
|
+
* Pool (Poolside's coding agent CLI) configuration-layout conventions.
|
|
37148
|
+
*
|
|
37149
|
+
* Pool reads `AGENTS.md` instruction files the same way the AGENTS.md standard
|
|
37150
|
+
* describes them: the personal `~/.config/poolside/AGENTS.md` (Pool itself
|
|
37151
|
+
* honours `XDG_CONFIG_HOME` upstream; rulesync writes only the XDG-default
|
|
37152
|
+
* path), the project-root `AGENTS.md`, and nested per-directory
|
|
37153
|
+
* `AGENTS.md` files from the repository root down through the working
|
|
37154
|
+
* directory, deeper files taking precedence. It skips ignored directories
|
|
37155
|
+
* (`.git/`, `node_modules/`, cache directories, repository ignore rules).
|
|
37156
|
+
*
|
|
37157
|
+
* Skills follow the Agent Skills format (`<name>/SKILL.md` bundles). Pool
|
|
37158
|
+
* scans `.poolside/skills/` (project) and `~/.config/poolside/skills/`
|
|
37159
|
+
* (global) plus the shared `.agents/skills/` / `~/.agents/skills/` roots and
|
|
37160
|
+
* the skill directories of other Agent Skills tools; rulesync writes only the
|
|
37161
|
+
* Pool-specific roots and leaves the shared ones to their own targets.
|
|
37162
|
+
*
|
|
37163
|
+
* @see https://docs.poolside.ai/agent-instructions
|
|
37164
|
+
* @see https://docs.poolside.ai/skills
|
|
37165
|
+
* @see https://github.com/poolsideai/pool
|
|
37166
|
+
*/
|
|
37167
|
+
/** Project-scoped `.poolside/` directory at the project root. */
|
|
37168
|
+
const POOL_DIR = ".poolside";
|
|
37169
|
+
/** Global config directory for Pool, relative to the home directory. */
|
|
37170
|
+
const POOL_GLOBAL_DIR = (0, node_path.join)(".config", "poolside");
|
|
37171
|
+
/** Project skills root, relative to the project root. */
|
|
37172
|
+
const POOL_SKILLS_DIR_PATH = (0, node_path.join)(POOL_DIR, "skills");
|
|
37173
|
+
/** Global skills root, relative to the home directory. */
|
|
37174
|
+
const POOL_GLOBAL_SKILLS_DIR_PATH = (0, node_path.join)(POOL_GLOBAL_DIR, "skills");
|
|
37175
|
+
/**
|
|
37176
|
+
* Pool's settings file name. MCP servers live under the top-level
|
|
37177
|
+
* `mcp_servers` key of `.poolside/settings.yaml` (project, committed) and
|
|
37178
|
+
* `~/.config/poolside/settings.yaml` (global); the untracked
|
|
37179
|
+
* `.poolside/settings.local.yaml` overlay is left to the user.
|
|
37180
|
+
*
|
|
37181
|
+
* @see https://docs.poolside.ai/mcp-servers
|
|
37182
|
+
* @see https://docs.poolside.ai/settings-file-reference
|
|
37183
|
+
*/
|
|
37184
|
+
const POOL_SETTINGS_FILE_NAME = "settings.yaml";
|
|
37185
|
+
/** Top-level key of Pool's settings file that holds the MCP server map. */
|
|
37186
|
+
const POOL_MCP_SERVERS_KEY = "mcp_servers";
|
|
37187
|
+
//#endregion
|
|
37188
|
+
//#region src/features/mcp/pool-mcp.ts
|
|
37189
|
+
/**
|
|
37190
|
+
* Single spelling of the settings.yaml codec/policy, matching the
|
|
37191
|
+
* `SHARED_CONFIG_OWNERSHIP` declaration for both scopes: fail closed on an
|
|
37192
|
+
* unparseable root rather than replacing the user's primary Pool settings with
|
|
37193
|
+
* generated output.
|
|
37194
|
+
*/
|
|
37195
|
+
function parsePoolSettings(fileContent, filePath) {
|
|
37196
|
+
return parseSharedConfig({
|
|
37197
|
+
format: "yaml",
|
|
37198
|
+
fileContent,
|
|
37199
|
+
filePath,
|
|
37200
|
+
invalidRootPolicy: "error"
|
|
37201
|
+
});
|
|
37202
|
+
}
|
|
37203
|
+
/**
|
|
37204
|
+
* The remote transports Pool's `transport.type` accepts. `streamable-http` is
|
|
37205
|
+
* the canonical rulesync name for what Pool calls `http`; everything else is
|
|
37206
|
+
* left to the caller to reject.
|
|
37207
|
+
*
|
|
37208
|
+
* @see https://docs.poolside.ai/mcp-servers
|
|
37209
|
+
*/
|
|
37210
|
+
function asPoolRemoteType(stated, url) {
|
|
37211
|
+
if (stated === "sse") return "sse";
|
|
37212
|
+
if (stated === "http" || stated === "streamable-http") return "http";
|
|
37213
|
+
if (stated === void 0) return /^wss?:\/\//i.test(url) ? void 0 : "http";
|
|
37214
|
+
}
|
|
37215
|
+
/**
|
|
37216
|
+
* Pool spells remote headers as a list of `"Name: value"` strings rather than
|
|
37217
|
+
* a map, so the canonical record is flattened at generate time and split back
|
|
37218
|
+
* at the first `:` on import. A list entry without a `:` names no header and
|
|
37219
|
+
* is dropped with a warning — silently losing what may be a mistyped auth
|
|
37220
|
+
* header would leave the canonical file short of one without a trace.
|
|
37221
|
+
*/
|
|
37222
|
+
function headersRecordToPoolList(headers) {
|
|
37223
|
+
return Object.entries(headers).map(([name, value]) => `${name}: ${value}`);
|
|
37224
|
+
}
|
|
37225
|
+
function poolHeadersListToRecord(serverName, headers) {
|
|
37226
|
+
if (!isStringArray$2(headers)) return void 0;
|
|
37227
|
+
const result = {};
|
|
37228
|
+
for (const entry of headers) {
|
|
37229
|
+
const separator = entry.indexOf(":");
|
|
37230
|
+
const name = separator > 0 ? entry.slice(0, separator).trim() : "";
|
|
37231
|
+
if (name === "" || PROTOTYPE_POLLUTION_KEYS.has(name)) {
|
|
37232
|
+
warnWithFallback(void 0, `Ignored malformed header ${quoteValueForWarning(entry)} in Pool MCP server ${quoteValueForWarning(serverName)}: expected "Name: value"`);
|
|
37233
|
+
continue;
|
|
37234
|
+
}
|
|
37235
|
+
result[name] = entry.slice(separator + 1).trim();
|
|
37236
|
+
}
|
|
37237
|
+
return Object.keys(result).length > 0 ? result : void 0;
|
|
37238
|
+
}
|
|
37239
|
+
/**
|
|
37240
|
+
* Pool's `deny`/`enabled_tools` entries are glob patterns, while the canonical
|
|
37241
|
+
* `enabledTools`/`disabledTools` lists are literal tool names that the other
|
|
37242
|
+
* targets match verbatim. A pattern therefore imports as a name that matches
|
|
37243
|
+
* nothing elsewhere — a `deny` that quietly stops denying is the case worth
|
|
37244
|
+
* calling out — so it is copied as-is but warned about. `allow` is exempt: it
|
|
37245
|
+
* imports to the Pool-only `poolAllow`, where globs are exactly what Pool
|
|
37246
|
+
* expects and no other tool ever reads them.
|
|
37247
|
+
*/
|
|
37248
|
+
const GLOB_METACHARACTERS = /[*?[]/;
|
|
37249
|
+
/** Patterns named in the glob warning before the rest collapse to `(+N more)`. */
|
|
37250
|
+
const MAX_LISTED_GLOB_PATTERNS = 10;
|
|
37251
|
+
function importPoolToolList({ serverName, poolKey, value, warnOnGlobs }) {
|
|
37252
|
+
if (!isStringArray$2(value)) {
|
|
37253
|
+
warnWithFallback(void 0, `Ignored malformed value for ${poolKey} in Pool MCP server ${quoteValueForWarning(serverName)}: expected a list of strings`);
|
|
37254
|
+
return;
|
|
37255
|
+
}
|
|
37256
|
+
if (!warnOnGlobs) return value;
|
|
37257
|
+
const patterns = value.filter((entry) => GLOB_METACHARACTERS.test(entry));
|
|
37258
|
+
if (patterns.length > 0) {
|
|
37259
|
+
const listed = patterns.slice(0, MAX_LISTED_GLOB_PATTERNS).map(quoteValueForWarning);
|
|
37260
|
+
const overflow = patterns.length - listed.length;
|
|
37261
|
+
const shown = overflow > 0 ? `${listed.join(", ")} (+${overflow} more)` : listed.join(", ");
|
|
37262
|
+
warnWithFallback(void 0, `${poolKey} in Pool MCP server ${quoteValueForWarning(serverName)} contains glob patterns (${shown}); other tools match the imported tool names literally`);
|
|
37263
|
+
}
|
|
37264
|
+
return value;
|
|
37265
|
+
}
|
|
37266
|
+
/**
|
|
37267
|
+
* Pool's `transport` block for a remote server, or `null` (after a warning)
|
|
37268
|
+
* when the server has no url or names a transport Pool cannot reach.
|
|
37269
|
+
*/
|
|
37270
|
+
function toPoolTransport(name, config, logger) {
|
|
37271
|
+
const url = resolveRemoteMcpUrl(config);
|
|
37272
|
+
if (!url) return warnAndSkipMcpServer({
|
|
37273
|
+
toolName: "Pool",
|
|
37274
|
+
serverName: name,
|
|
37275
|
+
reason: "a remote transport without a url",
|
|
37276
|
+
logger
|
|
37277
|
+
});
|
|
37278
|
+
const stated = config.type ?? config.transport;
|
|
37279
|
+
const type = asPoolRemoteType(stated, url);
|
|
37280
|
+
if (type === void 0) return warnAndSkipMcpServer({
|
|
37281
|
+
toolName: "Pool",
|
|
37282
|
+
serverName: name,
|
|
37283
|
+
reason: stated === void 0 ? "a WebSocket url, which Pool's remote transports (http and sse) cannot reach" : `the "${stated}" transport, which Pool does not offer for remote servers (only http and sse)`,
|
|
37284
|
+
logger
|
|
37285
|
+
});
|
|
37286
|
+
const transport = {
|
|
37287
|
+
type,
|
|
37288
|
+
url
|
|
37289
|
+
};
|
|
37290
|
+
if (config.headers && Object.keys(config.headers).length > 0) transport.headers = headersRecordToPoolList(config.headers);
|
|
37291
|
+
return transport;
|
|
37292
|
+
}
|
|
37293
|
+
/**
|
|
37294
|
+
* The transport-specific part of a Pool server entry: a `transport` block for
|
|
37295
|
+
* remote servers, `command`/`args`/`cwd` for stdio ones. `null` (after a
|
|
37296
|
+
* warning) when the server cannot be written.
|
|
37297
|
+
*/
|
|
37298
|
+
function toPoolTransportFields(name, config, logger) {
|
|
37299
|
+
if (isRemoteMcpServer(config)) {
|
|
37300
|
+
const transport = toPoolTransport(name, config, logger);
|
|
37301
|
+
return transport === null ? null : { transport };
|
|
37302
|
+
}
|
|
37303
|
+
const [command, ...args] = resolveLocalMcpCommand(config);
|
|
37304
|
+
if (!command) return warnAndSkipMcpServer({
|
|
37305
|
+
toolName: "Pool",
|
|
37306
|
+
serverName: name,
|
|
37307
|
+
reason: "a stdio transport without a command",
|
|
37308
|
+
logger
|
|
37309
|
+
});
|
|
37310
|
+
const fields = {
|
|
37311
|
+
command,
|
|
37312
|
+
args
|
|
37313
|
+
};
|
|
37314
|
+
if (config.cwd) fields.cwd = config.cwd;
|
|
37315
|
+
return fields;
|
|
37316
|
+
}
|
|
37317
|
+
/**
|
|
37318
|
+
* The fields shared by stdio and remote entries. Tool filters map onto Pool's
|
|
37319
|
+
* own switches — `enabledTools` becomes `enabled_tools` (the tools Pool
|
|
37320
|
+
* exposes at all) and `disabledTools` becomes `deny` (patterns agents may
|
|
37321
|
+
* never use); the Pool-only approval allowlist is authored as `poolAllow` and
|
|
37322
|
+
* written as `allow`. `disabled: true` is spelled the same on both sides.
|
|
37323
|
+
*/
|
|
37324
|
+
function toPoolCommonFields(config) {
|
|
37325
|
+
const fields = {};
|
|
37326
|
+
if (config.env && Object.keys(config.env).length > 0) fields.env = config.env;
|
|
37327
|
+
if (config.enabledTools && config.enabledTools.length > 0) fields.enabled_tools = config.enabledTools;
|
|
37328
|
+
if (config.poolAllow && config.poolAllow.length > 0) fields.allow = config.poolAllow;
|
|
37329
|
+
if (config.disabledTools && config.disabledTools.length > 0) fields.deny = config.disabledTools;
|
|
37330
|
+
if (config.disabled === true) fields.disabled = true;
|
|
37331
|
+
return fields;
|
|
37332
|
+
}
|
|
37333
|
+
/**
|
|
37334
|
+
* Convert canonical rulesync servers to Pool's native `mcp_servers` shape:
|
|
37335
|
+
* stdio servers carry `command`/`args`/`cwd`/`env`, remote servers carry a
|
|
37336
|
+
* `transport` block (`type` of `http` or `sse`, `url`, optional `headers`
|
|
37337
|
+
* list) plus optional `env`; see `toPoolCommonFields` for the tool filters.
|
|
37338
|
+
*
|
|
37339
|
+
* @see https://docs.poolside.ai/mcp-servers
|
|
37340
|
+
*/
|
|
37341
|
+
function convertToPoolFormat(mcpServers, logger) {
|
|
37342
|
+
const result = {};
|
|
37343
|
+
for (const [name, config] of Object.entries(mcpServers)) {
|
|
37344
|
+
if (PROTOTYPE_POLLUTION_KEYS.has(name)) continue;
|
|
37345
|
+
if (!isRecord$1(config)) continue;
|
|
37346
|
+
if (declaresNoTransport(config)) {
|
|
37347
|
+
warnAndSkipMcpServer({
|
|
37348
|
+
toolName: "Pool",
|
|
37349
|
+
serverName: name,
|
|
37350
|
+
reason: "no transport",
|
|
37351
|
+
logger
|
|
37352
|
+
});
|
|
37353
|
+
continue;
|
|
37354
|
+
}
|
|
37355
|
+
const transportFields = toPoolTransportFields(name, config, logger);
|
|
37356
|
+
if (transportFields === null) continue;
|
|
37357
|
+
result[name] = {
|
|
37358
|
+
...transportFields,
|
|
37359
|
+
...toPoolCommonFields(config)
|
|
37360
|
+
};
|
|
37361
|
+
}
|
|
37362
|
+
return result;
|
|
37363
|
+
}
|
|
37364
|
+
const POOL_TO_CANONICAL_TOOL_LIST_KEYS = {
|
|
37365
|
+
enabled_tools: "enabledTools",
|
|
37366
|
+
deny: "disabledTools",
|
|
37367
|
+
allow: "poolAllow"
|
|
37368
|
+
};
|
|
37369
|
+
/**
|
|
37370
|
+
* Convert Pool's native `mcp_servers` shape back to canonical rulesync
|
|
37371
|
+
* servers: the `transport` block is flattened to `type`/`url`/`headers`,
|
|
37372
|
+
* `enabled_tools` maps back to `enabledTools`, `deny` to `disabledTools` and
|
|
37373
|
+
* `allow` to the namespaced `poolAllow` (see `McpServerSchema`). Unknown keys
|
|
37374
|
+
* pass through untouched so an import keeps whatever a hand-authored entry
|
|
37375
|
+
* declared; generation back out is a whitelist, so only the keys
|
|
37376
|
+
* `convertToPoolFormat` writes are re-emitted.
|
|
37377
|
+
*/
|
|
37378
|
+
function convertFromPoolFormat(poolServers) {
|
|
37379
|
+
const result = {};
|
|
37380
|
+
for (const [name, config] of Object.entries(poolServers)) {
|
|
37381
|
+
if (PROTOTYPE_POLLUTION_KEYS.has(name) || !isRecord$1(config)) continue;
|
|
37382
|
+
const converted = {};
|
|
37383
|
+
for (const [key, value] of Object.entries(config)) {
|
|
37384
|
+
if (PROTOTYPE_POLLUTION_KEYS.has(key)) continue;
|
|
37385
|
+
if (key === "transport") {
|
|
37386
|
+
if (!isRecord$1(value)) continue;
|
|
37387
|
+
if (typeof value.type === "string") converted.type = value.type;
|
|
37388
|
+
if (typeof value.url === "string") converted.url = value.url;
|
|
37389
|
+
const headers = poolHeadersListToRecord(name, value.headers);
|
|
37390
|
+
if (headers) converted.headers = headers;
|
|
37391
|
+
continue;
|
|
37392
|
+
}
|
|
37393
|
+
const canonicalKey = Object.hasOwn(POOL_TO_CANONICAL_TOOL_LIST_KEYS, key) ? POOL_TO_CANONICAL_TOOL_LIST_KEYS[key] : void 0;
|
|
37394
|
+
if (canonicalKey) {
|
|
37395
|
+
const list = importPoolToolList({
|
|
37396
|
+
serverName: name,
|
|
37397
|
+
poolKey: key,
|
|
37398
|
+
value,
|
|
37399
|
+
warnOnGlobs: canonicalKey !== "poolAllow"
|
|
37400
|
+
});
|
|
37401
|
+
if (list) converted[canonicalKey] = list;
|
|
37402
|
+
continue;
|
|
37403
|
+
}
|
|
37404
|
+
converted[key] = value;
|
|
37405
|
+
}
|
|
37406
|
+
result[name] = converted;
|
|
37407
|
+
}
|
|
37408
|
+
return result;
|
|
37409
|
+
}
|
|
37410
|
+
/**
|
|
37411
|
+
* Pool MCP servers.
|
|
37412
|
+
*
|
|
37413
|
+
* Pool reads MCP servers from the `mcp_servers` block of its settings file:
|
|
37414
|
+
* `<project>/.poolside/settings.yaml` at project scope (shared and committed)
|
|
37415
|
+
* and `~/.config/poolside/settings.yaml` at user scope. The untracked
|
|
37416
|
+
* `.poolside/settings.local.yaml` overlay, which Pool layers on top of both,
|
|
37417
|
+
* is left to the user. Every other top-level settings key is preserved and
|
|
37418
|
+
* the file is never deleted.
|
|
37419
|
+
*
|
|
37420
|
+
* @see https://docs.poolside.ai/mcp-servers
|
|
37421
|
+
* @see https://docs.poolside.ai/settings-file-reference
|
|
37422
|
+
*/
|
|
37423
|
+
var PoolMcp = class PoolMcp extends ToolMcp {
|
|
37424
|
+
settings;
|
|
37425
|
+
constructor(params) {
|
|
37426
|
+
super(params);
|
|
37427
|
+
this.settings = parsePoolSettings(this.fileContent ?? "", (0, node_path.join)(this.relativeDirPath, this.relativeFilePath));
|
|
37428
|
+
}
|
|
37429
|
+
getSettings() {
|
|
37430
|
+
return this.settings;
|
|
37431
|
+
}
|
|
37432
|
+
isDeletable() {
|
|
37433
|
+
return false;
|
|
37434
|
+
}
|
|
37435
|
+
static getSettablePaths({ global = false } = {}) {
|
|
37436
|
+
return {
|
|
37437
|
+
relativeDirPath: global ? POOL_GLOBAL_DIR : POOL_DIR,
|
|
37438
|
+
relativeFilePath: POOL_SETTINGS_FILE_NAME
|
|
37439
|
+
};
|
|
37440
|
+
}
|
|
37441
|
+
static async fromFile({ outputRoot = process.cwd(), validate = true, global = false }) {
|
|
37442
|
+
const paths = this.getSettablePaths({ global });
|
|
37443
|
+
const fileContent = await readFileContentOrNull((0, node_path.join)(outputRoot, paths.relativeDirPath, paths.relativeFilePath)) ?? "";
|
|
37444
|
+
return new PoolMcp({
|
|
37445
|
+
outputRoot,
|
|
37446
|
+
relativeDirPath: paths.relativeDirPath,
|
|
37447
|
+
relativeFilePath: paths.relativeFilePath,
|
|
37448
|
+
fileContent,
|
|
37449
|
+
validate,
|
|
37450
|
+
global
|
|
37451
|
+
});
|
|
37452
|
+
}
|
|
37453
|
+
static async fromRulesyncMcp({ outputRoot = process.cwd(), rulesyncMcp, validate = true, global = false, logger }) {
|
|
37454
|
+
const paths = this.getSettablePaths({ global });
|
|
37455
|
+
const filePath = (0, node_path.join)(outputRoot, paths.relativeDirPath, paths.relativeFilePath);
|
|
37456
|
+
const existingContent = await readFileContentOrNull(filePath) ?? "";
|
|
37457
|
+
const converted = convertToPoolFormat(Object.fromEntries(Object.entries(rulesyncMcp.getMcpServers()).map(([serverName, serverConfig]) => {
|
|
37458
|
+
const rawServer = rulesyncMcp.getRawMcpServer(serverName);
|
|
37459
|
+
const poolAllow = isRecord$1(rawServer) ? rawServer.poolAllow : void 0;
|
|
37460
|
+
return [serverName, {
|
|
37461
|
+
...serverConfig,
|
|
37462
|
+
...isStringArray$2(poolAllow) && { poolAllow }
|
|
37463
|
+
}];
|
|
37464
|
+
})), logger);
|
|
37465
|
+
return new PoolMcp({
|
|
37466
|
+
outputRoot,
|
|
37467
|
+
relativeDirPath: paths.relativeDirPath,
|
|
37468
|
+
relativeFilePath: paths.relativeFilePath,
|
|
37469
|
+
fileContent: applySharedConfigPatch({
|
|
37470
|
+
fileKey: sharedConfigFileKey(paths),
|
|
37471
|
+
feature: "mcp",
|
|
37472
|
+
existingContent,
|
|
37473
|
+
patch: { [POOL_MCP_SERVERS_KEY]: converted },
|
|
37474
|
+
filePath
|
|
37475
|
+
}),
|
|
37476
|
+
validate,
|
|
37477
|
+
global
|
|
37478
|
+
});
|
|
37479
|
+
}
|
|
37480
|
+
toRulesyncMcp() {
|
|
37481
|
+
const converted = convertFromPoolFormat(isRecord$1(this.settings["mcp_servers"]) ? this.settings[POOL_MCP_SERVERS_KEY] : {});
|
|
37482
|
+
return this.toRulesyncMcpDefault({ fileContent: JSON.stringify({ mcpServers: converted }, null, 2) });
|
|
37483
|
+
}
|
|
37484
|
+
validate() {
|
|
37485
|
+
return {
|
|
37486
|
+
success: true,
|
|
37487
|
+
error: null
|
|
37488
|
+
};
|
|
37489
|
+
}
|
|
37490
|
+
static forDeletion({ outputRoot = process.cwd(), relativeDirPath, relativeFilePath, global = false }) {
|
|
37491
|
+
return new PoolMcp({
|
|
37492
|
+
outputRoot,
|
|
37493
|
+
relativeDirPath,
|
|
37494
|
+
relativeFilePath,
|
|
37495
|
+
fileContent: "",
|
|
37496
|
+
validate: false,
|
|
37497
|
+
global
|
|
37498
|
+
});
|
|
37499
|
+
}
|
|
37500
|
+
};
|
|
37501
|
+
//#endregion
|
|
37056
37502
|
//#region src/features/mcp/qwencode-mcp.ts
|
|
37057
37503
|
/**
|
|
37058
37504
|
* Map rulesync's canonical per-server tool filters to Qwen Code's field names.
|
|
@@ -39500,6 +39946,15 @@ const toolMcpFactories = /* @__PURE__ */ new Map([
|
|
|
39500
39946
|
supportsDisabledTools: true
|
|
39501
39947
|
}
|
|
39502
39948
|
}],
|
|
39949
|
+
["pool", {
|
|
39950
|
+
class: PoolMcp,
|
|
39951
|
+
meta: {
|
|
39952
|
+
supportsProject: true,
|
|
39953
|
+
supportsGlobal: true,
|
|
39954
|
+
supportsEnabledTools: true,
|
|
39955
|
+
supportsDisabledTools: true
|
|
39956
|
+
}
|
|
39957
|
+
}],
|
|
39503
39958
|
["qwencode", {
|
|
39504
39959
|
class: QwencodeMcp,
|
|
39505
39960
|
meta: {
|
|
@@ -56721,7 +57176,7 @@ const AmpSkillFrontmatterSchema = zod_mini.z.looseObject({
|
|
|
56721
57176
|
* Amp natively reads Agent Skills following the open standard: a directory with a
|
|
56722
57177
|
* `SKILL.md` carrying `name`/`description` frontmatter, scoped to the project
|
|
56723
57178
|
* (`.agents/skills/`) and user-wide (`~/.config/agents/skills/`).
|
|
56724
|
-
* @see https://ampcode.com/
|
|
57179
|
+
* @see https://ampcode.com/docs/customize/skills
|
|
56725
57180
|
*/
|
|
56726
57181
|
var AmpSkill = class AmpSkill extends ToolSkill {
|
|
56727
57182
|
constructor({ outputRoot = process.cwd(), relativeDirPath = AMP_SKILLS_PROJECT_DIR, dirName, frontmatter, body, otherFiles = [], validate = true, global = false }) {
|
|
@@ -59200,8 +59655,11 @@ const CursorSkillFrontmatterSchema = zod_mini.z.looseObject({
|
|
|
59200
59655
|
name: zod_mini.z.string(),
|
|
59201
59656
|
description: zod_mini.z.string(),
|
|
59202
59657
|
paths: zod_mini.z.optional(zod_mini.z.union([zod_mini.z.string(), zod_mini.z.array(zod_mini.z.string())])),
|
|
59658
|
+
globs: zod_mini.z.optional(zod_mini.z.union([zod_mini.z.string(), zod_mini.z.array(zod_mini.z.string())])),
|
|
59203
59659
|
"disable-model-invocation": zod_mini.z.optional(zod_mini.z.boolean()),
|
|
59204
59660
|
"user-invocable": zod_mini.z.optional(zod_mini.z.boolean()),
|
|
59661
|
+
icon: zod_mini.z.optional(zod_mini.z.string()),
|
|
59662
|
+
color: zod_mini.z.optional(zod_mini.z.string()),
|
|
59205
59663
|
metadata: zod_mini.z.optional(zod_mini.z.looseObject({}))
|
|
59206
59664
|
});
|
|
59207
59665
|
/**
|
|
@@ -59253,10 +59711,13 @@ var CursorSkill = class CursorSkill extends ToolSkill {
|
|
|
59253
59711
|
}
|
|
59254
59712
|
toRulesyncSkill() {
|
|
59255
59713
|
const frontmatter = this.getFrontmatter();
|
|
59714
|
+
const paths = frontmatter.paths ?? frontmatter.globs;
|
|
59256
59715
|
const cursorSection = {
|
|
59257
|
-
...
|
|
59716
|
+
...paths !== void 0 && { paths },
|
|
59258
59717
|
...frontmatter["disable-model-invocation"] !== void 0 && { "disable-model-invocation": frontmatter["disable-model-invocation"] },
|
|
59259
59718
|
...frontmatter["user-invocable"] !== void 0 && { "user-invocable": frontmatter["user-invocable"] },
|
|
59719
|
+
...frontmatter.icon !== void 0 && { icon: frontmatter.icon },
|
|
59720
|
+
...frontmatter.color !== void 0 && { color: frontmatter.color },
|
|
59260
59721
|
...frontmatter.metadata !== void 0 && { metadata: frontmatter.metadata }
|
|
59261
59722
|
};
|
|
59262
59723
|
const rulesyncFrontmatter = {
|
|
@@ -59298,6 +59759,8 @@ var CursorSkill = class CursorSkill extends ToolSkill {
|
|
|
59298
59759
|
...cursorSection?.paths !== void 0 && { paths: cursorSection.paths },
|
|
59299
59760
|
...resolvedDisableModelInvocation !== void 0 && { "disable-model-invocation": resolvedDisableModelInvocation },
|
|
59300
59761
|
...resolvedUserInvocable !== void 0 && { "user-invocable": resolvedUserInvocable },
|
|
59762
|
+
...cursorSection?.icon !== void 0 && { icon: cursorSection.icon },
|
|
59763
|
+
...cursorSection?.color !== void 0 && { color: cursorSection.color },
|
|
59301
59764
|
...metadata !== void 0 && { metadata }
|
|
59302
59765
|
};
|
|
59303
59766
|
return new CursorSkill({
|
|
@@ -61682,37 +62145,6 @@ var PiSkill = class PiSkill extends ToolSkill {
|
|
|
61682
62145
|
}
|
|
61683
62146
|
};
|
|
61684
62147
|
//#endregion
|
|
61685
|
-
//#region src/constants/pool-paths.ts
|
|
61686
|
-
/**
|
|
61687
|
-
* Pool (Poolside's coding agent CLI) configuration-layout conventions.
|
|
61688
|
-
*
|
|
61689
|
-
* Pool reads `AGENTS.md` instruction files the same way the AGENTS.md standard
|
|
61690
|
-
* describes them: the personal `~/.config/poolside/AGENTS.md` (Pool itself
|
|
61691
|
-
* honours `XDG_CONFIG_HOME` upstream; rulesync writes only the XDG-default
|
|
61692
|
-
* path), the project-root `AGENTS.md`, and nested per-directory
|
|
61693
|
-
* `AGENTS.md` files from the repository root down through the working
|
|
61694
|
-
* directory, deeper files taking precedence. It skips ignored directories
|
|
61695
|
-
* (`.git/`, `node_modules/`, cache directories, repository ignore rules).
|
|
61696
|
-
*
|
|
61697
|
-
* Skills follow the Agent Skills format (`<name>/SKILL.md` bundles). Pool
|
|
61698
|
-
* scans `.poolside/skills/` (project) and `~/.config/poolside/skills/`
|
|
61699
|
-
* (global) plus the shared `.agents/skills/` / `~/.agents/skills/` roots and
|
|
61700
|
-
* the skill directories of other Agent Skills tools; rulesync writes only the
|
|
61701
|
-
* Pool-specific roots and leaves the shared ones to their own targets.
|
|
61702
|
-
*
|
|
61703
|
-
* @see https://docs.poolside.ai/agent-instructions
|
|
61704
|
-
* @see https://docs.poolside.ai/skills
|
|
61705
|
-
* @see https://github.com/poolsideai/pool
|
|
61706
|
-
*/
|
|
61707
|
-
/** Project-scoped `.poolside/` directory at the project root. */
|
|
61708
|
-
const POOL_DIR = ".poolside";
|
|
61709
|
-
/** Global config directory for Pool, relative to the home directory. */
|
|
61710
|
-
const POOL_GLOBAL_DIR = (0, node_path.join)(".config", "poolside");
|
|
61711
|
-
/** Project skills root, relative to the project root. */
|
|
61712
|
-
const POOL_SKILLS_DIR_PATH = (0, node_path.join)(POOL_DIR, "skills");
|
|
61713
|
-
/** Global skills root, relative to the home directory. */
|
|
61714
|
-
const POOL_GLOBAL_SKILLS_DIR_PATH = (0, node_path.join)(POOL_GLOBAL_DIR, "skills");
|
|
61715
|
-
//#endregion
|
|
61716
62148
|
//#region src/features/skills/pool-skill.ts
|
|
61717
62149
|
const PoolSkillFrontmatterSchema = zod_mini.z.looseObject({
|
|
61718
62150
|
name: zod_mini.z.string(),
|
|
@@ -66057,11 +66489,17 @@ var CopilotSubagent = class CopilotSubagent extends ToolSubagent {
|
|
|
66057
66489
|
*
|
|
66058
66490
|
* Reference: https://docs.github.com/en/copilot/how-tos/copilot-cli/customize-copilot/create-custom-agents-for-cli
|
|
66059
66491
|
* https://docs.github.com/en/copilot/reference/custom-agents-configuration
|
|
66492
|
+
* https://docs.github.com/en/copilot/reference/copilot-cli-reference/cli-command-reference
|
|
66060
66493
|
*
|
|
66061
66494
|
* Per the configuration docs:
|
|
66062
66495
|
* - `description` is required.
|
|
66063
66496
|
* - `name`, `target`, `tools`, `model`, `disable-model-invocation`,
|
|
66064
66497
|
* `user-invocable`, `mcp-servers`, and `metadata` are optional.
|
|
66498
|
+
* - The CLI command reference adds `models` (authored models in priority
|
|
66499
|
+
* order, overriding `model` when both are set) and `modelPolicy`
|
|
66500
|
+
* (`preferred` lets a `subagents` settings override or the `/subagents`
|
|
66501
|
+
* picker replace the model; `required` locks dispatch to the authored
|
|
66502
|
+
* list). CLI v1.0.81+; the shared reference page still lists `model` only.
|
|
66065
66503
|
*
|
|
66066
66504
|
* `looseObject` is used so that future additional fields are passed through
|
|
66067
66505
|
* unchanged (consistent with the rest of rulesync's frontmatter schemas).
|
|
@@ -66072,6 +66510,8 @@ const CopilotCliSubagentFrontmatterSchema = zod_mini.z.looseObject({
|
|
|
66072
66510
|
target: zod_mini.z.optional(zod_mini.z.string()),
|
|
66073
66511
|
tools: zod_mini.z.optional(zod_mini.z.union([zod_mini.z.string(), zod_mini.z.array(zod_mini.z.string())])),
|
|
66074
66512
|
model: zod_mini.z.optional(zod_mini.z.string()),
|
|
66513
|
+
models: zod_mini.z.optional(zod_mini.z.array(zod_mini.z.string())),
|
|
66514
|
+
modelPolicy: zod_mini.z.optional(zod_mini.z.enum(["preferred", "required"])),
|
|
66075
66515
|
"disable-model-invocation": zod_mini.z.optional(zod_mini.z.boolean()),
|
|
66076
66516
|
"user-invocable": zod_mini.z.optional(zod_mini.z.boolean()),
|
|
66077
66517
|
"mcp-servers": zod_mini.z.optional(zod_mini.z.record(zod_mini.z.string(), zod_mini.z.unknown())),
|
|
@@ -70448,16 +70888,131 @@ var AgentsMdRule = class AgentsMdRule extends ToolRule {
|
|
|
70448
70888
|
}
|
|
70449
70889
|
};
|
|
70450
70890
|
//#endregion
|
|
70891
|
+
//#region src/utils/brace-aware-list.ts
|
|
70892
|
+
/**
|
|
70893
|
+
* Splits a comma-separated pattern list on the commas that separate its
|
|
70894
|
+
* entries, leaving the commas inside a brace group alone: in
|
|
70895
|
+
* `{src,lib}/**\/*.ts, tests/**\/*.test.ts` the first comma belongs to the
|
|
70896
|
+
* brace expansion and the second separates the two globs. Entries are trimmed;
|
|
70897
|
+
* empty entries are dropped.
|
|
70898
|
+
*
|
|
70899
|
+
* @example
|
|
70900
|
+
* splitBraceAwareList("src/**\/*.{ts,tsx}, docs/**")
|
|
70901
|
+
* // => ["src/**\/*.{ts,tsx}", "docs/**"]
|
|
70902
|
+
*/
|
|
70903
|
+
const splitBraceAwareList = (value) => {
|
|
70904
|
+
const entries = [];
|
|
70905
|
+
let current = "";
|
|
70906
|
+
let braceDepth = 0;
|
|
70907
|
+
for (const char of value) {
|
|
70908
|
+
if (char === "{") braceDepth += 1;
|
|
70909
|
+
else if (char === "}" && braceDepth > 0) braceDepth -= 1;
|
|
70910
|
+
else if (char === "," && braceDepth === 0) {
|
|
70911
|
+
entries.push(current);
|
|
70912
|
+
current = "";
|
|
70913
|
+
continue;
|
|
70914
|
+
}
|
|
70915
|
+
current += char;
|
|
70916
|
+
}
|
|
70917
|
+
entries.push(current);
|
|
70918
|
+
return entries.map((entry) => entry.trim()).filter((entry) => entry.length > 0);
|
|
70919
|
+
};
|
|
70920
|
+
/**
|
|
70921
|
+
* Upper bound on the patterns one glob may expand to; past it the glob is
|
|
70922
|
+
* returned verbatim, since `{a,b}` repeated k times yields 2^k branches.
|
|
70923
|
+
*/
|
|
70924
|
+
const MAX_BRACE_EXPANSIONS = 256;
|
|
70925
|
+
/** Matches an innermost brace group with at least one comma: `{a,b}`, not `{a}` or `{a,{b,c}}`. */
|
|
70926
|
+
const INNERMOST_BRACE_GROUP_REGEX = /\{([^{},]*(?:,[^{},]*)+)\}/;
|
|
70927
|
+
/**
|
|
70928
|
+
* Expands the comma alternations of a glob (`a.{ts,tsx}` → `a.ts`, `a.tsx`)
|
|
70929
|
+
* into one pattern per combination, innermost group first, so the result can
|
|
70930
|
+
* be handed to a consumer that splits its pattern list on bare commas. A glob
|
|
70931
|
+
* without a brace alternation is returned as is; a brace group without a
|
|
70932
|
+
* comma (e.g. `{a}`) is left untouched. A glob whose expansion would exceed
|
|
70933
|
+
* `MAX_BRACE_EXPANSIONS` patterns is returned as is as well.
|
|
70934
|
+
*
|
|
70935
|
+
* The cap is enforced on the work list itself rather than by counting the
|
|
70936
|
+
* groups up front: a nested group such as `{{a,b},c}` is rewritten into one
|
|
70937
|
+
* new group per inner alternative, so the number of patterns in flight is the
|
|
70938
|
+
* only bound that holds for arbitrary nesting.
|
|
70939
|
+
*
|
|
70940
|
+
* @example
|
|
70941
|
+
* expandBraceAlternations("src/**\/*.{ts,tsx}")
|
|
70942
|
+
* // => ["src/**\/*.ts", "src/**\/*.tsx"]
|
|
70943
|
+
*/
|
|
70944
|
+
const expandBraceAlternations = (glob) => {
|
|
70945
|
+
let pending = [glob];
|
|
70946
|
+
for (;;) {
|
|
70947
|
+
const next = [];
|
|
70948
|
+
let expanded = false;
|
|
70949
|
+
for (const pattern of pending) {
|
|
70950
|
+
const match = INNERMOST_BRACE_GROUP_REGEX.exec(pattern);
|
|
70951
|
+
if (match === null) {
|
|
70952
|
+
next.push(pattern);
|
|
70953
|
+
continue;
|
|
70954
|
+
}
|
|
70955
|
+
expanded = true;
|
|
70956
|
+
const prefix = pattern.slice(0, match.index);
|
|
70957
|
+
const suffix = pattern.slice(match.index + match[0].length);
|
|
70958
|
+
for (const alternative of (match[1] ?? "").split(",")) next.push(`${prefix}${alternative}${suffix}`);
|
|
70959
|
+
if (next.length > MAX_BRACE_EXPANSIONS) return [glob];
|
|
70960
|
+
}
|
|
70961
|
+
if (!expanded) return [...new Set(next)];
|
|
70962
|
+
pending = next;
|
|
70963
|
+
}
|
|
70964
|
+
};
|
|
70965
|
+
//#endregion
|
|
70451
70966
|
//#region src/features/rules/aiassistant-rule.ts
|
|
70452
70967
|
/**
|
|
70968
|
+
* Rule types AI Assistant recognizes on the `apply:` metadata line. The values
|
|
70969
|
+
* are matched case-sensitively by the plugin, so they are emitted verbatim.
|
|
70970
|
+
* Any other value is carried through untouched (the plugin treats it as Off
|
|
70971
|
+
* today; a future build may recognize it).
|
|
70972
|
+
*/
|
|
70973
|
+
const AIASSISTANT_APPLY_VALUES = [
|
|
70974
|
+
"always",
|
|
70975
|
+
"manually",
|
|
70976
|
+
"by model decision",
|
|
70977
|
+
"by file patterns",
|
|
70978
|
+
"off"
|
|
70979
|
+
];
|
|
70980
|
+
const isAiassistantApply = (value) => AIASSISTANT_APPLY_VALUES.includes(value);
|
|
70981
|
+
/**
|
|
70982
|
+
* Globs that match the whole project and so say nothing beyond "always".
|
|
70983
|
+
*/
|
|
70984
|
+
const UNIVERSAL_GLOBS$2 = /* @__PURE__ */ new Set(["**/*", "*"]);
|
|
70985
|
+
const METADATA_DELIMITER = "---";
|
|
70986
|
+
/**
|
|
70987
|
+
* Splits text into lines that keep their own line terminator, so the body can
|
|
70988
|
+
* be sliced back out of the file byte-for-byte. Lone `\r` counts as a break,
|
|
70989
|
+
* matching Kotlin's `lines()`, which the plugin uses on the block.
|
|
70990
|
+
*/
|
|
70991
|
+
const LINE_SPLIT_REGEX = /(?<=\r\n|\r(?!\n)|(?<!\r)\n)/;
|
|
70992
|
+
/**
|
|
70993
|
+
* The block is line-based, so a value must not carry a line break: one would
|
|
70994
|
+
* start a new `<field>: <value>` line and could override `apply:`.
|
|
70995
|
+
*/
|
|
70996
|
+
const flattenValue = (value) => value.split(/\r\n|\r|\n/).map((part) => part.trim()).filter((part) => part.length > 0).join(" ");
|
|
70997
|
+
/**
|
|
70453
70998
|
* Rule generator for JetBrains AI Assistant.
|
|
70454
70999
|
*
|
|
70455
71000
|
* AI Assistant reads project rules as flat Markdown files in
|
|
70456
|
-
* `.aiassistant/rules/*.md` (one file per rule, identified by filename).
|
|
70457
|
-
*
|
|
70458
|
-
*
|
|
70459
|
-
*
|
|
70460
|
-
*
|
|
71001
|
+
* `.aiassistant/rules/*.md` (one file per rule, identified by filename). There
|
|
71002
|
+
* is no special root file (unlike Junie's `guidelines.md`), so every rule —
|
|
71003
|
+
* root or non-root — is written as its own file under `rules/`.
|
|
71004
|
+
*
|
|
71005
|
+
* The rule type (Always / Manually / By model decision / By file patterns /
|
|
71006
|
+
* Off) is stored in the file itself as a leading `---` metadata block that the
|
|
71007
|
+
* IDE's "Rule type" selector edits in place. The block is not YAML: each line
|
|
71008
|
+
* is `<field>: <value>` split on the first colon, and `patterns` is a
|
|
71009
|
+
* comma-separated glob list, so it is written and parsed by hand here. A file
|
|
71010
|
+
* without an `apply:` line is treated as Off at retrieval time (it is never
|
|
71011
|
+
* attached automatically), which is why every generated file carries one.
|
|
71012
|
+
*
|
|
71013
|
+
* Field names and values come from the plugin (`AiRulesService`,
|
|
71014
|
+
* `AiRulesLLMBundle.properties`; verified on build 262.10968.75) — the docs
|
|
71015
|
+
* page describes the rule type only as an IDE setting.
|
|
70461
71016
|
*
|
|
70462
71017
|
* AI Assistant and Junie are different JetBrains products (real-time assistance
|
|
70463
71018
|
* vs. autonomous agent) with different layouts, so this is a separate target.
|
|
@@ -70465,18 +71020,154 @@ var AgentsMdRule = class AgentsMdRule extends ToolRule {
|
|
|
70465
71020
|
* @see https://www.jetbrains.com/help/ai-assistant/configure-project-rules.html
|
|
70466
71021
|
*/
|
|
70467
71022
|
var AiassistantRule = class AiassistantRule extends ToolRule {
|
|
71023
|
+
body;
|
|
71024
|
+
metadata;
|
|
71025
|
+
constructor({ body, metadata, ...rest }) {
|
|
71026
|
+
super({
|
|
71027
|
+
...rest,
|
|
71028
|
+
fileContent: AiassistantRule.buildFileContent({
|
|
71029
|
+
body,
|
|
71030
|
+
metadata
|
|
71031
|
+
})
|
|
71032
|
+
});
|
|
71033
|
+
this.body = body;
|
|
71034
|
+
this.metadata = metadata;
|
|
71035
|
+
}
|
|
70468
71036
|
static getSettablePaths(_options = {}) {
|
|
70469
71037
|
return { nonRoot: { relativeDirPath: AIASSISTANT_RULES_DIR_PATH } };
|
|
70470
71038
|
}
|
|
71039
|
+
getBody() {
|
|
71040
|
+
return this.body;
|
|
71041
|
+
}
|
|
71042
|
+
getMetadata() {
|
|
71043
|
+
return this.metadata;
|
|
71044
|
+
}
|
|
71045
|
+
/**
|
|
71046
|
+
* Serialize the metadata block the way the IDE writes it
|
|
71047
|
+
* (`---\n<field>: <value>\n---`), followed by the body.
|
|
71048
|
+
*/
|
|
71049
|
+
static buildFileContent({ body, metadata }) {
|
|
71050
|
+
if (metadata === void 0) return body;
|
|
71051
|
+
const lines = [`apply: ${metadata.apply}`];
|
|
71052
|
+
if (metadata.apply === "by model decision" && metadata.instructions) lines.push(`instructions: ${flattenValue(metadata.instructions)}`);
|
|
71053
|
+
if (metadata.apply === "by file patterns" && metadata.patterns && metadata.patterns.length > 0) lines.push(`patterns: ${metadata.patterns.map(flattenValue).join(", ")}`);
|
|
71054
|
+
return `${METADATA_DELIMITER}\n${lines.join("\n")}\n${METADATA_DELIMITER}\n\n${body}`;
|
|
71055
|
+
}
|
|
71056
|
+
/**
|
|
71057
|
+
* Split a rule file into its metadata block and body. The plugin locates the
|
|
71058
|
+
* block with `^---\s*([\s\S]*?)---\s*` (anchored at the very start of the
|
|
71059
|
+
* file); this reads it line by line instead — a delimiter is a line that is
|
|
71060
|
+
* exactly `---` — which is linear on any input and tolerates a BOM or blank
|
|
71061
|
+
* lines before the block. Each line is split on its first colon and trimmed;
|
|
71062
|
+
* unknown fields are ignored; a missing block or `apply:` line yields no
|
|
71063
|
+
* metadata, while an unrecognized `apply:` value is kept verbatim.
|
|
71064
|
+
*/
|
|
71065
|
+
static parseFileContent(fileContent) {
|
|
71066
|
+
const content = fileContent.replace(/^\uFEFF/, "").trimStart();
|
|
71067
|
+
const lines = content.split(LINE_SPLIT_REGEX);
|
|
71068
|
+
const closingIndex = lines[0]?.trim() === METADATA_DELIMITER ? lines.findIndex((line, index) => index > 0 && line.trim() === METADATA_DELIMITER) : -1;
|
|
71069
|
+
if (closingIndex === -1) return {
|
|
71070
|
+
metadata: void 0,
|
|
71071
|
+
body: content.trim()
|
|
71072
|
+
};
|
|
71073
|
+
const body = lines.slice(closingIndex + 1).join("").trim();
|
|
71074
|
+
const fields = /* @__PURE__ */ new Map();
|
|
71075
|
+
for (const line of lines.slice(1, closingIndex)) {
|
|
71076
|
+
const separatorIndex = line.indexOf(":");
|
|
71077
|
+
if (separatorIndex === -1) continue;
|
|
71078
|
+
fields.set(line.slice(0, separatorIndex).trim(), line.slice(separatorIndex + 1).trim());
|
|
71079
|
+
}
|
|
71080
|
+
const apply = fields.get("apply");
|
|
71081
|
+
if (apply === void 0 || apply.length === 0) return {
|
|
71082
|
+
metadata: void 0,
|
|
71083
|
+
body
|
|
71084
|
+
};
|
|
71085
|
+
const instructions = fields.get("instructions");
|
|
71086
|
+
const patterns = fields.get("patterns");
|
|
71087
|
+
return {
|
|
71088
|
+
metadata: {
|
|
71089
|
+
apply,
|
|
71090
|
+
...apply === "by model decision" && instructions && { instructions },
|
|
71091
|
+
...apply === "by file patterns" && patterns && { patterns: splitBraceAwareList(patterns) }
|
|
71092
|
+
},
|
|
71093
|
+
body
|
|
71094
|
+
};
|
|
71095
|
+
}
|
|
71096
|
+
/**
|
|
71097
|
+
* Derive the metadata block from the canonical frontmatter, mirroring the
|
|
71098
|
+
* Cursor mapping: an explicit `aiassistant.apply` wins; otherwise specific
|
|
71099
|
+
* `globs` become `by file patterns`, a `description` on a rule with no
|
|
71100
|
+
* `globs` at all becomes the `by model decision` condition, and everything
|
|
71101
|
+
* else — the root rule, universal globs (which already say "every file",
|
|
71102
|
+
* whatever the description), bare rules — is `always`.
|
|
71103
|
+
*
|
|
71104
|
+
* The plugin splits `patterns` on every comma, so a brace alternation such
|
|
71105
|
+
* as `*.{ts,tsx}` is expanded into one glob per branch before it is written.
|
|
71106
|
+
*/
|
|
71107
|
+
static buildMetadata(frontmatter) {
|
|
71108
|
+
const globs = (frontmatter.globs ?? []).map((glob) => flattenValue(glob)).filter((glob) => glob.length > 0).flatMap((glob) => expandBraceAlternations(glob));
|
|
71109
|
+
const hasUniversalGlob = globs.some((glob) => UNIVERSAL_GLOBS$2.has(glob));
|
|
71110
|
+
const instructions = frontmatter.description && flattenValue(frontmatter.description);
|
|
71111
|
+
const explicitApply = frontmatter.aiassistant?.apply && flattenValue(frontmatter.aiassistant.apply);
|
|
71112
|
+
const apply = explicitApply ? explicitApply : frontmatter.root !== true && globs.length > 0 && !hasUniversalGlob ? "by file patterns" : frontmatter.root !== true && globs.length === 0 && instructions ? "by model decision" : "always";
|
|
71113
|
+
const patterns = globs;
|
|
71114
|
+
return {
|
|
71115
|
+
apply,
|
|
71116
|
+
...apply === "by model decision" && instructions && { instructions },
|
|
71117
|
+
...apply === "by file patterns" && patterns.length > 0 && { patterns }
|
|
71118
|
+
};
|
|
71119
|
+
}
|
|
71120
|
+
/**
|
|
71121
|
+
* Warn, at generate time, about a block the plugin cannot act on as the
|
|
71122
|
+
* author presumably intended.
|
|
71123
|
+
*/
|
|
71124
|
+
static warnAboutMetadata({ metadata, relativeFilePath }) {
|
|
71125
|
+
const { apply, instructions, patterns = [] } = metadata;
|
|
71126
|
+
if (!isAiassistantApply(apply)) warnWithFallback(void 0, `${relativeFilePath}: aiassistant.apply "${apply}" is not one of ${AIASSISTANT_APPLY_VALUES.join(", ")}; it is written as is, and the current AI Assistant treats such a rule as Off.`);
|
|
71127
|
+
if (apply === "by file patterns" && patterns.length === 0) warnWithFallback(void 0, `${relativeFilePath}: aiassistant.apply is "by file patterns" but the rule has no globs, so AI Assistant will never attach it.`);
|
|
71128
|
+
if (apply === "by model decision" && !instructions) warnWithFallback(void 0, `${relativeFilePath}: aiassistant.apply is "by model decision" but the rule has no description, so AI Assistant has no condition to decide on.`);
|
|
71129
|
+
const commaGlob = patterns.find((glob) => glob.includes(","));
|
|
71130
|
+
if (commaGlob !== void 0) warnWithFallback(void 0, `${relativeFilePath}: the glob "${commaGlob}" contains a comma, which AI Assistant reads as a pattern separator.`);
|
|
71131
|
+
if ([
|
|
71132
|
+
apply,
|
|
71133
|
+
instructions ?? "",
|
|
71134
|
+
...patterns
|
|
71135
|
+
].some((value) => value.includes("---"))) warnWithFallback(void 0, `${relativeFilePath}: a metadata value contains "---", which AI Assistant reads as the end of the metadata block.`);
|
|
71136
|
+
}
|
|
70471
71137
|
toRulesyncRule() {
|
|
70472
|
-
|
|
71138
|
+
const metadata = this.metadata;
|
|
71139
|
+
const globs = metadata?.apply === "by file patterns" ? metadata.patterns ?? [] : metadata?.apply === "always" ? ["**/*"] : [];
|
|
71140
|
+
const frontmatter = {
|
|
71141
|
+
root: false,
|
|
71142
|
+
targets: ["*"],
|
|
71143
|
+
description: metadata?.apply === "by model decision" ? metadata.instructions : void 0,
|
|
71144
|
+
globs
|
|
71145
|
+
};
|
|
71146
|
+
const derivedApply = metadata === void 0 ? void 0 : AiassistantRule.buildMetadata(frontmatter).apply;
|
|
71147
|
+
const apply = metadata !== void 0 && derivedApply !== metadata.apply ? metadata.apply : void 0;
|
|
71148
|
+
return new RulesyncRule({
|
|
71149
|
+
outputRoot: process.cwd(),
|
|
71150
|
+
relativeDirPath: RULESYNC_RULES_RELATIVE_DIR_PATH,
|
|
71151
|
+
relativeFilePath: this.getRelativeFilePath(),
|
|
71152
|
+
frontmatter: {
|
|
71153
|
+
...frontmatter,
|
|
71154
|
+
...apply !== void 0 && { aiassistant: { apply } }
|
|
71155
|
+
},
|
|
71156
|
+
body: this.body
|
|
71157
|
+
});
|
|
70473
71158
|
}
|
|
70474
71159
|
static fromRulesyncRule({ outputRoot = process.cwd(), rulesyncRule, validate = true }) {
|
|
71160
|
+
const metadata = this.buildMetadata(rulesyncRule.getFrontmatter());
|
|
71161
|
+
this.warnAboutMetadata({
|
|
71162
|
+
metadata,
|
|
71163
|
+
relativeFilePath: rulesyncRule.getRelativeFilePath()
|
|
71164
|
+
});
|
|
70475
71165
|
return new AiassistantRule({
|
|
70476
71166
|
outputRoot,
|
|
70477
71167
|
relativeDirPath: this.getSettablePaths().nonRoot.relativeDirPath,
|
|
70478
71168
|
relativeFilePath: rulesyncRule.getRelativeFilePath(),
|
|
70479
|
-
|
|
71169
|
+
body: rulesyncRule.getBody(),
|
|
71170
|
+
metadata,
|
|
70480
71171
|
validate,
|
|
70481
71172
|
root: false
|
|
70482
71173
|
});
|
|
@@ -70484,11 +71175,13 @@ var AiassistantRule = class AiassistantRule extends ToolRule {
|
|
|
70484
71175
|
static async fromFile({ outputRoot = process.cwd(), relativeFilePath, validate = true }) {
|
|
70485
71176
|
const relativeDirPath = this.getSettablePaths().nonRoot.relativeDirPath;
|
|
70486
71177
|
const fileContent = await readFileContent((0, node_path.join)(outputRoot, relativeDirPath, relativeFilePath));
|
|
71178
|
+
const { metadata, body } = this.parseFileContent(fileContent);
|
|
70487
71179
|
return new AiassistantRule({
|
|
70488
71180
|
outputRoot,
|
|
70489
71181
|
relativeDirPath,
|
|
70490
71182
|
relativeFilePath,
|
|
70491
|
-
|
|
71183
|
+
body,
|
|
71184
|
+
metadata,
|
|
70492
71185
|
validate,
|
|
70493
71186
|
root: false
|
|
70494
71187
|
});
|
|
@@ -70498,7 +71191,7 @@ var AiassistantRule = class AiassistantRule extends ToolRule {
|
|
|
70498
71191
|
outputRoot,
|
|
70499
71192
|
relativeDirPath,
|
|
70500
71193
|
relativeFilePath,
|
|
70501
|
-
|
|
71194
|
+
body: "",
|
|
70502
71195
|
validate: false,
|
|
70503
71196
|
root: false
|
|
70504
71197
|
});
|
|
@@ -72055,40 +72748,14 @@ const CodebuddyRuleFrontmatterSchema = zod_mini.z.object({
|
|
|
72055
72748
|
enabled: zod_mini.z.optional(zod_mini.z.boolean())
|
|
72056
72749
|
});
|
|
72057
72750
|
/**
|
|
72058
|
-
* Splits a scalar `paths` value on the commas that separate patterns, leaving
|
|
72059
|
-
* the commas inside a brace group alone: CodeBuddy's memory docs say "You can
|
|
72060
|
-
* also combine multiple patterns with commas" and give
|
|
72061
|
-
* `paths: {src,lib}/**\/*.ts, tests/**\/*.test.ts` as the example, where the
|
|
72062
|
-
* first comma belongs to the brace expansion and the second separates the two
|
|
72063
|
-
* globs. Only a scalar is split — the docs describe the comma form for the
|
|
72064
|
-
* string shape, and a list already separates its patterns.
|
|
72065
|
-
*/
|
|
72066
|
-
function splitCodebuddyPathsScalar(paths) {
|
|
72067
|
-
const patterns = [];
|
|
72068
|
-
let current = "";
|
|
72069
|
-
let braceDepth = 0;
|
|
72070
|
-
for (const char of paths) {
|
|
72071
|
-
if (char === "{") braceDepth += 1;
|
|
72072
|
-
else if (char === "}" && braceDepth > 0) braceDepth -= 1;
|
|
72073
|
-
else if (char === "," && braceDepth === 0) {
|
|
72074
|
-
patterns.push(current);
|
|
72075
|
-
current = "";
|
|
72076
|
-
continue;
|
|
72077
|
-
}
|
|
72078
|
-
current += char;
|
|
72079
|
-
}
|
|
72080
|
-
patterns.push(current);
|
|
72081
|
-
return patterns.map((pattern) => pattern.trim());
|
|
72082
|
-
}
|
|
72083
|
-
/**
|
|
72084
72751
|
* Normalizes the documented `string` / `string[]` shapes of `paths` to the
|
|
72085
72752
|
* list form the rest of the adapter works with, splitting a comma-separated
|
|
72086
|
-
* scalar into its patterns (see `
|
|
72753
|
+
* scalar into its patterns (see `splitBraceAwareList`). An empty list
|
|
72087
72754
|
* and an empty string both mean "no paths".
|
|
72088
72755
|
*/
|
|
72089
72756
|
function normalizeCodebuddyPaths(paths) {
|
|
72090
72757
|
if (paths === void 0) return;
|
|
72091
|
-
const list = (typeof paths === "string" ?
|
|
72758
|
+
const list = (typeof paths === "string" ? splitBraceAwareList(paths) : paths).filter((path) => path.trim() !== "");
|
|
72092
72759
|
return list.length > 0 ? list : void 0;
|
|
72093
72760
|
}
|
|
72094
72761
|
/**
|