@nordbyte/nordrelay 0.2.1
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/.env.example +88 -0
- package/Dockerfile +19 -0
- package/LICENSE +21 -0
- package/README.md +749 -0
- package/dist/access-control.js +146 -0
- package/dist/agent-factory.js +22 -0
- package/dist/agent.js +57 -0
- package/dist/artifacts.js +515 -0
- package/dist/attachments.js +69 -0
- package/dist/bot-preferences.js +146 -0
- package/dist/bot-ui.js +161 -0
- package/dist/bot.js +4520 -0
- package/dist/codex-auth.js +150 -0
- package/dist/codex-cli.js +79 -0
- package/dist/codex-config.js +50 -0
- package/dist/codex-launch.js +109 -0
- package/dist/codex-session.js +591 -0
- package/dist/codex-state.js +573 -0
- package/dist/config.js +385 -0
- package/dist/context-key.js +23 -0
- package/dist/error-messages.js +73 -0
- package/dist/format.js +121 -0
- package/dist/index.js +140 -0
- package/dist/logger.js +27 -0
- package/dist/operations.js +133 -0
- package/dist/persistence.js +65 -0
- package/dist/pi-cli.js +19 -0
- package/dist/pi-rpc.js +158 -0
- package/dist/pi-session.js +573 -0
- package/dist/pi-state.js +226 -0
- package/dist/prompt-store.js +241 -0
- package/dist/redaction.js +47 -0
- package/dist/session-format.js +191 -0
- package/dist/session-registry.js +195 -0
- package/dist/telegram-rate-limit.js +136 -0
- package/dist/voice.js +373 -0
- package/dist/workspace-policy.js +41 -0
- package/docker-compose.yml +17 -0
- package/launchd/start.sh +8 -0
- package/package.json +69 -0
- package/plugins/nordrelay/.codex-plugin/plugin.json +48 -0
- package/plugins/nordrelay/assets/nordrelay.svg +5 -0
- package/plugins/nordrelay/commands/remote.md +33 -0
- package/plugins/nordrelay/scripts/nordrelay.mjs +396 -0
- package/plugins/nordrelay/skills/telegram-remote/SKILL.md +26 -0
|
@@ -0,0 +1,573 @@
|
|
|
1
|
+
import { spawnSync } from "node:child_process";
|
|
2
|
+
import { readFile } from "node:fs/promises";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { PI_AGENT_CAPABILITIES, PI_THINKING_LEVELS, } from "./agent.js";
|
|
5
|
+
import { createDefaultLaunchProfile } from "./codex-launch.js";
|
|
6
|
+
import { resolvePiCli } from "./pi-cli.js";
|
|
7
|
+
import { PiRpcClient } from "./pi-rpc.js";
|
|
8
|
+
import { getPiSession, listPiSessions, listPiWorkspaces, readPiSessionRecord, resolvePiSessionDir, } from "./pi-state.js";
|
|
9
|
+
export class PiSessionService {
|
|
10
|
+
config;
|
|
11
|
+
sessionDir;
|
|
12
|
+
cliPath;
|
|
13
|
+
launchProfile;
|
|
14
|
+
rpc = null;
|
|
15
|
+
currentWorkspace;
|
|
16
|
+
currentThreadId = null;
|
|
17
|
+
currentSessionPath;
|
|
18
|
+
currentModel;
|
|
19
|
+
currentThinking;
|
|
20
|
+
currentSessionName;
|
|
21
|
+
processing = false;
|
|
22
|
+
cachedStats = {};
|
|
23
|
+
constructor(config) {
|
|
24
|
+
this.config = config;
|
|
25
|
+
const cli = resolvePiCli(process.env, config.piCliPath);
|
|
26
|
+
if (!cli.path) {
|
|
27
|
+
throw new Error("Pi CLI not found. Install Pi from https://pi.dev/ or set PI_CLI_PATH.");
|
|
28
|
+
}
|
|
29
|
+
this.cliPath = cli.path;
|
|
30
|
+
this.sessionDir = resolvePiSessionDir({ sessionDir: config.piSessionDir });
|
|
31
|
+
this.currentWorkspace = config.workspace;
|
|
32
|
+
this.currentModel = config.piDefaultModel;
|
|
33
|
+
this.currentThinking = config.piDefaultThinking;
|
|
34
|
+
this.launchProfile = createDefaultLaunchProfile("workspace-write", "never");
|
|
35
|
+
}
|
|
36
|
+
static async create(config, options) {
|
|
37
|
+
const service = new PiSessionService(config);
|
|
38
|
+
service.currentWorkspace = options?.workspace ?? config.workspace;
|
|
39
|
+
service.currentModel = options?.model ?? config.piDefaultModel;
|
|
40
|
+
service.currentThinking = options?.reasoningEffort ?? config.piDefaultThinking;
|
|
41
|
+
if (options?.sessionPath) {
|
|
42
|
+
await service.switchSession(options.sessionPath);
|
|
43
|
+
return service;
|
|
44
|
+
}
|
|
45
|
+
if (options?.resumeThreadId) {
|
|
46
|
+
await service.resumeThread(options.resumeThreadId);
|
|
47
|
+
return service;
|
|
48
|
+
}
|
|
49
|
+
if (!options?.deferThreadStart) {
|
|
50
|
+
await service.newThread(service.currentWorkspace, service.currentModel);
|
|
51
|
+
}
|
|
52
|
+
return service;
|
|
53
|
+
}
|
|
54
|
+
getInfo() {
|
|
55
|
+
return {
|
|
56
|
+
agentId: "pi",
|
|
57
|
+
agentLabel: "Pi",
|
|
58
|
+
threadId: this.currentThreadId,
|
|
59
|
+
workspace: this.currentWorkspace,
|
|
60
|
+
model: this.currentModel,
|
|
61
|
+
reasoningEffort: this.currentThinking,
|
|
62
|
+
launchProfileId: "pi-rpc",
|
|
63
|
+
launchProfileLabel: "Pi RPC",
|
|
64
|
+
launchProfileBehavior: "rpc / host",
|
|
65
|
+
sandboxMode: "host",
|
|
66
|
+
approvalPolicy: "never",
|
|
67
|
+
fastMode: false,
|
|
68
|
+
unsafeLaunch: false,
|
|
69
|
+
sessionPath: this.currentSessionPath,
|
|
70
|
+
sessionUsage: this.cachedStats.sessionUsage,
|
|
71
|
+
contextUsage: this.cachedStats.contextUsage,
|
|
72
|
+
capabilities: PI_AGENT_CAPABILITIES,
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
isProcessing() {
|
|
76
|
+
return this.processing;
|
|
77
|
+
}
|
|
78
|
+
getActiveThreadId() {
|
|
79
|
+
return this.currentThreadId;
|
|
80
|
+
}
|
|
81
|
+
hasActiveThread() {
|
|
82
|
+
return Boolean(this.currentThreadId || this.currentSessionPath);
|
|
83
|
+
}
|
|
84
|
+
getCurrentWorkspace() {
|
|
85
|
+
return this.currentWorkspace;
|
|
86
|
+
}
|
|
87
|
+
async prompt(input, callbacks) {
|
|
88
|
+
if (this.processing) {
|
|
89
|
+
throw new Error("A Pi turn is already in progress");
|
|
90
|
+
}
|
|
91
|
+
await this.ensureSessionStarted();
|
|
92
|
+
const rpc = this.getRpc();
|
|
93
|
+
const promptPayload = await this.buildPromptPayload(input);
|
|
94
|
+
const lastToolOutput = new Map();
|
|
95
|
+
let didEnd = false;
|
|
96
|
+
this.processing = true;
|
|
97
|
+
const off = rpc.onEvent((event) => {
|
|
98
|
+
try {
|
|
99
|
+
switch (event.type) {
|
|
100
|
+
case "message_update":
|
|
101
|
+
handleMessageUpdate(event, callbacks);
|
|
102
|
+
break;
|
|
103
|
+
case "tool_execution_start": {
|
|
104
|
+
const toolCallId = stringValue(event.toolCallId) ?? "tool";
|
|
105
|
+
const toolName = stringValue(event.toolName) ?? "tool";
|
|
106
|
+
lastToolOutput.set(toolCallId, "");
|
|
107
|
+
callbacks.onToolStart(toolName, toolCallId);
|
|
108
|
+
break;
|
|
109
|
+
}
|
|
110
|
+
case "tool_execution_update": {
|
|
111
|
+
const toolCallId = stringValue(event.toolCallId) ?? "tool";
|
|
112
|
+
const text = extractContentText(objectValue(event.partialResult));
|
|
113
|
+
const previous = lastToolOutput.get(toolCallId) ?? "";
|
|
114
|
+
const delta = computeTextDelta(previous, text);
|
|
115
|
+
lastToolOutput.set(toolCallId, text);
|
|
116
|
+
if (delta) {
|
|
117
|
+
callbacks.onToolUpdate(toolCallId, delta);
|
|
118
|
+
}
|
|
119
|
+
break;
|
|
120
|
+
}
|
|
121
|
+
case "tool_execution_end": {
|
|
122
|
+
const toolCallId = stringValue(event.toolCallId) ?? "tool";
|
|
123
|
+
const resultText = extractContentText(objectValue(event.result));
|
|
124
|
+
const previous = lastToolOutput.get(toolCallId) ?? "";
|
|
125
|
+
const delta = computeTextDelta(previous, resultText);
|
|
126
|
+
if (delta) {
|
|
127
|
+
callbacks.onToolUpdate(toolCallId, delta);
|
|
128
|
+
}
|
|
129
|
+
callbacks.onToolEnd(toolCallId, Boolean(event.isError));
|
|
130
|
+
break;
|
|
131
|
+
}
|
|
132
|
+
case "turn_end":
|
|
133
|
+
this.refreshFromTurnEnd(event, callbacks);
|
|
134
|
+
break;
|
|
135
|
+
case "agent_end":
|
|
136
|
+
didEnd = true;
|
|
137
|
+
callbacks.onAgentEnd();
|
|
138
|
+
break;
|
|
139
|
+
case "extension_error": {
|
|
140
|
+
const toolCallId = stringValue(event.id) ?? "extension-error";
|
|
141
|
+
callbacks.onToolStart("extension_error", toolCallId);
|
|
142
|
+
callbacks.onToolUpdate(toolCallId, stringValue(event.error) ?? "Extension error");
|
|
143
|
+
callbacks.onToolEnd(toolCallId, true);
|
|
144
|
+
break;
|
|
145
|
+
}
|
|
146
|
+
default:
|
|
147
|
+
break;
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
catch (error) {
|
|
151
|
+
console.error("Failed to handle Pi RPC event:", error);
|
|
152
|
+
}
|
|
153
|
+
});
|
|
154
|
+
try {
|
|
155
|
+
await rpc.send({ type: "prompt", ...promptPayload }, 30_000);
|
|
156
|
+
await this.waitForAgentEnd(() => didEnd);
|
|
157
|
+
await this.refreshState().catch(() => { });
|
|
158
|
+
await this.refreshStats().catch(() => { });
|
|
159
|
+
}
|
|
160
|
+
finally {
|
|
161
|
+
off();
|
|
162
|
+
this.processing = false;
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
async abort() {
|
|
166
|
+
if (!this.rpc) {
|
|
167
|
+
this.processing = false;
|
|
168
|
+
return;
|
|
169
|
+
}
|
|
170
|
+
await this.rpc.send({ type: "abort" }, 10_000).catch(() => { });
|
|
171
|
+
this.processing = false;
|
|
172
|
+
}
|
|
173
|
+
async newThread(workspace, model) {
|
|
174
|
+
this.ensureIdle("start a new Pi session");
|
|
175
|
+
this.currentWorkspace = workspace ?? this.currentWorkspace;
|
|
176
|
+
if (model) {
|
|
177
|
+
this.currentModel = model;
|
|
178
|
+
}
|
|
179
|
+
this.currentThreadId = null;
|
|
180
|
+
this.currentSessionPath = undefined;
|
|
181
|
+
this.cachedStats = {};
|
|
182
|
+
this.restartRpc();
|
|
183
|
+
await this.refreshState();
|
|
184
|
+
return this.getInfo();
|
|
185
|
+
}
|
|
186
|
+
async resumeThread(threadId) {
|
|
187
|
+
return this.switchSession(threadId);
|
|
188
|
+
}
|
|
189
|
+
async switchSession(threadId) {
|
|
190
|
+
this.ensureIdle("switch Pi session");
|
|
191
|
+
const record = getPiSession(threadId, { sessionDir: this.sessionDir });
|
|
192
|
+
if (!record) {
|
|
193
|
+
throw new Error(`Unknown Pi session: ${threadId}`);
|
|
194
|
+
}
|
|
195
|
+
this.currentWorkspace = record.cwd;
|
|
196
|
+
this.currentThreadId = record.id;
|
|
197
|
+
this.currentSessionPath = record.sessionPath;
|
|
198
|
+
this.currentModel = record.model ?? this.currentModel;
|
|
199
|
+
this.currentThinking = record.reasoningEffort ?? this.currentThinking;
|
|
200
|
+
this.cachedStats = {};
|
|
201
|
+
if (this.rpc) {
|
|
202
|
+
const result = await this.rpc.send({ type: "switch_session", sessionPath: record.sessionPath }, 30_000);
|
|
203
|
+
if (objectValue(result.data)?.cancelled === true) {
|
|
204
|
+
throw new Error("Pi session switch was cancelled by an extension");
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
else {
|
|
208
|
+
this.restartRpc();
|
|
209
|
+
}
|
|
210
|
+
await this.refreshState();
|
|
211
|
+
await this.refreshStats().catch(() => { });
|
|
212
|
+
return this.getInfo();
|
|
213
|
+
}
|
|
214
|
+
listAllSessions(limit) {
|
|
215
|
+
return listPiSessions(limit ?? 20, { sessionDir: this.sessionDir });
|
|
216
|
+
}
|
|
217
|
+
listWorkspaces() {
|
|
218
|
+
const workspaces = new Set(listPiWorkspaces({ sessionDir: this.sessionDir }));
|
|
219
|
+
workspaces.add(this.currentWorkspace);
|
|
220
|
+
workspaces.add(this.config.workspace);
|
|
221
|
+
return [...workspaces].sort((left, right) => left.localeCompare(right));
|
|
222
|
+
}
|
|
223
|
+
listModels() {
|
|
224
|
+
const result = spawnSync(this.cliPath, ["--list-models"], {
|
|
225
|
+
cwd: this.currentWorkspace,
|
|
226
|
+
env: process.env,
|
|
227
|
+
encoding: "utf8",
|
|
228
|
+
timeout: 10_000,
|
|
229
|
+
});
|
|
230
|
+
if (result.status !== 0 || !result.stdout) {
|
|
231
|
+
return this.currentModel ? [{ slug: this.currentModel, displayName: this.currentModel }] : [];
|
|
232
|
+
}
|
|
233
|
+
const records = [];
|
|
234
|
+
for (const line of result.stdout
|
|
235
|
+
.split(/\r?\n/)
|
|
236
|
+
.slice(1)
|
|
237
|
+
.map((line) => line.trim())
|
|
238
|
+
.filter(Boolean)) {
|
|
239
|
+
const parts = line.split(/\s+/);
|
|
240
|
+
const provider = parts[0];
|
|
241
|
+
const model = parts[1];
|
|
242
|
+
if (!provider || !model) {
|
|
243
|
+
continue;
|
|
244
|
+
}
|
|
245
|
+
const slug = `${provider}/${model}`;
|
|
246
|
+
const maxInputTokens = parseCompactTokenCount(parts[2]);
|
|
247
|
+
records.push({
|
|
248
|
+
slug,
|
|
249
|
+
displayName: slug,
|
|
250
|
+
...(maxInputTokens !== undefined ? { maxInputTokens } : {}),
|
|
251
|
+
});
|
|
252
|
+
}
|
|
253
|
+
if (this.currentModel && !records.some((record) => record.slug === this.currentModel)) {
|
|
254
|
+
records.unshift({ slug: this.currentModel, displayName: this.currentModel });
|
|
255
|
+
}
|
|
256
|
+
return records;
|
|
257
|
+
}
|
|
258
|
+
getSessionRecord(threadId) {
|
|
259
|
+
return getPiSession(threadId, { sessionDir: this.sessionDir });
|
|
260
|
+
}
|
|
261
|
+
setModel(slug) {
|
|
262
|
+
this.currentModel = slug;
|
|
263
|
+
this.restartRpcIfIdle();
|
|
264
|
+
return slug;
|
|
265
|
+
}
|
|
266
|
+
async setModelForCurrentSession(slug) {
|
|
267
|
+
this.ensureIdle("change Pi model");
|
|
268
|
+
this.currentModel = slug;
|
|
269
|
+
let appliedToActiveThread = false;
|
|
270
|
+
if (this.rpc && this.currentThreadId) {
|
|
271
|
+
const { provider, modelId } = splitPiModelSlug(slug);
|
|
272
|
+
await this.rpc.send({ type: "set_model", provider, modelId }, 30_000);
|
|
273
|
+
await this.refreshState().catch(() => { });
|
|
274
|
+
appliedToActiveThread = true;
|
|
275
|
+
}
|
|
276
|
+
else {
|
|
277
|
+
this.restartRpcIfIdle();
|
|
278
|
+
}
|
|
279
|
+
return { value: slug, appliedToActiveThread };
|
|
280
|
+
}
|
|
281
|
+
setReasoningEffort(effort) {
|
|
282
|
+
this.currentThinking = normalizePiThinking(effort);
|
|
283
|
+
this.restartRpcIfIdle();
|
|
284
|
+
}
|
|
285
|
+
async setReasoningEffortForCurrentSession(effort) {
|
|
286
|
+
this.ensureIdle("change Pi thinking level");
|
|
287
|
+
const level = normalizePiThinking(effort);
|
|
288
|
+
this.currentThinking = level;
|
|
289
|
+
let appliedToActiveThread = false;
|
|
290
|
+
if (this.rpc && this.currentThreadId) {
|
|
291
|
+
await this.rpc.send({ type: "set_thinking_level", level }, 30_000);
|
|
292
|
+
await this.refreshState().catch(() => { });
|
|
293
|
+
appliedToActiveThread = true;
|
|
294
|
+
}
|
|
295
|
+
else {
|
|
296
|
+
this.restartRpcIfIdle();
|
|
297
|
+
}
|
|
298
|
+
return { value: level, appliedToActiveThread };
|
|
299
|
+
}
|
|
300
|
+
setLaunchProfile() {
|
|
301
|
+
throw new Error("Launch profiles are only supported by Codex sessions");
|
|
302
|
+
}
|
|
303
|
+
setFastMode() {
|
|
304
|
+
throw new Error("Fast mode is only supported by Codex sessions");
|
|
305
|
+
}
|
|
306
|
+
getSelectedLaunchProfile() {
|
|
307
|
+
return this.launchProfile;
|
|
308
|
+
}
|
|
309
|
+
syncFromCodexState() {
|
|
310
|
+
return {
|
|
311
|
+
threadId: this.currentThreadId,
|
|
312
|
+
changed: false,
|
|
313
|
+
reattached: false,
|
|
314
|
+
changedFields: [],
|
|
315
|
+
info: this.getInfo(),
|
|
316
|
+
};
|
|
317
|
+
}
|
|
318
|
+
handback() {
|
|
319
|
+
const threadId = this.currentThreadId;
|
|
320
|
+
const workspace = this.currentWorkspace;
|
|
321
|
+
const sessionPath = this.currentSessionPath;
|
|
322
|
+
this.rpc?.stop();
|
|
323
|
+
this.rpc = null;
|
|
324
|
+
this.currentThreadId = null;
|
|
325
|
+
this.currentSessionPath = undefined;
|
|
326
|
+
return {
|
|
327
|
+
threadId,
|
|
328
|
+
workspace,
|
|
329
|
+
command: sessionPath
|
|
330
|
+
? `cd ${shellQuote(workspace)} && pi --session ${shellQuote(sessionPath)}`
|
|
331
|
+
: undefined,
|
|
332
|
+
label: "Pi CLI",
|
|
333
|
+
};
|
|
334
|
+
}
|
|
335
|
+
dispose() {
|
|
336
|
+
this.rpc?.stop();
|
|
337
|
+
this.rpc = null;
|
|
338
|
+
this.processing = false;
|
|
339
|
+
}
|
|
340
|
+
async ensureSessionStarted() {
|
|
341
|
+
if (this.currentThreadId || this.currentSessionPath) {
|
|
342
|
+
this.getRpc().ensureStarted();
|
|
343
|
+
await this.refreshState().catch(() => { });
|
|
344
|
+
return;
|
|
345
|
+
}
|
|
346
|
+
await this.newThread(this.currentWorkspace, this.currentModel);
|
|
347
|
+
}
|
|
348
|
+
restartRpc() {
|
|
349
|
+
this.rpc?.stop();
|
|
350
|
+
this.rpc = new PiRpcClient({
|
|
351
|
+
commandPath: this.cliPath,
|
|
352
|
+
cwd: this.currentWorkspace,
|
|
353
|
+
sessionDir: this.sessionDir,
|
|
354
|
+
sessionPath: this.currentSessionPath,
|
|
355
|
+
model: this.currentModel,
|
|
356
|
+
thinking: this.currentThinking,
|
|
357
|
+
env: { PI_CODING_AGENT_SESSION_DIR: this.sessionDir },
|
|
358
|
+
});
|
|
359
|
+
}
|
|
360
|
+
restartRpcIfIdle() {
|
|
361
|
+
if (this.processing) {
|
|
362
|
+
return;
|
|
363
|
+
}
|
|
364
|
+
this.restartRpc();
|
|
365
|
+
}
|
|
366
|
+
getRpc() {
|
|
367
|
+
if (!this.rpc) {
|
|
368
|
+
this.restartRpc();
|
|
369
|
+
}
|
|
370
|
+
return this.rpc;
|
|
371
|
+
}
|
|
372
|
+
ensureIdle(action) {
|
|
373
|
+
if (this.processing) {
|
|
374
|
+
throw new Error(`Cannot ${action} while a turn is in progress`);
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
async refreshState() {
|
|
378
|
+
const response = await this.getRpc().send({ type: "get_state" }, 30_000);
|
|
379
|
+
const data = objectValue(response.data);
|
|
380
|
+
if (!data) {
|
|
381
|
+
return;
|
|
382
|
+
}
|
|
383
|
+
const model = objectValue(data.model);
|
|
384
|
+
const provider = stringValue(model?.provider);
|
|
385
|
+
const modelId = stringValue(model?.id);
|
|
386
|
+
this.currentModel = provider && modelId ? `${provider}/${modelId}` : modelId ?? this.currentModel;
|
|
387
|
+
this.currentThinking = stringValue(data.thinkingLevel) ?? this.currentThinking;
|
|
388
|
+
this.currentSessionPath = stringValue(data.sessionFile) ?? this.currentSessionPath;
|
|
389
|
+
this.currentThreadId = stringValue(data.sessionId) ?? this.currentThreadId;
|
|
390
|
+
this.currentSessionName = stringValue(data.sessionName) ?? this.currentSessionName;
|
|
391
|
+
if (this.currentSessionPath) {
|
|
392
|
+
const record = readPiSessionRecord(this.currentSessionPath, path.basename(path.dirname(this.currentSessionPath)));
|
|
393
|
+
if (record?.cwd) {
|
|
394
|
+
this.currentWorkspace = record.cwd;
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
async refreshStats() {
|
|
399
|
+
const response = await this.getRpc().send({ type: "get_session_stats" }, 30_000);
|
|
400
|
+
const data = objectValue(response.data);
|
|
401
|
+
if (!data) {
|
|
402
|
+
return;
|
|
403
|
+
}
|
|
404
|
+
const tokens = objectValue(data.tokens);
|
|
405
|
+
const contextUsage = objectValue(data.contextUsage);
|
|
406
|
+
this.cachedStats = {
|
|
407
|
+
sessionUsage: tokens
|
|
408
|
+
? {
|
|
409
|
+
input: numberValue(tokens.input) ?? 0,
|
|
410
|
+
output: numberValue(tokens.output) ?? 0,
|
|
411
|
+
cacheRead: numberValue(tokens.cacheRead) ?? 0,
|
|
412
|
+
cacheWrite: numberValue(tokens.cacheWrite) ?? 0,
|
|
413
|
+
total: numberValue(tokens.total) ?? 0,
|
|
414
|
+
cost: numberValue(data.cost) ?? undefined,
|
|
415
|
+
}
|
|
416
|
+
: undefined,
|
|
417
|
+
contextUsage: contextUsage
|
|
418
|
+
? {
|
|
419
|
+
tokens: numberValue(contextUsage.tokens),
|
|
420
|
+
contextWindow: numberValue(contextUsage.contextWindow),
|
|
421
|
+
percent: numberValue(contextUsage.percent),
|
|
422
|
+
}
|
|
423
|
+
: undefined,
|
|
424
|
+
};
|
|
425
|
+
}
|
|
426
|
+
async buildPromptPayload(input) {
|
|
427
|
+
if (typeof input === "string") {
|
|
428
|
+
return { message: input };
|
|
429
|
+
}
|
|
430
|
+
const textParts = [input.stagedFileInstructions, input.text].filter((part) => Boolean(part?.trim()));
|
|
431
|
+
const images = await Promise.all((input.imagePaths ?? []).map(async (imagePath) => ({
|
|
432
|
+
type: "image",
|
|
433
|
+
data: (await readFile(imagePath)).toString("base64"),
|
|
434
|
+
mimeType: mimeTypeForImage(imagePath),
|
|
435
|
+
})));
|
|
436
|
+
return {
|
|
437
|
+
message: textParts.join("\n\n") || "Please inspect the attached file(s).",
|
|
438
|
+
...(images.length > 0 ? { images } : {}),
|
|
439
|
+
};
|
|
440
|
+
}
|
|
441
|
+
refreshFromTurnEnd(event, callbacks) {
|
|
442
|
+
const message = objectValue(event.message);
|
|
443
|
+
const usage = objectValue(message?.usage);
|
|
444
|
+
if (!usage) {
|
|
445
|
+
return;
|
|
446
|
+
}
|
|
447
|
+
callbacks.onTurnComplete?.({
|
|
448
|
+
inputTokens: numberValue(usage.input) ?? 0,
|
|
449
|
+
cachedInputTokens: numberValue(usage.cacheRead) ?? 0,
|
|
450
|
+
outputTokens: numberValue(usage.output) ?? 0,
|
|
451
|
+
});
|
|
452
|
+
}
|
|
453
|
+
async waitForAgentEnd(done) {
|
|
454
|
+
if (done()) {
|
|
455
|
+
return;
|
|
456
|
+
}
|
|
457
|
+
await new Promise((resolve, reject) => {
|
|
458
|
+
const startedAt = Date.now();
|
|
459
|
+
const timer = setInterval(() => {
|
|
460
|
+
if (done()) {
|
|
461
|
+
clearInterval(timer);
|
|
462
|
+
resolve();
|
|
463
|
+
return;
|
|
464
|
+
}
|
|
465
|
+
if (!this.processing) {
|
|
466
|
+
clearInterval(timer);
|
|
467
|
+
resolve();
|
|
468
|
+
return;
|
|
469
|
+
}
|
|
470
|
+
if (Date.now() - startedAt > 24 * 60 * 60 * 1000) {
|
|
471
|
+
clearInterval(timer);
|
|
472
|
+
reject(new Error("Timed out waiting for Pi agent to finish"));
|
|
473
|
+
}
|
|
474
|
+
}, 250);
|
|
475
|
+
timer.unref?.();
|
|
476
|
+
});
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
function handleMessageUpdate(event, callbacks) {
|
|
480
|
+
const update = objectValue(event.assistantMessageEvent);
|
|
481
|
+
const updateType = stringValue(update?.type);
|
|
482
|
+
if (updateType === "text_delta") {
|
|
483
|
+
const delta = stringValue(update?.delta);
|
|
484
|
+
if (delta) {
|
|
485
|
+
callbacks.onTextDelta(delta);
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
else if (updateType === "toolcall_end") {
|
|
489
|
+
const toolCall = objectValue(update?.toolCall);
|
|
490
|
+
const id = stringValue(toolCall?.id);
|
|
491
|
+
const name = stringValue(toolCall?.name);
|
|
492
|
+
if (id && name) {
|
|
493
|
+
callbacks.onToolStart(name, id);
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
else if (updateType === "error") {
|
|
497
|
+
const toolId = "pi-message-error";
|
|
498
|
+
callbacks.onToolStart("message_error", toolId);
|
|
499
|
+
callbacks.onToolUpdate(toolId, stringValue(update?.reason) ?? "Message error");
|
|
500
|
+
callbacks.onToolEnd(toolId, true);
|
|
501
|
+
}
|
|
502
|
+
}
|
|
503
|
+
function objectValue(value) {
|
|
504
|
+
return typeof value === "object" && value !== null ? value : null;
|
|
505
|
+
}
|
|
506
|
+
function stringValue(value) {
|
|
507
|
+
return typeof value === "string" && value.trim() ? value : null;
|
|
508
|
+
}
|
|
509
|
+
function numberValue(value) {
|
|
510
|
+
return typeof value === "number" && Number.isFinite(value) ? value : null;
|
|
511
|
+
}
|
|
512
|
+
function extractContentText(container) {
|
|
513
|
+
const content = container?.content;
|
|
514
|
+
if (typeof content === "string") {
|
|
515
|
+
return content;
|
|
516
|
+
}
|
|
517
|
+
if (!Array.isArray(content)) {
|
|
518
|
+
return "";
|
|
519
|
+
}
|
|
520
|
+
return content
|
|
521
|
+
.map((entry) => {
|
|
522
|
+
const block = objectValue(entry);
|
|
523
|
+
return stringValue(block?.text) ?? "";
|
|
524
|
+
})
|
|
525
|
+
.join("");
|
|
526
|
+
}
|
|
527
|
+
function computeTextDelta(previousText, nextText) {
|
|
528
|
+
return nextText.startsWith(previousText) ? nextText.slice(previousText.length) : nextText;
|
|
529
|
+
}
|
|
530
|
+
function splitPiModelSlug(slug) {
|
|
531
|
+
const separatorIndex = slug.indexOf("/");
|
|
532
|
+
if (separatorIndex === -1) {
|
|
533
|
+
return { provider: "openai-codex", modelId: slug };
|
|
534
|
+
}
|
|
535
|
+
return {
|
|
536
|
+
provider: slug.slice(0, separatorIndex),
|
|
537
|
+
modelId: slug.slice(separatorIndex + 1),
|
|
538
|
+
};
|
|
539
|
+
}
|
|
540
|
+
function normalizePiThinking(value) {
|
|
541
|
+
if (PI_THINKING_LEVELS.includes(value)) {
|
|
542
|
+
return value;
|
|
543
|
+
}
|
|
544
|
+
throw new Error(`Unsupported Pi thinking level: ${value}`);
|
|
545
|
+
}
|
|
546
|
+
function parseCompactTokenCount(value) {
|
|
547
|
+
if (!value) {
|
|
548
|
+
return undefined;
|
|
549
|
+
}
|
|
550
|
+
const match = value.match(/^(\d+(?:\.\d+)?)([KMB])?$/i);
|
|
551
|
+
if (!match) {
|
|
552
|
+
return undefined;
|
|
553
|
+
}
|
|
554
|
+
const number = Number(match[1]);
|
|
555
|
+
const unit = match[2]?.toUpperCase();
|
|
556
|
+
const multiplier = unit === "M" ? 1_000_000 : unit === "K" ? 1_000 : unit === "B" ? 1_000_000_000 : 1;
|
|
557
|
+
return Math.round(number * multiplier);
|
|
558
|
+
}
|
|
559
|
+
function mimeTypeForImage(filePath) {
|
|
560
|
+
switch (path.extname(filePath).toLowerCase()) {
|
|
561
|
+
case ".png":
|
|
562
|
+
return "image/png";
|
|
563
|
+
case ".webp":
|
|
564
|
+
return "image/webp";
|
|
565
|
+
case ".gif":
|
|
566
|
+
return "image/gif";
|
|
567
|
+
default:
|
|
568
|
+
return "image/jpeg";
|
|
569
|
+
}
|
|
570
|
+
}
|
|
571
|
+
function shellQuote(value) {
|
|
572
|
+
return `'${value.replace(/'/g, `'\\''`)}'`;
|
|
573
|
+
}
|