@oldflag2333333/pi-subagent 0.1.2 → 0.1.3

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 CHANGED
@@ -117,9 +117,28 @@ Add these fields to `config.json` as needed:
117
117
  | `"model": "provider/model-id"` | Choose a model available in your Pi configuration. |
118
118
  | `"thinkingLevel": "high"` | Request a thinking level supported by the selected model. |
119
119
  | `"skills": ["skill-name", "./path/to/SKILL.md"]` | Include installed skills or paths relative to the profile. |
120
+ | `"extensions": ["npm:pi-model-calibration", "./hooks.ts"]` | Explicitly load extra extensions in Subs, including prompt/event plugins that provide no tools. |
120
121
 
121
122
  For a persistent specialist started with `/sub:reviewer`, invoking the command again from the same saved Main session resumes it after closure. A different Main session gets its own specialist. Manual-only profiles are not available for Pi to delegate on its own.
122
123
 
124
+ ### Sub extension allowlist
125
+
126
+ Add this to the profile's `config.json`, for example:
127
+
128
+ ```json
129
+ "extensions": ["npm:pi-model-calibration"]
130
+ ```
131
+
132
+ Entries accept Pi's npm, `git:`, HTTP(S) repository URLs, `builtin:` identifiers, and local extension files or package directories. Relative local paths resolve from the **configuration file's directory**; absolute paths and `~/` work too. Missing local paths fail startup; repeated entries load once. Package specs are resolved by Pi in the Sub and may download or install packages, never while Main reads the profile.
133
+
134
+ `npm:` / `git:` entries use Pi's temporary package cache; a globally installed package may not be present there. For offline use, list the installed entry file directly, for example `"extensions": ["~/.pi/agent/npm/node_modules/pi-model-calibration/index.ts"]`, to avoid another download.
135
+
136
+ Omitting the field or using `[]` loads no extra plugins. Subs still use `--no-extensions`: only communication/Herdr integration, normal MCP support, selected tools' extension dependencies, and this allowlist load explicitly. Other Main plugins are not inherited. Project profiles still require project trust; same-named overrides replace rather than merge allowlists.
137
+
138
+ This field controls **Sub launches only**; it does not dynamically load extensions in the current process started with `pi --profile`. Use Pi's `-e` or normal extension configuration for that case. Loading an extension does not automatically add its tools to the profile's `tools`; custom non-MCP tools must still be available in Main.
139
+
140
+ Extensions can execute code, read context, and change prompts. **This is not a sandbox**; list only trusted plugins. After editing, reload Main and close/reopen affected Subs. For `model-calibration`, its rules must also match the Sub's selected model before calibration text is injected.
141
+
123
142
  ### Profile-specific skills
124
143
 
125
144
  Put skills inside the profile directory to load them only when that profile is selected. No `skills` field is needed:
@@ -165,7 +184,7 @@ Messages use custom inbox cards and Pi’s native run preparation, without extra
165
184
  - **Subs have separate conversations.** They receive their task, profile, and normal project context—not Main's chat history. Include the details they need in the task.
166
185
  - **Reloading Main does not close Subs.** Close tabs you no longer need. Hard crashes may require manual cleanup.
167
186
 
168
- If a profile is missing or fails to start, run `/profiles` first. Check its directory/name match, project trust, and any referenced tools or skills. If Herdr is unavailable, confirm Pi is running inside a Herdr workspace.
187
+ If a profile is missing or fails to start, run `/profiles` first. Check its directory/name match, project trust, and any referenced tools, skills, or extensions. If Herdr is unavailable, confirm Pi is running inside a Herdr workspace.
169
188
 
170
189
  To hide or show Pi Subagent Subs in Herdr's Agents panel, see the optional [Herdr companion plugin](herdr-plugin/README.md).
171
190
 
package/README.zh-CN.md CHANGED
@@ -117,9 +117,28 @@ pi --profile reviewer
117
117
  | `"model": "provider/model-id"` | 指定 Pi 配置中可用的模型。 |
118
118
  | `"thinkingLevel": "high"` | 请求所选模型支持的思考级别。 |
119
119
  | `"skills": ["skill-name", "./path/to/SKILL.md"]` | 加载已安装的技能,或使用相对于角色配置的路径。 |
120
+ | `"extensions": ["npm:pi-model-calibration", "./hooks.ts"]` | 为子助手显式加载额外扩展,包括不提供工具的提示词或事件插件。 |
120
121
 
121
122
  通过 `/sub:reviewer` 启动的持久化助手,在关闭后,可以从同一个已保存的主会话中再次执行该命令来恢复。不同的主会话各自拥有独立的助手。设置为手动调用的角色不能由 Pi 自行委派。
122
123
 
124
+ ### 子助手扩展白名单
125
+
126
+ 在角色的 `config.json` 中添加,例如:
127
+
128
+ ```json
129
+ "extensions": ["npm:pi-model-calibration"]
130
+ ```
131
+
132
+ 支持 Pi 的 npm、`git:`、HTTP(S) 仓库地址、`builtin:` 标识,以及本地扩展文件或包目录。本地相对路径以 **配置文件所在目录** 为基准,也支持绝对路径和 `~/`。本地路径不存在时,启动会报错;重复条目只加载一次。包来源交给子助手中的 Pi 解析,可能触发下载或安装,不会在主 Pi 读取角色配置时执行。
133
+
134
+ `npm:` / `git:` 条目使用 Pi 的临时包缓存,已全局安装的包不一定在该缓存中;离线使用时,可直接列出已安装的入口文件,例如 `"extensions": ["~/.pi/agent/npm/node_modules/pi-model-calibration/index.ts"]`,避免重新下载。
135
+
136
+ 不配置或设置 `[]` 时,不额外加载插件。子助手仍使用 `--no-extensions`:只显式加载通信与 Herdr 集成、常规 MCP 支持、角色工具依赖的扩展,以及此白名单,不会自动继承主 Pi 的其他插件。项目角色仍须经过项目信任,同名角色覆盖时不会合并白名单。
137
+
138
+ 这个字段只控制 **Sub 启动**,不会给使用 `pi --profile` 的当前进程动态加载扩展;该场景仍需使用 Pi 的 `-e` 或常规扩展配置。扩展的加载不会自动将其工具加入角色的 `tools`;自定义非 MCP 工具仍须在主 Pi 中可用。
139
+
140
+ 扩展可以执行代码、读取上下文并修改提示词,**白名单不是沙箱**,只添加你信任的插件。修改后重载主 Pi,并关闭、重新打开相关子助手。对于 `model-calibration`,加载后还需其规则匹配子助手所选模型才会注入校准提示词。
141
+
123
142
  ### 角色专属技能
124
143
 
125
144
  将技能放在角色目录中,就只会在选中该角色时加载,无需填写 `skills` 字段:
@@ -165,7 +184,7 @@ reviewer/
165
184
  - **子助手拥有独立对话。** 它会收到任务、角色配置和常规项目上下文,但不会收到主对话的聊天记录。请在任务中提供必要信息。
166
185
  - **重载主 Pi 不会关闭子助手。** 不再需要时请关闭标签页;异常崩溃后可能需要手动清理。
167
186
 
168
- 如果找不到角色或启动失败,先运行 `/profiles`,检查目录名与 `name` 是否一致、项目是否受信任,以及引用的工具和技能是否可用。如果提示 Herdr 不可用,请确认 Pi 正在 Herdr 工作区内运行。
187
+ 如果找不到角色或启动失败,先运行 `/profiles`,检查目录名与 `name` 是否一致、项目是否受信任,以及引用的工具、技能和扩展是否可用。如果提示 Herdr 不可用,请确认 Pi 正在 Herdr 工作区内运行。
169
188
 
170
189
  如需在 Herdr 的 Agents 面板中隐藏或显示 Pi Subagent 子助手,可安装可选的 [Herdr 配套插件](herdr-plugin/README.md)(英文说明)。
171
190
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@oldflag2333333/pi-subagent",
3
- "version": "0.1.2",
3
+ "version": "0.1.3",
4
4
  "description": "Reusable specialist profiles and independent sub-agents for Pi",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -26,6 +26,7 @@ const ALLOWED_KEYS = new Set([
26
26
  "invocation",
27
27
  "tools",
28
28
  "skills",
29
+ "extensions",
29
30
  "instructions",
30
31
  ]);
31
32
 
@@ -48,6 +49,7 @@ function parseProfile(value: unknown, expectedName: string): ProfileDefinition {
48
49
  }
49
50
  if (!stringArray(value.tools, 128) || value.tools.length === 0) throw new Error("tools must be a non-empty string array with at most 128 entries");
50
51
  if (value.skills !== undefined && !stringArray(value.skills, 128)) throw new Error("skills must be a string array with at most 128 entries");
52
+ if (value.extensions !== undefined && !stringArray(value.extensions, 128)) throw new Error("extensions must be a string array with at most 128 entries");
51
53
  if (value.sessionPersistence !== undefined && value.sessionPersistence !== "ephemeral" && value.sessionPersistence !== "persistent") {
52
54
  throw new Error("sessionPersistence must be 'ephemeral' or 'persistent'");
53
55
  }
@@ -76,6 +78,7 @@ function parseProfile(value: unknown, expectedName: string): ProfileDefinition {
76
78
  ? { sessionPersistence: value.sessionPersistence }
77
79
  : {}),
78
80
  ...(Array.isArray(value.skills) ? { skills: [...new Set(value.skills as string[])] } : {}),
81
+ ...(Array.isArray(value.extensions) ? { extensions: [...new Set((value.extensions as string[]).map((entry) => entry.trim()))] } : {}),
79
82
  ...(typeof value.instructions === "string" ? { instructions: value.instructions } : {}),
80
83
  };
81
84
  }
@@ -294,6 +297,23 @@ function discoverProfileSkills(profile: LoadedProfile): string[] {
294
297
  return skills;
295
298
  }
296
299
 
300
+ function resolveExtension(reference: string, profile: LoadedProfile): string {
301
+ // Pi resolves package and built-in specs in the Sub process. Do not install
302
+ // packages or execute extension code while reading profiles in Main.
303
+ if (/^(?:npm:|git:|https?:\/\/|builtin:)/.test(reference)) return reference;
304
+ const resolved = path.resolve(path.dirname(profile.sourcePath), expandHome(reference));
305
+ let stat: fs.Stats;
306
+ try {
307
+ stat = fs.statSync(resolved);
308
+ } catch {
309
+ throw new Error(`Profile '${profile.name}' extension is missing or unreadable: ${resolved}.`);
310
+ }
311
+ if (!stat.isFile() && !stat.isDirectory()) {
312
+ throw new Error(`Profile '${profile.name}' has an unsupported extension path: ${resolved}.`);
313
+ }
314
+ return resolved;
315
+ }
316
+
297
317
  export function resolveProfile(name: string, cwd: string, includeProject: boolean, options: { agentOnly?: boolean } = {}): ResolvedProfile {
298
318
  const catalog = loadProfiles(cwd, includeProject);
299
319
  const invalid = catalog.diagnostics.map((item) => `${item.path}: ${item.message}`);
@@ -320,6 +340,6 @@ export function resolveProfile(name: string, cwd: string, includeProject: boolea
320
340
  return {
321
341
  ...profile,
322
342
  resolvedSkills,
323
- resolvedExtensions: [],
343
+ resolvedExtensions: [...new Set((profile.extensions ?? []).map((extension) => resolveExtension(extension, profile)))],
324
344
  };
325
345
  }
@@ -16,7 +16,8 @@ interface ToolSource {
16
16
 
17
17
  export function resolveToolExtensions(profile: ResolvedProfile, tools: ToolSource[]): ResolvedProfile {
18
18
  const available = new Map(tools.map((tool) => [tool.name, tool]));
19
- const extensionPaths = new Set<string>();
19
+ // Keep explicitly allowlisted event-only extensions alongside tool providers.
20
+ const extensionPaths = new Set<string>(profile.resolvedExtensions);
20
21
  for (const name of profile.tools) {
21
22
  // Native MCP connects asynchronously; do not require its tools in Main yet.
22
23
  if (isMcpTool(name)) continue;
@@ -13,6 +13,8 @@ export interface ProfileDefinition {
13
13
  invocation?: ProfileInvocation;
14
14
  tools: string[];
15
15
  skills?: string[];
16
+ /** Additional extensions explicitly loaded in Subs, not inherited from Main. */
17
+ extensions?: string[];
16
18
  instructions?: string;
17
19
  }
18
20