@p4code/cli 0.4.13 → 0.4.14

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/bin.mjs CHANGED
@@ -240,7 +240,7 @@ const make$93 = () => {
240
240
  const layer$82 = Layer.sync(NetService, make$93);
241
241
  //#endregion
242
242
  //#region package.json
243
- var version = "0.4.13";
243
+ var version = "0.4.14";
244
244
  //#endregion
245
245
  //#region src/config.ts
246
246
  /**
@@ -21599,6 +21599,90 @@ function expandHomePath$3(value) {
21599
21599
  if (value.startsWith("~/") || value.startsWith("~\\")) return NodePath.join(NodeOS.homedir(), value.slice(2));
21600
21600
  return value;
21601
21601
  }
21602
+ /**
21603
+ * Reasoning levels `muse exec --reasoning-effort` accepts, in the CLI's own
21604
+ * order (`muse exec --help`, Muse Code 1.1.1). `high` is the CLI default, so
21605
+ * it is the default here too.
21606
+ */
21607
+ const MUSE_REASONING_EFFORTS = [
21608
+ "none",
21609
+ "minimal",
21610
+ "low",
21611
+ "medium",
21612
+ "high",
21613
+ "xhigh",
21614
+ "max",
21615
+ "ultra"
21616
+ ];
21617
+ const MUSE_DEFAULT_REASONING_EFFORT = "high";
21618
+ /**
21619
+ * Keep a selection Muse cannot parse out of the argv. `reasoningEffort` is a
21620
+ * shared option id — Codex publishes one too — so a selection left over from
21621
+ * another provider can reach this driver, and an unknown level makes
21622
+ * `muse exec` reject the whole invocation.
21623
+ */
21624
+ function museReasoningEffort(value) {
21625
+ return value !== void 0 && MUSE_REASONING_EFFORTS.includes(value) ? value : void 0;
21626
+ }
21627
+ /**
21628
+ * Build the argument vector for one turn.
21629
+ *
21630
+ * Approval policy is the load-bearing decision here. Muse Code 1.1.1 does
21631
+ * express approval as a policy — `--approval-mode untrusted|on-request|never`,
21632
+ * plus named `--permission-profile` documents — but a headless `exec` run
21633
+ * still has no channel to answer a prompt: approvals are served over MSP
21634
+ * (`muse serve`), not on the `exec` stream. So the only two postures `exec`
21635
+ * can honour end to end are `--disable-approval` (keep the OS sandbox) and
21636
+ * `--yolo` (drop approval and the sandbox, and trust the workspace). P4Code
21637
+ * owns the user-facing runtime mode, so it maps onto those two:
21638
+ *
21639
+ * - `full-access` → `--yolo`
21640
+ * - everything else → `--disable-approval --trust-workspace`, leaving the
21641
+ * sandbox to contain what runs.
21642
+ *
21643
+ * `approval-required` cannot be honoured over `exec`; the adapter emits a
21644
+ * `config.warning` for it rather than silently pretending otherwise. Serving
21645
+ * a real approval prompt means moving the driver onto MSP.
21646
+ *
21647
+ * Flags here were verified against Muse Code 1.1.1-R2514.1 (`muse exec
21648
+ * --help`).
21649
+ */
21650
+ function buildMuseExecArgs(input) {
21651
+ const args = [
21652
+ "exec",
21653
+ "--json",
21654
+ "--session-id",
21655
+ input.sessionId
21656
+ ];
21657
+ if (input.model) args.push("--model", input.model);
21658
+ if (input.reasoningEffort) args.push("--reasoning-effort", input.reasoningEffort);
21659
+ if (input.baseUrl) args.push("--base-url", input.baseUrl);
21660
+ if (input.runtimeMode === "full-access") args.push("--yolo");
21661
+ else {
21662
+ args.push("--disable-approval", "--trust-workspace");
21663
+ switch (input.sandboxMode) {
21664
+ case "danger-full-access":
21665
+ args.push("--disable-sandbox");
21666
+ break;
21667
+ case "read-only":
21668
+ args.push("--disable-write", "--disable-shell");
21669
+ break;
21670
+ default: break;
21671
+ }
21672
+ }
21673
+ for (const imagePath of input.imagePaths ?? []) args.push("--image", imagePath);
21674
+ args.push(input.prompt);
21675
+ return args;
21676
+ }
21677
+ /** `muse --version` argv, used by the provider status probe. */
21678
+ const MUSE_VERSION_ARGS = ["--version"];
21679
+ /**
21680
+ * Whether a runtime mode asks for interactive approvals that Muse Code
21681
+ * cannot deliver headlessly.
21682
+ */
21683
+ function museApprovalsUnsupported(runtimeMode) {
21684
+ return runtimeMode === "approval-required";
21685
+ }
21602
21686
  //#endregion
21603
21687
  //#region src/provider/skillOverrides.ts
21604
21688
  /**
@@ -21680,19 +21764,7 @@ function buildSkillOverrides(disabled, discovered) {
21680
21764
  return Object.keys(overrides).length === 0 ? void 0 : overrides;
21681
21765
  }
21682
21766
  //#endregion
21683
- //#region src/provider/Drivers/ClaudeSkills.ts
21684
- /**
21685
- * ClaudeSkills — filesystem discovery of Claude Code skills for the `$` picker.
21686
- *
21687
- * Claude Code loads skills from `<config dir>/skills` (user scope) and
21688
- * `<cwd>/.claude/skills` (project scope), one directory per skill with a
21689
- * `SKILL.md` carrying YAML frontmatter. The Agent SDK init handshake surfaces
21690
- * skills only as slash commands without their filesystem paths, so the
21691
- * provider snapshot scans the same locations directly, mirroring how the
21692
- * Codex app-server reports its skills.
21693
- *
21694
- * @module provider/Drivers/ClaudeSkills
21695
- */
21767
+ //#region src/provider/Drivers/skillFrontmatter.ts
21696
21768
  const FRONTMATTER_PATTERN$1 = /^---\r?\n([\s\S]*?)\r?\n---(?:\r?\n|$)/;
21697
21769
  /** The `true` spellings the CLI accepts for a boolean frontmatter field. */
21698
21770
  const TRUTHY_FRONTMATTER_VALUES = /* @__PURE__ */ new Set([
@@ -21727,10 +21799,10 @@ function stripMarkdownFrontmatter(contents) {
21727
21799
  /**
21728
21800
  * The `name`/`description` header a skill and a subagent definition both carry.
21729
21801
  *
21730
- * Exported because agent definitions are the same header in a different file,
21802
+ * Shared because agent definitions are the same header in a different file,
21731
21803
  * and the sync validates them before publishing one. A second copy of this
21732
21804
  * would be a second answer to "does this load in the provider", which is the
21733
- * one question both callers are asking.
21805
+ * one question every caller is asking.
21734
21806
  */
21735
21807
  function parseMarkdownFrontmatter(contents) {
21736
21808
  const match = FRONTMATTER_PATTERN$1.exec(contents);
@@ -21755,6 +21827,245 @@ function parseMarkdownFrontmatter(contents) {
21755
21827
  ...requiredInteractionMode === void 0 ? {} : { requiredInteractionMode }
21756
21828
  };
21757
21829
  }
21830
+ //#endregion
21831
+ //#region src/provider/Drivers/SharedSkills.ts
21832
+ /**
21833
+ * SharedSkills — the skills a provider can reach, whichever provider it is.
21834
+ *
21835
+ * A skill is a directory holding `SKILL.md`, and by now every CLI P4Code drives
21836
+ * loads them from the *same* provider-neutral user root — `~/.agents/skills` —
21837
+ * on top of its own. That is why the sync materializes there
21838
+ * (`sync/sharedSkillRoot.ts`): one copy of a skill, visible to all of them.
21839
+ *
21840
+ * What was still Claude- and Codex-only is the *reporting*: the composer's `$`
21841
+ * picker and the transcript's skill chips read `ServerProvider.skills`, and a
21842
+ * provider that never filled that array looked like it had no skills at all
21843
+ * while its CLI was happily loading them. So discovery is done here once,
21844
+ * against the roots each CLI documents, and every driver fills the same field.
21845
+ *
21846
+ * **Reported, not enforced.** `enabled` mirrors P4Code's own disabled-skill
21847
+ * setting so a skill switched off disappears from the picker everywhere. Only
21848
+ * Claude can additionally be told to withhold it from the model
21849
+ * (`skillOverrides`); the other CLIs own their listing, so the toggle is a
21850
+ * P4Code-surface toggle there, exactly as the flag's name in discovery says.
21851
+ *
21852
+ * Roots per CLI, from each vendor's own documentation:
21853
+ * - Cursor: `~/.agents/skills`, `~/.cursor/skills`, `.agents/skills`,
21854
+ * `.cursor/skills` — https://cursor.com/docs/skills
21855
+ * - Grok: `~/.agents/skills`, `~/.grok/skills`, `.agents/skills`,
21856
+ * `.grok/skills` — https://docs.x.ai/build/features/skills-plugins-marketplaces
21857
+ * - OpenCode: `~/.agents/skills`, `~/.claude/skills`,
21858
+ * `<XDG config>/opencode/skills`, and the project mirrors of all three —
21859
+ * https://opencode.ai/docs/skills/
21860
+ * - Muse: `~/.agents/skills`, `<XDG config>/muse/skills`, `.agents/skills`,
21861
+ * plus the foreign personal roots it reads unless `--no-foreign-personal-context`
21862
+ * is passed (`~/.claude/skills`, `~/.codex/skills`), which `muse skills list`
21863
+ * reports as user-scope entries. Those two are gated on
21864
+ * `MUSE_EXEC_LOADS_FOREIGN_PERSONAL_SKILLS`, which the argv builder owns,
21865
+ * so the claim tracks the flag the adapter actually spawns with.
21866
+ *
21867
+ * Claude and Codex are absent on purpose: each already reports its own skills
21868
+ * from a source that knows more than the filesystem does — Claude's config
21869
+ * directory may be redirected per instance, and Codex answers a `skills/list`
21870
+ * RPC that includes skills no directory scan would find.
21871
+ *
21872
+ * @module provider/Drivers/SharedSkills
21873
+ */
21874
+ /** `SKILL.md` is the only file a skill is required to have. */
21875
+ const SKILL_FILENAME$1 = "SKILL.md";
21876
+ /** The provider-neutral user skill root the sync materializes into. */
21877
+ const SHARED_USER_SKILL_DIRNAME = ".agents";
21878
+ /**
21879
+ * Enumerate skills across roots, least specific first.
21880
+ *
21881
+ * Discovery is best-effort: unreadable roots and malformed skill entries are
21882
+ * skipped so a broken skill never degrades the provider snapshot. On name
21883
+ * collisions the later root wins, matching the most-specific-wins resolution
21884
+ * every one of these CLIs uses — which is also why the shared root is listed
21885
+ * before the provider's own.
21886
+ */
21887
+ const discoverSkillsInRoots = Effect.fn("discoverSkillsInRoots")(function* (roots, disabledSkills) {
21888
+ const fileSystem = yield* FileSystem.FileSystem;
21889
+ const path = yield* Path.Path;
21890
+ const skillsByName = /* @__PURE__ */ new Map();
21891
+ for (const root of roots) {
21892
+ const entries = yield* fileSystem.readDirectory(root.directory).pipe(Effect.orElseSucceed(() => []));
21893
+ for (const entry of [...entries].sort()) {
21894
+ const skillPath = path.join(root.directory, entry, SKILL_FILENAME$1);
21895
+ const contents = yield* fileSystem.readFileString(skillPath).pipe(Effect.orElseSucceed(() => void 0));
21896
+ if (contents === void 0) continue;
21897
+ const frontmatter = parseMarkdownFrontmatter(contents);
21898
+ if (frontmatter.kind === "malformed") continue;
21899
+ const name = (frontmatter.kind === "parsed" ? frontmatter.name : void 0) ?? entry.trim();
21900
+ if (!name) continue;
21901
+ skillsByName.set(name, {
21902
+ name,
21903
+ path: skillPath,
21904
+ enabled: !isSkillDisabled({
21905
+ name,
21906
+ path: skillPath
21907
+ }, disabledSkills ?? []),
21908
+ scope: root.scope,
21909
+ ...frontmatter.kind === "parsed" && frontmatter.description ? { description: frontmatter.description } : {},
21910
+ ...frontmatter.kind === "parsed" && frontmatter.requiredInteractionMode ? { requiredInteractionMode: frontmatter.requiredInteractionMode } : {}
21911
+ });
21912
+ }
21913
+ }
21914
+ return [...skillsByName.values()].sort((left, right) => left.name.localeCompare(right.name));
21915
+ });
21916
+ /**
21917
+ * The config home a CLI following the XDG convention reads, matching what the
21918
+ * spawned process would see: its own `XDG_CONFIG_HOME` when set, else
21919
+ * `~/.config`. Read from the provider environment rather than `process.env`
21920
+ * because an instance may be configured with a different one.
21921
+ */
21922
+ function xdgConfigHome$1(environment) {
21923
+ const configured = environment.XDG_CONFIG_HOME?.trim() ?? "";
21924
+ return configured.length > 0 ? configured : `${homeDirectory(environment)}/.config`;
21925
+ }
21926
+ /**
21927
+ * The home the spawned CLI would resolve: the provider instance's own `HOME`
21928
+ * when it sets one, else this process's. A provider configured with a separate
21929
+ * home keeps its own skills, which is the point of configuring one.
21930
+ */
21931
+ function homeDirectory(environment) {
21932
+ const configured = environment.HOME?.trim() ?? "";
21933
+ return configured.length > 0 ? configured : NodeOS.homedir();
21934
+ }
21935
+ /**
21936
+ * The provider-neutral roots every CLI reads, least specific first.
21937
+ *
21938
+ * For callers that act on a skill without knowing which provider will run it —
21939
+ * the server-side expansion of a user-invoked skill, which happens before the
21940
+ * turn reaches an adapter.
21941
+ */
21942
+ const sharedSkillDirectories = Effect.fn("sharedSkillDirectories")(function* (cwd, environment) {
21943
+ const path = yield* Path.Path;
21944
+ const user = path.join(homeDirectory(environment ?? process.env), SHARED_USER_SKILL_DIRNAME, "skills");
21945
+ return cwd === void 0 ? [user] : [user, path.join(cwd, SHARED_USER_SKILL_DIRNAME, "skills")];
21946
+ });
21947
+ /** User-scope roots per driver, shared root first. */
21948
+ function userSkillDirectories(driver, environment) {
21949
+ const home = homeDirectory(environment);
21950
+ const shared = [
21951
+ home,
21952
+ SHARED_USER_SKILL_DIRNAME,
21953
+ "skills"
21954
+ ];
21955
+ switch (driver) {
21956
+ case "cursor": return [shared, [
21957
+ home,
21958
+ ".cursor",
21959
+ "skills"
21960
+ ]];
21961
+ case "grok": return [shared, [
21962
+ home,
21963
+ ".grok",
21964
+ "skills"
21965
+ ]];
21966
+ case "opencode": return [
21967
+ shared,
21968
+ [
21969
+ home,
21970
+ ".claude",
21971
+ "skills"
21972
+ ],
21973
+ [
21974
+ xdgConfigHome$1(environment),
21975
+ "opencode",
21976
+ "skills"
21977
+ ]
21978
+ ];
21979
+ case "muse": return [
21980
+ shared,
21981
+ ...[[
21982
+ home,
21983
+ ".claude",
21984
+ "skills"
21985
+ ], [
21986
+ home,
21987
+ ".codex",
21988
+ "skills"
21989
+ ]],
21990
+ [
21991
+ xdgConfigHome$1(environment),
21992
+ "muse",
21993
+ "skills"
21994
+ ]
21995
+ ];
21996
+ default: return [shared];
21997
+ }
21998
+ }
21999
+ /** Project-scope roots per driver, relative to the workspace. */
22000
+ function projectSkillDirectories(driver) {
22001
+ const shared = [SHARED_USER_SKILL_DIRNAME, "skills"];
22002
+ switch (driver) {
22003
+ case "cursor": return [shared, [".cursor", "skills"]];
22004
+ case "grok": return [shared, [".grok", "skills"]];
22005
+ case "opencode": return [
22006
+ shared,
22007
+ [".claude", "skills"],
22008
+ [".opencode", "skills"]
22009
+ ];
22010
+ default: return [shared];
22011
+ }
22012
+ }
22013
+ /** Every root a driver's CLI reads, least specific first. */
22014
+ const providerSkillRoots = Effect.fn("providerSkillRoots")(function* (input) {
22015
+ const path = yield* Path.Path;
22016
+ const environment = input.environment ?? process.env;
22017
+ const user = userSkillDirectories(input.driver, environment).map((segments) => ({
22018
+ directory: path.join(...segments),
22019
+ scope: "user"
22020
+ }));
22021
+ const cwd = input.cwd;
22022
+ if (cwd === void 0) return user;
22023
+ return [...user, ...projectSkillDirectories(input.driver).map((segments) => ({
22024
+ directory: path.join(cwd, ...segments),
22025
+ scope: "project"
22026
+ }))];
22027
+ });
22028
+ /**
22029
+ * A snapshot draft carrying the skills its CLI can reach.
22030
+ *
22031
+ * Applied after the probe rather than inside it: what a CLI reports about
22032
+ * itself — version, auth, models — and what it loads off disk are independent,
22033
+ * and a provider that is failing its probe still has the skills sitting there
22034
+ * for the picker.
22035
+ */
22036
+ const withProviderSkills = Effect.fn("withProviderSkills")(function* (draft, input) {
22037
+ const skills = yield* discoverProviderSkills(input);
22038
+ return {
22039
+ ...draft,
22040
+ skills
22041
+ };
22042
+ });
22043
+ /**
22044
+ * The skills a provider snapshot should report for a driver that loads them
22045
+ * from disk. Drivers call this instead of leaving `skills` empty.
22046
+ */
22047
+ const discoverProviderSkills = Effect.fn("discoverProviderSkills")(function* (input) {
22048
+ const roots = yield* providerSkillRoots({
22049
+ driver: input.driver,
22050
+ cwd: input.cwd,
22051
+ environment: input.environment
22052
+ });
22053
+ return yield* discoverSkillsInRoots(roots, input.disabledSkills);
22054
+ });
22055
+ //#endregion
22056
+ //#region src/provider/Drivers/ClaudeSkills.ts
22057
+ /**
22058
+ * ClaudeSkills — filesystem discovery of Claude Code skills for the `$` picker.
22059
+ *
22060
+ * Claude Code loads skills from `<config dir>/skills` (user scope) and
22061
+ * `<cwd>/.claude/skills` (project scope), one directory per skill with a
22062
+ * `SKILL.md` carrying YAML frontmatter. The Agent SDK init handshake surfaces
22063
+ * skills only as slash commands without their filesystem paths, so the
22064
+ * provider snapshot scans the same locations directly, mirroring how the
22065
+ * Codex app-server reports its skills.
22066
+ *
22067
+ * @module provider/Drivers/ClaudeSkills
22068
+ */
21758
22069
  /**
21759
22070
  * Resolve the Claude config directory the CLI would use, matching the
21760
22071
  * precedence the spawned CLI sees: the instance's `homePath` (exported as
@@ -21807,41 +22118,15 @@ const resolveClaudeUserAgentsDir = Effect.fn("resolveClaudeUserAgentsDir")(funct
21807
22118
  * most-specific-wins resolution.
21808
22119
  */
21809
22120
  const discoverClaudeSkills = Effect.fn("discoverClaudeSkills")(function* (config, cwd, environment, disabledSkills) {
21810
- const fileSystem = yield* FileSystem.FileSystem;
21811
22121
  const path = yield* Path.Path;
21812
22122
  const configDirPath = yield* resolveClaudeConfigDirPath(config, environment ?? process.env, cwd);
21813
- const roots = [{
22123
+ return yield* discoverSkillsInRoots([{
21814
22124
  directory: path.join(configDirPath, "skills"),
21815
22125
  scope: "user"
21816
22126
  }, ...cwd ? [{
21817
22127
  directory: path.join(cwd, ".claude", "skills"),
21818
22128
  scope: "project"
21819
- }] : []];
21820
- const skillsByName = /* @__PURE__ */ new Map();
21821
- for (const root of roots) {
21822
- const entries = yield* fileSystem.readDirectory(root.directory).pipe(Effect.orElseSucceed(() => []));
21823
- for (const entry of [...entries].sort()) {
21824
- const skillPath = path.join(root.directory, entry, "SKILL.md");
21825
- const contents = yield* fileSystem.readFileString(skillPath).pipe(Effect.orElseSucceed(() => void 0));
21826
- if (contents === void 0) continue;
21827
- const frontmatter = parseMarkdownFrontmatter(contents);
21828
- if (frontmatter.kind === "malformed") continue;
21829
- const name = (frontmatter.kind === "parsed" ? frontmatter.name : void 0) ?? entry.trim();
21830
- if (!name) continue;
21831
- skillsByName.set(name, {
21832
- name,
21833
- path: skillPath,
21834
- enabled: !isSkillDisabled({
21835
- name,
21836
- path: skillPath
21837
- }, disabledSkills ?? []),
21838
- scope: root.scope,
21839
- ...frontmatter.kind === "parsed" && frontmatter.description ? { description: frontmatter.description } : {},
21840
- ...frontmatter.kind === "parsed" && frontmatter.requiredInteractionMode ? { requiredInteractionMode: frontmatter.requiredInteractionMode } : {}
21841
- });
21842
- }
21843
- }
21844
- return [...skillsByName.values()].sort((left, right) => left.name.localeCompare(right.name));
22129
+ }] : []], disabledSkills);
21845
22130
  });
21846
22131
  //#endregion
21847
22132
  //#region src/mcp/ClaudeMcpFiles.ts
@@ -23818,6 +24103,138 @@ function planSkillSync(input) {
23818
24103
  };
23819
24104
  }
23820
24105
  //#endregion
24106
+ //#region src/sync/assetMirrorOps.ts
24107
+ /**
24108
+ * Whether this machine may write the asset into a mirror root.
24109
+ *
24110
+ * `foreign` covers the case that matters: a file someone else wrote, or a
24111
+ * symlink a user set up between two providers, whose content matches neither
24112
+ * what is being written nor what P4Code last wrote from here. Overwriting it
24113
+ * would silently replace a configuration the user owns.
24114
+ */
24115
+ function decideMirrorWrite(input) {
24116
+ if (input.existing === void 0) return "write";
24117
+ if (input.existing === input.next) return "up-to-date";
24118
+ if (input.previous !== void 0 && input.existing === input.previous) return "write";
24119
+ return "foreign";
24120
+ }
24121
+ /**
24122
+ * Whether a mirrored copy may be removed.
24123
+ *
24124
+ * Only a copy that still matches what P4Code wrote. An edited mirror is the
24125
+ * user's, and a deletion upstream is not a reason to throw their edit away.
24126
+ */
24127
+ function shouldRemoveMirror(input) {
24128
+ return input.existing !== void 0 && input.managed !== void 0 && input.existing === input.managed;
24129
+ }
24130
+ //#endregion
24131
+ //#region src/sync/providerAssetRoots.ts
24132
+ /**
24133
+ * providerAssetRoots — where a memory file or a subagent definition has to
24134
+ * land for a provider other than Claude to read it.
24135
+ *
24136
+ * Skills needed none of this: every CLI P4Code drives loads
24137
+ * `~/.agents/skills`, so one directory serves them all. Memory and subagent
24138
+ * definitions have no such shared root — there is a proposal to standardize a
24139
+ * user-level `AGENTS.md` location and nothing shipped — so each provider reads
24140
+ * its own path, and an asset that exists only in Claude's config directory is
24141
+ * an asset the other providers never see.
24142
+ *
24143
+ * Verified on this machine rather than taken from documentation:
24144
+ * - Codex keeps user instructions at `$CODEX_HOME/AGENTS.md` (default
24145
+ * `~/.codex`) and its subagent definitions at `$CODEX_HOME/agents/*.toml`.
24146
+ * - OpenCode keeps user instructions at `<XDG config>/opencode/AGENTS.md`
24147
+ * and its subagent definitions at `<XDG config>/opencode/agents/*.md`,
24148
+ * markdown with the same `name`/`description` frontmatter Claude uses.
24149
+ * - Cursor reads subagent definitions from `~/.cursor/agents`, and its own
24150
+ * documentation says it also reads `~/.claude/agents` and `~/.codex/agents`
24151
+ * for cross-tool compatibility.
24152
+ * - Muse reads the other CLIs' personal rules — `~/.claude/CLAUDE.md` and
24153
+ * `~/.codex/AGENTS.md` — unless a run passes
24154
+ * `--no-foreign-personal-context`, which P4Code never does
24155
+ * (`MUSE_EXEC_LOADS_FOREIGN_PERSONAL_SKILLS`). It therefore needs no
24156
+ * mirror of its own.
24157
+ *
24158
+ * Deliberately absent:
24159
+ * - **Codex subagents.** They are TOML with `developer_instructions`, not
24160
+ * markdown with frontmatter. Mirroring one would mean converting a
24161
+ * definition rather than copying it, and a lossy conversion of the file
24162
+ * that decides what a subagent is handed is worse than not having it.
24163
+ * - **Cursor user rules.** Cursor keeps cross-project prose in its own
24164
+ * settings store, not in a file on disk, so there is nothing to write.
24165
+ * - **Grok.** Its skills root is confirmed (`~/.agents/skills`); a
24166
+ * user-level instructions file is not, and x.ai's own documentation does
24167
+ * not name one. Writing a guessed path is worse than writing nothing.
24168
+ *
24169
+ * @module sync/providerAssetRoots
24170
+ */
24171
+ /**
24172
+ * The filename every non-Claude provider here reads its user instructions from.
24173
+ *
24174
+ * Codex and OpenCode read `AGENTS.md` and nothing else, so a `CLAUDE.md` asset
24175
+ * — the default, and what most machines actually hold — reaches them only
24176
+ * under this name. That rename is the one place in the sync where the asset's
24177
+ * name and the file on disk differ, and it is narrow on purpose: it happens for
24178
+ * this one pair of names, into these roots, and only when there is no
24179
+ * `AGENTS.md` asset of its own to mirror instead.
24180
+ */
24181
+ const SHARED_MEMORY_FILE_NAME = "AGENTS.md";
24182
+ /**
24183
+ * The config home of a CLI following the XDG convention, as the CLI itself
24184
+ * would resolve it.
24185
+ */
24186
+ function xdgConfigHome(environment) {
24187
+ const configured = environment.XDG_CONFIG_HOME?.trim() ?? "";
24188
+ if (configured.length > 0) return configured;
24189
+ const home = environment.HOME?.trim() ?? "";
24190
+ return `${home.length > 0 ? home : NodeOS.homedir()}/.config`;
24191
+ }
24192
+ /** Codex's home, matching the CLI's own `CODEX_HOME` precedence. */
24193
+ function codexHome(environment) {
24194
+ const configured = environment.CODEX_HOME?.trim() ?? "";
24195
+ return configured.length > 0 ? configured : `${NodeOS.homedir()}/.codex`;
24196
+ }
24197
+ /**
24198
+ * Directories that should hold a copy of a shared memory file.
24199
+ *
24200
+ * Directories rather than file paths because the sync writes an asset by name
24201
+ * into a root, and the name *is* the filename.
24202
+ */
24203
+ const memoryMirrorRoots = Effect.fn("memoryMirrorRoots")(function* (environment) {
24204
+ const path = yield* Path.Path;
24205
+ const resolved = environment ?? process.env;
24206
+ return [path.resolve(codexHome(resolved)), path.join(path.resolve(xdgConfigHome(resolved)), "opencode")];
24207
+ });
24208
+ /**
24209
+ * Where a memory asset should be copied, and under which name.
24210
+ *
24211
+ * `AGENTS.md` maps to itself. `CLAUDE.md` maps to `AGENTS.md`, but only when no
24212
+ * `AGENTS.md` asset exists: with both in play the destination would otherwise
24213
+ * flip between two sources depending on which one synced last, and a file whose
24214
+ * content depends on ordering is worse than one provider missing a mirror.
24215
+ */
24216
+ const memoryMirrorTargets = Effect.fn("memoryMirrorTargets")(function* (input) {
24217
+ if (!(input.name === "AGENTS.md" || input.name === "CLAUDE.md" && !input.siblingNames.has("AGENTS.md"))) return [];
24218
+ return (yield* memoryMirrorRoots(input.environment)).map((root) => ({
24219
+ root,
24220
+ name: SHARED_MEMORY_FILE_NAME
24221
+ }));
24222
+ });
24223
+ /** Directories that should hold a copy of a markdown subagent definition. */
24224
+ const agentDefinitionMirrorRoots = Effect.fn("agentDefinitionMirrorRoots")(function* (environment) {
24225
+ const path = yield* Path.Path;
24226
+ const resolved = environment ?? process.env;
24227
+ const home = resolved.HOME?.trim() ?? "";
24228
+ return [path.join(home.length > 0 ? home : NodeOS.homedir(), ".cursor", "agents"), path.join(path.resolve(xdgConfigHome(resolved)), "opencode", "agents")];
24229
+ });
24230
+ /** Where a markdown subagent definition should be copied, under its own name. */
24231
+ const agentDefinitionMirrorTargets = Effect.fn("agentDefinitionMirrorTargets")(function* (input) {
24232
+ return (yield* agentDefinitionMirrorRoots(input.environment)).map((root) => ({
24233
+ root,
24234
+ name: input.name
24235
+ }));
24236
+ });
24237
+ //#endregion
23821
24238
  //#region src/sync/sharedSkillRoot.ts
23822
24239
  const SHARED_SKILL_ROOT_REFUSAL = "shared-root-migration-blocked";
23823
24240
  const inspectLayout = Effect.fn("sharedSkillRoot.inspectLayout")(function* (input) {
@@ -24055,15 +24472,70 @@ const make$77 = Effect.gen(function* () {
24055
24472
  }]
24056
24473
  }))));
24057
24474
  });
24475
+ const prepareSkillRoot = Effect.gen(function* () {
24476
+ yield* prepareSharedSkillRoot({
24477
+ claudeSkillsRoot: yield* claudeHome().pipe(Effect.flatMap(resolveClaudeUserSkillsDir)),
24478
+ manifestPath
24479
+ });
24480
+ }).pipe(Effect.catchCause((cause) => Effect.logWarning("shared skill root migration failed", { cause })));
24481
+ /** What a mirror root currently holds under that name, if anything. */
24482
+ const mirrorDigest = Effect.fn("AssetSync.mirrorDigest")(function* (driver, root, name) {
24483
+ const existing = (yield* driver.read(root)).skills.find((entry) => entry.name === name);
24484
+ return existing ? digestSkillFiles(existing.files) : void 0;
24485
+ });
24486
+ /**
24487
+ * Copy a managed asset into the roots other providers read.
24488
+ *
24489
+ * Nothing P4Code did not put there is ever overwritten: a mirror holding
24490
+ * content that matches neither what is being written nor what was last
24491
+ * written from here belongs to someone else, and it is left alone. That is
24492
+ * the whole guard — a file the user wrote by hand, or a symlink they set up
24493
+ * between two providers, survives a sync untouched.
24494
+ */
24495
+ const mirrorWrite = Effect.fn("AssetSync.mirrorWrite")(function* (input) {
24496
+ const mirrors = input.driver.mirrors;
24497
+ if (mirrors === void 0) return;
24498
+ const nextDigest = digestSkillFiles(input.files);
24499
+ const targets = yield* mirrors({
24500
+ name: input.name,
24501
+ siblingNames: input.siblingNames
24502
+ });
24503
+ for (const target of targets) {
24504
+ if (decideMirrorWrite({
24505
+ existing: yield* mirrorDigest(input.driver, target.root, target.name),
24506
+ next: nextDigest,
24507
+ previous: input.previousDigest
24508
+ }) !== "write") continue;
24509
+ yield* input.driver.write({
24510
+ root: target.root,
24511
+ name: target.name,
24512
+ files: input.files
24513
+ });
24514
+ }
24515
+ });
24516
+ /** Drop a mirrored copy, and only a copy that still matches what we wrote. */
24517
+ const mirrorRemove = Effect.fn("AssetSync.mirrorRemove")(function* (input) {
24518
+ const mirrors = input.driver.mirrors;
24519
+ if (mirrors === void 0) return;
24520
+ const targets = yield* mirrors({
24521
+ name: input.name,
24522
+ siblingNames: input.siblingNames
24523
+ });
24524
+ for (const target of targets) {
24525
+ if (!shouldRemoveMirror({
24526
+ existing: yield* mirrorDigest(input.driver, target.root, target.name),
24527
+ managed: input.digest
24528
+ })) continue;
24529
+ yield* input.driver.remove({
24530
+ root: target.root,
24531
+ name: target.name
24532
+ });
24533
+ }
24534
+ });
24058
24535
  const drivers = [
24059
24536
  {
24060
24537
  kind: "skill",
24061
- prepareRoot: Effect.gen(function* () {
24062
- yield* prepareSharedSkillRoot({
24063
- claudeSkillsRoot: yield* claudeHome().pipe(Effect.flatMap(resolveClaudeUserSkillsDir)),
24064
- manifestPath
24065
- });
24066
- }).pipe(Effect.catchCause((cause) => Effect.logWarning("shared skill root migration failed", { cause }))),
24538
+ prepareRoot: prepareSkillRoot,
24067
24539
  resolveState: inspectSkillRoot(),
24068
24540
  resolveRoot: inspectSkillRoot().pipe(Effect.map((result) => result.root)),
24069
24541
  read: (root) => readSkillDirectory(root),
@@ -24091,7 +24563,11 @@ const make$77 = Effect.gen(function* () {
24091
24563
  memoryRoot: root,
24092
24564
  sharedMemoryRoot,
24093
24565
  name
24094
- }).pipe(Effect.ignore)
24566
+ }).pipe(Effect.ignore),
24567
+ mirrors: ({ name, siblingNames }) => memoryMirrorTargets({
24568
+ name,
24569
+ siblingNames
24570
+ })
24095
24571
  },
24096
24572
  {
24097
24573
  kind: "agent",
@@ -24105,7 +24581,8 @@ const make$77 = Effect.gen(function* () {
24105
24581
  remove: ({ root, name }) => removeAgentDefinition({
24106
24582
  agentsRoot: root,
24107
24583
  name
24108
- }).pipe(Effect.ignore)
24584
+ }).pipe(Effect.ignore),
24585
+ mirrors: ({ name }) => agentDefinitionMirrorTargets({ name })
24109
24586
  },
24110
24587
  {
24111
24588
  kind: "mcp",
@@ -24212,6 +24689,11 @@ const make$77 = Effect.gen(function* () {
24212
24689
  manifest: views.managed
24213
24690
  });
24214
24691
  const entries = new Map(views.managed.map((entry) => [entry.name, entry]));
24692
+ /**
24693
+ * Every asset of this kind in play, which the memory driver reads to
24694
+ * decide whether a `CLAUDE.md` may stand in for a missing `AGENTS.md`.
24695
+ */
24696
+ const assetNames = /* @__PURE__ */ new Set([...views.hub.map((asset) => asset.name), ...views.local.map((asset) => asset.name)]);
24215
24697
  /** One manifest for the whole run, re-read per kind so roots do not clobber. */
24216
24698
  const commit = Effect.fn("AssetSync.commit")(function* () {
24217
24699
  const manifest = yield* readSkillManifest(manifestPath);
@@ -24232,6 +24714,13 @@ const make$77 = Effect.gen(function* () {
24232
24714
  });
24233
24715
  break;
24234
24716
  }
24717
+ yield* mirrorWrite({
24718
+ driver,
24719
+ name: action.asset.name,
24720
+ siblingNames: assetNames,
24721
+ files: action.asset.files,
24722
+ previousDigest: entries.get(action.asset.name)?.digest
24723
+ });
24235
24724
  entries.set(action.asset.name, {
24236
24725
  name: action.asset.name,
24237
24726
  version: action.asset.version,
@@ -24274,6 +24763,12 @@ const make$77 = Effect.gen(function* () {
24274
24763
  root,
24275
24764
  name: action.name
24276
24765
  });
24766
+ yield* mirrorRemove({
24767
+ driver,
24768
+ name: action.name,
24769
+ siblingNames: assetNames,
24770
+ digest: entries.get(action.name)?.digest
24771
+ });
24277
24772
  entries.delete(action.name);
24278
24773
  removed.push({
24279
24774
  kind,
@@ -24711,6 +25206,13 @@ const make$77 = Effect.gen(function* () {
24711
25206
  name: input.name,
24712
25207
  files: input.files
24713
25208
  });
25209
+ if (written === "written") yield* mirrorWrite({
25210
+ driver,
25211
+ name: input.name,
25212
+ siblingNames: new Set(local.skills.map((asset) => asset.name)),
25213
+ files: input.files,
25214
+ previousDigest: void 0
25215
+ });
24714
25216
  if (written !== "written") return {
24715
25217
  outcome: written,
24716
25218
  path: null,
@@ -24747,10 +25249,17 @@ const make$77 = Effect.gen(function* () {
24747
25249
  path: null,
24748
25250
  detail: decision.detail
24749
25251
  };
25252
+ const removedDigest = local.skills.find((entry) => entry.name === ref.name);
24750
25253
  yield* driver.remove({
24751
25254
  root,
24752
25255
  name: ref.name
24753
25256
  });
25257
+ yield* mirrorRemove({
25258
+ driver,
25259
+ name: ref.name,
25260
+ siblingNames: new Set(local.skills.map((asset) => asset.name)),
25261
+ digest: removedDigest ? digestSkillFiles(removedDigest.files) : void 0
25262
+ });
24754
25263
  return {
24755
25264
  outcome: "removed",
24756
25265
  path: null,
@@ -102007,6 +102516,7 @@ const CursorDriver = {
102007
102516
  const fileSystem = yield* FileSystem.FileSystem;
102008
102517
  const path = yield* Path.Path;
102009
102518
  const httpClient = yield* HttpClient.HttpClient;
102519
+ const serverConfig = yield* ServerConfig$1;
102010
102520
  const serverSettings = yield* ServerSettingsService;
102011
102521
  const eventLoggers = yield* ProviderEventLoggers;
102012
102522
  const processEnv = mergeProviderInstanceEnvironment(environment);
@@ -102035,7 +102545,12 @@ const CursorDriver = {
102035
102545
  instanceId
102036
102546
  });
102037
102547
  const textGeneration = yield* makeCursorTextGeneration(effectiveConfig, processEnv);
102038
- const checkProvider = checkCursorProviderStatus(effectiveConfig, processEnv).pipe(Effect.map(stampIdentity), Effect.provideService(Crypto.Crypto, crypto), Effect.provideService(ChildProcessSpawner.ChildProcessSpawner, spawner), Effect.provideService(FileSystem.FileSystem, fileSystem), Effect.provideService(Path.Path, path));
102548
+ const checkProvider = serverSettings.getSettings.pipe(Effect.map((settings) => settings.disabledSkills), Effect.orElseSucceed(() => [])).pipe(Effect.flatMap((disabledSkills) => checkCursorProviderStatus(effectiveConfig, processEnv).pipe(Effect.flatMap((draft) => withProviderSkills(draft, {
102549
+ driver: DRIVER_KIND$3,
102550
+ cwd: serverConfig.cwd,
102551
+ environment: processEnv,
102552
+ disabledSkills
102553
+ })))), Effect.map(stampIdentity), Effect.provideService(Crypto.Crypto, crypto), Effect.provideService(ChildProcessSpawner.ChildProcessSpawner, spawner), Effect.provideService(FileSystem.FileSystem, fileSystem), Effect.provideService(Path.Path, path));
102039
102554
  const snapshotSettings = makeProviderSnapshotSettingsSource(effectiveConfig, serverSettings);
102040
102555
  const snapshot = yield* makeManagedServerProvider({
102041
102556
  maintenanceCapabilities,
@@ -103576,6 +104091,9 @@ const GrokDriver = {
103576
104091
  const crypto = yield* Crypto.Crypto;
103577
104092
  const spawner = yield* ChildProcessSpawner.ChildProcessSpawner;
103578
104093
  const httpClient = yield* HttpClient.HttpClient;
104094
+ const fileSystem = yield* FileSystem.FileSystem;
104095
+ const path = yield* Path.Path;
104096
+ const serverConfig = yield* ServerConfig$1;
103579
104097
  const serverSettings = yield* ServerSettingsService;
103580
104098
  const eventLoggers = yield* ProviderEventLoggers;
103581
104099
  const processEnv = mergeProviderInstanceEnvironment(environment);
@@ -103604,7 +104122,12 @@ const GrokDriver = {
103604
104122
  instanceId
103605
104123
  });
103606
104124
  const textGeneration = yield* makeGrokTextGeneration(effectiveConfig, processEnv);
103607
- const checkProvider = checkGrokProviderStatus(effectiveConfig, processEnv).pipe(Effect.map(stampIdentity), Effect.provideService(Crypto.Crypto, crypto), Effect.provideService(ChildProcessSpawner.ChildProcessSpawner, spawner));
104125
+ const checkProvider = serverSettings.getSettings.pipe(Effect.map((settings) => settings.disabledSkills), Effect.orElseSucceed(() => [])).pipe(Effect.flatMap((disabledSkills) => checkGrokProviderStatus(effectiveConfig, processEnv).pipe(Effect.flatMap((draft) => withProviderSkills(draft, {
104126
+ driver: DRIVER_KIND$2,
104127
+ cwd: serverConfig.cwd,
104128
+ environment: processEnv,
104129
+ disabledSkills
104130
+ })))), Effect.map(stampIdentity), Effect.provideService(Crypto.Crypto, crypto), Effect.provideService(ChildProcessSpawner.ChildProcessSpawner, spawner), Effect.provideService(FileSystem.FileSystem, fileSystem), Effect.provideService(Path.Path, path));
103608
104131
  const snapshotSettings = makeProviderSnapshotSettingsSource(effectiveConfig, serverSettings);
103609
104132
  const snapshot = yield* makeManagedServerProvider({
103610
104133
  maintenanceCapabilities,
@@ -103640,90 +104163,6 @@ const GrokDriver = {
103640
104163
  };
103641
104164
  })
103642
104165
  };
103643
- /**
103644
- * Reasoning levels `muse exec --reasoning-effort` accepts, in the CLI's own
103645
- * order (`muse exec --help`, Muse Code 1.1.1). `high` is the CLI default, so
103646
- * it is the default here too.
103647
- */
103648
- const MUSE_REASONING_EFFORTS = [
103649
- "none",
103650
- "minimal",
103651
- "low",
103652
- "medium",
103653
- "high",
103654
- "xhigh",
103655
- "max",
103656
- "ultra"
103657
- ];
103658
- const MUSE_DEFAULT_REASONING_EFFORT = "high";
103659
- /**
103660
- * Keep a selection Muse cannot parse out of the argv. `reasoningEffort` is a
103661
- * shared option id — Codex publishes one too — so a selection left over from
103662
- * another provider can reach this driver, and an unknown level makes
103663
- * `muse exec` reject the whole invocation.
103664
- */
103665
- function museReasoningEffort(value) {
103666
- return value !== void 0 && MUSE_REASONING_EFFORTS.includes(value) ? value : void 0;
103667
- }
103668
- /**
103669
- * Build the argument vector for one turn.
103670
- *
103671
- * Approval policy is the load-bearing decision here. Muse Code 1.1.1 does
103672
- * express approval as a policy — `--approval-mode untrusted|on-request|never`,
103673
- * plus named `--permission-profile` documents — but a headless `exec` run
103674
- * still has no channel to answer a prompt: approvals are served over MSP
103675
- * (`muse serve`), not on the `exec` stream. So the only two postures `exec`
103676
- * can honour end to end are `--disable-approval` (keep the OS sandbox) and
103677
- * `--yolo` (drop approval and the sandbox, and trust the workspace). P4Code
103678
- * owns the user-facing runtime mode, so it maps onto those two:
103679
- *
103680
- * - `full-access` → `--yolo`
103681
- * - everything else → `--disable-approval --trust-workspace`, leaving the
103682
- * sandbox to contain what runs.
103683
- *
103684
- * `approval-required` cannot be honoured over `exec`; the adapter emits a
103685
- * `config.warning` for it rather than silently pretending otherwise. Serving
103686
- * a real approval prompt means moving the driver onto MSP.
103687
- *
103688
- * Flags here were verified against Muse Code 1.1.1-R2514.1 (`muse exec
103689
- * --help`).
103690
- */
103691
- function buildMuseExecArgs(input) {
103692
- const args = [
103693
- "exec",
103694
- "--json",
103695
- "--session-id",
103696
- input.sessionId
103697
- ];
103698
- if (input.model) args.push("--model", input.model);
103699
- if (input.reasoningEffort) args.push("--reasoning-effort", input.reasoningEffort);
103700
- if (input.baseUrl) args.push("--base-url", input.baseUrl);
103701
- if (input.runtimeMode === "full-access") args.push("--yolo");
103702
- else {
103703
- args.push("--disable-approval", "--trust-workspace");
103704
- switch (input.sandboxMode) {
103705
- case "danger-full-access":
103706
- args.push("--disable-sandbox");
103707
- break;
103708
- case "read-only":
103709
- args.push("--disable-write", "--disable-shell");
103710
- break;
103711
- default: break;
103712
- }
103713
- }
103714
- for (const imagePath of input.imagePaths ?? []) args.push("--image", imagePath);
103715
- args.push(input.prompt);
103716
- return args;
103717
- }
103718
- /** `muse --version` argv, used by the provider status probe. */
103719
- const MUSE_VERSION_ARGS = ["--version"];
103720
- /**
103721
- * Whether a runtime mode asks for interactive approvals that Muse Code
103722
- * cannot deliver headlessly.
103723
- */
103724
- function museApprovalsUnsupported(runtimeMode) {
103725
- return runtimeMode === "approval-required";
103726
- }
103727
104166
  //#endregion
103728
104167
  //#region src/provider/muse/MuseExecEvents.ts
103729
104168
  /**
@@ -104255,6 +104694,163 @@ const makeMuseTextGeneration = (museSettings, environment = process.env) => Effe
104255
104694
  })
104256
104695
  };
104257
104696
  });
104697
+ /** The one file the overlay owns; everything else links back to the source. */
104698
+ const MUSE_SETTINGS_FILENAME = "settings.json";
104699
+ /** The credential the launcher and the binary both read through this variable. */
104700
+ const MUSE_AUTH_FILENAME = "auth.json";
104701
+ /** The config subdirectory Muse keeps under an XDG config home. */
104702
+ const MUSE_CONFIG_DIRNAME = "muse";
104703
+ /**
104704
+ * The generated settings file carries this session's bearer token, so the
104705
+ * overlay is private to the account running the server.
104706
+ */
104707
+ const OVERLAY_DIRECTORY_MODE = 448;
104708
+ const OVERLAY_SETTINGS_MODE = 384;
104709
+ /**
104710
+ * Registered servers as Muse settings entries.
104711
+ *
104712
+ * SSE has no spelling here — Muse reads an entry carrying `type: "sse"` as no
104713
+ * entry at all — so those are reported as skipped instead of written and
104714
+ * quietly ignored. The reserved names are dropped for the reason every other
104715
+ * adapter drops them: a registration called `p4-code` must not displace the
104716
+ * built-in one and take the session's own tools with it.
104717
+ */
104718
+ function toMuseMcpServers(resolved, reservedNames) {
104719
+ const entries = {};
104720
+ const skipped = [];
104721
+ for (const [name, server] of Object.entries(resolved)) {
104722
+ if (reservedNames.has(name)) continue;
104723
+ if (server.type === "stdio") {
104724
+ entries[name] = {
104725
+ type: "stdio",
104726
+ command: server.command,
104727
+ ...server.args.length > 0 ? { args: [...server.args] } : {},
104728
+ ...Object.keys(server.env).length > 0 ? { env: { ...server.env } } : {}
104729
+ };
104730
+ continue;
104731
+ }
104732
+ if (server.type === "sse") {
104733
+ skipped.push(name);
104734
+ continue;
104735
+ }
104736
+ entries[name] = {
104737
+ type: "http",
104738
+ url: server.url,
104739
+ ...Object.keys(server.headers).length > 0 ? { headers: { ...server.headers } } : {}
104740
+ };
104741
+ }
104742
+ return {
104743
+ entries,
104744
+ skipped
104745
+ };
104746
+ }
104747
+ /**
104748
+ * The config home a CLI following the XDG convention reads, matching what the
104749
+ * spawned process would see: its own `XDG_CONFIG_HOME` when set, else
104750
+ * `~/.config` under the environment's own `HOME`.
104751
+ */
104752
+ function resolveXdgConfigHome(environment) {
104753
+ const configured = environment.XDG_CONFIG_HOME?.trim() ?? "";
104754
+ if (configured.length > 0) return configured;
104755
+ const home = environment.HOME?.trim() ?? "";
104756
+ return `${home.length > 0 ? home : NodeOS.homedir()}/.config`;
104757
+ }
104758
+ /**
104759
+ * The settings document as this module needs to see it: the one key it owns,
104760
+ * with every other key carried through untouched, so an overlay never quietly
104761
+ * drops a model default or a TUI preference the user set.
104762
+ */
104763
+ const MuseSettingsFile = Schema$1.StructWithRest(Schema$1.Struct({
104764
+ schema_version: Schema$1.optional(Schema$1.Number),
104765
+ mcpServers: Schema$1.optional(Schema$1.Unknown)
104766
+ }), [Schema$1.Record(Schema$1.String, Schema$1.Unknown)]);
104767
+ const MuseSettingsFileFromJson = Schema$1.fromJsonString(MuseSettingsFile);
104768
+ const decodeSettingsFile = Schema$1.decodeUnknownExit(MuseSettingsFileFromJson);
104769
+ const encodeSettingsFile = Schema$1.encodeSync(MuseSettingsFileFromJson);
104770
+ /** The user's settings document, or an empty one when there is nothing usable to read. */
104771
+ const readSourceSettings = Effect.fnUntraced(function* (settingsPath) {
104772
+ const contents = yield* (yield* FileSystem.FileSystem).readFileString(settingsPath).pipe(Effect.orElseSucceed(() => void 0));
104773
+ if (contents === void 0) return {};
104774
+ const decoded = decodeSettingsFile(contents);
104775
+ return decoded._tag === "Success" ? decoded.value : {};
104776
+ });
104777
+ /**
104778
+ * Prepare the overlay and return the environment a turn must add to reach it.
104779
+ *
104780
+ * Rebuilt every turn: the endpoint and the bearer token are per session, and a
104781
+ * stale settings file would point the CLI at a credential that no longer
104782
+ * authorizes anything.
104783
+ */
104784
+ const prepareMuseConfigOverlay = Effect.fn("prepareMuseConfigOverlay")(function* (input) {
104785
+ const fileSystem = yield* FileSystem.FileSystem;
104786
+ const path = yield* Path.Path;
104787
+ const sourceMuseDir = path.join(input.sourceConfigHome, MUSE_CONFIG_DIRNAME);
104788
+ const overlayMuseDir = path.join(input.overlayRoot, MUSE_CONFIG_DIRNAME);
104789
+ yield* fileSystem.makeDirectory(overlayMuseDir, {
104790
+ recursive: true,
104791
+ mode: OVERLAY_DIRECTORY_MODE
104792
+ });
104793
+ yield* fileSystem.chmod(input.overlayRoot, OVERLAY_DIRECTORY_MODE).pipe(Effect.ignore);
104794
+ yield* fileSystem.chmod(overlayMuseDir, OVERLAY_DIRECTORY_MODE).pipe(Effect.ignore);
104795
+ const sourceEntries = yield* fileSystem.readDirectory(sourceMuseDir).pipe(Effect.orElseSucceed(() => []));
104796
+ for (const entry of sourceEntries) {
104797
+ if (entry === MUSE_SETTINGS_FILENAME) continue;
104798
+ const link = path.join(overlayMuseDir, entry);
104799
+ if (!(yield* fileSystem.readLink(link).pipe(Effect.as(true), Effect.orElseSucceed(() => false))) && (yield* fileSystem.exists(link))) continue;
104800
+ yield* fileSystem.remove(link, {
104801
+ recursive: true,
104802
+ force: true
104803
+ });
104804
+ yield* fileSystem.symlink(path.join(sourceMuseDir, entry), link).pipe(Effect.ignore);
104805
+ }
104806
+ const sourceSettings = yield* readSourceSettings(path.join(sourceMuseDir, MUSE_SETTINGS_FILENAME));
104807
+ const sourceServers = sourceSettings.mcpServers;
104808
+ const mergedServers = typeof sourceServers === "object" && sourceServers !== null && !Array.isArray(sourceServers) ? { ...sourceServers } : {};
104809
+ const external = toMuseMcpServers(input.externalServers ?? {}, /* @__PURE__ */ new Set([input.mcpServer.name]));
104810
+ Object.assign(mergedServers, external.entries);
104811
+ mergedServers[input.mcpServer.name] = {
104812
+ type: "http",
104813
+ url: input.mcpServer.url,
104814
+ headers: { Authorization: input.mcpServer.authorizationHeader }
104815
+ };
104816
+ const settings = {
104817
+ ...sourceSettings,
104818
+ schema_version: sourceSettings.schema_version ?? 1,
104819
+ mcpServers: mergedServers
104820
+ };
104821
+ const settingsPath = path.join(overlayMuseDir, MUSE_SETTINGS_FILENAME);
104822
+ yield* fileSystem.writeFileString(settingsPath, `${encodeSettingsFile(settings)}\n`, { mode: OVERLAY_SETTINGS_MODE });
104823
+ yield* fileSystem.chmod(settingsPath, OVERLAY_SETTINGS_MODE).pipe(Effect.ignore);
104824
+ return {
104825
+ skippedServers: external.skipped,
104826
+ environment: {
104827
+ XDG_CONFIG_HOME: input.overlayRoot,
104828
+ MUSE_AUTH_PATH: path.join(sourceMuseDir, MUSE_AUTH_FILENAME)
104829
+ }
104830
+ };
104831
+ });
104832
+ /**
104833
+ * Drop what the overlay generated, and nothing else.
104834
+ *
104835
+ * The links are P4Code's to remove; a real file under the overlay is something
104836
+ * Muse wrote during the session — a trust decision, an OAuth token — and it
104837
+ * stays, so the next session on this thread still has it. The directory goes
104838
+ * only when removing the generated file and the links leaves it empty.
104839
+ */
104840
+ const clearMuseConfigOverlay = Effect.fn("clearMuseConfigOverlay")(function* (overlayRoot) {
104841
+ const fileSystem = yield* FileSystem.FileSystem;
104842
+ const path = yield* Path.Path;
104843
+ const overlayMuseDir = path.join(overlayRoot, MUSE_CONFIG_DIRNAME);
104844
+ yield* fileSystem.remove(path.join(overlayMuseDir, MUSE_SETTINGS_FILENAME), { force: true }).pipe(Effect.ignore);
104845
+ const entries = yield* fileSystem.readDirectory(overlayMuseDir).pipe(Effect.orElseSucceed(() => []));
104846
+ for (const entry of entries) {
104847
+ const entryPath = path.join(overlayMuseDir, entry);
104848
+ if (!(yield* fileSystem.readLink(entryPath).pipe(Effect.as(true), Effect.orElseSucceed(() => false)))) continue;
104849
+ yield* fileSystem.remove(entryPath, { force: true }).pipe(Effect.ignore);
104850
+ }
104851
+ yield* fileSystem.remove(overlayMuseDir).pipe(Effect.ignore);
104852
+ yield* fileSystem.remove(overlayRoot).pipe(Effect.ignore);
104853
+ });
104258
104854
  //#endregion
104259
104855
  //#region src/provider/muse/MuseExecRuntime.ts
104260
104856
  /**
@@ -104547,6 +105143,11 @@ function makeMuseAdapter(museSettings, options) {
104547
105143
  const fileSystem = yield* FileSystem.FileSystem;
104548
105144
  const path = yield* Path.Path;
104549
105145
  const serverConfig = yield* Effect.service(ServerConfig$1);
105146
+ /**
105147
+ * Where a thread's generated Muse configuration lives. One directory per
105148
+ * thread because the bearer token inside it is per session.
105149
+ */
105150
+ const overlayRootFor = (threadId) => path.join(serverConfig.stateDir, "muse", "config", threadId);
104550
105151
  const sessions = /* @__PURE__ */ new Map();
104551
105152
  const runtimeEventPubSub = yield* PubSub.unbounded();
104552
105153
  const nowIso = Effect.map(DateTime.now, DateTime.formatIso);
@@ -104664,6 +105265,26 @@ function makeMuseAdapter(museSettings, options) {
104664
105265
  runtimeMode: ctx.session.runtimeMode,
104665
105266
  imagePaths
104666
105267
  });
105268
+ const mcpSession = readMcpProviderSession(input.threadId);
105269
+ const overlay = mcpSession ? yield* prepareMuseConfigOverlay({
105270
+ overlayRoot: overlayRootFor(input.threadId),
105271
+ sourceConfigHome: resolveXdgConfigHome(environment),
105272
+ mcpServer: {
105273
+ name: P4CODE_MCP_SERVER_NAME,
105274
+ url: mcpSession.endpoint,
105275
+ authorizationHeader: mcpSession.authorizationHeader
105276
+ },
105277
+ externalServers: options?.resolveMcpServers === void 0 ? {} : yield* options.resolveMcpServers
105278
+ }).pipe(Effect.provideService(FileSystem.FileSystem, fileSystem), Effect.provideService(Path.Path, path), Effect.orElseSucceed(() => void 0)) : void 0;
105279
+ if (overlay && overlay.skippedServers.length > 0) yield* Effect.logWarning("Muse cannot express these MCP servers", {
105280
+ threadId: input.threadId,
105281
+ servers: overlay.skippedServers,
105282
+ transport: "sse"
105283
+ });
105284
+ const turnEnvironment = {
105285
+ ...environment,
105286
+ ...overlay?.environment
105287
+ };
104667
105288
  const state = makeMuseExecTurnState();
104668
105289
  const mappingContext = {
104669
105290
  provider: PROVIDER$1,
@@ -104750,7 +105371,7 @@ function makeMuseAdapter(museSettings, options) {
104750
105371
  binaryPath: museSettings.binaryPath || "muse",
104751
105372
  args,
104752
105373
  cwd: ctx.cwd,
104753
- environment,
105374
+ environment: turnEnvironment,
104754
105375
  onLine: handleLine
104755
105376
  }).pipe(Effect.provideService(Scope.Scope, turnScope), Effect.provideService(ChildProcessSpawner.ChildProcessSpawner, spawner), Effect.tapError(() => finishTurn), Effect.tap((run) => Effect.sync(() => {
104756
105377
  runStarted = true;
@@ -104836,6 +105457,7 @@ function makeMuseAdapter(museSettings, options) {
104836
105457
  yield* ctx.activeTurn.run.interrupt.pipe(Effect.ignore);
104837
105458
  ctx.activeTurn = void 0;
104838
105459
  }
105460
+ yield* clearMuseConfigOverlay(overlayRootFor(ctx.threadId)).pipe(Effect.provideService(FileSystem.FileSystem, fileSystem), Effect.provideService(Path.Path, path), Effect.ignore);
104839
105461
  const updatedAt = yield* nowIso;
104840
105462
  ctx.session = {
104841
105463
  ...ctx.session,
@@ -105135,6 +105757,7 @@ const MuseDriver = {
105135
105757
  const spawner = yield* ChildProcessSpawner.ChildProcessSpawner;
105136
105758
  const fileSystem = yield* FileSystem.FileSystem;
105137
105759
  const path = yield* Path.Path;
105760
+ const serverConfig = yield* ServerConfig$1;
105138
105761
  const serverSettings = yield* ServerSettingsService;
105139
105762
  const processEnv = mergeProviderInstanceEnvironment(environment);
105140
105763
  const continuationIdentity = defaultProviderContinuationIdentity({
@@ -105157,9 +105780,16 @@ const MuseDriver = {
105157
105780
  });
105158
105781
  const adapter = yield* makeMuseAdapter(effectiveConfig, {
105159
105782
  environment: processEnv,
105160
- instanceId
105783
+ instanceId,
105784
+ resolveMcpServers: (yield* McpRegistry).resolveForSession
105161
105785
  });
105162
105786
  const textGeneration = yield* makeMuseTextGeneration(effectiveConfig, processEnv);
105787
+ const checkProvider = serverSettings.getSettings.pipe(Effect.map((settings) => settings.disabledSkills), Effect.orElseSucceed(() => [])).pipe(Effect.flatMap((disabledSkills) => checkMuseProviderStatus(effectiveConfig, processEnv).pipe(Effect.flatMap((draft) => withProviderSkills(draft, {
105788
+ driver: DRIVER_KIND$1,
105789
+ cwd: serverConfig.cwd,
105790
+ environment: processEnv,
105791
+ disabledSkills
105792
+ })))));
105163
105793
  const snapshotSettings = makeProviderSnapshotSettingsSource(effectiveConfig, serverSettings);
105164
105794
  const snapshot = yield* makeManagedServerProvider({
105165
105795
  maintenanceCapabilities,
@@ -105167,7 +105797,7 @@ const MuseDriver = {
105167
105797
  streamSettings: snapshotSettings.streamSettings,
105168
105798
  haveSettingsChanged: haveProviderSnapshotSettingsChanged,
105169
105799
  initialSnapshot: (settings) => buildInitialMuseProviderSnapshot(settings.provider).pipe(Effect.map(stampIdentity)),
105170
- checkProvider: checkMuseProviderStatus(effectiveConfig, processEnv).pipe(Effect.map(stampIdentity), Effect.provideService(ChildProcessSpawner.ChildProcessSpawner, spawner), Effect.provideService(FileSystem.FileSystem, fileSystem), Effect.provideService(Path.Path, path)),
105800
+ checkProvider: checkProvider.pipe(Effect.map(stampIdentity), Effect.provideService(ChildProcessSpawner.ChildProcessSpawner, spawner), Effect.provideService(FileSystem.FileSystem, fileSystem), Effect.provideService(Path.Path, path)),
105171
105801
  enrichSnapshot: ({ settings, snapshot: currentSnapshot, publishSnapshot }) => enrichMuseSnapshot({
105172
105802
  snapshot: currentSnapshot,
105173
105803
  maintenanceCapabilities,
@@ -106918,6 +107548,8 @@ const BUILT_IN_DRIVERS = [
106918
107548
  defaultConfig: () => decodeOpenCodeSettings({}),
106919
107549
  create: ({ instanceId, displayName, accentColor, environment, enabled, config }) => Effect.gen(function* () {
106920
107550
  const openCodeRuntime = yield* OpenCodeRuntime;
107551
+ const fileSystem = yield* FileSystem.FileSystem;
107552
+ const path = yield* Path.Path;
106921
107553
  const serverConfig = yield* ServerConfig$1;
106922
107554
  const httpClient = yield* HttpClient.HttpClient;
106923
107555
  const serverSettings = yield* ServerSettingsService;
@@ -106948,7 +107580,12 @@ const BUILT_IN_DRIVERS = [
106948
107580
  ...eventLoggers.native ? { nativeEventLogger: eventLoggers.native } : {}
106949
107581
  });
106950
107582
  const textGeneration = yield* makeOpenCodeTextGeneration(effectiveConfig, processEnv);
106951
- const checkProvider = checkOpenCodeProviderStatus(effectiveConfig, serverConfig.cwd, processEnv).pipe(Effect.map(stampIdentity), Effect.provideService(OpenCodeRuntime, openCodeRuntime));
107583
+ const checkProvider = serverSettings.getSettings.pipe(Effect.map((settings) => settings.disabledSkills), Effect.orElseSucceed(() => [])).pipe(Effect.flatMap((disabledSkills) => checkOpenCodeProviderStatus(effectiveConfig, serverConfig.cwd, processEnv).pipe(Effect.flatMap((draft) => withProviderSkills(draft, {
107584
+ driver: DRIVER_KIND,
107585
+ cwd: serverConfig.cwd,
107586
+ environment: processEnv,
107587
+ disabledSkills
107588
+ })))), Effect.map(stampIdentity), Effect.provideService(OpenCodeRuntime, openCodeRuntime), Effect.provideService(FileSystem.FileSystem, fileSystem), Effect.provideService(Path.Path, path));
106952
107589
  const snapshotSettings = makeProviderSnapshotSettingsSource(effectiveConfig, serverSettings);
106953
107590
  const snapshot = yield* makeManagedServerProvider({
106954
107591
  maintenanceCapabilities,
@@ -111108,7 +111745,13 @@ const make$4 = Effect.gen(function* () {
111108
111745
  projects: project ? [project] : [],
111109
111746
  chatWorkspaceDir: serverConfig.chatWorkspaceDir
111110
111747
  });
111111
- return [userSkillsDir, ...cwd ? [path.join(cwd, ".claude", "skills")] : []];
111748
+ const [sharedUserDir, sharedProjectDir] = yield* sharedSkillDirectories(cwd ?? void 0);
111749
+ return [
111750
+ ...sharedUserDir !== void 0 && sharedUserDir !== userSkillsDir ? [sharedUserDir] : [],
111751
+ userSkillsDir,
111752
+ ...sharedProjectDir === void 0 ? [] : [sharedProjectDir],
111753
+ ...cwd ? [path.join(cwd, ".claude", "skills")] : []
111754
+ ];
111112
111755
  });
111113
111756
  const resolveThread = Effect.fnUntraced(function* (threadId) {
111114
111757
  return yield* projectionSnapshotQuery.getThreadDetailById(threadId).pipe(Effect.map(Option.getOrUndefined));