@oh-my-pi/pi-coding-agent 3.25.0 → 3.31.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 (157) hide show
  1. package/CHANGELOG.md +90 -0
  2. package/package.json +5 -5
  3. package/src/cli/args.ts +4 -0
  4. package/src/core/agent-session.ts +29 -2
  5. package/src/core/bash-executor.ts +2 -1
  6. package/src/core/custom-commands/bundled/review/index.ts +369 -14
  7. package/src/core/custom-commands/bundled/wt/index.ts +1 -1
  8. package/src/core/session-manager.ts +158 -246
  9. package/src/core/session-storage.ts +379 -0
  10. package/src/core/settings-manager.ts +155 -4
  11. package/src/core/system-prompt.ts +62 -64
  12. package/src/core/tools/ask.ts +5 -4
  13. package/src/core/tools/bash-interceptor.ts +26 -61
  14. package/src/core/tools/bash.ts +13 -8
  15. package/src/core/tools/complete.ts +2 -4
  16. package/src/core/tools/edit-diff.ts +11 -4
  17. package/src/core/tools/edit.ts +7 -13
  18. package/src/core/tools/find.ts +111 -50
  19. package/src/core/tools/gemini-image.ts +128 -147
  20. package/src/core/tools/grep.ts +397 -415
  21. package/src/core/tools/index.test.ts +5 -1
  22. package/src/core/tools/index.ts +6 -8
  23. package/src/core/tools/jtd-to-json-schema.ts +174 -196
  24. package/src/core/tools/ls.ts +12 -10
  25. package/src/core/tools/lsp/client.ts +58 -9
  26. package/src/core/tools/lsp/config.ts +205 -656
  27. package/src/core/tools/lsp/defaults.json +465 -0
  28. package/src/core/tools/lsp/index.ts +55 -32
  29. package/src/core/tools/lsp/rust-analyzer.ts +49 -10
  30. package/src/core/tools/lsp/types.ts +1 -0
  31. package/src/core/tools/lsp/utils.ts +1 -1
  32. package/src/core/tools/read.ts +152 -76
  33. package/src/core/tools/render-utils.ts +70 -10
  34. package/src/core/tools/review.ts +38 -126
  35. package/src/core/tools/task/artifacts.ts +5 -4
  36. package/src/core/tools/task/executor.ts +204 -67
  37. package/src/core/tools/task/index.ts +129 -92
  38. package/src/core/tools/task/name-generator.ts +1544 -214
  39. package/src/core/tools/task/parallel.ts +30 -3
  40. package/src/core/tools/task/render.ts +85 -39
  41. package/src/core/tools/task/types.ts +34 -11
  42. package/src/core/tools/task/worker.ts +152 -27
  43. package/src/core/tools/web-fetch.ts +220 -1657
  44. package/src/core/tools/web-scrapers/academic.test.ts +239 -0
  45. package/src/core/tools/web-scrapers/artifacthub.ts +215 -0
  46. package/src/core/tools/web-scrapers/arxiv.ts +88 -0
  47. package/src/core/tools/web-scrapers/aur.ts +175 -0
  48. package/src/core/tools/web-scrapers/biorxiv.ts +141 -0
  49. package/src/core/tools/web-scrapers/bluesky.ts +284 -0
  50. package/src/core/tools/web-scrapers/brew.ts +177 -0
  51. package/src/core/tools/web-scrapers/business.test.ts +82 -0
  52. package/src/core/tools/web-scrapers/cheatsh.ts +78 -0
  53. package/src/core/tools/web-scrapers/chocolatey.ts +158 -0
  54. package/src/core/tools/web-scrapers/choosealicense.ts +110 -0
  55. package/src/core/tools/web-scrapers/cisa-kev.ts +100 -0
  56. package/src/core/tools/web-scrapers/clojars.ts +180 -0
  57. package/src/core/tools/web-scrapers/coingecko.ts +184 -0
  58. package/src/core/tools/web-scrapers/crates-io.ts +128 -0
  59. package/src/core/tools/web-scrapers/crossref.ts +149 -0
  60. package/src/core/tools/web-scrapers/dev-platforms.test.ts +254 -0
  61. package/src/core/tools/web-scrapers/devto.ts +177 -0
  62. package/src/core/tools/web-scrapers/discogs.ts +308 -0
  63. package/src/core/tools/web-scrapers/discourse.ts +221 -0
  64. package/src/core/tools/web-scrapers/dockerhub.ts +160 -0
  65. package/src/core/tools/web-scrapers/documentation.test.ts +85 -0
  66. package/src/core/tools/web-scrapers/fdroid.ts +158 -0
  67. package/src/core/tools/web-scrapers/finance-media.test.ts +144 -0
  68. package/src/core/tools/web-scrapers/firefox-addons.ts +214 -0
  69. package/src/core/tools/web-scrapers/flathub.ts +239 -0
  70. package/src/core/tools/web-scrapers/git-hosting.test.ts +272 -0
  71. package/src/core/tools/web-scrapers/github-gist.ts +68 -0
  72. package/src/core/tools/web-scrapers/github.ts +455 -0
  73. package/src/core/tools/web-scrapers/gitlab.ts +456 -0
  74. package/src/core/tools/web-scrapers/go-pkg.ts +275 -0
  75. package/src/core/tools/web-scrapers/hackage.ts +94 -0
  76. package/src/core/tools/web-scrapers/hackernews.ts +208 -0
  77. package/src/core/tools/web-scrapers/hex.ts +121 -0
  78. package/src/core/tools/web-scrapers/huggingface.ts +385 -0
  79. package/src/core/tools/web-scrapers/iacr.ts +86 -0
  80. package/src/core/tools/web-scrapers/index.ts +250 -0
  81. package/src/core/tools/web-scrapers/jetbrains-marketplace.ts +169 -0
  82. package/src/core/tools/web-scrapers/lemmy.ts +220 -0
  83. package/src/core/tools/web-scrapers/lobsters.ts +186 -0
  84. package/src/core/tools/web-scrapers/mastodon.ts +310 -0
  85. package/src/core/tools/web-scrapers/maven.ts +152 -0
  86. package/src/core/tools/web-scrapers/mdn.ts +174 -0
  87. package/src/core/tools/web-scrapers/media.test.ts +138 -0
  88. package/src/core/tools/web-scrapers/metacpan.ts +253 -0
  89. package/src/core/tools/web-scrapers/musicbrainz.ts +273 -0
  90. package/src/core/tools/web-scrapers/npm.ts +114 -0
  91. package/src/core/tools/web-scrapers/nuget.ts +205 -0
  92. package/src/core/tools/web-scrapers/nvd.ts +243 -0
  93. package/src/core/tools/web-scrapers/ollama.ts +267 -0
  94. package/src/core/tools/web-scrapers/open-vsx.ts +119 -0
  95. package/src/core/tools/web-scrapers/opencorporates.ts +275 -0
  96. package/src/core/tools/web-scrapers/openlibrary.ts +319 -0
  97. package/src/core/tools/web-scrapers/orcid.ts +299 -0
  98. package/src/core/tools/web-scrapers/osv.ts +189 -0
  99. package/src/core/tools/web-scrapers/package-managers-2.test.ts +199 -0
  100. package/src/core/tools/web-scrapers/package-managers.test.ts +171 -0
  101. package/src/core/tools/web-scrapers/package-registries.test.ts +259 -0
  102. package/src/core/tools/web-scrapers/packagist.ts +174 -0
  103. package/src/core/tools/web-scrapers/pub-dev.ts +185 -0
  104. package/src/core/tools/web-scrapers/pubmed.ts +178 -0
  105. package/src/core/tools/web-scrapers/pypi.ts +129 -0
  106. package/src/core/tools/web-scrapers/rawg.ts +124 -0
  107. package/src/core/tools/web-scrapers/readthedocs.ts +126 -0
  108. package/src/core/tools/web-scrapers/reddit.ts +104 -0
  109. package/src/core/tools/web-scrapers/repology.ts +262 -0
  110. package/src/core/tools/web-scrapers/research.test.ts +107 -0
  111. package/src/core/tools/web-scrapers/rfc.ts +209 -0
  112. package/src/core/tools/web-scrapers/rubygems.ts +117 -0
  113. package/src/core/tools/web-scrapers/searchcode.ts +217 -0
  114. package/src/core/tools/web-scrapers/sec-edgar.ts +274 -0
  115. package/src/core/tools/web-scrapers/security.test.ts +103 -0
  116. package/src/core/tools/web-scrapers/semantic-scholar.ts +190 -0
  117. package/src/core/tools/web-scrapers/snapcraft.ts +200 -0
  118. package/src/core/tools/web-scrapers/social-extended.test.ts +192 -0
  119. package/src/core/tools/web-scrapers/social.test.ts +259 -0
  120. package/src/core/tools/web-scrapers/sourcegraph.ts +373 -0
  121. package/src/core/tools/web-scrapers/spdx.ts +121 -0
  122. package/src/core/tools/web-scrapers/spotify.ts +218 -0
  123. package/src/core/tools/web-scrapers/stackexchange.test.ts +120 -0
  124. package/src/core/tools/web-scrapers/stackoverflow.ts +124 -0
  125. package/src/core/tools/web-scrapers/standards.test.ts +122 -0
  126. package/src/core/tools/web-scrapers/terraform.ts +304 -0
  127. package/src/core/tools/web-scrapers/tldr.ts +51 -0
  128. package/src/core/tools/web-scrapers/twitter.ts +96 -0
  129. package/src/core/tools/web-scrapers/types.ts +234 -0
  130. package/src/core/tools/web-scrapers/utils.ts +162 -0
  131. package/src/core/tools/web-scrapers/vimeo.ts +152 -0
  132. package/src/core/tools/web-scrapers/vscode-marketplace.ts +195 -0
  133. package/src/core/tools/web-scrapers/w3c.ts +163 -0
  134. package/src/core/tools/web-scrapers/wikidata.ts +357 -0
  135. package/src/core/tools/web-scrapers/wikipedia.test.ts +73 -0
  136. package/src/core/tools/web-scrapers/wikipedia.ts +95 -0
  137. package/src/core/tools/web-scrapers/youtube.test.ts +198 -0
  138. package/src/core/tools/web-scrapers/youtube.ts +371 -0
  139. package/src/core/tools/write.ts +21 -18
  140. package/src/core/voice.ts +3 -2
  141. package/src/lib/worktree/collapse.ts +2 -1
  142. package/src/lib/worktree/git.ts +2 -18
  143. package/src/main.ts +59 -3
  144. package/src/modes/interactive/components/extensions/extension-dashboard.ts +33 -19
  145. package/src/modes/interactive/components/extensions/extension-list.ts +15 -8
  146. package/src/modes/interactive/components/hook-editor.ts +2 -1
  147. package/src/modes/interactive/components/model-selector.ts +19 -4
  148. package/src/modes/interactive/interactive-mode.ts +41 -38
  149. package/src/modes/interactive/theme/theme.ts +58 -58
  150. package/src/modes/rpc/rpc-mode.ts +10 -9
  151. package/src/prompts/review-request.md +27 -0
  152. package/src/prompts/reviewer.md +64 -68
  153. package/src/prompts/tools/output.md +22 -3
  154. package/src/prompts/tools/task.md +32 -33
  155. package/src/utils/clipboard.ts +2 -1
  156. package/src/utils/tools-manager.ts +110 -8
  157. package/examples/extensions/subagent/agents/reviewer.md +0 -35
@@ -21,7 +21,6 @@ import { formatDuration } from "../render-utils";
21
21
  import { cleanupTempDir, createTempArtifactsDir, getArtifactsDir } from "./artifacts";
22
22
  import { discoverAgents, getAgent } from "./discovery";
23
23
  import { runSubprocess } from "./executor";
24
- import { generateTaskName } from "./name-generator";
25
24
  import { mapWithConcurrencyLimit } from "./parallel";
26
25
  import { renderCall, renderResult } from "./render";
27
26
  import {
@@ -135,17 +134,34 @@ export async function createTaskTool(
135
134
  execute: async (_toolCallId, params, signal, onUpdate) => {
136
135
  const startTime = Date.now();
137
136
  const { agents, projectAgentsDir } = await discoverAgents(session.cwd);
138
- const context = params.context;
139
- const outputSchema = params.output_schema;
137
+ const { agent: agentName, context, model, output: outputSchema } = params;
138
+
139
+ // Validate agent exists
140
+ const agent = getAgent(agents, agentName);
141
+ if (!agent) {
142
+ const available = agents.map((a) => a.name).join(", ") || "none";
143
+ return {
144
+ content: [
145
+ {
146
+ type: "text",
147
+ text: `Unknown agent "${agentName}". Available: ${available}`,
148
+ },
149
+ ],
150
+ details: {
151
+ projectAgentsDir,
152
+ results: [],
153
+ totalDurationMs: 0,
154
+ },
155
+ };
156
+ }
140
157
 
141
158
  // Handle empty or missing tasks
142
159
  if (!params.tasks || params.tasks.length === 0) {
143
- const available = agents.map((a) => a.name).join(", ") || "none";
144
160
  return {
145
161
  content: [
146
162
  {
147
163
  type: "text",
148
- text: `No tasks provided. Use: { tasks: [{agent, task}, ...] }\nAvailable agents: ${available}`,
164
+ text: `No tasks provided. Use: { agent, context, tasks: [{id, task, description}, ...] }`,
149
165
  },
150
166
  ],
151
167
  details: {
@@ -173,6 +189,56 @@ export async function createTaskTool(
173
189
  };
174
190
  }
175
191
 
192
+ const tasks = params.tasks;
193
+ const missingTaskIndexes: number[] = [];
194
+ const idIndexes = new Map<string, number[]>();
195
+
196
+ for (let i = 0; i < tasks.length; i++) {
197
+ const id = tasks[i]?.id;
198
+ if (typeof id !== "string" || id.trim() === "") {
199
+ missingTaskIndexes.push(i);
200
+ continue;
201
+ }
202
+ const normalizedId = id.toLowerCase();
203
+ const indexes = idIndexes.get(normalizedId);
204
+ if (indexes) {
205
+ indexes.push(i);
206
+ } else {
207
+ idIndexes.set(normalizedId, [i]);
208
+ }
209
+ }
210
+
211
+ const duplicateIds: Array<{ id: string; indexes: number[] }> = [];
212
+ for (const [normalizedId, indexes] of idIndexes.entries()) {
213
+ if (indexes.length > 1) {
214
+ duplicateIds.push({
215
+ id: tasks[indexes[0]]?.id ?? normalizedId,
216
+ indexes,
217
+ });
218
+ }
219
+ }
220
+
221
+ if (missingTaskIndexes.length > 0 || duplicateIds.length > 0) {
222
+ const problems: string[] = [];
223
+ if (missingTaskIndexes.length > 0) {
224
+ problems.push(`Missing task ids at indexes: ${missingTaskIndexes.join(", ")}`);
225
+ }
226
+ if (duplicateIds.length > 0) {
227
+ const details = duplicateIds
228
+ .map((entry) => `${entry.id} (indexes ${entry.indexes.join(", ")})`)
229
+ .join("; ");
230
+ problems.push(`Duplicate task ids detected (case-insensitive): ${details}`);
231
+ }
232
+ return {
233
+ content: [{ type: "text", text: `Invalid tasks: ${problems.join(". ")}` }],
234
+ details: {
235
+ projectAgentsDir,
236
+ results: [],
237
+ totalDurationMs: 0,
238
+ },
239
+ };
240
+ }
241
+
176
242
  // Derive artifacts directory
177
243
  const sessionFile = session.getSessionFile();
178
244
  const artifactsDir = sessionFile ? getArtifactsDir(sessionFile) : null;
@@ -197,88 +263,59 @@ export async function createTaskTool(
197
263
  };
198
264
 
199
265
  try {
200
- let tasks = params.tasks;
201
- let skippedSelfRecursion = 0;
202
-
203
- // Filter out blocked agent (self-recursion prevention)
204
- if (blockedAgent) {
205
- const blockedTasks = tasks.filter((t) => t.agent === blockedAgent);
206
- tasks = tasks.filter((t) => t.agent !== blockedAgent);
207
- skippedSelfRecursion = blockedTasks.length;
208
-
209
- if (skippedSelfRecursion > 0 && tasks.length === 0) {
210
- return {
211
- content: [
212
- {
213
- type: "text",
214
- text: `Cannot spawn ${blockedAgent} agent from within itself (recursion prevention). Use a different agent type.`,
215
- },
216
- ],
217
- details: {
218
- projectAgentsDir,
219
- results: [],
220
- totalDurationMs: Date.now() - startTime,
266
+ // Check self-recursion prevention
267
+ if (blockedAgent && agentName === blockedAgent) {
268
+ return {
269
+ content: [
270
+ {
271
+ type: "text",
272
+ text: `Cannot spawn ${blockedAgent} agent from within itself (recursion prevention). Use a different agent type.`,
221
273
  },
222
- };
223
- }
224
- }
225
-
226
- // Validate all agents exist
227
- for (const task of tasks) {
228
- if (!getAgent(agents, task.agent)) {
229
- const available = agents.map((a) => a.name).join(", ");
230
- return {
231
- content: [{ type: "text", text: `Unknown agent: ${task.agent}. Available: ${available}` }],
232
- details: {
233
- projectAgentsDir,
234
- results: [],
235
- totalDurationMs: Date.now() - startTime,
236
- },
237
- };
238
- }
274
+ ],
275
+ details: {
276
+ projectAgentsDir,
277
+ results: [],
278
+ totalDurationMs: Date.now() - startTime,
279
+ },
280
+ };
239
281
  }
240
282
 
241
283
  // Check spawn restrictions from parent
242
284
  const parentSpawns = session.getSessionSpawns() ?? "*";
243
285
  const allowedSpawns = parentSpawns.split(",").map((s) => s.trim());
244
- const isSpawnAllowed = (agentName: string): boolean => {
286
+ const isSpawnAllowed = (): boolean => {
245
287
  if (parentSpawns === "") return false; // Empty = deny all
246
288
  if (parentSpawns === "*") return true; // Wildcard = allow all
247
289
  return allowedSpawns.includes(agentName);
248
290
  };
249
291
 
250
- for (const task of tasks) {
251
- if (!isSpawnAllowed(task.agent)) {
252
- const allowed = parentSpawns === "" ? "none (spawns disabled for this agent)" : parentSpawns;
253
- return {
254
- content: [{ type: "text", text: `Cannot spawn '${task.agent}'. Allowed: ${allowed}` }],
255
- details: {
256
- projectAgentsDir,
257
- results: [],
258
- totalDurationMs: Date.now() - startTime,
259
- },
260
- };
261
- }
292
+ if (!isSpawnAllowed()) {
293
+ const allowed = parentSpawns === "" ? "none (spawns disabled for this agent)" : parentSpawns;
294
+ return {
295
+ content: [{ type: "text", text: `Cannot spawn '${agentName}'. Allowed: ${allowed}` }],
296
+ details: {
297
+ projectAgentsDir,
298
+ results: [],
299
+ totalDurationMs: Date.now() - startTime,
300
+ },
301
+ };
262
302
  }
263
303
 
264
- // Build full prompts with context prepended and generate task IDs
304
+ // Build full prompts with context prepended
265
305
  const tasksWithContext = tasks.map((t) => ({
266
- agent: t.agent,
267
306
  task: context ? `${context}\n\n${t.task}` : t.task,
268
- model: t.model,
269
307
  description: t.description,
270
- taskId: generateTaskName(),
308
+ taskId: t.id,
271
309
  }));
272
310
 
273
311
  // Initialize progress for all tasks
274
312
  for (let i = 0; i < tasksWithContext.length; i++) {
275
313
  const t = tasksWithContext[i];
276
- const agentCfg = getAgent(agents, t.agent);
277
314
  progressMap.set(i, {
278
315
  index: i,
279
316
  taskId: t.taskId,
280
- agent: t.agent,
281
- agentSource: agentCfg?.source ?? "user",
317
+ agent: agentName,
318
+ agentSource: agent.source,
282
319
  status: "pending",
283
320
  task: t.task,
284
321
  recentTools: [],
@@ -286,36 +323,40 @@ export async function createTaskTool(
286
323
  toolCount: 0,
287
324
  tokens: 0,
288
325
  durationMs: 0,
289
- modelOverride: t.model,
326
+ modelOverride: model,
290
327
  description: t.description,
291
328
  });
292
329
  }
293
330
  emitProgress();
294
331
 
295
332
  // Execute in parallel with concurrency limit
296
- const results = await mapWithConcurrencyLimit(tasksWithContext, MAX_CONCURRENCY, async (task, index) => {
297
- const agent = getAgent(agents, task.agent)!;
298
- return runSubprocess({
299
- cwd: session.cwd,
300
- agent,
301
- task: task.task,
302
- description: task.description,
303
- index,
304
- taskId: task.taskId,
305
- context: undefined, // Already prepended above
306
- modelOverride: task.model,
307
- outputSchema,
308
- sessionFile,
309
- persistArtifacts: !!artifactsDir,
310
- artifactsDir: effectiveArtifactsDir,
311
- signal,
312
- eventBus: undefined,
313
- onProgress: (progress) => {
314
- progressMap.set(index, structuredClone(progress));
315
- emitProgress();
316
- },
317
- });
318
- });
333
+ const results = await mapWithConcurrencyLimit(
334
+ tasksWithContext,
335
+ MAX_CONCURRENCY,
336
+ async (task, index) => {
337
+ return runSubprocess({
338
+ cwd: session.cwd,
339
+ agent,
340
+ task: task.task,
341
+ description: task.description,
342
+ index,
343
+ taskId: task.taskId,
344
+ context: undefined, // Already prepended above
345
+ modelOverride: model,
346
+ outputSchema,
347
+ sessionFile,
348
+ persistArtifacts: !!artifactsDir,
349
+ artifactsDir: effectiveArtifactsDir,
350
+ signal,
351
+ eventBus: undefined,
352
+ onProgress: (progress) => {
353
+ progressMap.set(index, structuredClone(progress));
354
+ emitProgress();
355
+ },
356
+ });
357
+ },
358
+ signal,
359
+ );
319
360
 
320
361
  // Aggregate usage from executor results (already accumulated incrementally)
321
362
  const aggregatedUsage = createUsageTotals();
@@ -349,14 +390,10 @@ export async function createTaskTool(
349
390
  return `[${r.agent}] ${status}${meta} ${r.taskId}\n${preview}`;
350
391
  });
351
392
 
352
- const skippedNote =
353
- skippedSelfRecursion > 0
354
- ? ` (${skippedSelfRecursion} ${blockedAgent} task${skippedSelfRecursion > 1 ? "s" : ""} skipped - self-recursion blocked)`
355
- : "";
356
393
  const outputIds = results.map((r) => r.taskId);
357
394
  const outputHint =
358
395
  outputIds.length > 0 ? `\n\nUse output tool for full logs: output ids ${outputIds.join(", ")}` : "";
359
- const summary = `${successCount}/${results.length} succeeded${skippedNote} [${formatDuration(
396
+ const summary = `${successCount}/${results.length} succeeded [${formatDuration(
360
397
  totalDuration,
361
398
  )}]\n\n${summaries.join("\n\n---\n\n")}${outputHint}`;
362
399