pi-profile-switch 0.9.2 → 0.11.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 +6 -14
- package/README.zh-CN.md +6 -14
- package/extensions/pi-profile/index.ts +96 -189
- package/package.json +1 -1
- package/src/launcher/initial-profile.ts +4 -2
- package/src/profile-catalog.ts +2 -2
- package/src/profile-resolver.ts +114 -27
- package/src/runtime-state-store.ts +3 -4
- package/src/settings-generator.ts +1 -0
- package/src/startup-notifier.ts +636 -0
- package/src/switching/apply-plan.ts +36 -8
- package/src/switching/list-profiles.ts +2 -2
- package/src/switching/{customize.ts → overlay.ts} +67 -35
- package/src/switching/status.ts +1 -1
- package/src/switching/switch-profile.ts +7 -3
- package/src/profile-catalog-store.ts +0 -86
- package/src/switching/profile-crud.ts +0 -136
- package/src/switching/profile-wizard.ts +0 -163
package/src/profile-resolver.ts
CHANGED
|
@@ -67,6 +67,10 @@ export interface ActivationPlan {
|
|
|
67
67
|
extensions: Array<{ id: string; entry: string; origin?: "package" | "local" | "path" }>;
|
|
68
68
|
/** Expanded tool allowlist; undefined when the profile declares no tools. */
|
|
69
69
|
tools?: string[];
|
|
70
|
+
/** Overlay tool disable entries (names or globs, verbatim); undefined
|
|
71
|
+
* when the overlay disables no tools. The session-start application
|
|
72
|
+
* subtracts their live matches from the base expansion. */
|
|
73
|
+
disabledTools?: string[];
|
|
70
74
|
/** The raw tool references (globs included) for extension-side expansion
|
|
71
75
|
* against Pi's live tool registry, which includes extension-provided
|
|
72
76
|
* tools the pre-spawn expansion cannot know. Set iff `tools` is set. */
|
|
@@ -114,12 +118,54 @@ export interface ResolveInput {
|
|
|
114
118
|
* any resolved reference may be narrowed or disabled.
|
|
115
119
|
*/
|
|
116
120
|
overlay?: RuntimeOverlay;
|
|
121
|
+
/**
|
|
122
|
+
* Pi's live tool names (`pi.getAllTools()`), supplied by the in-session
|
|
123
|
+
* switch path. Required when the overlay disables tools on a profile
|
|
124
|
+
* without declared `tools` — the live registry is then the base set the
|
|
125
|
+
* entries disable from. The launcher never passes an overlay, so it never
|
|
126
|
+
* needs this.
|
|
127
|
+
*/
|
|
128
|
+
liveToolNames?: string[];
|
|
117
129
|
}
|
|
118
130
|
|
|
119
131
|
function isGlob(reference: string): boolean {
|
|
120
132
|
return reference.includes("*") || reference.includes("?");
|
|
121
133
|
}
|
|
122
134
|
|
|
135
|
+
/** Expands overlay disable entries against the names the profile resolved.
|
|
136
|
+
* Entries are names or globs stored as written, re-expanded at every
|
|
137
|
+
* resolution with the same matcher as profile references (ADR-0009 tiering):
|
|
138
|
+
* an unmatched literal fails and identifies the entry; a zero-match glob is
|
|
139
|
+
* reported through `onZeroMatch` instead of failing. Returns the resolved
|
|
140
|
+
* names to disable. */
|
|
141
|
+
function expandDisableEntries(
|
|
142
|
+
entries: string[],
|
|
143
|
+
activeNames: readonly string[],
|
|
144
|
+
kind: string,
|
|
145
|
+
profileName: string,
|
|
146
|
+
onZeroMatch: (entry: string) => void,
|
|
147
|
+
): Set<string> {
|
|
148
|
+
const disabled = new Set<string>();
|
|
149
|
+
for (const entry of entries) {
|
|
150
|
+
if (isGlob(entry)) {
|
|
151
|
+
let matched = 0;
|
|
152
|
+
for (const name of activeNames) {
|
|
153
|
+
if (minimatch(name, entry)) {
|
|
154
|
+
disabled.add(name);
|
|
155
|
+
matched += 1;
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
if (matched === 0) onZeroMatch(entry);
|
|
159
|
+
continue;
|
|
160
|
+
}
|
|
161
|
+
if (!activeNames.includes(entry)) {
|
|
162
|
+
throw new ActivationError(`profile "${profileName}": overlay disables unknown ${kind} "${entry}"`);
|
|
163
|
+
}
|
|
164
|
+
disabled.add(entry);
|
|
165
|
+
}
|
|
166
|
+
return disabled;
|
|
167
|
+
}
|
|
168
|
+
|
|
123
169
|
/** Expands one reference list against a named universe. Literal misses fail
|
|
124
170
|
* when `literalMustExist`; globs expand to zero or more matches, and a
|
|
125
171
|
* zero-match glob is reported through `onZeroMatch`. */
|
|
@@ -195,42 +241,48 @@ export async function resolveProfile(input: ResolveInput): Promise<ActivationPla
|
|
|
195
241
|
);
|
|
196
242
|
|
|
197
243
|
// --- overlay narrowing (ticket 06) ---
|
|
198
|
-
// Overlay
|
|
199
|
-
//
|
|
200
|
-
|
|
244
|
+
// Overlay disable entries are names or globs stored as written and
|
|
245
|
+
// re-expanded at every resolution; unmatched literals fail and identify
|
|
246
|
+
// the entry, zero-match globs join `unmatched` with an `overlay ` prefix.
|
|
247
|
+
// Overlays can disable any resolved extension.
|
|
248
|
+
const toolReferences = definition.tools;
|
|
201
249
|
if (overlay !== undefined) {
|
|
202
250
|
if (overlay.disabledSkills !== undefined && overlay.disabledSkills.length > 0) {
|
|
203
|
-
const
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
251
|
+
const disabled = expandDisableEntries(
|
|
252
|
+
overlay.disabledSkills,
|
|
253
|
+
selectedSkills.map((skill) => skill.name),
|
|
254
|
+
"skill",
|
|
255
|
+
profile.name,
|
|
256
|
+
(entry) => unmatched.push(`overlay skill:${entry}`),
|
|
257
|
+
);
|
|
210
258
|
selectedSkills = selectedSkills.filter((skill) => !disabled.has(skill.name));
|
|
211
259
|
}
|
|
212
260
|
if (overlay.disabledExtensions !== undefined && overlay.disabledExtensions.length > 0) {
|
|
213
|
-
const
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
261
|
+
const disabled = expandDisableEntries(
|
|
262
|
+
overlay.disabledExtensions,
|
|
263
|
+
planExtensions.map((entry) => entry.id),
|
|
264
|
+
"extension",
|
|
265
|
+
profile.name,
|
|
266
|
+
(entry) => unmatched.push(`overlay extension:${entry}`),
|
|
267
|
+
);
|
|
220
268
|
planExtensions = planExtensions.filter((entry) => !disabled.has(entry.id));
|
|
221
269
|
}
|
|
222
270
|
if (overlay.disabledMcps !== undefined && overlay.disabledMcps.length > 0) {
|
|
223
|
-
const active =
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
271
|
+
const active = mcps ?? [];
|
|
272
|
+
const disabled = expandDisableEntries(
|
|
273
|
+
overlay.disabledMcps,
|
|
274
|
+
active,
|
|
275
|
+
"MCP server",
|
|
276
|
+
profile.name,
|
|
277
|
+
(entry) => unmatched.push(`overlay mcp:${entry}`),
|
|
278
|
+
);
|
|
279
|
+
// A profile that declared no `mcps` has no MCP restriction; overlay
|
|
280
|
+
// narrowing must not turn that into an empty whitelist. Literals
|
|
281
|
+
// still fail and zero-match globs still warn — the expansion above
|
|
282
|
+
// runs either way — only the reassignment is guarded.
|
|
283
|
+
if (mcps !== undefined) {
|
|
284
|
+
mcps = active.filter((name) => !disabled.has(name));
|
|
228
285
|
}
|
|
229
|
-
const disabled = new Set(overlay.disabledMcps);
|
|
230
|
-
mcps = (mcps ?? []).filter((name) => !disabled.has(name));
|
|
231
|
-
}
|
|
232
|
-
if (overlay.tools !== undefined) {
|
|
233
|
-
toolReferences = overlay.tools;
|
|
234
286
|
}
|
|
235
287
|
}
|
|
236
288
|
|
|
@@ -241,6 +293,40 @@ export async function resolveProfile(input: ResolveInput): Promise<ActivationPla
|
|
|
241
293
|
});
|
|
242
294
|
}
|
|
243
295
|
|
|
296
|
+
// Tool disable entries join the uniform grammar: the base set is the
|
|
297
|
+
// profile's resolved tool references when declared (expanded against the
|
|
298
|
+
// live registry, which the in-session switch path always supplies) and
|
|
299
|
+
// the live registry itself when the profile declares no tools. The
|
|
300
|
+
// plan carries the entries verbatim; session-start application subtracts
|
|
301
|
+
// their live matches from the base expansion at that moment.
|
|
302
|
+
let disabledTools: string[] | undefined;
|
|
303
|
+
if (overlay?.disabledTools !== undefined && overlay.disabledTools.length > 0) {
|
|
304
|
+
if (input.liveToolNames === undefined) {
|
|
305
|
+
throw new ActivationError(
|
|
306
|
+
`profile "${profile.name}": overlay disables tools but no live tool registry is available`,
|
|
307
|
+
);
|
|
308
|
+
}
|
|
309
|
+
const base =
|
|
310
|
+
toolReferences !== undefined
|
|
311
|
+
? expandReferences(toolReferences, input.liveToolNames, (name) => name, "tool", {
|
|
312
|
+
literalMustExist: false,
|
|
313
|
+
})
|
|
314
|
+
: input.liveToolNames;
|
|
315
|
+
const disabled = expandDisableEntries(
|
|
316
|
+
overlay.disabledTools,
|
|
317
|
+
base,
|
|
318
|
+
"tool",
|
|
319
|
+
profile.name,
|
|
320
|
+
(entry) => unmatched.push(`overlay tool:${entry}`),
|
|
321
|
+
);
|
|
322
|
+
disabledTools = [...overlay.disabledTools];
|
|
323
|
+
// The pre-computed boot baseline additionally subtracts the disabled
|
|
324
|
+
// matches (a no-op for tools only the live registry knows).
|
|
325
|
+
if (tools !== undefined) {
|
|
326
|
+
tools = tools.filter((name) => !disabled.has(name));
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
|
|
244
330
|
let model: ProfileModel | undefined;
|
|
245
331
|
const declaredModel = extractModel(definition);
|
|
246
332
|
if (declaredModel !== undefined) {
|
|
@@ -267,6 +353,7 @@ export async function resolveProfile(input: ResolveInput): Promise<ActivationPla
|
|
|
267
353
|
skills: selectedSkills,
|
|
268
354
|
extensions: planExtensions.map((entry) => ({ id: entry.id, entry: entry.entry })),
|
|
269
355
|
...(tools !== undefined && toolReferences !== undefined ? { tools, toolReferences: [...toolReferences] } : {}),
|
|
356
|
+
...(disabledTools !== undefined ? { disabledTools } : {}),
|
|
270
357
|
...(model !== undefined ? { model } : {}),
|
|
271
358
|
...(definition.instructions !== undefined ? { instructions: definition.instructions } : {}),
|
|
272
359
|
...(mcps !== undefined ? { mcps } : {}),
|
|
@@ -32,14 +32,13 @@ export interface RuntimeOverlay {
|
|
|
32
32
|
disabledSkills?: string[];
|
|
33
33
|
disabledExtensions?: string[];
|
|
34
34
|
disabledMcps?: string[];
|
|
35
|
-
|
|
36
|
-
tools?: string[];
|
|
35
|
+
disabledTools?: string[];
|
|
37
36
|
}
|
|
38
37
|
|
|
39
38
|
function parseOverlay(value: unknown): RuntimeOverlay | undefined {
|
|
40
39
|
if (!isRecord(value)) return undefined;
|
|
41
40
|
const overlay: RuntimeOverlay = {};
|
|
42
|
-
for (const key of ["disabledSkills", "disabledExtensions", "disabledMcps", "
|
|
41
|
+
for (const key of ["disabledSkills", "disabledExtensions", "disabledMcps", "disabledTools"] as const) {
|
|
43
42
|
const list = value[key];
|
|
44
43
|
if (Array.isArray(list) && list.every((entry) => typeof entry === "string")) {
|
|
45
44
|
overlay[key] = list;
|
|
@@ -77,7 +76,7 @@ export class RuntimeStateStore {
|
|
|
77
76
|
}
|
|
78
77
|
|
|
79
78
|
/** Read-modify-write merge. A field set to `undefined` is deleted; absent
|
|
80
|
-
* fields keep their stored value. Used by the switch/
|
|
79
|
+
* fields keep their stored value. Used by the switch/overlay paths so
|
|
81
80
|
* one concern (selection, anchor, overlay) never clobbers another. */
|
|
82
81
|
async update(patch: Partial<RuntimeState>): Promise<RuntimeState> {
|
|
83
82
|
const current = await this.read();
|
|
@@ -401,6 +401,7 @@ export async function writeRuntimeFiles(
|
|
|
401
401
|
agentDir: options.agentDir,
|
|
402
402
|
...(plan.tools !== undefined ? { tools: plan.tools } : {}),
|
|
403
403
|
...(plan.toolReferences !== undefined ? { toolReferences: plan.toolReferences } : {}),
|
|
404
|
+
...(plan.disabledTools !== undefined ? { disabledTools: plan.disabledTools } : {}),
|
|
404
405
|
...(plan.mcps !== undefined ? { mcps: plan.mcps } : {}),
|
|
405
406
|
// The resolved sets feed /profile status (absolute paths) and the
|
|
406
407
|
// glob-delta diff against the previous activation.
|