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.
- package/README.md +31 -36
- package/README.zh-CN.md +31 -36
- package/bin/pi-profile.js +11 -0
- package/bin/pi-profile.ts +65 -0
- package/bin/postinstall.d.ts +13 -0
- package/bin/postinstall.js +88 -0
- package/defaults/profiles.json +18 -0
- package/examples/profiles.json +31 -5
- package/extensions/pi-profile/index.ts +451 -0
- package/package.json +10 -13
- package/schemas/profiles.schema.json +22 -24
- package/src/extension-discovery.ts +347 -0
- package/src/json-file.ts +1 -21
- package/src/launcher/args.ts +57 -0
- package/src/launcher/discovery.ts +64 -0
- package/src/launcher/initial-profile.ts +179 -0
- package/src/launcher/model-check.ts +52 -0
- package/src/launcher/runtime-cleanup.ts +85 -0
- package/src/launcher/spawn.ts +82 -0
- package/src/mcp-config.ts +37 -153
- package/src/mcp-coordination.ts +29 -10
- package/src/profile-catalog-store.ts +31 -12
- package/src/profile-catalog.ts +45 -93
- package/src/profile-resolver.ts +239 -245
- package/src/project-trust.ts +82 -0
- package/src/runtime-state-store.ts +25 -41
- package/src/settings-generator.ts +541 -0
- package/src/skill-registry.ts +94 -0
- package/src/switching/apply-plan.ts +197 -0
- package/src/switching/customize.ts +62 -33
- package/src/switching/list-profiles.ts +9 -6
- package/src/switching/mcp-toggle.ts +14 -26
- package/src/switching/profile-crud.ts +31 -24
- package/src/switching/profile-wizard.ts +21 -49
- package/src/switching/status.ts +142 -72
- package/src/switching/switch-profile.ts +219 -0
- package/src/switching/tool-references.ts +40 -0
- package/src/workspace.ts +57 -0
- package/LICENSE +0 -21
- package/examples/profiles.example.json +0 -74
- package/extensions/pi-profile-switch/index.ts +0 -778
- package/src/adapter-presence.ts +0 -75
- package/src/default-profiles.ts +0 -59
- package/src/mcp-overlay-file.ts +0 -35
- package/src/mcp-overlay.ts +0 -122
- package/src/model-selection.ts +0 -64
- package/src/name-matching.ts +0 -50
- package/src/profile-badge.ts +0 -142
- package/src/profile-presets.ts +0 -61
- package/src/skill-selection.ts +0 -81
- package/src/startup-mcp-scope.ts +0 -271
- package/src/startup-selection.ts +0 -201
- package/src/switching/activate-profile.ts +0 -144
- 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
|
-
}
|