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
package/src/profile-catalog.ts
CHANGED
|
@@ -1,23 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* ProfileCatalog: reads profile definitions from the global catalog
|
|
3
|
-
* (`<agentDir>/profiles.json`)
|
|
4
|
-
* catalog (`<projectDir>/.pi/profiles.json`).
|
|
5
|
-
*
|
|
6
|
-
* ADR-0007 semantics:
|
|
7
|
-
* - A profile references skills, MCP servers, and tools, and may declare
|
|
8
|
-
* instructions and a model preset. Extensions are not a profile resource:
|
|
9
|
-
* every installed extension loads natively in every profile.
|
|
10
|
-
* - schemaVersion 1 is the only accepted version: any other value fails
|
|
11
|
-
* loudly instead of guessing at a shape.
|
|
12
|
-
* - Unknown fields (an `extensions` declaration left over from v0.1.0, an
|
|
13
|
-
* inheritance key) are ignored silently; saving drops them, so a written
|
|
14
|
-
* definition always matches the current shape.
|
|
15
|
-
* - The catalog key for MCP servers is `mcps` (plural, like `skills` and
|
|
16
|
-
* `tools`). The legacy name `mcp` is still READ as an alias so an
|
|
17
|
-
* existing catalog keeps its allowlist; `mcps` wins when both keys are
|
|
18
|
-
* present, and a value of the wrong type fails loudly either way.
|
|
19
|
-
* Definitions always carry the canonical `mcps` field in memory, and
|
|
20
|
-
* profile-catalog-store.ts drops a file's legacy key on its next write.
|
|
3
|
+
* (`~/.pi-profile-switch/profiles.json`, legacy fallback `<agentDir>/profiles.json`)
|
|
4
|
+
* and, for trusted projects, the project catalog (`<projectDir>/.pi/profiles.json`).
|
|
21
5
|
*
|
|
22
6
|
* Invariants:
|
|
23
7
|
* - The built-in `default` profile never exists in either file and cannot be
|
|
@@ -25,14 +9,15 @@
|
|
|
25
9
|
* - A project profile with the same name fully replaces the global
|
|
26
10
|
* definition (no merge, no inheritance); removing the project entry
|
|
27
11
|
* immediately reveals the global one.
|
|
28
|
-
* - The caller passes `projectDir` only when
|
|
29
|
-
* an untrusted project's catalog is never read.
|
|
12
|
+
* - The caller passes `projectDir` only when the resolver's trust check
|
|
13
|
+
* passed — an untrusted project's catalog is never read.
|
|
30
14
|
* - A malformed catalog fails loudly (CatalogError) rather than silently
|
|
31
|
-
* starting
|
|
15
|
+
* starting unfiltered.
|
|
32
16
|
*/
|
|
33
17
|
|
|
34
18
|
import path from "node:path";
|
|
35
19
|
|
|
20
|
+
import { resolveGlobalProfilesPath } from "./workspace.ts";
|
|
36
21
|
import { isRecord, readJsonFile } from "./json-file.ts";
|
|
37
22
|
|
|
38
23
|
export const PROFILE_SCHEMA_VERSION = 1;
|
|
@@ -45,14 +30,19 @@ export interface ProfileModel {
|
|
|
45
30
|
}
|
|
46
31
|
|
|
47
32
|
/** A profile definition as stored in a catalog file. All fields optional:
|
|
48
|
-
* undeclared fields leave Pi's behavior untouched.
|
|
33
|
+
* undeclared fields leave Pi's behavior untouched (PRD default-first rule).
|
|
34
|
+
* Model fields mirror Pi's settings.json keys (defaultProvider/defaultModel/
|
|
35
|
+
* defaultThinkingLevel) for direct compatibility. */
|
|
49
36
|
export interface ProfileDefinition {
|
|
50
37
|
label?: string;
|
|
51
38
|
description?: string;
|
|
52
39
|
skills?: string[];
|
|
40
|
+
extensions?: string[];
|
|
53
41
|
mcps?: string[];
|
|
54
42
|
tools?: string[];
|
|
55
|
-
|
|
43
|
+
defaultProvider?: string;
|
|
44
|
+
defaultModel?: string;
|
|
45
|
+
defaultThinkingLevel?: string;
|
|
56
46
|
instructions?: string;
|
|
57
47
|
}
|
|
58
48
|
|
|
@@ -85,27 +75,6 @@ function readStringArray(value: unknown, field: string, profileName: string): st
|
|
|
85
75
|
return value as string[];
|
|
86
76
|
}
|
|
87
77
|
|
|
88
|
-
/** Legacy catalog key names, still accepted on read (canonical name wins).
|
|
89
|
-
* The write paths (/profile CRUD, /mcp enable|disable) delete the alias, so
|
|
90
|
-
* a file migrates to the canonical spelling the next time it is saved. */
|
|
91
|
-
export const LEGACY_FIELD_ALIASES = { mcps: "mcp" } as const;
|
|
92
|
-
|
|
93
|
-
/** Reads an array-of-strings field under its canonical name or a legacy
|
|
94
|
-
* alias. Both spellings are validated when present, so a value of the
|
|
95
|
-
* wrong type never passes silently through either key. */
|
|
96
|
-
function readAliasedStringArray(
|
|
97
|
-
raw: Record<string, unknown>,
|
|
98
|
-
canonical: string,
|
|
99
|
-
legacy: string,
|
|
100
|
-
profileName: string,
|
|
101
|
-
): string[] | undefined {
|
|
102
|
-
for (const key of [canonical, legacy]) {
|
|
103
|
-
if (raw[key] !== undefined) readStringArray(raw[key], key, profileName);
|
|
104
|
-
}
|
|
105
|
-
const source = raw[canonical] !== undefined ? canonical : legacy;
|
|
106
|
-
return readStringArray(raw[source], source, profileName);
|
|
107
|
-
}
|
|
108
|
-
|
|
109
78
|
function readOptionalString(value: unknown, field: string, profileName: string): string | undefined {
|
|
110
79
|
if (value === undefined) return undefined;
|
|
111
80
|
if (typeof value !== "string") {
|
|
@@ -115,9 +84,7 @@ function readOptionalString(value: unknown, field: string, profileName: string):
|
|
|
115
84
|
}
|
|
116
85
|
|
|
117
86
|
/** Parses one raw profile definition; exported for the write-side store
|
|
118
|
-
* (profile-catalog-store.ts) so anything written is loadable.
|
|
119
|
-
* fields are ignored by design — an `extensions` key left over from
|
|
120
|
-
* v0.1.0 is dropped silently, exactly like any other unknown key. */
|
|
87
|
+
* (profile-catalog-store.ts) so anything written is loadable. */
|
|
121
88
|
export function parseProfileDefinition(name: string, raw: unknown): ProfileDefinition {
|
|
122
89
|
if (!isRecord(raw)) {
|
|
123
90
|
throw new CatalogError(`profile "${name}" must be an object`);
|
|
@@ -127,61 +94,52 @@ export function parseProfileDefinition(name: string, raw: unknown): ProfileDefin
|
|
|
127
94
|
if (label !== undefined) definition.label = label;
|
|
128
95
|
const description = readOptionalString(raw.description, "description", name);
|
|
129
96
|
if (description !== undefined) definition.description = description;
|
|
130
|
-
for (const field of ["skills", "tools"] as const) {
|
|
97
|
+
for (const field of ["skills", "extensions", "mcps", "tools"] as const) {
|
|
131
98
|
const entries = readStringArray(raw[field], field, name);
|
|
132
99
|
if (entries !== undefined) definition[field] = entries;
|
|
133
100
|
}
|
|
134
|
-
const
|
|
135
|
-
if (
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
const thinkingLevel = readOptionalString(raw.model.thinkingLevel, "model.thinkingLevel", name);
|
|
141
|
-
definition.model = { provider: raw.model.provider, id: raw.model.id, ...(thinkingLevel ? { thinkingLevel } : {}) };
|
|
142
|
-
}
|
|
101
|
+
const defaultProvider = readOptionalString(raw.defaultProvider, "defaultProvider", name);
|
|
102
|
+
if (defaultProvider !== undefined) definition.defaultProvider = defaultProvider;
|
|
103
|
+
const defaultModel = readOptionalString(raw.defaultModel, "defaultModel", name);
|
|
104
|
+
if (defaultModel !== undefined) definition.defaultModel = defaultModel;
|
|
105
|
+
const defaultThinkingLevel = readOptionalString(raw.defaultThinkingLevel, "defaultThinkingLevel", name);
|
|
106
|
+
if (defaultThinkingLevel !== undefined) definition.defaultThinkingLevel = defaultThinkingLevel;
|
|
143
107
|
const instructions = readOptionalString(raw.instructions, "instructions", name);
|
|
144
108
|
if (instructions !== undefined) definition.instructions = instructions;
|
|
145
109
|
return definition;
|
|
146
110
|
}
|
|
147
111
|
|
|
148
|
-
/**
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
112
|
+
/** Reads one catalog file; missing → empty map, malformed → CatalogError. */
|
|
113
|
+
async function loadCatalogFile(catalogPath: string): Promise<Map<string, ProfileDefinition>> {
|
|
114
|
+
const result = await readJsonFile(catalogPath);
|
|
115
|
+
const profiles = new Map<string, ProfileDefinition>();
|
|
116
|
+
if (!result.ok) {
|
|
117
|
+
if (result.reason === "missing") return profiles;
|
|
118
|
+
throw new CatalogError(`invalid JSON in ${catalogPath}`);
|
|
153
119
|
}
|
|
154
|
-
const
|
|
155
|
-
if (
|
|
120
|
+
const parsed = result.value;
|
|
121
|
+
if (!isRecord(parsed)) {
|
|
122
|
+
throw new CatalogError(`${catalogPath}: catalog must be an object`);
|
|
123
|
+
}
|
|
124
|
+
if (parsed.schemaVersion !== PROFILE_SCHEMA_VERSION) {
|
|
156
125
|
throw new CatalogError(
|
|
157
|
-
`${
|
|
126
|
+
`${catalogPath}: unsupported schemaVersion ${JSON.stringify(parsed.schemaVersion)} (expected ${PROFILE_SCHEMA_VERSION})`,
|
|
158
127
|
);
|
|
159
128
|
}
|
|
160
|
-
if (!isRecord(
|
|
161
|
-
throw new CatalogError(`${
|
|
129
|
+
if (!isRecord(parsed.profiles)) {
|
|
130
|
+
throw new CatalogError(`${catalogPath}: "profiles" must be an object mapping names to definitions`);
|
|
162
131
|
}
|
|
163
|
-
const
|
|
164
|
-
for (const [name, raw] of Object.entries(value.profiles)) {
|
|
132
|
+
for (const [name, definition] of Object.entries(parsed.profiles)) {
|
|
165
133
|
if (name === DEFAULT_PROFILE_NAME) {
|
|
166
134
|
throw new CatalogError(
|
|
167
|
-
`${
|
|
135
|
+
`${catalogPath}: "${DEFAULT_PROFILE_NAME}" is built in and must not be defined in the catalog`,
|
|
168
136
|
);
|
|
169
137
|
}
|
|
170
|
-
profiles.set(name, parseProfileDefinition(name,
|
|
138
|
+
profiles.set(name, parseProfileDefinition(name, definition));
|
|
171
139
|
}
|
|
172
140
|
return profiles;
|
|
173
141
|
}
|
|
174
142
|
|
|
175
|
-
/** Reads one catalog file; missing → empty map, malformed → CatalogError. */
|
|
176
|
-
async function loadCatalogFile(catalogPath: string): Promise<Map<string, ProfileDefinition>> {
|
|
177
|
-
const result = await readJsonFile(catalogPath);
|
|
178
|
-
if (!result.ok) {
|
|
179
|
-
if (result.reason === "missing") return new Map();
|
|
180
|
-
throw new CatalogError(`invalid JSON in ${catalogPath}`);
|
|
181
|
-
}
|
|
182
|
-
return parseCatalogDocument(result.value, catalogPath);
|
|
183
|
-
}
|
|
184
|
-
|
|
185
143
|
export class ProfileCatalog {
|
|
186
144
|
readonly #profiles: ReadonlyMap<string, CatalogEntry>;
|
|
187
145
|
|
|
@@ -191,19 +149,19 @@ export class ProfileCatalog {
|
|
|
191
149
|
|
|
192
150
|
/**
|
|
193
151
|
* Reads the global catalog, plus the project catalog when `projectDir` is
|
|
194
|
-
* given (trusted projects only — the caller gates on
|
|
152
|
+
* given (trusted projects only — the caller gates on the trust check).
|
|
195
153
|
* Missing files mean an empty catalog; malformed content throws
|
|
196
154
|
* CatalogError. Project entries replace same-name global entries.
|
|
197
155
|
*/
|
|
198
156
|
static async load(agentDir: string, options?: { projectDir?: string }): Promise<ProfileCatalog> {
|
|
199
|
-
const
|
|
157
|
+
const globalProfiles = await loadCatalogFile(resolveGlobalProfilesPath(agentDir));
|
|
200
158
|
const profiles = new Map<string, CatalogEntry>();
|
|
201
|
-
for (const [name, definition] of
|
|
159
|
+
for (const [name, definition] of globalProfiles) {
|
|
202
160
|
profiles.set(name, { source: "global", definition });
|
|
203
161
|
}
|
|
204
162
|
if (options?.projectDir !== undefined) {
|
|
205
|
-
const
|
|
206
|
-
for (const [name, definition] of
|
|
163
|
+
const projectProfiles = await loadCatalogFile(path.join(options.projectDir, ".pi", "profiles.json"));
|
|
164
|
+
for (const [name, definition] of projectProfiles) {
|
|
207
165
|
profiles.set(name, { source: "project", definition });
|
|
208
166
|
}
|
|
209
167
|
}
|
|
@@ -211,7 +169,7 @@ export class ProfileCatalog {
|
|
|
211
169
|
}
|
|
212
170
|
|
|
213
171
|
/** Resolves a profile by name. `default` always resolves to the built-in
|
|
214
|
-
*
|
|
172
|
+
* full-resource profile; unknown names return undefined. */
|
|
215
173
|
resolve(name: string): ResolvedProfile | undefined {
|
|
216
174
|
if (name === DEFAULT_PROFILE_NAME) {
|
|
217
175
|
return { name: DEFAULT_PROFILE_NAME, source: "builtin", definition: {} };
|
|
@@ -228,10 +186,4 @@ export class ProfileCatalog {
|
|
|
228
186
|
...[...this.#profiles.keys()].map((name) => this.resolve(name)!),
|
|
229
187
|
];
|
|
230
188
|
}
|
|
231
|
-
|
|
232
|
-
/** True when a global definition of the same name is shadowed by the
|
|
233
|
-
* project entry. */
|
|
234
|
-
shadowsGlobal(name: string): boolean {
|
|
235
|
-
return this.#profiles.get(name)?.source === "project";
|
|
236
|
-
}
|
|
237
189
|
}
|