pi-profile-switch 0.2.0 → 0.3.1
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 +11 -27
- package/README.zh-CN.md +11 -27
- package/examples/profiles.example.json +74 -0
- package/extensions/pi-profile-switch/index.ts +150 -9
- package/package.json +1 -1
- package/schemas/profiles.schema.json +4 -11
- package/src/adapter-presence.ts +75 -0
- package/src/default-profiles.ts +59 -0
- package/src/json-file.ts +20 -0
- package/src/mcp-config.ts +153 -34
- package/src/mcp-overlay-file.ts +35 -0
- package/src/mcp-overlay.ts +122 -0
- package/src/profile-catalog-store.ts +1 -1
- package/src/profile-catalog.ts +39 -12
- package/src/profile-presets.ts +3 -2
- package/src/profile-resolver.ts +3 -3
- package/src/runtime-state-store.ts +6 -0
- package/src/startup-mcp-scope.ts +271 -0
- package/src/startup-selection.ts +59 -2
- package/src/switching/activate-profile.ts +33 -4
- package/src/switching/mcp-toggle.ts +17 -9
- package/src/switching/profile-wizard.ts +1 -1
package/README.md
CHANGED
|
@@ -6,7 +6,7 @@ Named profiles for [Pi](https://github.com/badlogic/pi-mono). A profile selects
|
|
|
6
6
|
|
|
7
7
|
A shipped `read-only` preset covers the read-and-report workflow; other workflows you write yourself — a profile can narrow the tool set, hide skills, pin a model, or add instructions.
|
|
8
8
|
|
|
9
|
-
`pi-profile-switch` is a plain Pi package (ADR-0007). It installs like any other extension,
|
|
9
|
+
`pi-profile-switch` is a plain Pi package (ADR-0007). It installs like any other extension, keeps Pi's configuration directory as the single source of truth, and keeps sessions, packages, project trust, and every other installed extension native. The only file it adds is the default catalog below.
|
|
10
10
|
|
|
11
11
|
## Install
|
|
12
12
|
|
|
@@ -16,6 +16,8 @@ pi install npm:pi-profile-switch
|
|
|
16
16
|
|
|
17
17
|
Requires [Pi](https://www.npmjs.com/package/@earendil-works/pi-coding-agent) (installed automatically as a peer dependency).
|
|
18
18
|
|
|
19
|
+
Pi packages have no install hook, so the default catalog is written the first time the extension loads (in any mode, including `--mode rpc`): if `~/.pi/agent/profiles.json` does not exist, it is created from the shipped `read-only` profile. An existing file is never read, rewritten, or backed up — delete it to get the default back.
|
|
20
|
+
|
|
19
21
|
## Quick start
|
|
20
22
|
|
|
21
23
|
```bash
|
|
@@ -29,30 +31,13 @@ pi --profile read-only
|
|
|
29
31
|
pi --profile default
|
|
30
32
|
```
|
|
31
33
|
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
{
|
|
36
|
-
"schemaVersion": 1,
|
|
37
|
-
"profiles": {
|
|
38
|
-
"read-only": {
|
|
39
|
-
"label": "Read-only",
|
|
40
|
-
"description": "Read-only session; no skills or MCP servers assumed — add your own.",
|
|
41
|
-
"tools": [
|
|
42
|
-
"read",
|
|
43
|
-
"grep",
|
|
44
|
-
"find",
|
|
45
|
-
"ls"
|
|
46
|
-
],
|
|
47
|
-
"instructions": "Read-only session: inspect and report; never create, edit, rename, or delete files.\nIf a change is needed, describe it in your reply instead of applying it.\nDo not run commands that modify state (installs, formatters, commits, pushes, network writes).\nPrefer an available skill or MCP tool when it fits the request; otherwise use the tools you have.\nGround claims in evidence: cite file:line and separate verified facts from inferences.\nReply in English."
|
|
48
|
-
}
|
|
49
|
-
}
|
|
50
|
-
}
|
|
51
|
-
```
|
|
34
|
+
A fresh install already holds one profile, `read-only`; activate it with `pi --profile read-only` or `/profile use read-only`.
|
|
35
|
+
|
|
36
|
+
Create more with `/profile create`, which writes `~/.pi/agent/profiles.json` (global) or `<project>/.pi/profiles.json` (project, trusted projects only). The wizard offers the shipped `read-only` preset — Pi's built-in tools only, no skills, MCP servers, or model assumed — or a blank definition.
|
|
52
37
|
|
|
53
|
-
A preset is a one-time copy into your catalog: it is not tracked, so a package update never changes a profile you already created. Profiles **reference** resources by name — they never copy them. [`examples/profiles.json`](examples/profiles.json) is
|
|
38
|
+
A preset is a one-time copy into your catalog: it is not tracked, so a package update never changes a profile you already created. Profiles **reference** resources by name — they never copy them. [`examples/profiles.example.json`](examples/profiles.example.json) is a complete catalog with several profiles covering every field; replace its resource names with ones you own.
|
|
54
39
|
|
|
55
|
-
`schemaVersion` is 1. A profile cannot select extensions: they load natively in every profile, so manage them with `pi install`.
|
|
40
|
+
`schemaVersion` is 1 and is the only accepted value. A profile cannot select extensions: they load natively in every profile, so manage them with `pi install`.
|
|
56
41
|
|
|
57
42
|
## What a profile controls
|
|
58
43
|
|
|
@@ -61,7 +46,7 @@ A preset is a one-time copy into your catalog: it is not tracked, so a package u
|
|
|
61
46
|
| `instructions` | Text appended to the system prompt every turn |
|
|
62
47
|
| `model` | Session-start model preset; an explicit `--model`/`--thinking` or a model recorded in the session wins |
|
|
63
48
|
| `skills` | What the model sees in the prompt's skills list. Every installed skill stays loaded and callable by the user through `/skill:name` |
|
|
64
|
-
| `
|
|
49
|
+
| `mcps` | Which MCP servers the session exposes: the profile's allowlist is written into the adapter's own config (`~/.pi/agent/mcp.json`), disabling every other server. Servers you keep in that file move to `mcp.user.json`; connection details stay in your config, never in the profile. The legacy key `mcp` is still read and becomes `mcps` the next time the profile is saved |
|
|
65
50
|
| `tools` | Active tool set: declared names/globs become the active set; names that register later (MCP, extensions) are applied when they appear |
|
|
66
51
|
|
|
67
52
|
Anything a profile does not declare keeps Pi's native behavior, and `default` declares nothing.
|
|
@@ -74,7 +59,7 @@ In the TUI, the `/profile` command family manages everything in-session:
|
|
|
74
59
|
| --- | --- |
|
|
75
60
|
| `/profile` | Interactive profile picker |
|
|
76
61
|
| `/profile list` / `/profile status` | Show profiles / active profile details |
|
|
77
|
-
| `/profile use <name>` | Switch
|
|
62
|
+
| `/profile use <name>` | Switch in place — same session; when the MCP selection changes, the runtime reloads automatically to re-apply it |
|
|
78
63
|
| `/profile create\|edit\|delete\|duplicate` | Guided profile CRUD (TUI only) |
|
|
79
64
|
| `/profile customize` / `/profile reset` | Narrow the active profile for this session only |
|
|
80
65
|
| `/mcp enable\|disable <server>` | Toggle MCP servers in the active profile |
|
|
@@ -88,12 +73,11 @@ All commands work in non-interactive modes (`--mode rpc|print|json`); CRUD wizar
|
|
|
88
73
|
- **Reference, never copy** — profiles point at resources you already own and maintain.
|
|
89
74
|
- **Pi-native** — the configuration directory is Pi's own, so sessions, extension config, packages, context files, and trust behave exactly as they do in plain Pi.
|
|
90
75
|
- **Fail safe** — untrusted project directories are never read; a failed activation applies nothing and reports the cause.
|
|
91
|
-
- **No reload** — switching re-applies runtime state in place
|
|
76
|
+
- **No manual reload** — switching re-applies runtime state in place and the next turn's prompt carries the new selection. Only a change to the MCP selection rebuilds the runtime, automatically, in the same session, and the switch survives that rebuild: the rebuild continues the selection you made, it does not fall back to the profile you started with.
|
|
92
77
|
|
|
93
78
|
## Docs
|
|
94
79
|
|
|
95
80
|
- [Architecture](docs/architecture/overview.md) · [ADRs](docs/adr/) · [Glossary](CONTEXT.md) (Chinese)
|
|
96
|
-
- JSON Schema: [`schemas/profiles.schema.json`](schemas/profiles.schema.json)
|
|
97
81
|
|
|
98
82
|
## License
|
|
99
83
|
|
package/README.zh-CN.md
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
|
|
7
7
|
随包提供一个 `read-only` 预设覆盖「读代码 + 报告」的工作流;其余工作流由你自己定义——profile 可以收窄工具集、隐藏 skill、固定模型或附加指令。
|
|
8
8
|
|
|
9
|
-
`pi-profile-switch` 是普通 Pi package(ADR-0007
|
|
9
|
+
`pi-profile-switch` 是普通 Pi package(ADR-0007):像其他扩展一样安装,配置目录始终是 Pi 自己的那一份,session、packages、项目信任和其他扩展全部保持原生。它只在配置目录里新增一个文件,就是下面这个默认 catalog。
|
|
10
10
|
|
|
11
11
|
## 安装
|
|
12
12
|
|
|
@@ -16,6 +16,8 @@ pi install npm:pi-profile-switch
|
|
|
16
16
|
|
|
17
17
|
依赖 [Pi](https://www.npmjs.com/package/@earendil-works/pi-coding-agent)(作为 peer dependency 自动安装)。
|
|
18
18
|
|
|
19
|
+
Pi package 没有安装钩子,因此默认 catalog 在扩展首次加载时写入(任何模式都会执行,包括 `--mode rpc`):`~/.pi/agent/profiles.json` 不存在时,用随包的 `read-only` profile 创建它。已存在的文件不会被读取、改写或备份——删掉它即可拿回默认值。
|
|
20
|
+
|
|
19
21
|
## 快速上手
|
|
20
22
|
|
|
21
23
|
```bash
|
|
@@ -29,30 +31,13 @@ pi --profile read-only
|
|
|
29
31
|
pi --profile default
|
|
30
32
|
```
|
|
31
33
|
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
{
|
|
36
|
-
"schemaVersion": 1,
|
|
37
|
-
"profiles": {
|
|
38
|
-
"read-only": {
|
|
39
|
-
"label": "Read-only",
|
|
40
|
-
"description": "Read-only session; no skills or MCP servers assumed — add your own.",
|
|
41
|
-
"tools": [
|
|
42
|
-
"read",
|
|
43
|
-
"grep",
|
|
44
|
-
"find",
|
|
45
|
-
"ls"
|
|
46
|
-
],
|
|
47
|
-
"instructions": "Read-only session: inspect and report; never create, edit, rename, or delete files.\nIf a change is needed, describe it in your reply instead of applying it.\nDo not run commands that modify state (installs, formatters, commits, pushes, network writes).\nPrefer an available skill or MCP tool when it fits the request; otherwise use the tools you have.\nGround claims in evidence: cite file:line and separate verified facts from inferences.\nReply in English."
|
|
48
|
-
}
|
|
49
|
-
}
|
|
50
|
-
}
|
|
51
|
-
```
|
|
34
|
+
新安装已经带有一个 `read-only` profile,用 `pi --profile read-only` 或 `/profile use read-only` 激活。
|
|
35
|
+
|
|
36
|
+
用 `/profile create` 创建更多 profile,写入 `~/.pi/agent/profiles.json`(全局)或 `<项目>/.pi/profiles.json`(项目级,仅限已信任项目)。向导提供随包的 `read-only` 预设——只用 Pi 内建工具,不假设任何 skill、MCP server 或模型——也可以从空定义开始。
|
|
52
37
|
|
|
53
|
-
预设只被复制一次:它不被跟踪,包升级不会改动你已经创建的 profile。Profile 只**引用**资源,从不复制资源。[`examples/profiles.json`](examples/profiles.json)
|
|
38
|
+
预设只被复制一次:它不被跟踪,包升级不会改动你已经创建的 profile。Profile 只**引用**资源,从不复制资源。[`examples/profiles.example.json`](examples/profiles.example.json) 是覆盖全部字段的完整 catalog,示例中的资源名请替换成你本机已有的名字。
|
|
54
39
|
|
|
55
|
-
`schemaVersion` 为 1
|
|
40
|
+
`schemaVersion` 为 1,也是唯一接受的值。Profile 不选择 extension:extensions 在所有 profile 中原生加载,请用 `pi install` 管理。
|
|
56
41
|
|
|
57
42
|
## Profile 控制的范围
|
|
58
43
|
|
|
@@ -61,7 +46,7 @@ pi --profile default
|
|
|
61
46
|
| `instructions` | 每个 turn 追加到 system prompt 末尾 |
|
|
62
47
|
| `model` | 会话启动的模型预设;显式 `--model`/`--thinking` 或 session 历史中记录的模型优先 |
|
|
63
48
|
| `skills` | 模型在 prompt skills 列表中看到的内容;所有已安装 skill 仍保持加载,用户可用 `/skill:name` 手动调用 |
|
|
64
|
-
| `
|
|
49
|
+
| `mcps` | 本次会话暴露哪些 MCP server:白名单写入 adapter 自己的配置(`~/.pi/agent/mcp.json`),其余 server 一律标为 disabled。你原本放在该文件里的 server 会迁移到 `mcp.user.json`;连接参数只在你自己的配置里,从不进入 profile。旧键 `mcp` 仍可读取,下次保存 profile 时变为 `mcps` |
|
|
65
50
|
| `tools` | 活动工具集:声明的名字/glob 成为活动集合;之后才注册的工具(MCP、扩展)在出现时补上 |
|
|
66
51
|
|
|
67
52
|
未声明的字段保持 Pi 原生行为,`default` 什么都不声明。
|
|
@@ -74,7 +59,7 @@ pi --profile default
|
|
|
74
59
|
| --- | --- |
|
|
75
60
|
| `/profile` | 交互式选择 profile |
|
|
76
61
|
| `/profile list` / `/profile status` | 列出 profile / 查看活动 profile 详情 |
|
|
77
|
-
| `/profile use <name>` |
|
|
62
|
+
| `/profile use <name>` | 原位切换——同一 session;当 MCP 选择发生变化时会自动 reload 以重新应用 |
|
|
78
63
|
| `/profile create\|edit\|delete\|duplicate` | 向导式 profile 增删改(仅 TUI) |
|
|
79
64
|
| `/profile customize` / `/profile reset` | 仅本次会话收窄活动 profile |
|
|
80
65
|
| `/mcp enable\|disable <server>` | 在活动 profile 中开关 MCP server |
|
|
@@ -88,12 +73,11 @@ pi --profile default
|
|
|
88
73
|
- **引用而非复制**——profile 指向你自己拥有和维护的资源。
|
|
89
74
|
- **Pi 原生**——配置目录就是 Pi 自己的目录,session、扩展配置、packages、context 文件和信任行为与原生 Pi 完全一致。
|
|
90
75
|
- **失败安全**——未信任的项目目录从不读取;激活失败时不应用任何设置并报出原因。
|
|
91
|
-
-
|
|
76
|
+
- **无需手动 reload**——切换在原位重新应用运行时状态,下一个 turn 的 prompt 直接带上新选择;只有 MCP 选择变化时才会自动重建运行时(同一 session),且切换结果在重建后保持不变:重建沿用你刚做的选择,不会退回启动时的 profile
|
|
92
77
|
|
|
93
78
|
## 文档
|
|
94
79
|
|
|
95
80
|
- [架构设计](docs/architecture/overview.md) · [ADR](docs/adr/) · [术语表](CONTEXT.md)
|
|
96
|
-
- JSON Schema:[`schemas/profiles.schema.json`](schemas/profiles.schema.json)
|
|
97
81
|
|
|
98
82
|
## 许可证
|
|
99
83
|
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
{
|
|
2
|
+
"schemaVersion": 1,
|
|
3
|
+
"profiles": {
|
|
4
|
+
"read-only": {
|
|
5
|
+
"label": "Read-only",
|
|
6
|
+
"description": "Read-only session; no skills or MCP servers assumed — add your own.",
|
|
7
|
+
"tools": [
|
|
8
|
+
"read",
|
|
9
|
+
"grep",
|
|
10
|
+
"find",
|
|
11
|
+
"ls"
|
|
12
|
+
],
|
|
13
|
+
"instructions": "Read-only session: inspect and report; never create, edit, rename, or delete files.\nIf a change is needed, describe it in your reply instead of applying it.\nDo not run commands that modify state (installs, formatters, commits, pushes, network writes).\nPrefer an available skill or MCP tool when it fits the request; otherwise use the tools you have.\nGround claims in evidence: cite file:line and separate verified facts from inferences.\nReply in English."
|
|
14
|
+
},
|
|
15
|
+
"review": {
|
|
16
|
+
"label": "Code review",
|
|
17
|
+
"description": "Review a diff or a branch: read the change, report findings, modify nothing.",
|
|
18
|
+
"skills": [
|
|
19
|
+
"code-review",
|
|
20
|
+
"git-*"
|
|
21
|
+
],
|
|
22
|
+
"mcps": [
|
|
23
|
+
"github"
|
|
24
|
+
],
|
|
25
|
+
"tools": [
|
|
26
|
+
"read",
|
|
27
|
+
"grep",
|
|
28
|
+
"find",
|
|
29
|
+
"ls",
|
|
30
|
+
"bash"
|
|
31
|
+
],
|
|
32
|
+
"model": {
|
|
33
|
+
"provider": "anthropic",
|
|
34
|
+
"id": "claude-sonnet-4-5",
|
|
35
|
+
"thinkingLevel": "high"
|
|
36
|
+
},
|
|
37
|
+
"instructions": "Review only: read the change under review, then report findings with file:line evidence.\nNever edit tracked files, commit, or push; describe the required fix instead.\nSeparate verified behavior from suspected issues."
|
|
38
|
+
},
|
|
39
|
+
"plan": {
|
|
40
|
+
"label": "Plan",
|
|
41
|
+
"description": "Research a change and write the plan; no edits, no implementation.",
|
|
42
|
+
"skills": [],
|
|
43
|
+
"tools": [
|
|
44
|
+
"read",
|
|
45
|
+
"grep",
|
|
46
|
+
"find",
|
|
47
|
+
"ls",
|
|
48
|
+
"bash"
|
|
49
|
+
],
|
|
50
|
+
"instructions": "Planning session: research the code first, then write the plan in your reply.\nDo not create, edit, rename, or delete files."
|
|
51
|
+
},
|
|
52
|
+
"implement": {
|
|
53
|
+
"label": "Implement",
|
|
54
|
+
"description": "Full tool access for implementing a change end to end.",
|
|
55
|
+
"skills": [
|
|
56
|
+
"git-commit",
|
|
57
|
+
"test-*"
|
|
58
|
+
],
|
|
59
|
+
"mcps": [
|
|
60
|
+
"github"
|
|
61
|
+
],
|
|
62
|
+
"tools": [
|
|
63
|
+
"read",
|
|
64
|
+
"grep",
|
|
65
|
+
"find",
|
|
66
|
+
"ls",
|
|
67
|
+
"bash",
|
|
68
|
+
"edit",
|
|
69
|
+
"write"
|
|
70
|
+
],
|
|
71
|
+
"instructions": "Implement the requested change: read before writing, keep the diff minimal.\nRun the tests that cover your change and report the exact commands."
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
}
|
|
@@ -6,11 +6,13 @@ import {
|
|
|
6
6
|
type ExtensionContext,
|
|
7
7
|
} from "@earendil-works/pi-coding-agent";
|
|
8
8
|
|
|
9
|
+
import { adapterPresent } from "../../src/adapter-presence.ts";
|
|
9
10
|
import { discoverAdapterServerNames } from "../../src/mcp-config.ts";
|
|
10
11
|
import { probeAdapterPresence } from "../../src/mcp-coordination.ts";
|
|
11
12
|
import { readSessionChoices } from "../../src/model-selection.ts";
|
|
12
13
|
import { buildProfileBadge, PROFILE_STATUS_KEY, renderProfileBadge } from "../../src/profile-badge.ts";
|
|
13
14
|
import type { ProfileDefinition } from "../../src/profile-catalog.ts";
|
|
15
|
+
import { seedDefaultProfilesSync } from "../../src/default-profiles.ts";
|
|
14
16
|
import {
|
|
15
17
|
formatSelectionWarnings,
|
|
16
18
|
formatSkillWarnings,
|
|
@@ -25,11 +27,18 @@ import {
|
|
|
25
27
|
type SkillsFilterOutcome,
|
|
26
28
|
} from "../../src/skill-selection.ts";
|
|
27
29
|
import {
|
|
30
|
+
appliedProfile,
|
|
28
31
|
detectExplicitDeclarations,
|
|
29
32
|
readProfileFlag,
|
|
33
|
+
recordAppliedProfile,
|
|
30
34
|
registerProfileFlag,
|
|
31
35
|
resolveStartupProfile,
|
|
32
36
|
} from "../../src/startup-selection.ts";
|
|
37
|
+
import {
|
|
38
|
+
readFlagFromArgv,
|
|
39
|
+
syncMcpOverlayForSelection,
|
|
40
|
+
syncStartupMcpOverlay,
|
|
41
|
+
} from "../../src/startup-mcp-scope.ts";
|
|
33
42
|
import { retryPendingTools, type ApplySurface } from "../../src/switching/apply-profile.ts";
|
|
34
43
|
import {
|
|
35
44
|
activateProfile,
|
|
@@ -63,10 +72,14 @@ import { buildStatusReport, formatStatusMarkdown } from "../../src/switching/sta
|
|
|
63
72
|
* all native.
|
|
64
73
|
*
|
|
65
74
|
* Responsibilities:
|
|
75
|
+
* - load: seed `<agentDir>/profiles.json` from the shipped default catalog
|
|
76
|
+
* when the file does not exist yet (Pi packages have no install hook), and
|
|
77
|
+
* report a failure once at `session_start`.
|
|
66
78
|
* - `session_start`: resolve the startup profile (`--profile <flag>`, else
|
|
67
|
-
* the saved selection, else `default
|
|
68
|
-
* the selection
|
|
69
|
-
*
|
|
79
|
+
* the saved selection, else `default`; every start after the first
|
|
80
|
+
* continues the selection this process already applied), then apply the
|
|
81
|
+
* runtime parts of the selection — model preset, active tools, MCP
|
|
82
|
+
* allowlist. A failed activation applies nothing and reports loudly.
|
|
70
83
|
* - `before_agent_start`: rebuild the system prompt each turn — replace the
|
|
71
84
|
* skills section with the profile's visible set and append the profile's
|
|
72
85
|
* instructions. Unselected skills stay loaded and `/skill:`-invocable.
|
|
@@ -115,7 +128,41 @@ const PROFILE_USAGE = [
|
|
|
115
128
|
|
|
116
129
|
export default function piProfileExtension(pi: ExtensionAPI): void {
|
|
117
130
|
registerProfileFlag(pi);
|
|
118
|
-
const
|
|
131
|
+
const argv = process.argv.slice(2);
|
|
132
|
+
const explicit = detectExplicitDeclarations(argv);
|
|
133
|
+
const loadAgentDir = getAgentDir();
|
|
134
|
+
// Install-time default: Pi packages have no install hook, so the first
|
|
135
|
+
// load seeds <agentDir>/profiles.json from the shipped catalog. The write
|
|
136
|
+
// is idempotent, never overwrites a user catalog, and a failure is
|
|
137
|
+
// reported at session_start instead of blocking the load.
|
|
138
|
+
let seedWarning: string | undefined;
|
|
139
|
+
try {
|
|
140
|
+
seedDefaultProfilesSync(loadAgentDir);
|
|
141
|
+
} catch (error) {
|
|
142
|
+
seedWarning = `pi-profile-switch: could not write the default profiles.json — ${error instanceof Error ? error.message : String(error)}`;
|
|
143
|
+
}
|
|
144
|
+
// pi-mcp-adapter reads its config before any session event fires (and, for
|
|
145
|
+
// eager servers, at its own load time), so the startup profile's overlay
|
|
146
|
+
// is generated here, synchronously. Pi applies CLI flag values only after
|
|
147
|
+
// extension loading, hence argv.
|
|
148
|
+
const adapterInstalled = adapterPresent({
|
|
149
|
+
agentDir: loadAgentDir,
|
|
150
|
+
argv,
|
|
151
|
+
probeAnswered: probeAdapterPresence(pi.events),
|
|
152
|
+
});
|
|
153
|
+
if (adapterInstalled) {
|
|
154
|
+
const requestedConfigPath = readFlagFromArgv(argv, "mcp-config");
|
|
155
|
+
// A reload's overlay belongs to the run's current selection: the same
|
|
156
|
+
// continuation `session_start` applies right after this pass.
|
|
157
|
+
const runProfile = appliedProfile();
|
|
158
|
+
syncStartupMcpOverlay({
|
|
159
|
+
agentDir: loadAgentDir,
|
|
160
|
+
cwd: process.cwd(),
|
|
161
|
+
argv,
|
|
162
|
+
...(requestedConfigPath === undefined ? {} : { overridePath: requestedConfigPath }),
|
|
163
|
+
...(runProfile === undefined ? {} : { continuation: runProfile }),
|
|
164
|
+
});
|
|
165
|
+
}
|
|
119
166
|
let current: Activation | undefined;
|
|
120
167
|
let filterWarningShown = false;
|
|
121
168
|
/** The last badge written to the footer, so a refresh only talks to Pi
|
|
@@ -217,6 +264,8 @@ export default function piProfileExtension(pi: ExtensionAPI): void {
|
|
|
217
264
|
overlay: options?.overlay ?? null,
|
|
218
265
|
persist: options?.persist ?? true,
|
|
219
266
|
});
|
|
267
|
+
// The run's current selection, for the reload continuation.
|
|
268
|
+
recordAppliedProfile(result.selection.name);
|
|
220
269
|
setCurrent(ctx, activationOf(result, deps.live.skills !== undefined));
|
|
221
270
|
reportWarnings(ctx, formatSelectionWarnings(result.selection));
|
|
222
271
|
return result;
|
|
@@ -239,6 +288,75 @@ export default function piProfileExtension(pi: ExtensionAPI): void {
|
|
|
239
288
|
});
|
|
240
289
|
}
|
|
241
290
|
|
|
291
|
+
/** `session_start` re-check. The load-time pass cannot know an interactive
|
|
292
|
+
* trust answer, and the adapter has already read its config by now — so a
|
|
293
|
+
* difference is written for the next reload and reported instead. */
|
|
294
|
+
function reportMcpOverlayState(
|
|
295
|
+
ctx: ExtensionContext,
|
|
296
|
+
profileName: string,
|
|
297
|
+
selection: ResolvedSelection,
|
|
298
|
+
input: { agentDir: string; cwd: string; projectTrusted: boolean },
|
|
299
|
+
): void {
|
|
300
|
+
const configOverride = readFlagFromArgv(argv, "mcp-config");
|
|
301
|
+
const sync = syncStartupMcpOverlay({
|
|
302
|
+
agentDir: input.agentDir,
|
|
303
|
+
cwd: input.cwd,
|
|
304
|
+
projectTrusted: input.projectTrusted,
|
|
305
|
+
profileName,
|
|
306
|
+
argv,
|
|
307
|
+
...(configOverride === undefined ? {} : { overridePath: configOverride }),
|
|
308
|
+
});
|
|
309
|
+
if (sync.error !== undefined) {
|
|
310
|
+
notify(ctx, `pi-profile-switch: MCP overlay unavailable — ${sync.error}`, "warning");
|
|
311
|
+
return;
|
|
312
|
+
}
|
|
313
|
+
if (!sync.managed) {
|
|
314
|
+
if (selection.mcp !== undefined) {
|
|
315
|
+
notify(
|
|
316
|
+
ctx,
|
|
317
|
+
`pi-profile-switch: --mcp-config points at another file — profile "${profileName}" cannot filter MCP servers`,
|
|
318
|
+
"warning",
|
|
319
|
+
);
|
|
320
|
+
}
|
|
321
|
+
return;
|
|
322
|
+
}
|
|
323
|
+
if (!sync.changed) return;
|
|
324
|
+
notify(ctx, `pi-profile-switch: MCP config updated for profile "${profileName}" — run /reload to apply`, "warning");
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
/** Repoints the adapter at the overlay for a just-activated selection and
|
|
328
|
+
* rebuilds the runtime when the MCP surface actually moved. Must be the
|
|
329
|
+
* caller's LAST use of `ctx`: `reload()` invalidates the old context. */
|
|
330
|
+
async function reloadForMcpOverlay(ctx: ExtensionCommandContext, selection: ResolvedSelection): Promise<void> {
|
|
331
|
+
if (!adapterInstalled) return;
|
|
332
|
+
const configOverride = readFlagFromArgv(argv, "mcp-config");
|
|
333
|
+
const sync = syncMcpOverlayForSelection({
|
|
334
|
+
agentDir: getAgentDir(),
|
|
335
|
+
cwd: ctx.cwd,
|
|
336
|
+
projectTrusted: ctx.isProjectTrusted(),
|
|
337
|
+
allowed: selection.mcp === undefined ? "all" : selection.mcp,
|
|
338
|
+
...(configOverride === undefined ? {} : { overridePath: configOverride }),
|
|
339
|
+
});
|
|
340
|
+
if (sync.error !== undefined) {
|
|
341
|
+
notify(ctx, `pi-profile-switch: MCP overlay not updated — ${sync.error}`, "warning");
|
|
342
|
+
return;
|
|
343
|
+
}
|
|
344
|
+
if (!sync.managed) {
|
|
345
|
+
if (selection.mcp !== undefined) {
|
|
346
|
+
notify(
|
|
347
|
+
ctx,
|
|
348
|
+
`pi-profile-switch: --mcp-config points at another file — profile "${selection.name}" cannot filter MCP servers`,
|
|
349
|
+
"warning",
|
|
350
|
+
);
|
|
351
|
+
}
|
|
352
|
+
return;
|
|
353
|
+
}
|
|
354
|
+
if (!sync.changed) return;
|
|
355
|
+
notify(ctx, `profile "${selection.name}": MCP servers updated — reloading runtime`, "info");
|
|
356
|
+
await ctx.waitForIdle();
|
|
357
|
+
await ctx.reload();
|
|
358
|
+
}
|
|
359
|
+
|
|
242
360
|
/** Bare `/profile`: the interactive picker, with a list fallback for
|
|
243
361
|
* modes without dialogs. */
|
|
244
362
|
async function runPicker(ctx: ExtensionCommandContext, entries: ProfileListEntry[]): Promise<void> {
|
|
@@ -258,6 +376,7 @@ export default function piProfileExtension(pi: ExtensionAPI): void {
|
|
|
258
376
|
if (chosen === undefined || chosen.name === current?.selection.name) return;
|
|
259
377
|
const result = await activate(ctx, chosen.name, { force: true, overlay: null, persist: true });
|
|
260
378
|
notify(ctx, `profile active: ${result.selection.name}`, "info");
|
|
379
|
+
await reloadForMcpOverlay(ctx, result.selection);
|
|
261
380
|
}
|
|
262
381
|
|
|
263
382
|
/** `/profile create|duplicate|edit|delete`: TUI-only catalog CRUD. */
|
|
@@ -319,8 +438,9 @@ export default function piProfileExtension(pi: ExtensionAPI): void {
|
|
|
319
438
|
if (wizard === undefined) return;
|
|
320
439
|
await editProfile(scopeInput, wizard.scope, wizard.name, wizard.definition);
|
|
321
440
|
if (current !== undefined && name === current.selection.name) {
|
|
322
|
-
await activate(ctx, name, { persist: true });
|
|
441
|
+
const reactivated = await activate(ctx, name, { persist: true });
|
|
323
442
|
notify(ctx, `saved and reactivated profile "${name}"`, "info");
|
|
443
|
+
await reloadForMcpOverlay(ctx, reactivated.selection);
|
|
324
444
|
} else {
|
|
325
445
|
notify(ctx, `saved profile "${name}" (inactive — runtime untouched)`, "info");
|
|
326
446
|
}
|
|
@@ -358,16 +478,26 @@ export default function piProfileExtension(pi: ExtensionAPI): void {
|
|
|
358
478
|
if (isActive) {
|
|
359
479
|
const result = await activate(ctx, replacement ?? name, { force: replacement !== undefined, persist: true });
|
|
360
480
|
notify(ctx, `deleted "${name}" (${scope}); profile active: ${result.selection.name}`, "info");
|
|
481
|
+
await reloadForMcpOverlay(ctx, result.selection);
|
|
361
482
|
} else {
|
|
362
483
|
notify(ctx, `deleted profile "${name}" (${scope})`, "info");
|
|
363
484
|
}
|
|
364
485
|
}
|
|
365
486
|
|
|
366
|
-
pi.on("session_start", async (
|
|
487
|
+
pi.on("session_start", async (event, ctx) => {
|
|
367
488
|
setCurrent(ctx, undefined);
|
|
368
489
|
filterWarningShown = false;
|
|
490
|
+
if (seedWarning !== undefined) {
|
|
491
|
+
notify(ctx, seedWarning, "warning");
|
|
492
|
+
seedWarning = undefined;
|
|
493
|
+
}
|
|
369
494
|
const agentDir = getAgentDir();
|
|
370
495
|
const projectTrusted = ctx.isProjectTrusted();
|
|
496
|
+
// Every session start after the first (reload, new, resume, fork)
|
|
497
|
+
// continues the selection this process already applied. The `--profile`
|
|
498
|
+
// flag is a startup directive: re-reading it here would silently
|
|
499
|
+
// resurrect the profile the user just left.
|
|
500
|
+
const continuation = event.reason === "startup" ? undefined : appliedProfile();
|
|
371
501
|
const requested = readProfileFlag(pi);
|
|
372
502
|
let startup;
|
|
373
503
|
try {
|
|
@@ -376,6 +506,7 @@ export default function piProfileExtension(pi: ExtensionAPI): void {
|
|
|
376
506
|
cwd: ctx.cwd,
|
|
377
507
|
projectTrusted,
|
|
378
508
|
...(requested !== undefined ? { requested } : {}),
|
|
509
|
+
...(continuation !== undefined ? { continuation } : {}),
|
|
379
510
|
});
|
|
380
511
|
} catch (error) {
|
|
381
512
|
notify(ctx, error instanceof Error ? error.message : String(error), "error");
|
|
@@ -384,9 +515,17 @@ export default function piProfileExtension(pi: ExtensionAPI): void {
|
|
|
384
515
|
try {
|
|
385
516
|
// Startup activation never persists (a `--profile` selection is for
|
|
386
517
|
// this run only) and never applies a stored overlay.
|
|
387
|
-
await activate(ctx, startup.name, { persist: false, overlay: null });
|
|
518
|
+
const activated = await activate(ctx, startup.name, { persist: false, overlay: null });
|
|
388
519
|
reportWarnings(ctx, startup.warnings);
|
|
520
|
+
if (adapterInstalled) {
|
|
521
|
+
reportMcpOverlayState(ctx, startup.name, activated.selection, {
|
|
522
|
+
agentDir,
|
|
523
|
+
cwd: ctx.cwd,
|
|
524
|
+
projectTrusted,
|
|
525
|
+
});
|
|
526
|
+
}
|
|
389
527
|
} catch (error) {
|
|
528
|
+
recordAppliedProfile(undefined);
|
|
390
529
|
notify(ctx, error instanceof Error ? error.message : String(error), "error");
|
|
391
530
|
reportWarnings(ctx, startup.warnings);
|
|
392
531
|
}
|
|
@@ -498,6 +637,7 @@ export default function piProfileExtension(pi: ExtensionAPI): void {
|
|
|
498
637
|
case "use": {
|
|
499
638
|
const result = await activate(ctx, rest[0] as string, { force: true, overlay: null, persist: true });
|
|
500
639
|
notify(ctx, `profile active: ${result.selection.name}`, "info");
|
|
640
|
+
await reloadForMcpOverlay(ctx, result.selection);
|
|
501
641
|
return;
|
|
502
642
|
}
|
|
503
643
|
case "customize": {
|
|
@@ -578,12 +718,13 @@ export default function piProfileExtension(pi: ExtensionAPI): void {
|
|
|
578
718
|
// catalog; the stored overlay is preserved.
|
|
579
719
|
const overlay = (await new RuntimeStateStore(stateDirFor(profile.source, { agentDir, cwd: ctx.cwd })).read())
|
|
580
720
|
.overlay;
|
|
581
|
-
await activate(ctx, profile.name, { overlay: overlay ?? null, persist: true });
|
|
721
|
+
const reactivated = await activate(ctx, profile.name, { overlay: overlay ?? null, persist: true });
|
|
582
722
|
notify(
|
|
583
723
|
ctx,
|
|
584
|
-
`${action}d MCP server "${server}" in profile "${profile.name}" (
|
|
724
|
+
`${action}d MCP server "${server}" in profile "${profile.name}" (mcps: [${result.mcps.join(", ")}])`,
|
|
585
725
|
"info",
|
|
586
726
|
);
|
|
727
|
+
await reloadForMcpOverlay(ctx, reactivated.selection);
|
|
587
728
|
} catch (error) {
|
|
588
729
|
notify(ctx, error instanceof Error ? error.message : String(error), "error");
|
|
589
730
|
}
|
package/package.json
CHANGED
|
@@ -10,8 +10,8 @@
|
|
|
10
10
|
"additionalProperties": false,
|
|
11
11
|
"properties": {
|
|
12
12
|
"schemaVersion": {
|
|
13
|
-
"description": "
|
|
14
|
-
"
|
|
13
|
+
"description": "Catalog format version. Only 1 is accepted.",
|
|
14
|
+
"const": 1
|
|
15
15
|
},
|
|
16
16
|
"profiles": {
|
|
17
17
|
"type": "object",
|
|
@@ -44,19 +44,12 @@
|
|
|
44
44
|
},
|
|
45
45
|
"description": "Skill names or globs. Controls what the model sees in the system prompt's skills section; every loaded skill stays callable by the user through /skill:name."
|
|
46
46
|
},
|
|
47
|
-
"
|
|
47
|
+
"mcps": {
|
|
48
48
|
"type": "array",
|
|
49
49
|
"items": {
|
|
50
50
|
"type": "string"
|
|
51
51
|
},
|
|
52
|
-
"description": "
|
|
53
|
-
},
|
|
54
|
-
"mcp": {
|
|
55
|
-
"type": "array",
|
|
56
|
-
"items": {
|
|
57
|
-
"type": "string"
|
|
58
|
-
},
|
|
59
|
-
"description": "MCP server names or globs, discovered by pi-mcp-adapter configuration. Connection details stay in adapter-managed config."
|
|
52
|
+
"description": "MCP server names or globs, discovered by pi-mcp-adapter configuration. Connection details stay in adapter-managed config. The legacy key \"mcp\" is still accepted on read and migrates to \"mcps\" on the next save."
|
|
60
53
|
},
|
|
61
54
|
"tools": {
|
|
62
55
|
"type": "array",
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AdapterPresence: a cheap, deterministic "is pi-mcp-adapter installed?"
|
|
3
|
+
* check that does not depend on extension load order.
|
|
4
|
+
*
|
|
5
|
+
* The overlay mechanism only exists to serve the adapter; when the adapter is
|
|
6
|
+
* absent the extension must not register the `mcp-config` flag default and
|
|
7
|
+
* must not write anything. Signals, in order of reliability:
|
|
8
|
+
*
|
|
9
|
+
* 1. Pi's npm package root (`<agentDir>/npm/node_modules/pi-mcp-adapter`).
|
|
10
|
+
* 2. The command line (`-e <path>` / `--extension <path>`).
|
|
11
|
+
* 3. Pi settings `packages` entries.
|
|
12
|
+
* 4. The adapter's own event-bus presence probe, when it answered during
|
|
13
|
+
* extension loading (only reliable when the adapter loaded first).
|
|
14
|
+
*
|
|
15
|
+
* Any failure reads as "absent": a false negative only disables the overlay,
|
|
16
|
+
* while a false positive could hide the user's own Pi-global slot file.
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
20
|
+
import path from "node:path";
|
|
21
|
+
|
|
22
|
+
import { isRecord } from "./json-file.ts";
|
|
23
|
+
|
|
24
|
+
const ADAPTER_PACKAGE = "pi-mcp-adapter";
|
|
25
|
+
|
|
26
|
+
export interface AdapterPresenceInput {
|
|
27
|
+
agentDir: string;
|
|
28
|
+
argv: readonly string[];
|
|
29
|
+
/** Result of the adapter's event-bus probe, when the caller ran one. */
|
|
30
|
+
probeAnswered?: boolean;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function adapterPresent(input: AdapterPresenceInput): boolean {
|
|
34
|
+
if (input.probeAnswered === true) return true;
|
|
35
|
+
try {
|
|
36
|
+
for (const candidate of [
|
|
37
|
+
path.join(input.agentDir, "npm", "node_modules", ADAPTER_PACKAGE),
|
|
38
|
+
path.join(input.agentDir, "node_modules", ADAPTER_PACKAGE),
|
|
39
|
+
]) {
|
|
40
|
+
if (existsSync(candidate)) return true;
|
|
41
|
+
}
|
|
42
|
+
return argvMentionsAdapter(input.argv) || settingsListAdapter(input.agentDir);
|
|
43
|
+
} catch {
|
|
44
|
+
return false;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function argvMentionsAdapter(argv: readonly string[]): boolean {
|
|
49
|
+
for (let index = 0; index < argv.length; index++) {
|
|
50
|
+
const token = argv[index] ?? "";
|
|
51
|
+
if (token.includes(ADAPTER_PACKAGE)) return true;
|
|
52
|
+
if ((token === "-e" || token === "--extension") && (argv[index + 1] ?? "").includes(ADAPTER_PACKAGE)) {
|
|
53
|
+
return true;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
return false;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function settingsListAdapter(agentDir: string): boolean {
|
|
60
|
+
try {
|
|
61
|
+
const raw: unknown = JSON.parse(readFileSync(path.join(agentDir, "settings.json"), "utf8"));
|
|
62
|
+
if (!isRecord(raw) || !Array.isArray(raw.packages)) return false;
|
|
63
|
+
return raw.packages.some((entry) => {
|
|
64
|
+
const source =
|
|
65
|
+
typeof entry === "string"
|
|
66
|
+
? entry
|
|
67
|
+
: isRecord(entry) && typeof entry.source === "string"
|
|
68
|
+
? entry.source
|
|
69
|
+
: undefined;
|
|
70
|
+
return source !== undefined && source.includes(ADAPTER_PACKAGE);
|
|
71
|
+
});
|
|
72
|
+
} catch {
|
|
73
|
+
return false;
|
|
74
|
+
}
|
|
75
|
+
}
|