oh-my-im 0.1.14 → 0.1.17
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 +21 -16
- package/dist/agents/codex-agent.js +7 -3
- package/dist/agents/index.js +6 -5
- package/dist/agents/opencode-agent.js +156 -0
- package/dist/agents/pi-agent.js +23 -2
- package/dist/agents/process-utils.js +9 -0
- package/dist/bot-app.js +97 -46
- package/dist/bot-worker.js +25 -8
- package/dist/config.js +12 -4
- package/dist/dashboard-worker.js +53 -10
- package/dist/dingtalk.js +1 -1
- package/dist/dws-dashboard.js +67 -29
- package/dist/group-worker.js +98 -44
- package/dist/monitor-command.js +3 -1
- package/package.json +1 -1
- package/outputs/session-flowcharts/01-agent-session-create.png +0 -0
- package/outputs/session-flowcharts/02-pi-session-rpc.png +0 -0
- package/outputs/session-flowcharts/03-session-list-switch.png +0 -0
- package/outputs/session-flowcharts/04-private-chat-message-flow.png +0 -0
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# oh-my-im
|
|
2
2
|
|
|
3
|
-
`oh-my-im` 是一个运行在本机的钉钉 AI Agent 桥接器。它接收钉钉单聊和配置群中的消息,调用本机的 [Codex CLI](https://github.com/openai/codex)
|
|
3
|
+
`oh-my-im` 是一个运行在本机的钉钉 AI Agent 桥接器。它接收钉钉单聊和配置群中的消息,调用本机的 [Codex CLI](https://github.com/openai/codex)、[Pi](https://github.com/badlogic/pi-mono) 或 [OpenCode](https://opencode.ai/),再通过钉钉文本或互动卡片返回结果。
|
|
4
4
|
|
|
5
5
|
项目是一个 TypeScript/Node.js 应用,不使用数据库、Redis 或 Docker。配置、运行状态、日志和回复历史默认保存在当前用户的 `~/.oh-my-im/` 目录。
|
|
6
6
|
|
|
@@ -8,8 +8,8 @@
|
|
|
8
8
|
|
|
9
9
|
- 单聊机器人:按用户白名单接收钉钉单聊,支持文本、图片、文件、语音等可解析消息。
|
|
10
10
|
- 群消息监听:消费 DWS 全群消息事件,再按“群 + 发送人”规则做本地过滤。
|
|
11
|
-
- Agent 切换:在管理页选择 Codex 或
|
|
12
|
-
- Agent
|
|
11
|
+
- Agent 切换:在管理页选择 Codex、Pi 或 OpenCode,也可以在会话中使用已配置的关键词切换。
|
|
12
|
+
- Agent 模型:管理页可分别配置 Pi 和 OpenCode 模型;Codex 始终使用系统 Codex CLI 已设置的默认模型。
|
|
13
13
|
- 回复方式:在管理页选择互动卡片或普通文本;该设置同时作用于群聊和私聊。普通文本模式只发送 Agent 最终结果。
|
|
14
14
|
- 互动卡片:显示处理中、工具调用和最终回复;群聊支持 Markdown 或纯文本格式。
|
|
15
15
|
- Session 管理:单聊用户只能查看和切换自己工作目录下的 Session;超级管理员可管理其他工作目录。
|
|
@@ -86,7 +86,7 @@ http://127.0.0.1:12525
|
|
|
86
86
|
1. 在“钉钉应用”区域填写 Client ID、Client Secret、机器人名称和机器人 ID。
|
|
87
87
|
2. 在 DWS 区域完成登录;管理页可发起 Device Flow 登录并查看状态。
|
|
88
88
|
3. 搜索群并选择群成员,保存群监听规则。每条规则的唯一键是 `groupId + senderId`。
|
|
89
|
-
4. 选择默认 Agent,必要时填写 Agent
|
|
89
|
+
4. 选择默认 Agent,必要时填写 Agent 模型名和群提示词后缀。
|
|
90
90
|
5. 如需单聊,打开单聊开关并添加单聊授权人员;可另外配置 Session 超级管理员。
|
|
91
91
|
6. 点击“保存并生效”,然后从钉钉发送一条测试消息。
|
|
92
92
|
|
|
@@ -94,14 +94,14 @@ http://127.0.0.1:12525
|
|
|
94
94
|
|
|
95
95
|
### Agent 模型
|
|
96
96
|
|
|
97
|
-
|
|
97
|
+
管理页展示 Pi 和 OpenCode 的模型选择:
|
|
98
98
|
|
|
99
99
|
```text
|
|
100
|
-
Codex 默认模型 -> 所有使用 Codex 的群聊和单聊
|
|
101
100
|
Pi 默认模型 -> 所有使用 Pi 的群聊和单聊
|
|
101
|
+
OpenCode 默认模型 -> 所有使用 OpenCode 的群聊和单聊
|
|
102
102
|
```
|
|
103
103
|
|
|
104
|
-
|
|
104
|
+
Codex 始终不会传递 `--model`,也不读取 Web 后台或系统变量中的模型值,由 Codex CLI 自己决定默认模型。Pi 的模型列表通过 `pi --list-models` 获取,OpenCode 的模型列表通过 `opencode models` 获取;如果对应 CLI 暂不可用,可以先完成 CLI 登录或直接保留默认模型。
|
|
105
105
|
|
|
106
106
|
## `omi` 命令
|
|
107
107
|
|
|
@@ -139,8 +139,10 @@ omi
|
|
|
139
139
|
- 钉钉应用凭证、机器人名称、机器人发送者 ID。
|
|
140
140
|
- 单聊开关、单聊授权人员和 Session 超级管理员。
|
|
141
141
|
- 群监听规则:群、群成员和每次个人历史消息拉取参数。
|
|
142
|
-
-
|
|
142
|
+
- 群提示词后缀、互动卡片格式、卡片更新间隔和是否显示耗时。
|
|
143
|
+
- 提示词后缀会追加在用户消息事件 JSON 的最下方。
|
|
143
144
|
- Agent 回复方式:互动卡片模式会创建并更新卡片;普通文本模式只在 Agent 完成后发送最终结果。
|
|
145
|
+
- 处理详情:默认不显示,可在卡片设置中开启;开启后展示消息数和工具调用数。
|
|
144
146
|
- 卡片更新间隔设为正数时按间隔更新处理中内容;设为 `0` 或负数时关闭处理中更新,仅在 Agent 完成后发送最终结果。
|
|
145
147
|
- 暂停、开启/关闭监听、切换 Agent 的关键词。多个关键词用 `|` 分隔。
|
|
146
148
|
- 可选的 Webhook,用于 Agent 处理失败时向群发送文本通知。
|
|
@@ -154,18 +156,18 @@ omi
|
|
|
154
156
|
```text
|
|
155
157
|
/help
|
|
156
158
|
/status
|
|
157
|
-
/sessions [pi|codex]
|
|
158
|
-
/use <pi|codex> <编号或 sessionId>
|
|
159
|
+
/sessions [pi|codex|opencode]
|
|
160
|
+
/use <pi|codex|opencode> <编号或 sessionId>
|
|
159
161
|
/current
|
|
160
|
-
/new
|
|
162
|
+
/new # 清空当前会话的 Agent session,下一条消息使用新会话
|
|
161
163
|
```
|
|
162
164
|
|
|
163
165
|
Session 超级管理员额外拥有:
|
|
164
166
|
|
|
165
167
|
```text
|
|
166
|
-
/admin-sessions [pi|codex]
|
|
168
|
+
/admin-sessions [pi|codex|opencode]
|
|
167
169
|
/admin-cd <目录编号或路径>
|
|
168
|
-
/admin-use <pi|codex> <编号或 sessionId>
|
|
170
|
+
/admin-use <pi|codex|opencode> <编号或 sessionId>
|
|
169
171
|
/admin-current
|
|
170
172
|
/admin-reset
|
|
171
173
|
```
|
|
@@ -189,14 +191,15 @@ Session 超级管理员额外拥有:
|
|
|
189
191
|
| `DWS_CLI_PATH` | `dws` | DWS CLI 路径 |
|
|
190
192
|
| `CODEX_CLI_PATH` | `codex` | 群侧 Codex CLI 路径 |
|
|
191
193
|
| `PI_CLI_PATH` | `pi` | Pi CLI 路径 |
|
|
192
|
-
| `
|
|
194
|
+
| `OPENCODE_CLI_PATH` | `opencode` | OpenCode CLI 路径 |
|
|
195
|
+
| `DWS_CODEX_MODEL` | 不使用 | 不读取;Codex 使用系统 CLI 默认模型 |
|
|
193
196
|
| `DWS_CODEX_TIMEOUT_MS` | `300000` | 群侧 Codex 超时,单位毫秒 |
|
|
194
197
|
| `CODEX_PROXY` | 未设置 | 传给 Agent 的代理配置 |
|
|
195
198
|
| `CODEX_HOME` | `~/.codex` | Codex Session 根目录 |
|
|
196
199
|
| `PI_CODING_AGENT_SESSION_DIR` | `~/.pi/agent/sessions` | Pi Session 根目录 |
|
|
197
200
|
| `OHMIM_DATA_DIR` | `~/.oh-my-im` | 回复历史目录使用的数据根目录 |
|
|
198
201
|
|
|
199
|
-
|
|
202
|
+
模型来源:Codex 使用系统 CLI 默认模型,Pi 和 OpenCode 使用 Web 中各自的模型配置;未配置时分别使用对应 CLI 默认模型。OpenCode 模型列表来自 `opencode models`,保存值格式为 `provider/model`。
|
|
200
203
|
|
|
201
204
|
当前实现默认以 bypass/full approval 方式运行 Agent。请只在可信的本地工作目录中使用,并确保 Agent 运行账号拥有合适的文件权限。
|
|
202
205
|
|
|
@@ -214,6 +217,8 @@ Session 超级管理员额外拥有:
|
|
|
214
217
|
├── group-worker.lock # 群 worker 锁
|
|
215
218
|
├── omi-bot-status.json # 单聊连接状态
|
|
216
219
|
├── dws-cards.json # 群卡片状态
|
|
220
|
+
├── group-sessions.json # 群聊 Agent Session 绑定
|
|
221
|
+
├── private-sessions.json # 私聊 Agent Session 绑定
|
|
217
222
|
├── dws-history-cursor.json # 个人群历史轮询时间游标和消息去重键
|
|
218
223
|
└── replies/YYYY-MM-DD.json # 单聊和群聊回复历史
|
|
219
224
|
```
|
|
@@ -291,7 +296,7 @@ src/
|
|
|
291
296
|
├── dingtalk.ts # Stream 单聊消息解析与发送
|
|
292
297
|
├── dingtalk-card.ts # StandardCard 创建和更新
|
|
293
298
|
├── dingtalk-robot.ts # 钉钉机器人 OpenAPI/Webhook
|
|
294
|
-
├── agents/ # Codex/Pi 进程适配
|
|
299
|
+
├── agents/ # Codex/Pi/OpenCode 进程适配
|
|
295
300
|
└── conversation-log.ts # 回复历史持久化
|
|
296
301
|
```
|
|
297
302
|
|
|
@@ -179,16 +179,20 @@ function buildArgs(prompt, workDir, sessionId, config) {
|
|
|
179
179
|
else {
|
|
180
180
|
common.push("--full-auto");
|
|
181
181
|
}
|
|
182
|
-
const modelOptions = config.agentModel ? ["--model", config.agentModel] : config.codexModel ? ["--model", config.codexModel] : [];
|
|
183
182
|
return sessionId
|
|
184
|
-
? ["exec", "resume", ...common,
|
|
185
|
-
: ["exec", ...common,
|
|
183
|
+
? ["exec", "resume", ...common, sessionId, "-"]
|
|
184
|
+
: ["exec", ...common, "--cd", workDir, "-"];
|
|
186
185
|
}
|
|
187
186
|
export function runCodex(prompt, sessionId, config, callbacks = {}) {
|
|
188
187
|
return new Promise((resolve, reject) => {
|
|
189
188
|
const start = Date.now();
|
|
190
189
|
const args = buildArgs(prompt, config.codexWorkDir, sessionId, config);
|
|
191
190
|
const env = createAgentEnv(config.codexProxy);
|
|
191
|
+
// Codex model selection belongs exclusively to the CLI config. Do not
|
|
192
|
+
// let legacy application or model environment variables override it.
|
|
193
|
+
delete env.DWS_CODEX_MODEL;
|
|
194
|
+
delete env.CODEX_MODEL;
|
|
195
|
+
delete env.OPENAI_MODEL;
|
|
192
196
|
log.info(`spawn ${config.codexCliPath} ${args.join(" ")}`);
|
|
193
197
|
const child = spawn(config.codexCliPath, args, {
|
|
194
198
|
cwd: config.codexWorkDir,
|
package/dist/agents/index.js
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { listCodexSessions, runCodex } from "./codex-agent.js";
|
|
2
2
|
import { listPiSessions, runPi } from "./pi-agent.js";
|
|
3
|
+
import { listOpenCodeSessions, runOpenCode } from "./opencode-agent.js";
|
|
3
4
|
export function agentLabel(agent) {
|
|
4
|
-
return agent === "pi" ? "Pi" : "Codex";
|
|
5
|
+
return agent === "pi" ? "Pi" : agent === "opencode" ? "OpenCode" : "Codex";
|
|
5
6
|
}
|
|
6
7
|
export function agentSwitchMessage(agent) {
|
|
7
8
|
const label = `${agentLabel(agent)} Agent`;
|
|
@@ -11,12 +12,12 @@ export function agentSwitchMessage(agent) {
|
|
|
11
12
|
].join("\n");
|
|
12
13
|
}
|
|
13
14
|
export function listAgentSessions(agent, config) {
|
|
14
|
-
return agent === "pi"
|
|
15
|
-
? listPiSessions(config)
|
|
16
|
-
: listCodexSessions(config);
|
|
15
|
+
return agent === "pi" ? listPiSessions(config) : agent === "opencode" ? listOpenCodeSessions(config) : listCodexSessions(config);
|
|
17
16
|
}
|
|
18
17
|
export function runAgent(agent, prompt, sessionId, config, callbacks = {}) {
|
|
19
18
|
return agent === "pi"
|
|
20
19
|
? runPi(prompt, sessionId, config, callbacks)
|
|
21
|
-
:
|
|
20
|
+
: agent === "opencode"
|
|
21
|
+
? runOpenCode(prompt, sessionId, config, callbacks)
|
|
22
|
+
: runCodex(prompt, sessionId, config, callbacks);
|
|
22
23
|
}
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
2
|
+
import { createLogger } from "../logger.js";
|
|
3
|
+
import { asObject, attachJsonlReader, createOpenCodeEnv } from "./process-utils.js";
|
|
4
|
+
const log = createLogger("OpenCode");
|
|
5
|
+
function asTimestamp(value) {
|
|
6
|
+
if (typeof value === "string")
|
|
7
|
+
return value;
|
|
8
|
+
if (typeof value !== "number")
|
|
9
|
+
return undefined;
|
|
10
|
+
return new Date(value).toISOString();
|
|
11
|
+
}
|
|
12
|
+
export function listOpenCodeSessions(config) {
|
|
13
|
+
return new Promise((resolve) => {
|
|
14
|
+
const cliPath = config.opencodeCliPath || "opencode";
|
|
15
|
+
const child = spawn(cliPath, ["session", "list", "--format", "json", "-n", "500"], {
|
|
16
|
+
cwd: config.codexWorkDir,
|
|
17
|
+
env: createOpenCodeEnv(config.codexProxy),
|
|
18
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
19
|
+
});
|
|
20
|
+
let stdout = "";
|
|
21
|
+
child.stdout.on("data", (chunk) => { stdout += chunk.toString(); });
|
|
22
|
+
child.on("error", () => resolve([]));
|
|
23
|
+
child.on("close", (code) => {
|
|
24
|
+
if (code !== 0)
|
|
25
|
+
return resolve([]);
|
|
26
|
+
try {
|
|
27
|
+
const value = JSON.parse(stdout);
|
|
28
|
+
resolve(value.flatMap((item) => {
|
|
29
|
+
const cwd = typeof item.directory === "string" ? item.directory : undefined;
|
|
30
|
+
const id = typeof item.id === "string" ? item.id : undefined;
|
|
31
|
+
if (!id)
|
|
32
|
+
return [];
|
|
33
|
+
return [{
|
|
34
|
+
id,
|
|
35
|
+
title: typeof item.title === "string" ? item.title : undefined,
|
|
36
|
+
createdAt: asTimestamp(item.created),
|
|
37
|
+
updatedAt: asTimestamp(item.updated),
|
|
38
|
+
cwd,
|
|
39
|
+
}];
|
|
40
|
+
}));
|
|
41
|
+
}
|
|
42
|
+
catch {
|
|
43
|
+
resolve([]);
|
|
44
|
+
}
|
|
45
|
+
});
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
function textFromEvent(event) {
|
|
49
|
+
const part = asObject(event.part);
|
|
50
|
+
const text = part?.text ?? event.text;
|
|
51
|
+
return typeof text === "string" && text ? text : undefined;
|
|
52
|
+
}
|
|
53
|
+
function eventSessionId(event) {
|
|
54
|
+
for (const value of [event.sessionID, event.sessionId, event.session_id, asObject(event.part)?.sessionID]) {
|
|
55
|
+
if (typeof value === "string" && value.trim())
|
|
56
|
+
return value.trim();
|
|
57
|
+
}
|
|
58
|
+
return undefined;
|
|
59
|
+
}
|
|
60
|
+
function toolFromEvent(event) {
|
|
61
|
+
if (event.type !== "tool_use" && event.type !== "tool")
|
|
62
|
+
return undefined;
|
|
63
|
+
const part = asObject(event.part);
|
|
64
|
+
const name = [part?.tool, event.tool, event.name].find((value) => typeof value === "string" && Boolean(value.trim()));
|
|
65
|
+
if (!name)
|
|
66
|
+
return undefined;
|
|
67
|
+
const id = [part?.callID, part?.callId, event.callID, event.callId].find((value) => typeof value === "string" && Boolean(value.trim()));
|
|
68
|
+
return { name, id };
|
|
69
|
+
}
|
|
70
|
+
export function runOpenCode(prompt, sessionId, config, callbacks = {}) {
|
|
71
|
+
return new Promise((resolve, reject) => {
|
|
72
|
+
const start = Date.now();
|
|
73
|
+
const args = ["run", "--format", "json", "--dir", config.codexWorkDir, "--auto"];
|
|
74
|
+
if (config.agentModel)
|
|
75
|
+
args.push("--model", config.agentModel);
|
|
76
|
+
if (sessionId)
|
|
77
|
+
args.push("--session", sessionId);
|
|
78
|
+
args.push(prompt);
|
|
79
|
+
const child = spawn(config.opencodeCliPath || "opencode", args, {
|
|
80
|
+
cwd: config.codexWorkDir,
|
|
81
|
+
env: createOpenCodeEnv(config.codexProxy),
|
|
82
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
83
|
+
});
|
|
84
|
+
let completed = false;
|
|
85
|
+
let latestText = "";
|
|
86
|
+
let stderr = "";
|
|
87
|
+
let nextSessionId = sessionId;
|
|
88
|
+
const toolStats = {};
|
|
89
|
+
const seenTools = new Set();
|
|
90
|
+
const timeout = setTimeout(() => finishError(`OpenCode timeout after ${Math.round(config.cliTimeoutMs / 1000)}s`), config.cliTimeoutMs);
|
|
91
|
+
timeout.unref();
|
|
92
|
+
const finish = () => {
|
|
93
|
+
if (completed)
|
|
94
|
+
return;
|
|
95
|
+
completed = true;
|
|
96
|
+
clearTimeout(timeout);
|
|
97
|
+
resolve({ sessionId: nextSessionId, text: latestText.trim() || "(无输出)", toolStats, durationMs: Date.now() - start });
|
|
98
|
+
};
|
|
99
|
+
function finishError(message) {
|
|
100
|
+
if (completed)
|
|
101
|
+
return;
|
|
102
|
+
completed = true;
|
|
103
|
+
clearTimeout(timeout);
|
|
104
|
+
child.kill("SIGTERM");
|
|
105
|
+
reject(new Error(message));
|
|
106
|
+
}
|
|
107
|
+
callbacks.onAbortReady?.(() => {
|
|
108
|
+
if (!completed) {
|
|
109
|
+
child.kill("SIGTERM");
|
|
110
|
+
setTimeout(() => { if (!completed)
|
|
111
|
+
child.kill("SIGKILL"); }, 1_000).unref();
|
|
112
|
+
}
|
|
113
|
+
});
|
|
114
|
+
child.stderr.on("data", (chunk) => { stderr += chunk.toString(); });
|
|
115
|
+
attachJsonlReader(child.stdout, (line) => {
|
|
116
|
+
let event;
|
|
117
|
+
try {
|
|
118
|
+
event = JSON.parse(line);
|
|
119
|
+
}
|
|
120
|
+
catch {
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
123
|
+
nextSessionId = eventSessionId(event) || nextSessionId;
|
|
124
|
+
if (event.type === "error") {
|
|
125
|
+
const error = asObject(event.error);
|
|
126
|
+
finishError(typeof error?.message === "string" ? error.message : typeof event.message === "string" ? event.message : "OpenCode failed");
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
129
|
+
const tool = toolFromEvent(event);
|
|
130
|
+
if (tool) {
|
|
131
|
+
const key = tool.id || `${tool.name}:${seenTools.size}`;
|
|
132
|
+
if (!seenTools.has(key)) {
|
|
133
|
+
seenTools.add(key);
|
|
134
|
+
toolStats[tool.name] = (toolStats[tool.name] ?? 0) + 1;
|
|
135
|
+
callbacks.onToolUse?.(tool.name, { ...toolStats });
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
if (event.type === "text" || event.type === "message") {
|
|
139
|
+
const text = textFromEvent(event);
|
|
140
|
+
if (text) {
|
|
141
|
+
latestText = text.startsWith(latestText) ? text : latestText.startsWith(text) ? latestText : latestText + text;
|
|
142
|
+
callbacks.onText?.(latestText);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
});
|
|
146
|
+
child.on("error", (error) => finishError(error.message));
|
|
147
|
+
child.on("close", (code) => {
|
|
148
|
+
if (completed)
|
|
149
|
+
return;
|
|
150
|
+
if (code && code !== 0)
|
|
151
|
+
return finishError(stderr.trim() || `OpenCode exited with code ${code}`);
|
|
152
|
+
finish();
|
|
153
|
+
});
|
|
154
|
+
log.info(`spawn ${config.opencodeCliPath || "opencode"} ${args.join(" ")}`);
|
|
155
|
+
});
|
|
156
|
+
}
|
package/dist/agents/pi-agent.js
CHANGED
|
@@ -79,10 +79,10 @@ function extractAssistantText(message) {
|
|
|
79
79
|
const text = value.content.flatMap((part) => {
|
|
80
80
|
const item = asObject(part);
|
|
81
81
|
return item?.type === "text" && typeof item.text === "string" ? [item.text] : [];
|
|
82
|
-
}).join("
|
|
82
|
+
}).join("");
|
|
83
83
|
return text || undefined;
|
|
84
84
|
}
|
|
85
|
-
|
|
85
|
+
function runPiOnce(prompt, sessionId, config, callbacks = {}) {
|
|
86
86
|
return new Promise((resolve, reject) => {
|
|
87
87
|
const start = Date.now();
|
|
88
88
|
const args = ["--mode", "rpc", "--approve"];
|
|
@@ -209,6 +209,10 @@ export function runPi(prompt, sessionId, config, callbacks = {}) {
|
|
|
209
209
|
child.on("close", (code) => {
|
|
210
210
|
if (completed)
|
|
211
211
|
return;
|
|
212
|
+
if (stderr && /Session found in different project|Fork this session\?/i.test(stderr)) {
|
|
213
|
+
fail(stderr.trim());
|
|
214
|
+
return;
|
|
215
|
+
}
|
|
212
216
|
if (code && code !== 0) {
|
|
213
217
|
fail(stderr.trim() || `Pi Agent exited with code ${code}`);
|
|
214
218
|
return;
|
|
@@ -218,3 +222,20 @@ export function runPi(prompt, sessionId, config, callbacks = {}) {
|
|
|
218
222
|
child.stdin.write(`${JSON.stringify({ id: "state", type: "get_state" })}\n`);
|
|
219
223
|
});
|
|
220
224
|
}
|
|
225
|
+
function isMissingSessionError(error) {
|
|
226
|
+
return error instanceof Error && /No session found matching ['"]?[^'"\s]+['"]?/i.test(error.message);
|
|
227
|
+
}
|
|
228
|
+
function isDifferentProjectError(error) {
|
|
229
|
+
return error instanceof Error && /Session found in different project|Fork this session\?/i.test(error.message);
|
|
230
|
+
}
|
|
231
|
+
export async function runPi(prompt, sessionId, config, callbacks = {}) {
|
|
232
|
+
try {
|
|
233
|
+
return await runPiOnce(prompt, sessionId, config, callbacks);
|
|
234
|
+
}
|
|
235
|
+
catch (error) {
|
|
236
|
+
if (!sessionId || (!isMissingSessionError(error) && !isDifferentProjectError(error)))
|
|
237
|
+
throw error;
|
|
238
|
+
log.warn(`session=${sessionId} is unavailable for the current project; starting a new Pi session`);
|
|
239
|
+
return runPiOnce(prompt, undefined, config, callbacks);
|
|
240
|
+
}
|
|
241
|
+
}
|
|
@@ -14,6 +14,15 @@ export function createAgentEnv(proxy) {
|
|
|
14
14
|
env.all_proxy = proxy;
|
|
15
15
|
return env;
|
|
16
16
|
}
|
|
17
|
+
export function createOpenCodeEnv(proxy) {
|
|
18
|
+
const env = createAgentEnv(proxy);
|
|
19
|
+
// A parent npm/pnpm process can put a broken opencode shim from an
|
|
20
|
+
// unrelated node_modules/.bin directory ahead of the working CLI.
|
|
21
|
+
if (env.PATH) {
|
|
22
|
+
env.PATH = env.PATH.split(":").filter((entry) => !entry.endsWith("/node_modules/.bin")).join(":");
|
|
23
|
+
}
|
|
24
|
+
return env;
|
|
25
|
+
}
|
|
17
26
|
/** Read strict LF-delimited JSONL without treating Unicode separators as records. */
|
|
18
27
|
export function attachJsonlReader(stream, onLine) {
|
|
19
28
|
const decoder = new StringDecoder("utf8");
|