pi-profile-switch 0.4.3 → 0.4.4

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 CHANGED
@@ -59,7 +59,6 @@ In the TUI, the `/profile` command family manages everything in-session:
59
59
  | `/profile use <name>` / `/profile reload` | Switch / reload without restarting (rollback on failure) |
60
60
  | `/profile create\|edit\|delete\|duplicate` | Guided profile CRUD (TUI only) |
61
61
  | `/profile customize` / `/profile reset` | Narrow the active profile for this session only |
62
- | `/mcp enable\|disable <server>` | Toggle MCP servers in the active profile |
63
62
 
64
63
  All commands work in non-interactive modes (`--mode rpc|print|json`); CRUD wizards are TUI-only.
65
64
 
package/README.zh-CN.md CHANGED
@@ -59,7 +59,6 @@ Profile 只**引用**资源,从不复制资源。已安装的包和标准目
59
59
  | `/profile use <name>` / `/profile reload` | 会话内切换 / 重载(失败自动回滚) |
60
60
  | `/profile create\|edit\|delete\|duplicate` | 向导式 profile 增删改(仅 TUI) |
61
61
  | `/profile customize` / `/profile reset` | 仅本次会话收窄活动 profile |
62
- | `/mcp enable\|disable <server>` | 在活动 profile 中开关 MCP server |
63
62
 
64
63
  非交互模式(`--mode rpc|print|json`)下命令同样生效;CRUD 向导仅 TUI 可用。
65
64
 
@@ -21,8 +21,6 @@ import {
21
21
  runProfileDuplicateWizard,
22
22
  runProfileEditWizard,
23
23
  } from "../../src/switching/profile-wizard.ts";
24
- import { probeAdapterPresence } from "../../src/mcp-coordination.ts";
25
- import { setMcpServerEnabled } from "../../src/switching/mcp-toggle.ts";
26
24
  import { buildStatusReport, formatStatusMarkdown } from "../../src/switching/status.ts";
27
25
  import { switchProfile, type SwitchDeps } from "../../src/switching/switch-profile.ts";
28
26
  import { getGlobalStateDir } from "../../src/workspace.ts";
@@ -53,9 +51,6 @@ import { getGlobalStateDir } from "../../src/workspace.ts";
53
51
  * is stale afterwards.
54
52
  * - list/status ship structured `details` payloads (`{kind, profiles}` /
55
53
  * `{kind, report}`) for RPC consumers (ticket 11).
56
- * - `/mcp enable|disable <server>`: edit the active profile's mcp array in
57
- * its owning catalog, then reload (ticket 10). Fails clearly with the
58
- * adapter absent; notifies before the reload (stale context after).
59
54
  *
60
55
  * Pi re-executes this module on reload, so post-reload state is established
61
56
  * exclusively through `session_start` — nothing stale survives.
@@ -383,69 +378,4 @@ export default function piProfileExtension(pi: ExtensionAPI): void {
383
378
  }
384
379
  },
385
380
  });
386
-
387
- // Persistent profile-scoped MCP toggles (ticket 10): edit the active
388
- // profile's mcp array in its owning catalog, then reload so the runtime
389
- // and the republished allowlist match. The adapter's own configuration
390
- // (mcp.json files) is never written.
391
- pi.registerCommand("mcp", {
392
- description: "pi-profile: /mcp enable <server> | /mcp disable <server>",
393
- handler: async (args, ctx) => {
394
- const notify = (message: string, level: "info" | "warning" | "error") => {
395
- try {
396
- ctx.ui?.notify(message, level);
397
- } catch {
398
- // stale context after reload — see /profile
399
- }
400
- };
401
- const [action, server] = args.trim().split(/\s+/).filter(Boolean);
402
- if (!["enable", "disable"].includes(action ?? "") || server === undefined) {
403
- notify("usage: /mcp enable <server> | /mcp disable <server>", "error");
404
- return;
405
- }
406
- try {
407
- const plan = await readLaunchPlanFile(runtimeDir);
408
- if (plan?.agentDir === undefined) {
409
- notify("pi-profile: launch plan is missing agentDir — cannot toggle MCP servers", "error");
410
- return;
411
- }
412
- if (!probeAdapterPresence(pi.events)) {
413
- throw new Error(
414
- "pi-mcp-adapter is not active in this session — /mcp enable|disable requires it " +
415
- "(select the adapter in the profile's extensions).",
416
- );
417
- }
418
- const result = await setMcpServerEnabled(
419
- { realAgentDir: plan.agentDir, cwd: ctx.cwd, profile: { name: plan.profile, source: plan.source } },
420
- server,
421
- action === "enable",
422
- );
423
- if (!result.changed) {
424
- notify(`MCP server "${server}" is already ${action}d in profile "${plan.profile}"`, "info");
425
- return;
426
- }
427
- // Notify BEFORE the reload: this context is stale afterwards.
428
- notify(
429
- `${action}d MCP server "${server}" in profile "${plan.profile}" (mcps: [${result.mcps.join(", ")}]); reloading`,
430
- "info",
431
- );
432
- const switched = await switchProfile(plan.profile, {
433
- runtimeDir,
434
- realAgentDir: plan.agentDir,
435
- cwd: ctx.cwd,
436
- waitForIdle: () => ctx.waitForIdle(),
437
- reload: () => ctx.reload(),
438
- // Same staleness probe as /profile: interactive Pi
439
- // swallows reload refusals; a live context afterwards
440
- // means the reload never ran.
441
- assertStale: () => {
442
- void ctx.cwd;
443
- },
444
- }, { reloadCurrent: true });
445
- for (const warning of switched.warnings) notify(warning, "warning");
446
- } catch (error) {
447
- notify(error instanceof Error ? error.message : String(error), "error");
448
- }
449
- },
450
- });
451
381
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-profile-switch",
3
- "version": "0.4.3",
3
+ "version": "0.4.4",
4
4
  "description": "Named profiles for Pi: reference skills, extensions, MCP servers, and tools per workflow, switched without restarting.",
5
5
  "type": "module",
6
6
  "keywords": [
@@ -1,71 +0,0 @@
1
- /**
2
- * McpToggle: persistent, profile-scoped `/mcp enable|disable` (ticket 10).
3
- *
4
- * The profile's `mcps` array in its OWNING catalog is the profile-scoped
5
- * state store (ticket 04 established that pi-mcp-adapter@2.33.0 has no
6
- * allowlist/profile-state API — ADR-0002's assumed store does not exist;
7
- * pi-profile owns the contract). Runtime effect flows through the standard
8
- * rewrite-settings-and-reload path: the post-reload session_start
9
- * republishes the allowlist over the coordination channel.
10
- *
11
- * Invariants:
12
- * - Enable accepts only adapter-discovered names (fail fast on typos);
13
- * disable also removes names the adapter no longer reports (stale
14
- * cleanup).
15
- * - The built-in default profile has no catalog entry — toggling it is a
16
- * clear error, not a synthetic write.
17
- * - Only the ACTIVE profile's array changes; other profiles and the
18
- * adapter's own configuration (global mcp.json, project .pi/mcp.json)
19
- * are never modified.
20
- */
21
-
22
- import { discoverAdapterServerNames } from "../mcp-config.ts";
23
- import { CatalogError, DEFAULT_PROFILE_NAME, type ProfileDefinition } from "../profile-catalog.ts";
24
- import { readTrustInputs as readTrust } from "../launcher/initial-profile.ts";
25
- import { catalogStore, readCatalogScope, type CatalogScope } from "./profile-crud.ts";
26
-
27
- export async function setMcpServerEnabled(
28
- input: { realAgentDir: string; cwd: string; profile: { name: string; source: string } },
29
- server: string,
30
- enabled: boolean,
31
- ): Promise<{ mcps: string[]; changed: boolean }> {
32
- if (input.profile.name === DEFAULT_PROFILE_NAME || input.profile.source === "builtin") {
33
- throw new CatalogError(
34
- `the built-in default profile has no catalog entry — create a named profile (/profile create) to toggle MCP servers`,
35
- );
36
- }
37
- const scope = input.profile.source as CatalogScope;
38
- if (scope !== "global" && scope !== "project") {
39
- throw new CatalogError(`profile "${input.profile.name}" has no writable owning catalog (source: ${input.profile.source})`);
40
- }
41
-
42
- const { projectTrusted } = await readTrust({ agentDir: input.realAgentDir, cwd: input.cwd });
43
- if (scope === "project" && !projectTrusted) {
44
- throw new CatalogError(`project catalog is unavailable: ${input.cwd} is not trusted`);
45
- }
46
- const discovered = await discoverAdapterServerNames(input.realAgentDir, projectTrusted ? input.cwd : undefined);
47
- if (enabled && !discovered.includes(server)) {
48
- throw new CatalogError(
49
- `unknown MCP server "${server}" — adapter discovered: [${discovered.join(", ") || "(none)"}]`,
50
- );
51
- }
52
-
53
- const definitions = await readCatalogScope(input, scope);
54
- const definition = definitions.get(input.profile.name);
55
- if (definition === undefined) {
56
- throw new CatalogError(`profile "${input.profile.name}" not found in the ${scope} catalog`);
57
- }
58
-
59
- const current = definition.mcps ?? [];
60
- if (enabled === current.includes(server)) {
61
- return { mcps: current, changed: false }; // already in the requested state
62
- }
63
- const next = enabled ? [...current, server] : current.filter((name) => name !== server);
64
- // Drop the key entirely when empty (exactOptionalPropertyTypes; a
65
- // written `mcps: undefined` would also misrepresent the definition).
66
- const rest = { ...definition };
67
- delete rest.mcps;
68
- const updated: ProfileDefinition = next.length > 0 ? { ...rest, mcps: next } : rest;
69
- await catalogStore(input, scope).upsert(input.profile.name, updated);
70
- return { mcps: next, changed: true };
71
- }