@rohaquinlop/pi-subagents 0.2.0 → 0.3.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.
Files changed (3) hide show
  1. package/README.md +21 -0
  2. package/index.ts +70 -0
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -159,6 +159,27 @@ Built-in tools (`read`, `write`, `edit`, `bash`, `grep`, `find`, `ls`) work auto
159
159
 
160
160
  The `subagent` tool itself is listed in `CUSTOM_TOOL_EXTENSIONS` pointing back to this extension's own `index.ts` — that's how an agent like `worker` can recursively spawn other agents. Recursion is bounded only by each agent's `subagent_agents` allowlist (e.g. worker can spawn scout/researcher, neither of which declares the `subagent` tool, so the chain stops at depth 2).
161
161
 
162
+ ### 4. Model-specific extensions (no manual mapping)
163
+
164
+ Extensions that apply to specific models (not tools) can declare `appliesToModels`
165
+ in their `package.json` and auto-load without any `CUSTOM_TOOL_EXTENSIONS` mapping:
166
+
167
+ ```json
168
+ {
169
+ "pi": {
170
+ "extensions": ["./extensions/index.ts"],
171
+ "appliesToModels": ["deepseek-*"]
172
+ }
173
+ }
174
+ ```
175
+
176
+ When a subagent's configured model matches one of the patterns, the extension is
177
+ loaded via `--extension` in the child process. Glob patterns (`*` wildcard) match
178
+ the **model ID** (the portion after the last `/`, e.g. `deepseek-v4-flash` in
179
+ `nan/deepseek-v4-flash`). Plain strings without wildcards match the **provider
180
+ name** (the portion before the `/`, e.g. `nan` in `nan/deepseek-v4-flash`).
181
+ Matching is case-insensitive.
182
+
162
183
  ## Structure
163
184
 
164
185
  ```
package/index.ts CHANGED
@@ -156,6 +156,43 @@ function buildCustomToolExtensions(): Record<string, string> {
156
156
 
157
157
  const CUSTOM_TOOL_EXTENSIONS: Record<string, string> = buildCustomToolExtensions();
158
158
 
159
+ interface ModelExtension {
160
+ patterns: string[];
161
+ path: string;
162
+ }
163
+
164
+ function buildModelExtensions(): ModelExtension[] {
165
+ const result: ModelExtension[] = [];
166
+ try {
167
+ const entries = fs.readdirSync(EXT_BASE, { withFileTypes: true });
168
+ for (const entry of entries) {
169
+ if (!entry.isDirectory()) continue;
170
+ const pkgPath = path.join(EXT_BASE, entry.name, "package.json");
171
+ if (!fs.existsSync(pkgPath)) continue;
172
+ try {
173
+ const pkg = JSON.parse(fs.readFileSync(pkgPath, "utf-8"));
174
+ const patterns: unknown = pkg?.pi?.appliesToModels;
175
+ const extEntries: unknown = pkg?.pi?.extensions;
176
+ if (!Array.isArray(patterns) || patterns.length === 0) continue;
177
+ if (!patterns.every((p: unknown): p is string => typeof p === "string")) continue;
178
+ if (!Array.isArray(extEntries) || extEntries.length === 0) continue;
179
+ if (!extEntries.every((e: unknown): e is string => typeof e === "string")) continue;
180
+ const extPath = path.join(EXT_BASE, entry.name, extEntries[0]);
181
+ if (fs.existsSync(extPath)) {
182
+ result.push({ patterns, path: extPath });
183
+ }
184
+ } catch {
185
+ // skip corrupted package.json
186
+ }
187
+ }
188
+ } catch {
189
+ // skip if EXT_BASE doesn't exist
190
+ }
191
+ return result;
192
+ }
193
+
194
+ const MODEL_EXTENSIONS: ModelExtension[] = buildModelExtensions();
195
+
159
196
  // ── Agent Discovery & Registration ────────────────────────────────────
160
197
 
161
198
  let agents: AgentConfig[] = [];
@@ -300,6 +337,29 @@ function truncLine(text: string, maxWidth: number): string {
300
337
 
301
338
  // ── Subagent Execution ────────────────────────────────────────────────
302
339
 
340
+ /**
341
+ * Match a model string against a glob-style pattern (supports only `*` as wildcard).
342
+ * Pattern "deepseek-*" matches "nan/deepseek-v4-flash" and "deepseek-v4-pro".
343
+ * Plain strings without wildcards match the provider name (portion before `/`).
344
+ */
345
+ function matchModelPattern(model: string, pattern: string): boolean {
346
+ const slashIdx = model.indexOf("/");
347
+ const hasProvider = slashIdx !== -1;
348
+ const modelId = hasProvider ? model.slice(slashIdx + 1) : model;
349
+ const provider = hasProvider ? model.slice(0, slashIdx) : "";
350
+
351
+ // Pattern without wildcards: match against provider name
352
+ if (!pattern.includes("*") && hasProvider) {
353
+ return provider.toLowerCase() === pattern.toLowerCase();
354
+ }
355
+
356
+ // Glob pattern: match against model ID
357
+ // Escape regex-special characters, then convert escaped * back to .*
358
+ const escaped = pattern.replace(/[.*+?^${}()|[\]\\]/g, "\\$&").replace(/\\\*/g, ".*");
359
+ const regex = new RegExp("^" + escaped + "$", "i");
360
+ return regex.test(modelId);
361
+ }
362
+
303
363
  async function buildPiArgs(
304
364
  agent: AgentConfig,
305
365
  task: string,
@@ -348,6 +408,16 @@ async function buildPiArgs(
348
408
  args.push("--no-tools");
349
409
  }
350
410
 
411
+ // Auto-load model-specific extensions (e.g. deepseek-cache)
412
+ for (const me of MODEL_EXTENSIONS) {
413
+ for (const pattern of me.patterns) {
414
+ if (matchModelPattern(agent.model, pattern)) {
415
+ extensionPaths.add(me.path);
416
+ break;
417
+ }
418
+ }
419
+ }
420
+
351
421
  for (const extPath of extensionPaths) {
352
422
  args.push("--extension", extPath);
353
423
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rohaquinlop/pi-subagents",
3
- "version": "0.2.0",
3
+ "version": "0.3.0",
4
4
  "description": "Pi extension for delegating tasks to subagents — parallel execution, agent discovery, and TUI rendering",
5
5
  "keywords": [
6
6
  "pi-package",