pi-profile-switch 0.3.1 → 0.4.2

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.
Files changed (54) hide show
  1. package/README.md +31 -36
  2. package/README.zh-CN.md +31 -36
  3. package/bin/pi-profile.js +11 -0
  4. package/bin/pi-profile.ts +65 -0
  5. package/bin/postinstall.d.ts +13 -0
  6. package/bin/postinstall.js +88 -0
  7. package/defaults/profiles.json +18 -0
  8. package/examples/profiles.json +31 -5
  9. package/extensions/pi-profile/index.ts +451 -0
  10. package/package.json +10 -13
  11. package/schemas/profiles.schema.json +22 -24
  12. package/src/extension-discovery.ts +347 -0
  13. package/src/json-file.ts +1 -21
  14. package/src/launcher/args.ts +57 -0
  15. package/src/launcher/discovery.ts +64 -0
  16. package/src/launcher/initial-profile.ts +179 -0
  17. package/src/launcher/model-check.ts +52 -0
  18. package/src/launcher/runtime-cleanup.ts +85 -0
  19. package/src/launcher/spawn.ts +82 -0
  20. package/src/mcp-config.ts +37 -153
  21. package/src/mcp-coordination.ts +29 -10
  22. package/src/profile-catalog-store.ts +31 -12
  23. package/src/profile-catalog.ts +45 -93
  24. package/src/profile-resolver.ts +239 -245
  25. package/src/project-trust.ts +82 -0
  26. package/src/runtime-state-store.ts +25 -41
  27. package/src/settings-generator.ts +541 -0
  28. package/src/skill-registry.ts +94 -0
  29. package/src/switching/apply-plan.ts +197 -0
  30. package/src/switching/customize.ts +62 -33
  31. package/src/switching/list-profiles.ts +9 -6
  32. package/src/switching/mcp-toggle.ts +14 -26
  33. package/src/switching/profile-crud.ts +31 -24
  34. package/src/switching/profile-wizard.ts +21 -49
  35. package/src/switching/status.ts +142 -72
  36. package/src/switching/switch-profile.ts +219 -0
  37. package/src/switching/tool-references.ts +40 -0
  38. package/src/workspace.ts +57 -0
  39. package/LICENSE +0 -21
  40. package/examples/profiles.example.json +0 -74
  41. package/extensions/pi-profile-switch/index.ts +0 -778
  42. package/src/adapter-presence.ts +0 -75
  43. package/src/default-profiles.ts +0 -59
  44. package/src/mcp-overlay-file.ts +0 -35
  45. package/src/mcp-overlay.ts +0 -122
  46. package/src/model-selection.ts +0 -64
  47. package/src/name-matching.ts +0 -50
  48. package/src/profile-badge.ts +0 -142
  49. package/src/profile-presets.ts +0 -61
  50. package/src/skill-selection.ts +0 -81
  51. package/src/startup-mcp-scope.ts +0 -271
  52. package/src/startup-selection.ts +0 -201
  53. package/src/switching/activate-profile.ts +0 -144
  54. package/src/switching/apply-profile.ts +0 -131
@@ -1,144 +0,0 @@
1
- /**
2
- * SwitchProfile: the in-session activation orchestrator (ADR-0007).
3
- *
4
- * There is no reload and no generated-settings rewrite. Activating a profile
5
- * means: resolve it against Pi's live resources, apply the runtime parts
6
- * (model, tools, MCP allowlist), and remember the choice. Skills and
7
- * instructions need no action here — they are applied by the next
8
- * `before_agent_start` from the returned selection.
9
- *
10
- * Flow:
11
- * 1. load the catalog (project entries only when Pi reports the project
12
- * trusted) and resolve the name — unknown names fail with candidates
13
- * 2. resolve the selection against the live resources; a declared MCP
14
- * intent that cannot be satisfied fails here
15
- * 3. validate the model preset (exists, has auth) — still nothing applied
16
- * 4. persist the choice to the target profile's scope state file
17
- * 5. apply model/thinking/tools/MCP; a failure reports loudly
18
- *
19
- * Startup activation passes `persist: false` (the flag selection must not
20
- * overwrite the saved one) and `overlay: null` (a stored overlay never
21
- * outlives its runtime).
22
- */
23
-
24
- import { ProfileCatalog } from "../profile-catalog.ts";
25
- import { decidePreset, type SessionChoices } from "../model-selection.ts";
26
- import { resolveSelection, type LiveResources, type ResolvedSelection } from "../profile-resolver.ts";
27
- import type { ExplicitDeclarations } from "../startup-selection.ts";
28
- import { RuntimeStateStore, stateDirFor, type RuntimeOverlay } from "../runtime-state-store.ts";
29
- import { applySelection, validateSelection, type ApplySurface } from "./apply-profile.ts";
30
-
31
- export class ActivationError extends Error {
32
- constructor(message: string) {
33
- super(message);
34
- this.name = "ActivationError";
35
- }
36
- }
37
-
38
- export interface ActivationDeps {
39
- /** The user's real agent dir (e.g. ~/.pi/agent). */
40
- agentDir: string;
41
- /** The project working directory. */
42
- cwd: string;
43
- /** Pi's trust decision for `cwd` (`ctx.isProjectTrusted()`). */
44
- projectTrusted: boolean;
45
- /** The live resource view loaded by the caller. */
46
- live: LiveResources;
47
- /** The narrow Pi surface the selection is applied to. */
48
- surface: ApplySurface;
49
- /** Inputs for the model-preset precedence decision (decided after the
50
- * profile resolves, since it depends on the declared model). */
51
- presetInputs: { explicit: ExplicitDeclarations; session: SessionChoices; force: boolean };
52
- }
53
-
54
- export interface ActivationResult {
55
- selection: ResolvedSelection;
56
- /** The overlay this activation applied; absent when the runtime runs the
57
- * profile exactly as it is declared. Callers surface it (the footer badge)
58
- * instead of re-reading the state file. */
59
- overlay?: RuntimeOverlay;
60
- }
61
-
62
- /** Resolves a profile against the live resources without applying it.
63
- * Throws ActivationError for unknown names and SelectionError for declared
64
- * MCP intents that cannot be satisfied. A CLI-declared
65
- * `--tools`/`--exclude-tools` suppresses the profile's tool selection
66
- * unless the user made an explicit `/profile use` choice. */
67
- export async function resolveProfileSelection(
68
- name: string,
69
- deps: Pick<ActivationDeps, "agentDir" | "cwd" | "projectTrusted" | "live" | "presetInputs">,
70
- overlay?: RuntimeOverlay,
71
- ): Promise<ResolvedSelection> {
72
- const catalog = await ProfileCatalog.load(deps.agentDir, {
73
- projectDir: deps.projectTrusted ? deps.cwd : undefined,
74
- });
75
- const profile = catalog.resolve(name);
76
- if (profile === undefined) {
77
- throw new ActivationError(
78
- `unknown profile ${JSON.stringify(name)} — available: [${catalog.list().map((entry) => entry.name).join(", ")}]`,
79
- );
80
- }
81
- const suppressTools = !deps.presetInputs.force && deps.presetInputs.explicit.tools;
82
- return resolveSelection({ profile, overlay, live: deps.live, suppressTools });
83
- }
84
-
85
- /** Drops a stale saved selection from the scope that no longer owns it. A
86
- * project state file left behind by a previous project profile shadows the
87
- * new global choice on the next startup (project state wins), so the switch
88
- * path clears it; the scope's overlay stays, because that belongs to the
89
- * profile it was created for. */
90
- async function clearOtherSelection(stateDir: string): Promise<void> {
91
- const store = new RuntimeStateStore(stateDir);
92
- if ((await store.read()).activeProfile !== undefined) {
93
- await store.update({ otherActiveProfile: undefined });
94
- }
95
- }
96
-
97
- /** Activates a profile: resolve, validate, optionally persist, apply. */
98
- export async function activateProfile(
99
- name: string,
100
- deps: ActivationDeps,
101
- options?: { overlay?: RuntimeOverlay | null; persist?: boolean },
102
- ): Promise<ActivationResult> {
103
- const overlay = options?.overlay ?? undefined;
104
- const selection = await resolveProfileSelection(name, deps, overlay);
105
- const preset = decidePreset({ model: selection.model, ...deps.presetInputs });
106
-
107
- // Validate before touching anything: the model preset is the only
108
- // fallible runtime step.
109
- const validationError = validateSelection(selection, deps.surface, preset);
110
- if (validationError !== undefined) {
111
- throw new ActivationError(validationError);
112
- }
113
-
114
- if (options?.persist !== false) {
115
- // Exactly one scope holds the saved selection: the project state file
116
- // for a project profile, the global one otherwise (including the
117
- // built-in `default`). A project profile is only reachable in a
118
- // trusted project, so an untrusted project is skipped exactly like
119
- // its catalog read — its state file stays untouched (ADR-0007).
120
- const ownDir = stateDirFor(selection.source, deps);
121
- const projectDir = stateDirFor("project", deps);
122
- const writable = selection.source !== "project" || deps.projectTrusted;
123
- if (writable) {
124
- await new RuntimeStateStore(ownDir).update({
125
- activeProfile: selection.name,
126
- overlay: overlay ?? undefined,
127
- });
128
- // The scope that owned the previous selection must let go of it,
129
- // or the next startup restores it: project state wins over global.
130
- // An untrusted project is never read or written here either.
131
- const otherDir = selection.source === "project" ? stateDirFor("global", deps) : projectDir;
132
- if (otherDir !== ownDir && (otherDir !== projectDir || deps.projectTrusted)) {
133
- await clearOtherSelection(otherDir);
134
- }
135
- }
136
- }
137
-
138
- const result = await applySelection({ selection, surface: deps.surface, preset });
139
- if (!result.applied) {
140
- throw new ActivationError(result.error ?? `profile "${name}" could not be applied`);
141
- }
142
-
143
- return { selection, ...(overlay === undefined ? {} : { overlay }) };
144
- }
@@ -1,131 +0,0 @@
1
- /**
2
- * ApplyProfile: applies a resolved selection to the running Pi.
3
- *
4
- * Replaces the launch-plan application of the host architecture: there is no
5
- * plan file and no reload. Application is ordered so that the only
6
- * fallible step — the model preset — runs first; a failure leaves the
7
- * runtime untouched and the caller keeps the previous state.
8
- *
9
- * Steps:
10
- * 1. validate: the declared model exists and has configured auth.
11
- * 2. model: `setModel` + `setThinkingLevel` (when the preset applies).
12
- * 3. tools: `setActiveTools` with the resolved active set. Literals the
13
- * live registry does not provide yet stay in `pendingTools` and are
14
- * retried by the caller each turn (MCP and extension tools register
15
- * after session start).
16
- * 4. mcp: publish the runtime server allowlist to pi-mcp-adapter.
17
- *
18
- * Dependency-injected against a narrow surface so unit tests never need a
19
- * real Pi.
20
- */
21
-
22
- import {
23
- MCP_ALLOWLIST_EVENT,
24
- MCP_ALLOWLIST_VERSION,
25
- } from "../mcp-coordination.ts";
26
- import type { PresetDecisions } from "../model-selection.ts";
27
- import type { ResolvedSelection } from "../profile-resolver.ts";
28
-
29
- /** The narrow slice of ExtensionAPI/Context the application needs. */
30
- export interface ApplySurface {
31
- getAllTools(): Array<{ name: string }>;
32
- setActiveTools(names: string[]): void;
33
- modelRegistry: {
34
- find(provider: string, id: string): unknown | undefined;
35
- hasConfiguredAuth(model: unknown): boolean;
36
- };
37
- setModel(model: unknown): Promise<boolean>;
38
- setThinkingLevel(level: string): void;
39
- events: { emit(channel: string, data: unknown): void };
40
- }
41
-
42
- export interface ApplyResult {
43
- applied: boolean;
44
- /** Present when nothing was applied. */
45
- error?: string;
46
- warnings: string[];
47
- /** Tool literals still missing from the live registry. */
48
- pendingTools: string[];
49
- }
50
-
51
- /** Checks everything that can be checked before mutating the runtime. */
52
- export function validateSelection(
53
- selection: ResolvedSelection,
54
- surface: ApplySurface,
55
- preset: PresetDecisions,
56
- ): string | undefined {
57
- if (selection.model === undefined || !preset.model) return undefined;
58
- const model = surface.modelRegistry.find(selection.model.provider, selection.model.id);
59
- if (model === undefined) {
60
- return `profile "${selection.name}": declared model ${selection.model.provider}/${selection.model.id} not found`;
61
- }
62
- if (!surface.modelRegistry.hasConfiguredAuth(model)) {
63
- return `profile "${selection.name}": model ${selection.model.provider}/${selection.model.id} has no configured auth`;
64
- }
65
- return undefined;
66
- }
67
-
68
- /** Applies the selection. Validation runs first; on failure nothing is
69
- * applied and the runtime keeps its previous state. */
70
- export async function applySelection(input: {
71
- selection: ResolvedSelection;
72
- surface: ApplySurface;
73
- preset: PresetDecisions;
74
- }): Promise<ApplyResult> {
75
- const { selection, surface, preset } = input;
76
- const error = validateSelection(selection, surface, preset);
77
- if (error !== undefined) {
78
- return { applied: false, error, warnings: [], pendingTools: [] };
79
- }
80
-
81
- if (selection.model !== undefined && preset.model) {
82
- const model = surface.modelRegistry.find(selection.model.provider, selection.model.id);
83
- const applied = await surface.setModel(model);
84
- if (!applied) {
85
- return {
86
- applied: false,
87
- error: `profile "${selection.name}": model ${selection.model.provider}/${selection.model.id} could not be activated`,
88
- warnings: [],
89
- pendingTools: [],
90
- };
91
- }
92
- if (selection.model.thinkingLevel !== undefined && preset.thinking) {
93
- surface.setThinkingLevel(selection.model.thinkingLevel);
94
- }
95
- }
96
-
97
- if (selection.tools !== undefined) {
98
- surface.setActiveTools(selection.tools);
99
- }
100
-
101
- if (selection.mcp !== undefined) {
102
- surface.events.emit(MCP_ALLOWLIST_EVENT, {
103
- version: MCP_ALLOWLIST_VERSION,
104
- profile: selection.name,
105
- servers: selection.mcp,
106
- });
107
- }
108
-
109
- return { applied: true, warnings: [], pendingTools: selection.pendingTools };
110
- }
111
-
112
- /** Retries pending tool literals against the live registry. Once every
113
- * literal resolves the caller stops retrying, so a user's later native tool
114
- * toggle is never clobbered. */
115
- export function retryPendingTools(input: {
116
- selection: ResolvedSelection;
117
- surface: ApplySurface;
118
- }): { pendingTools: string[]; active?: string[]; applied: boolean } {
119
- const { selection, surface } = input;
120
- if (selection.pendingTools.length === 0) {
121
- return { pendingTools: [], applied: false };
122
- }
123
- const live = new Set(surface.getAllTools().map((tool) => tool.name));
124
- const resolved = selection.pendingTools.filter((name) => live.has(name));
125
- if (resolved.length === 0) {
126
- return { pendingTools: selection.pendingTools, applied: false };
127
- }
128
- const active = [...new Set([...(selection.tools ?? []), ...resolved])];
129
- surface.setActiveTools(active);
130
- return { pendingTools: selection.pendingTools.filter((name) => !live.has(name)), active, applied: true };
131
- }