pi-profile-switch 0.3.0 → 0.4.0
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 +32 -32
- package/README.zh-CN.md +32 -32
- 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 -17
- 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 +44 -63
- package/src/profile-resolver.ts +239 -245
- package/src/project-trust.ts +82 -0
- package/src/runtime-state-store.ts +25 -35
- 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 +21 -25
- 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 -744
- package/src/adapter-presence.ts +0 -75
- 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 -262
- package/src/startup-selection.ts +0 -144
- package/src/switching/activate-profile.ts +0 -115
- package/src/switching/apply-profile.ts +0 -131
package/src/profile-catalog.ts
CHANGED
|
@@ -3,30 +3,21 @@
|
|
|
3
3
|
* (`<agentDir>/profiles.json`) and, for trusted projects, the project
|
|
4
4
|
* catalog (`<projectDir>/.pi/profiles.json`).
|
|
5
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
|
-
*
|
|
16
6
|
* Invariants:
|
|
17
7
|
* - The built-in `default` profile never exists in either file and cannot be
|
|
18
8
|
* redefined there.
|
|
19
9
|
* - A project profile with the same name fully replaces the global
|
|
20
10
|
* definition (no merge, no inheritance); removing the project entry
|
|
21
11
|
* immediately reveals the global one.
|
|
22
|
-
* - The caller passes `projectDir` only when
|
|
23
|
-
* 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.
|
|
24
14
|
* - A malformed catalog fails loudly (CatalogError) rather than silently
|
|
25
|
-
* starting
|
|
15
|
+
* starting unfiltered.
|
|
26
16
|
*/
|
|
27
17
|
|
|
28
18
|
import path from "node:path";
|
|
29
19
|
|
|
20
|
+
import { resolveGlobalProfilesPath } from "./workspace.ts";
|
|
30
21
|
import { isRecord, readJsonFile } from "./json-file.ts";
|
|
31
22
|
|
|
32
23
|
export const PROFILE_SCHEMA_VERSION = 1;
|
|
@@ -39,14 +30,19 @@ export interface ProfileModel {
|
|
|
39
30
|
}
|
|
40
31
|
|
|
41
32
|
/** A profile definition as stored in a catalog file. All fields optional:
|
|
42
|
-
* 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. */
|
|
43
36
|
export interface ProfileDefinition {
|
|
44
37
|
label?: string;
|
|
45
38
|
description?: string;
|
|
46
39
|
skills?: string[];
|
|
47
|
-
|
|
40
|
+
extensions?: string[];
|
|
41
|
+
mcps?: string[];
|
|
48
42
|
tools?: string[];
|
|
49
|
-
|
|
43
|
+
defaultProvider?: string;
|
|
44
|
+
defaultModel?: string;
|
|
45
|
+
defaultThinkingLevel?: string;
|
|
50
46
|
instructions?: string;
|
|
51
47
|
}
|
|
52
48
|
|
|
@@ -88,9 +84,7 @@ function readOptionalString(value: unknown, field: string, profileName: string):
|
|
|
88
84
|
}
|
|
89
85
|
|
|
90
86
|
/** Parses one raw profile definition; exported for the write-side store
|
|
91
|
-
* (profile-catalog-store.ts) so anything written is loadable.
|
|
92
|
-
* fields are ignored by design — an `extensions` key left over from
|
|
93
|
-
* v0.1.0 is dropped silently, exactly like any other unknown key. */
|
|
87
|
+
* (profile-catalog-store.ts) so anything written is loadable. */
|
|
94
88
|
export function parseProfileDefinition(name: string, raw: unknown): ProfileDefinition {
|
|
95
89
|
if (!isRecord(raw)) {
|
|
96
90
|
throw new CatalogError(`profile "${name}" must be an object`);
|
|
@@ -100,59 +94,52 @@ export function parseProfileDefinition(name: string, raw: unknown): ProfileDefin
|
|
|
100
94
|
if (label !== undefined) definition.label = label;
|
|
101
95
|
const description = readOptionalString(raw.description, "description", name);
|
|
102
96
|
if (description !== undefined) definition.description = description;
|
|
103
|
-
for (const field of ["skills", "
|
|
97
|
+
for (const field of ["skills", "extensions", "mcps", "tools"] as const) {
|
|
104
98
|
const entries = readStringArray(raw[field], field, name);
|
|
105
99
|
if (entries !== undefined) definition[field] = entries;
|
|
106
100
|
}
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
}
|
|
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;
|
|
114
107
|
const instructions = readOptionalString(raw.instructions, "instructions", name);
|
|
115
108
|
if (instructions !== undefined) definition.instructions = instructions;
|
|
116
109
|
return definition;
|
|
117
110
|
}
|
|
118
111
|
|
|
119
|
-
/**
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
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}`);
|
|
124
119
|
}
|
|
125
|
-
const
|
|
126
|
-
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) {
|
|
127
125
|
throw new CatalogError(
|
|
128
|
-
`${
|
|
126
|
+
`${catalogPath}: unsupported schemaVersion ${JSON.stringify(parsed.schemaVersion)} (expected ${PROFILE_SCHEMA_VERSION})`,
|
|
129
127
|
);
|
|
130
128
|
}
|
|
131
|
-
if (!isRecord(
|
|
132
|
-
throw new CatalogError(`${
|
|
129
|
+
if (!isRecord(parsed.profiles)) {
|
|
130
|
+
throw new CatalogError(`${catalogPath}: "profiles" must be an object mapping names to definitions`);
|
|
133
131
|
}
|
|
134
|
-
const
|
|
135
|
-
for (const [name, raw] of Object.entries(value.profiles)) {
|
|
132
|
+
for (const [name, definition] of Object.entries(parsed.profiles)) {
|
|
136
133
|
if (name === DEFAULT_PROFILE_NAME) {
|
|
137
134
|
throw new CatalogError(
|
|
138
|
-
`${
|
|
135
|
+
`${catalogPath}: "${DEFAULT_PROFILE_NAME}" is built in and must not be defined in the catalog`,
|
|
139
136
|
);
|
|
140
137
|
}
|
|
141
|
-
profiles.set(name, parseProfileDefinition(name,
|
|
138
|
+
profiles.set(name, parseProfileDefinition(name, definition));
|
|
142
139
|
}
|
|
143
140
|
return profiles;
|
|
144
141
|
}
|
|
145
142
|
|
|
146
|
-
/** Reads one catalog file; missing → empty map, malformed → CatalogError. */
|
|
147
|
-
async function loadCatalogFile(catalogPath: string): Promise<Map<string, ProfileDefinition>> {
|
|
148
|
-
const result = await readJsonFile(catalogPath);
|
|
149
|
-
if (!result.ok) {
|
|
150
|
-
if (result.reason === "missing") return new Map();
|
|
151
|
-
throw new CatalogError(`invalid JSON in ${catalogPath}`);
|
|
152
|
-
}
|
|
153
|
-
return parseCatalogDocument(result.value, catalogPath);
|
|
154
|
-
}
|
|
155
|
-
|
|
156
143
|
export class ProfileCatalog {
|
|
157
144
|
readonly #profiles: ReadonlyMap<string, CatalogEntry>;
|
|
158
145
|
|
|
@@ -162,19 +149,19 @@ export class ProfileCatalog {
|
|
|
162
149
|
|
|
163
150
|
/**
|
|
164
151
|
* Reads the global catalog, plus the project catalog when `projectDir` is
|
|
165
|
-
* given (trusted projects only — the caller gates on
|
|
152
|
+
* given (trusted projects only — the caller gates on the trust check).
|
|
166
153
|
* Missing files mean an empty catalog; malformed content throws
|
|
167
154
|
* CatalogError. Project entries replace same-name global entries.
|
|
168
155
|
*/
|
|
169
156
|
static async load(agentDir: string, options?: { projectDir?: string }): Promise<ProfileCatalog> {
|
|
170
|
-
const
|
|
157
|
+
const globalProfiles = await loadCatalogFile(resolveGlobalProfilesPath(agentDir));
|
|
171
158
|
const profiles = new Map<string, CatalogEntry>();
|
|
172
|
-
for (const [name, definition] of
|
|
159
|
+
for (const [name, definition] of globalProfiles) {
|
|
173
160
|
profiles.set(name, { source: "global", definition });
|
|
174
161
|
}
|
|
175
162
|
if (options?.projectDir !== undefined) {
|
|
176
|
-
const
|
|
177
|
-
for (const [name, definition] of
|
|
163
|
+
const projectProfiles = await loadCatalogFile(path.join(options.projectDir, ".pi", "profiles.json"));
|
|
164
|
+
for (const [name, definition] of projectProfiles) {
|
|
178
165
|
profiles.set(name, { source: "project", definition });
|
|
179
166
|
}
|
|
180
167
|
}
|
|
@@ -182,7 +169,7 @@ export class ProfileCatalog {
|
|
|
182
169
|
}
|
|
183
170
|
|
|
184
171
|
/** Resolves a profile by name. `default` always resolves to the built-in
|
|
185
|
-
*
|
|
172
|
+
* full-resource profile; unknown names return undefined. */
|
|
186
173
|
resolve(name: string): ResolvedProfile | undefined {
|
|
187
174
|
if (name === DEFAULT_PROFILE_NAME) {
|
|
188
175
|
return { name: DEFAULT_PROFILE_NAME, source: "builtin", definition: {} };
|
|
@@ -199,10 +186,4 @@ export class ProfileCatalog {
|
|
|
199
186
|
...[...this.#profiles.keys()].map((name) => this.resolve(name)!),
|
|
200
187
|
];
|
|
201
188
|
}
|
|
202
|
-
|
|
203
|
-
/** True when a global definition of the same name is shadowed by the
|
|
204
|
-
* project entry. */
|
|
205
|
-
shadowsGlobal(name: string): boolean {
|
|
206
|
-
return this.#profiles.get(name)?.source === "project";
|
|
207
|
-
}
|
|
208
189
|
}
|