oh-pi 0.1.49 → 0.1.50

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.fr.md CHANGED
@@ -45,13 +45,14 @@ Vous avez déjà une config ? oh-pi la détecte et propose une **sauvegarde avan
45
45
  ├── settings.json Modèle, thème, niveau de réflexion
46
46
  ├── keybindings.json Raccourcis Vim/Emacs (optionnel)
47
47
  ├── AGENTS.md Directives IA par rôle
48
- ├── extensions/ 7 extensions (6 par défaut + colonie)
48
+ ├── extensions/ 8 extensions (7 par défaut + colonie)
49
49
  │ ├── safe-guard Confirmation des commandes dangereuses + protection des chemins
50
50
  │ ├── git-guard Points de contrôle stash auto + alerte dépôt sale
51
51
  │ ├── auto-session Nommage de session depuis le premier message
52
52
  │ ├── custom-footer Barre d'état améliorée (token/coût/temps/git/cwd)
53
53
  │ ├── compact-header Informations de démarrage simplifiées
54
54
  │ ├── auto-update Vérification des mises à jour au lancement
55
+ │ ├── bg-process ⏳ **Bg Process** — Mise en arrière-plan automatique des commandes longues (serveurs dev, etc.)
55
56
  │ └── ant-colony/ 🐜 Essaim multi-agents autonome (optionnel)
56
57
  ├── prompts/ 10 modèles (/review /fix /commit /test ...)
57
58
  ├── skills/ 11 compétences (outils + design UI + workflows)
@@ -68,14 +69,11 @@ Vous avez déjà une config ? oh-pi la détecte et propose une **sauvegarde avan
68
69
 
69
70
  ### Préréglages
70
71
 
71
- | | Thème | Réflexion | Inclut |
72
- |---|-------|-----------|--------|
73
- | 🟢 Débutant | oh-pi Dark | medium | Sécurité + bases git |
74
- | 🔵 Pro | Catppuccin | high | Chaîne d'outils complète |
75
- | 🟣 Chercheur en sécurité | Cyberpunk | high | Audit + pentest |
76
- | 🟠 Data & IA | Tokyo Night | medium | MLOps + pipelines |
77
- | 🔴 Minimal | Default | off | Noyau uniquement |
78
- | ⚫ Pleine puissance | oh-pi Dark | high | Tout + colonie de fourmis |
72
+ | | Inclut |
73
+ |---|--------|
74
+ | 🟢 **Complet** | Toutes extensions + colonie + bg-process |
75
+ | 🔵 **Propre** | Aucune extension |
76
+ | 🟣 **Colonie** | Colonie uniquement |
79
77
 
80
78
  ### Fournisseurs
81
79
 
@@ -133,12 +131,23 @@ Les vraies colonies de fourmis résolvent des problèmes complexes sans contrôl
133
131
 
134
132
  En mode interactif, la colonie affiche la progression en direct :
135
133
 
136
- - **Widget** — fourmis actives et leur flux de sortie
137
- - **Barre de statut** progression des tâches, nombre actif, coût
134
+ - **Barre de statut** — footer compact avec métriques réelles : tâches terminées, fourmis actives, appels d'outils, tokens de sortie, coût, durée
135
+ - **Ctrl+Shift+A** — panneau de détails en overlay avec liste des tâches, flux des fourmis actives et journal de la colonie
138
136
  - **Notification** — résumé à la fin
139
137
 
140
138
  Utilisez `/colony-stop` pour arrêter une colonie en cours.
141
139
 
140
+ ### Protocole de signaux
141
+
142
+ La colonie communique avec la conversation principale via des signaux structurés, empêchant le LLM de vérifier ou deviner l'état :
143
+
144
+ | Signal | Signification |
145
+ |--------|---------------|
146
+ | `COLONY_SIGNAL:LAUNCHED` | Colonie démarrée — ne pas vérifier |
147
+ | `COLONY_SIGNAL:RUNNING` | Colonie active — injecté à chaque tour |
148
+ | `COLONY_SIGNAL:COMPLETE` | Colonie terminée — consulter le rapport |
149
+ | `COLONY_SIGNAL:FAILED` | Colonie crashée — signaler l'erreur |
150
+
142
151
  ### Contrôle des tours
143
152
 
144
153
  Chaque fourmi a un budget strict de tours pour éviter les exécutions incontrôlées :
@@ -174,7 +183,7 @@ Le LLM décide quand déployer la colonie. Vous n'avez pas à y penser :
174
183
  La colonie trouve automatiquement le parallélisme optimal pour votre machine :
175
184
 
176
185
  ```
177
- Démarrage à froid → 1-2 fourmis (conservateur)
186
+ Démarrage à froid → ceil(max/2) fourmis (démarrage rapide)
178
187
  Exploration → +1 par vague, surveillance du débit
179
188
  Débit ↓ → verrouiller l'optimal, stabiliser
180
189
  CPU > 85% → réduire immédiatement
package/README.md CHANGED
@@ -45,13 +45,14 @@ Already have a config? oh-pi detects it and offers **backup before overwriting**
45
45
  ├── settings.json Model, theme, thinking level
46
46
  ├── keybindings.json Vim/Emacs shortcuts (optional)
47
47
  ├── AGENTS.md Role-specific AI guidelines
48
- ├── extensions/ 7 extensions (6 default + ant-colony)
48
+ ├── extensions/ 8 extensions (7 default + ant-colony)
49
49
  │ ├── safe-guard Dangerous command confirmation + path protection
50
50
  │ ├── git-guard Auto stash checkpoints + dirty repo warning
51
51
  │ ├── auto-session Session naming from first message
52
52
  │ ├── custom-footer Enhanced status bar (token/cost/time/git/cwd)
53
53
  │ ├── compact-header Streamlined startup info
54
54
  │ ├── auto-update Check for updates on launch
55
+ │ ├── bg-process ⏳ **Bg Process** — Auto-background long-running commands (dev servers, etc.)
55
56
  │ └── ant-colony/ 🐜 Autonomous multi-agent swarm (optional)
56
57
  ├── prompts/ 10 templates (/review /fix /commit /test ...)
57
58
  ├── skills/ 11 skills (tools + UI design + workflows)
@@ -70,12 +71,9 @@ Already have a config? oh-pi detects it and offers **backup before overwriting**
70
71
 
71
72
  | | Theme | Thinking | Includes |
72
73
  |---|-------|----------|----------|
73
- | 🟢 Starter | oh-pi Dark | medium | Safety + git basics |
74
- | 🔵 Pro Developer | Catppuccin | high | Full toolchain |
75
- | 🟣 Security Researcher | Cyberpunk | high | Audit + pentesting |
76
- | 🟠 Data & AI | Tokyo Night | medium | MLOps + pipelines |
77
- | 🔴 Minimal | Default | off | Core only |
78
- | ⚫ Full Power | oh-pi Dark | high | Everything + ant colony |
74
+ | Full Power | oh-pi Dark | high | All extensions + bg-process + ant-colony |
75
+ | 🔴 Clean | Default | off | No extensions, just core |
76
+ | 🐜 Colony Only | oh-pi Dark | medium | Ant-colony with minimal setup |
79
77
 
80
78
  ### Providers
81
79
 
@@ -133,12 +131,23 @@ Real ant colonies solve complex problems without central control. Each ant follo
133
131
 
134
132
  In interactive mode, the colony shows live progress:
135
133
 
136
- - **Widget** — active ants and their current output stream
137
- - **Status bar** — task progress, active count, cost
134
+ - **Status bar** — compact footer with real metrics: tasks done, active ants, tool calls, output tokens, cost, elapsed time
135
+ - **Ctrl+Shift+A** — overlay detail panel with task list, active ant streams, and colony log
138
136
  - **Notification** — completion summary when done
139
137
 
140
138
  Use `/colony-stop` to abort a running colony.
141
139
 
140
+ ### Signal Protocol
141
+
142
+ The colony communicates with the main conversation via structured signals, preventing the LLM from polling or guessing colony status:
143
+
144
+ | Signal | Meaning |
145
+ |--------|---------|
146
+ | `COLONY_SIGNAL:LAUNCHED` | Colony started — don't poll |
147
+ | `COLONY_SIGNAL:RUNNING` | Colony active — injected each turn |
148
+ | `COLONY_SIGNAL:COMPLETE` | Colony finished — review report |
149
+ | `COLONY_SIGNAL:FAILED` | Colony crashed — report error |
150
+
142
151
  ### Turn Control
143
152
 
144
153
  Each ant has a strict turn budget to prevent runaway execution:
@@ -174,7 +183,7 @@ The LLM decides when to deploy the colony. You don't have to think about it:
174
183
  The colony automatically finds the optimal parallelism for your machine:
175
184
 
176
185
  ```
177
- Cold start → 1-2 ants (conservative)
186
+ Cold start → ceil(max/2) ants (fast ramp-up)
178
187
  Exploration → +1 each wave, monitoring throughput
179
188
  Throughput ↓ → lock optimal, stabilize
180
189
  CPU > 85% → reduce immediately
package/README.zh.md CHANGED
@@ -45,13 +45,14 @@ pi # 开始编码
45
45
  ├── settings.json 模型、主题、思维级别
46
46
  ├── keybindings.json Vim/Emacs 快捷键(可选)
47
47
  ├── AGENTS.md 角色专属 AI 指南
48
- ├── extensions/ 7 个扩展(6 个默认 + 蚁群)
48
+ ├── extensions/ 8 个扩展(7 个默认 + 蚁群)
49
49
  │ ├── safe-guard 危险命令确认 + 路径保护
50
50
  │ ├── git-guard 自动 stash 检查点 + 脏仓库警告
51
51
  │ ├── auto-session 从首条消息自动命名会话
52
52
  │ ├── custom-footer 增强状态栏(token/成本/时间/git/cwd)
53
53
  │ ├── compact-header 精简启动信息
54
54
  │ ├── auto-update 启动时检查更新
55
+ │ ├── bg-process ⏳ **后台进程** — 自动后台化长时间运行的命令(开发服务器等)
55
56
  │ └── ant-colony/ 🐜 自主多智能体蚁群系统(可选)
56
57
  ├── prompts/ 10 个模板(/review /fix /commit /test ...)
57
58
  ├── skills/ 11 个技能(工具 + UI 设计 + 工作流)
@@ -70,12 +71,9 @@ pi # 开始编码
70
71
 
71
72
  | | 主题 | 思维 | 包含 |
72
73
  |---|------|------|------|
73
- | 🟢 入门 | oh-pi Dark | medium | 安全 + git 基础 |
74
- | 🔵 专业开发 | Catppuccin | high | 全工具链 |
75
- | 🟣 安全研究 | Cyberpunk | high | 审计 + 渗透测试 |
76
- | 🟠 数据 & AI | Tokyo Night | medium | MLOps + 管道 |
77
- | 🔴 极简 | Default | off | 仅核心 |
78
- | ⚫ 全功率 | oh-pi Dark | high | 全部 + 蚁群 |
74
+ | 🟢 全功能 | oh-pi Dark | high | 全部扩展含蚁群 + 后台进程 |
75
+ | 🔵 干净 | Default | off | 无扩展仅核心 |
76
+ | 🟣 仅蚁群 | oh-pi Dark | medium | 蚁群 + 最小配置 |
79
77
 
80
78
  ### 提供商
81
79
 
@@ -133,12 +131,23 @@ pi(主进程)
133
131
 
134
132
  交互模式下,蚁群显示实时进度:
135
133
 
136
- - **Widget**活跃蚂蚁及其当前输出流
137
- - **状态栏**任务进度、活跃数量、成本
134
+ - **状态栏**紧凑 footer 显示真实指标:完成任务数、活跃蚂蚁、工具调用次数、输出 token 数、成本、耗时
135
+ - **Ctrl+Shift+A**弹出详情面板,显示任务列表、活跃蚂蚁流、蚁群日志
138
136
  - **通知** — 完成时显示汇总
139
137
 
140
138
  使用 `/colony-stop` 中止运行中的蚁群。
141
139
 
140
+ ### 信号协议
141
+
142
+ 蚁群通过结构化信号与主对话通信,防止 LLM 轮询或猜测蚁群状态:
143
+
144
+ | 信号 | 含义 |
145
+ |------|------|
146
+ | `COLONY_SIGNAL:LAUNCHED` | 蚁群已启动 — 不要轮询 |
147
+ | `COLONY_SIGNAL:RUNNING` | 蚁群运行中 — 每轮注入 |
148
+ | `COLONY_SIGNAL:COMPLETE` | 蚁群完成 — 查看报告 |
149
+ | `COLONY_SIGNAL:FAILED` | 蚁群崩溃 — 报告错误 |
150
+
142
151
  ### 轮次控制
143
152
 
144
153
  每只蚂蚁有严格的轮次预算,防止失控执行:
@@ -174,7 +183,7 @@ LLM 自行决定何时部署蚁群,你不需要操心:
174
183
  蚁群自动找到你机器的最优并行度:
175
184
 
176
185
  ```
177
- 冷启动 → 1-2 只蚂蚁(保守)
186
+ 冷启动 → ceil(max/2) 只蚂蚁(快速启动)
178
187
  探索阶段 → 每波 +1,监控吞吐量
179
188
  吞吐量下降 → 锁定最优值,稳定运行
180
189
  CPU > 85% → 立即降低
package/dist/i18n.js CHANGED
@@ -63,18 +63,12 @@ const messages = {
63
63
  "provider.detectedAddHint": "Configure additional providers",
64
64
  // preset
65
65
  "preset.select": "Choose a preset:",
66
- "preset.starter": "🟢 Starter",
67
- "preset.starterHint": "New to AI coding? Start here",
68
- "preset.pro": "🔵 Pro Developer",
69
- "preset.proHint": "Full-stack dev with all the bells and whistles",
70
- "preset.security": "🟣 Security Researcher",
71
- "preset.securityHint": "Pentesting, auditing, vulnerability research",
72
- "preset.dataai": "🟠 Data & AI Engineer",
73
- "preset.dataaiHint": "MLOps, data pipelines, AI applications",
74
- "preset.minimal": "🔴 Minimal",
75
- "preset.minimalHint": "Just the core, nothing extra",
76
66
  "preset.full": "⚫ Full Power",
77
- "preset.fullHint": "Everything installed, ant colony included",
67
+ "preset.fullHint": "All features, extensions, and skills enabled",
68
+ "preset.clean": "✨ Clean",
69
+ "preset.cleanHint": "Minimal setup, just the essentials",
70
+ "preset.colony": "🐜 Colony",
71
+ "preset.colonyHint": "Ant swarm multi-agent system",
78
72
  // theme
79
73
  "theme.select": "Choose a theme:",
80
74
  // keybindings
@@ -199,18 +193,12 @@ const messages = {
199
193
  "provider.detectedAdd": "➕ 添加新 Provider",
200
194
  "provider.detectedAddHint": "配置额外的 Provider",
201
195
  "preset.select": "选择预设方案:",
202
- "preset.starter": "🟢 入门",
203
- "preset.starterHint": "AI 编程新手?从这里开始",
204
- "preset.pro": "🔵 专业开发者",
205
- "preset.proHint": "全栈开发,功能齐全",
206
- "preset.security": "🟣 安全研究员",
207
- "preset.securityHint": "渗透测试、审计、漏洞研究",
208
- "preset.dataai": "🟠 数据与 AI 工程师",
209
- "preset.dataaiHint": "MLOps、数据管道、AI 应用",
210
- "preset.minimal": "🔴 极简",
211
- "preset.minimalHint": "仅核心功能",
212
196
  "preset.full": "⚫ 全功能",
213
- "preset.fullHint": "全部安装,含蚁群模式",
197
+ "preset.fullHint": "所有功能,包括蚁群系统",
198
+ "preset.clean": "🟢 简洁",
199
+ "preset.cleanHint": "核心功能,无蚁群",
200
+ "preset.colony": "🐜 蚁群",
201
+ "preset.colonyHint": "多 Agent 协同系统",
214
202
  "theme.select": "选择主题:",
215
203
  "kb.select": "快捷键方案:",
216
204
  "kb.default": "⌨️ 默认",
@@ -328,18 +316,12 @@ const messages = {
328
316
  "provider.detectedAdd": "➕ Ajouter de nouveaux fournisseurs",
329
317
  "provider.detectedAddHint": "Configurer des fournisseurs supplémentaires",
330
318
  "preset.select": "Choisir un préréglage :",
331
- "preset.starter": "🟢 Débutant",
332
- "preset.starterHint": "Nouveau en codage IA ? Commencez ici",
333
- "preset.pro": "🔵 Développeur Pro",
334
- "preset.proHint": "Full-stack avec toutes les options",
335
- "preset.security": "🟣 Chercheur en sécurité",
336
- "preset.securityHint": "Pentest, audit, recherche de vulnérabilités",
337
- "preset.dataai": "🟠 Ingénieur Data & IA",
338
- "preset.dataaiHint": "MLOps, pipelines de données, applications IA",
339
- "preset.minimal": "🔴 Minimal",
340
- "preset.minimalHint": "Juste l'essentiel",
341
- "preset.full": "⚫ Pleine puissance",
342
- "preset.fullHint": "Tout installé, colonie de fourmis incluse",
319
+ "preset.full": " Complet",
320
+ "preset.fullHint": "Toutes les extensions et compétences",
321
+ "preset.clean": "🟢 Propre",
322
+ "preset.cleanHint": "Configuration minimale, aucune extension",
323
+ "preset.colony": "🐜 Colonie",
324
+ "preset.colonyHint": "Mode essaim multi-agent de fourmis",
343
325
  "theme.select": "Choisir un thème :",
344
326
  "kb.select": "Schéma de raccourcis :",
345
327
  "kb.default": "⌨️ Par défaut",
@@ -1,55 +1,28 @@
1
1
  import * as p from "@clack/prompts";
2
2
  import { t } from "../i18n.js";
3
3
  const PRESETS = {
4
- starter: {
5
- labelKey: "preset.starter", hintKey: "preset.starterHint",
6
- config: {
7
- theme: "dark", keybindings: "default", thinking: "medium",
8
- extensions: ["safe-guard", "git-guard", "auto-session-name", "custom-footer", "compact-header", "auto-update"],
9
- prompts: ["review", "fix", "explain", "commit"],
10
- agents: "general-developer",
11
- },
12
- },
13
- pro: {
14
- labelKey: "preset.pro", hintKey: "preset.proHint",
15
- config: {
16
- theme: "catppuccin-mocha", keybindings: "default", thinking: "high",
17
- extensions: ["safe-guard", "git-guard", "auto-session-name", "custom-footer", "compact-header", "auto-update"],
18
- prompts: ["review", "fix", "explain", "commit", "test", "refactor", "optimize", "document", "pr"],
19
- agents: "fullstack-developer",
20
- },
21
- },
22
- security: {
23
- labelKey: "preset.security", hintKey: "preset.securityHint",
24
- config: {
25
- theme: "cyberpunk", keybindings: "default", thinking: "high",
26
- extensions: ["safe-guard", "custom-footer", "compact-header", "auto-update"],
27
- prompts: ["review", "security", "fix", "explain"],
28
- agents: "security-researcher",
29
- },
30
- },
31
- dataai: {
32
- labelKey: "preset.dataai", hintKey: "preset.dataaiHint",
4
+ full: {
5
+ labelKey: "preset.full", hintKey: "preset.fullHint",
33
6
  config: {
34
- theme: "tokyo-night", keybindings: "default", thinking: "medium",
35
- extensions: ["safe-guard", "git-guard", "auto-session-name", "custom-footer", "compact-header", "auto-update"],
36
- prompts: ["review", "fix", "explain", "optimize", "document", "test"],
37
- agents: "data-ai-engineer",
7
+ theme: "dark", keybindings: "default", thinking: "high",
8
+ extensions: ["safe-guard", "git-guard", "auto-session-name", "custom-footer", "compact-header", "ant-colony", "auto-update", "bg-process"],
9
+ prompts: ["review", "fix", "explain", "commit", "test", "refactor", "optimize", "security", "document", "pr"],
10
+ agents: "colony-operator",
38
11
  },
39
12
  },
40
- minimal: {
41
- labelKey: "preset.minimal", hintKey: "preset.minimalHint",
13
+ clean: {
14
+ labelKey: "preset.clean", hintKey: "preset.cleanHint",
42
15
  config: {
43
16
  theme: "dark", keybindings: "default", thinking: "off",
44
17
  extensions: [], prompts: [], agents: "general-developer",
45
18
  },
46
19
  },
47
- full: {
48
- labelKey: "preset.full", hintKey: "preset.fullHint",
20
+ colony: {
21
+ labelKey: "preset.colony", hintKey: "preset.colonyHint",
49
22
  config: {
50
- theme: "dark", keybindings: "default", thinking: "high",
51
- extensions: ["safe-guard", "git-guard", "auto-session-name", "custom-footer", "compact-header", "ant-colony", "auto-update"],
52
- prompts: ["review", "fix", "explain", "commit", "test", "refactor", "optimize", "security", "document", "pr"],
23
+ theme: "dark", keybindings: "default", thinking: "medium",
24
+ extensions: ["ant-colony", "auto-session-name", "compact-header"],
25
+ prompts: ["review", "fix", "explain", "commit"],
53
26
  agents: "colony-operator",
54
27
  },
55
28
  },
package/dist/types.js CHANGED
@@ -49,6 +49,7 @@ export const EXTENSIONS = [
49
49
  { name: "compact-header", label: "⚡ Compact Header — Dense startup info replacing verbose output", default: true },
50
50
  { name: "ant-colony", label: "🐜 Ant Colony — Autonomous multi-agent swarm with adaptive concurrency", default: false },
51
51
  { name: "auto-update", label: "🔄 Auto Update — Check for oh-pi updates on startup and notify", default: true },
52
+ { name: "bg-process", label: "⏳ Bg Process — Auto-background long-running commands (dev servers, etc.)", default: false },
52
53
  ];
53
54
  /** 快捷键绑定方案(default / vim / emacs) */
54
55
  export const KEYBINDING_SCHEMES = {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "oh-pi",
3
- "version": "0.1.49",
3
+ "version": "0.1.50",
4
4
  "description": "One-click setup for pi-coding-agent. Like oh-my-zsh for pi.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -0,0 +1,209 @@
1
+ /**
2
+ * oh-pi Background Process Extension
3
+ *
4
+ * 任何 bash 命令超时未完成时,自动送到后台执行。
5
+ * 提供 bg_status 工具让 LLM 查看/停止后台进程。
6
+ */
7
+ import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
8
+ import { Type } from "@sinclair/typebox";
9
+ import { StringEnum } from "@mariozechner/pi-ai";
10
+ import { spawn, execSync } from "node:child_process";
11
+ import { writeFileSync, readFileSync, existsSync } from "node:fs";
12
+
13
+ /** 超时阈值(毫秒),超过此时间自动后台化 */
14
+ const BG_TIMEOUT_MS = 10_000;
15
+
16
+ interface BgProcess {
17
+ pid: number;
18
+ command: string;
19
+ logFile: string;
20
+ startedAt: number;
21
+ }
22
+
23
+ export default function (pi: ExtensionAPI) {
24
+ const bgProcesses = new Map<number, BgProcess>();
25
+
26
+ // 覆盖内置 bash 工具
27
+ pi.registerTool({
28
+ name: "bash",
29
+ label: "Bash",
30
+ description: `Execute a bash command. Output is truncated to 2000 lines or 50KB. If a command runs longer than ${BG_TIMEOUT_MS / 1000}s, it is automatically backgrounded and you get the PID + log file path. Use the bg_status tool to check on backgrounded processes.`,
31
+ parameters: Type.Object({
32
+ command: Type.String({ description: "Bash command to execute" }),
33
+ timeout: Type.Optional(Type.Number({ description: "Timeout in seconds (optional)" })),
34
+ }),
35
+ async execute(toolCallId, params, signal) {
36
+ const { command } = params;
37
+ const userTimeout = params.timeout ? params.timeout * 1000 : undefined;
38
+ const effectiveTimeout = userTimeout ?? BG_TIMEOUT_MS;
39
+
40
+ return new Promise((resolve) => {
41
+ let stdout = "";
42
+ let stderr = "";
43
+ let settled = false;
44
+
45
+ const child = spawn("bash", ["-c", command], {
46
+ cwd: process.cwd(),
47
+ env: { ...process.env },
48
+ stdio: ["ignore", "pipe", "pipe"],
49
+ });
50
+
51
+ child.stdout?.on("data", (d: Buffer) => { stdout += d.toString(); });
52
+ child.stderr?.on("data", (d: Buffer) => { stderr += d.toString(); });
53
+
54
+ // 超时处理:分离进程,送到后台
55
+ const timer = setTimeout(() => {
56
+ if (settled) return;
57
+ settled = true;
58
+
59
+ // 分离子进程,让它继续运行
60
+ child.stdout?.removeAllListeners();
61
+ child.stderr?.removeAllListeners();
62
+ child.removeAllListeners();
63
+ child.unref();
64
+
65
+ const logFile = `/tmp/oh-pi-bg-${Date.now()}.log`;
66
+ const pid = child.pid!;
67
+
68
+ // 启动一个 tail 进程把后续输出写入日志
69
+ try {
70
+ const tailCmd = `(echo ${JSON.stringify(stdout + stderr)}; tail --pid=${pid} -f /proc/${pid}/fd/1 2>/dev/null) > ${logFile} 2>&1 &`;
71
+ spawn("bash", ["-c", tailCmd], { detached: true, stdio: "ignore" }).unref();
72
+ } catch {
73
+ // fallback: 至少把已有输出写入日志
74
+ writeFileSync(logFile, stdout + stderr);
75
+ }
76
+
77
+ bgProcesses.set(pid, { pid, command, logFile, startedAt: Date.now() });
78
+
79
+ const preview = (stdout + stderr).slice(0, 500);
80
+ const text = `Command still running after ${effectiveTimeout / 1000}s, moved to background.\nPID: ${pid}\nLog: ${logFile}\nView output: tail -f ${logFile}\nStop: kill ${pid}\n\nOutput so far:\n${preview}`;
81
+
82
+ resolve({
83
+ content: [{ type: "text", text }],
84
+ details: {},
85
+ });
86
+ }, effectiveTimeout);
87
+
88
+ // 正常结束
89
+ child.on("close", (code) => {
90
+ if (settled) return;
91
+ settled = true;
92
+ clearTimeout(timer);
93
+
94
+ const output = (stdout + stderr).trim();
95
+ const exitInfo = code !== 0 ? `\n[Exit code: ${code}]` : "";
96
+
97
+ resolve({
98
+ content: [{ type: "text", text: output + exitInfo }],
99
+ details: {},
100
+ });
101
+ });
102
+
103
+ child.on("error", (err) => {
104
+ if (settled) return;
105
+ settled = true;
106
+ clearTimeout(timer);
107
+
108
+ resolve({
109
+ content: [{ type: "text", text: `Error: ${err.message}` }],
110
+ details: {},
111
+ isError: true,
112
+ });
113
+ });
114
+
115
+ // 处理 abort signal
116
+ if (signal) {
117
+ signal.addEventListener("abort", () => {
118
+ if (settled) return;
119
+ settled = true;
120
+ clearTimeout(timer);
121
+ try { child.kill(); } catch {}
122
+ resolve({
123
+ content: [{ type: "text", text: "Command cancelled." }],
124
+ details: {},
125
+ });
126
+ }, { once: true });
127
+ }
128
+ });
129
+ },
130
+ });
131
+
132
+ // bg_status 工具:查看/管理后台进程
133
+ pi.registerTool({
134
+ name: "bg_status",
135
+ label: "Background Process Status",
136
+ description: "Check status, view output, or stop background processes that were auto-backgrounded.",
137
+ parameters: Type.Object({
138
+ action: StringEnum(["list", "log", "stop"] as const, { description: "list=show all, log=view output, stop=kill process" }),
139
+ pid: Type.Optional(Type.Number({ description: "PID of the process (required for log/stop)" })),
140
+ }),
141
+ async execute(toolCallId, params) {
142
+ const { action, pid } = params;
143
+
144
+ if (action === "list") {
145
+ if (bgProcesses.size === 0) {
146
+ return { content: [{ type: "text", text: "No background processes." }], details: {} };
147
+ }
148
+ const lines = [...bgProcesses.values()].map((p) => {
149
+ const alive = isAlive(p.pid);
150
+ const status = alive ? "🟢 running" : "⚪ stopped";
151
+ return `PID: ${p.pid} | ${status} | Log: ${p.logFile}\n Cmd: ${p.command}`;
152
+ });
153
+ return { content: [{ type: "text", text: lines.join("\n\n") }], details: {} };
154
+ }
155
+
156
+ if (!pid) {
157
+ return { content: [{ type: "text", text: "Error: pid is required for log/stop" }], details: {}, isError: true };
158
+ }
159
+
160
+ const proc = bgProcesses.get(pid);
161
+
162
+ if (action === "log") {
163
+ const logFile = proc?.logFile;
164
+ if (logFile && existsSync(logFile)) {
165
+ try {
166
+ const content = readFileSync(logFile, "utf-8");
167
+ const tail = content.slice(-5000);
168
+ const truncated = content.length > 5000 ? `[...truncated, showing last 5000 chars]\n${tail}` : tail;
169
+ return { content: [{ type: "text", text: truncated || "(empty)" }], details: {} };
170
+ } catch (e: any) {
171
+ return { content: [{ type: "text", text: `Error reading log: ${e.message}` }], details: {}, isError: true };
172
+ }
173
+ }
174
+ // fallback: 直接读 /proc
175
+ try {
176
+ const out = execSync(`tail -20 /proc/${pid}/fd/1 2>/dev/null || echo "(cannot read output)"`, { timeout: 3000 }).toString();
177
+ return { content: [{ type: "text", text: out }], details: {} };
178
+ } catch {
179
+ return { content: [{ type: "text", text: "No log available for this PID." }], details: {} };
180
+ }
181
+ }
182
+
183
+ if (action === "stop") {
184
+ try {
185
+ process.kill(pid, "SIGTERM");
186
+ bgProcesses.delete(pid);
187
+ return { content: [{ type: "text", text: `Process ${pid} terminated.` }], details: {} };
188
+ } catch {
189
+ bgProcesses.delete(pid);
190
+ return { content: [{ type: "text", text: `Process ${pid} not found (already stopped?).` }], details: {} };
191
+ }
192
+ }
193
+
194
+ return { content: [{ type: "text", text: `Unknown action: ${action}` }], details: {}, isError: true };
195
+ },
196
+ });
197
+
198
+ // 清理:退出时杀掉所有后台进程
199
+ pi.on("session_shutdown", async () => {
200
+ for (const [pid] of bgProcesses) {
201
+ try { process.kill(pid, "SIGTERM"); } catch {}
202
+ }
203
+ bgProcesses.clear();
204
+ });
205
+ }
206
+
207
+ function isAlive(pid: number): boolean {
208
+ try { process.kill(pid, 0); return true; } catch { return false; }
209
+ }