rulesync 26.0.0 → 27.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/README.md +5 -5
- package/dist/cli/index.cjs +10 -5
- package/dist/cli/index.js +10 -5
- package/dist/cli/index.js.map +1 -1
- package/dist/{import-CGGCq_jP.cjs → import-B8FW-x0a.cjs} +1952 -434
- package/dist/{import-CFKjn6d-.js → import-CnhfUGo7.js} +1931 -419
- package/dist/import-CnhfUGo7.js.map +1 -0
- package/dist/index.cjs +1 -1
- package/dist/index.d.cts +1 -1
- package/dist/index.d.cts.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/package.json +1 -1
- package/dist/import-CFKjn6d-.js.map +0 -1
|
@@ -687,6 +687,7 @@ const subagentsProcessorToolTargetTuple = [
|
|
|
687
687
|
"antigravity-plugin",
|
|
688
688
|
"augmentcode",
|
|
689
689
|
"augmentcode-plugin",
|
|
690
|
+
"bob",
|
|
690
691
|
"claudecode",
|
|
691
692
|
"claudecode-plugin",
|
|
692
693
|
"claudecode-legacy",
|
|
@@ -853,6 +854,7 @@ const permissionsProcessorToolTargetTuple = [
|
|
|
853
854
|
"claudecode",
|
|
854
855
|
"cline",
|
|
855
856
|
"codebuddy",
|
|
857
|
+
"codewhale",
|
|
856
858
|
"codexcli",
|
|
857
859
|
"commandcode",
|
|
858
860
|
"continue",
|
|
@@ -874,6 +876,7 @@ const permissionsProcessorToolTargetTuple = [
|
|
|
874
876
|
"kiro-ide",
|
|
875
877
|
"lettacode",
|
|
876
878
|
"mimocode",
|
|
879
|
+
"omp",
|
|
877
880
|
"opencode",
|
|
878
881
|
"pi",
|
|
879
882
|
"pool",
|
|
@@ -897,6 +900,7 @@ const checksProcessorToolTargetTuple = [
|
|
|
897
900
|
"factorydroid",
|
|
898
901
|
"gitlabduo",
|
|
899
902
|
"hermesagent",
|
|
903
|
+
"kilo",
|
|
900
904
|
"qwencode",
|
|
901
905
|
"rovodev",
|
|
902
906
|
"takt"
|
|
@@ -3691,6 +3695,8 @@ const SHARED_USER_MANAGED_CONFIG_PATHS = [
|
|
|
3691
3695
|
".grok/sandbox.toml",
|
|
3692
3696
|
".letta/settings.json",
|
|
3693
3697
|
".qoder/settings.json",
|
|
3698
|
+
".omp/config.yml",
|
|
3699
|
+
".omp/agent/config.yml",
|
|
3694
3700
|
".rovodev/config.yml",
|
|
3695
3701
|
".rovodev/mcp.json",
|
|
3696
3702
|
".poolside/settings.yaml",
|
|
@@ -7664,6 +7670,32 @@ const ToolScopedPermissionSchema = z.record(PermissionCategorySchema, Permission
|
|
|
7664
7670
|
* added without a schema break.
|
|
7665
7671
|
*/
|
|
7666
7672
|
const CanonicalPermissionsOverrideSchema = z.looseObject({ permission: z.optional(ToolScopedPermissionSchema) });
|
|
7673
|
+
/**
|
|
7674
|
+
* One Codewhale `[[rules]]` record of `~/.codewhale/permissions.toml`, written
|
|
7675
|
+
* verbatim. Codewhale parses the file with unknown fields denied, so a single
|
|
7676
|
+
* misspelled key would make it reject every rule in the file; the record is
|
|
7677
|
+
* therefore strict here, surfacing the typo on the rulesync source instead.
|
|
7678
|
+
*
|
|
7679
|
+
* @see https://github.com/Hmbown/Codewhale/blob/main/docs/CONFIGURATION.md
|
|
7680
|
+
*/
|
|
7681
|
+
const CodewhalePermissionRuleSchema = z.strictObject({
|
|
7682
|
+
tool: z.string(),
|
|
7683
|
+
command: z.optional(z.string()),
|
|
7684
|
+
command_exact: z.optional(z.boolean()),
|
|
7685
|
+
path: z.optional(z.string()),
|
|
7686
|
+
workspace: z.optional(z.string()),
|
|
7687
|
+
action: z.optional(PermissionActionSchema)
|
|
7688
|
+
});
|
|
7689
|
+
/**
|
|
7690
|
+
* Tool-scoped override block for Codewhale. `rules` carries native records the
|
|
7691
|
+
* canonical category/pattern shape cannot express — a `workspace`-scoped rule,
|
|
7692
|
+
* a tool rulesync does not map, an exact-command deny — and is emitted ahead of
|
|
7693
|
+
* the rules converted from the canonical block.
|
|
7694
|
+
*/
|
|
7695
|
+
const CodewhalePermissionsOverrideSchema = z.looseObject({
|
|
7696
|
+
permission: z.optional(ToolScopedPermissionSchema),
|
|
7697
|
+
rules: z.optional(z.array(CodewhalePermissionRuleSchema))
|
|
7698
|
+
});
|
|
7667
7699
|
const KimiCodePermissionsOverrideSchema = z.looseObject({
|
|
7668
7700
|
permission: z.optional(ToolScopedPermissionSchema),
|
|
7669
7701
|
defaultPermissionMode: z.optional(z.enum([
|
|
@@ -7884,12 +7916,15 @@ const CursorPermissionsOverrideSchema = z.looseObject({
|
|
|
7884
7916
|
* autonomy/sandbox controls with no canonical permission category — under
|
|
7885
7917
|
* `tools` (`approvalMode` = plan/default/auto-edit/auto/yolo, `autoAccept`,
|
|
7886
7918
|
* `sandbox`, `sandboxImage`, `disabled`, `visible`, `eager`, `listDirectory`,
|
|
7887
|
-
* `todoWrite`, `workflowsEnabled`
|
|
7888
|
-
* `
|
|
7889
|
-
* `tools.workflowsEnabled`,
|
|
7919
|
+
* `todoWrite`, `workflowsEnabled`, `workflowNameOnly`, `executionSandbox`) and
|
|
7920
|
+
* `security` (`folderTrust`, `allowedHttpHookUrls`, `allowPrivateNetworkHooks`,
|
|
7921
|
+
* `allowedInsecureVoiceBaseUrls`). Qwen Code strips `tools.workflowsEnabled`,
|
|
7922
|
+
* `tools.executionSandbox`, `security.allowPrivateNetworkHooks` and
|
|
7890
7923
|
* `security.allowedInsecureVoiceBaseUrls` out of workspace settings, so generate
|
|
7891
|
-
* skips those
|
|
7892
|
-
* scope
|
|
7924
|
+
* skips those four in project scope and announces a granting value in global
|
|
7925
|
+
* scope (any `executionSandbox` change counts). `tools.workflowNameOnly` is
|
|
7926
|
+
* written in both scopes, but a workspace may only turn it on, which the
|
|
7927
|
+
* project-scope note says. `security.allowedHttpHookUrls` (honored in a workspace only while no
|
|
7893
7928
|
* higher scope sets it) and `security.folderTrust` (the initial trust decision
|
|
7894
7929
|
* is made from user/system settings alone, before the workspace merge) are
|
|
7895
7930
|
* written in both scopes, with a note in project scope and an announcement of
|
|
@@ -8777,6 +8812,7 @@ const PermissionsConfigSchema = z.looseObject({
|
|
|
8777
8812
|
augmentcode: z.optional(AugmentcodePermissionsOverrideSchema),
|
|
8778
8813
|
kiro: z.optional(KiroPermissionsOverrideSchema),
|
|
8779
8814
|
codexcli: z.optional(CodexcliPermissionsOverrideSchema),
|
|
8815
|
+
codewhale: z.optional(CodewhalePermissionsOverrideSchema),
|
|
8780
8816
|
zed: z.optional(ZedPermissionsOverrideSchema),
|
|
8781
8817
|
devin: z.optional(DevinPermissionsOverrideSchema),
|
|
8782
8818
|
"antigravity-ide": z.optional(CanonicalPermissionsOverrideSchema),
|
|
@@ -8789,6 +8825,7 @@ const PermissionsConfigSchema = z.looseObject({
|
|
|
8789
8825
|
copilotcli: z.optional(CanonicalPermissionsOverrideSchema),
|
|
8790
8826
|
crush: z.optional(CanonicalPermissionsOverrideSchema),
|
|
8791
8827
|
goose: z.optional(CanonicalPermissionsOverrideSchema),
|
|
8828
|
+
omp: z.optional(CanonicalPermissionsOverrideSchema),
|
|
8792
8829
|
pool: z.optional(CanonicalPermissionsOverrideSchema),
|
|
8793
8830
|
qoder: z.optional(CanonicalPermissionsOverrideSchema),
|
|
8794
8831
|
grokcli: z.optional(CanonicalPermissionsOverrideSchema),
|
|
@@ -10722,6 +10759,13 @@ const RulesyncSubagentFrontmatterSchema = z.looseObject({
|
|
|
10722
10759
|
customInstructions: z.optional(z.string()),
|
|
10723
10760
|
groups: z.optional(z.array(z.unknown()))
|
|
10724
10761
|
})),
|
|
10762
|
+
bob: z.optional(z.looseObject({
|
|
10763
|
+
slug: z.optional(z.string()),
|
|
10764
|
+
whenToUse: z.optional(z.string()),
|
|
10765
|
+
roleDefinition: z.optional(z.string()),
|
|
10766
|
+
customInstructions: z.optional(z.string()),
|
|
10767
|
+
groups: z.optional(z.array(z.unknown()))
|
|
10768
|
+
})),
|
|
10725
10769
|
zoocode: z.optional(z.looseObject({
|
|
10726
10770
|
/** Per-mode MCP server allowlist (Zoo Code v3.60.0+); omitted = all. */
|
|
10727
10771
|
allowedMcpServers: z.optional(z.array(z.string())) })),
|
|
@@ -11062,6 +11106,27 @@ const GITLABDUO_SKILLS_DIR = "skills";
|
|
|
11062
11106
|
/** User skills (requires `--enable-global-skills` / `GITLAB_ENABLE_GLOBAL_SKILLS`). */
|
|
11063
11107
|
const GITLABDUO_GLOBAL_SKILLS_DIR = join(GITLABDUO_DIR, "skills");
|
|
11064
11108
|
//#endregion
|
|
11109
|
+
//#region src/constants/kilo-paths.ts
|
|
11110
|
+
const KILO_DIR = ".kilo";
|
|
11111
|
+
const KILO_GLOBAL_DIR = join(".config", "kilo");
|
|
11112
|
+
const KILO_RULES_DIR_NAME = "rules";
|
|
11113
|
+
const KILO_COMMANDS_DIR_PATH = join(KILO_DIR, "commands");
|
|
11114
|
+
const KILO_GLOBAL_COMMANDS_DIR_PATH = join(KILO_GLOBAL_DIR, "commands");
|
|
11115
|
+
const KILO_SKILLS_DIR_PATH = join(KILO_DIR, "skills");
|
|
11116
|
+
const KILO_AGENTS_DIR_PATH = join(KILO_DIR, "agents");
|
|
11117
|
+
const KILO_GLOBAL_AGENTS_DIR_PATH = join(KILO_GLOBAL_DIR, "agents");
|
|
11118
|
+
const KILO_PLUGINS_DIR_PATH = join(KILO_DIR, "plugins");
|
|
11119
|
+
const KILO_GLOBAL_PLUGINS_DIR_PATH = join(KILO_GLOBAL_DIR, "plugins");
|
|
11120
|
+
const KILO_PLUGIN_DIR_PATH = join(KILO_DIR, "plugin");
|
|
11121
|
+
const KILO_GLOBAL_PLUGIN_DIR_PATH = join(KILO_GLOBAL_DIR, "plugin");
|
|
11122
|
+
const KILO_RULE_FILE_NAME = "AGENTS.md";
|
|
11123
|
+
const KILO_IGNORE_FILE_NAME = ".kilocodeignore";
|
|
11124
|
+
const KILO_LEGACY_GLOBAL_DIR = ".kilocode";
|
|
11125
|
+
const KILO_JSONC_FILE_NAME = "kilo.jsonc";
|
|
11126
|
+
const KILO_JSON_FILE_NAME = "kilo.json";
|
|
11127
|
+
const KILO_HOOKS_FILE_NAME = "rulesync-hooks.js";
|
|
11128
|
+
const KILO_REVIEW_FILE_NAME = "REVIEW.md";
|
|
11129
|
+
//#endregion
|
|
11065
11130
|
//#region src/constants/qwencode-paths.ts
|
|
11066
11131
|
const QWENCODE_DIR = ".qwen";
|
|
11067
11132
|
const QWENCODE_COMMANDS_DIR_PATH = join(QWENCODE_DIR, "commands");
|
|
@@ -11453,8 +11518,14 @@ var FeatureProcessor = class extends RulesyncSourceConsumer {
|
|
|
11453
11518
|
async removeOrphanAiFiles(existingFiles, generatedFiles) {
|
|
11454
11519
|
const generatedPaths = new Set(generatedFiles.map((f) => caseFoldIdentity(f.getFilePath())));
|
|
11455
11520
|
const orphanFiles = existingFiles.filter((f) => !generatedPaths.has(caseFoldIdentity(f.getFilePath())));
|
|
11521
|
+
let removedCount = 0;
|
|
11456
11522
|
for (const aiFile of orphanFiles) {
|
|
11457
11523
|
const filePath = aiFile.getFilePath();
|
|
11524
|
+
if (await refusesDeleteOutsideRoot({
|
|
11525
|
+
logger: this.logger,
|
|
11526
|
+
rootPath: aiFile.getOutputRoot(),
|
|
11527
|
+
filePath
|
|
11528
|
+
})) continue;
|
|
11458
11529
|
const loggedPath = stripControlCharacters(filePath);
|
|
11459
11530
|
if (this.dryRun) this.logger.info(`[DRY RUN] Would delete: ${loggedPath}`);
|
|
11460
11531
|
else {
|
|
@@ -11465,8 +11536,9 @@ var FeatureProcessor = class extends RulesyncSourceConsumer {
|
|
|
11465
11536
|
path: aiFile.getRelativePathFromCwd(),
|
|
11466
11537
|
kind: "file"
|
|
11467
11538
|
});
|
|
11539
|
+
removedCount++;
|
|
11468
11540
|
}
|
|
11469
|
-
return
|
|
11541
|
+
return removedCount;
|
|
11470
11542
|
}
|
|
11471
11543
|
/**
|
|
11472
11544
|
* The paths {@link removeOrphanAiFiles} deleted, or would have deleted under
|
|
@@ -11574,6 +11646,25 @@ async function refusesWriteOutsideRoot({ logger, rootPath, targetPath }) {
|
|
|
11574
11646
|
return true;
|
|
11575
11647
|
}
|
|
11576
11648
|
/**
|
|
11649
|
+
* Whether deleting `filePath` has to be refused because a link on the way to
|
|
11650
|
+
* the directory holding it leads out of `rootPath`, warning about the refusal.
|
|
11651
|
+
*
|
|
11652
|
+
* Deletion candidates are enumerated without following links *inside* the
|
|
11653
|
+
* scanned directory, but the scanned directory itself is reached through
|
|
11654
|
+
* whatever links its path contains: a checked-out repository can carry
|
|
11655
|
+
* `.cursor/commands -> ~/notes`, and every file in `~/notes` would then read as
|
|
11656
|
+
* an orphan. Only the parent is judged, because removing a file that is itself
|
|
11657
|
+
* a link unlinks the link where it is spelled and never touches its target.
|
|
11658
|
+
*/
|
|
11659
|
+
async function refusesDeleteOutsideRoot({ logger, rootPath, filePath }) {
|
|
11660
|
+
if (!await writablePathEscapesRoot({
|
|
11661
|
+
rootPath,
|
|
11662
|
+
targetPath: dirname(filePath)
|
|
11663
|
+
})) return false;
|
|
11664
|
+
logger.warn(`Refusing to delete ${quoteForLog(filePath)}: it resolves outside ${quoteForLog(rootPath)} through a symbolic link`);
|
|
11665
|
+
return true;
|
|
11666
|
+
}
|
|
11667
|
+
/**
|
|
11577
11668
|
* {@link refusesWriteOutsideRoot} over every path a single unit of output
|
|
11578
11669
|
* writes, stopping at the first refusal: a directory written as one unit is
|
|
11579
11670
|
* held back whole when any one file in it would land outside the root.
|
|
@@ -12716,7 +12807,7 @@ function stripSkillFrontmatter(fileContent) {
|
|
|
12716
12807
|
content = rest.slice(closing.index + closing[0].length).replace(LEADING_BLANK_LINE_PATTERN, "");
|
|
12717
12808
|
}
|
|
12718
12809
|
}
|
|
12719
|
-
function handWrittenWarning({ filePath, displayName, toolTarget }) {
|
|
12810
|
+
function handWrittenWarning$1({ filePath, displayName, toolTarget }) {
|
|
12720
12811
|
return `${displayName} checks: ${filePath} holds instructions rulesync did not write, so it is left as it is and no checks were generated for ${displayName}. Run \`rulesync import --targets ${toolTarget} --features checks\` to bring them into \`.rulesync/checks/\` and then delete the file, so the next generate writes it back from there; delete it outright if you no longer want it, or rename the directory if it is an ordinary skill rather than review guidelines. Importing alone leaves this file as it is, so it keeps blocking generation until it is gone. A rulesync skill named \`${FACTORYDROID_REVIEW_GUIDELINES_DIR_NAME}\` is no longer generated here — Factory's reviewer reads this path, so the checks feature owns it — but a directory an older rulesync wrote can still be sitting there.`;
|
|
12721
12812
|
}
|
|
12722
12813
|
/**
|
|
@@ -12773,7 +12864,7 @@ var FactorydroidCheck = class extends AggregatedToolCheck {
|
|
|
12773
12864
|
toolTarget: "factorydroid",
|
|
12774
12865
|
fallbackCheckName: FACTORYDROID_REVIEW_GUIDELINES_DIR_NAME,
|
|
12775
12866
|
handWrittenPreamble: "skip",
|
|
12776
|
-
handWrittenWarning,
|
|
12867
|
+
handWrittenWarning: handWrittenWarning$1,
|
|
12777
12868
|
transformImportedContent: stripSkillFrontmatter
|
|
12778
12869
|
};
|
|
12779
12870
|
}
|
|
@@ -13254,6 +13345,51 @@ var HermesagentCheck = class HermesagentCheck extends ToolCheck {
|
|
|
13254
13345
|
}
|
|
13255
13346
|
};
|
|
13256
13347
|
//#endregion
|
|
13348
|
+
//#region src/features/checks/kilo-check.ts
|
|
13349
|
+
function handWrittenWarning({ filePath, displayName, toolTarget }) {
|
|
13350
|
+
return `${displayName} checks: ${filePath} holds instructions rulesync did not write, so it is left as it is and no checks were generated for ${displayName}. Run \`rulesync import --targets ${toolTarget} --features checks\` to bring them into \`.rulesync/checks/\` and then delete the file, so the next generate writes it back from there. Importing alone leaves this file as it is, so it keeps blocking generation until it is gone.`;
|
|
13351
|
+
}
|
|
13352
|
+
/**
|
|
13353
|
+
* Checks adapter for Kilo Code Reviews' repository guidance (root `REVIEW.md`).
|
|
13354
|
+
*
|
|
13355
|
+
* The Kilo review agent reads one plain-Markdown file at the repository root
|
|
13356
|
+
* (from the PR/MR base branch, once "Use REVIEW.md" is enabled in the Kilo web
|
|
13357
|
+
* app), so every `.rulesync/checks/*.md` targeting Kilo collapses into it via
|
|
13358
|
+
* the `fromRulesyncChecks` on {@link AggregatedToolCheck}, with each check
|
|
13359
|
+
* written as a marked section (see `aggregated-check-file.ts` for the marker
|
|
13360
|
+
* convention).
|
|
13361
|
+
*
|
|
13362
|
+
* The reviewer reads the file as free prose, so a check's `severity` and
|
|
13363
|
+
* `tools` have no equivalent there: they are not written and do not come back
|
|
13364
|
+
* on import. Neither does `description` whenever the check also has a body.
|
|
13365
|
+
*
|
|
13366
|
+
* Project scope only — the reviewer reads the file from the repository and
|
|
13367
|
+
* there is no user-level equivalent.
|
|
13368
|
+
*
|
|
13369
|
+
* A root-level `REVIEW.md` is a generic name other reviewers may read too, so
|
|
13370
|
+
* unlike Cursor Bugbot's tool-specific `.cursor/BUGBOT.md` a hand-written file
|
|
13371
|
+
* here is left untouched (the `skip` policy) rather than replaced.
|
|
13372
|
+
*
|
|
13373
|
+
* @see https://kilo.ai/docs/automate/code-reviews
|
|
13374
|
+
*/
|
|
13375
|
+
var KiloCheck = class extends AggregatedToolCheck {
|
|
13376
|
+
static getSettablePaths(_options = {}) {
|
|
13377
|
+
return {
|
|
13378
|
+
relativeDirPath: ".",
|
|
13379
|
+
relativeFilePath: KILO_REVIEW_FILE_NAME
|
|
13380
|
+
};
|
|
13381
|
+
}
|
|
13382
|
+
static getAggregatedCheckConfig() {
|
|
13383
|
+
return {
|
|
13384
|
+
displayName: "Kilo Code",
|
|
13385
|
+
toolTarget: "kilo",
|
|
13386
|
+
fallbackCheckName: "review",
|
|
13387
|
+
handWrittenPreamble: "skip",
|
|
13388
|
+
handWrittenWarning
|
|
13389
|
+
};
|
|
13390
|
+
}
|
|
13391
|
+
};
|
|
13392
|
+
//#endregion
|
|
13257
13393
|
//#region src/features/checks/qwencode-check.ts
|
|
13258
13394
|
/**
|
|
13259
13395
|
* Checks adapter for Qwen Code's `/review` project rules (`.qwen/review-rules.md`).
|
|
@@ -13350,6 +13486,12 @@ const CLAUDECODE_LOCAL_RULE_FILE_NAME = "CLAUDE.local.md";
|
|
|
13350
13486
|
const CLAUDECODE_RULES_DIR_NAME = "rules";
|
|
13351
13487
|
/** Memories directory name under `.claude/` (legacy format). */
|
|
13352
13488
|
const CLAUDECODE_MEMORIES_DIR_NAME = "memories";
|
|
13489
|
+
/**
|
|
13490
|
+
* Directory under `.claude/` where Claude Code keeps the memory of a subagent
|
|
13491
|
+
* declared with `memory: local`; documented as not checked into version control.
|
|
13492
|
+
* @see https://code.claude.com/docs/en/sub-agents#enable-persistent-memory
|
|
13493
|
+
*/
|
|
13494
|
+
const CLAUDECODE_AGENT_MEMORY_LOCAL_DIR_NAME = "agent-memory-local";
|
|
13353
13495
|
const CLAUDECODE_COMMANDS_DIR_PATH = join(CLAUDECODE_DIR, "commands");
|
|
13354
13496
|
const CLAUDECODE_AGENTS_DIR_PATH = join(CLAUDECODE_DIR, "agents");
|
|
13355
13497
|
const CLAUDECODE_SKILLS_DIR_PATH = join(CLAUDECODE_DIR, "skills");
|
|
@@ -14530,6 +14672,22 @@ const SHARED_CONFIG_OWNERSHIP = {
|
|
|
14530
14672
|
}
|
|
14531
14673
|
}
|
|
14532
14674
|
},
|
|
14675
|
+
".omp/config.yml": {
|
|
14676
|
+
format: "yaml",
|
|
14677
|
+
invalidRootPolicy: "error",
|
|
14678
|
+
features: { permissions: {
|
|
14679
|
+
kind: "replace-owned-keys",
|
|
14680
|
+
ownedKeys: ["tools", "bash"]
|
|
14681
|
+
} }
|
|
14682
|
+
},
|
|
14683
|
+
".omp/agent/config.yml": {
|
|
14684
|
+
format: "yaml",
|
|
14685
|
+
invalidRootPolicy: "error",
|
|
14686
|
+
features: { permissions: {
|
|
14687
|
+
kind: "replace-owned-keys",
|
|
14688
|
+
ownedKeys: ["tools", "bash"]
|
|
14689
|
+
} }
|
|
14690
|
+
},
|
|
14533
14691
|
".pi/settings.json": {
|
|
14534
14692
|
format: "json",
|
|
14535
14693
|
invalidRootPolicy: "error",
|
|
@@ -15576,6 +15734,14 @@ const toolCheckFactories = /* @__PURE__ */ new Map([
|
|
|
15576
15734
|
filePattern: "*.json"
|
|
15577
15735
|
}
|
|
15578
15736
|
}],
|
|
15737
|
+
["kilo", {
|
|
15738
|
+
class: KiloCheck,
|
|
15739
|
+
meta: {
|
|
15740
|
+
supportsGlobal: false,
|
|
15741
|
+
filePattern: KILO_REVIEW_FILE_NAME,
|
|
15742
|
+
committedOutput: true
|
|
15743
|
+
}
|
|
15744
|
+
}],
|
|
15579
15745
|
["qwencode", {
|
|
15580
15746
|
class: QwencodeCheck,
|
|
15581
15747
|
meta: {
|
|
@@ -16357,6 +16523,7 @@ const ANTIGRAVITY_IDE_RULE_FILE_NAME = "AGENTS.md";
|
|
|
16357
16523
|
const ANTIGRAVITY_IDE_GEMINI_DIR = ".gemini";
|
|
16358
16524
|
const ANTIGRAVITY_IDE_GLOBAL_RULE_FILE_NAME = "GEMINI.md";
|
|
16359
16525
|
const ANTIGRAVITY_IDE_GLOBAL_CONFIG_SUBDIR = "config";
|
|
16526
|
+
const ANTIGRAVITY_IDE_GLOBAL_RULES_SUBDIR = join(ANTIGRAVITY_IDE_GLOBAL_CONFIG_SUBDIR, "rules");
|
|
16360
16527
|
const ANTIGRAVITY_IDE_GLOBAL_WORKFLOWS_DIR_PATH = join(ANTIGRAVITY_IDE_GEMINI_DIR, "antigravity", "global_workflows");
|
|
16361
16528
|
const ANTIGRAVITY_IDE_PERMISSIONS_DIR = ".antigravity";
|
|
16362
16529
|
const ANTIGRAVITY_IDE_PERMISSIONS_FILE_NAME = "settings.json";
|
|
@@ -16635,6 +16802,7 @@ const BOB_COMMANDS_DIR_PATH = join(BOB_DIR, "commands");
|
|
|
16635
16802
|
const BOB_SKILLS_DIR_PATH = join(BOB_DIR, "skills");
|
|
16636
16803
|
const BOB_SETTINGS_FILE_NAME = "settings.json";
|
|
16637
16804
|
const BOB_GLOBAL_SETTINGS_DIR_PATH = join(BOB_DIR, "settings");
|
|
16805
|
+
const BOB_CUSTOM_MODES_FILE_NAME = "custom_modes.yaml";
|
|
16638
16806
|
//#endregion
|
|
16639
16807
|
//#region src/features/commands/bob-command.ts
|
|
16640
16808
|
/**
|
|
@@ -17164,6 +17332,7 @@ const CODEWHALE_COMMANDS_DIR_PATH = join(CODEWHALE_DIR, "commands");
|
|
|
17164
17332
|
const CODEWHALE_HOOKS_FILE_NAME = "hooks.toml";
|
|
17165
17333
|
const CODEWHALE_CONFIG_FILE_NAME = "config.toml";
|
|
17166
17334
|
const CODEWHALE_MCP_FILE_NAME = "mcp.json";
|
|
17335
|
+
const CODEWHALE_PERMISSIONS_FILE_NAME = "permissions.toml";
|
|
17167
17336
|
//#endregion
|
|
17168
17337
|
//#region src/features/commands/codewhale-command.ts
|
|
17169
17338
|
/**
|
|
@@ -20096,25 +20265,6 @@ var JunieCommand = class JunieCommand extends ToolCommand {
|
|
|
20096
20265
|
}
|
|
20097
20266
|
};
|
|
20098
20267
|
//#endregion
|
|
20099
|
-
//#region src/constants/kilo-paths.ts
|
|
20100
|
-
const KILO_DIR = ".kilo";
|
|
20101
|
-
const KILO_GLOBAL_DIR = join(".config", "kilo");
|
|
20102
|
-
const KILO_RULES_DIR_NAME = "rules";
|
|
20103
|
-
const KILO_COMMANDS_DIR_PATH = join(KILO_DIR, "commands");
|
|
20104
|
-
const KILO_GLOBAL_COMMANDS_DIR_PATH = join(KILO_GLOBAL_DIR, "commands");
|
|
20105
|
-
const KILO_SKILLS_DIR_PATH = join(KILO_DIR, "skills");
|
|
20106
|
-
const KILO_AGENTS_DIR_PATH = join(KILO_DIR, "agents");
|
|
20107
|
-
const KILO_GLOBAL_AGENTS_DIR_PATH = join(KILO_GLOBAL_DIR, "agents");
|
|
20108
|
-
const KILO_PLUGINS_DIR_PATH = join(KILO_DIR, "plugins");
|
|
20109
|
-
const KILO_GLOBAL_PLUGINS_DIR_PATH = join(KILO_GLOBAL_DIR, "plugins");
|
|
20110
|
-
const KILO_PLUGIN_DIR_PATH = join(KILO_DIR, "plugin");
|
|
20111
|
-
const KILO_GLOBAL_PLUGIN_DIR_PATH = join(KILO_GLOBAL_DIR, "plugin");
|
|
20112
|
-
const KILO_RULE_FILE_NAME = "AGENTS.md";
|
|
20113
|
-
const KILO_IGNORE_FILE_NAME = ".kilocodeignore";
|
|
20114
|
-
const KILO_JSONC_FILE_NAME = "kilo.jsonc";
|
|
20115
|
-
const KILO_JSON_FILE_NAME = "kilo.json";
|
|
20116
|
-
const KILO_HOOKS_FILE_NAME = "rulesync-hooks.js";
|
|
20117
|
-
//#endregion
|
|
20118
20268
|
//#region src/features/commands/kilo-command.ts
|
|
20119
20269
|
const KiloCommandFrontmatterSchema = z.looseObject({
|
|
20120
20270
|
description: z.optional(z.string()),
|
|
@@ -20608,9 +20758,26 @@ const OpenCodeCommandFrontmatterSchema = z.looseObject({
|
|
|
20608
20758
|
description: z.optional(z.string()),
|
|
20609
20759
|
agent: optional(z.string()),
|
|
20610
20760
|
subtask: optional(z.boolean()),
|
|
20761
|
+
subagent: optional(z.boolean()),
|
|
20611
20762
|
model: optional(z.string()),
|
|
20612
20763
|
variant: optional(z.string())
|
|
20613
20764
|
});
|
|
20765
|
+
/**
|
|
20766
|
+
* Rewrites a boolean `subagent` (OpenCode V2) into `subtask`, the key both V1
|
|
20767
|
+
* and V2 read: V1's command schema rejects unknown keys, while V2 resolves
|
|
20768
|
+
* `subagent ?? subtask`. `subagent` therefore wins when both are set, matching
|
|
20769
|
+
* V2. A non-boolean `subagent` is left untouched for validation to report.
|
|
20770
|
+
*
|
|
20771
|
+
* @see https://opencode.ai/v2/docs/commands/
|
|
20772
|
+
*/
|
|
20773
|
+
function foldSubagentAlias(fields) {
|
|
20774
|
+
if (typeof fields.subagent !== "boolean") return fields;
|
|
20775
|
+
const { subagent, ...rest } = fields;
|
|
20776
|
+
return {
|
|
20777
|
+
...rest,
|
|
20778
|
+
subtask: subagent
|
|
20779
|
+
};
|
|
20780
|
+
}
|
|
20614
20781
|
var OpenCodeCommand = class extends ToolCommand {
|
|
20615
20782
|
/** Directory layout; OpenCode forks (MiMo Code) override it. */
|
|
20616
20783
|
static layout = OPENCODE_LAYOUT;
|
|
@@ -20638,7 +20805,8 @@ var OpenCodeCommand = class extends ToolCommand {
|
|
|
20638
20805
|
return this.frontmatter;
|
|
20639
20806
|
}
|
|
20640
20807
|
toRulesyncCommand() {
|
|
20641
|
-
const { description, ...
|
|
20808
|
+
const { description, ...toolFields } = this.frontmatter;
|
|
20809
|
+
const restFields = foldSubagentAlias(toolFields);
|
|
20642
20810
|
const { toolTarget } = this.constructor.layout;
|
|
20643
20811
|
const rulesyncFrontmatter = {
|
|
20644
20812
|
targets: ["*"],
|
|
@@ -20658,7 +20826,7 @@ var OpenCodeCommand = class extends ToolCommand {
|
|
|
20658
20826
|
}
|
|
20659
20827
|
static fromRulesyncCommand({ outputRoot = process.cwd(), rulesyncCommand, validate = true, global = false }) {
|
|
20660
20828
|
const rulesyncFrontmatter = rulesyncCommand.getFrontmatter();
|
|
20661
|
-
const opencodeFields = rulesyncFrontmatter[this.layout.toolTarget] ?? {};
|
|
20829
|
+
const opencodeFields = foldSubagentAlias(rulesyncFrontmatter[this.layout.toolTarget] ?? {});
|
|
20662
20830
|
const opencodeFrontmatter = {
|
|
20663
20831
|
description: rulesyncFrontmatter.description,
|
|
20664
20832
|
...opencodeFields
|
|
@@ -20714,7 +20882,8 @@ var OpenCodeCommand = class extends ToolCommand {
|
|
|
20714
20882
|
* Imports commands defined inline in `opencode.json` / `opencode.jsonc` under
|
|
20715
20883
|
* the top-level `command` key (in addition to the Markdown files under
|
|
20716
20884
|
* `.opencode/commands/`). Each entry's `template` becomes the command body,
|
|
20717
|
-
* while `description` / `agent` / `model` / `subtask` map to the frontmatter
|
|
20885
|
+
* while `description` / `agent` / `model` / `subtask` map to the frontmatter
|
|
20886
|
+
* (a V2 `subagent` is read as `subtask`, taking precedence like V2 does).
|
|
20718
20887
|
*
|
|
20719
20888
|
* Import-only: this is invoked by the commands processor when loading tool
|
|
20720
20889
|
* files for conversion to rulesync, never for orphan deletion.
|
|
@@ -20734,11 +20903,12 @@ var OpenCodeCommand = class extends ToolCommand {
|
|
|
20734
20903
|
const entry = asOpencodeEntries(rawEntry);
|
|
20735
20904
|
if (!entry) continue;
|
|
20736
20905
|
const body = typeof entry.template === "string" ? entry.template : "";
|
|
20906
|
+
const { subtask } = foldSubagentAlias(entry);
|
|
20737
20907
|
const frontmatter = {
|
|
20738
20908
|
...typeof entry.description === "string" && { description: entry.description },
|
|
20739
20909
|
...typeof entry.agent === "string" && { agent: entry.agent },
|
|
20740
20910
|
...typeof entry.model === "string" && { model: entry.model },
|
|
20741
|
-
...typeof
|
|
20911
|
+
...typeof subtask === "boolean" && { subtask }
|
|
20742
20912
|
};
|
|
20743
20913
|
commands.push(new this({
|
|
20744
20914
|
outputRoot,
|
|
@@ -20791,6 +20961,7 @@ const OMP_RULE_FILE_NAME = "AGENTS.md";
|
|
|
20791
20961
|
const OMP_RULES_DIR_NAME = "rules";
|
|
20792
20962
|
const OMP_AGENTS_DIR_NAME = "agents";
|
|
20793
20963
|
const OMP_MCP_FILE_NAME = "mcp.json";
|
|
20964
|
+
const OMP_CONFIG_FILE_NAME = "config.yml";
|
|
20794
20965
|
const OMP_LAYOUT = {
|
|
20795
20966
|
toolTarget: "omp",
|
|
20796
20967
|
dir: OMP_DIR,
|
|
@@ -22120,6 +22291,7 @@ function warpcliConfigDir() {
|
|
|
22120
22291
|
}
|
|
22121
22292
|
}
|
|
22122
22293
|
const WARP_RULE_FILE_NAME = "AGENTS.md";
|
|
22294
|
+
const WARP_LEGACY_RULE_FILE_NAME = "WARP.md";
|
|
22123
22295
|
const WARP_GLOBAL_RULE_DIR = ".agents";
|
|
22124
22296
|
const WARP_MCP_FILE_NAME = ".mcp.json";
|
|
22125
22297
|
const WARP_PERMISSIONS_FILE_NAME = "settings.toml";
|
|
@@ -35700,7 +35872,9 @@ var JunieIgnore = class JunieIgnore extends ToolIgnore {
|
|
|
35700
35872
|
* KiloIgnore represents ignore patterns for the Kilo Code VSCode extension.
|
|
35701
35873
|
*
|
|
35702
35874
|
* Based on the Kilo Code specification:
|
|
35703
|
-
* - File location:
|
|
35875
|
+
* - File location: workspace root (.kilocodeignore) for project scope, and
|
|
35876
|
+
* `~/.kilocode/.kilocodeignore` for global scope. Kilo merges the global
|
|
35877
|
+
* patterns first and the project ones after, so the project file wins.
|
|
35704
35878
|
* - Syntax: Same as .gitignore
|
|
35705
35879
|
* - Immediate reflection when saved
|
|
35706
35880
|
* - Complete blocking of file access for ignored patterns
|
|
@@ -35708,11 +35882,12 @@ var JunieIgnore = class JunieIgnore extends ToolIgnore {
|
|
|
35708
35882
|
*
|
|
35709
35883
|
* Kilo reads `.kilocodeignore` (not `.kiloignore`), so emitting `.kiloignore`
|
|
35710
35884
|
* left the file inert. https://kilo.ai/docs/customize/context/kilocodeignore
|
|
35885
|
+
* @see https://github.com/Kilo-Org/kilocode/blob/main/packages/opencode/src/kilocode/ignore-migrator.ts
|
|
35711
35886
|
*/
|
|
35712
35887
|
var KiloIgnore = class KiloIgnore extends ToolIgnore {
|
|
35713
|
-
static getSettablePaths() {
|
|
35888
|
+
static getSettablePaths({ global = false } = {}) {
|
|
35714
35889
|
return {
|
|
35715
|
-
relativeDirPath: ".",
|
|
35890
|
+
relativeDirPath: global ? KILO_LEGACY_GLOBAL_DIR : ".",
|
|
35716
35891
|
relativeFilePath: KILO_IGNORE_FILE_NAME
|
|
35717
35892
|
};
|
|
35718
35893
|
}
|
|
@@ -35725,35 +35900,40 @@ var KiloIgnore = class KiloIgnore extends ToolIgnore {
|
|
|
35725
35900
|
/**
|
|
35726
35901
|
* Create KiloIgnore from RulesyncIgnore
|
|
35727
35902
|
*/
|
|
35728
|
-
static fromRulesyncIgnore({ outputRoot = process.cwd(), rulesyncIgnore }) {
|
|
35903
|
+
static fromRulesyncIgnore({ outputRoot = process.cwd(), rulesyncIgnore, global = false }) {
|
|
35729
35904
|
const body = rulesyncIgnore.getFileContent();
|
|
35905
|
+
const paths = this.getSettablePaths({ global });
|
|
35730
35906
|
return new KiloIgnore({
|
|
35731
35907
|
outputRoot,
|
|
35732
|
-
relativeDirPath:
|
|
35733
|
-
relativeFilePath:
|
|
35734
|
-
fileContent: body
|
|
35908
|
+
relativeDirPath: paths.relativeDirPath,
|
|
35909
|
+
relativeFilePath: paths.relativeFilePath,
|
|
35910
|
+
fileContent: body,
|
|
35911
|
+
global
|
|
35735
35912
|
});
|
|
35736
35913
|
}
|
|
35737
35914
|
/**
|
|
35738
35915
|
* Load KiloIgnore from .kilocodeignore file
|
|
35739
35916
|
*/
|
|
35740
|
-
static async fromFile({ outputRoot = process.cwd(), validate = true }) {
|
|
35741
|
-
const
|
|
35917
|
+
static async fromFile({ outputRoot = process.cwd(), validate = true, global = false }) {
|
|
35918
|
+
const paths = this.getSettablePaths({ global });
|
|
35919
|
+
const fileContent = await readFileContent(join(outputRoot, paths.relativeDirPath, paths.relativeFilePath));
|
|
35742
35920
|
return new KiloIgnore({
|
|
35743
35921
|
outputRoot,
|
|
35744
|
-
relativeDirPath:
|
|
35745
|
-
relativeFilePath:
|
|
35922
|
+
relativeDirPath: paths.relativeDirPath,
|
|
35923
|
+
relativeFilePath: paths.relativeFilePath,
|
|
35746
35924
|
fileContent,
|
|
35747
|
-
validate
|
|
35925
|
+
validate,
|
|
35926
|
+
global
|
|
35748
35927
|
});
|
|
35749
35928
|
}
|
|
35750
|
-
static forDeletion({ outputRoot = process.cwd(), relativeDirPath, relativeFilePath }) {
|
|
35929
|
+
static forDeletion({ outputRoot = process.cwd(), relativeDirPath, relativeFilePath, global = false }) {
|
|
35751
35930
|
return new KiloIgnore({
|
|
35752
35931
|
outputRoot,
|
|
35753
35932
|
relativeDirPath,
|
|
35754
35933
|
relativeFilePath,
|
|
35755
35934
|
fileContent: "",
|
|
35756
|
-
validate: false
|
|
35935
|
+
validate: false,
|
|
35936
|
+
global
|
|
35757
35937
|
});
|
|
35758
35938
|
}
|
|
35759
35939
|
};
|
|
@@ -36427,6 +36607,7 @@ const ignoreProcessorGlobalToolTargets = [
|
|
|
36427
36607
|
"continue",
|
|
36428
36608
|
"devin",
|
|
36429
36609
|
"grokcli",
|
|
36610
|
+
"kilo",
|
|
36430
36611
|
"kiro",
|
|
36431
36612
|
"kiro-cli",
|
|
36432
36613
|
"kiro-ide",
|
|
@@ -40288,6 +40469,11 @@ var CrushMcp = class CrushMcp extends ToolMcp {
|
|
|
40288
40469
|
* `g` flag is required because callers may have multiple references per value.
|
|
40289
40470
|
*/
|
|
40290
40471
|
const CANONICAL_ENV_VAR_PATTERN = /\$\{(?!env:)([^}:]+)\}/g;
|
|
40472
|
+
/**
|
|
40473
|
+
* OpenCode / Kilo `{env:VAR}` env var reference pattern. The negative lookbehind
|
|
40474
|
+
* avoids matching Cursor's `${env:VAR}` format.
|
|
40475
|
+
*/
|
|
40476
|
+
const BRACE_ENV_VAR_PATTERN = /(?<!\$)\{env:([^}:]+)\}/g;
|
|
40291
40477
|
function convertRecordValues({ record, pattern, replacement }) {
|
|
40292
40478
|
return Object.fromEntries(Object.entries(record).map(([k, v]) => [k, v.replace(pattern, replacement)]));
|
|
40293
40479
|
}
|
|
@@ -40339,6 +40525,14 @@ function convertEnvVarRefsToToolFormat({ mcpServers, replacement }) {
|
|
|
40339
40525
|
}) }
|
|
40340
40526
|
}]));
|
|
40341
40527
|
}
|
|
40528
|
+
/**
|
|
40529
|
+
* Names of the servers whose `env` or `headers` values carry at least one
|
|
40530
|
+
* canonical `${VAR}` env var reference.
|
|
40531
|
+
*/
|
|
40532
|
+
function findServersWithEnvVarRefs(mcpServers) {
|
|
40533
|
+
const hasRef = (record) => Object.values(record ?? {}).some((value) => value.search(CANONICAL_ENV_VAR_PATTERN) !== -1);
|
|
40534
|
+
return Object.entries(mcpServers).filter(([, config]) => hasRef(config.env) || hasRef(config.headers)).map(([name]) => name);
|
|
40535
|
+
}
|
|
40342
40536
|
//#endregion
|
|
40343
40537
|
//#region src/features/mcp/cursor-mcp.ts
|
|
40344
40538
|
const CURSOR_ENV_VAR_PATTERN = /\$\{env:([^}:]+)\}/g;
|
|
@@ -42627,17 +42821,50 @@ function readExistingKiloMcpEntries(fileContent) {
|
|
|
42627
42821
|
}
|
|
42628
42822
|
return entries;
|
|
42629
42823
|
}
|
|
42630
|
-
|
|
42824
|
+
const KILO_UNTRUSTED_ENV_REF_PATTERN = /\{env:[^}]+\}/;
|
|
42825
|
+
/**
|
|
42826
|
+
* Judged on the entry as written, not the canonical server: fields the entry
|
|
42827
|
+
* drops (a toggle keeps only `enabled`) cannot make Kilo reject the file.
|
|
42828
|
+
*/
|
|
42829
|
+
function rejectUntrustedEnvRef(serverName, converted, rejectEnvRefs, logger) {
|
|
42830
|
+
if (!rejectEnvRefs || converted === null) return converted;
|
|
42831
|
+
if (!KILO_UNTRUSTED_ENV_REF_PATTERN.test(JSON.stringify(converted))) return converted;
|
|
42832
|
+
return warnAndSkipMcpServer({
|
|
42833
|
+
toolName: "Kilo",
|
|
42834
|
+
serverName,
|
|
42835
|
+
reason: "an {env:...} reference, which makes Kilo reject the whole project config",
|
|
42836
|
+
logger
|
|
42837
|
+
});
|
|
42838
|
+
}
|
|
42839
|
+
function convertToKiloFormat(mcpServers, existingMcp, logger, rejectEnvRefs = false) {
|
|
42631
42840
|
const tools = {};
|
|
42632
42841
|
return {
|
|
42633
42842
|
mcp: Object.fromEntries(Object.entries(mcpServers).map(([serverName, serverConfig]) => {
|
|
42634
|
-
const converted = convertServerToKiloFormat(serverName, serverConfig, Object.hasOwn(existingMcp, serverName) ? existingMcp[serverName] : void 0, logger);
|
|
42843
|
+
const converted = rejectUntrustedEnvRef(serverName, convertServerToKiloFormat(serverName, serverConfig, Object.hasOwn(existingMcp, serverName) ? existingMcp[serverName] : void 0, logger), rejectEnvRefs, logger);
|
|
42635
42844
|
collectKiloServerTools(tools, serverName, serverConfig);
|
|
42636
42845
|
return converted === null ? null : [serverName, converted];
|
|
42637
42846
|
}).filter((entry) => entry !== null)),
|
|
42638
42847
|
tools
|
|
42639
42848
|
};
|
|
42640
42849
|
}
|
|
42850
|
+
/**
|
|
42851
|
+
* Kilo expands only `{env:VAR}`, never the canonical `${VAR}`, and only in a
|
|
42852
|
+
* trusted config: the global `~/.config/kilo/kilo.jsonc`. A project config is
|
|
42853
|
+
* untrusted, so any `{env:` in it makes Kilo drop the whole file (or, in MCP
|
|
42854
|
+
* headers, the server). In project scope canonical references are therefore
|
|
42855
|
+
* left as written — Kilo passes them through literally — with a warning (a
|
|
42856
|
+
* written entry already carrying `{env:` is skipped by `convertToKiloFormat`).
|
|
42857
|
+
* @see https://github.com/Kilo-Org/kilocode/blob/main/packages/opencode/src/config/variable.ts
|
|
42858
|
+
*/
|
|
42859
|
+
function resolveKiloEnvVarRefs({ mcpServers, global, logger }) {
|
|
42860
|
+
if (global) return convertEnvVarRefsToToolFormat({
|
|
42861
|
+
mcpServers,
|
|
42862
|
+
replacement: "{env:$1}"
|
|
42863
|
+
});
|
|
42864
|
+
const serverNames = findServersWithEnvVarRefs(mcpServers);
|
|
42865
|
+
if (serverNames.length > 0) logger?.warn(`Kilo MCP servers ${serverNames.map((name) => `"${name}"`).join(", ")} use environment variable references (\${VAR}), which Kilo does not resolve in a project config; they are written literally. Use global mode (~/.config/kilo/kilo.jsonc) to have them expanded.`);
|
|
42866
|
+
return mcpServers;
|
|
42867
|
+
}
|
|
42641
42868
|
var KiloMcp = class KiloMcp extends ToolMcp {
|
|
42642
42869
|
json;
|
|
42643
42870
|
constructor(params) {
|
|
@@ -42736,7 +42963,11 @@ var KiloMcp = class KiloMcp extends ToolMcp {
|
|
|
42736
42963
|
if (fileContent) relativeFilePath = KILO_JSON_FILE_NAME;
|
|
42737
42964
|
}
|
|
42738
42965
|
const existingMcp = readExistingKiloMcpEntries(fileContent);
|
|
42739
|
-
const { mcp: convertedMcp, tools: mcpTools } = convertToKiloFormat(
|
|
42966
|
+
const { mcp: convertedMcp, tools: mcpTools } = convertToKiloFormat(resolveKiloEnvVarRefs({
|
|
42967
|
+
mcpServers: rulesyncMcp.getMcpServers(),
|
|
42968
|
+
global,
|
|
42969
|
+
logger
|
|
42970
|
+
}), existingMcp, logger, !global);
|
|
42740
42971
|
return new KiloMcp({
|
|
42741
42972
|
outputRoot,
|
|
42742
42973
|
relativeDirPath: basePaths.relativeDirPath,
|
|
@@ -42802,7 +43033,10 @@ var KiloMcp = class KiloMcp extends ToolMcp {
|
|
|
42802
43033
|
});
|
|
42803
43034
|
}
|
|
42804
43035
|
toRulesyncMcp() {
|
|
42805
|
-
const { shared, toolOnly } = splitMcpServersByTransport(
|
|
43036
|
+
const { shared, toolOnly } = splitMcpServersByTransport(convertEnvVarRefsFromToolFormat({
|
|
43037
|
+
mcpServers: convertFromKiloFormat(this.json.mcp ?? {}, this.json.tools),
|
|
43038
|
+
pattern: BRACE_ENV_VAR_PATTERN
|
|
43039
|
+
}));
|
|
42806
43040
|
return this.toRulesyncMcpDefault({ fileContent: JSON.stringify({
|
|
42807
43041
|
mcpServers: shared,
|
|
42808
43042
|
...Object.keys(toolOnly).length > 0 && { kilo: { mcpServers: toolOnly } }
|
|
@@ -43259,7 +43493,6 @@ var KiroMcp = class KiroMcp extends ToolMcp {
|
|
|
43259
43493
|
};
|
|
43260
43494
|
//#endregion
|
|
43261
43495
|
//#region src/features/mcp/opencode-mcp.ts
|
|
43262
|
-
const OPENCODE_ENV_VAR_PATTERN = /(?<!\$)\{env:([^}:]+)\}/g;
|
|
43263
43496
|
const OpencodeMcpLocalServerSchema = z.looseObject({
|
|
43264
43497
|
type: z.literal("local"),
|
|
43265
43498
|
command: z.array(z.string()),
|
|
@@ -43642,7 +43875,7 @@ var OpencodeMcp = class extends ToolMcp {
|
|
|
43642
43875
|
toRulesyncMcp() {
|
|
43643
43876
|
const { shared, toolOnly } = splitMcpServersByTransport(convertEnvVarRefsFromToolFormat({
|
|
43644
43877
|
mcpServers: convertFromOpencodeFormat(this.json.mcp ?? {}, this.json.tools),
|
|
43645
|
-
pattern:
|
|
43878
|
+
pattern: BRACE_ENV_VAR_PATTERN
|
|
43646
43879
|
}));
|
|
43647
43880
|
return this.toRulesyncMcpDefault({ fileContent: JSON.stringify({
|
|
43648
43881
|
mcpServers: shared,
|
|
@@ -44925,6 +45158,28 @@ const REASONIX_RETIRED_PLUGIN_FIELDS = ["trusted_read_only_tools"];
|
|
|
44925
45158
|
function warnAboutRetiredFields({ name, serverRecord, logger }) {
|
|
44926
45159
|
for (const field of REASONIX_RETIRED_PLUGIN_FIELDS) if (serverRecord[field] !== void 0) logger?.warn(`Reasonix MCP: dropping "${field}" from "${name}"; Reasonix retired the field in v1.17.18 and ignores it, so it is no longer written.`);
|
|
44927
45160
|
}
|
|
45161
|
+
/**
|
|
45162
|
+
* The `concurrency` values Reasonix honors. It trims and lowercases before
|
|
45163
|
+
* comparing (`mcpServerIsSerial` in `internal/agent/mcp_concurrency.go`), so
|
|
45164
|
+
* `" Serial "` counts too and is written as authored.
|
|
45165
|
+
*/
|
|
45166
|
+
const REASONIX_CONCURRENCY_VALUES = /* @__PURE__ */ new Set(["serial", "parallel"]);
|
|
45167
|
+
/**
|
|
45168
|
+
* Why a `concurrency` / `auto_start` value cannot be written, or `undefined`
|
|
45169
|
+
* when it can (and for every other field). A wrong type is the case that
|
|
45170
|
+
* matters: Reasonix declares the fields as `string` / `*bool`, and a TOML type
|
|
45171
|
+
* mismatch makes it refuse the whole config file. An unknown `concurrency`
|
|
45172
|
+
* string decodes fine but is ignored in favor of the server-name default, so it
|
|
45173
|
+
* is dropped too rather than written as a setting that does nothing.
|
|
45174
|
+
* @see https://github.com/esengine/DeepSeek-Reasonix/blob/main-v2/internal/config/plugin_entry.go
|
|
45175
|
+
*/
|
|
45176
|
+
function invalidSchedulingFieldReason(field, value) {
|
|
45177
|
+
if (field === "auto_start" && typeof value !== "boolean") return "Reasonix expects a boolean and fails to load a config file holding any other type.";
|
|
45178
|
+
if (field === "concurrency") {
|
|
45179
|
+
if (typeof value !== "string") return "Reasonix expects a string and fails to load a config file holding any other type.";
|
|
45180
|
+
if (!REASONIX_CONCURRENCY_VALUES.has(value.trim().toLowerCase())) return `Reasonix accepts only "serial" or "parallel" and ignores ${JSON.stringify(value)}, falling back to the server-name default.`;
|
|
45181
|
+
}
|
|
45182
|
+
}
|
|
44928
45183
|
function rulesyncMcpServerToReasonix(name, server, logger) {
|
|
44929
45184
|
const serverRecord = server;
|
|
44930
45185
|
const type = resolveReasonixType(server);
|
|
@@ -44949,7 +45204,14 @@ function rulesyncMcpServerToReasonix(name, server, logger) {
|
|
|
44949
45204
|
}
|
|
44950
45205
|
for (const field of REASONIX_PLUGIN_FIELDS) {
|
|
44951
45206
|
if (field === "type" || field === "command" || field === "args") continue;
|
|
44952
|
-
|
|
45207
|
+
const value = serverRecord[field];
|
|
45208
|
+
if (value === void 0) continue;
|
|
45209
|
+
const invalidReason = invalidSchedulingFieldReason(field, value);
|
|
45210
|
+
if (invalidReason !== void 0) {
|
|
45211
|
+
logger?.warn(`Reasonix MCP: dropping "${field}" from "${name}"; ${invalidReason}`);
|
|
45212
|
+
continue;
|
|
45213
|
+
}
|
|
45214
|
+
plugin[field] = value;
|
|
44953
45215
|
}
|
|
44954
45216
|
warnAboutRetiredFields({
|
|
44955
45217
|
name,
|
|
@@ -52016,7 +52278,7 @@ const CODEBUDDY_RULE_LISTS = [
|
|
|
52016
52278
|
"ask",
|
|
52017
52279
|
"deny"
|
|
52018
52280
|
];
|
|
52019
|
-
const CATCH_ALL_PATTERN$
|
|
52281
|
+
const CATCH_ALL_PATTERN$11 = "*";
|
|
52020
52282
|
const MCP_CATEGORY = "mcp";
|
|
52021
52283
|
const MCP_RULE_PREFIX = "mcp__";
|
|
52022
52284
|
const ALL_MCP_RULE = "mcp__*";
|
|
@@ -52089,7 +52351,7 @@ function categoryOfRule$2(rule) {
|
|
|
52089
52351
|
* and reach the `Bash` rules through `honorAllToolsOnBash`.
|
|
52090
52352
|
*/
|
|
52091
52353
|
function buildCodebuddyRule({ category, pattern, action, logger }) {
|
|
52092
|
-
const catchAll = pattern === CATCH_ALL_PATTERN$
|
|
52354
|
+
const catchAll = pattern === CATCH_ALL_PATTERN$11;
|
|
52093
52355
|
if (category === MCP_CATEGORY) {
|
|
52094
52356
|
if (catchAll && action === "allow") {
|
|
52095
52357
|
logger?.warn(`CodeBuddy permissions: skipping the 'mcp' allow rule '*' — CodeBuddy honors the all-MCP wildcard 'mcp__*' in deny and ask only. Allow servers by name instead (for example 'mcp__github').`);
|
|
@@ -52098,7 +52360,7 @@ function buildCodebuddyRule({ category, pattern, action, logger }) {
|
|
|
52098
52360
|
return catchAll ? ALL_MCP_RULE : `${MCP_RULE_PREFIX}${pattern}`;
|
|
52099
52361
|
}
|
|
52100
52362
|
if (category === "*") {
|
|
52101
|
-
if (catchAll) return CATCH_ALL_PATTERN$
|
|
52363
|
+
if (catchAll) return CATCH_ALL_PATTERN$11;
|
|
52102
52364
|
logger?.warn(`CodeBuddy permissions: skipping the all-tools '*' rule '${pattern}' — CodeBuddy's standalone '*' rule takes no specifier. Write it under the category it is meant for (for example 'bash' or 'read').`);
|
|
52103
52365
|
return null;
|
|
52104
52366
|
}
|
|
@@ -52116,7 +52378,7 @@ function parseCodebuddyRule(rule) {
|
|
|
52116
52378
|
const { specifier } = splitCodebuddyRule(rule);
|
|
52117
52379
|
if (specifier === void 0) return {
|
|
52118
52380
|
category,
|
|
52119
|
-
pattern: CATCH_ALL_PATTERN$
|
|
52381
|
+
pattern: CATCH_ALL_PATTERN$11
|
|
52120
52382
|
};
|
|
52121
52383
|
const trimmed = specifier.trim();
|
|
52122
52384
|
return trimmed === "" ? null : {
|
|
@@ -52292,6 +52554,633 @@ function parseCodebuddySettings({ fileContent, relativePath }) {
|
|
|
52292
52554
|
}
|
|
52293
52555
|
}
|
|
52294
52556
|
//#endregion
|
|
52557
|
+
//#region src/features/permissions/codewhale-permissions.ts
|
|
52558
|
+
const CODEWHALE_SHELL_TOOL = "exec_shell";
|
|
52559
|
+
/**
|
|
52560
|
+
* Canonical path categories and the Codewhale policy tool names their rules are
|
|
52561
|
+
* written for. Codewhale consults typed rules only for `exec_shell` and these
|
|
52562
|
+
* file tools, so a rule for any other tool (`fetch_url`, `web_search`, an MCP
|
|
52563
|
+
* tool, ...) is never evaluated. `edit` covers `apply_patch` too: both modify
|
|
52564
|
+
* existing files, and an `edit` deny that left `apply_patch` open would not
|
|
52565
|
+
* hold.
|
|
52566
|
+
*
|
|
52567
|
+
* @see https://github.com/Hmbown/Codewhale/blob/main/crates/tui/src/core/engine.rs (`file_tool_permission_paths`)
|
|
52568
|
+
* @see https://github.com/Hmbown/Codewhale/blob/main/crates/tui/src/tools/canonical_action.rs
|
|
52569
|
+
*/
|
|
52570
|
+
const CODEWHALE_PATH_CATEGORY_TOOLS = {
|
|
52571
|
+
read: ["read_file"],
|
|
52572
|
+
write: ["write_file"],
|
|
52573
|
+
edit: ["edit_file", "apply_patch"],
|
|
52574
|
+
grep: ["grep_files"],
|
|
52575
|
+
glob: ["file_search"],
|
|
52576
|
+
list: ["list_dir"]
|
|
52577
|
+
};
|
|
52578
|
+
/**
|
|
52579
|
+
* `apply_patch` can also create files (a `--- /dev/null` hunk), so a `write`
|
|
52580
|
+
* restriction is written for it too; a `write` allow is not, since it would
|
|
52581
|
+
* approve edits to existing files the `edit` category never allowed.
|
|
52582
|
+
*
|
|
52583
|
+
* @see https://github.com/Hmbown/Codewhale/blob/main/crates/tui/src/tools/apply_patch.rs
|
|
52584
|
+
*/
|
|
52585
|
+
const CODEWHALE_WRITE_RESTRICTION_TOOLS = ["write_file", "apply_patch"];
|
|
52586
|
+
/** The Codewhale tools a canonical path rule is written for. */
|
|
52587
|
+
function pathCategoryTools(category, action) {
|
|
52588
|
+
if (category === "write" && action !== "allow") return CODEWHALE_WRITE_RESTRICTION_TOOLS;
|
|
52589
|
+
return CODEWHALE_PATH_CATEGORY_TOOLS[category] ?? [];
|
|
52590
|
+
}
|
|
52591
|
+
/** The canonical path categories a Codewhale file tool's rule can come from. */
|
|
52592
|
+
const CODEWHALE_TOOL_TO_PATH_CATEGORIES = {
|
|
52593
|
+
read_file: ["read"],
|
|
52594
|
+
write_file: ["write"],
|
|
52595
|
+
edit_file: ["edit"],
|
|
52596
|
+
apply_patch: ["edit", "write"],
|
|
52597
|
+
grep_files: ["grep"],
|
|
52598
|
+
file_search: ["glob"],
|
|
52599
|
+
list_dir: ["list"]
|
|
52600
|
+
};
|
|
52601
|
+
const CODEWHALE_GLOBAL_ONLY_MESSAGE = "Codewhale permissions are global-only; use --global to sync ~/.codewhale/permissions.toml";
|
|
52602
|
+
const COMMAND_WILDCARD_CHARACTERS = /[*?[]/;
|
|
52603
|
+
const PATH_GLOB_CHARACTERS = /[*?[\]{}]/;
|
|
52604
|
+
function isCatchAllPattern(pattern) {
|
|
52605
|
+
return pattern === "*" || pattern === "**";
|
|
52606
|
+
}
|
|
52607
|
+
/**
|
|
52608
|
+
* The literal command prefix a wildcard pattern pins down, cut back to a word
|
|
52609
|
+
* boundary so it never names a partial word (`git push* --force` pins down
|
|
52610
|
+
* `git`, not `git push`). Empty when the pattern starts with a wildcard.
|
|
52611
|
+
*/
|
|
52612
|
+
function literalCommandPrefix(pattern) {
|
|
52613
|
+
const firstWildcard = pattern.search(COMMAND_WILDCARD_CHARACTERS);
|
|
52614
|
+
const head = pattern.slice(0, firstWildcard);
|
|
52615
|
+
if (/\s$/.test(head)) return head.trim();
|
|
52616
|
+
const lastBoundary = head.search(/\s\S*$/);
|
|
52617
|
+
return lastBoundary < 0 ? "" : head.slice(0, lastBoundary).trim();
|
|
52618
|
+
}
|
|
52619
|
+
/**
|
|
52620
|
+
* Convert one canonical `bash` rule. Codewhale matches `command` as a
|
|
52621
|
+
* word-boundary prefix of the invocation (or the whole invocation with
|
|
52622
|
+
* `command_exact = true`); it expands no wildcards.
|
|
52623
|
+
*
|
|
52624
|
+
* - `*` / `**` → a tool-wide rule.
|
|
52625
|
+
* - `git *` → the prefix `git`.
|
|
52626
|
+
* - A wildcard-free `allow` → an exact command, so it approves nothing more
|
|
52627
|
+
* than the command it names. A wildcard-free `deny` / `ask` → a prefix, the
|
|
52628
|
+
* way Codewhale's own deny rules are matched, so the restriction also covers
|
|
52629
|
+
* the command with more arguments.
|
|
52630
|
+
* - Any other wildcard: a restricting rule is broadened to the literal prefix
|
|
52631
|
+
* it pins down; an `allow` is skipped, since no prefix approves only what it
|
|
52632
|
+
* names.
|
|
52633
|
+
*/
|
|
52634
|
+
function convertShellRule({ pattern, action, source = SHELL_PERMISSION_CATEGORY, logger }) {
|
|
52635
|
+
const trimmed = pattern.trim();
|
|
52636
|
+
if (isCatchAllPattern(trimmed)) return {
|
|
52637
|
+
tool: CODEWHALE_SHELL_TOOL,
|
|
52638
|
+
action
|
|
52639
|
+
};
|
|
52640
|
+
if (trimmed === "") {
|
|
52641
|
+
logger?.warn(`Codewhale permissions: skipping empty ${source} ${action} pattern.`);
|
|
52642
|
+
return null;
|
|
52643
|
+
}
|
|
52644
|
+
if (!COMMAND_WILDCARD_CHARACTERS.test(trimmed)) return action === "allow" ? {
|
|
52645
|
+
tool: CODEWHALE_SHELL_TOOL,
|
|
52646
|
+
command: trimmed,
|
|
52647
|
+
command_exact: true,
|
|
52648
|
+
action
|
|
52649
|
+
} : {
|
|
52650
|
+
tool: CODEWHALE_SHELL_TOOL,
|
|
52651
|
+
command: trimmed,
|
|
52652
|
+
action
|
|
52653
|
+
};
|
|
52654
|
+
const trailingPrefix = trimmed.endsWith(" *") ? trimmed.slice(0, -2).trim() : null;
|
|
52655
|
+
if (trailingPrefix && !COMMAND_WILDCARD_CHARACTERS.test(trailingPrefix)) return {
|
|
52656
|
+
tool: CODEWHALE_SHELL_TOOL,
|
|
52657
|
+
command: trailingPrefix,
|
|
52658
|
+
action
|
|
52659
|
+
};
|
|
52660
|
+
if (action === "allow") {
|
|
52661
|
+
logger?.warn(`Codewhale permissions: skipping ${source} allow "${pattern}" because Codewhale matches commands by prefix and expands no wildcards.`);
|
|
52662
|
+
return null;
|
|
52663
|
+
}
|
|
52664
|
+
const prefix = literalCommandPrefix(trimmed);
|
|
52665
|
+
if (prefix === "") {
|
|
52666
|
+
logger?.warn(`Codewhale permissions: skipping ${source} ${action} "${pattern}" because it pins down no literal command prefix Codewhale could match.`);
|
|
52667
|
+
return null;
|
|
52668
|
+
}
|
|
52669
|
+
logger?.warn(`Codewhale permissions: broadening ${source} ${action} "${pattern}" to the command prefix "${prefix}" because Codewhale expands no wildcards.`);
|
|
52670
|
+
return {
|
|
52671
|
+
tool: CODEWHALE_SHELL_TOOL,
|
|
52672
|
+
command: prefix,
|
|
52673
|
+
action
|
|
52674
|
+
};
|
|
52675
|
+
}
|
|
52676
|
+
/**
|
|
52677
|
+
* Convert one canonical path rule. Codewhale matches `path` exactly (after
|
|
52678
|
+
* normalizing it against the workspace) and expands no globs, so `*` / `**`
|
|
52679
|
+
* become a tool-wide rule and any other glob is skipped: narrowing it to a
|
|
52680
|
+
* single path would leave the rest of the glob unrestricted, and widening it to
|
|
52681
|
+
* the whole tool would restrict far more than written.
|
|
52682
|
+
*/
|
|
52683
|
+
function convertPathRules({ category, pattern, action, logger }) {
|
|
52684
|
+
const tools = pathCategoryTools(category, action);
|
|
52685
|
+
const trimmed = pattern.trim();
|
|
52686
|
+
if (isCatchAllPattern(trimmed)) return tools.map((tool) => ({
|
|
52687
|
+
tool,
|
|
52688
|
+
action
|
|
52689
|
+
}));
|
|
52690
|
+
if (trimmed === "" || PATH_GLOB_CHARACTERS.test(trimmed)) {
|
|
52691
|
+
logger?.warn(`Codewhale permissions: skipping ${category} ${action} "${pattern}" because Codewhale matches exact paths and expands no globs.`);
|
|
52692
|
+
return [];
|
|
52693
|
+
}
|
|
52694
|
+
return tools.map((tool) => ({
|
|
52695
|
+
tool,
|
|
52696
|
+
path: trimmed,
|
|
52697
|
+
action
|
|
52698
|
+
}));
|
|
52699
|
+
}
|
|
52700
|
+
function ruleKey(rule) {
|
|
52701
|
+
return JSON.stringify([
|
|
52702
|
+
rule.tool,
|
|
52703
|
+
rule.command ?? null,
|
|
52704
|
+
rule.command_exact ?? false,
|
|
52705
|
+
rule.path ?? null,
|
|
52706
|
+
rule.workspace ?? null,
|
|
52707
|
+
rule.action ?? "ask"
|
|
52708
|
+
]);
|
|
52709
|
+
}
|
|
52710
|
+
function withExplicitAction(rule) {
|
|
52711
|
+
return {
|
|
52712
|
+
...rule,
|
|
52713
|
+
action: rule.action ?? "ask"
|
|
52714
|
+
};
|
|
52715
|
+
}
|
|
52716
|
+
/**
|
|
52717
|
+
* Codewhale picks the matching rule by action (`deny` > `ask` > `allow`) before
|
|
52718
|
+
* specificity, so file order never changes a decision. The rules are still
|
|
52719
|
+
* sorted that way, then by tool, so the output is stable and reads in the order
|
|
52720
|
+
* Codewhale applies it.
|
|
52721
|
+
*/
|
|
52722
|
+
function sortRules(rules) {
|
|
52723
|
+
return rules.map((rule, index) => ({
|
|
52724
|
+
rule,
|
|
52725
|
+
index
|
|
52726
|
+
})).toSorted((left, right) => {
|
|
52727
|
+
const actionOrder = PERMISSION_ACTION_PRIORITY[right.rule.action] - PERMISSION_ACTION_PRIORITY[left.rule.action];
|
|
52728
|
+
if (actionOrder !== 0) return actionOrder;
|
|
52729
|
+
const toolOrder = left.rule.tool.localeCompare(right.rule.tool);
|
|
52730
|
+
if (toolOrder !== 0) return toolOrder;
|
|
52731
|
+
return left.index - right.index;
|
|
52732
|
+
}).map(({ rule }) => rule);
|
|
52733
|
+
}
|
|
52734
|
+
function recordDroppedRestriction({ dropped, tools, glob }) {
|
|
52735
|
+
for (const tool of tools) dropped.set(tool, [...dropped.get(tool) ?? [], glob]);
|
|
52736
|
+
}
|
|
52737
|
+
/**
|
|
52738
|
+
* A `[...]` class in a restriction widens to `?` (any one character): matched
|
|
52739
|
+
* after lowercasing, a negated or mixed-case class would otherwise narrow what
|
|
52740
|
+
* the restriction covers.
|
|
52741
|
+
*/
|
|
52742
|
+
function widenRestrictionClasses(pattern) {
|
|
52743
|
+
return pattern.replace(/\[[!^]?\]?[^\]]*\]/g, "?");
|
|
52744
|
+
}
|
|
52745
|
+
/**
|
|
52746
|
+
* An `allow` rule names a literal command or path, so a `[` in it widens to `?`
|
|
52747
|
+
* rather than opening a class that would not even match the rule itself.
|
|
52748
|
+
*/
|
|
52749
|
+
function literalAsGlob(value) {
|
|
52750
|
+
return value.replaceAll("[", "?");
|
|
52751
|
+
}
|
|
52752
|
+
/**
|
|
52753
|
+
* Codewhale matches commands case-insensitively and word by word, so commands
|
|
52754
|
+
* are compared lowercased with their whitespace collapsed.
|
|
52755
|
+
*/
|
|
52756
|
+
function normalizeCommandGlob(glob) {
|
|
52757
|
+
return glob.trim().replace(/\s+/g, " ").toLowerCase();
|
|
52758
|
+
}
|
|
52759
|
+
/**
|
|
52760
|
+
* Codewhale normalizes a rule's path against the workspace, dropping empty and
|
|
52761
|
+
* `.` segments, so the same file can be spelled several ways. Paths are
|
|
52762
|
+
* compared lowercased (for case-insensitive file systems) with those segments
|
|
52763
|
+
* dropped, a `{a,b}` group widens to `*`, a `**` segment widens so it also
|
|
52764
|
+
* matches no directory at all (a leading `**` segment before `*.pem` reaches
|
|
52765
|
+
* `dev.pem`, and `secrets/**` reaches `secrets`), and a path that is absolute,
|
|
52766
|
+
* home-relative or climbs with `..` widens to `*` outright, since it cannot be
|
|
52767
|
+
* compared against a workspace-relative one. Every rewrite only widens a
|
|
52768
|
+
* pattern, so an inexact comparison withholds an `allow` rather than writing
|
|
52769
|
+
* one a dropped restriction overlaps.
|
|
52770
|
+
*/
|
|
52771
|
+
function normalizePathGlob(glob) {
|
|
52772
|
+
const normalized = glob.trim().replaceAll("\\", "/").toLowerCase();
|
|
52773
|
+
if (/^(?:\/|~|[a-z]:)/.test(normalized) || /(?:^|\/)\.\.(?:\/|$)/.test(normalized)) return "*";
|
|
52774
|
+
return normalized.split("/").filter((segment) => segment !== "" && segment !== ".").join("/").replace(/\{.*\}/g, "*").replace(/\*\*\//g, "*").replace(/\/\*\*$/, "*");
|
|
52775
|
+
}
|
|
52776
|
+
/** A dropped `bash` (or all-tools) restriction, as a glob to compare `allow` rules against. */
|
|
52777
|
+
function shellRestrictionGlob(pattern) {
|
|
52778
|
+
return normalizeCommandGlob(widenRestrictionClasses(pattern));
|
|
52779
|
+
}
|
|
52780
|
+
/** A dropped path restriction, as a glob to compare `allow` rules against. */
|
|
52781
|
+
function pathRestrictionGlob(pattern) {
|
|
52782
|
+
return normalizePathGlob(widenRestrictionClasses(pattern));
|
|
52783
|
+
}
|
|
52784
|
+
/**
|
|
52785
|
+
* The file tools whose `path` is the directory a search or listing starts from,
|
|
52786
|
+
* so an `allow` on a directory reaches everything under it.
|
|
52787
|
+
*/
|
|
52788
|
+
const CODEWHALE_DIRECTORY_ROOTED_TOOLS = /* @__PURE__ */ new Set([
|
|
52789
|
+
"grep_files",
|
|
52790
|
+
"file_search",
|
|
52791
|
+
"list_dir"
|
|
52792
|
+
]);
|
|
52793
|
+
/**
|
|
52794
|
+
* The globs a written `allow` rule approves, for comparison with dropped
|
|
52795
|
+
* restrictions. A path that normalizes to the workspace root (`.`) approves
|
|
52796
|
+
* everything, and a directory-rooted tool's path approves what lies under it.
|
|
52797
|
+
*/
|
|
52798
|
+
function allowRuleComparisonGlobs(rule) {
|
|
52799
|
+
if (rule.tool === CODEWHALE_SHELL_TOOL) {
|
|
52800
|
+
if (rule.command === void 0) return ["*"];
|
|
52801
|
+
const command = normalizeCommandGlob(literalAsGlob(rule.command));
|
|
52802
|
+
return rule.command_exact ? [command] : [command, `${command} *`];
|
|
52803
|
+
}
|
|
52804
|
+
const path = rule.path === void 0 ? "" : normalizePathGlob(literalAsGlob(rule.path));
|
|
52805
|
+
if (path === "") return ["*"];
|
|
52806
|
+
return CODEWHALE_DIRECTORY_ROOTED_TOOLS.has(rule.tool) ? [path, `${path}/*`] : [path];
|
|
52807
|
+
}
|
|
52808
|
+
/**
|
|
52809
|
+
* Drop every `allow` rule that a restriction which could not be written
|
|
52810
|
+
* overlaps (any one command or path matches both), so what the restriction
|
|
52811
|
+
* named reaches Codewhale's approval prompt instead of being auto-approved. An
|
|
52812
|
+
* `allow` no dropped restriction overlaps is kept. Comparisons share one budget;
|
|
52813
|
+
* once it runs out every comparison answers "overlaps", so the rule is withheld.
|
|
52814
|
+
*/
|
|
52815
|
+
function withholdShadowedAllowRules({ rules, dropped, logger }) {
|
|
52816
|
+
const budget = createIntersectionBudget();
|
|
52817
|
+
const tests = new Map([...dropped].map(([tool, globs]) => [tool, createShadowingRestrictionsTest(globs.map((pattern) => ({
|
|
52818
|
+
pattern,
|
|
52819
|
+
fromAllToolsCategory: false
|
|
52820
|
+
})), { budget })]));
|
|
52821
|
+
return rules.filter((rule) => {
|
|
52822
|
+
const test = tests.get(rule.tool);
|
|
52823
|
+
if ((rule.action ?? "ask") !== "allow" || test === void 0) return true;
|
|
52824
|
+
const shadowing = [...new Set(allowRuleComparisonGlobs(rule).flatMap((glob) => test(glob)))];
|
|
52825
|
+
if (shadowing.length === 0) return true;
|
|
52826
|
+
logger?.warn(`Codewhale permissions: withholding ${rule.tool} allow ${JSON.stringify(rule.command ?? rule.path ?? "*")}${rule.workspace === void 0 ? "" : ` (workspace ${rule.workspace})`} because the deny/ask rule(s) ${shadowing.map((pattern) => JSON.stringify(pattern)).join(", ")} for ${rule.tool} could not be written; those calls keep Codewhale's approval prompt.`);
|
|
52827
|
+
return false;
|
|
52828
|
+
});
|
|
52829
|
+
}
|
|
52830
|
+
/**
|
|
52831
|
+
* An all-tools `deny` / `ask` names paths as much as commands (`secrets/**`
|
|
52832
|
+
* under `*` denies a path), so it is written for every file tool as well. A
|
|
52833
|
+
* pattern Codewhale cannot match as a path is recorded as dropped for every file
|
|
52834
|
+
* tool instead, the same way a skipped restriction of a path category is.
|
|
52835
|
+
*/
|
|
52836
|
+
function convertAllToolsPathRestriction({ pattern, action, converted, dropped, logger }) {
|
|
52837
|
+
const categories = Object.keys(CODEWHALE_PATH_CATEGORY_TOOLS);
|
|
52838
|
+
const pathRules = categories.flatMap((category) => convertPathRules({
|
|
52839
|
+
category,
|
|
52840
|
+
pattern,
|
|
52841
|
+
action
|
|
52842
|
+
}));
|
|
52843
|
+
if (pathRules.length > 0) {
|
|
52844
|
+
converted.push(...pathRules);
|
|
52845
|
+
return;
|
|
52846
|
+
}
|
|
52847
|
+
logger?.warn(`Codewhale permissions: all-tools ${action} "${pattern}" cannot be written for Codewhale's file tools, which match exact paths and expand no globs.`);
|
|
52848
|
+
for (const category of categories) recordDroppedRestriction({
|
|
52849
|
+
dropped,
|
|
52850
|
+
tools: pathCategoryTools(category, action),
|
|
52851
|
+
glob: pathRestrictionGlob(pattern)
|
|
52852
|
+
});
|
|
52853
|
+
}
|
|
52854
|
+
/**
|
|
52855
|
+
* Convert the rules that govern shell commands (`bash`, plus the restricting
|
|
52856
|
+
* rules of the all-tools `*` category) into `converted`, recording in
|
|
52857
|
+
* `dropped` each restriction that could not be written.
|
|
52858
|
+
*/
|
|
52859
|
+
function convertShellCommandRules({ config, converted, dropped, logger }) {
|
|
52860
|
+
const { rules: shellRules, ignoredAllToolsAllowPatterns } = collectShellCommandRules(config.permission);
|
|
52861
|
+
for (const { pattern, action, fromAllToolsCategory } of shellRules) {
|
|
52862
|
+
const rule = convertShellRule({
|
|
52863
|
+
pattern,
|
|
52864
|
+
action,
|
|
52865
|
+
source: fromAllToolsCategory ? "all-tools (as a shell command)" : SHELL_PERMISSION_CATEGORY,
|
|
52866
|
+
logger
|
|
52867
|
+
});
|
|
52868
|
+
if (rule) converted.push(rule);
|
|
52869
|
+
else if (action !== "allow") recordDroppedRestriction({
|
|
52870
|
+
dropped,
|
|
52871
|
+
tools: [CODEWHALE_SHELL_TOOL],
|
|
52872
|
+
glob: shellRestrictionGlob(pattern)
|
|
52873
|
+
});
|
|
52874
|
+
if (fromAllToolsCategory) convertAllToolsPathRestriction({
|
|
52875
|
+
pattern,
|
|
52876
|
+
action,
|
|
52877
|
+
converted,
|
|
52878
|
+
dropped,
|
|
52879
|
+
logger
|
|
52880
|
+
});
|
|
52881
|
+
}
|
|
52882
|
+
if (ignoredAllToolsAllowPatterns.length > 0) logger?.warn(`Codewhale permissions: skipping all-tools allow rule(s) ${ignoredAllToolsAllowPatterns.map((pattern) => `"${pattern}"`).join(", ")}; an allow under "*" is never widened to Codewhale's tools.`);
|
|
52883
|
+
}
|
|
52884
|
+
/**
|
|
52885
|
+
* Convert the canonical block. The all-tools `*` category contributes its
|
|
52886
|
+
* restricting rules to shell commands (as every command-only adapter reads it,
|
|
52887
|
+
* see `collectShellCommandRules`), and its catch-all `*` restriction to every
|
|
52888
|
+
* file tool as well.
|
|
52889
|
+
*
|
|
52890
|
+
* A `deny` / `ask` Codewhale cannot express is not merely skipped: every
|
|
52891
|
+
* `allow` rule it overlaps for a tool it was meant to restrict is withheld too —
|
|
52892
|
+
* the generated ones and the preserved workspace-scoped grants alike — so the
|
|
52893
|
+
* commands or paths it named reach Codewhale's approval prompt instead of being
|
|
52894
|
+
* auto-approved by a broader `allow` (`{ "*": "allow", "secrets/**": "deny" }`
|
|
52895
|
+
* must not become a bare tool-wide allow). The `codewhale.rules` passthrough is
|
|
52896
|
+
* written as it stands.
|
|
52897
|
+
*/
|
|
52898
|
+
function canonicalToCodewhaleRules({ config, preservedRules, logger }) {
|
|
52899
|
+
const converted = [];
|
|
52900
|
+
const dropped = /* @__PURE__ */ new Map();
|
|
52901
|
+
convertShellCommandRules({
|
|
52902
|
+
config,
|
|
52903
|
+
converted,
|
|
52904
|
+
dropped,
|
|
52905
|
+
logger
|
|
52906
|
+
});
|
|
52907
|
+
for (const [category, rules] of Object.entries(config.permission)) {
|
|
52908
|
+
if (category === "bash" || category === "*") continue;
|
|
52909
|
+
if (CODEWHALE_PATH_CATEGORY_TOOLS[category] === void 0) {
|
|
52910
|
+
if (Object.keys(rules).length > 0) logger?.warn(`Codewhale permissions: skipping unsupported category "${category}"; Codewhale evaluates permission rules only for shell commands and file tools.`);
|
|
52911
|
+
continue;
|
|
52912
|
+
}
|
|
52913
|
+
for (const [pattern, action] of Object.entries(rules)) {
|
|
52914
|
+
const pathRules = convertPathRules({
|
|
52915
|
+
category,
|
|
52916
|
+
pattern,
|
|
52917
|
+
action,
|
|
52918
|
+
logger
|
|
52919
|
+
});
|
|
52920
|
+
converted.push(...pathRules);
|
|
52921
|
+
if (pathRules.length === 0 && action !== "allow") recordDroppedRestriction({
|
|
52922
|
+
dropped,
|
|
52923
|
+
tools: pathCategoryTools(category, action),
|
|
52924
|
+
glob: pathRestrictionGlob(pattern)
|
|
52925
|
+
});
|
|
52926
|
+
}
|
|
52927
|
+
}
|
|
52928
|
+
const kept = withholdShadowedAllowRules({
|
|
52929
|
+
rules: converted,
|
|
52930
|
+
dropped,
|
|
52931
|
+
logger
|
|
52932
|
+
});
|
|
52933
|
+
const keptPreserved = withholdShadowedAllowRules({
|
|
52934
|
+
rules: preservedRules,
|
|
52935
|
+
dropped,
|
|
52936
|
+
logger
|
|
52937
|
+
});
|
|
52938
|
+
return [
|
|
52939
|
+
...(config.codewhale?.rules ?? []).map(withExplicitAction),
|
|
52940
|
+
...sortRules(kept),
|
|
52941
|
+
...keptPreserved
|
|
52942
|
+
];
|
|
52943
|
+
}
|
|
52944
|
+
function parsePermissionsDocument({ fileContent, filePath }) {
|
|
52945
|
+
if (fileContent.trim() === "") return {};
|
|
52946
|
+
try {
|
|
52947
|
+
return smolToml.parse(fileContent);
|
|
52948
|
+
} catch (error) {
|
|
52949
|
+
throw new Error(`Failed to parse existing Codewhale permissions at ${filePath}: ${formatError(error)}`, { cause: error });
|
|
52950
|
+
}
|
|
52951
|
+
}
|
|
52952
|
+
/**
|
|
52953
|
+
* The workspace-scoped rules already in the file. Codewhale's approval card
|
|
52954
|
+
* appends one ("Always allow this exact rule in this repo") with the absolute
|
|
52955
|
+
* `workspace` it was granted in, and rulesync never derives a `workspace` from
|
|
52956
|
+
* the canonical block, so these are remembered grants rather than earlier
|
|
52957
|
+
* rulesync output and survive a regenerate. A record Codewhale would reject is
|
|
52958
|
+
* not carried over, since it would make Codewhale refuse the whole file.
|
|
52959
|
+
*/
|
|
52960
|
+
function collectWorkspaceScopedRules({ document, logger }) {
|
|
52961
|
+
const { valid, invalidCount } = partitionRuleRecords(document);
|
|
52962
|
+
if (invalidCount > 0) logger?.warn(`Codewhale permissions: dropping ${invalidCount} existing rule record(s) Codewhale would reject (an unknown key or a wrong type).`);
|
|
52963
|
+
return valid.filter((rule) => rule.workspace !== void 0);
|
|
52964
|
+
}
|
|
52965
|
+
/**
|
|
52966
|
+
* Codewhale reads `permissions.toml` next to the `config.toml` in use, which
|
|
52967
|
+
* `CODEWHALE_HOME` or `CODEWHALE_CONFIG_PATH` can move away from
|
|
52968
|
+
* `~/.codewhale/`. rulesync always writes `~/.codewhale/permissions.toml`, so
|
|
52969
|
+
* say so when either is set rather than let the rules go unread.
|
|
52970
|
+
*/
|
|
52971
|
+
function warnAboutRelocatedConfig(logger) {
|
|
52972
|
+
for (const name of ["CODEWHALE_HOME", "CODEWHALE_CONFIG_PATH"]) if (process.env[name]) logger?.warn(`Codewhale permissions: ${name} is set, but rulesync only syncs ~/.codewhale/permissions.toml; Codewhale reads permissions.toml next to the config.toml in use.`);
|
|
52973
|
+
}
|
|
52974
|
+
/** The records of a parsed file that Codewhale itself would accept. */
|
|
52975
|
+
function partitionRuleRecords(document) {
|
|
52976
|
+
const rawRules = Array.isArray(document.rules) ? document.rules : [];
|
|
52977
|
+
const valid = [];
|
|
52978
|
+
let invalidCount = 0;
|
|
52979
|
+
for (const raw of rawRules) {
|
|
52980
|
+
const parsed = CodewhalePermissionRuleSchema.safeParse(raw);
|
|
52981
|
+
if (parsed.success) valid.push(parsed.data);
|
|
52982
|
+
else invalidCount += 1;
|
|
52983
|
+
}
|
|
52984
|
+
return {
|
|
52985
|
+
valid,
|
|
52986
|
+
invalidCount
|
|
52987
|
+
};
|
|
52988
|
+
}
|
|
52989
|
+
/**
|
|
52990
|
+
* The canonical `bash` pattern a native shell rule round-trips through, or
|
|
52991
|
+
* `null` when the next generate would not write the same rule back from it.
|
|
52992
|
+
*/
|
|
52993
|
+
function shellRuleToPattern(rule) {
|
|
52994
|
+
if (rule.path !== void 0) return null;
|
|
52995
|
+
if (rule.command === void 0) return rule.command_exact ? null : "*";
|
|
52996
|
+
const command = rule.command.trim();
|
|
52997
|
+
if (command === "" || COMMAND_WILDCARD_CHARACTERS.test(command)) return null;
|
|
52998
|
+
if (rule.command_exact) return rule.action === "allow" ? command : null;
|
|
52999
|
+
return `${command} *`;
|
|
53000
|
+
}
|
|
53001
|
+
function pathRuleToPattern(rule) {
|
|
53002
|
+
if (rule.command !== void 0 || rule.command_exact) return null;
|
|
53003
|
+
if (rule.path === void 0) return "*";
|
|
53004
|
+
const path = rule.path.trim();
|
|
53005
|
+
return path === "" || PATH_GLOB_CHARACTERS.test(path) ? null : path;
|
|
53006
|
+
}
|
|
53007
|
+
function toImportCandidates(rule) {
|
|
53008
|
+
if (rule.workspace !== void 0) return [];
|
|
53009
|
+
if (rule.tool === CODEWHALE_SHELL_TOOL) {
|
|
53010
|
+
const pattern = shellRuleToPattern(rule);
|
|
53011
|
+
return pattern === null ? [] : [{
|
|
53012
|
+
category: SHELL_PERMISSION_CATEGORY,
|
|
53013
|
+
pattern,
|
|
53014
|
+
rule
|
|
53015
|
+
}];
|
|
53016
|
+
}
|
|
53017
|
+
const pattern = pathRuleToPattern(rule);
|
|
53018
|
+
if (pattern === null) return [];
|
|
53019
|
+
return (CODEWHALE_TOOL_TO_PATH_CATEGORIES[rule.tool] ?? []).map((category) => ({
|
|
53020
|
+
category,
|
|
53021
|
+
pattern,
|
|
53022
|
+
rule
|
|
53023
|
+
}));
|
|
53024
|
+
}
|
|
53025
|
+
/**
|
|
53026
|
+
* Split the native rules into the canonical block and the `codewhale.rules`
|
|
53027
|
+
* passthrough so that the next generate writes the same file back. A canonical
|
|
53028
|
+
* pattern is taken only when every tool its category expands to carries the
|
|
53029
|
+
* same rule (`edit` needs both `edit_file` and `apply_patch`, a `write`
|
|
53030
|
+
* restriction both `write_file` and `apply_patch`). A rule no canonical pattern
|
|
53031
|
+
* claims — including the weaker of two rules for one pattern — stays native.
|
|
53032
|
+
*/
|
|
53033
|
+
function codewhaleRulesToCanonical(rules) {
|
|
53034
|
+
const permission = {};
|
|
53035
|
+
const claimedIndexes = /* @__PURE__ */ new Set();
|
|
53036
|
+
const grouped = /* @__PURE__ */ new Map();
|
|
53037
|
+
for (const [index, rule] of rules.entries()) for (const candidate of toImportCandidates(rule)) {
|
|
53038
|
+
const key = JSON.stringify([
|
|
53039
|
+
candidate.category,
|
|
53040
|
+
candidate.pattern,
|
|
53041
|
+
rule.action
|
|
53042
|
+
]);
|
|
53043
|
+
grouped.set(key, [...grouped.get(key) ?? [], {
|
|
53044
|
+
...candidate,
|
|
53045
|
+
index
|
|
53046
|
+
}]);
|
|
53047
|
+
}
|
|
53048
|
+
const strongestFirst = [...grouped.values()].toSorted((left, right) => PERMISSION_ACTION_PRIORITY[right[0].rule.action] - PERMISSION_ACTION_PRIORITY[left[0].rule.action]);
|
|
53049
|
+
for (const group of strongestFirst) {
|
|
53050
|
+
const { category, pattern, rule } = group[0];
|
|
53051
|
+
const expectedTools = category === "bash" ? [CODEWHALE_SHELL_TOOL] : pathCategoryTools(category, rule.action);
|
|
53052
|
+
const presentTools = new Set(group.map((candidate) => candidate.rule.tool));
|
|
53053
|
+
const complete = expectedTools.every((tool) => presentTools.has(tool));
|
|
53054
|
+
const categoryRules = permission[category] ?? {};
|
|
53055
|
+
if (!complete || Object.hasOwn(categoryRules, pattern)) continue;
|
|
53056
|
+
permission[category] = {
|
|
53057
|
+
...categoryRules,
|
|
53058
|
+
[pattern]: rule.action
|
|
53059
|
+
};
|
|
53060
|
+
for (const candidate of group) if (expectedTools.includes(candidate.rule.tool)) claimedIndexes.add(candidate.index);
|
|
53061
|
+
}
|
|
53062
|
+
return {
|
|
53063
|
+
permission,
|
|
53064
|
+
nativeRules: rules.filter((_rule, index) => !claimedIndexes.has(index))
|
|
53065
|
+
};
|
|
53066
|
+
}
|
|
53067
|
+
/**
|
|
53068
|
+
* Codewhale permission rules in `~/.codewhale/permissions.toml`.
|
|
53069
|
+
*
|
|
53070
|
+
* The file holds a `rules` array of typed records (`tool`, optional `command`
|
|
53071
|
+
* prefix or exact `path`, optional `command_exact`, optional absolute
|
|
53072
|
+
* `workspace`, and `action`). It is the only permission-rule source Codewhale
|
|
53073
|
+
* reads — the project `.codewhale/config.toml` overlay cannot carry rules — so
|
|
53074
|
+
* the feature is global-only.
|
|
53075
|
+
*
|
|
53076
|
+
* rulesync owns the file's rules, except for workspace-scoped ones: those are
|
|
53077
|
+
* the grants Codewhale's approval card remembers per repository, and they are
|
|
53078
|
+
* kept across a regenerate. Codewhale also appends plain `exec_shell` ask rules
|
|
53079
|
+
* from the approval card (`S`), which cannot be told apart from rulesync's own
|
|
53080
|
+
* output and are replaced; run an import first to keep them.
|
|
53081
|
+
*
|
|
53082
|
+
* @see https://github.com/Hmbown/Codewhale/blob/main/docs/CONFIGURATION.md
|
|
53083
|
+
* @see https://github.com/Hmbown/Codewhale/blob/main/docs/AUTHORIZATION_ORDER.md
|
|
53084
|
+
*/
|
|
53085
|
+
var CodewhalePermissions = class CodewhalePermissions extends ToolPermissions {
|
|
53086
|
+
constructor(params) {
|
|
53087
|
+
super({
|
|
53088
|
+
...params,
|
|
53089
|
+
fileContent: params.fileContent ?? ""
|
|
53090
|
+
});
|
|
53091
|
+
}
|
|
53092
|
+
/**
|
|
53093
|
+
* Codewhale's approval card writes to the same file, so this feature never
|
|
53094
|
+
* deletes it.
|
|
53095
|
+
*/
|
|
53096
|
+
isDeletable() {
|
|
53097
|
+
return false;
|
|
53098
|
+
}
|
|
53099
|
+
shouldSkipCreationWhenPayloadEmpty() {
|
|
53100
|
+
return true;
|
|
53101
|
+
}
|
|
53102
|
+
static getSettablePaths(_options) {
|
|
53103
|
+
return {
|
|
53104
|
+
relativeDirPath: CODEWHALE_DIR,
|
|
53105
|
+
relativeFilePath: CODEWHALE_PERMISSIONS_FILE_NAME
|
|
53106
|
+
};
|
|
53107
|
+
}
|
|
53108
|
+
static async fromFile({ outputRoot = process.cwd(), validate = true, global = false, logger }) {
|
|
53109
|
+
if (!global) throw new Error(CODEWHALE_GLOBAL_ONLY_MESSAGE);
|
|
53110
|
+
warnAboutRelocatedConfig(logger);
|
|
53111
|
+
const paths = CodewhalePermissions.getSettablePaths();
|
|
53112
|
+
const filePath = join(outputRoot, paths.relativeDirPath, paths.relativeFilePath);
|
|
53113
|
+
const fileContent = await readFileContentOrNull(filePath) ?? "";
|
|
53114
|
+
const { invalidCount } = partitionRuleRecords(parsePermissionsDocument({
|
|
53115
|
+
fileContent,
|
|
53116
|
+
filePath
|
|
53117
|
+
}));
|
|
53118
|
+
if (invalidCount > 0) logger?.warn(`Codewhale permissions: skipping ${invalidCount} rule record(s) in ${filePath} that Codewhale would reject (an unknown key or a wrong type).`);
|
|
53119
|
+
return new CodewhalePermissions({
|
|
53120
|
+
outputRoot,
|
|
53121
|
+
relativeDirPath: paths.relativeDirPath,
|
|
53122
|
+
relativeFilePath: paths.relativeFilePath,
|
|
53123
|
+
fileContent,
|
|
53124
|
+
validate,
|
|
53125
|
+
global: true
|
|
53126
|
+
});
|
|
53127
|
+
}
|
|
53128
|
+
static async fromRulesyncPermissions({ outputRoot = process.cwd(), rulesyncPermissions, logger, global = false }) {
|
|
53129
|
+
if (!global) throw new Error(CODEWHALE_GLOBAL_ONLY_MESSAGE);
|
|
53130
|
+
warnAboutRelocatedConfig(logger);
|
|
53131
|
+
const paths = CodewhalePermissions.getSettablePaths();
|
|
53132
|
+
const filePath = join(outputRoot, paths.relativeDirPath, paths.relativeFilePath);
|
|
53133
|
+
const existing = parsePermissionsDocument({
|
|
53134
|
+
fileContent: await readFileContentOrNull(filePath) ?? "",
|
|
53135
|
+
filePath
|
|
53136
|
+
});
|
|
53137
|
+
const generated = canonicalToCodewhaleRules({
|
|
53138
|
+
config: rulesyncPermissions.getJson(),
|
|
53139
|
+
preservedRules: collectWorkspaceScopedRules({
|
|
53140
|
+
document: existing,
|
|
53141
|
+
logger
|
|
53142
|
+
}),
|
|
53143
|
+
logger
|
|
53144
|
+
});
|
|
53145
|
+
const seen = /* @__PURE__ */ new Set();
|
|
53146
|
+
const rules = [];
|
|
53147
|
+
for (const rule of generated) {
|
|
53148
|
+
const key = ruleKey(rule);
|
|
53149
|
+
if (seen.has(key)) continue;
|
|
53150
|
+
seen.add(key);
|
|
53151
|
+
rules.push(rule);
|
|
53152
|
+
}
|
|
53153
|
+
return new CodewhalePermissions({
|
|
53154
|
+
outputRoot,
|
|
53155
|
+
relativeDirPath: paths.relativeDirPath,
|
|
53156
|
+
relativeFilePath: paths.relativeFilePath,
|
|
53157
|
+
fileContent: rules.length > 0 ? smolToml.stringify({ rules }) : "",
|
|
53158
|
+
validate: true,
|
|
53159
|
+
global: true
|
|
53160
|
+
});
|
|
53161
|
+
}
|
|
53162
|
+
toRulesyncPermissions() {
|
|
53163
|
+
const { permission, nativeRules } = codewhaleRulesToCanonical(partitionRuleRecords(parsePermissionsDocument({
|
|
53164
|
+
fileContent: this.getFileContent(),
|
|
53165
|
+
filePath: join(this.getRelativeDirPath(), this.getRelativeFilePath())
|
|
53166
|
+
})).valid.map(withExplicitAction));
|
|
53167
|
+
return this.toRulesyncPermissionsDefault({ fileContent: JSON.stringify({
|
|
53168
|
+
permission,
|
|
53169
|
+
...nativeRules.length > 0 && { codewhale: { rules: nativeRules } }
|
|
53170
|
+
}, null, 2) });
|
|
53171
|
+
}
|
|
53172
|
+
static forDeletion({ outputRoot = process.cwd(), relativeDirPath, relativeFilePath }) {
|
|
53173
|
+
return new CodewhalePermissions({
|
|
53174
|
+
outputRoot,
|
|
53175
|
+
relativeDirPath,
|
|
53176
|
+
relativeFilePath,
|
|
53177
|
+
fileContent: "",
|
|
53178
|
+
validate: false,
|
|
53179
|
+
global: true
|
|
53180
|
+
});
|
|
53181
|
+
}
|
|
53182
|
+
};
|
|
53183
|
+
//#endregion
|
|
52295
53184
|
//#region src/features/permissions/codexcli-permissions.ts
|
|
52296
53185
|
const RULESYNC_PROFILE_NAME = "rulesync";
|
|
52297
53186
|
const CODEX_WORKSPACE_ROOTS_KEY = ":workspace_roots";
|
|
@@ -53167,7 +54056,7 @@ function mapBashActionToDecision(action) {
|
|
|
53167
54056
|
//#region src/features/permissions/commandcode-permissions.ts
|
|
53168
54057
|
/** Top-level key of `.commandcode/settings.json` that rulesync owns. */
|
|
53169
54058
|
const COMMANDCODE_PERMISSIONS_KEY = "permissions";
|
|
53170
|
-
const CATCH_ALL_PATTERN$
|
|
54059
|
+
const CATCH_ALL_PATTERN$10 = "*";
|
|
53171
54060
|
const MCP_CANONICAL_PREFIX$6 = "mcp__";
|
|
53172
54061
|
const COMMANDCODE_ALL_MCP_RULE = "mcp__*";
|
|
53173
54062
|
const CATEGORY_TO_COMMANDCODE_TOOL = {
|
|
@@ -53222,10 +54111,10 @@ const PADDED_SHELL_WILDCARD_REASON = "a pattern narrower than the bare 'Shell' (
|
|
|
53222
54111
|
* restrictions and reach the `Shell` entries through `honorAllToolsOnBash`.
|
|
53223
54112
|
*/
|
|
53224
54113
|
function buildCommandcodeRule(category, pattern) {
|
|
53225
|
-
const catchAll = pattern === CATCH_ALL_PATTERN$
|
|
54114
|
+
const catchAll = pattern === CATCH_ALL_PATTERN$10 || pattern === "";
|
|
53226
54115
|
if (category.startsWith(MCP_CANONICAL_PREFIX$6)) return catchAll ? category : `${category}(${pattern})`;
|
|
53227
54116
|
if (category === "mcp") return catchAll ? COMMANDCODE_ALL_MCP_RULE : `${MCP_CANONICAL_PREFIX$6}${pattern}`;
|
|
53228
|
-
if (category === "*") return catchAll ? CATCH_ALL_PATTERN$
|
|
54117
|
+
if (category === "*") return catchAll ? CATCH_ALL_PATTERN$10 : null;
|
|
53229
54118
|
const tool = Object.hasOwn(CATEGORY_TO_COMMANDCODE_TOOL, category) ? CATEGORY_TO_COMMANDCODE_TOOL[category] : void 0;
|
|
53230
54119
|
if (tool === void 0) return null;
|
|
53231
54120
|
return catchAll ? tool : `${tool}(${pattern})`;
|
|
@@ -53261,7 +54150,7 @@ function splitCommandcodeRule(rule) {
|
|
|
53261
54150
|
}
|
|
53262
54151
|
/** Whether a specifier means "no specifier" to Command Code: only `` and `*` do. */
|
|
53263
54152
|
function isCatchAllSpecifier(inner) {
|
|
53264
|
-
return inner === "" || inner === CATCH_ALL_PATTERN$
|
|
54153
|
+
return inner === "" || inner === CATCH_ALL_PATTERN$10;
|
|
53265
54154
|
}
|
|
53266
54155
|
/**
|
|
53267
54156
|
* Read an MCP-shaped rule the way Command Code does (`command-code` 1.54.0,
|
|
@@ -53295,34 +54184,34 @@ function parseMcpCommandcodeRule({ tool, inner }) {
|
|
|
53295
54184
|
if (server.length === 0) return null;
|
|
53296
54185
|
const everyServer = {
|
|
53297
54186
|
category: "mcp",
|
|
53298
|
-
pattern: CATCH_ALL_PATTERN$
|
|
54187
|
+
pattern: CATCH_ALL_PATTERN$10,
|
|
53299
54188
|
notInAllow: true
|
|
53300
54189
|
};
|
|
53301
54190
|
if (tool.startsWith(MCP_CANONICAL_PREFIX$6)) {
|
|
53302
|
-
if (server === CATCH_ALL_PATTERN$
|
|
53303
|
-
if (server.includes(CATCH_ALL_PATTERN$
|
|
54191
|
+
if (server === CATCH_ALL_PATTERN$10) return everyServer;
|
|
54192
|
+
if (server.includes(CATCH_ALL_PATTERN$10)) return null;
|
|
53304
54193
|
return {
|
|
53305
|
-
category: rest === "" || rest === CATCH_ALL_PATTERN$
|
|
53306
|
-
pattern: CATCH_ALL_PATTERN$
|
|
54194
|
+
category: rest === "" || rest === CATCH_ALL_PATTERN$10 ? `${MCP_CANONICAL_PREFIX$6}${server}` : `${MCP_CANONICAL_PREFIX$6}${remainder}`,
|
|
54195
|
+
pattern: CATCH_ALL_PATTERN$10,
|
|
53307
54196
|
notInAllow: false
|
|
53308
54197
|
};
|
|
53309
54198
|
}
|
|
53310
54199
|
const category = `${MCP_CANONICAL_PREFIX$6}${remainder}`;
|
|
53311
|
-
const namesTool = separator !== -1 && rest.length > 0 && !remainder.includes(CATCH_ALL_PATTERN$
|
|
54200
|
+
const namesTool = separator !== -1 && rest.length > 0 && !remainder.includes(CATCH_ALL_PATTERN$10);
|
|
53312
54201
|
if (!isCatchAllSpecifier(inner)) return namesTool ? {
|
|
53313
54202
|
category,
|
|
53314
54203
|
pattern: inner,
|
|
53315
54204
|
notInAllow: true
|
|
53316
54205
|
} : null;
|
|
53317
|
-
if (remainder === CATCH_ALL_PATTERN$
|
|
53318
|
-
if (rest === CATCH_ALL_PATTERN$
|
|
54206
|
+
if (remainder === CATCH_ALL_PATTERN$10) return everyServer;
|
|
54207
|
+
if (rest === CATCH_ALL_PATTERN$10 && !server.includes(CATCH_ALL_PATTERN$10)) return {
|
|
53319
54208
|
category: `${MCP_CANONICAL_PREFIX$6}${server}`,
|
|
53320
|
-
pattern: CATCH_ALL_PATTERN$
|
|
54209
|
+
pattern: CATCH_ALL_PATTERN$10,
|
|
53321
54210
|
notInAllow: true
|
|
53322
54211
|
};
|
|
53323
54212
|
return namesTool ? {
|
|
53324
54213
|
category,
|
|
53325
|
-
pattern: CATCH_ALL_PATTERN$
|
|
54214
|
+
pattern: CATCH_ALL_PATTERN$10,
|
|
53326
54215
|
notInAllow: false
|
|
53327
54216
|
} : null;
|
|
53328
54217
|
}
|
|
@@ -53346,14 +54235,14 @@ function friendlyToolRule({ category, inner }) {
|
|
|
53346
54235
|
if (category === "bash") {
|
|
53347
54236
|
if (isCatchAllSpecifier(inner)) return {
|
|
53348
54237
|
category,
|
|
53349
|
-
pattern: CATCH_ALL_PATTERN$
|
|
54238
|
+
pattern: CATCH_ALL_PATTERN$10,
|
|
53350
54239
|
notInAllow: false
|
|
53351
54240
|
};
|
|
53352
54241
|
const normalized = inner.trim().replace(/\s+/g, " ");
|
|
53353
54242
|
if (normalized === "" || normalized === ":*") return null;
|
|
53354
54243
|
return isCatchAllSpecifier(normalized) ? {
|
|
53355
54244
|
category,
|
|
53356
|
-
pattern: CATCH_ALL_PATTERN$
|
|
54245
|
+
pattern: CATCH_ALL_PATTERN$10,
|
|
53357
54246
|
notInAllow: true
|
|
53358
54247
|
} : {
|
|
53359
54248
|
category,
|
|
@@ -53363,7 +54252,7 @@ function friendlyToolRule({ category, inner }) {
|
|
|
53363
54252
|
}
|
|
53364
54253
|
if (isCatchAllSpecifier(inner)) return {
|
|
53365
54254
|
category,
|
|
53366
|
-
pattern: CATCH_ALL_PATTERN$
|
|
54255
|
+
pattern: CATCH_ALL_PATTERN$10,
|
|
53367
54256
|
notInAllow: false
|
|
53368
54257
|
};
|
|
53369
54258
|
return isCatchAllSpecifier(inner.trim()) ? null : {
|
|
@@ -53382,9 +54271,9 @@ function friendlyToolRule({ category, inner }) {
|
|
|
53382
54271
|
*/
|
|
53383
54272
|
function parseCommandcodeRule(rule) {
|
|
53384
54273
|
const { tool, inner } = splitCommandcodeRule(rule);
|
|
53385
|
-
if (tool === CATCH_ALL_PATTERN$
|
|
54274
|
+
if (tool === CATCH_ALL_PATTERN$10) return isCatchAllSpecifier(inner) ? {
|
|
53386
54275
|
category: "*",
|
|
53387
|
-
pattern: CATCH_ALL_PATTERN$
|
|
54276
|
+
pattern: CATCH_ALL_PATTERN$10,
|
|
53388
54277
|
notInAllow: true
|
|
53389
54278
|
} : null;
|
|
53390
54279
|
const lowered = tool.toLowerCase();
|
|
@@ -53454,7 +54343,7 @@ function preservedRules({ existingPermissions, key, managedCategories, written,
|
|
|
53454
54343
|
}).filter((rule) => {
|
|
53455
54344
|
const parsed = parseCommandcodeRule(rule);
|
|
53456
54345
|
if (parsed === null || !managedCategories.has(parsed.category)) return true;
|
|
53457
|
-
const replacement = written.get(`${parsed.category}(${parsed.pattern})`) ?? written.get(`${parsed.category}(${CATCH_ALL_PATTERN$
|
|
54346
|
+
const replacement = written.get(`${parsed.category}(${parsed.pattern})`) ?? written.get(`${parsed.category}(${CATCH_ALL_PATTERN$10})`);
|
|
53458
54347
|
if (key !== "allow" && isStricterAction({
|
|
53459
54348
|
action: key,
|
|
53460
54349
|
existing: replacement
|
|
@@ -53511,7 +54400,7 @@ function buildCommandcodeRuleLists({ config, existingPermissions, logger }) {
|
|
|
53511
54400
|
const ranked = /* @__PURE__ */ new Map();
|
|
53512
54401
|
const managedCategories = /* @__PURE__ */ new Set();
|
|
53513
54402
|
for (const [category, rules] of Object.entries(permission)) {
|
|
53514
|
-
const categoryRule = buildCommandcodeRule(category, CATCH_ALL_PATTERN$
|
|
54403
|
+
const categoryRule = buildCommandcodeRule(category, CATCH_ALL_PATTERN$10);
|
|
53515
54404
|
const managedCategory = categoryRule === null ? null : parseCommandcodeRule(categoryRule);
|
|
53516
54405
|
if (managedCategory !== null) managedCategories.add(managedCategory.category);
|
|
53517
54406
|
for (const [pattern, action] of Object.entries(rules)) {
|
|
@@ -53782,7 +54671,7 @@ function parseCommandcodeSettings({ fileContent, relativePath }) {
|
|
|
53782
54671
|
//#endregion
|
|
53783
54672
|
//#region src/features/permissions/continue-permissions.ts
|
|
53784
54673
|
const CONTINUE_GLOBAL_ONLY_MESSAGE = "Continue permissions are global-only; use --global to sync ~/.continue/permissions.yaml";
|
|
53785
|
-
const CATCH_ALL_PATTERN$
|
|
54674
|
+
const CATCH_ALL_PATTERN$9 = "*";
|
|
53786
54675
|
/**
|
|
53787
54676
|
* A run of `*` (`**`, `***`) is the catch-all spelled another way: Continue
|
|
53788
54677
|
* would match `Tool(**)` against every argument just as the bare tool name
|
|
@@ -53790,7 +54679,7 @@ const CATCH_ALL_PATTERN$8 = "*";
|
|
|
53790
54679
|
* the bare entry instead of being skipped as a pattern-specific rule.
|
|
53791
54680
|
*/
|
|
53792
54681
|
function normalizeCatchAllPattern$1(pattern) {
|
|
53793
|
-
return /^\*+$/.test(pattern) ? CATCH_ALL_PATTERN$
|
|
54682
|
+
return /^\*+$/.test(pattern) ? CATCH_ALL_PATTERN$9 : pattern;
|
|
53794
54683
|
}
|
|
53795
54684
|
const CANONICAL_TO_CONTINUE_TOOL_NAMES = {
|
|
53796
54685
|
bash: "Bash",
|
|
@@ -53827,7 +54716,7 @@ function toCanonicalToolName$6(continueName) {
|
|
|
53827
54716
|
* `Tool(pattern)`.
|
|
53828
54717
|
*/
|
|
53829
54718
|
function buildContinuePermissionEntry(toolName, pattern) {
|
|
53830
|
-
return pattern === CATCH_ALL_PATTERN$
|
|
54719
|
+
return pattern === CATCH_ALL_PATTERN$9 ? toolName : `${toolName}(${pattern})`;
|
|
53831
54720
|
}
|
|
53832
54721
|
/**
|
|
53833
54722
|
* Split a permissions.yaml entry into its tool name and argument pattern, or
|
|
@@ -53841,7 +54730,7 @@ function parseContinuePermissionEntry(entry) {
|
|
|
53841
54730
|
if (toolName === "") return;
|
|
53842
54731
|
return {
|
|
53843
54732
|
toolName,
|
|
53844
|
-
pattern: pattern === "" ? CATCH_ALL_PATTERN$
|
|
54733
|
+
pattern: pattern === "" ? CATCH_ALL_PATTERN$9 : pattern
|
|
53845
54734
|
};
|
|
53846
54735
|
}
|
|
53847
54736
|
/**
|
|
@@ -54015,7 +54904,7 @@ function convertRulesyncToContinuePermissions({ config, logger }) {
|
|
|
54015
54904
|
logger?.warn(`Continue permissions.yaml cannot hold a parenthesis inside a pattern, so the "${action}" rule for "${category}" (pattern ${quoteValueForWarning(pattern)}) was skipped.`);
|
|
54016
54905
|
continue;
|
|
54017
54906
|
}
|
|
54018
|
-
if (toolName === "*" && pattern !== CATCH_ALL_PATTERN$
|
|
54907
|
+
if (toolName === "*" && pattern !== CATCH_ALL_PATTERN$9) {
|
|
54019
54908
|
logger?.warn(`Continue permissions.yaml cannot scope a pattern to every tool, so the "${action}" rule for "*" (pattern ${quoteValueForWarning(pattern)}) was skipped.`);
|
|
54020
54909
|
continue;
|
|
54021
54910
|
}
|
|
@@ -54364,14 +55253,14 @@ var CopilotcliPermissions = class CopilotcliPermissions extends ToolPermissions
|
|
|
54364
55253
|
};
|
|
54365
55254
|
//#endregion
|
|
54366
55255
|
//#region src/features/permissions/crush-permissions.ts
|
|
54367
|
-
const CATCH_ALL_PATTERN$
|
|
55256
|
+
const CATCH_ALL_PATTERN$8 = "*";
|
|
54368
55257
|
/**
|
|
54369
55258
|
* A run of `*` (`**`, `***`) is the catch-all spelled another way: Crush has no
|
|
54370
55259
|
* argument patterns at all, so it is the only pattern a rule can carry and
|
|
54371
55260
|
* still be written.
|
|
54372
55261
|
*/
|
|
54373
55262
|
function normalizeCatchAllPattern(pattern) {
|
|
54374
|
-
return /^\*+$/.test(pattern) ? CATCH_ALL_PATTERN$
|
|
55263
|
+
return /^\*+$/.test(pattern) ? CATCH_ALL_PATTERN$8 : pattern;
|
|
54375
55264
|
}
|
|
54376
55265
|
const CANONICAL_TO_CRUSH_TOOL_NAMES = {
|
|
54377
55266
|
bash: "bash",
|
|
@@ -54570,7 +55459,7 @@ function resolveCategoryAction({ category, toolName, rules, logger }) {
|
|
|
54570
55459
|
for (const [rawPattern, action] of Object.entries(rules)) {
|
|
54571
55460
|
if (isPrototypePollutionKey(rawPattern)) continue;
|
|
54572
55461
|
const pattern = normalizeCatchAllPattern(rawPattern);
|
|
54573
|
-
if (pattern === CATCH_ALL_PATTERN$
|
|
55462
|
+
if (pattern === CATCH_ALL_PATTERN$8) {
|
|
54574
55463
|
catchAllAction = action;
|
|
54575
55464
|
continue;
|
|
54576
55465
|
}
|
|
@@ -54643,11 +55532,11 @@ function convertCrushListsToRulesync({ allowed, disabled }) {
|
|
|
54643
55532
|
};
|
|
54644
55533
|
for (const entry of disabled) {
|
|
54645
55534
|
if (entry === "" || isPrototypePollutionKey(entry)) continue;
|
|
54646
|
-
bucketFor(toCanonicalToolName$5(entry))[CATCH_ALL_PATTERN$
|
|
55535
|
+
bucketFor(toCanonicalToolName$5(entry))[CATCH_ALL_PATTERN$8] = "deny";
|
|
54647
55536
|
}
|
|
54648
55537
|
for (const entry of allowed) {
|
|
54649
55538
|
if (entry === "" || entry.includes(":") || isPrototypePollutionKey(entry)) continue;
|
|
54650
|
-
bucketFor(toCanonicalToolName$5(entry))[CATCH_ALL_PATTERN$
|
|
55539
|
+
bucketFor(toCanonicalToolName$5(entry))[CATCH_ALL_PATTERN$8] ??= "allow";
|
|
54651
55540
|
}
|
|
54652
55541
|
return { permission };
|
|
54653
55542
|
}
|
|
@@ -56512,7 +57401,7 @@ function convertFactorydroidToRulesyncPermissions({ allow, deny }) {
|
|
|
56512
57401
|
//#region src/features/permissions/goose-permissions.ts
|
|
56513
57402
|
const GOOSE_GLOBAL_ONLY_MESSAGE = "Goose permissions are global-only; use --global to sync ~/.config/goose/permission.yaml";
|
|
56514
57403
|
const GOOSE_USER_KEY = "user";
|
|
56515
|
-
const CATCH_ALL_PATTERN$
|
|
57404
|
+
const CATCH_ALL_PATTERN$7 = "*";
|
|
56516
57405
|
const RULESYNC_TO_GOOSE_TOOL_NAME = { bash: "shell" };
|
|
56517
57406
|
const GOOSE_TO_RULESYNC_TOOL_NAMES = {
|
|
56518
57407
|
shell: ["bash"],
|
|
@@ -56676,7 +57565,7 @@ function convertRulesyncToGoosePermissionConfig({ config, logger }) {
|
|
|
56676
57565
|
for (const [category, rules] of Object.entries(honorAllToolsOnBash(config.permission))) {
|
|
56677
57566
|
const toolName = RULESYNC_TO_GOOSE_TOOL_NAME[category] ?? category;
|
|
56678
57567
|
for (const [pattern, action] of Object.entries(rules)) {
|
|
56679
|
-
if (pattern !== CATCH_ALL_PATTERN$
|
|
57568
|
+
if (pattern !== CATCH_ALL_PATTERN$7) {
|
|
56680
57569
|
logger?.warn(`Goose permission.yaml lists whole tool names, so the per-pattern "${action}" rule for "${category}" (pattern "${pattern}") cannot be expressed and was skipped.`);
|
|
56681
57570
|
continue;
|
|
56682
57571
|
}
|
|
@@ -56707,7 +57596,7 @@ function convertGoosePermissionConfigToRulesync(userPermission) {
|
|
|
56707
57596
|
const action = GOOSE_LIST_TO_ACTION[key];
|
|
56708
57597
|
for (const toolName of toolNames) for (const category of GOOSE_TO_RULESYNC_TOOL_NAMES[toolName] ?? [toolName]) {
|
|
56709
57598
|
permission[category] ??= {};
|
|
56710
|
-
permission[category][CATCH_ALL_PATTERN$
|
|
57599
|
+
permission[category][CATCH_ALL_PATTERN$7] = action;
|
|
56711
57600
|
}
|
|
56712
57601
|
}
|
|
56713
57602
|
return { permission };
|
|
@@ -56718,7 +57607,7 @@ const GROKCLI_UI_KEY = "ui";
|
|
|
56718
57607
|
const GROKCLI_PERMISSION_MODE_KEY = "permission_mode";
|
|
56719
57608
|
const GROKCLI_PERMISSION_KEY = "permission";
|
|
56720
57609
|
const GROKCLI_AUTO_PERMISSION_MODE = "auto";
|
|
56721
|
-
const CATCH_ALL_PATTERN$
|
|
57610
|
+
const CATCH_ALL_PATTERN$6 = "*";
|
|
56722
57611
|
const MCP_CANONICAL_PREFIX$3 = "mcp__";
|
|
56723
57612
|
const CATEGORY_TO_GROK_TOOL = {
|
|
56724
57613
|
bash: "Bash",
|
|
@@ -56764,10 +57653,10 @@ function buildGrokEntry(category, pattern) {
|
|
|
56764
57653
|
const remainder = category.slice(5);
|
|
56765
57654
|
return remainder.length > 0 ? `${GROK_MCP_TOOL}(${remainder})` : GROK_MCP_TOOL;
|
|
56766
57655
|
}
|
|
56767
|
-
if (category === "mcp") return pattern === CATCH_ALL_PATTERN$
|
|
57656
|
+
if (category === "mcp") return pattern === CATCH_ALL_PATTERN$6 || pattern === "" ? GROK_MCP_TOOL : `${GROK_MCP_TOOL}(${pattern})`;
|
|
56768
57657
|
const tool = CATEGORY_TO_GROK_TOOL[category];
|
|
56769
57658
|
if (tool === void 0) return null;
|
|
56770
|
-
return pattern === CATCH_ALL_PATTERN$
|
|
57659
|
+
return pattern === CATCH_ALL_PATTERN$6 || pattern === "" ? tool : `${tool}(${pattern})`;
|
|
56771
57660
|
}
|
|
56772
57661
|
/**
|
|
56773
57662
|
* Parse a Grok Claude-style permission entry back into a canonical category +
|
|
@@ -56788,16 +57677,16 @@ function parseGrokEntry(entry) {
|
|
|
56788
57677
|
}
|
|
56789
57678
|
if (tool === GROK_MCP_TOOL) return inner.length > 0 ? {
|
|
56790
57679
|
category: `${MCP_CANONICAL_PREFIX$3}${inner}`,
|
|
56791
|
-
pattern: CATCH_ALL_PATTERN$
|
|
57680
|
+
pattern: CATCH_ALL_PATTERN$6
|
|
56792
57681
|
} : {
|
|
56793
57682
|
category: "mcp",
|
|
56794
|
-
pattern: CATCH_ALL_PATTERN$
|
|
57683
|
+
pattern: CATCH_ALL_PATTERN$6
|
|
56795
57684
|
};
|
|
56796
57685
|
const category = GROK_TOOL_TO_CATEGORY[tool];
|
|
56797
57686
|
if (category === void 0) return null;
|
|
56798
57687
|
return {
|
|
56799
57688
|
category,
|
|
56800
|
-
pattern: inner.length > 0 ? inner : CATCH_ALL_PATTERN$
|
|
57689
|
+
pattern: inner.length > 0 ? inner : CATCH_ALL_PATTERN$6
|
|
56801
57690
|
};
|
|
56802
57691
|
}
|
|
56803
57692
|
/**
|
|
@@ -56818,15 +57707,15 @@ function parseGrokRule(rule) {
|
|
|
56818
57707
|
if (typeof tool !== "string") return null;
|
|
56819
57708
|
if (pattern !== void 0 && typeof pattern !== "string") return null;
|
|
56820
57709
|
const trimmedPattern = pattern?.trim() ?? "";
|
|
56821
|
-
const resolvedPattern = trimmedPattern.length > 0 ? trimmedPattern : CATCH_ALL_PATTERN$
|
|
57710
|
+
const resolvedPattern = trimmedPattern.length > 0 ? trimmedPattern : CATCH_ALL_PATTERN$6;
|
|
56822
57711
|
const lowerTool = tool.trim().toLowerCase();
|
|
56823
|
-
if (GROK_MCP_TOOL_ALIASES.has(lowerTool)) return resolvedPattern === CATCH_ALL_PATTERN$
|
|
57712
|
+
if (GROK_MCP_TOOL_ALIASES.has(lowerTool)) return resolvedPattern === CATCH_ALL_PATTERN$6 ? {
|
|
56824
57713
|
category: "mcp",
|
|
56825
|
-
pattern: CATCH_ALL_PATTERN$
|
|
57714
|
+
pattern: CATCH_ALL_PATTERN$6,
|
|
56826
57715
|
action
|
|
56827
57716
|
} : {
|
|
56828
57717
|
category: `${MCP_CANONICAL_PREFIX$3}${resolvedPattern}`,
|
|
56829
|
-
pattern: CATCH_ALL_PATTERN$
|
|
57718
|
+
pattern: CATCH_ALL_PATTERN$6,
|
|
56830
57719
|
action
|
|
56831
57720
|
};
|
|
56832
57721
|
if (!Object.hasOwn(GROK_TOOL_TO_CATEGORY_LOWER, lowerTool)) return null;
|
|
@@ -56974,7 +57863,7 @@ var GrokcliPermissions = class GrokcliPermissions extends ToolPermissions {
|
|
|
56974
57863
|
throw new Error(`Failed to parse Grok config.toml content in ${join(this.getRelativeDirPath(), this.getRelativeFilePath())}: ${formatError(error)}`, { cause: error });
|
|
56975
57864
|
}
|
|
56976
57865
|
const fineGrained = parseGrokPermissionArrays(isRecord$1(parsed[GROKCLI_PERMISSION_KEY]) ? parsed[GROKCLI_PERMISSION_KEY] : {});
|
|
56977
|
-
const rulesyncConfig = fineGrained ? { permission: fineGrained } : { permission: { bash: { [CATCH_ALL_PATTERN$
|
|
57866
|
+
const rulesyncConfig = fineGrained ? { permission: fineGrained } : { permission: { bash: { [CATCH_ALL_PATTERN$6]: legacyModeAction(parsed) } } };
|
|
56978
57867
|
return this.toRulesyncPermissionsDefault({ fileContent: JSON.stringify(rulesyncConfig, null, 2) });
|
|
56979
57868
|
}
|
|
56980
57869
|
validate() {
|
|
@@ -57160,12 +58049,12 @@ function reconcileNativeDenies({ permission, category, patterns }) {
|
|
|
57160
58049
|
deleteRulesByAction(rules, "deny");
|
|
57161
58050
|
for (const pattern of patterns) rules[pattern] = "deny";
|
|
57162
58051
|
}
|
|
57163
|
-
function withoutKey(record, key) {
|
|
58052
|
+
function withoutKey$1(record, key) {
|
|
57164
58053
|
return Object.fromEntries(Object.entries(record).filter(([entryKey]) => entryKey !== key));
|
|
57165
58054
|
}
|
|
57166
58055
|
function buildHermesOverride(config, provenance) {
|
|
57167
58056
|
const base = isRecord$1(provenance.hermes) ? { ...provenance.hermes } : {};
|
|
57168
|
-
const approvalsOverride = withoutKey(isRecord$1(config.approvals) ? config.approvals : {}, "deny");
|
|
58057
|
+
const approvalsOverride = withoutKey$1(isRecord$1(config.approvals) ? config.approvals : {}, "deny");
|
|
57169
58058
|
if (Object.keys(approvalsOverride).length > 0) base.approvals = approvalsOverride;
|
|
57170
58059
|
else delete base.approvals;
|
|
57171
58060
|
const security = isRecord$1(config.security) ? { ...config.security } : {};
|
|
@@ -57750,6 +58639,35 @@ function narrowSandboxToProjectScope({ authored, logger }) {
|
|
|
57750
58639
|
if (dropped.length > 0) logger?.warn(`Kilo honors these 'sandbox' keys from the global config only, so they were dropped from the project config: ${dropped.toSorted().join(", ")}. A project config may only tighten the sandbox ('enabled', 'network'); generate with --global to author the rest.`);
|
|
57751
58640
|
return emitted;
|
|
57752
58641
|
}
|
|
58642
|
+
const KILO_MARKDOWN_SOURCE_KEY = "markdown_source";
|
|
58643
|
+
/**
|
|
58644
|
+
* Narrow an authored `markdown_source` rule to what a *project* `kilo.jsonc`
|
|
58645
|
+
* can mean. Kilo only grants it when the winning pattern came from the global
|
|
58646
|
+
* config with the action `allow` ("Project configuration cannot grant this
|
|
58647
|
+
* permission"). A project pattern that wins last-match-wins therefore never
|
|
58648
|
+
* grants: `deny` and `ask` both block (Kilo never prompts for it), so they
|
|
58649
|
+
* are kept — a project config may only tighten. A project `allow` would block
|
|
58650
|
+
* too, the opposite of what it says, so it is dropped with a warning pointing
|
|
58651
|
+
* at `--global`.
|
|
58652
|
+
* Returns `undefined` when nothing is left to write.
|
|
58653
|
+
* @see https://kilo.ai/docs/customize/workflows
|
|
58654
|
+
* @see https://github.com/Kilo-Org/kilocode/blob/main/packages/opencode/src/kilocode/config/external-markdown.ts
|
|
58655
|
+
*/
|
|
58656
|
+
function narrowMarkdownSourceToProjectScope({ authored, logger }) {
|
|
58657
|
+
let emitted;
|
|
58658
|
+
let dropped;
|
|
58659
|
+
if (typeof authored === "string") {
|
|
58660
|
+
emitted = authored === "allow" ? void 0 : authored;
|
|
58661
|
+
dropped = authored === "allow" ? ["*"] : [];
|
|
58662
|
+
} else {
|
|
58663
|
+
const entries = Object.entries(asKiloRecord(authored));
|
|
58664
|
+
const kept = entries.filter(([, action]) => action !== "allow");
|
|
58665
|
+
emitted = kept.length > 0 ? Object.fromEntries(kept) : void 0;
|
|
58666
|
+
dropped = entries.filter(([, action]) => action === "allow").map(([pattern]) => pattern);
|
|
58667
|
+
}
|
|
58668
|
+
if (dropped.length > 0) logger?.warn(`Kilo grants '${KILO_MARKDOWN_SOURCE_KEY}' from the global config only, so these 'allow' patterns were dropped from the project config: ${dropped.join(", ")}. A project config can only block it ('deny' or 'ask'); generate with --global to grant access.`);
|
|
58669
|
+
return emitted;
|
|
58670
|
+
}
|
|
57753
58671
|
var KiloPermissions = class KiloPermissions extends ToolPermissions {
|
|
57754
58672
|
json;
|
|
57755
58673
|
constructor(params) {
|
|
@@ -57836,6 +58754,13 @@ var KiloPermissions = class KiloPermissions extends ToolPermissions {
|
|
|
57836
58754
|
...honorAllToolsOnBash(rulesyncJson.permission),
|
|
57837
58755
|
...kiloOverride?.permission
|
|
57838
58756
|
};
|
|
58757
|
+
if (!global && Object.hasOwn(incomingPermission, KILO_MARKDOWN_SOURCE_KEY)) {
|
|
58758
|
+
const narrowed = narrowMarkdownSourceToProjectScope({
|
|
58759
|
+
authored: incomingPermission[KILO_MARKDOWN_SOURCE_KEY],
|
|
58760
|
+
logger
|
|
58761
|
+
});
|
|
58762
|
+
incomingPermission[KILO_MARKDOWN_SOURCE_KEY] = narrowed;
|
|
58763
|
+
}
|
|
57839
58764
|
const droppedDenyByKey = {};
|
|
57840
58765
|
for (const [key, value] of Object.entries(incomingPermission)) {
|
|
57841
58766
|
const previousDenyPatterns = collectKiloDenyPatterns(existingPermission[key]);
|
|
@@ -57851,6 +58776,7 @@ var KiloPermissions = class KiloPermissions extends ToolPermissions {
|
|
|
57851
58776
|
...existingPermission,
|
|
57852
58777
|
...incomingPermission
|
|
57853
58778
|
};
|
|
58779
|
+
if (mergedPermission[KILO_MARKDOWN_SOURCE_KEY] === void 0) delete mergedPermission[KILO_MARKDOWN_SOURCE_KEY];
|
|
57854
58780
|
const nextJson = {
|
|
57855
58781
|
...parsed,
|
|
57856
58782
|
permission: mergedPermission
|
|
@@ -58527,7 +59453,7 @@ function rulesFromArrays(settings, allowKey, denyKey) {
|
|
|
58527
59453
|
//#region src/features/permissions/lettacode-permissions.ts
|
|
58528
59454
|
/** Top-level key of `.letta/settings.json` that rulesync owns. */
|
|
58529
59455
|
const LETTACODE_PERMISSIONS_KEY = "permissions";
|
|
58530
|
-
const CATCH_ALL_PATTERN$
|
|
59456
|
+
const CATCH_ALL_PATTERN$5 = "*";
|
|
58531
59457
|
/** Letta Code's `Bash(prefix:*)` prefix-match suffix. */
|
|
58532
59458
|
const PREFIX_SUFFIX = ":*";
|
|
58533
59459
|
const CATEGORY_TO_LETTACODE_TOOL = {
|
|
@@ -58620,7 +59546,7 @@ function categoryOfRule$1(rule) {
|
|
|
58620
59546
|
* widening it would grant commands the source never allowed.
|
|
58621
59547
|
*/
|
|
58622
59548
|
function toLettacodeBashPayload({ pattern, action, logger }) {
|
|
58623
|
-
if (pattern === CATCH_ALL_PATTERN$
|
|
59549
|
+
if (pattern === CATCH_ALL_PATTERN$5) return "";
|
|
58624
59550
|
const firstStar = pattern.indexOf("*");
|
|
58625
59551
|
if (firstStar === -1) return pattern;
|
|
58626
59552
|
if (pattern.endsWith(PREFIX_SUFFIX) && firstStar === pattern.length - 1) return pattern;
|
|
@@ -58641,7 +59567,7 @@ function toLettacodeRule({ category, pattern, action, logger }) {
|
|
|
58641
59567
|
pattern,
|
|
58642
59568
|
action,
|
|
58643
59569
|
logger
|
|
58644
|
-
}) : pattern === CATCH_ALL_PATTERN$
|
|
59570
|
+
}) : pattern === CATCH_ALL_PATTERN$5 ? "" : pattern;
|
|
58645
59571
|
if (payload === null) return null;
|
|
58646
59572
|
return payload === "" ? tool : `${tool}(${payload})`;
|
|
58647
59573
|
}
|
|
@@ -58652,7 +59578,7 @@ function fromLettacodeRule(rule) {
|
|
|
58652
59578
|
const { payload } = splitLettacodeRule(rule);
|
|
58653
59579
|
if (payload === void 0) return {
|
|
58654
59580
|
category,
|
|
58655
|
-
pattern: CATCH_ALL_PATTERN$
|
|
59581
|
+
pattern: CATCH_ALL_PATTERN$5
|
|
58656
59582
|
};
|
|
58657
59583
|
const trimmed = payload.trim();
|
|
58658
59584
|
if (trimmed === "") return null;
|
|
@@ -58660,7 +59586,7 @@ function fromLettacodeRule(rule) {
|
|
|
58660
59586
|
const prefix = trimmed.slice(0, -2).trimEnd();
|
|
58661
59587
|
return {
|
|
58662
59588
|
category,
|
|
58663
|
-
pattern: prefix === "" ? CATCH_ALL_PATTERN$
|
|
59589
|
+
pattern: prefix === "" ? CATCH_ALL_PATTERN$5 : `${prefix} *`
|
|
58664
59590
|
};
|
|
58665
59591
|
}
|
|
58666
59592
|
return {
|
|
@@ -59116,6 +60042,377 @@ var MimocodePermissions = class extends OpencodePermissions {
|
|
|
59116
60042
|
static layout = MIMOCODE_LAYOUT;
|
|
59117
60043
|
};
|
|
59118
60044
|
//#endregion
|
|
60045
|
+
//#region src/features/permissions/omp-permissions.ts
|
|
60046
|
+
const CATCH_ALL_PATTERN$4 = "*";
|
|
60047
|
+
const ACTION_TO_OMP_APPROVAL = {
|
|
60048
|
+
allow: "allow",
|
|
60049
|
+
ask: "prompt",
|
|
60050
|
+
deny: "deny"
|
|
60051
|
+
};
|
|
60052
|
+
const OMP_APPROVAL_TO_ACTION = {
|
|
60053
|
+
allow: "allow",
|
|
60054
|
+
prompt: "ask",
|
|
60055
|
+
deny: "deny"
|
|
60056
|
+
};
|
|
60057
|
+
const CATEGORY_TO_OMP_TOOL = {
|
|
60058
|
+
websearch: "web_search",
|
|
60059
|
+
agent: "task"
|
|
60060
|
+
};
|
|
60061
|
+
const OMP_TOOL_TO_CATEGORY = Object.fromEntries(Object.entries(CATEGORY_TO_OMP_TOOL).map(([category, tool]) => [tool, category]));
|
|
60062
|
+
const UNSUPPORTED_CATEGORIES = /* @__PURE__ */ new Set([
|
|
60063
|
+
"webfetch",
|
|
60064
|
+
"notebookedit",
|
|
60065
|
+
"mcp"
|
|
60066
|
+
]);
|
|
60067
|
+
const MANAGED_OMP_TOOLS = /* @__PURE__ */ new Set([
|
|
60068
|
+
"bash",
|
|
60069
|
+
"read",
|
|
60070
|
+
"edit",
|
|
60071
|
+
"write",
|
|
60072
|
+
"grep",
|
|
60073
|
+
"glob",
|
|
60074
|
+
"web_search",
|
|
60075
|
+
"task"
|
|
60076
|
+
]);
|
|
60077
|
+
/**
|
|
60078
|
+
* Permissions adapter for oh-my-pi (`omp`).
|
|
60079
|
+
*
|
|
60080
|
+
* oh-my-pi reads approval policy from `config.yml`: `<cwd>/.omp/config.yml` in
|
|
60081
|
+
* project scope and `~/.omp/agent/config.yml` (default profile) globally.
|
|
60082
|
+
*
|
|
60083
|
+
* - `tools.approval.<tool>: allow | prompt | deny` is a per-tool policy honored
|
|
60084
|
+
* in every approval mode. A category's catch-all `*` rule maps here
|
|
60085
|
+
* (`ask` → `prompt`). oh-my-pi has no per-path matcher, so a non-bash category
|
|
60086
|
+
* with pattern-specific rules collapses to one action (deny > ask > allow, with
|
|
60087
|
+
* an implicit `ask` when there is no catch-all) and a warning.
|
|
60088
|
+
* - `bash.patterns: [{ match, approval }]` is an ordered, first-match-wins list
|
|
60089
|
+
* of command globs (only `*` is a wildcard). Every `bash` rule except a
|
|
60090
|
+
* catch-all `allow` is written there, ordered deny → prompt → allow so that
|
|
60091
|
+
* the first match agrees with rulesync's deny > ask > allow precedence. A
|
|
60092
|
+
* catch-all `allow` goes to `tools.approval.bash` instead, because by default
|
|
60093
|
+
* (without `bash.allowCompoundCommands`) an `allow` pattern never approves a
|
|
60094
|
+
* compound command line while the tool policy does — unless the category has
|
|
60095
|
+
* an `ask` rule, which makes `tools.approval.bash` `prompt` (see
|
|
60096
|
+
* `convertBashRules`) and the catch-all `allow` a trailing pattern. oh-my-pi
|
|
60097
|
+
* expands no `?` or `[...]`, so a deny or ask pattern widens them to `*`.
|
|
60098
|
+
* - The all-tools `*` category has no key of its own: its rules restrict bash
|
|
60099
|
+
* commands, and its deny / ask also restrict every other tool's policy (a
|
|
60100
|
+
* catch-all one at its own strictness, a pattern-specific one as `prompt`).
|
|
60101
|
+
*
|
|
60102
|
+
* `config.yml` carries every other oh-my-pi setting, so only `tools.approval`
|
|
60103
|
+
* (the keys rulesync manages) and `bash.patterns` are rewritten; sibling keys
|
|
60104
|
+
* such as `tools.approvalMode` and `bash.enabled` are kept, and the file is
|
|
60105
|
+
* never deleted.
|
|
60106
|
+
*
|
|
60107
|
+
* @see https://github.com/can1357/oh-my-pi/blob/main/docs/settings.md
|
|
60108
|
+
* @see https://github.com/can1357/oh-my-pi/blob/main/docs/approval-mode.md
|
|
60109
|
+
*/
|
|
60110
|
+
var OmpPermissions = class OmpPermissions extends ToolPermissions {
|
|
60111
|
+
constructor(params) {
|
|
60112
|
+
super({
|
|
60113
|
+
...params,
|
|
60114
|
+
fileContent: params.fileContent ?? ""
|
|
60115
|
+
});
|
|
60116
|
+
}
|
|
60117
|
+
/** `config.yml` holds unrelated user settings, so it must not be deleted. */
|
|
60118
|
+
isDeletable() {
|
|
60119
|
+
return false;
|
|
60120
|
+
}
|
|
60121
|
+
static getSettablePaths({ global = false } = {}) {
|
|
60122
|
+
return {
|
|
60123
|
+
relativeDirPath: global ? OMP_GLOBAL_DIR : OMP_DIR,
|
|
60124
|
+
relativeFilePath: OMP_CONFIG_FILE_NAME
|
|
60125
|
+
};
|
|
60126
|
+
}
|
|
60127
|
+
static async fromFile({ outputRoot = process.cwd(), validate = true, global = false }) {
|
|
60128
|
+
const paths = OmpPermissions.getSettablePaths({ global });
|
|
60129
|
+
const fileContent = await readFileContentOrNull(join(outputRoot, paths.relativeDirPath, paths.relativeFilePath)) ?? "";
|
|
60130
|
+
return new OmpPermissions({
|
|
60131
|
+
outputRoot,
|
|
60132
|
+
relativeDirPath: paths.relativeDirPath,
|
|
60133
|
+
relativeFilePath: paths.relativeFilePath,
|
|
60134
|
+
fileContent,
|
|
60135
|
+
validate,
|
|
60136
|
+
global
|
|
60137
|
+
});
|
|
60138
|
+
}
|
|
60139
|
+
static async fromRulesyncPermissions({ outputRoot = process.cwd(), rulesyncPermissions, logger, global = false }) {
|
|
60140
|
+
const paths = OmpPermissions.getSettablePaths({ global });
|
|
60141
|
+
const filePath = join(outputRoot, paths.relativeDirPath, paths.relativeFilePath);
|
|
60142
|
+
const existingContent = await readFileContentOrNull(filePath) ?? "";
|
|
60143
|
+
const existing = parseSharedConfig({
|
|
60144
|
+
format: "yaml",
|
|
60145
|
+
fileContent: existingContent,
|
|
60146
|
+
filePath
|
|
60147
|
+
});
|
|
60148
|
+
const { approval, patterns, floor } = convertRulesyncToOmp({
|
|
60149
|
+
config: rulesyncPermissions.getJson(),
|
|
60150
|
+
logger
|
|
60151
|
+
});
|
|
60152
|
+
const existingTools = isRecord$1(existing.tools) ? existing.tools : {};
|
|
60153
|
+
const nextApproval = {
|
|
60154
|
+
...isRecord$1(existingTools.approval) ? Object.fromEntries(Object.entries(existingTools.approval).filter(([tool]) => !MANAGED_OMP_TOOLS.has(tool) && !Object.hasOwn(approval, tool)).map(([tool, value]) => [tool, raisedApproval(value, floor)])) : {},
|
|
60155
|
+
...approval
|
|
60156
|
+
};
|
|
60157
|
+
const toolsSiblings = withoutKey(existingTools, "approval");
|
|
60158
|
+
const nextTools = Object.keys(nextApproval).length > 0 ? {
|
|
60159
|
+
...toolsSiblings,
|
|
60160
|
+
approval: nextApproval
|
|
60161
|
+
} : toolsSiblings;
|
|
60162
|
+
const bashSiblings = withoutKey(isRecord$1(existing.bash) ? existing.bash : {}, "patterns");
|
|
60163
|
+
const nextBash = patterns.length > 0 ? {
|
|
60164
|
+
...bashSiblings,
|
|
60165
|
+
patterns
|
|
60166
|
+
} : bashSiblings;
|
|
60167
|
+
return new OmpPermissions({
|
|
60168
|
+
outputRoot,
|
|
60169
|
+
relativeDirPath: paths.relativeDirPath,
|
|
60170
|
+
relativeFilePath: paths.relativeFilePath,
|
|
60171
|
+
fileContent: applySharedConfigPatch({
|
|
60172
|
+
fileKey: sharedConfigFileKey(paths),
|
|
60173
|
+
feature: "permissions",
|
|
60174
|
+
existingContent,
|
|
60175
|
+
patch: {
|
|
60176
|
+
tools: Object.keys(nextTools).length > 0 ? nextTools : void 0,
|
|
60177
|
+
bash: Object.keys(nextBash).length > 0 ? nextBash : void 0
|
|
60178
|
+
},
|
|
60179
|
+
filePath,
|
|
60180
|
+
logger
|
|
60181
|
+
}),
|
|
60182
|
+
validate: true,
|
|
60183
|
+
global
|
|
60184
|
+
});
|
|
60185
|
+
}
|
|
60186
|
+
toRulesyncPermissions() {
|
|
60187
|
+
let config;
|
|
60188
|
+
try {
|
|
60189
|
+
config = parseSharedConfig({
|
|
60190
|
+
format: "yaml",
|
|
60191
|
+
fileContent: this.getFileContent()
|
|
60192
|
+
});
|
|
60193
|
+
} catch (error) {
|
|
60194
|
+
throw new Error(`Failed to parse oh-my-pi config in ${join(this.getRelativeDirPath(), this.getRelativeFilePath())}: ${formatError(error)}`, { cause: error });
|
|
60195
|
+
}
|
|
60196
|
+
return this.toRulesyncPermissionsDefault({ fileContent: JSON.stringify(convertOmpToRulesync(config), null, 2) });
|
|
60197
|
+
}
|
|
60198
|
+
validate() {
|
|
60199
|
+
return {
|
|
60200
|
+
success: true,
|
|
60201
|
+
error: null
|
|
60202
|
+
};
|
|
60203
|
+
}
|
|
60204
|
+
static forDeletion({ outputRoot = process.cwd(), relativeDirPath, relativeFilePath, global = false }) {
|
|
60205
|
+
return new OmpPermissions({
|
|
60206
|
+
outputRoot,
|
|
60207
|
+
relativeDirPath,
|
|
60208
|
+
relativeFilePath,
|
|
60209
|
+
fileContent: "",
|
|
60210
|
+
validate: false,
|
|
60211
|
+
global
|
|
60212
|
+
});
|
|
60213
|
+
}
|
|
60214
|
+
};
|
|
60215
|
+
function withoutKey(record, key) {
|
|
60216
|
+
return Object.fromEntries(Object.entries(record).filter(([name]) => name !== key));
|
|
60217
|
+
}
|
|
60218
|
+
function renamed(map, name) {
|
|
60219
|
+
return Object.hasOwn(map, name) ? map[name] ?? name : name;
|
|
60220
|
+
}
|
|
60221
|
+
function stricter(a, b) {
|
|
60222
|
+
return a !== void 0 && PERMISSION_ACTION_PRIORITY[a] >= PERMISSION_ACTION_PRIORITY[b] ? a : b;
|
|
60223
|
+
}
|
|
60224
|
+
/**
|
|
60225
|
+
* The floor the all-tools `*` category puts under every tool's single policy:
|
|
60226
|
+
* a catch-all deny / ask at its own strictness, and a pattern-specific one as
|
|
60227
|
+
* `ask`, since some input of each tool may match it and a per-tool policy
|
|
60228
|
+
* cannot single that input out. `undefined` when `*` restricts nothing.
|
|
60229
|
+
*/
|
|
60230
|
+
function allToolsFloor(rules) {
|
|
60231
|
+
let floor;
|
|
60232
|
+
let patternSpecific = false;
|
|
60233
|
+
for (const [pattern, action] of Object.entries(rules ?? {})) {
|
|
60234
|
+
if (action === "allow" || isPrototypePollutionKey(pattern)) continue;
|
|
60235
|
+
if (pattern === CATCH_ALL_PATTERN$4) floor = stricter(floor, action);
|
|
60236
|
+
else {
|
|
60237
|
+
patternSpecific = true;
|
|
60238
|
+
floor = stricter(floor, "ask");
|
|
60239
|
+
}
|
|
60240
|
+
}
|
|
60241
|
+
return {
|
|
60242
|
+
floor,
|
|
60243
|
+
patternSpecific
|
|
60244
|
+
};
|
|
60245
|
+
}
|
|
60246
|
+
/**
|
|
60247
|
+
* oh-my-pi expands only `*`, so a `?` or `[...]` in a deny / ask pattern widens
|
|
60248
|
+
* to `*` (a literal match would restrict almost nothing). An allow pattern is
|
|
60249
|
+
* left as is: matched literally it approves less, never more.
|
|
60250
|
+
*/
|
|
60251
|
+
function widenRestrictionPattern(pattern) {
|
|
60252
|
+
return pattern.replace(/\[!?\]?[^\]]*\]|\[|\?/gu, "*").replace(/\*{2,}/gu, "*");
|
|
60253
|
+
}
|
|
60254
|
+
/**
|
|
60255
|
+
* A `bash` category as `bash.patterns` entries (unsorted), plus the
|
|
60256
|
+
* `tools.approval.bash` policy it needs.
|
|
60257
|
+
*
|
|
60258
|
+
* oh-my-pi replaces a `prompt` (or `allow`) pattern match on a critical command
|
|
60259
|
+
* such as `rm -rf /` with an override that carries no policy, which yolo mode
|
|
60260
|
+
* then resolves by `tools.approval.bash` alone. So any `ask` rule sets the tool
|
|
60261
|
+
* policy to `prompt`, and a catch-all `allow` then becomes a trailing
|
|
60262
|
+
* `match: "*"` allow pattern (patterns beat the tool policy) instead of an
|
|
60263
|
+
* `allow` policy that would wave such a command through.
|
|
60264
|
+
*/
|
|
60265
|
+
function convertBashRules$1({ rules, logger }) {
|
|
60266
|
+
const patterns = [];
|
|
60267
|
+
const entries = Object.entries(rules).filter(([pattern]) => !isPrototypePollutionKey(pattern));
|
|
60268
|
+
const hasAsk = entries.some(([, action]) => action === "ask");
|
|
60269
|
+
let policy = hasAsk ? "ask" : void 0;
|
|
60270
|
+
if (hasAsk && rules[CATCH_ALL_PATTERN$4] !== "ask") logger?.warn("oh-my-pi lets a critical command (such as `rm -rf /`) skip a bash prompt pattern unless the bash tool policy prompts, so `tools.approval.bash` was set to \"prompt\": a command no bash allow pattern matches now prompts in every approval mode.");
|
|
60271
|
+
for (const [pattern, action] of entries) {
|
|
60272
|
+
if (pattern === CATCH_ALL_PATTERN$4 && action === "allow") {
|
|
60273
|
+
if (hasAsk) patterns.push({
|
|
60274
|
+
match: CATCH_ALL_PATTERN$4,
|
|
60275
|
+
approval: "allow"
|
|
60276
|
+
});
|
|
60277
|
+
else policy = "allow";
|
|
60278
|
+
continue;
|
|
60279
|
+
}
|
|
60280
|
+
let match = pattern;
|
|
60281
|
+
if (/[?[]/u.test(pattern)) {
|
|
60282
|
+
if (action === "allow") logger?.warn(`oh-my-pi matches only "*" as a wildcard, so "?" and "[" in the bash allow pattern "${pattern}" are matched literally.`);
|
|
60283
|
+
else {
|
|
60284
|
+
match = widenRestrictionPattern(pattern);
|
|
60285
|
+
logger?.warn(`oh-my-pi matches only "*" as a wildcard, so the bash ${action} pattern "${pattern}" was widened to "${match}".`);
|
|
60286
|
+
}
|
|
60287
|
+
}
|
|
60288
|
+
patterns.push({
|
|
60289
|
+
match,
|
|
60290
|
+
approval: ACTION_TO_OMP_APPROVAL[action]
|
|
60291
|
+
});
|
|
60292
|
+
}
|
|
60293
|
+
return {
|
|
60294
|
+
patterns,
|
|
60295
|
+
policy
|
|
60296
|
+
};
|
|
60297
|
+
}
|
|
60298
|
+
/** Raise every non-bash tool policy to the all-tools floor (bash is handled by patterns). */
|
|
60299
|
+
function applyAllToolsFloor({ actions, allTools, logger }) {
|
|
60300
|
+
const { floor } = allTools;
|
|
60301
|
+
if (floor === void 0) return;
|
|
60302
|
+
if (allTools.patternSpecific) logger?.warn(`oh-my-pi sets one approval policy per tool, so the pattern-specific "*" category restrictions were written as "${ACTION_TO_OMP_APPROVAL[floor]}" for every non-bash tool.`);
|
|
60303
|
+
for (const tool of /* @__PURE__ */ new Set([...MANAGED_OMP_TOOLS, ...Object.keys(actions)])) if (tool !== "bash") actions[tool] = stricter(actions[tool], floor);
|
|
60304
|
+
}
|
|
60305
|
+
function convertRulesyncToOmp({ config, logger }) {
|
|
60306
|
+
const actions = {};
|
|
60307
|
+
const patterns = [];
|
|
60308
|
+
const permission = config.permission;
|
|
60309
|
+
const allTools = allToolsFloor(permission["*"]);
|
|
60310
|
+
const categories = permission["*"] === void 0 ? permission : {
|
|
60311
|
+
...permission,
|
|
60312
|
+
bash: bashRulesHonoringAllTools(permission)
|
|
60313
|
+
};
|
|
60314
|
+
for (const [category, rules] of Object.entries(categories)) {
|
|
60315
|
+
if (isPrototypePollutionKey(category) || Object.keys(rules).length === 0) continue;
|
|
60316
|
+
if (category === "*") {
|
|
60317
|
+
if (Object.values(rules).includes("allow")) logger?.warn("oh-my-pi has no all-tools approval key, so the \"*\" category allow rules were skipped.");
|
|
60318
|
+
continue;
|
|
60319
|
+
}
|
|
60320
|
+
if (UNSUPPORTED_CATEGORIES.has(category)) {
|
|
60321
|
+
logger?.warn(`oh-my-pi has no "${category}" tool to set an approval policy on, so its rules were skipped` + (category === "webfetch" ? " (oh-my-pi fetches web pages through `read`)." : "."));
|
|
60322
|
+
continue;
|
|
60323
|
+
}
|
|
60324
|
+
if (category === "bash") {
|
|
60325
|
+
const bash = convertBashRules$1({
|
|
60326
|
+
rules,
|
|
60327
|
+
logger
|
|
60328
|
+
});
|
|
60329
|
+
patterns.push(...bash.patterns);
|
|
60330
|
+
if (bash.policy !== void 0) actions.bash = bash.policy;
|
|
60331
|
+
continue;
|
|
60332
|
+
}
|
|
60333
|
+
const action = collapseRulesToSingleAction({ rules });
|
|
60334
|
+
if (action === void 0) continue;
|
|
60335
|
+
if (hasPatternSpecificRules(rules)) logger?.warn(`oh-my-pi sets one approval policy per tool, so the pattern-specific "${category}" rules were collapsed to "${ACTION_TO_OMP_APPROVAL[action]}" (deny > ask > allow).`);
|
|
60336
|
+
const tool = renamed(CATEGORY_TO_OMP_TOOL, category);
|
|
60337
|
+
if (/^mcp__.+__/u.test(tool)) logger?.warn(`oh-my-pi registers MCP tools as "mcp__<server>_<tool>", so "${tool}" may never match; write oh-my-pi's registered name under a tool-scoped \`omp.permission\` block.`);
|
|
60338
|
+
actions[tool] = stricter(actions[tool], action);
|
|
60339
|
+
}
|
|
60340
|
+
applyAllToolsFloor({
|
|
60341
|
+
actions,
|
|
60342
|
+
allTools,
|
|
60343
|
+
logger
|
|
60344
|
+
});
|
|
60345
|
+
const order = (pattern) => -PERMISSION_ACTION_PRIORITY[OMP_APPROVAL_TO_ACTION[pattern.approval]];
|
|
60346
|
+
patterns.sort((a, b) => order(a) - order(b));
|
|
60347
|
+
return {
|
|
60348
|
+
approval: Object.fromEntries(Object.entries(actions).map(([tool, action]) => [tool, ACTION_TO_OMP_APPROVAL[action]])),
|
|
60349
|
+
patterns,
|
|
60350
|
+
floor: allTools.floor
|
|
60351
|
+
};
|
|
60352
|
+
}
|
|
60353
|
+
/** An existing `tools.approval` value raised to the all-tools floor, if any. */
|
|
60354
|
+
function raisedApproval(value, floor) {
|
|
60355
|
+
if (floor === void 0) return value;
|
|
60356
|
+
const approval = toOmpApproval(value);
|
|
60357
|
+
const current = approval === void 0 ? void 0 : OMP_APPROVAL_TO_ACTION[approval];
|
|
60358
|
+
return ACTION_TO_OMP_APPROVAL[stricter(current, floor)];
|
|
60359
|
+
}
|
|
60360
|
+
function toOmpApproval(value) {
|
|
60361
|
+
if (typeof value !== "string") return;
|
|
60362
|
+
const normalized = value.trim().toLowerCase();
|
|
60363
|
+
return Object.hasOwn(OMP_APPROVAL_TO_ACTION, normalized) ? normalized : void 0;
|
|
60364
|
+
}
|
|
60365
|
+
/**
|
|
60366
|
+
* Fold `tools.approval.bash` (`undefined` when unset) into the imported `bash`
|
|
60367
|
+
* rules. A user `deny` is absolute. Otherwise a `*` pattern beats the tool
|
|
60368
|
+
* policy, which then reaches only compound and critical command lines — so a
|
|
60369
|
+
* `*` allow pattern imports as `allow` only behind an explicit `allow` policy
|
|
60370
|
+
* or in the shape rulesync writes (a `prompt` policy next to `ask` patterns).
|
|
60371
|
+
* Any other `*` allow pattern still prompts for compound lines and imports as
|
|
60372
|
+
* `ask`, so a regenerate cannot widen it into an `allow` policy. A `prompt`
|
|
60373
|
+
* policy with no `*` pattern but an `ask` pattern is what rulesync writes for
|
|
60374
|
+
* ask rules (see `convertBashRules`), so it adds no catch-all.
|
|
60375
|
+
*/
|
|
60376
|
+
function importBashPolicy(bashRules, action) {
|
|
60377
|
+
const catchAll = bashRules[CATCH_ALL_PATTERN$4];
|
|
60378
|
+
const hasAskPattern = Object.values(bashRules).includes("ask");
|
|
60379
|
+
if (action === "deny") bashRules[CATCH_ALL_PATTERN$4] = "deny";
|
|
60380
|
+
else if (catchAll === void 0) {
|
|
60381
|
+
if (action !== void 0 && !(action === "ask" && hasAskPattern)) bashRules[CATCH_ALL_PATTERN$4] = action;
|
|
60382
|
+
} else if (catchAll === "allow" && action !== "allow" && !(action === "ask" && hasAskPattern)) bashRules[CATCH_ALL_PATTERN$4] = "ask";
|
|
60383
|
+
}
|
|
60384
|
+
/** `bash.patterns` as canonical `bash` rules, in first-match order. */
|
|
60385
|
+
function importBashPatterns(value) {
|
|
60386
|
+
const bashRules = {};
|
|
60387
|
+
if (!Array.isArray(value)) return bashRules;
|
|
60388
|
+
for (const entry of value) {
|
|
60389
|
+
if (!isRecord$1(entry) || typeof entry.match !== "string") continue;
|
|
60390
|
+
const match = entry.match.trim().replace(/\s+/gu, " ");
|
|
60391
|
+
const approval = toOmpApproval(entry.approval);
|
|
60392
|
+
if (match === "" || approval === void 0 || Object.hasOwn(bashRules, match)) continue;
|
|
60393
|
+
if (isPrototypePollutionKey(match)) continue;
|
|
60394
|
+
bashRules[match] = OMP_APPROVAL_TO_ACTION[approval];
|
|
60395
|
+
}
|
|
60396
|
+
return bashRules;
|
|
60397
|
+
}
|
|
60398
|
+
function convertOmpToRulesync(config) {
|
|
60399
|
+
const permission = {};
|
|
60400
|
+
const tools = isRecord$1(config.tools) ? config.tools : {};
|
|
60401
|
+
const bashRules = importBashPatterns((isRecord$1(config.bash) ? config.bash : {}).patterns);
|
|
60402
|
+
const bashApproval = toOmpApproval((isRecord$1(tools.approval) ? tools.approval : {}).bash);
|
|
60403
|
+
importBashPolicy(bashRules, bashApproval === void 0 ? void 0 : OMP_APPROVAL_TO_ACTION[bashApproval]);
|
|
60404
|
+
if (isRecord$1(tools.approval)) for (const [tool, value] of Object.entries(tools.approval)) {
|
|
60405
|
+
const approval = toOmpApproval(value);
|
|
60406
|
+
if (approval === void 0 || tool.trim() === "" || isPrototypePollutionKey(tool)) continue;
|
|
60407
|
+
const action = OMP_APPROVAL_TO_ACTION[approval];
|
|
60408
|
+
if (tool === "bash") continue;
|
|
60409
|
+
const category = renamed(OMP_TOOL_TO_CATEGORY, tool);
|
|
60410
|
+
permission[category] = { [CATCH_ALL_PATTERN$4]: stricter(permission[category]?.[CATCH_ALL_PATTERN$4], action) };
|
|
60411
|
+
}
|
|
60412
|
+
if (Object.keys(bashRules).length > 0) permission.bash = bashRules;
|
|
60413
|
+
return { permission };
|
|
60414
|
+
}
|
|
60415
|
+
//#endregion
|
|
59119
60416
|
//#region src/features/permissions/pi-permissions.ts
|
|
59120
60417
|
/** The one `settings.json` key this feature owns. */
|
|
59121
60418
|
const DEFAULT_TOOLS_KEY = "defaultTools";
|
|
@@ -60143,7 +61440,9 @@ const QWEN_OVERRIDE_TOOLS_KEYS = [
|
|
|
60143
61440
|
"eager",
|
|
60144
61441
|
"listDirectory",
|
|
60145
61442
|
"todoWrite",
|
|
60146
|
-
"workflowsEnabled"
|
|
61443
|
+
"workflowsEnabled",
|
|
61444
|
+
"workflowNameOnly",
|
|
61445
|
+
"executionSandbox"
|
|
60147
61446
|
];
|
|
60148
61447
|
const QWEN_OVERRIDE_SECURITY_KEYS = [
|
|
60149
61448
|
"folderTrust",
|
|
@@ -60212,6 +61511,16 @@ const QWEN_OVERRIDE_GROUPS = [{
|
|
|
60212
61511
|
overrideKeys: QWEN_OVERRIDE_TOOLS_KEYS,
|
|
60213
61512
|
scopedKeys: {
|
|
60214
61513
|
workflowsEnabled: { rule: "workspace-stripped" },
|
|
61514
|
+
executionSandbox: {
|
|
61515
|
+
rule: "workspace-stripped",
|
|
61516
|
+
grants: () => true,
|
|
61517
|
+
globalNote: "Qwen Code ignores this key in workspace settings so a repository cannot choose its own sandbox; in the global scope it sets the filesystem and network policy every tool command runs under for every project on this machine (a System value still replaces it, and Qwen Code needs a restart to pick it up)."
|
|
61518
|
+
},
|
|
61519
|
+
workflowNameOnly: {
|
|
61520
|
+
rule: "global-machine-wide",
|
|
61521
|
+
projectNote: ({ qualifiedKey, quotedValue, filePath }) => `${qualifiedKey} = ${quotedValue} was written to the project-scoped ${filePath}. Qwen Code lets a workspace only turn this key on: \`true\` restricts the model to named workflows in this repository, while any other value cannot lift a restriction set in user or system settings.`,
|
|
61522
|
+
globalNote: "Qwen Code honors this key in the global scope for every project on this machine, and a repository can only add the restriction, never lift it, so this decides whether the model may run inline or path-based workflow scripts anywhere a workspace does not turn the restriction on itself."
|
|
61523
|
+
},
|
|
60215
61524
|
approvalMode: {
|
|
60216
61525
|
rule: "global-machine-wide",
|
|
60217
61526
|
projectNote: autonomyProjectNote
|
|
@@ -66030,6 +67339,14 @@ const toolPermissionsFactories = /* @__PURE__ */ new Map([
|
|
|
66030
67339
|
supportsImport: true
|
|
66031
67340
|
}
|
|
66032
67341
|
}],
|
|
67342
|
+
["codewhale", {
|
|
67343
|
+
class: CodewhalePermissions,
|
|
67344
|
+
meta: {
|
|
67345
|
+
supportsProject: false,
|
|
67346
|
+
supportsGlobal: true,
|
|
67347
|
+
supportsImport: true
|
|
67348
|
+
}
|
|
67349
|
+
}],
|
|
66033
67350
|
["codexcli", {
|
|
66034
67351
|
class: CodexcliPermissions,
|
|
66035
67352
|
meta: {
|
|
@@ -66198,6 +67515,14 @@ const toolPermissionsFactories = /* @__PURE__ */ new Map([
|
|
|
66198
67515
|
supportsImport: true
|
|
66199
67516
|
}
|
|
66200
67517
|
}],
|
|
67518
|
+
["omp", {
|
|
67519
|
+
class: OmpPermissions,
|
|
67520
|
+
meta: {
|
|
67521
|
+
supportsProject: true,
|
|
67522
|
+
supportsGlobal: true,
|
|
67523
|
+
supportsImport: true
|
|
67524
|
+
}
|
|
67525
|
+
}],
|
|
66201
67526
|
["opencode", {
|
|
66202
67527
|
class: OpencodePermissions,
|
|
66203
67528
|
meta: {
|
|
@@ -67077,6 +68402,10 @@ var DirFeatureProcessor = class extends RulesyncSourceConsumer {
|
|
|
67077
68402
|
continue;
|
|
67078
68403
|
}
|
|
67079
68404
|
try {
|
|
68405
|
+
await assertWritablePathInsideRoot({
|
|
68406
|
+
rootPath: this.outputRoot,
|
|
68407
|
+
targetPath: root
|
|
68408
|
+
});
|
|
67080
68409
|
await assertWritablePathInsideRoot({
|
|
67081
68410
|
rootPath: this.outputRoot,
|
|
67082
68411
|
targetPath: dirPath
|
|
@@ -67108,7 +68437,7 @@ var DirFeatureProcessor = class extends RulesyncSourceConsumer {
|
|
|
67108
68437
|
this.logger.warn(`Refusing to delete ${quoteForLog(filePath)}: this run wrote a file whose path differs from it only in case, which on a case-insensitive filesystem is the very file it wrote`);
|
|
67109
68438
|
continue;
|
|
67110
68439
|
}
|
|
67111
|
-
if (isClaimed(filePath)) continue;
|
|
68440
|
+
if (await isClaimed(filePath)) continue;
|
|
67112
68441
|
orphanPaths.add(filePath);
|
|
67113
68442
|
}
|
|
67114
68443
|
}
|
|
@@ -75664,16 +76993,30 @@ var SkillsProcessor = class extends DirFeatureProcessor {
|
|
|
75664
76993
|
* path and vetted as writable inside this run's output root. Shared by the
|
|
75665
76994
|
* two halves of the orphan sweep so both look in exactly the same places,
|
|
75666
76995
|
* under exactly the same guard.
|
|
76996
|
+
*
|
|
76997
|
+
* A root that fails the guard — a symbolic link, even one that stays inside
|
|
76998
|
+
* the output root, such as a dotfiles checkout linked from the home
|
|
76999
|
+
* directory — is left out with a warning rather than failing the run. The
|
|
77000
|
+
* write path already wrote through such a link, and the sweep runs after
|
|
77001
|
+
* those writes land, so throwing here would report a failure for a run whose
|
|
77002
|
+
* output is in place. Sweeping through the link is not an option either: it
|
|
77003
|
+
* can lead to a directory rulesync does not manage, and its entries would be
|
|
77004
|
+
* deleted as orphans.
|
|
75667
77005
|
*/
|
|
75668
77006
|
async loadExistingSkillsRoots(paths) {
|
|
75669
77007
|
const existingRoots = [];
|
|
75670
77008
|
for (const root of toolSkillSearchRoots(paths)) {
|
|
75671
77009
|
const skillsDirPath = join(this.outputRoot, root);
|
|
75672
77010
|
if (!await directoryExists(skillsDirPath)) continue;
|
|
75673
|
-
|
|
75674
|
-
|
|
75675
|
-
|
|
75676
|
-
|
|
77011
|
+
try {
|
|
77012
|
+
await assertWritablePathInsideRoot({
|
|
77013
|
+
rootPath: this.outputRoot,
|
|
77014
|
+
targetPath: skillsDirPath
|
|
77015
|
+
});
|
|
77016
|
+
} catch (error) {
|
|
77017
|
+
this.logger.warn(`Skipping the orphan sweep for ${quoteForLog(skillsDirPath)}; nothing under it is deleted: ${stripControlCharacters(formatError(error))}`);
|
|
77018
|
+
continue;
|
|
77019
|
+
}
|
|
75677
77020
|
existingRoots.push({
|
|
75678
77021
|
root,
|
|
75679
77022
|
skillsDirPath
|
|
@@ -76780,6 +78123,288 @@ var AugmentcodePluginSubagent = class extends AugmentcodeSubagent {
|
|
|
76780
78123
|
}
|
|
76781
78124
|
};
|
|
76782
78125
|
//#endregion
|
|
78126
|
+
//#region src/features/subagents/roo-subagent.ts
|
|
78127
|
+
/**
|
|
78128
|
+
* Default tool groups assigned to a generated mode when the rulesync source
|
|
78129
|
+
* does not specify a `roo.groups` override. Mirrors Roo's built-in code mode
|
|
78130
|
+
* (full read/edit/command/mcp access; browser is opt-in).
|
|
78131
|
+
*/
|
|
78132
|
+
const DEFAULT_GROUPS = [
|
|
78133
|
+
"read",
|
|
78134
|
+
"edit",
|
|
78135
|
+
"command",
|
|
78136
|
+
"mcp"
|
|
78137
|
+
];
|
|
78138
|
+
/**
|
|
78139
|
+
* One entry of a mode's `groups` array. Either a bare toolset name
|
|
78140
|
+
* (`"read"`, `"edit"`, `"command"`, `"mcp"`, `"browser"`) or a tuple pairing a
|
|
78141
|
+
* toolset (in practice `"edit"`) with a file restriction object.
|
|
78142
|
+
* @see https://roocodeinc.github.io/Roo-Code/features/custom-modes
|
|
78143
|
+
*/
|
|
78144
|
+
const RooModeGroupSchema = z.union([z.string(), z.tuple([z.string(), z.looseObject({})])]);
|
|
78145
|
+
/**
|
|
78146
|
+
* A single custom mode inside the `customModes` array of `.roomodes`.
|
|
78147
|
+
* @see https://roocodeinc.github.io/Roo-Code/features/custom-modes
|
|
78148
|
+
*/
|
|
78149
|
+
const RooModeSchema = z.looseObject({
|
|
78150
|
+
slug: z.string(),
|
|
78151
|
+
name: z.string(),
|
|
78152
|
+
description: z.optional(z.string()),
|
|
78153
|
+
roleDefinition: z.string(),
|
|
78154
|
+
whenToUse: z.optional(z.string()),
|
|
78155
|
+
groups: z.optional(z.array(RooModeGroupSchema)),
|
|
78156
|
+
customInstructions: z.optional(z.string())
|
|
78157
|
+
});
|
|
78158
|
+
const RooModesFileSchema = z.looseObject({ customModes: z.array(RooModeSchema) });
|
|
78159
|
+
/**
|
|
78160
|
+
* Sanitize a string into a valid Roo mode slug (`^[a-zA-Z0-9-]+$`): lowercase,
|
|
78161
|
+
* non-alphanumeric runs collapsed to a single hyphen, leading/trailing hyphens
|
|
78162
|
+
* trimmed. Falls back to `"mode"` if nothing usable remains.
|
|
78163
|
+
*/
|
|
78164
|
+
function sanitizeRooSlug(raw) {
|
|
78165
|
+
const slug = raw.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
|
|
78166
|
+
return slug.length > 0 ? slug : "mode";
|
|
78167
|
+
}
|
|
78168
|
+
function stringifyRooModes(modes) {
|
|
78169
|
+
return dump({ customModes: modes }, {
|
|
78170
|
+
lineWidth: -1,
|
|
78171
|
+
noRefs: true
|
|
78172
|
+
});
|
|
78173
|
+
}
|
|
78174
|
+
/**
|
|
78175
|
+
* Roo Code custom-modes adapter.
|
|
78176
|
+
*
|
|
78177
|
+
* Roo reads project-level custom modes from a single aggregated `.roomodes`
|
|
78178
|
+
* file at the workspace root (`docs.roocode.com` 301-redirects to the docs
|
|
78179
|
+
* below). Each rulesync subagent becomes one entry in the `customModes` array.
|
|
78180
|
+
* Because the native format aggregates N subagents into ONE file, every
|
|
78181
|
+
* `RooSubagent` instance carries the FULL `customModes` array and targets the
|
|
78182
|
+
* same `.roomodes` path; generation builds the aggregate once via
|
|
78183
|
+
* {@link RooSubagent.fromRulesyncSubagents}.
|
|
78184
|
+
*
|
|
78185
|
+
* @see https://roocodeinc.github.io/Roo-Code/features/custom-modes
|
|
78186
|
+
*/
|
|
78187
|
+
var RooSubagent = class extends ToolSubagent {
|
|
78188
|
+
/** Name of the aggregated custom-modes file inside `getSettablePaths().relativeDirPath`. */
|
|
78189
|
+
static modesFileName = ROO_MODES_FILE_NAME;
|
|
78190
|
+
/** Frontmatter section of a rulesync subagent that carries the mode fields. */
|
|
78191
|
+
static modeSectionKey = "roo";
|
|
78192
|
+
/** Tool groups a generated mode gets when its section sets no `groups`. */
|
|
78193
|
+
static defaultGroups = DEFAULT_GROUPS;
|
|
78194
|
+
/** Tool target written into the frontmatter of imported subagents. */
|
|
78195
|
+
static importTarget = "roo";
|
|
78196
|
+
modes;
|
|
78197
|
+
constructor({ modes, ...rest }) {
|
|
78198
|
+
if (rest.validate !== false) {
|
|
78199
|
+
const result = RooModesFileSchema.safeParse({ customModes: modes });
|
|
78200
|
+
if (!result.success) throw new Error(`Invalid custom modes in ${join(rest.relativeDirPath, rest.relativeFilePath)}: ${formatError(result.error)}`);
|
|
78201
|
+
}
|
|
78202
|
+
super({
|
|
78203
|
+
...rest,
|
|
78204
|
+
fileContent: stringifyRooModes(modes)
|
|
78205
|
+
});
|
|
78206
|
+
this.modes = modes;
|
|
78207
|
+
}
|
|
78208
|
+
static getSettablePaths(_options = {}) {
|
|
78209
|
+
return { relativeDirPath: "." };
|
|
78210
|
+
}
|
|
78211
|
+
getModes() {
|
|
78212
|
+
return this.modes;
|
|
78213
|
+
}
|
|
78214
|
+
getBody() {
|
|
78215
|
+
return this.fileContent;
|
|
78216
|
+
}
|
|
78217
|
+
/**
|
|
78218
|
+
* Map a single rulesync subagent to a Roo custom mode. The body becomes
|
|
78219
|
+
* `roleDefinition`; the optional `roo:` frontmatter section (the
|
|
78220
|
+
* {@link RooSubagent.modeSectionKey} section) supplies
|
|
78221
|
+
* `groups`, `whenToUse`, `customInstructions`, an explicit `slug`, and may
|
|
78222
|
+
* override `roleDefinition`. rulesync `name`/`description` are preferred,
|
|
78223
|
+
* while tool-specific `roo:` values take precedence where defined.
|
|
78224
|
+
*/
|
|
78225
|
+
static toRooMode(rulesyncSubagent) {
|
|
78226
|
+
const frontmatter = rulesyncSubagent.getFrontmatter();
|
|
78227
|
+
const rawSection = frontmatter[this.modeSectionKey] ?? {};
|
|
78228
|
+
const slug = typeof rawSection.slug === "string" && rawSection.slug.length > 0 ? sanitizeRooSlug(rawSection.slug) : sanitizeRooSlug(basename(rulesyncSubagent.getRelativeFilePath()).replace(/\.[^.]+$/, ""));
|
|
78229
|
+
const roleDefinition = typeof rawSection.roleDefinition === "string" ? rawSection.roleDefinition : rulesyncSubagent.getBody();
|
|
78230
|
+
const groups = Array.isArray(rawSection.groups) ? rawSection.groups : [...this.defaultGroups];
|
|
78231
|
+
const mode = {
|
|
78232
|
+
slug,
|
|
78233
|
+
name: frontmatter.name,
|
|
78234
|
+
roleDefinition,
|
|
78235
|
+
groups
|
|
78236
|
+
};
|
|
78237
|
+
if (frontmatter.description) mode.description = frontmatter.description;
|
|
78238
|
+
if (typeof rawSection.whenToUse === "string") mode.whenToUse = rawSection.whenToUse;
|
|
78239
|
+
if (typeof rawSection.customInstructions === "string") mode.customInstructions = rawSection.customInstructions;
|
|
78240
|
+
return mode;
|
|
78241
|
+
}
|
|
78242
|
+
/**
|
|
78243
|
+
* Aggregate every targeted rulesync subagent into a single `.roomodes` file.
|
|
78244
|
+
* Modes are de-duplicated by `slug` (last one wins) to keep the array unique.
|
|
78245
|
+
*/
|
|
78246
|
+
static fromRulesyncSubagents({ outputRoot = process.cwd(), rulesyncSubagents, validate = true }) {
|
|
78247
|
+
const bySlug = /* @__PURE__ */ new Map();
|
|
78248
|
+
for (const rulesyncSubagent of rulesyncSubagents) {
|
|
78249
|
+
const mode = this.toRooMode(rulesyncSubagent);
|
|
78250
|
+
bySlug.set(mode.slug, mode);
|
|
78251
|
+
}
|
|
78252
|
+
return new this({
|
|
78253
|
+
outputRoot,
|
|
78254
|
+
relativeDirPath: this.getSettablePaths().relativeDirPath,
|
|
78255
|
+
relativeFilePath: this.modesFileName,
|
|
78256
|
+
modes: [...bySlug.values()],
|
|
78257
|
+
validate
|
|
78258
|
+
});
|
|
78259
|
+
}
|
|
78260
|
+
static fromRulesyncSubagent({ outputRoot = process.cwd(), rulesyncSubagent, validate = true }) {
|
|
78261
|
+
return this.fromRulesyncSubagents({
|
|
78262
|
+
outputRoot,
|
|
78263
|
+
rulesyncSubagents: [rulesyncSubagent],
|
|
78264
|
+
validate
|
|
78265
|
+
});
|
|
78266
|
+
}
|
|
78267
|
+
/**
|
|
78268
|
+
* Convert every custom mode in the aggregated `.roomodes` file back into an
|
|
78269
|
+
* individual rulesync subagent. The processor calls this in the import
|
|
78270
|
+
* direction (one `.roomodes` tool file fans out to N rulesync files).
|
|
78271
|
+
*/
|
|
78272
|
+
toRulesyncSubagents() {
|
|
78273
|
+
const { modeSectionKey, importTarget } = this.constructor;
|
|
78274
|
+
return this.modes.map((mode) => {
|
|
78275
|
+
const { slug, name, description, roleDefinition, whenToUse, groups, customInstructions, ...rest } = mode;
|
|
78276
|
+
const modeSection = {
|
|
78277
|
+
...rest,
|
|
78278
|
+
slug,
|
|
78279
|
+
...groups ? { groups } : {},
|
|
78280
|
+
...whenToUse !== void 0 ? { whenToUse } : {},
|
|
78281
|
+
...customInstructions !== void 0 ? { customInstructions } : {}
|
|
78282
|
+
};
|
|
78283
|
+
return new RulesyncSubagent({
|
|
78284
|
+
outputRoot: ".",
|
|
78285
|
+
frontmatter: {
|
|
78286
|
+
targets: [importTarget],
|
|
78287
|
+
name,
|
|
78288
|
+
...description !== void 0 ? { description } : {},
|
|
78289
|
+
[modeSectionKey]: modeSection
|
|
78290
|
+
},
|
|
78291
|
+
body: roleDefinition,
|
|
78292
|
+
relativeDirPath: RULESYNC_SUBAGENTS_RELATIVE_DIR_PATH,
|
|
78293
|
+
relativeFilePath: `${sanitizeRooSlug(slug)}.md`,
|
|
78294
|
+
validate: true
|
|
78295
|
+
});
|
|
78296
|
+
});
|
|
78297
|
+
}
|
|
78298
|
+
toRulesyncSubagent() {
|
|
78299
|
+
const first = this.toRulesyncSubagents()[0];
|
|
78300
|
+
if (!first) throw new Error(`No custom modes found in ${join(this.relativeDirPath, this.relativeFilePath)} to convert.`);
|
|
78301
|
+
return first;
|
|
78302
|
+
}
|
|
78303
|
+
validate() {
|
|
78304
|
+
const result = RooModesFileSchema.safeParse({ customModes: this.modes });
|
|
78305
|
+
if (result.success) return {
|
|
78306
|
+
success: true,
|
|
78307
|
+
error: null
|
|
78308
|
+
};
|
|
78309
|
+
return {
|
|
78310
|
+
success: false,
|
|
78311
|
+
error: /* @__PURE__ */ new Error(`Invalid custom modes in ${join(this.relativeDirPath, this.relativeFilePath)}: ${formatError(result.error)}`)
|
|
78312
|
+
};
|
|
78313
|
+
}
|
|
78314
|
+
static isTargetedByRulesyncSubagent(rulesyncSubagent) {
|
|
78315
|
+
return this.isTargetedByRulesyncSubagentDefault({
|
|
78316
|
+
rulesyncSubagent,
|
|
78317
|
+
toolTarget: "roo"
|
|
78318
|
+
});
|
|
78319
|
+
}
|
|
78320
|
+
static async fromFile({ outputRoot = process.cwd(), relativeFilePath, validate = true }) {
|
|
78321
|
+
const paths = this.getSettablePaths();
|
|
78322
|
+
const filePath = join(outputRoot, paths.relativeDirPath, relativeFilePath);
|
|
78323
|
+
const fileContent = await readFileContent(filePath);
|
|
78324
|
+
let parsed;
|
|
78325
|
+
try {
|
|
78326
|
+
parsed = loadYaml(fileContent);
|
|
78327
|
+
} catch (error) {
|
|
78328
|
+
throw new Error(`Failed to parse custom modes (${filePath}): ${formatError(error)}`, { cause: error });
|
|
78329
|
+
}
|
|
78330
|
+
const result = RooModesFileSchema.safeParse(parsed ?? { customModes: [] });
|
|
78331
|
+
if (!result.success) throw new Error(`Invalid custom modes in ${filePath}: ${formatError(result.error)}`);
|
|
78332
|
+
return new this({
|
|
78333
|
+
outputRoot,
|
|
78334
|
+
relativeDirPath: paths.relativeDirPath,
|
|
78335
|
+
relativeFilePath: basename(relativeFilePath),
|
|
78336
|
+
modes: result.data.customModes,
|
|
78337
|
+
validate
|
|
78338
|
+
});
|
|
78339
|
+
}
|
|
78340
|
+
static forDeletion({ outputRoot = process.cwd(), relativeDirPath, relativeFilePath }) {
|
|
78341
|
+
return new this({
|
|
78342
|
+
outputRoot,
|
|
78343
|
+
relativeDirPath,
|
|
78344
|
+
relativeFilePath,
|
|
78345
|
+
modes: [],
|
|
78346
|
+
validate: false
|
|
78347
|
+
});
|
|
78348
|
+
}
|
|
78349
|
+
};
|
|
78350
|
+
//#endregion
|
|
78351
|
+
//#region src/features/subagents/bob-subagent.ts
|
|
78352
|
+
/**
|
|
78353
|
+
* Subagent (custom-mode) generator for **IBM Bob** (Bob IDE and Bob Shell).
|
|
78354
|
+
*
|
|
78355
|
+
* Bob has no file-based subagents — its native subagents are the two fixed
|
|
78356
|
+
* presets `explore` and `general` — but it reads custom modes from an
|
|
78357
|
+
* aggregated `customModes` YAML file in Roo's format, so every targeted rulesync
|
|
78358
|
+
* subagent becomes one mode in `.bob/custom_modes.yaml`, exactly as the `roo`
|
|
78359
|
+
* target fills `.roomodes`. Only the project file is emitted: the Bob IDE and
|
|
78360
|
+
* Bob Shell docs name different global files.
|
|
78361
|
+
*
|
|
78362
|
+
* The mode fields ride a `bob:` frontmatter section rather than Roo's `roo:`
|
|
78363
|
+
* one, because Bob's tool groups are spelled differently (`execute` where Roo
|
|
78364
|
+
* says `command`, plus `skill`, `workflow`, `todo`, `subtask`, `subagent` and
|
|
78365
|
+
* `mode`), so a `roo.groups` list is not valid for Bob. A mode without
|
|
78366
|
+
* `bob.groups` gets `["read", "edit", "execute", "mcp"]`, Roo's default with
|
|
78367
|
+
* Bob IDE's spelling — the one Bob's shared approval settings use too. The Bob
|
|
78368
|
+
* Shell custom-modes page still lists Roo's `command` / `browser` groups, so a
|
|
78369
|
+
* mode meant for the Shell should spell its groups out in `bob.groups`.
|
|
78370
|
+
* `bob.allowedSubagents` (Bob IDE) is emitted when it is a string list, and
|
|
78371
|
+
* every other mode field is lifted back into `bob:` on import.
|
|
78372
|
+
*
|
|
78373
|
+
* @see https://bob.ibm.com/docs/ide/configuration/custom-modes
|
|
78374
|
+
* @see https://bob.ibm.com/docs/shell/configuration/custom-modes-bobshell
|
|
78375
|
+
* @see https://bob.ibm.com/docs/ide/features/subagents
|
|
78376
|
+
*/
|
|
78377
|
+
var BobSubagent = class extends RooSubagent {
|
|
78378
|
+
static modesFileName = BOB_CUSTOM_MODES_FILE_NAME;
|
|
78379
|
+
static modeSectionKey = "bob";
|
|
78380
|
+
static defaultGroups = [
|
|
78381
|
+
"read",
|
|
78382
|
+
"edit",
|
|
78383
|
+
"execute",
|
|
78384
|
+
"mcp"
|
|
78385
|
+
];
|
|
78386
|
+
static importTarget = "bob";
|
|
78387
|
+
static getSettablePaths(_options = {}) {
|
|
78388
|
+
return {
|
|
78389
|
+
relativeDirPath: BOB_DIR,
|
|
78390
|
+
relativeFilePath: BOB_CUSTOM_MODES_FILE_NAME
|
|
78391
|
+
};
|
|
78392
|
+
}
|
|
78393
|
+
static isTargetedByRulesyncSubagent(rulesyncSubagent) {
|
|
78394
|
+
return this.isTargetedByRulesyncSubagentDefault({
|
|
78395
|
+
rulesyncSubagent,
|
|
78396
|
+
toolTarget: "bob"
|
|
78397
|
+
});
|
|
78398
|
+
}
|
|
78399
|
+
static toRooMode(rulesyncSubagent) {
|
|
78400
|
+
const mode = super.toRooMode(rulesyncSubagent);
|
|
78401
|
+
const frontmatter = rulesyncSubagent.getFrontmatter();
|
|
78402
|
+
const bobSection = isRecord$1(frontmatter.bob) ? frontmatter.bob : {};
|
|
78403
|
+
if (isStringArray$2(bobSection.allowedSubagents)) mode.allowedSubagents = bobSection.allowedSubagents;
|
|
78404
|
+
return mode;
|
|
78405
|
+
}
|
|
78406
|
+
};
|
|
78407
|
+
//#endregion
|
|
76783
78408
|
//#region src/features/subagents/claudecode-subagent.ts
|
|
76784
78409
|
const ClaudecodeSubagentFrontmatterSchema = z.looseObject({
|
|
76785
78410
|
name: z.string(),
|
|
@@ -78527,15 +80152,14 @@ var DevinSubagent = class extends ToolSubagent {
|
|
|
78527
80152
|
toRulesyncSubagent() {
|
|
78528
80153
|
const { name, description, ...restFields } = this.frontmatter;
|
|
78529
80154
|
const devinSection = { ...restFields };
|
|
78530
|
-
const rulesyncFrontmatter = {
|
|
78531
|
-
targets: ["*"],
|
|
78532
|
-
name,
|
|
78533
|
-
description,
|
|
78534
|
-
...Object.keys(devinSection).length > 0 && { devin: devinSection }
|
|
78535
|
-
};
|
|
78536
80155
|
return new RulesyncSubagent({
|
|
78537
|
-
outputRoot:
|
|
78538
|
-
frontmatter:
|
|
80156
|
+
outputRoot: ".",
|
|
80157
|
+
frontmatter: {
|
|
80158
|
+
targets: ["*"],
|
|
80159
|
+
name,
|
|
80160
|
+
description,
|
|
80161
|
+
...Object.keys(devinSection).length > 0 && { devin: devinSection }
|
|
80162
|
+
},
|
|
78539
80163
|
body: this.body,
|
|
78540
80164
|
relativeDirPath: RULESYNC_SUBAGENTS_RELATIVE_DIR_PATH,
|
|
78541
80165
|
relativeFilePath: `${this.getSubagentName()}.md`,
|
|
@@ -81304,221 +82928,6 @@ var ReasonixSubagent = class ReasonixSubagent extends ToolSubagent {
|
|
|
81304
82928
|
}
|
|
81305
82929
|
};
|
|
81306
82930
|
//#endregion
|
|
81307
|
-
//#region src/features/subagents/roo-subagent.ts
|
|
81308
|
-
/**
|
|
81309
|
-
* Default tool groups assigned to a generated mode when the rulesync source
|
|
81310
|
-
* does not specify a `roo.groups` override. Mirrors Roo's built-in code mode
|
|
81311
|
-
* (full read/edit/command/mcp access; browser is opt-in).
|
|
81312
|
-
*/
|
|
81313
|
-
const DEFAULT_GROUPS = [
|
|
81314
|
-
"read",
|
|
81315
|
-
"edit",
|
|
81316
|
-
"command",
|
|
81317
|
-
"mcp"
|
|
81318
|
-
];
|
|
81319
|
-
/**
|
|
81320
|
-
* One entry of a mode's `groups` array. Either a bare toolset name
|
|
81321
|
-
* (`"read"`, `"edit"`, `"command"`, `"mcp"`, `"browser"`) or a tuple pairing a
|
|
81322
|
-
* toolset (in practice `"edit"`) with a file restriction object.
|
|
81323
|
-
* @see https://roocodeinc.github.io/Roo-Code/features/custom-modes
|
|
81324
|
-
*/
|
|
81325
|
-
const RooModeGroupSchema = z.union([z.string(), z.tuple([z.string(), z.looseObject({})])]);
|
|
81326
|
-
/**
|
|
81327
|
-
* A single custom mode inside the `customModes` array of `.roomodes`.
|
|
81328
|
-
* @see https://roocodeinc.github.io/Roo-Code/features/custom-modes
|
|
81329
|
-
*/
|
|
81330
|
-
const RooModeSchema = z.looseObject({
|
|
81331
|
-
slug: z.string(),
|
|
81332
|
-
name: z.string(),
|
|
81333
|
-
description: z.optional(z.string()),
|
|
81334
|
-
roleDefinition: z.string(),
|
|
81335
|
-
whenToUse: z.optional(z.string()),
|
|
81336
|
-
groups: z.optional(z.array(RooModeGroupSchema)),
|
|
81337
|
-
customInstructions: z.optional(z.string())
|
|
81338
|
-
});
|
|
81339
|
-
const RooModesFileSchema = z.looseObject({ customModes: z.array(RooModeSchema) });
|
|
81340
|
-
/**
|
|
81341
|
-
* Sanitize a string into a valid Roo mode slug (`^[a-zA-Z0-9-]+$`): lowercase,
|
|
81342
|
-
* non-alphanumeric runs collapsed to a single hyphen, leading/trailing hyphens
|
|
81343
|
-
* trimmed. Falls back to `"mode"` if nothing usable remains.
|
|
81344
|
-
*/
|
|
81345
|
-
function sanitizeRooSlug(raw) {
|
|
81346
|
-
const slug = raw.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
|
|
81347
|
-
return slug.length > 0 ? slug : "mode";
|
|
81348
|
-
}
|
|
81349
|
-
function stringifyRooModes(modes) {
|
|
81350
|
-
return dump({ customModes: modes }, {
|
|
81351
|
-
lineWidth: -1,
|
|
81352
|
-
noRefs: true
|
|
81353
|
-
});
|
|
81354
|
-
}
|
|
81355
|
-
/**
|
|
81356
|
-
* Roo Code custom-modes adapter.
|
|
81357
|
-
*
|
|
81358
|
-
* Roo reads project-level custom modes from a single aggregated `.roomodes`
|
|
81359
|
-
* file at the workspace root (`docs.roocode.com` 301-redirects to the docs
|
|
81360
|
-
* below). Each rulesync subagent becomes one entry in the `customModes` array.
|
|
81361
|
-
* Because the native format aggregates N subagents into ONE file, every
|
|
81362
|
-
* `RooSubagent` instance carries the FULL `customModes` array and targets the
|
|
81363
|
-
* same `.roomodes` path; generation builds the aggregate once via
|
|
81364
|
-
* {@link RooSubagent.fromRulesyncSubagents}.
|
|
81365
|
-
*
|
|
81366
|
-
* @see https://roocodeinc.github.io/Roo-Code/features/custom-modes
|
|
81367
|
-
*/
|
|
81368
|
-
var RooSubagent = class extends ToolSubagent {
|
|
81369
|
-
modes;
|
|
81370
|
-
constructor({ modes, ...rest }) {
|
|
81371
|
-
if (rest.validate !== false) {
|
|
81372
|
-
const result = RooModesFileSchema.safeParse({ customModes: modes });
|
|
81373
|
-
if (!result.success) throw new Error(`Invalid .roomodes in ${join(rest.relativeDirPath, rest.relativeFilePath)}: ${formatError(result.error)}`);
|
|
81374
|
-
}
|
|
81375
|
-
super({
|
|
81376
|
-
...rest,
|
|
81377
|
-
fileContent: stringifyRooModes(modes)
|
|
81378
|
-
});
|
|
81379
|
-
this.modes = modes;
|
|
81380
|
-
}
|
|
81381
|
-
static getSettablePaths(_options = {}) {
|
|
81382
|
-
return { relativeDirPath: "." };
|
|
81383
|
-
}
|
|
81384
|
-
getModes() {
|
|
81385
|
-
return this.modes;
|
|
81386
|
-
}
|
|
81387
|
-
getBody() {
|
|
81388
|
-
return this.fileContent;
|
|
81389
|
-
}
|
|
81390
|
-
/**
|
|
81391
|
-
* Map a single rulesync subagent to a Roo custom mode. The body becomes
|
|
81392
|
-
* `roleDefinition`; the optional `roo:` frontmatter section supplies
|
|
81393
|
-
* `groups`, `whenToUse`, `customInstructions`, an explicit `slug`, and may
|
|
81394
|
-
* override `roleDefinition`. rulesync `name`/`description` are preferred,
|
|
81395
|
-
* while tool-specific `roo:` values take precedence where defined.
|
|
81396
|
-
*/
|
|
81397
|
-
static toRooMode(rulesyncSubagent) {
|
|
81398
|
-
const frontmatter = rulesyncSubagent.getFrontmatter();
|
|
81399
|
-
const rawSection = frontmatter.roo ?? {};
|
|
81400
|
-
const slug = typeof rawSection.slug === "string" && rawSection.slug.length > 0 ? sanitizeRooSlug(rawSection.slug) : sanitizeRooSlug(basename(rulesyncSubagent.getRelativeFilePath()).replace(/\.[^.]+$/, ""));
|
|
81401
|
-
const roleDefinition = typeof rawSection.roleDefinition === "string" ? rawSection.roleDefinition : rulesyncSubagent.getBody();
|
|
81402
|
-
const groups = Array.isArray(rawSection.groups) ? rawSection.groups : [...DEFAULT_GROUPS];
|
|
81403
|
-
const mode = {
|
|
81404
|
-
slug,
|
|
81405
|
-
name: frontmatter.name,
|
|
81406
|
-
roleDefinition,
|
|
81407
|
-
groups
|
|
81408
|
-
};
|
|
81409
|
-
if (frontmatter.description) mode.description = frontmatter.description;
|
|
81410
|
-
if (typeof rawSection.whenToUse === "string") mode.whenToUse = rawSection.whenToUse;
|
|
81411
|
-
if (typeof rawSection.customInstructions === "string") mode.customInstructions = rawSection.customInstructions;
|
|
81412
|
-
return mode;
|
|
81413
|
-
}
|
|
81414
|
-
/**
|
|
81415
|
-
* Aggregate every targeted rulesync subagent into a single `.roomodes` file.
|
|
81416
|
-
* Modes are de-duplicated by `slug` (last one wins) to keep the array unique.
|
|
81417
|
-
*/
|
|
81418
|
-
static fromRulesyncSubagents({ outputRoot = process.cwd(), rulesyncSubagents, validate = true }) {
|
|
81419
|
-
const bySlug = /* @__PURE__ */ new Map();
|
|
81420
|
-
for (const rulesyncSubagent of rulesyncSubagents) {
|
|
81421
|
-
const mode = this.toRooMode(rulesyncSubagent);
|
|
81422
|
-
bySlug.set(mode.slug, mode);
|
|
81423
|
-
}
|
|
81424
|
-
return new this({
|
|
81425
|
-
outputRoot,
|
|
81426
|
-
relativeDirPath: this.getSettablePaths().relativeDirPath,
|
|
81427
|
-
relativeFilePath: ROO_MODES_FILE_NAME,
|
|
81428
|
-
modes: [...bySlug.values()],
|
|
81429
|
-
validate
|
|
81430
|
-
});
|
|
81431
|
-
}
|
|
81432
|
-
static fromRulesyncSubagent({ outputRoot = process.cwd(), rulesyncSubagent, validate = true }) {
|
|
81433
|
-
return this.fromRulesyncSubagents({
|
|
81434
|
-
outputRoot,
|
|
81435
|
-
rulesyncSubagents: [rulesyncSubagent],
|
|
81436
|
-
validate
|
|
81437
|
-
});
|
|
81438
|
-
}
|
|
81439
|
-
/**
|
|
81440
|
-
* Convert every custom mode in the aggregated `.roomodes` file back into an
|
|
81441
|
-
* individual rulesync subagent. The processor calls this in the import
|
|
81442
|
-
* direction (one `.roomodes` tool file fans out to N rulesync files).
|
|
81443
|
-
*/
|
|
81444
|
-
toRulesyncSubagents() {
|
|
81445
|
-
return this.modes.map((mode) => {
|
|
81446
|
-
const { slug, name, description, roleDefinition, whenToUse, groups, customInstructions, ...rest } = mode;
|
|
81447
|
-
const rooSection = {
|
|
81448
|
-
...rest,
|
|
81449
|
-
slug,
|
|
81450
|
-
...groups ? { groups } : {},
|
|
81451
|
-
...whenToUse !== void 0 ? { whenToUse } : {},
|
|
81452
|
-
...customInstructions !== void 0 ? { customInstructions } : {}
|
|
81453
|
-
};
|
|
81454
|
-
return new RulesyncSubagent({
|
|
81455
|
-
outputRoot: ".",
|
|
81456
|
-
frontmatter: {
|
|
81457
|
-
targets: ["roo"],
|
|
81458
|
-
name,
|
|
81459
|
-
...description !== void 0 ? { description } : {},
|
|
81460
|
-
roo: rooSection
|
|
81461
|
-
},
|
|
81462
|
-
body: roleDefinition,
|
|
81463
|
-
relativeDirPath: RULESYNC_SUBAGENTS_RELATIVE_DIR_PATH,
|
|
81464
|
-
relativeFilePath: `${sanitizeRooSlug(slug)}.md`,
|
|
81465
|
-
validate: true
|
|
81466
|
-
});
|
|
81467
|
-
});
|
|
81468
|
-
}
|
|
81469
|
-
toRulesyncSubagent() {
|
|
81470
|
-
const first = this.toRulesyncSubagents()[0];
|
|
81471
|
-
if (!first) throw new Error("No custom modes found in .roomodes to convert.");
|
|
81472
|
-
return first;
|
|
81473
|
-
}
|
|
81474
|
-
validate() {
|
|
81475
|
-
const result = RooModesFileSchema.safeParse({ customModes: this.modes });
|
|
81476
|
-
if (result.success) return {
|
|
81477
|
-
success: true,
|
|
81478
|
-
error: null
|
|
81479
|
-
};
|
|
81480
|
-
return {
|
|
81481
|
-
success: false,
|
|
81482
|
-
error: /* @__PURE__ */ new Error(`Invalid .roomodes in ${join(this.relativeDirPath, this.relativeFilePath)}: ${formatError(result.error)}`)
|
|
81483
|
-
};
|
|
81484
|
-
}
|
|
81485
|
-
static isTargetedByRulesyncSubagent(rulesyncSubagent) {
|
|
81486
|
-
return this.isTargetedByRulesyncSubagentDefault({
|
|
81487
|
-
rulesyncSubagent,
|
|
81488
|
-
toolTarget: "roo"
|
|
81489
|
-
});
|
|
81490
|
-
}
|
|
81491
|
-
static async fromFile({ outputRoot = process.cwd(), relativeFilePath, validate = true }) {
|
|
81492
|
-
const paths = this.getSettablePaths();
|
|
81493
|
-
const filePath = join(outputRoot, paths.relativeDirPath, relativeFilePath);
|
|
81494
|
-
const fileContent = await readFileContent(filePath);
|
|
81495
|
-
let parsed;
|
|
81496
|
-
try {
|
|
81497
|
-
parsed = loadYaml(fileContent);
|
|
81498
|
-
} catch (error) {
|
|
81499
|
-
throw new Error(`Failed to parse .roomodes (${filePath}): ${formatError(error)}`, { cause: error });
|
|
81500
|
-
}
|
|
81501
|
-
const result = RooModesFileSchema.safeParse(parsed ?? { customModes: [] });
|
|
81502
|
-
if (!result.success) throw new Error(`Invalid .roomodes in ${filePath}: ${formatError(result.error)}`);
|
|
81503
|
-
return new this({
|
|
81504
|
-
outputRoot,
|
|
81505
|
-
relativeDirPath: paths.relativeDirPath,
|
|
81506
|
-
relativeFilePath: basename(relativeFilePath),
|
|
81507
|
-
modes: result.data.customModes,
|
|
81508
|
-
validate
|
|
81509
|
-
});
|
|
81510
|
-
}
|
|
81511
|
-
static forDeletion({ outputRoot = process.cwd(), relativeDirPath, relativeFilePath }) {
|
|
81512
|
-
return new this({
|
|
81513
|
-
outputRoot,
|
|
81514
|
-
relativeDirPath,
|
|
81515
|
-
relativeFilePath,
|
|
81516
|
-
modes: [],
|
|
81517
|
-
validate: false
|
|
81518
|
-
});
|
|
81519
|
-
}
|
|
81520
|
-
};
|
|
81521
|
-
//#endregion
|
|
81522
82931
|
//#region src/features/subagents/tabnine-subagent.ts
|
|
81523
82932
|
const TabnineSubagentFrontmatterSchema = z.looseObject({
|
|
81524
82933
|
name: z.string(),
|
|
@@ -82016,20 +83425,19 @@ var VibeSubagent = class VibeSubagent extends ToolSubagent {
|
|
|
82016
83425
|
const resolvedBody = this.resolvedSystemPrompt ?? system_prompt;
|
|
82017
83426
|
const unresolvedPromptId = this.resolvedSystemPrompt === void 0 ? system_prompt_id : void 0;
|
|
82018
83427
|
const fileStem = basename(this.getRelativeFilePath(), ".toml");
|
|
82019
|
-
const rulesyncFrontmatter = {
|
|
82020
|
-
targets: ["vibe"],
|
|
82021
|
-
name: display_name ?? fileStem,
|
|
82022
|
-
...description !== void 0 && { description },
|
|
82023
|
-
vibe: {
|
|
82024
|
-
...display_name !== void 0 && { display_name },
|
|
82025
|
-
...description !== void 0 && { description },
|
|
82026
|
-
...unresolvedPromptId !== void 0 && { system_prompt_id: unresolvedPromptId },
|
|
82027
|
-
...vibeSection
|
|
82028
|
-
}
|
|
82029
|
-
};
|
|
82030
83428
|
return new RulesyncSubagent({
|
|
82031
|
-
outputRoot:
|
|
82032
|
-
frontmatter:
|
|
83429
|
+
outputRoot: ".",
|
|
83430
|
+
frontmatter: {
|
|
83431
|
+
targets: ["vibe"],
|
|
83432
|
+
name: display_name ?? fileStem,
|
|
83433
|
+
...description !== void 0 && { description },
|
|
83434
|
+
vibe: {
|
|
83435
|
+
...display_name !== void 0 && { display_name },
|
|
83436
|
+
...description !== void 0 && { description },
|
|
83437
|
+
...unresolvedPromptId !== void 0 && { system_prompt_id: unresolvedPromptId },
|
|
83438
|
+
...vibeSection
|
|
83439
|
+
}
|
|
83440
|
+
},
|
|
82033
83441
|
body: resolvedBody ?? "",
|
|
82034
83442
|
relativeDirPath: RULESYNC_SUBAGENTS_RELATIVE_DIR_PATH,
|
|
82035
83443
|
relativeFilePath: this.getRelativeFilePath().replace(/\.toml$/, ".md"),
|
|
@@ -82462,6 +83870,15 @@ const toolSubagentFactories = /* @__PURE__ */ new Map([
|
|
|
82462
83870
|
filePattern: "*.md"
|
|
82463
83871
|
}
|
|
82464
83872
|
}],
|
|
83873
|
+
["bob", {
|
|
83874
|
+
class: BobSubagent,
|
|
83875
|
+
meta: {
|
|
83876
|
+
supportsProject: true,
|
|
83877
|
+
supportsSimulated: false,
|
|
83878
|
+
supportsGlobal: false,
|
|
83879
|
+
filePattern: "custom_modes.yaml"
|
|
83880
|
+
}
|
|
83881
|
+
}],
|
|
82465
83882
|
["claudecode", {
|
|
82466
83883
|
class: ClaudecodeSubagent,
|
|
82467
83884
|
meta: {
|
|
@@ -83084,10 +84501,15 @@ var SubagentsProcessor = class extends FeatureProcessor {
|
|
|
83084
84501
|
const rootOutputRoot = typeof root === "string" ? this.outputRoot : root.outputRoot;
|
|
83085
84502
|
const dirPath = typeof root === "string" ? root : root.relativeDirPath;
|
|
83086
84503
|
const baseDir = join(rootOutputRoot, dirPath);
|
|
83087
|
-
if (forDeletion && await directoryExists(baseDir))
|
|
83088
|
-
|
|
83089
|
-
|
|
83090
|
-
|
|
84504
|
+
if (forDeletion && await directoryExists(baseDir)) try {
|
|
84505
|
+
await assertWritablePathInsideRoot({
|
|
84506
|
+
rootPath: rootOutputRoot,
|
|
84507
|
+
targetPath: baseDir
|
|
84508
|
+
});
|
|
84509
|
+
} catch (error) {
|
|
84510
|
+
this.logger.warn(`Skipping the orphan sweep for ${quoteForLog(baseDir)}; nothing under it is deleted: ${stripControlCharacters(formatError(error))}`);
|
|
84511
|
+
continue;
|
|
84512
|
+
}
|
|
83091
84513
|
const subagentFilePaths = (await findFilesByGlobs(factory.meta.filePattern, {
|
|
83092
84514
|
cwd: baseDir,
|
|
83093
84515
|
followSymbolicLinks: !forDeletion
|
|
@@ -84049,6 +85471,16 @@ const AntigravityRuleFrontmatterSchema = z.looseObject({
|
|
|
84049
85471
|
description: z.optional(z.string())
|
|
84050
85472
|
});
|
|
84051
85473
|
/**
|
|
85474
|
+
* File name of a non-root rule in the shared global `~/.gemini/config/rules/`
|
|
85475
|
+
* directory. Antigravity reads only its top level, so a nested rulesync rule
|
|
85476
|
+
* (`frontend/style.md`) is flattened into a single file name
|
|
85477
|
+
* (`frontend-style.md`). The IDE and the CLI both use it so they write the
|
|
85478
|
+
* same file for the same rule.
|
|
85479
|
+
*/
|
|
85480
|
+
function toGlobalRuleFileName(relativeFilePath) {
|
|
85481
|
+
return relativeFilePath.split(/[\\/]/).join("-");
|
|
85482
|
+
}
|
|
85483
|
+
/**
|
|
84052
85484
|
* Converts a comma-separated globs string or array to an array of globs.
|
|
84053
85485
|
* @param globs - Comma-separated globs string (e.g., "*.ts,*.js") or array of globs
|
|
84054
85486
|
* @returns Array of glob patterns
|
|
@@ -84279,7 +85711,7 @@ var AntigravityCliRule = class AntigravityCliRule extends ToolRule {
|
|
|
84279
85711
|
});
|
|
84280
85712
|
return new AntigravityCliRule({
|
|
84281
85713
|
...params,
|
|
84282
|
-
relativeFilePath: params.relativeFilePath
|
|
85714
|
+
relativeFilePath: toGlobalRuleFileName(params.relativeFilePath),
|
|
84283
85715
|
fileContent,
|
|
84284
85716
|
global
|
|
84285
85717
|
});
|
|
@@ -84377,10 +85809,14 @@ var AntigravityCliRule = class AntigravityCliRule extends ToolRule {
|
|
|
84377
85809
|
*
|
|
84378
85810
|
* - Project scope: every rule is placed as a non-root file in
|
|
84379
85811
|
* `.agents/rules/` with Antigravity trigger frontmatter.
|
|
84380
|
-
* - Global scope: a
|
|
84381
|
-
*
|
|
85812
|
+
* - Global scope: a plain `~/.gemini/GEMINI.md` root file without
|
|
85813
|
+
* frontmatter, and non-root rules with trigger frontmatter in
|
|
85814
|
+
* `~/.gemini/config/rules/`. Both are shared with the Antigravity CLI, which
|
|
85815
|
+
* writes the same files for the same rules.
|
|
85816
|
+
*
|
|
85817
|
+
* @see https://antigravity.google/docs/rules
|
|
84382
85818
|
*/
|
|
84383
|
-
var AntigravityIdeRule = class
|
|
85819
|
+
var AntigravityIdeRule = class extends ToolRule {
|
|
84384
85820
|
frontmatter;
|
|
84385
85821
|
body;
|
|
84386
85822
|
constructor({ frontmatter, body, ...rest }) {
|
|
@@ -84408,28 +85844,18 @@ var AntigravityIdeRule = class AntigravityIdeRule extends ToolRule {
|
|
|
84408
85844
|
};
|
|
84409
85845
|
}
|
|
84410
85846
|
static getSettablePaths({ global = false, excludeToolDir } = {}) {
|
|
84411
|
-
if (global) return {
|
|
85847
|
+
if (global) return {
|
|
85848
|
+
root: this.getGlobalRootPath(excludeToolDir),
|
|
85849
|
+
nonRoot: { relativeDirPath: buildToolPath(ANTIGRAVITY_IDE_GEMINI_DIR, ANTIGRAVITY_IDE_GLOBAL_RULES_SUBDIR, excludeToolDir) }
|
|
85850
|
+
};
|
|
84412
85851
|
return {
|
|
84413
85852
|
root: this.getProjectRootPath(),
|
|
84414
85853
|
nonRoot: { relativeDirPath: buildToolPath(ANTIGRAVITY_IDE_AGENTS_DIR, "rules", excludeToolDir) }
|
|
84415
85854
|
};
|
|
84416
85855
|
}
|
|
84417
|
-
static async fromFile({ outputRoot = process.cwd(), relativeFilePath, validate = true, global = false }) {
|
|
85856
|
+
static async fromFile({ outputRoot = process.cwd(), relativeDirPath, relativeFilePath, validate = true, global = false }) {
|
|
84418
85857
|
const paths = this.getSettablePaths({ global });
|
|
84419
|
-
if (
|
|
84420
|
-
const rootPath = paths.root;
|
|
84421
|
-
const fileContent = await readFileContent(join(outputRoot, rootPath.relativeDirPath, rootPath.relativeFilePath));
|
|
84422
|
-
return new this({
|
|
84423
|
-
outputRoot,
|
|
84424
|
-
relativeDirPath: rootPath.relativeDirPath,
|
|
84425
|
-
relativeFilePath: rootPath.relativeFilePath,
|
|
84426
|
-
frontmatter: {},
|
|
84427
|
-
body: fileContent,
|
|
84428
|
-
validate,
|
|
84429
|
-
root: true
|
|
84430
|
-
});
|
|
84431
|
-
}
|
|
84432
|
-
if (relativeFilePath === paths.root.relativeFilePath) {
|
|
85858
|
+
if (relativeFilePath === paths.root.relativeFilePath && (relativeDirPath === void 0 || relativeDirPath === paths.root.relativeDirPath)) {
|
|
84433
85859
|
const rootPath = paths.root;
|
|
84434
85860
|
const rootContent = await readFileContent(join(outputRoot, rootPath.relativeDirPath, rootPath.relativeFilePath));
|
|
84435
85861
|
return new this({
|
|
@@ -84439,10 +85865,10 @@ var AntigravityIdeRule = class AntigravityIdeRule extends ToolRule {
|
|
|
84439
85865
|
frontmatter: {},
|
|
84440
85866
|
body: rootContent,
|
|
84441
85867
|
validate,
|
|
84442
|
-
root: true
|
|
85868
|
+
root: true,
|
|
85869
|
+
global
|
|
84443
85870
|
});
|
|
84444
85871
|
}
|
|
84445
|
-
if (!("nonRoot" in paths) || !paths.nonRoot) throw new Error(`nonRoot path is not set for ${relativeFilePath}`);
|
|
84446
85872
|
const nonRootDirPath = paths.nonRoot.relativeDirPath;
|
|
84447
85873
|
const filePath = join(outputRoot, nonRootDirPath, relativeFilePath);
|
|
84448
85874
|
const { frontmatter, body } = parseFrontmatter(await readFileContent(filePath), filePath);
|
|
@@ -84459,23 +85885,12 @@ var AntigravityIdeRule = class AntigravityIdeRule extends ToolRule {
|
|
|
84459
85885
|
body,
|
|
84460
85886
|
frontmatter: parsedFrontmatter,
|
|
84461
85887
|
validate,
|
|
84462
|
-
root: false
|
|
85888
|
+
root: false,
|
|
85889
|
+
global
|
|
84463
85890
|
});
|
|
84464
85891
|
}
|
|
84465
85892
|
static fromRulesyncRule({ outputRoot = process.cwd(), rulesyncRule, validate = true, global = false }) {
|
|
84466
85893
|
const paths = this.getSettablePaths({ global });
|
|
84467
|
-
if (global) {
|
|
84468
|
-
const rootPath = paths.root;
|
|
84469
|
-
return new this({
|
|
84470
|
-
outputRoot,
|
|
84471
|
-
relativeDirPath: rootPath.relativeDirPath,
|
|
84472
|
-
relativeFilePath: rootPath.relativeFilePath,
|
|
84473
|
-
frontmatter: {},
|
|
84474
|
-
body: rulesyncRule.getBody(),
|
|
84475
|
-
validate,
|
|
84476
|
-
root: true
|
|
84477
|
-
});
|
|
84478
|
-
}
|
|
84479
85894
|
if (rulesyncRule.getFrontmatter().root) {
|
|
84480
85895
|
const rootPath = paths.root;
|
|
84481
85896
|
return new this({
|
|
@@ -84485,7 +85900,8 @@ var AntigravityIdeRule = class AntigravityIdeRule extends ToolRule {
|
|
|
84485
85900
|
frontmatter: {},
|
|
84486
85901
|
body: rulesyncRule.getBody(),
|
|
84487
85902
|
validate,
|
|
84488
|
-
root: true
|
|
85903
|
+
root: true,
|
|
85904
|
+
global
|
|
84489
85905
|
});
|
|
84490
85906
|
}
|
|
84491
85907
|
const rulesyncFrontmatter = rulesyncRule.getFrontmatter();
|
|
@@ -84495,16 +85911,16 @@ var AntigravityIdeRule = class AntigravityIdeRule extends ToolRule {
|
|
|
84495
85911
|
const strategy = STRATEGIES$1.find((s) => s.canHandle(storedTrigger));
|
|
84496
85912
|
if (!strategy) throw new Error(`No strategy found for trigger: ${storedTrigger}`);
|
|
84497
85913
|
const frontmatter = strategy.generateFrontmatter(normalized, rulesyncFrontmatter);
|
|
84498
|
-
|
|
84499
|
-
const kebabCaseFilename = toKebabCaseFilename(rulesyncRule.getRelativeFilePath());
|
|
85914
|
+
const relativeFilePath = global ? toGlobalRuleFileName(rulesyncRule.getRelativeFilePath()) : toKebabCaseFilename(rulesyncRule.getRelativeFilePath());
|
|
84500
85915
|
return new this({
|
|
84501
85916
|
outputRoot,
|
|
84502
85917
|
relativeDirPath: paths.nonRoot.relativeDirPath,
|
|
84503
|
-
relativeFilePath
|
|
85918
|
+
relativeFilePath,
|
|
84504
85919
|
frontmatter,
|
|
84505
85920
|
body: rulesyncRule.getBody(),
|
|
84506
85921
|
validate,
|
|
84507
|
-
root: false
|
|
85922
|
+
root: false,
|
|
85923
|
+
global
|
|
84508
85924
|
});
|
|
84509
85925
|
}
|
|
84510
85926
|
toRulesyncRule() {
|
|
@@ -84549,15 +85965,26 @@ var AntigravityIdeRule = class AntigravityIdeRule extends ToolRule {
|
|
|
84549
85965
|
error: null
|
|
84550
85966
|
};
|
|
84551
85967
|
}
|
|
85968
|
+
/**
|
|
85969
|
+
* `~/.gemini/config/rules/` is shared with the Antigravity CLI and holds
|
|
85970
|
+
* global rules the user created outside rulesync, which cannot be told apart
|
|
85971
|
+
* from generated ones, so the orphan sweep never removes files there.
|
|
85972
|
+
*/
|
|
85973
|
+
isDeletable() {
|
|
85974
|
+
return !this.global || this.root;
|
|
85975
|
+
}
|
|
84552
85976
|
static forDeletion({ outputRoot = process.cwd(), relativeDirPath, relativeFilePath, global = false }) {
|
|
84553
|
-
|
|
85977
|
+
const rootPath = this.getSettablePaths({ global }).root;
|
|
85978
|
+
const isRoot = relativeFilePath === rootPath.relativeFilePath && relativeDirPath === rootPath.relativeDirPath;
|
|
85979
|
+
return new this({
|
|
84554
85980
|
outputRoot,
|
|
84555
85981
|
relativeDirPath,
|
|
84556
85982
|
relativeFilePath,
|
|
84557
85983
|
frontmatter: {},
|
|
84558
85984
|
body: "",
|
|
84559
85985
|
validate: false,
|
|
84560
|
-
root:
|
|
85986
|
+
root: isRoot,
|
|
85987
|
+
global
|
|
84561
85988
|
});
|
|
84562
85989
|
}
|
|
84563
85990
|
static isTargetedByRulesyncRule(rulesyncRule) {
|
|
@@ -91238,7 +92665,8 @@ const toolRuleFactories = /* @__PURE__ */ new Map([
|
|
|
91238
92665
|
meta: {
|
|
91239
92666
|
extension: "md",
|
|
91240
92667
|
supportsGlobal: true,
|
|
91241
|
-
ruleDiscoveryMode: "auto"
|
|
92668
|
+
ruleDiscoveryMode: "auto",
|
|
92669
|
+
sharedGlobalNonRootDir: true
|
|
91242
92670
|
}
|
|
91243
92671
|
}],
|
|
91244
92672
|
["antigravity-plugin", {
|
|
@@ -91875,6 +93303,7 @@ var RulesProcessor = class extends FeatureProcessor {
|
|
|
91875
93303
|
toolRules,
|
|
91876
93304
|
factory
|
|
91877
93305
|
});
|
|
93306
|
+
await this.warnForWarpLegacyRootFile(toolRules);
|
|
91878
93307
|
this.warnForInstructionBudget({
|
|
91879
93308
|
toolRules,
|
|
91880
93309
|
meta
|
|
@@ -92288,6 +93717,24 @@ var RulesProcessor = class extends FeatureProcessor {
|
|
|
92288
93717
|
this.logger.warn(`Writing ${stripControlCharacters(rootFileRelativePath)} for ${this.toolTarget} means ${listedNames.join(", ")}${remainingCount > 0 ? ` and ${remainingCount} more` : ""} will no longer be read. Run \`rulesync import --targets ${this.toolTarget}\` first to carry that content into ${RULESYNC_RULES_RELATIVE_DIR_PATH}, or delete ${listedNames.length === 1 ? "it" : "them"} once you have checked the content is already in the root file.`);
|
|
92289
93718
|
}
|
|
92290
93719
|
/**
|
|
93720
|
+
* Warp still reads a legacy `WARP.md`, and when both `WARP.md` and
|
|
93721
|
+
* `AGENTS.md` sit in the same directory, `WARP.md` takes priority — so the
|
|
93722
|
+
* `AGENTS.md` this run writes would never be read. Rulesync neither writes
|
|
93723
|
+
* nor deletes `WARP.md` (it is hand-authored), so the only thing it can do is
|
|
93724
|
+
* say so. Project scope only: the documented precedence is about project
|
|
93725
|
+
* rules, and Warp's global rule source is `~/.agents/AGENTS.md`.
|
|
93726
|
+
* @see https://docs.warp.dev/agents/capabilities/rules/
|
|
93727
|
+
*/
|
|
93728
|
+
async warnForWarpLegacyRootFile(toolRules) {
|
|
93729
|
+
if (this.toolTarget !== "warp" || this.global) return;
|
|
93730
|
+
if (toolRules.length === 0) return;
|
|
93731
|
+
const { root } = WarpRule.getSettablePaths();
|
|
93732
|
+
const legacyRelativePath = join(root.relativeDirPath, WARP_LEGACY_RULE_FILE_NAME);
|
|
93733
|
+
if (!await fileExists(join(this.outputRoot, legacyRelativePath))) return;
|
|
93734
|
+
const rootRelativePath = join(root.relativeDirPath, root.relativeFilePath);
|
|
93735
|
+
this.logger.warn(`${legacyRelativePath} exists next to ${rootRelativePath}, and Warp reads WARP.md instead of AGENTS.md when both are present, so the generated ${rootRelativePath} is ignored by Warp. Move any content you still need into ${RULESYNC_RULES_RELATIVE_DIR_PATH}, then delete or rename ${legacyRelativePath}.`);
|
|
93736
|
+
}
|
|
93737
|
+
/**
|
|
92291
93738
|
* Handle localRoot rule generation based on tool target.
|
|
92292
93739
|
* - `separate-local-file`: writes a dedicated `*.local.md` root file
|
|
92293
93740
|
* (claudecode/legacy: `./CLAUDE.local.md`, rovodev: `./AGENTS.local.md`)
|
|
@@ -93669,26 +95116,64 @@ function createFoldRootOverwriteWatch({ logger }) {
|
|
|
93669
95116
|
}
|
|
93670
95117
|
//#endregion
|
|
93671
95118
|
//#region src/lib/orphan-sweep.ts
|
|
93672
|
-
function createOrphanSweepPlan() {
|
|
95119
|
+
function createOrphanSweepPlan({ logger } = {}) {
|
|
93673
95120
|
const generatedPaths = /* @__PURE__ */ new Set();
|
|
93674
95121
|
const generatedTrees = /* @__PURE__ */ new Set();
|
|
93675
95122
|
const deferredSweeps = [];
|
|
93676
95123
|
const deletedPathsByFeature = /* @__PURE__ */ new Map();
|
|
93677
|
-
const
|
|
93678
|
-
|
|
95124
|
+
const warnedUnresolvable = /* @__PURE__ */ new Set();
|
|
95125
|
+
let landingClaims;
|
|
95126
|
+
const isInsideTree = ({ path, trees }) => {
|
|
95127
|
+
let current = path;
|
|
93679
95128
|
let parent = dirname(current);
|
|
93680
95129
|
while (parent !== current) {
|
|
93681
|
-
if (
|
|
95130
|
+
if (trees.has(parent)) return true;
|
|
93682
95131
|
current = parent;
|
|
93683
95132
|
parent = dirname(current);
|
|
93684
95133
|
}
|
|
93685
95134
|
return false;
|
|
93686
95135
|
};
|
|
95136
|
+
const followLinks = async (path) => {
|
|
95137
|
+
try {
|
|
95138
|
+
return await writeLandingPath(path);
|
|
95139
|
+
} catch {
|
|
95140
|
+
return null;
|
|
95141
|
+
}
|
|
95142
|
+
};
|
|
95143
|
+
const landingsOf = async (paths) => {
|
|
95144
|
+
const landings = await Promise.all([...paths].map((path) => followLinks(path)));
|
|
95145
|
+
return new Set(landings.filter((landing) => landing !== null));
|
|
95146
|
+
};
|
|
95147
|
+
const getLandingClaims = () => {
|
|
95148
|
+
landingClaims ??= Promise.all([landingsOf(generatedPaths), landingsOf(generatedTrees)]).then(([paths, trees]) => ({
|
|
95149
|
+
paths,
|
|
95150
|
+
trees: new Set([...trees].filter((tree) => dirname(tree) !== tree))
|
|
95151
|
+
}));
|
|
95152
|
+
return landingClaims;
|
|
95153
|
+
};
|
|
95154
|
+
const landsOnClaim = async ({ path, exactly }) => {
|
|
95155
|
+
const resolved = resolve(path);
|
|
95156
|
+
const landing = await followLinks(resolved);
|
|
95157
|
+
if (landing === null) {
|
|
95158
|
+
if (!warnedUnresolvable.has(resolved)) {
|
|
95159
|
+
warnedUnresolvable.add(resolved);
|
|
95160
|
+
logger?.warn(`Refusing to sweep ${quoteForLog(resolved)}: its symbolic links cannot be followed (a link cycle or an unreadable directory)`);
|
|
95161
|
+
}
|
|
95162
|
+
return true;
|
|
95163
|
+
}
|
|
95164
|
+
const claims = await getLandingClaims();
|
|
95165
|
+
return claims.paths.has(landing) || !exactly && isInsideTree({
|
|
95166
|
+
path: landing,
|
|
95167
|
+
trees: claims.trees
|
|
95168
|
+
});
|
|
95169
|
+
};
|
|
93687
95170
|
const plan = {
|
|
93688
95171
|
registerGenerated({ paths }) {
|
|
95172
|
+
landingClaims = void 0;
|
|
93689
95173
|
for (const path of paths) generatedPaths.add(resolve(path));
|
|
93690
95174
|
},
|
|
93691
95175
|
registerGeneratedTree({ paths }) {
|
|
95176
|
+
landingClaims = void 0;
|
|
93692
95177
|
for (const path of paths) {
|
|
93693
95178
|
const resolved = resolve(path);
|
|
93694
95179
|
generatedPaths.add(resolved);
|
|
@@ -93697,13 +95182,26 @@ function createOrphanSweepPlan() {
|
|
|
93697
95182
|
},
|
|
93698
95183
|
isGenerated({ path }) {
|
|
93699
95184
|
const resolved = resolve(path);
|
|
93700
|
-
return generatedPaths.has(resolved) ||
|
|
95185
|
+
return generatedPaths.has(resolved) || isInsideTree({
|
|
95186
|
+
path: resolved,
|
|
95187
|
+
trees: generatedTrees
|
|
95188
|
+
});
|
|
93701
95189
|
},
|
|
93702
|
-
isGeneratedExactly({ path }) {
|
|
93703
|
-
return generatedPaths.has(resolve(path))
|
|
95190
|
+
async isGeneratedExactly({ path }) {
|
|
95191
|
+
return generatedPaths.has(resolve(path)) || await landsOnClaim({
|
|
95192
|
+
path,
|
|
95193
|
+
exactly: true
|
|
95194
|
+
});
|
|
93704
95195
|
},
|
|
93705
|
-
rejectClaimed({ items, getPath }) {
|
|
93706
|
-
|
|
95196
|
+
async rejectClaimed({ items, getPath }) {
|
|
95197
|
+
const claimed = await Promise.all(items.map(async (item) => {
|
|
95198
|
+
const path = getPath(item);
|
|
95199
|
+
return plan.isGenerated({ path }) || await landsOnClaim({
|
|
95200
|
+
path,
|
|
95201
|
+
exactly: false
|
|
95202
|
+
});
|
|
95203
|
+
}));
|
|
95204
|
+
return items.filter((_, index) => !claimed[index]);
|
|
93707
95205
|
},
|
|
93708
95206
|
defer({ sweep, reportDeleted }) {
|
|
93709
95207
|
deferredSweeps.push({
|
|
@@ -93897,19 +95395,19 @@ function scheduleRetiredTargetSweeps({ config, logger, sweepPlan, sourceLoadFail
|
|
|
93897
95395
|
}
|
|
93898
95396
|
}
|
|
93899
95397
|
async function sweepFiles({ processor, sweepPlan, unownedPaths }) {
|
|
93900
|
-
const candidates = sweepPlan.rejectClaimed({
|
|
95398
|
+
const candidates = (await sweepPlan.rejectClaimed({
|
|
93901
95399
|
items: await processor.loadToolFiles({ forDeletion: true }),
|
|
93902
95400
|
getPath: (f) => f.getFilePath()
|
|
93903
|
-
}).filter((f) => !unownedPaths.has(resolve(f.getFilePath())));
|
|
95401
|
+
})).filter((f) => !unownedPaths.has(resolve(f.getFilePath())));
|
|
93904
95402
|
const present = await Promise.all(candidates.map((f) => fileExists(f.getFilePath())));
|
|
93905
95403
|
return await processor.removeOrphanAiFiles(candidates.filter((_, index) => present[index]), []) > 0;
|
|
93906
95404
|
}
|
|
93907
95405
|
async function sweepDirs({ processor, sweepPlan }) {
|
|
93908
|
-
return await processor.removeOrphanAiDirs(sweepPlan.rejectClaimed({
|
|
95406
|
+
return await processor.removeOrphanAiDirs(await sweepPlan.rejectClaimed({
|
|
93909
95407
|
items: await processor.loadToolDirsToDelete(),
|
|
93910
95408
|
getPath: (d) => d.getDirPath()
|
|
93911
95409
|
}), []) + await processor.removeOrphanFlatFiles({
|
|
93912
|
-
existingFlatFiles: sweepPlan.rejectClaimed({
|
|
95410
|
+
existingFlatFiles: await sweepPlan.rejectClaimed({
|
|
93913
95411
|
items: await processor.loadToolFlatFilesToDelete(),
|
|
93914
95412
|
getPath: (d) => d.getFlatFilePath() ?? d.getDirPath()
|
|
93915
95413
|
}),
|
|
@@ -94096,7 +95594,7 @@ async function processFeatureGeneration(params) {
|
|
|
94096
95594
|
if (config.getDelete() && !processor.hasRulesyncSourceLoadFailure()) sweepPlan.defer({
|
|
94097
95595
|
sweep: async () => {
|
|
94098
95596
|
const existingToolFiles = await processor.loadToolFiles({ forDeletion: true });
|
|
94099
|
-
return await processor.removeOrphanAiFiles(sweepPlan.rejectClaimed({
|
|
95597
|
+
return await processor.removeOrphanAiFiles(await sweepPlan.rejectClaimed({
|
|
94100
95598
|
items: existingToolFiles,
|
|
94101
95599
|
getPath: (f) => f.getFilePath()
|
|
94102
95600
|
}), toolFiles) > 0;
|
|
@@ -94110,6 +95608,16 @@ async function processFeatureGeneration(params) {
|
|
|
94110
95608
|
sourceLoadFailed: processor.hasRulesyncSourceLoadFailure()
|
|
94111
95609
|
};
|
|
94112
95610
|
}
|
|
95611
|
+
/**
|
|
95612
|
+
* Whether a write to `targetPath` lands strictly below `rootPath` once every
|
|
95613
|
+
* link on the way to either is followed. A cycle on either side answers false.
|
|
95614
|
+
*/
|
|
95615
|
+
async function landsStrictlyBelowRoot({ rootPath, targetPath }) {
|
|
95616
|
+
const [rootLanding, targetLanding] = await Promise.all([writeLandingPath(rootPath), writeLandingPath(targetPath)]);
|
|
95617
|
+
if (rootLanding === null || targetLanding === null) return false;
|
|
95618
|
+
const relativePath = relative(rootLanding, targetLanding);
|
|
95619
|
+
return relativePath !== "" && !pathEscapesRoot(relativePath);
|
|
95620
|
+
}
|
|
94113
95621
|
async function processDirFeatureGeneration(params) {
|
|
94114
95622
|
const { config, processor, toolDirs, sweepPlan } = params;
|
|
94115
95623
|
let totalCount = 0;
|
|
@@ -94120,7 +95628,11 @@ async function processDirFeatureGeneration(params) {
|
|
|
94120
95628
|
allPaths.push(...writeResult.paths);
|
|
94121
95629
|
if (writeResult.count > 0) hasDiff = true;
|
|
94122
95630
|
const ownedTrees = toolDirs.filter((d) => d.ownsDirTree());
|
|
94123
|
-
|
|
95631
|
+
const treesBelowRoot = await Promise.all(ownedTrees.map((d) => landsStrictlyBelowRoot({
|
|
95632
|
+
rootPath: d.getOutputRoot(),
|
|
95633
|
+
targetPath: d.getDirPath()
|
|
95634
|
+
})));
|
|
95635
|
+
sweepPlan.registerGeneratedTree({ paths: ownedTrees.filter((_, index) => treesBelowRoot[index]).map((d) => d.getDirPath()) });
|
|
94124
95636
|
sweepPlan.registerGenerated({ paths: toolDirs.flatMap((d) => {
|
|
94125
95637
|
const dirPath = d.getDirPath();
|
|
94126
95638
|
const mainFile = d.getMainFile();
|
|
@@ -94133,13 +95645,13 @@ async function processDirFeatureGeneration(params) {
|
|
|
94133
95645
|
if (config.getDelete() && !processor.hasRulesyncSourceLoadFailure()) sweepPlan.defer({
|
|
94134
95646
|
sweep: async () => {
|
|
94135
95647
|
const existingToolDirs = await processor.loadToolDirsToDelete();
|
|
94136
|
-
const orphanDirCount = await processor.removeOrphanAiDirs(sweepPlan.rejectClaimed({
|
|
95648
|
+
const orphanDirCount = await processor.removeOrphanAiDirs(await sweepPlan.rejectClaimed({
|
|
94137
95649
|
items: existingToolDirs,
|
|
94138
95650
|
getPath: (d) => d.getDirPath()
|
|
94139
95651
|
}), toolDirs);
|
|
94140
95652
|
const existingFlatFiles = await processor.loadToolFlatFilesToDelete();
|
|
94141
95653
|
const orphanFileCount = await processor.removeOrphanFlatFiles({
|
|
94142
|
-
existingFlatFiles: sweepPlan.rejectClaimed({
|
|
95654
|
+
existingFlatFiles: await sweepPlan.rejectClaimed({
|
|
94143
95655
|
items: existingFlatFiles,
|
|
94144
95656
|
getPath: (d) => d.getFlatFilePath() ?? d.getDirPath()
|
|
94145
95657
|
}),
|
|
@@ -94166,10 +95678,10 @@ async function processEmptyFeatureGeneration(params) {
|
|
|
94166
95678
|
if (config.getDelete() && !processor.hasRulesyncSourceLoadFailure()) sweepPlan.defer({
|
|
94167
95679
|
sweep: async () => {
|
|
94168
95680
|
const existingToolFiles = await processor.loadToolFiles({ forDeletion: true });
|
|
94169
|
-
const filesToDelete = sweepPlan.rejectClaimed({
|
|
95681
|
+
const filesToDelete = (await sweepPlan.rejectClaimed({
|
|
94170
95682
|
items: existingToolFiles,
|
|
94171
95683
|
getPath: (f) => f.getFilePath()
|
|
94172
|
-
}).filter((f) => !skipFilePaths?.has(f.getRelativePathFromCwd()));
|
|
95684
|
+
})).filter((f) => !skipFilePaths?.has(f.getRelativePathFromCwd()));
|
|
94173
95685
|
return await processor.removeOrphanAiFiles(filesToDelete, []) > 0;
|
|
94174
95686
|
},
|
|
94175
95687
|
reportDeleted: () => processor.getRemovedPaths()
|
|
@@ -94495,7 +96007,7 @@ async function generate(params) {
|
|
|
94495
96007
|
logger
|
|
94496
96008
|
});
|
|
94497
96009
|
let skillsResult;
|
|
94498
|
-
const sweepPlan = createOrphanSweepPlan();
|
|
96010
|
+
const sweepPlan = createOrphanSweepPlan({ logger });
|
|
94499
96011
|
const runners = {
|
|
94500
96012
|
ignore: () => generateIgnoreCore({
|
|
94501
96013
|
config,
|
|
@@ -95634,6 +97146,6 @@ async function importChecksCore(params) {
|
|
|
95634
97146
|
return writtenCount;
|
|
95635
97147
|
}
|
|
95636
97148
|
//#endregion
|
|
95637
|
-
export {
|
|
97149
|
+
export { RulesyncHooks as $, removeFile as $t, CLAUDECODE_DIR as A, RULESYNC_RELATIVE_DIR_PATH as An, applyFileMode as At, QWENCODE_LOCAL_RULE_FILE_NAME as B, DEPRECATED_FEATURE_REPLACEMENTS as Bn, getFileSize as Bt, COMMANDCODE_SETTINGS_LOCAL_FILE_NAME as C, RULESYNC_IGNORE_RELATIVE_FILE_PATH as Cn, fallbackLogger as Ct, CODEXCLI_BASH_RULES_FILE_NAME as D, RULESYNC_NPM_SOURCES_LOCK_RELATIVE_FILE_PATH as Dn, withWarnOnceScope as Dt, ChecksProcessor as E, RULESYNC_MCP_SCHEMA_URL as En, resetRunWarningState as Et, FACTORYDROID_DIR as F, RULESYNC_USER_CONFIG_DIR_NAME as Fn, createTempDirectory as Ft, RulesyncSubagentFrontmatterSchema as G, quoteForLog as Gn, listDirectoryEntryNames as Gt, AUGMENTCODE_SETTINGS_LOCAL_FILE_NAME as H, truncateText as Hn, isFileNotFoundError as Ht, FACTORYDROID_SETTINGS_LOCAL_FILE_NAME as I, RULESYNC_XDG_CONFIG_HOME_DEFAULT_DIR_NAME as In, directoryExists as It, RulesyncRule as J, stripHiddenCharacters as Jn, pathEscapesRoot as Jt, RulesyncSkill as K, stripControlCharacters as Kn, listFilePathsRecursively as Kt, caseFoldIdentity as L, parseCommaSeparatedList as Ln, ensureDir as Lt, CLAUDECODE_MEMORIES_DIR_NAME as M, RULESYNC_SKILLS_RELATIVE_DIR_PATH as Mn, assertTreeContainsNoSymlinks as Mt, CLAUDECODE_SETTINGS_LOCAL_FILE_NAME as N, RULESYNC_SOURCES_LOCK_RELATIVE_FILE_PATH as Nn, assertWritablePathInsideRoot as Nt, CODEXCLI_DIR as O, RULESYNC_PERMISSIONS_RELATIVE_FILE_PATH as On, CLIError as Ot, CLAUDECODE_SKILLS_DIR_PATH as P, RULESYNC_SUBAGENTS_RELATIVE_DIR_PATH as Pn, checkPathTraversal as Pt, RulesyncIgnore as Q, removeDirectoryStrict as Qt, groupSpellingsByCaseFoldedIdentity as R, ALL_FEATURES as Rn, fileExists as Rt, COMMANDCODE_DIR as S, RULESYNC_HOOKS_RELATIVE_FILE_PATH as Sn, WarningCollectingLogger as St, CODEBUDDY_LOCAL_RULE_FILE_NAME as T, RULESYNC_MCP_RELATIVE_FILE_PATH as Tn, withFallbackLoggerTarget as Tt, getLocalSkillDirNames as U, hasDeceptiveHiddenCharacters as Un, isFileSystemError as Ut, AUGMENTCODE_DIR as V, formatError as Vn, getHomeDirectory as Vt, RulesyncSubagent as W, hasEnclosingMarkOutsideKeycap as Wn, isSymlink as Wt, RulesyncPermissions as X, readFileContentOrNull as Xt, RulesyncRuleFrontmatterSchema as Y, readFileContent as Yt, RulesyncMcp as Z, removeDirectory as Zt, LETTACODE_DIR as _, RULESYNC_COMMANDS_RELATIVE_DIR_PATH as _n, SourceEntrySchema as _t, getProcessorRegistryEntry as a, toPosixPath as an, RulesyncCheck as at, CommandsProcessor as b, RULESYNC_CURATED_RULES_RELATIVE_DIR_PATH as bn, ConsoleLogger as bt, RulesProcessor as c, ALL_TOOL_TARGETS as cn, loadYaml as ct, ELLIPSIS_WIDTH as d, ToolTargetSchema as dn, ConfigResolver as dt, removeFileStrict as en, getRulesyncSourceCandidates as et, displayWidthOf as f, CURATED_RULES_FEATURE_SUBDIR as fn, mergeInputRootConfigs as ft, HooksProcessor as g, RULESYNC_CHECKS_RELATIVE_DIR_PATH as gn, GITIGNORE_DESTINATION_KEY as gt, IgnoreProcessor as h, RULESYNC_AIIGNORE_RELATIVE_FILE_PATH as hn, ConfigFileSchema as ht, inspectInputRoots as i, splitPathSegments as in, RulesyncCommandFrontmatterSchema as it, CLAUDECODE_LOCAL_RULE_FILE_NAME as j, RULESYNC_RULES_RELATIVE_DIR_PATH as jn, assertDirectoryIfExists as jt, CLAUDECODE_AGENT_MEMORY_LOCAL_DIR_NAME as k, RULESYNC_PERMISSIONS_SCHEMA_URL as kn, ErrorCodes as kt, SubagentsProcessor as l, ALL_TOOL_TARGETS_WITH_WILDCARD as ln, SHARED_USER_MANAGED_CONFIG_PATHS as lt, McpProcessor as m, MAX_FILE_SIZE as mn, CONFLICTING_TARGET_PAIRS as mt, formatSourceLoadFailure as n, resolvePath as nn, parseJsonc as nt, convertFromTool as o, writeFileBuffer as on, RulesyncCheckFrontmatterSchema as ot, shortenToWidth as p, FEATURE_SOURCE_TREE_ENTRIES as pn, resolveEffectiveInputRoots as pt, RulesyncSkillFrontmatterSchema as q, stripControlCharactersKeepingLineFeeds as qn, listSubdirectoryNames as qt, generate as r, runWithDirectoryRollback as rn, RulesyncCommand as rt, isPackagingToolTarget as s, writeFileContent as sn, stringifyFrontmatter as st, importFromTool as t, removeTempDirectory as tn, resolveRulesyncSourceWritePath as tt, SkillsProcessor as u, PACKAGING_TOOL_TARGETS as un, SKILL_FILE_NAME as ut, LETTACODE_SETTINGS_LOCAL_FILE_NAME as v, RULESYNC_CONFIG_RELATIVE_FILE_PATH as vn, assertTargetsFeaturesExclusive as vt, CODEBUDDY_DIR as w, RULESYNC_LOCAL_CONFIG_RELATIVE_FILE_PATH as wn, warnOnConflictingFlags as wt, REASONIX_LOCAL_RULE_FILE_NAME as x, RULESYNC_CURATED_SKILLS_RELATIVE_DIR_PATH as xn, JsonLogger as xt, CRUSH_LOCAL_RULE_FILE_NAME as y, RULESYNC_CONFIG_SCHEMA_URL as yn, findControlCharacter as yt, QWENCODE_DIR as z, ALL_FEATURES_WITH_WILDCARD as zn, findFilesByGlobs as zt };
|
|
95638
97150
|
|
|
95639
|
-
//# sourceMappingURL=import-
|
|
97151
|
+
//# sourceMappingURL=import-CnhfUGo7.js.map
|