@konglx/rotom 2.21.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.
- package/README.md +417 -0
- package/bin/mesh-master.sh +439 -0
- package/bin/rotom +29 -0
- package/bin/rotom-link.sh +136 -0
- package/bin/rotom-send-with-status +57 -0
- package/bin/rotom-up.sh +428 -0
- package/dist/cli/ask.js +62 -0
- package/dist/cli/common.js +321 -0
- package/dist/cli/config.js +65 -0
- package/dist/cli/directory.js +17 -0
- package/dist/cli/executor.js +58 -0
- package/dist/cli/fed.js +91 -0
- package/dist/cli/group.js +273 -0
- package/dist/cli/identity.js +62 -0
- package/dist/cli/init.js +268 -0
- package/dist/cli/issue.js +202 -0
- package/dist/cli/join.js +170 -0
- package/dist/cli/link.js +47 -0
- package/dist/cli/master.js +51 -0
- package/dist/cli/memory.js +307 -0
- package/dist/cli/note.js +68 -0
- package/dist/cli/repo.js +77 -0
- package/dist/cli/rotom.js +277 -0
- package/dist/cli/routes.js +118 -0
- package/dist/cli/run.js +45 -0
- package/dist/cli/schedule.js +237 -0
- package/dist/cli/skill.js +173 -0
- package/dist/cli/team.js +106 -0
- package/dist/executor/claude-code-hook.cjs +80 -0
- package/dist/executor/cli-executor.js +8 -0
- package/dist/executor/executors/claude-code.js +780 -0
- package/dist/executor/executors/codex.js +719 -0
- package/dist/executor/executors/hermes-cli.js +855 -0
- package/dist/executor/executors/openclaw.js +467 -0
- package/dist/executor/executors/pi.js +514 -0
- package/dist/executor/index.js +269 -0
- package/dist/executor/jsonrpc-transport.js +125 -0
- package/dist/executor/process-runner.js +101 -0
- package/dist/executor/reasoning-status.js +83 -0
- package/dist/executor/repo-cache.js +502 -0
- package/dist/executor/session-store.js +188 -0
- package/dist/executor/worker-chat.js +257 -0
- package/dist/executor/worker-connection.js +89 -0
- package/dist/executor/worker-issue.js +264 -0
- package/dist/executor/worker.js +877 -0
- package/dist/link/pending-requests.js +72 -0
- package/dist/link/server.js +233 -0
- package/dist/link/visibility-store.js +58 -0
- package/dist/master/api/agents.js +333 -0
- package/dist/master/api/artifacts.js +271 -0
- package/dist/master/api/domains.js +64 -0
- package/dist/master/api/groups.js +635 -0
- package/dist/master/api/guidance-templates.js +147 -0
- package/dist/master/api/index.js +89 -0
- package/dist/master/api/issues-patrol.js +172 -0
- package/dist/master/api/issues.js +663 -0
- package/dist/master/api/links-patrol.js +168 -0
- package/dist/master/api/links.js +114 -0
- package/dist/master/api/memory.js +259 -0
- package/dist/master/api/messages.js +157 -0
- package/dist/master/api/notes.js +77 -0
- package/dist/master/api/schedule-patterns.js +133 -0
- package/dist/master/api/schedules.js +272 -0
- package/dist/master/api/sessions.js +158 -0
- package/dist/master/api/share.js +269 -0
- package/dist/master/api/skills.js +190 -0
- package/dist/master/api/teams.js +122 -0
- package/dist/master/api/uploads.js +245 -0
- package/dist/master/auth.js +134 -0
- package/dist/master/dashboard/animations/calico-dozing.apng +0 -0
- package/dist/master/dashboard/animations/calico-error.apng +0 -0
- package/dist/master/dashboard/animations/calico-happy.apng +0 -0
- package/dist/master/dashboard/animations/calico-notification.apng +0 -0
- package/dist/master/dashboard/animations/calico-sleeping.apng +0 -0
- package/dist/master/dashboard/animations/calico-thinking.apng +0 -0
- package/dist/master/dashboard/animations/calico-waking.apng +0 -0
- package/dist/master/dashboard/assets/ApprovalCard-C38VV6ko.css +1 -0
- package/dist/master/dashboard/assets/ApprovalCard-CHPh2dmE.js +17 -0
- package/dist/master/dashboard/assets/ArtifactPanel-P_2gAP7v.js +1 -0
- package/dist/master/dashboard/assets/ArtifactPanel-aGHySny5.css +1 -0
- package/dist/master/dashboard/assets/css.worker-DaIe3gwK.js +84 -0
- package/dist/master/dashboard/assets/editor.worker-BCzxt1at.js +12 -0
- package/dist/master/dashboard/assets/html.worker-CKrFyw_2.js +461 -0
- package/dist/master/dashboard/assets/index-CChrTn81.css +32 -0
- package/dist/master/dashboard/assets/index-Dhu4SN1z.js +181 -0
- package/dist/master/dashboard/assets/json.worker-B7c_PmGb.js +49 -0
- package/dist/master/dashboard/assets/markdown-CeN5IgdF.js +29 -0
- package/dist/master/dashboard/assets/monaco-core-DyX1CsEw.css +1 -0
- package/dist/master/dashboard/assets/monaco-core-oQiQUisy.js +833 -0
- package/dist/master/dashboard/assets/monaco-setup-CiOPQdmo.js +1 -0
- package/dist/master/dashboard/assets/react-vendor-C8IxlyCR.js +67 -0
- package/dist/master/dashboard/assets/ts.worker-BhkL8olL.js +51334 -0
- package/dist/master/dashboard/assets/useMonaco-ILb4vyPh.js +12 -0
- package/dist/master/dashboard/assets/vite-preload-CxJPbCTl.js +1 -0
- package/dist/master/dashboard/debug-auth.html +197 -0
- package/dist/master/dashboard/favicon.ico +0 -0
- package/dist/master/dashboard/index.html +20 -0
- package/dist/master/dashboard/rotom-avatar.png +0 -0
- package/dist/master/db/agent-sessions.js +60 -0
- package/dist/master/db/agent-visibility.js +64 -0
- package/dist/master/db/agents.js +119 -0
- package/dist/master/db/ask-bridges.js +157 -0
- package/dist/master/db/build-update.js +59 -0
- package/dist/master/db/core.js +82 -0
- package/dist/master/db/domains.js +80 -0
- package/dist/master/db/groups.js +316 -0
- package/dist/master/db/guidance-templates.js +58 -0
- package/dist/master/db/index.js +12 -0
- package/dist/master/db/internal.js +45 -0
- package/dist/master/db/issues-patrol.js +81 -0
- package/dist/master/db/issues.js +373 -0
- package/dist/master/db/links.js +221 -0
- package/dist/master/db/master-node.js +43 -0
- package/dist/master/db/memory.js +272 -0
- package/dist/master/db/messages.js +210 -0
- package/dist/master/db/notes.js +55 -0
- package/dist/master/db/schedule-patterns.js +56 -0
- package/dist/master/db/schedules.js +135 -0
- package/dist/master/db/skills.js +144 -0
- package/dist/master/db/team.js +88 -0
- package/dist/master/db/types.js +10 -0
- package/dist/master/db.js +12 -0
- package/dist/master/embedded.js +133 -0
- package/dist/master/federation/client.js +283 -0
- package/dist/master/federation/identity.js +133 -0
- package/dist/master/federation/manager.js +267 -0
- package/dist/master/federation/publisher.js +87 -0
- package/dist/master/federation/self-publisher.js +69 -0
- package/dist/master/federation/server.js +487 -0
- package/dist/master/group-paths.js +208 -0
- package/dist/master/offline-queue.js +38 -0
- package/dist/master/opc-bootstrap.js +245 -0
- package/dist/master/patrol-terminal.js +275 -0
- package/dist/master/repo-scan.js +188 -0
- package/dist/master/router.js +214 -0
- package/dist/master/scheduler-handlers.js +510 -0
- package/dist/master/scheduler.js +201 -0
- package/dist/master/server.js +203 -0
- package/dist/master/services/link-collector.js +82 -0
- package/dist/master/services/link-patrol-bootstrap.js +50 -0
- package/dist/master/services/memory-extract-prompt.js +34 -0
- package/dist/master/services/patrol-bootstrap.js +63 -0
- package/dist/master/share-tokens.js +56 -0
- package/dist/master/terminal-hub.js +300 -0
- package/dist/master/uploads.js +108 -0
- package/dist/master/util/fs.js +100 -0
- package/dist/master/util/paths.js +50 -0
- package/dist/master/util/persona.js +10 -0
- package/dist/master/ws-hub/connection.js +928 -0
- package/dist/master/ws-hub/conversation.js +290 -0
- package/dist/master/ws-hub/directory.js +70 -0
- package/dist/master/ws-hub/dispatch-enrich.js +34 -0
- package/dist/master/ws-hub/hub.js +136 -0
- package/dist/master/ws-hub/index.js +9 -0
- package/dist/master/ws-hub/internal.js +35 -0
- package/dist/master/ws-hub/routing.js +295 -0
- package/dist/master/ws-hub/sessions.js +130 -0
- package/dist/master/ws-hub.js +11 -0
- package/dist/shared/agent-profile.js +44 -0
- package/dist/shared/constants.js +55 -0
- package/dist/shared/dedup.js +33 -0
- package/dist/shared/group-context.js +62 -0
- package/dist/shared/json-codec.js +33 -0
- package/dist/shared/logger.js +136 -0
- package/dist/shared/mention.js +22 -0
- package/dist/shared/network.js +40 -0
- package/dist/shared/parse.js +18 -0
- package/dist/shared/prompt-composer.js +171 -0
- package/dist/shared/protocol/client-messages.js +8 -0
- package/dist/shared/protocol/enums.js +6 -0
- package/dist/shared/protocol/federation.js +62 -0
- package/dist/shared/protocol/guards.js +87 -0
- package/dist/shared/protocol/server-messages.js +8 -0
- package/dist/shared/protocol/types.js +8 -0
- package/dist/shared/protocol.js +19 -0
- package/dist/shared/readonly-allowlist.js +122 -0
- package/dist/shared/rotom-cli-prompt.js +23 -0
- package/dist/shared/skill-context.js +19 -0
- package/dist/shared/skill-md.js +43 -0
- package/dist/shared/slash-commands.js +50 -0
- package/dist/shared/time.js +80 -0
- package/dist/shared/title.js +46 -0
- package/dist/shared/url-extractor.js +99 -0
- package/migrations/001-schema.sql +942 -0
- package/package.json +68 -0
- package/scripts/fix-node-pty-perms.mjs +46 -0
- package/skill/rotom-a2a-communicate/SKILL.md +257 -0
- package/skill/rotom-bus-host/SKILL.md +78 -0
- package/skill/rotom-bus-host/scripts/poll-replies.sh +148 -0
|
@@ -0,0 +1,514 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pi CLI Executor — port of multica/server/pkg/agent/pi.go.
|
|
3
|
+
*
|
|
4
|
+
* Spawns `pi -p --mode json --session <path> <prompt>` per turn
|
|
5
|
+
* (spawn-and-exit, mirrors the openclaw template structurally).
|
|
6
|
+
*
|
|
7
|
+
* - -p / --print non-interactive mode; prompt is positional
|
|
8
|
+
* - --mode json emit one JSON event per line on stdout, then exit
|
|
9
|
+
* - --session <path> file path where Pi appends event JSONL; doubles as
|
|
10
|
+
* our opaque session id (returned + reused on resume)
|
|
11
|
+
*
|
|
12
|
+
* Session as file path: we pre-create an empty file at the path before
|
|
13
|
+
* spawning (Pi refuses to start when --session points at a missing file).
|
|
14
|
+
* On resume we pass the same path back; Pi reads it to reconstruct history
|
|
15
|
+
* and appends new events. readSessionContent reads the file directly — no
|
|
16
|
+
* tree walk.
|
|
17
|
+
*
|
|
18
|
+
* stdin close (#2188): Pi's event loop polls stdin even in print mode. When
|
|
19
|
+
* run under a daemon (no interactive TTY), Pi blocks awaiting stdin events
|
|
20
|
+
* instead of progressing to "done". We close stdin immediately after spawn
|
|
21
|
+
* to deliver an explicit EOF. Without this the process hangs for the full
|
|
22
|
+
* wall-clock timeout.
|
|
23
|
+
*
|
|
24
|
+
* Tool-call markup sanitization: Pi's text_delta events can embed structured
|
|
25
|
+
* tool-call markup (`<|call:bash{...}|>`, `<|response:{...}|>`) and control
|
|
26
|
+
* tokens (`<|foo|>`) for providers that emit tool calls inline as text.
|
|
27
|
+
* Providers using the anthropic-messages API emit tool calls as separate
|
|
28
|
+
* toolcall_* events and text is clean, but we strip defensively so the user
|
|
29
|
+
* never sees raw protocol markup. Ported from multica's
|
|
30
|
+
* drainPiSanitizedText / stripPiToolCallMarkup.
|
|
31
|
+
*
|
|
32
|
+
* Usage: captured from turn_end.message.usage (accumulated across turns,
|
|
33
|
+
* keyed by model). Pi may emit multiple turn_end events within one
|
|
34
|
+
* execution; we sum them.
|
|
35
|
+
*/
|
|
36
|
+
import { runProcess } from "../process-runner.js";
|
|
37
|
+
import fs from "node:fs";
|
|
38
|
+
import os from "node:os";
|
|
39
|
+
import path from "node:path";
|
|
40
|
+
import { buildPlanModeInstruction } from "../../shared/slash-commands.js";
|
|
41
|
+
import { emitStatus } from "../reasoning-status.js";
|
|
42
|
+
// ── Pi tool-call markup sanitization ───────────────────────────────────
|
|
43
|
+
// Ported from multica/server/pkg/agent/pi.go (stripPiToolCallMarkup et al.).
|
|
44
|
+
const PI_CONTROL_TOKEN_RE = /<\|[A-Za-z0-9_-]+>[A-Za-z0-9_-]*|<[A-Za-z0-9_-]+\|>/g;
|
|
45
|
+
function stripPiControlTokens(s) {
|
|
46
|
+
return s.replace(PI_CONTROL_TOKEN_RE, "");
|
|
47
|
+
}
|
|
48
|
+
function isPiToolNameByte(b) {
|
|
49
|
+
return /^[A-Za-z0-9_-]$/.test(b);
|
|
50
|
+
}
|
|
51
|
+
/** Find the next `call:` or `response:` prefix from index `from`. Returns
|
|
52
|
+
* `[index, prefixLen]`; index=-1 when none found. */
|
|
53
|
+
function nextPiToolMarkupPrefix(s, from) {
|
|
54
|
+
let best = -1;
|
|
55
|
+
let bestLen = 0;
|
|
56
|
+
for (const prefix of ["call:", "response:"]) {
|
|
57
|
+
const i = s.indexOf(prefix, from);
|
|
58
|
+
if (i >= 0 && (best === -1 || i < best)) {
|
|
59
|
+
best = i;
|
|
60
|
+
bestLen = prefix.length;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
return [best, bestLen];
|
|
64
|
+
}
|
|
65
|
+
/** Scan from the byte after a `call:`/`response:` prefix to the matching
|
|
66
|
+
* closing `}`. Handles `<|"|>` quote escaping and nested braces. Returns
|
|
67
|
+
* `[endIndex, ok]`; ok=false when the block is unterminated. */
|
|
68
|
+
function scanPiToolMarkupEnd(s, i) {
|
|
69
|
+
const nameStart = i;
|
|
70
|
+
while (i < s.length && isPiToolNameByte(s[i]))
|
|
71
|
+
i++;
|
|
72
|
+
if (i === nameStart || i >= s.length || s[i] !== "{")
|
|
73
|
+
return [0, false];
|
|
74
|
+
const quoteMarker = '<|"|>';
|
|
75
|
+
let depth = 0;
|
|
76
|
+
let inQuote = false;
|
|
77
|
+
while (i < s.length) {
|
|
78
|
+
if (s.startsWith(quoteMarker, i)) {
|
|
79
|
+
inQuote = !inQuote;
|
|
80
|
+
i += quoteMarker.length;
|
|
81
|
+
continue;
|
|
82
|
+
}
|
|
83
|
+
if (!inQuote) {
|
|
84
|
+
if (s[i] === "{") {
|
|
85
|
+
depth++;
|
|
86
|
+
}
|
|
87
|
+
else if (s[i] === "}") {
|
|
88
|
+
depth--;
|
|
89
|
+
if (depth === 0) {
|
|
90
|
+
i++;
|
|
91
|
+
if (s.startsWith("<tool_call|>", i))
|
|
92
|
+
i += "<tool_call|>".length;
|
|
93
|
+
return [i, true];
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
i++;
|
|
98
|
+
}
|
|
99
|
+
return [0, false];
|
|
100
|
+
}
|
|
101
|
+
function stripPiStructuredToolMarkup(s) {
|
|
102
|
+
let out = "";
|
|
103
|
+
let i = 0;
|
|
104
|
+
while (i < s.length) {
|
|
105
|
+
const [start, prefixLen] = nextPiToolMarkupPrefix(s, i);
|
|
106
|
+
if (start === -1) {
|
|
107
|
+
out += s.slice(i);
|
|
108
|
+
break;
|
|
109
|
+
}
|
|
110
|
+
out += s.slice(i, start);
|
|
111
|
+
const [end, ok] = scanPiToolMarkupEnd(s, start + prefixLen);
|
|
112
|
+
if (!ok) {
|
|
113
|
+
out += s.slice(start);
|
|
114
|
+
break;
|
|
115
|
+
}
|
|
116
|
+
i = end;
|
|
117
|
+
}
|
|
118
|
+
return out;
|
|
119
|
+
}
|
|
120
|
+
function stripPiToolCallMarkup(s) {
|
|
121
|
+
return stripPiControlTokens(stripPiStructuredToolMarkup(s));
|
|
122
|
+
}
|
|
123
|
+
/** Detect a partial control-token prefix at end of buffer (`<|foo` without
|
|
124
|
+
* closing `|>`), so we can hold it back until we see more deltas. */
|
|
125
|
+
function looksLikePiControlTokenPrefix(s) {
|
|
126
|
+
if (s.length === 0 || s[0] !== "<" || s.length > 64)
|
|
127
|
+
return false;
|
|
128
|
+
for (let i = 1; i < s.length; i++) {
|
|
129
|
+
if (!/[A-Za-z0-9_|>-]/.test(s[i]))
|
|
130
|
+
return false;
|
|
131
|
+
}
|
|
132
|
+
return true;
|
|
133
|
+
}
|
|
134
|
+
/** How much of the tail we can safely emit now without cutting a markup
|
|
135
|
+
* prefix in half. Holds back any suffix that looks like the start of
|
|
136
|
+
* `call:` / `response:` or a partial control token. */
|
|
137
|
+
function safePiTextEmitLen(s) {
|
|
138
|
+
let hold = 0;
|
|
139
|
+
for (const prefix of ["call:", "response:"]) {
|
|
140
|
+
for (let n = 1; n < prefix.length && n <= s.length; n++) {
|
|
141
|
+
if (s.endsWith(prefix.slice(0, n)) && n > hold)
|
|
142
|
+
hold = n;
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
const lt = s.lastIndexOf("<");
|
|
146
|
+
if (lt >= 0 && looksLikePiControlTokenPrefix(s.slice(lt))) {
|
|
147
|
+
if (s.length - lt > hold)
|
|
148
|
+
hold = s.length - lt;
|
|
149
|
+
}
|
|
150
|
+
return s.length - hold;
|
|
151
|
+
}
|
|
152
|
+
/** Core drain: emit sanitized text, return `[emit, pending]`. `pending` is
|
|
153
|
+
* the un-emittable tail (partial markup prefix or unterminated block). */
|
|
154
|
+
function drainPiSanitizedText(s) {
|
|
155
|
+
let out = "";
|
|
156
|
+
let i = 0;
|
|
157
|
+
while (i < s.length) {
|
|
158
|
+
const [start, prefixLen] = nextPiToolMarkupPrefix(s, i);
|
|
159
|
+
if (start === -1) {
|
|
160
|
+
const safeLen = safePiTextEmitLen(s.slice(i));
|
|
161
|
+
out += s.slice(i, i + safeLen);
|
|
162
|
+
return [stripPiControlTokens(out), s.slice(i + safeLen)];
|
|
163
|
+
}
|
|
164
|
+
out += s.slice(i, start);
|
|
165
|
+
const [end, ok] = scanPiToolMarkupEnd(s, start + prefixLen);
|
|
166
|
+
if (!ok) {
|
|
167
|
+
return [stripPiControlTokens(out), s.slice(start)];
|
|
168
|
+
}
|
|
169
|
+
i = end;
|
|
170
|
+
}
|
|
171
|
+
return [stripPiControlTokens(out), ""];
|
|
172
|
+
}
|
|
173
|
+
/**
|
|
174
|
+
* Running text buffer that accumulates text_delta chunks and emits sanitized
|
|
175
|
+
* prose, holding back partial markup prefixes so we don't emit `<|call:bash`
|
|
176
|
+
* then realize on the next delta it was the start of a tool-call block.
|
|
177
|
+
*/
|
|
178
|
+
class PiTextBuffer {
|
|
179
|
+
buf = "";
|
|
180
|
+
append(delta) {
|
|
181
|
+
this.buf += delta;
|
|
182
|
+
const [emit, pending] = drainPiSanitizedText(this.buf);
|
|
183
|
+
this.buf = pending;
|
|
184
|
+
return emit;
|
|
185
|
+
}
|
|
186
|
+
flush() {
|
|
187
|
+
const s = this.buf;
|
|
188
|
+
this.buf = "";
|
|
189
|
+
const [emit, pending] = drainPiSanitizedText(s);
|
|
190
|
+
return emit + stripPiControlTokens(pending);
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
// ── Executor ────────────────────────────────────────────────────────────
|
|
194
|
+
const ROTOM_HOME = process.env.ROTOM_HOME || path.join(os.homedir(), ".rotom");
|
|
195
|
+
const PI_SESSIONS_DIR = path.join(ROTOM_HOME, "pi-sessions");
|
|
196
|
+
function newPiSessionPath() {
|
|
197
|
+
const stamp = new Date().toISOString().replace(/[:.]/g, "").replace("T", "T");
|
|
198
|
+
const rand = Math.random().toString(36).slice(2, 8);
|
|
199
|
+
return path.join(PI_SESSIONS_DIR, `${stamp}-${rand}.jsonl`);
|
|
200
|
+
}
|
|
201
|
+
function ensurePiSessionFile(p) {
|
|
202
|
+
fs.mkdirSync(path.dirname(p), { recursive: true });
|
|
203
|
+
// Pi refuses to start when --session points at a missing file. Create an
|
|
204
|
+
// empty file if none exists; leave existing files (resumed sessions)
|
|
205
|
+
// untouched so Pi can append.
|
|
206
|
+
if (!fs.existsSync(p)) {
|
|
207
|
+
const f = fs.openSync(p, "w");
|
|
208
|
+
fs.closeSync(f);
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
export class PiExecutor {
|
|
212
|
+
async execute(prompt, workingDir, onOutput, options) {
|
|
213
|
+
return new Promise((resolve) => {
|
|
214
|
+
// Session path: reuse the cached one if its file still exists;
|
|
215
|
+
// otherwise mint a new one (the old session was pruned by pi or
|
|
216
|
+
// invalidated, so we start fresh).
|
|
217
|
+
let sessionPath = options?.sessionId ?? "";
|
|
218
|
+
if (sessionPath && !fs.existsSync(sessionPath)) {
|
|
219
|
+
console.log(`[pi] cached session file missing, starting fresh: ${sessionPath}`);
|
|
220
|
+
sessionPath = "";
|
|
221
|
+
}
|
|
222
|
+
if (!sessionPath) {
|
|
223
|
+
sessionPath = newPiSessionPath();
|
|
224
|
+
}
|
|
225
|
+
ensurePiSessionFile(sessionPath);
|
|
226
|
+
// prompt 已由 worker 用 composePrompt() 拼好,executor 不再二次包装。
|
|
227
|
+
const args = ["-p", "--mode", "json", "--session", sessionPath];
|
|
228
|
+
// /plan → pi 没有原生 plan 模式,通过 --append-system-prompt 注入开发者级
|
|
229
|
+
// 系统指令,引导其"先方案后落盘"。注册表见 src/shared/slash-commands.ts。
|
|
230
|
+
if (options?.slashCommand === "/plan") {
|
|
231
|
+
args.push("--append-system-prompt", buildPlanModeInstruction());
|
|
232
|
+
}
|
|
233
|
+
args.push(prompt);
|
|
234
|
+
const spawnEnv = { ...process.env, ...options?.env };
|
|
235
|
+
const timeoutMs = options?.timeoutMs;
|
|
236
|
+
console.log(`[pi] Spawning pi -p --mode json (cwd: ${workingDir}, session: ${sessionPath}, slash: ${options?.slashCommand ?? "(none)"}, timeoutMs=${timeoutMs ?? "none"})`);
|
|
237
|
+
const { proc, done: procDone } = runProcess({
|
|
238
|
+
bin: "pi",
|
|
239
|
+
args,
|
|
240
|
+
cwd: workingDir,
|
|
241
|
+
env: spawnEnv,
|
|
242
|
+
label: "pi",
|
|
243
|
+
signal: options?.signal,
|
|
244
|
+
timeoutMs: timeoutMs && timeoutMs > 0 ? timeoutMs + 5_000 : undefined,
|
|
245
|
+
});
|
|
246
|
+
// #2188 fix: close stdin immediately. Pi's event loop polls stdin even
|
|
247
|
+
// in print mode; under a daemon (no TTY) it blocks awaiting stdin
|
|
248
|
+
// events instead of finishing. EOF unblocks the readable side.
|
|
249
|
+
try {
|
|
250
|
+
proc.stdin?.end();
|
|
251
|
+
}
|
|
252
|
+
catch { /* already closed */ }
|
|
253
|
+
// ── Per-run state ──
|
|
254
|
+
let timedOut = false;
|
|
255
|
+
let killedByUser = false;
|
|
256
|
+
let done = false;
|
|
257
|
+
let fullOutput = "";
|
|
258
|
+
let capturedModel;
|
|
259
|
+
let capturedUsage;
|
|
260
|
+
let failed = false;
|
|
261
|
+
let errorMessage;
|
|
262
|
+
let sawTurnEnd = false;
|
|
263
|
+
const textBuffer = new PiTextBuffer();
|
|
264
|
+
if (options?.signal) {
|
|
265
|
+
if (options.signal.aborted)
|
|
266
|
+
killedByUser = true;
|
|
267
|
+
else
|
|
268
|
+
options.signal.addEventListener("abort", () => { killedByUser = true; }, { once: true });
|
|
269
|
+
}
|
|
270
|
+
function handleEvent(event) {
|
|
271
|
+
switch (event.type) {
|
|
272
|
+
case "session":
|
|
273
|
+
// Header line (emitted in json mode). We already know our
|
|
274
|
+
// session path — nothing to capture.
|
|
275
|
+
return;
|
|
276
|
+
case "agent_start":
|
|
277
|
+
emitStatus(onOutput, "Working");
|
|
278
|
+
return;
|
|
279
|
+
case "message_update": {
|
|
280
|
+
const sub = event.assistantMessageEvent;
|
|
281
|
+
if (!sub)
|
|
282
|
+
return;
|
|
283
|
+
switch (sub.type) {
|
|
284
|
+
case "text_delta": {
|
|
285
|
+
if (sub.delta) {
|
|
286
|
+
const emit = textBuffer.append(sub.delta);
|
|
287
|
+
if (emit) {
|
|
288
|
+
fullOutput += emit;
|
|
289
|
+
onOutput(emit);
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
return;
|
|
293
|
+
}
|
|
294
|
+
case "thinking_delta": {
|
|
295
|
+
// v1: 不把 thinking 流推给 dashboard(只在 stderr 记日志),
|
|
296
|
+
// 避免和正文混杂。后续可考虑用 [thinking]…[/thinking] 块。
|
|
297
|
+
if (sub.delta && (process.env.PI_VERBOSE || options?.env?.PI_VERBOSE)) {
|
|
298
|
+
console.error(`[pi:thinking] ${sub.delta}`);
|
|
299
|
+
}
|
|
300
|
+
return;
|
|
301
|
+
}
|
|
302
|
+
default:
|
|
303
|
+
// text_start / text_end / thinking_start / thinking_end /
|
|
304
|
+
// toolcall_* — 不需要,v1 只消费 text_delta 和独立的
|
|
305
|
+
// tool_execution_* 事件。
|
|
306
|
+
return;
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
case "tool_execution_start":
|
|
310
|
+
onOutput(`[tool:exec]${JSON.stringify(event.args ?? {})}[/tool:exec]\n`);
|
|
311
|
+
emitStatus(onOutput, toolStatusFor(event.toolName));
|
|
312
|
+
return;
|
|
313
|
+
case "tool_execution_end": {
|
|
314
|
+
const text = extractToolResultText(event.result);
|
|
315
|
+
if (text) {
|
|
316
|
+
const truncated = text.length > 500 ? `${text.slice(0, 500)}...` : text;
|
|
317
|
+
onOutput(`[tool-result:exec]${truncated}[/tool-result:exec]\n`);
|
|
318
|
+
}
|
|
319
|
+
if (event.isError) {
|
|
320
|
+
console.warn(`[pi] tool ${event.toolName} (${event.toolCallId}) returned isError=true`);
|
|
321
|
+
}
|
|
322
|
+
emitStatus(onOutput, "Working");
|
|
323
|
+
return;
|
|
324
|
+
}
|
|
325
|
+
case "turn_end": {
|
|
326
|
+
sawTurnEnd = true;
|
|
327
|
+
const msg = event.message;
|
|
328
|
+
if (msg && typeof msg === "object" && msg.usage) {
|
|
329
|
+
const u = msg.usage;
|
|
330
|
+
capturedUsage = {
|
|
331
|
+
inputTokens: (capturedUsage?.inputTokens ?? 0) + u.input,
|
|
332
|
+
outputTokens: (capturedUsage?.outputTokens ?? 0) + u.output,
|
|
333
|
+
cacheReadTokens: (capturedUsage?.cacheReadTokens ?? 0) + u.cacheRead,
|
|
334
|
+
cacheCreationTokens: (capturedUsage?.cacheCreationTokens ?? 0) + u.cacheWrite,
|
|
335
|
+
totalCostUsd: capturedUsage?.totalCostUsd,
|
|
336
|
+
};
|
|
337
|
+
if (msg.model && !capturedModel)
|
|
338
|
+
capturedModel = msg.model;
|
|
339
|
+
}
|
|
340
|
+
// Flush any buffered text in case the turn ended without a final
|
|
341
|
+
// text_delta closing the buffer.
|
|
342
|
+
const flushed = textBuffer.flush();
|
|
343
|
+
if (flushed) {
|
|
344
|
+
fullOutput += flushed;
|
|
345
|
+
onOutput(flushed);
|
|
346
|
+
}
|
|
347
|
+
emitStatus(onOutput, failed ? "Failed" : "Answered");
|
|
348
|
+
return;
|
|
349
|
+
}
|
|
350
|
+
case "error": {
|
|
351
|
+
const msg = typeof event.message === "string" ? event.message : "pi error";
|
|
352
|
+
console.error(`[pi] error event: ${msg}`);
|
|
353
|
+
onOutput(`[error] ${msg}\n`);
|
|
354
|
+
if (!failed) {
|
|
355
|
+
failed = true;
|
|
356
|
+
errorMessage = msg;
|
|
357
|
+
}
|
|
358
|
+
emitStatus(onOutput, "Failed");
|
|
359
|
+
return;
|
|
360
|
+
}
|
|
361
|
+
case "auto_retry_end": {
|
|
362
|
+
if (event.success === false && !failed) {
|
|
363
|
+
failed = true;
|
|
364
|
+
errorMessage = event.finalError || "pi exhausted automatic retries";
|
|
365
|
+
console.error(`[pi] auto_retry_end failed: ${errorMessage}`);
|
|
366
|
+
emitStatus(onOutput, "Failed");
|
|
367
|
+
}
|
|
368
|
+
return;
|
|
369
|
+
}
|
|
370
|
+
// agent_end / turn_start / message_start / message_end /
|
|
371
|
+
// tool_execution_update / compaction_* / queue_update /
|
|
372
|
+
// extension_ui_request — v1 不消费。usage 从 turn_end 拿(更可靠);
|
|
373
|
+
// 工具执行从 tool_execution_* 拿(不靠 message_end)。
|
|
374
|
+
default:
|
|
375
|
+
return;
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
function handleLine(line) {
|
|
379
|
+
const trimmed = line.trim();
|
|
380
|
+
if (!trimmed)
|
|
381
|
+
return;
|
|
382
|
+
if (trimmed[0] !== "{")
|
|
383
|
+
return; // non-JSON log noise on stdout (rare)
|
|
384
|
+
let parsed;
|
|
385
|
+
try {
|
|
386
|
+
parsed = JSON.parse(trimmed);
|
|
387
|
+
}
|
|
388
|
+
catch {
|
|
389
|
+
return;
|
|
390
|
+
}
|
|
391
|
+
handleEvent(parsed);
|
|
392
|
+
}
|
|
393
|
+
function finalize(code, reason) {
|
|
394
|
+
if (done)
|
|
395
|
+
return;
|
|
396
|
+
done = true;
|
|
397
|
+
const flushed = textBuffer.flush();
|
|
398
|
+
if (flushed) {
|
|
399
|
+
fullOutput += flushed;
|
|
400
|
+
onOutput(flushed);
|
|
401
|
+
}
|
|
402
|
+
const finalCode = code ?? 0;
|
|
403
|
+
// 进程退出但没发 turn_end(被 SIGKILL / 崩溃) → 视为失败,让 worker
|
|
404
|
+
// 丢缓存 sessionId 下次重开。
|
|
405
|
+
if (!sawTurnEnd && !killedByUser && finalCode !== 0 && !failed) {
|
|
406
|
+
failed = true;
|
|
407
|
+
errorMessage = `pi exited with code ${finalCode} without producing turn_end`;
|
|
408
|
+
}
|
|
409
|
+
if (!fullOutput && failed) {
|
|
410
|
+
if (timedOut) {
|
|
411
|
+
fullOutput = `[错误] pi 执行超时 (>${timeoutMs}ms),已强制结束`;
|
|
412
|
+
}
|
|
413
|
+
else if (reason === "error") {
|
|
414
|
+
fullOutput = `[错误] pi 启动失败`;
|
|
415
|
+
}
|
|
416
|
+
else {
|
|
417
|
+
fullOutput = `[错误] pi 返回内容为空 (exit=${finalCode})`;
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
console.log(`[pi] Exited code=${finalCode} reason=${reason}, output=${fullOutput.length} chars, session=${sessionPath}, turn_end=${sawTurnEnd}`);
|
|
421
|
+
resolve({
|
|
422
|
+
exitCode: failed && finalCode === 0 ? 1 : finalCode,
|
|
423
|
+
fullOutput,
|
|
424
|
+
sessionId: sessionPath,
|
|
425
|
+
usage: capturedUsage,
|
|
426
|
+
model: capturedModel,
|
|
427
|
+
failed: failed || undefined,
|
|
428
|
+
errorMessage,
|
|
429
|
+
});
|
|
430
|
+
}
|
|
431
|
+
let stdoutBuffer = "";
|
|
432
|
+
proc.stdout.on("data", (data) => {
|
|
433
|
+
stdoutBuffer += data.toString();
|
|
434
|
+
let idx;
|
|
435
|
+
while ((idx = stdoutBuffer.indexOf("\n")) !== -1) {
|
|
436
|
+
const line = stdoutBuffer.slice(0, idx);
|
|
437
|
+
stdoutBuffer = stdoutBuffer.slice(idx + 1);
|
|
438
|
+
handleLine(line);
|
|
439
|
+
}
|
|
440
|
+
});
|
|
441
|
+
// stderr: log-only. Pi 在 verbose 模式下会把诊断/堆栈打到 stderr,不含 JSON。
|
|
442
|
+
proc.stderr.on("data", (data) => {
|
|
443
|
+
if (process.env.PI_VERBOSE || options?.env?.PI_VERBOSE) {
|
|
444
|
+
console.error(`[pi:stderr] ${data.toString().trimEnd()}`);
|
|
445
|
+
}
|
|
446
|
+
});
|
|
447
|
+
procDone.then(({ exitCode, signal }) => {
|
|
448
|
+
if (signal === "SIGKILL" && !killedByUser && timeoutMs && timeoutMs > 0) {
|
|
449
|
+
timedOut = true;
|
|
450
|
+
console.warn(`[pi] Wall-clock timeout (${timeoutMs}ms + 5_000ms grace) reached, SIGKILL pid=${proc.pid}`);
|
|
451
|
+
}
|
|
452
|
+
finalize(exitCode, "close");
|
|
453
|
+
});
|
|
454
|
+
});
|
|
455
|
+
}
|
|
456
|
+
/**
|
|
457
|
+
* Read the tail of pi's session transcript. Since we own the session file
|
|
458
|
+
* path (passed to `pi --session <path>`), we read it directly — no tree
|
|
459
|
+
* walk. Tolerant of missing files (pi may prune, or the session was
|
|
460
|
+
* invalidated) — returns empty content + an explanatory `error`.
|
|
461
|
+
*/
|
|
462
|
+
async readSessionContent(args) {
|
|
463
|
+
const file = args.sessionId;
|
|
464
|
+
if (!file || !fs.existsSync(file)) {
|
|
465
|
+
return {
|
|
466
|
+
format: "jsonl",
|
|
467
|
+
content: "",
|
|
468
|
+
error: "pi session 文件不存在(可能已被清理或会话失效)",
|
|
469
|
+
};
|
|
470
|
+
}
|
|
471
|
+
const text = fs.readFileSync(file, "utf-8");
|
|
472
|
+
const lines = text.split("\n");
|
|
473
|
+
const tail = args.tailLines ?? 200;
|
|
474
|
+
const sliced = lines.length > tail ? lines.slice(-tail).join("\n") : text;
|
|
475
|
+
return { format: "jsonl", content: sliced };
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
// ── Helpers ─────────────────────────────────────────────────────────────
|
|
479
|
+
function toolStatusFor(name) {
|
|
480
|
+
switch (name) {
|
|
481
|
+
case "edit":
|
|
482
|
+
case "write":
|
|
483
|
+
return "Patching";
|
|
484
|
+
case "bash":
|
|
485
|
+
return "Running";
|
|
486
|
+
case "read":
|
|
487
|
+
case "grep":
|
|
488
|
+
case "find":
|
|
489
|
+
case "ls":
|
|
490
|
+
return "Reading";
|
|
491
|
+
default:
|
|
492
|
+
return "Running";
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
function extractToolResultText(result) {
|
|
496
|
+
if (result == null)
|
|
497
|
+
return undefined;
|
|
498
|
+
if (typeof result === "string")
|
|
499
|
+
return result;
|
|
500
|
+
if (typeof result === "object") {
|
|
501
|
+
const r = result;
|
|
502
|
+
if (typeof r.text === "string")
|
|
503
|
+
return r.text;
|
|
504
|
+
if (typeof r.output === "string")
|
|
505
|
+
return r.output;
|
|
506
|
+
}
|
|
507
|
+
try {
|
|
508
|
+
const s = JSON.stringify(result);
|
|
509
|
+
return s && s.length > 0 ? s : undefined;
|
|
510
|
+
}
|
|
511
|
+
catch {
|
|
512
|
+
return undefined;
|
|
513
|
+
}
|
|
514
|
+
}
|