pikiclaw 0.2.35
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/LICENSE +21 -0
- package/README.md +315 -0
- package/dist/agent-driver.js +24 -0
- package/dist/bot-command-ui.js +299 -0
- package/dist/bot-commands.js +236 -0
- package/dist/bot-feishu-render.js +527 -0
- package/dist/bot-feishu.js +752 -0
- package/dist/bot-handler.js +115 -0
- package/dist/bot-menu.js +44 -0
- package/dist/bot-streaming.js +165 -0
- package/dist/bot-telegram-directory.js +74 -0
- package/dist/bot-telegram-live-preview.js +192 -0
- package/dist/bot-telegram-render.js +369 -0
- package/dist/bot-telegram.js +789 -0
- package/dist/bot.js +897 -0
- package/dist/channel-base.js +46 -0
- package/dist/channel-feishu.js +873 -0
- package/dist/channel-states.js +3 -0
- package/dist/channel-telegram.js +773 -0
- package/dist/cli-channels.js +24 -0
- package/dist/cli.js +484 -0
- package/dist/code-agent.js +1080 -0
- package/dist/config-validation.js +244 -0
- package/dist/dashboard-ui.js +31 -0
- package/dist/dashboard.js +840 -0
- package/dist/driver-claude.js +520 -0
- package/dist/driver-codex.js +1055 -0
- package/dist/driver-gemini.js +230 -0
- package/dist/mcp-bridge.js +192 -0
- package/dist/mcp-session-server.js +321 -0
- package/dist/onboarding.js +138 -0
- package/dist/process-control.js +259 -0
- package/dist/run.js +275 -0
- package/dist/session-status.js +43 -0
- package/dist/setup-wizard.js +231 -0
- package/dist/user-config.js +195 -0
- package/package.json +60 -0
package/dist/bot.js
ADDED
|
@@ -0,0 +1,897 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* bot.ts — shared bot logic: config, state, streaming bridge, helpers, keep-alive.
|
|
3
|
+
*
|
|
4
|
+
* Channel-agnostic. Subclass per IM (see bot-telegram.ts).
|
|
5
|
+
*/
|
|
6
|
+
import os from 'node:os';
|
|
7
|
+
import fs from 'node:fs';
|
|
8
|
+
import path from 'node:path';
|
|
9
|
+
import { execSync, spawn } from 'node:child_process';
|
|
10
|
+
import { getActiveUserConfig, onUserConfigChange, resolveUserWorkdir, setUserWorkdir } from './user-config.js';
|
|
11
|
+
import { doStream, getSessions, getSessionTail, getUsage, initializeProjectSkills, listAgents, listModels, listSkills, } from './code-agent.js';
|
|
12
|
+
import { getDriver, hasDriver, allDriverIds } from './agent-driver.js';
|
|
13
|
+
import { terminateProcessTree } from './process-control.js';
|
|
14
|
+
export const VERSION = '0.2.35';
|
|
15
|
+
const MACOS_USER_ACTIVITY_PULSE_INTERVAL_MS = 20_000;
|
|
16
|
+
const MACOS_USER_ACTIVITY_PULSE_TIMEOUT_S = 30;
|
|
17
|
+
// ---------------------------------------------------------------------------
|
|
18
|
+
// Helpers
|
|
19
|
+
// ---------------------------------------------------------------------------
|
|
20
|
+
/**
|
|
21
|
+
* If `dir` has a .gitignore, ensure runtime state is ignored while `.pikiclaw/skills`
|
|
22
|
+
* stays trackable as the canonical project skill location.
|
|
23
|
+
*/
|
|
24
|
+
function ensureGitignore(dir) {
|
|
25
|
+
try {
|
|
26
|
+
const gi = path.join(dir, '.gitignore');
|
|
27
|
+
if (!fs.existsSync(gi))
|
|
28
|
+
return;
|
|
29
|
+
const managedLines = [
|
|
30
|
+
'.pikiclaw/*',
|
|
31
|
+
'!.pikiclaw/skills/',
|
|
32
|
+
'!.pikiclaw/skills/**',
|
|
33
|
+
'.claude/skills/',
|
|
34
|
+
'.agents/skills/',
|
|
35
|
+
];
|
|
36
|
+
const legacyLines = new Set(['.pikiclaw/']);
|
|
37
|
+
const rawLines = fs.readFileSync(gi, 'utf8').split(/\r?\n/);
|
|
38
|
+
const normalized = rawLines.filter(line => {
|
|
39
|
+
const trimmed = line.trim();
|
|
40
|
+
return trimmed && !managedLines.includes(trimmed) && !legacyLines.has(trimmed);
|
|
41
|
+
});
|
|
42
|
+
const next = [...normalized, ...managedLines, ''].join('\n');
|
|
43
|
+
const current = fs.readFileSync(gi, 'utf8');
|
|
44
|
+
if (current === next)
|
|
45
|
+
return;
|
|
46
|
+
fs.writeFileSync(gi, next);
|
|
47
|
+
}
|
|
48
|
+
catch { /* best-effort */ }
|
|
49
|
+
}
|
|
50
|
+
export function envBool(name, def) {
|
|
51
|
+
const raw = process.env[name];
|
|
52
|
+
if (raw == null)
|
|
53
|
+
return def;
|
|
54
|
+
return ['1', 'true', 'yes', 'on'].includes(raw.trim().toLowerCase());
|
|
55
|
+
}
|
|
56
|
+
export function envInt(name, def) {
|
|
57
|
+
const raw = process.env[name];
|
|
58
|
+
if (raw == null || raw.trim() === '')
|
|
59
|
+
return def;
|
|
60
|
+
const n = parseInt(raw, 10);
|
|
61
|
+
return Number.isNaN(n) ? def : n;
|
|
62
|
+
}
|
|
63
|
+
export function shellSplit(str) {
|
|
64
|
+
const args = [];
|
|
65
|
+
let cur = '', inS = false, inD = false;
|
|
66
|
+
for (const ch of str) {
|
|
67
|
+
if (ch === "'" && !inD) {
|
|
68
|
+
inS = !inS;
|
|
69
|
+
continue;
|
|
70
|
+
}
|
|
71
|
+
if (ch === '"' && !inS) {
|
|
72
|
+
inD = !inD;
|
|
73
|
+
continue;
|
|
74
|
+
}
|
|
75
|
+
if (ch === ' ' && !inS && !inD) {
|
|
76
|
+
if (cur) {
|
|
77
|
+
args.push(cur);
|
|
78
|
+
cur = '';
|
|
79
|
+
}
|
|
80
|
+
continue;
|
|
81
|
+
}
|
|
82
|
+
cur += ch;
|
|
83
|
+
}
|
|
84
|
+
if (cur)
|
|
85
|
+
args.push(cur);
|
|
86
|
+
return args;
|
|
87
|
+
}
|
|
88
|
+
export function whichSync(cmd) {
|
|
89
|
+
try {
|
|
90
|
+
return execSync(`which ${cmd} 2>/dev/null`, { encoding: 'utf-8' }).trim() || null;
|
|
91
|
+
}
|
|
92
|
+
catch {
|
|
93
|
+
return null;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
export function fmtTokens(n) {
|
|
97
|
+
if (n == null)
|
|
98
|
+
return '-';
|
|
99
|
+
return n >= 1000 ? `${(n / 1000).toFixed(1)}k` : String(n);
|
|
100
|
+
}
|
|
101
|
+
export function fmtUptime(ms) {
|
|
102
|
+
const s = Math.floor(ms / 1000);
|
|
103
|
+
if (s < 60)
|
|
104
|
+
return `${s}s`;
|
|
105
|
+
const m = Math.floor(s / 60);
|
|
106
|
+
if (m < 60)
|
|
107
|
+
return `${m}m ${s % 60}s`;
|
|
108
|
+
const h = Math.floor(m / 60);
|
|
109
|
+
if (h < 24)
|
|
110
|
+
return `${h}h ${m % 60}m`;
|
|
111
|
+
const d = Math.floor(h / 24);
|
|
112
|
+
return `${d}d ${h % 24}h`;
|
|
113
|
+
}
|
|
114
|
+
export function fmtBytes(bytes) {
|
|
115
|
+
if (bytes < 1024)
|
|
116
|
+
return `${bytes}B`;
|
|
117
|
+
if (bytes < 1024 * 1024)
|
|
118
|
+
return `${(bytes / 1024).toFixed(0)}KB`;
|
|
119
|
+
if (bytes < 1024 * 1024 * 1024)
|
|
120
|
+
return `${(bytes / 1024 / 1024).toFixed(1)}MB`;
|
|
121
|
+
if (bytes < 1024 * 1024 * 1024 * 1024)
|
|
122
|
+
return `${(bytes / 1024 / 1024 / 1024).toFixed(1)}GB`;
|
|
123
|
+
return `${(bytes / 1024 / 1024 / 1024 / 1024).toFixed(1)}TB`;
|
|
124
|
+
}
|
|
125
|
+
export function parseAllowedChatIds(raw) {
|
|
126
|
+
const ids = new Set();
|
|
127
|
+
for (const t of raw.split(',')) {
|
|
128
|
+
const v = t.trim();
|
|
129
|
+
if (!v)
|
|
130
|
+
continue;
|
|
131
|
+
const n = parseInt(v, 10);
|
|
132
|
+
// If the string is purely numeric, store as number for backward compat (Telegram).
|
|
133
|
+
// Otherwise store as string (Feishu, Discord, etc.).
|
|
134
|
+
if (!Number.isNaN(n) && String(n) === v)
|
|
135
|
+
ids.add(n);
|
|
136
|
+
else if (v)
|
|
137
|
+
ids.add(v);
|
|
138
|
+
}
|
|
139
|
+
return ids;
|
|
140
|
+
}
|
|
141
|
+
export function normalizeAgent(raw) {
|
|
142
|
+
const v = raw.trim().toLowerCase();
|
|
143
|
+
if (!hasDriver(v))
|
|
144
|
+
throw new Error(`Invalid agent: ${v}. Use: ${allDriverIds().join(', ')}`);
|
|
145
|
+
return v;
|
|
146
|
+
}
|
|
147
|
+
export function listSubdirs(dirPath) {
|
|
148
|
+
try {
|
|
149
|
+
return fs.readdirSync(dirPath)
|
|
150
|
+
.filter(name => {
|
|
151
|
+
if (name.startsWith('.'))
|
|
152
|
+
return false;
|
|
153
|
+
try {
|
|
154
|
+
return fs.statSync(path.join(dirPath, name)).isDirectory();
|
|
155
|
+
}
|
|
156
|
+
catch {
|
|
157
|
+
return false;
|
|
158
|
+
}
|
|
159
|
+
})
|
|
160
|
+
.sort((a, b) => a.toLowerCase().localeCompare(b.toLowerCase()));
|
|
161
|
+
}
|
|
162
|
+
catch {
|
|
163
|
+
return [];
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
export function thinkLabel(agent) {
|
|
167
|
+
try {
|
|
168
|
+
return getDriver(agent).thinkLabel;
|
|
169
|
+
}
|
|
170
|
+
catch {
|
|
171
|
+
return 'Thinking';
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
export function extractThinkingTail(text, maxLines = 3) {
|
|
175
|
+
const normalized = text.replace(/\r\n?/g, '\n').trim();
|
|
176
|
+
if (!normalized)
|
|
177
|
+
return '';
|
|
178
|
+
const blocks = normalized
|
|
179
|
+
.split(/\n\s*\n+/)
|
|
180
|
+
.map(block => block.trim())
|
|
181
|
+
.filter(Boolean);
|
|
182
|
+
if (blocks.length > 1)
|
|
183
|
+
return blocks[blocks.length - 1];
|
|
184
|
+
const lines = normalized
|
|
185
|
+
.split('\n')
|
|
186
|
+
.map(line => line.trimEnd())
|
|
187
|
+
.filter(line => line.trim());
|
|
188
|
+
if (lines.length > 1)
|
|
189
|
+
return lines.slice(-Math.min(maxLines, lines.length)).join('\n').trim();
|
|
190
|
+
return normalized;
|
|
191
|
+
}
|
|
192
|
+
export function formatThinkingForDisplay(text, maxChars = 800) {
|
|
193
|
+
let display = extractThinkingTail(text);
|
|
194
|
+
if (display.length > maxChars)
|
|
195
|
+
display = '...\n' + display.slice(-maxChars);
|
|
196
|
+
return display;
|
|
197
|
+
}
|
|
198
|
+
export function buildPrompt(text, files) {
|
|
199
|
+
if (!files.length)
|
|
200
|
+
return text;
|
|
201
|
+
return `${text || 'Please analyze this.'}\n\n[Files: ${files.map(f => path.basename(f)).join(', ')}]`;
|
|
202
|
+
}
|
|
203
|
+
function configModelValue(config, agent) {
|
|
204
|
+
switch (agent) {
|
|
205
|
+
case 'claude': return String(config.claudeModel || process.env.CLAUDE_MODEL || 'claude-opus-4-6').trim();
|
|
206
|
+
case 'codex': return String(config.codexModel || process.env.CODEX_MODEL || 'gpt-5.4').trim();
|
|
207
|
+
case 'gemini': return String(config.geminiModel || process.env.GEMINI_MODEL || 'gemini-3.1-pro-preview').trim();
|
|
208
|
+
}
|
|
209
|
+
return '';
|
|
210
|
+
}
|
|
211
|
+
function configReasoningEffortValue(config, agent) {
|
|
212
|
+
switch (agent) {
|
|
213
|
+
case 'claude': return String(config.claudeReasoningEffort || process.env.CLAUDE_REASONING_EFFORT || 'high').trim().toLowerCase() || 'high';
|
|
214
|
+
case 'codex': return String(config.codexReasoningEffort || process.env.CODEX_REASONING_EFFORT || 'xhigh').trim().toLowerCase() || 'xhigh';
|
|
215
|
+
case 'gemini': return null;
|
|
216
|
+
}
|
|
217
|
+
return null;
|
|
218
|
+
}
|
|
219
|
+
function normalizeBatteryState(raw) {
|
|
220
|
+
const state = (raw || '').trim().toLowerCase().replace(/\s+/g, ' ');
|
|
221
|
+
if (!state)
|
|
222
|
+
return 'unknown';
|
|
223
|
+
if (state === 'finishing charge')
|
|
224
|
+
return 'charging';
|
|
225
|
+
if (state === 'ac attached')
|
|
226
|
+
return 'plugged in';
|
|
227
|
+
return state;
|
|
228
|
+
}
|
|
229
|
+
function getMacBatteryData() {
|
|
230
|
+
try {
|
|
231
|
+
const output = execSync('pmset -g batt', { encoding: 'utf-8', timeout: 3000 }).trim();
|
|
232
|
+
if (!output || /no batteries/i.test(output))
|
|
233
|
+
return null;
|
|
234
|
+
const line = output.split('\n').find(v => /\d+%/.test(v));
|
|
235
|
+
if (!line)
|
|
236
|
+
return null;
|
|
237
|
+
const percent = line.match(/(\d+)%/)?.[1];
|
|
238
|
+
if (!percent)
|
|
239
|
+
return null;
|
|
240
|
+
const states = line
|
|
241
|
+
.split(';')
|
|
242
|
+
.slice(1)
|
|
243
|
+
.map(segment => segment.replace(/\bpresent:\s*(true|false)\b/ig, '').trim())
|
|
244
|
+
.filter(Boolean);
|
|
245
|
+
const state = states.find(segment => /(charging|discharging|charged|not charging|finishing charge|full)/i.test(segment))
|
|
246
|
+
?? states.find(segment => !/remaining/i.test(segment))
|
|
247
|
+
?? 'unknown';
|
|
248
|
+
return { percent: `${percent}%`, state: normalizeBatteryState(state) };
|
|
249
|
+
}
|
|
250
|
+
catch {
|
|
251
|
+
return null;
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
function getLinuxBatteryData() {
|
|
255
|
+
try {
|
|
256
|
+
const powerDir = '/sys/class/power_supply';
|
|
257
|
+
const batteries = fs.readdirSync(powerDir).filter(name => /^BAT/i.test(name));
|
|
258
|
+
for (const battery of batteries) {
|
|
259
|
+
const batteryDir = path.join(powerDir, battery);
|
|
260
|
+
const capacityPath = path.join(batteryDir, 'capacity');
|
|
261
|
+
if (!fs.existsSync(capacityPath))
|
|
262
|
+
continue;
|
|
263
|
+
const capacity = fs.readFileSync(capacityPath, 'utf-8').trim();
|
|
264
|
+
if (!capacity)
|
|
265
|
+
continue;
|
|
266
|
+
const statusPath = path.join(batteryDir, 'status');
|
|
267
|
+
const state = fs.existsSync(statusPath) ? fs.readFileSync(statusPath, 'utf-8').trim() : 'unknown';
|
|
268
|
+
return {
|
|
269
|
+
percent: capacity.endsWith('%') ? capacity : `${capacity}%`,
|
|
270
|
+
state: normalizeBatteryState(state),
|
|
271
|
+
};
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
catch { }
|
|
275
|
+
try {
|
|
276
|
+
const output = execSync('upower -e | grep -m1 battery | xargs -I{} upower -i "{}"', { encoding: 'utf-8', timeout: 3000 }).trim();
|
|
277
|
+
if (!output)
|
|
278
|
+
return null;
|
|
279
|
+
const percent = output.match(/percentage:\s*(\d+%)/i)?.[1];
|
|
280
|
+
if (!percent)
|
|
281
|
+
return null;
|
|
282
|
+
const state = output.match(/state:\s*([^\n]+)/i)?.[1];
|
|
283
|
+
return { percent, state: normalizeBatteryState(state) };
|
|
284
|
+
}
|
|
285
|
+
catch {
|
|
286
|
+
return null;
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
function getWindowsBatteryData() {
|
|
290
|
+
try {
|
|
291
|
+
const output = execSync('powershell -NoProfile -Command "Get-CimInstance Win32_Battery | Select-Object -First 1 EstimatedChargeRemaining,BatteryStatus | ConvertTo-Json -Compress"', { encoding: 'utf-8', timeout: 3000 }).trim();
|
|
292
|
+
if (!output || output === 'null')
|
|
293
|
+
return null;
|
|
294
|
+
const parsed = JSON.parse(output);
|
|
295
|
+
const percent = Number(parsed?.EstimatedChargeRemaining);
|
|
296
|
+
if (!Number.isFinite(percent))
|
|
297
|
+
return null;
|
|
298
|
+
const status = Number(parsed?.BatteryStatus);
|
|
299
|
+
const state = status === 6 ? 'charging'
|
|
300
|
+
: status === 3 ? 'charged'
|
|
301
|
+
: status === 2 ? 'plugged in'
|
|
302
|
+
: status === 1 ? 'discharging'
|
|
303
|
+
: 'unknown';
|
|
304
|
+
return { percent: `${percent}%`, state };
|
|
305
|
+
}
|
|
306
|
+
catch {
|
|
307
|
+
return null;
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
function getHostBatteryData() {
|
|
311
|
+
if (process.platform === 'darwin')
|
|
312
|
+
return getMacBatteryData();
|
|
313
|
+
if (process.platform === 'linux')
|
|
314
|
+
return getLinuxBatteryData();
|
|
315
|
+
if (process.platform === 'win32')
|
|
316
|
+
return getWindowsBatteryData();
|
|
317
|
+
return null;
|
|
318
|
+
}
|
|
319
|
+
function parsePercent(value) {
|
|
320
|
+
if (!value)
|
|
321
|
+
return null;
|
|
322
|
+
const n = Number.parseFloat(value.trim());
|
|
323
|
+
return Number.isFinite(n) ? n : null;
|
|
324
|
+
}
|
|
325
|
+
function getMacCpuUsageData() {
|
|
326
|
+
try {
|
|
327
|
+
const output = execSync('top -l 1 -n 0 | sed -n \'1,6p\'', { encoding: 'utf-8', timeout: 3000 });
|
|
328
|
+
const line = output.split('\n').find(entry => /^CPU usage:/i.test(entry.trim()));
|
|
329
|
+
if (!line)
|
|
330
|
+
return null;
|
|
331
|
+
const match = line.match(/CPU usage:\s*([\d.]+)% user,\s*([\d.]+)% sys,\s*([\d.]+)% idle/i);
|
|
332
|
+
if (!match)
|
|
333
|
+
return null;
|
|
334
|
+
const userPercent = parsePercent(match[1]);
|
|
335
|
+
const sysPercent = parsePercent(match[2]);
|
|
336
|
+
const idlePercent = parsePercent(match[3]);
|
|
337
|
+
if (userPercent == null || sysPercent == null || idlePercent == null)
|
|
338
|
+
return null;
|
|
339
|
+
return {
|
|
340
|
+
userPercent,
|
|
341
|
+
sysPercent,
|
|
342
|
+
idlePercent,
|
|
343
|
+
usedPercent: Math.max(0, userPercent + sysPercent),
|
|
344
|
+
};
|
|
345
|
+
}
|
|
346
|
+
catch {
|
|
347
|
+
return null;
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
function getMacMemoryUsageData(totalMem) {
|
|
351
|
+
try {
|
|
352
|
+
const output = execSync('vm_stat', { encoding: 'utf-8', timeout: 3000 });
|
|
353
|
+
const pageSize = Number.parseInt(output.match(/page size of (\d+) bytes/i)?.[1] || '', 10);
|
|
354
|
+
if (!Number.isFinite(pageSize) || pageSize <= 0)
|
|
355
|
+
return null;
|
|
356
|
+
const pages = new Map();
|
|
357
|
+
for (const line of output.split('\n')) {
|
|
358
|
+
const match = line.match(/^Pages ([^:]+):\s+(\d+)\./);
|
|
359
|
+
if (!match)
|
|
360
|
+
continue;
|
|
361
|
+
pages.set(match[1].trim().toLowerCase(), Number.parseInt(match[2], 10));
|
|
362
|
+
}
|
|
363
|
+
const reclaimablePages = (pages.get('free') || 0) +
|
|
364
|
+
(pages.get('inactive') || 0) +
|
|
365
|
+
(pages.get('speculative') || 0) +
|
|
366
|
+
(pages.get('purgeable') || 0);
|
|
367
|
+
const availableBytes = Math.max(0, reclaimablePages * pageSize);
|
|
368
|
+
const usedBytes = Math.max(0, Math.min(totalMem, totalMem - availableBytes));
|
|
369
|
+
const percent = totalMem > 0 ? (usedBytes / totalMem) * 100 : 0;
|
|
370
|
+
return { usedBytes, availableBytes, percent, source: 'vm_stat' };
|
|
371
|
+
}
|
|
372
|
+
catch {
|
|
373
|
+
return null;
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
function getHostCpuUsageData() {
|
|
377
|
+
if (process.platform === 'darwin')
|
|
378
|
+
return getMacCpuUsageData();
|
|
379
|
+
return null;
|
|
380
|
+
}
|
|
381
|
+
function getHostDisplayName() {
|
|
382
|
+
if (process.platform === 'darwin') {
|
|
383
|
+
try {
|
|
384
|
+
const name = execSync('scutil --get ComputerName', { encoding: 'utf-8', timeout: 3000 }).trim();
|
|
385
|
+
if (name)
|
|
386
|
+
return name;
|
|
387
|
+
}
|
|
388
|
+
catch { /* fall through */ }
|
|
389
|
+
}
|
|
390
|
+
return os.hostname();
|
|
391
|
+
}
|
|
392
|
+
function getHostMemoryUsageData(totalMem, freeMem) {
|
|
393
|
+
if (process.platform === 'darwin') {
|
|
394
|
+
const macData = getMacMemoryUsageData(totalMem);
|
|
395
|
+
if (macData)
|
|
396
|
+
return macData;
|
|
397
|
+
}
|
|
398
|
+
const usedBytes = Math.max(0, totalMem - freeMem);
|
|
399
|
+
const availableBytes = Math.max(0, freeMem);
|
|
400
|
+
const percent = totalMem > 0 ? (usedBytes / totalMem) * 100 : 0;
|
|
401
|
+
return { usedBytes, availableBytes, percent, source: 'os' };
|
|
402
|
+
}
|
|
403
|
+
// ---------------------------------------------------------------------------
|
|
404
|
+
// Bot
|
|
405
|
+
// ---------------------------------------------------------------------------
|
|
406
|
+
export class Bot {
|
|
407
|
+
workdir;
|
|
408
|
+
defaultAgent;
|
|
409
|
+
runTimeout;
|
|
410
|
+
allowedChatIds;
|
|
411
|
+
// Per-agent config — keyed by agent id
|
|
412
|
+
agentConfigs = {};
|
|
413
|
+
// Convenience accessors (backward-compat)
|
|
414
|
+
get codexModel() { return this.agentConfigs.codex?.model || ''; }
|
|
415
|
+
set codexModel(v) { this.agentConfigs.codex.model = v; }
|
|
416
|
+
get codexReasoningEffort() { return this.agentConfigs.codex?.reasoningEffort || 'xhigh'; }
|
|
417
|
+
set codexReasoningEffort(v) { this.agentConfigs.codex.reasoningEffort = v; }
|
|
418
|
+
get codexFullAccess() { return this.agentConfigs.codex?.fullAccess ?? true; }
|
|
419
|
+
get codexExtraArgs() { return this.agentConfigs.codex?.extraArgs || []; }
|
|
420
|
+
get claudeModel() { return this.agentConfigs.claude?.model || ''; }
|
|
421
|
+
set claudeModel(v) { this.agentConfigs.claude.model = v; }
|
|
422
|
+
get claudePermissionMode() { return this.agentConfigs.claude?.permissionMode || 'bypassPermissions'; }
|
|
423
|
+
get claudeExtraArgs() { return this.agentConfigs.claude?.extraArgs || []; }
|
|
424
|
+
chats = new Map();
|
|
425
|
+
sessionStates = new Map();
|
|
426
|
+
activeTasks = new Map();
|
|
427
|
+
startedAt = Date.now();
|
|
428
|
+
connected = false;
|
|
429
|
+
stats = { totalTurns: 0, totalInputTokens: 0, totalOutputTokens: 0, totalCachedTokens: 0 };
|
|
430
|
+
keepAliveProc = null;
|
|
431
|
+
keepAlivePulseTimer = null;
|
|
432
|
+
sessionChains = new Map();
|
|
433
|
+
userConfigUnsubscribe = null;
|
|
434
|
+
constructor() {
|
|
435
|
+
this.workdir = resolveUserWorkdir();
|
|
436
|
+
ensureGitignore(this.workdir);
|
|
437
|
+
initializeProjectSkills(this.workdir);
|
|
438
|
+
const config = getActiveUserConfig();
|
|
439
|
+
// Initialize per-agent configs
|
|
440
|
+
this.agentConfigs = {
|
|
441
|
+
codex: {
|
|
442
|
+
model: configModelValue(config, 'codex'),
|
|
443
|
+
reasoningEffort: configReasoningEffortValue(config, 'codex') || 'xhigh',
|
|
444
|
+
fullAccess: envBool('CODEX_FULL_ACCESS', true),
|
|
445
|
+
extraArgs: shellSplit(process.env.CODEX_EXTRA_ARGS || ''),
|
|
446
|
+
},
|
|
447
|
+
claude: {
|
|
448
|
+
model: configModelValue(config, 'claude'),
|
|
449
|
+
reasoningEffort: configReasoningEffortValue(config, 'claude') || 'high',
|
|
450
|
+
permissionMode: (process.env.CLAUDE_PERMISSION_MODE || 'bypassPermissions').trim(),
|
|
451
|
+
extraArgs: shellSplit(process.env.CLAUDE_EXTRA_ARGS || ''),
|
|
452
|
+
},
|
|
453
|
+
gemini: {
|
|
454
|
+
model: configModelValue(config, 'gemini'),
|
|
455
|
+
extraArgs: shellSplit(process.env.GEMINI_EXTRA_ARGS || ''),
|
|
456
|
+
},
|
|
457
|
+
};
|
|
458
|
+
this.defaultAgent = normalizeAgent('codex');
|
|
459
|
+
this.runTimeout = envInt('PIKICLAW_TIMEOUT', 1800);
|
|
460
|
+
this.allowedChatIds = parseAllowedChatIds(process.env.PIKICLAW_ALLOWED_IDS || '');
|
|
461
|
+
this.refreshManagedConfig(getActiveUserConfig(), { initial: true });
|
|
462
|
+
this.userConfigUnsubscribe = onUserConfigChange(config => this.refreshManagedConfig(config));
|
|
463
|
+
}
|
|
464
|
+
log(msg) {
|
|
465
|
+
const ts = new Date().toTimeString().slice(0, 8);
|
|
466
|
+
process.stdout.write(`[pikiclaw ${ts}] ${msg}\n`);
|
|
467
|
+
}
|
|
468
|
+
chat(chatId) {
|
|
469
|
+
let s = this.chats.get(chatId);
|
|
470
|
+
if (!s) {
|
|
471
|
+
s = { agent: this.defaultAgent, sessionId: null, activeSessionKey: null, modelId: null };
|
|
472
|
+
this.chats.set(chatId, s);
|
|
473
|
+
}
|
|
474
|
+
return s;
|
|
475
|
+
}
|
|
476
|
+
sessionKey(agent, sessionId) {
|
|
477
|
+
return `${agent}:${sessionId}`;
|
|
478
|
+
}
|
|
479
|
+
getSessionRuntimeByKey(sessionKey, opts = {}) {
|
|
480
|
+
if (!sessionKey)
|
|
481
|
+
return null;
|
|
482
|
+
const runtime = this.sessionStates.get(sessionKey) || null;
|
|
483
|
+
if (!runtime)
|
|
484
|
+
return null;
|
|
485
|
+
if (!opts.allowAnyWorkdir && runtime.workdir !== this.workdir)
|
|
486
|
+
return null;
|
|
487
|
+
return runtime;
|
|
488
|
+
}
|
|
489
|
+
getSelectedSession(cs) {
|
|
490
|
+
return this.getSessionRuntimeByKey(cs.activeSessionKey);
|
|
491
|
+
}
|
|
492
|
+
upsertSessionRuntime(session) {
|
|
493
|
+
const workdir = path.resolve(session.workdir || this.workdir);
|
|
494
|
+
const key = this.sessionKey(session.agent, session.sessionId);
|
|
495
|
+
const existing = this.sessionStates.get(key);
|
|
496
|
+
if (existing) {
|
|
497
|
+
existing.workdir = workdir;
|
|
498
|
+
existing.agent = session.agent;
|
|
499
|
+
existing.sessionId = session.sessionId;
|
|
500
|
+
if (session.workspacePath !== undefined)
|
|
501
|
+
existing.workspacePath = session.workspacePath ?? null;
|
|
502
|
+
if (session.codexCumulative !== undefined)
|
|
503
|
+
existing.codexCumulative = session.codexCumulative;
|
|
504
|
+
if (session.modelId !== undefined)
|
|
505
|
+
existing.modelId = session.modelId ?? null;
|
|
506
|
+
return existing;
|
|
507
|
+
}
|
|
508
|
+
const runtime = {
|
|
509
|
+
key,
|
|
510
|
+
workdir,
|
|
511
|
+
agent: session.agent,
|
|
512
|
+
sessionId: session.sessionId,
|
|
513
|
+
workspacePath: session.workspacePath ?? null,
|
|
514
|
+
codexCumulative: session.codexCumulative,
|
|
515
|
+
modelId: session.modelId ?? null,
|
|
516
|
+
runningTaskIds: new Set(),
|
|
517
|
+
};
|
|
518
|
+
this.sessionStates.set(key, runtime);
|
|
519
|
+
return runtime;
|
|
520
|
+
}
|
|
521
|
+
applySessionSelection(cs, session) {
|
|
522
|
+
cs.activeSessionKey = session?.key ?? null;
|
|
523
|
+
if (session) {
|
|
524
|
+
cs.agent = session.agent;
|
|
525
|
+
cs.sessionId = session.sessionId;
|
|
526
|
+
cs.workspacePath = session.workspacePath;
|
|
527
|
+
cs.codexCumulative = session.codexCumulative;
|
|
528
|
+
cs.modelId = session.modelId ?? null;
|
|
529
|
+
return;
|
|
530
|
+
}
|
|
531
|
+
cs.sessionId = null;
|
|
532
|
+
cs.workspacePath = null;
|
|
533
|
+
cs.codexCumulative = undefined;
|
|
534
|
+
cs.modelId = null;
|
|
535
|
+
}
|
|
536
|
+
resetChatConversation(cs) {
|
|
537
|
+
this.applySessionSelection(cs, null);
|
|
538
|
+
}
|
|
539
|
+
adoptSession(cs, session) {
|
|
540
|
+
if (!session.sessionId) {
|
|
541
|
+
this.applySessionSelection(cs, null);
|
|
542
|
+
return;
|
|
543
|
+
}
|
|
544
|
+
const runtime = this.upsertSessionRuntime({
|
|
545
|
+
agent: session.agent,
|
|
546
|
+
sessionId: session.sessionId,
|
|
547
|
+
workspacePath: session.workspacePath ?? null,
|
|
548
|
+
modelId: session.model ?? null,
|
|
549
|
+
});
|
|
550
|
+
this.applySessionSelection(cs, runtime);
|
|
551
|
+
}
|
|
552
|
+
syncSelectedChats(session) {
|
|
553
|
+
for (const [, cs] of this.chats) {
|
|
554
|
+
if (cs.activeSessionKey !== session.key)
|
|
555
|
+
continue;
|
|
556
|
+
this.applySessionSelection(cs, session);
|
|
557
|
+
}
|
|
558
|
+
}
|
|
559
|
+
beginTask(task) {
|
|
560
|
+
this.activeTasks.set(task.taskId, task);
|
|
561
|
+
const session = this.getSessionRuntimeByKey(task.sessionKey, { allowAnyWorkdir: true });
|
|
562
|
+
session?.runningTaskIds.add(task.taskId);
|
|
563
|
+
}
|
|
564
|
+
finishTask(taskId) {
|
|
565
|
+
const task = this.activeTasks.get(taskId);
|
|
566
|
+
if (!task)
|
|
567
|
+
return;
|
|
568
|
+
this.activeTasks.delete(taskId);
|
|
569
|
+
const session = this.getSessionRuntimeByKey(task.sessionKey, { allowAnyWorkdir: true });
|
|
570
|
+
if (!session)
|
|
571
|
+
return;
|
|
572
|
+
session.runningTaskIds.delete(taskId);
|
|
573
|
+
if (!session.runningTaskIds.size && session.workdir !== this.workdir) {
|
|
574
|
+
this.sessionStates.delete(session.key);
|
|
575
|
+
}
|
|
576
|
+
}
|
|
577
|
+
runningTaskForSession(sessionKey) {
|
|
578
|
+
const session = this.getSessionRuntimeByKey(sessionKey, { allowAnyWorkdir: true });
|
|
579
|
+
if (!session || !session.runningTaskIds.size)
|
|
580
|
+
return null;
|
|
581
|
+
let running = null;
|
|
582
|
+
for (const taskId of session.runningTaskIds) {
|
|
583
|
+
const task = this.activeTasks.get(taskId);
|
|
584
|
+
if (!task)
|
|
585
|
+
continue;
|
|
586
|
+
if (!running || task.startedAt < running.startedAt)
|
|
587
|
+
running = task;
|
|
588
|
+
}
|
|
589
|
+
return running;
|
|
590
|
+
}
|
|
591
|
+
queueSessionTask(session, task) {
|
|
592
|
+
const prev = this.sessionChains.get(session.key) || Promise.resolve();
|
|
593
|
+
const current = prev.catch(() => { }).then(task);
|
|
594
|
+
const settled = current.then(() => { }, () => { });
|
|
595
|
+
const chained = settled.finally(() => {
|
|
596
|
+
if (this.sessionChains.get(session.key) === chained)
|
|
597
|
+
this.sessionChains.delete(session.key);
|
|
598
|
+
});
|
|
599
|
+
this.sessionChains.set(session.key, chained);
|
|
600
|
+
return current;
|
|
601
|
+
}
|
|
602
|
+
sessionHasPendingWork(session) {
|
|
603
|
+
return this.sessionChains.has(session.key);
|
|
604
|
+
}
|
|
605
|
+
selectedSession(chatId) {
|
|
606
|
+
return this.getSelectedSession(this.chat(chatId));
|
|
607
|
+
}
|
|
608
|
+
resetConversationForChat(chatId) {
|
|
609
|
+
this.resetChatConversation(this.chat(chatId));
|
|
610
|
+
}
|
|
611
|
+
adoptExistingSessionForChat(chatId, session) {
|
|
612
|
+
const cs = this.chat(chatId);
|
|
613
|
+
this.adoptSession(cs, session);
|
|
614
|
+
return this.getSelectedSession(cs);
|
|
615
|
+
}
|
|
616
|
+
switchAgentForChat(chatId, agent) {
|
|
617
|
+
const cs = this.chat(chatId);
|
|
618
|
+
if (cs.agent === agent)
|
|
619
|
+
return false;
|
|
620
|
+
cs.agent = agent;
|
|
621
|
+
this.resetChatConversation(cs);
|
|
622
|
+
this.log(`agent switched to ${agent} chat=${chatId}`);
|
|
623
|
+
return true;
|
|
624
|
+
}
|
|
625
|
+
switchModelForChat(chatId, modelId) {
|
|
626
|
+
const cs = this.chat(chatId);
|
|
627
|
+
this.setModelForAgent(cs.agent, modelId);
|
|
628
|
+
this.resetChatConversation(cs);
|
|
629
|
+
this.log(`model switched to ${modelId} for ${cs.agent} chat=${chatId}`);
|
|
630
|
+
}
|
|
631
|
+
switchEffortForChat(chatId, effort) {
|
|
632
|
+
const cs = this.chat(chatId);
|
|
633
|
+
this.setEffortForAgent(cs.agent, effort);
|
|
634
|
+
this.log(`effort switched to ${effort} for ${cs.agent} chat=${chatId}`);
|
|
635
|
+
}
|
|
636
|
+
modelForAgent(agent) {
|
|
637
|
+
return this.agentConfigs[agent]?.model || '';
|
|
638
|
+
}
|
|
639
|
+
fetchSessions(agent) {
|
|
640
|
+
return getSessions({ agent, workdir: this.workdir });
|
|
641
|
+
}
|
|
642
|
+
fetchSessionTail(agent, sessionId, limit) {
|
|
643
|
+
return getSessionTail({ agent, sessionId, workdir: this.workdir, limit });
|
|
644
|
+
}
|
|
645
|
+
fetchAgents(options = {}) {
|
|
646
|
+
return listAgents(options);
|
|
647
|
+
}
|
|
648
|
+
fetchSkills() {
|
|
649
|
+
initializeProjectSkills(this.workdir);
|
|
650
|
+
return listSkills(this.workdir);
|
|
651
|
+
}
|
|
652
|
+
fetchModels(agent) {
|
|
653
|
+
return listModels(agent, { workdir: this.workdir, currentModel: this.modelForAgent(agent) });
|
|
654
|
+
}
|
|
655
|
+
setDefaultAgent(agent) {
|
|
656
|
+
const next = normalizeAgent(agent);
|
|
657
|
+
const prev = this.defaultAgent;
|
|
658
|
+
this.defaultAgent = next;
|
|
659
|
+
for (const [, cs] of this.chats) {
|
|
660
|
+
if (cs.activeSessionKey || cs.sessionId)
|
|
661
|
+
continue;
|
|
662
|
+
if (cs.agent === prev)
|
|
663
|
+
cs.agent = next;
|
|
664
|
+
}
|
|
665
|
+
this.log(`default agent changed to ${next}`);
|
|
666
|
+
}
|
|
667
|
+
setModelForAgent(agent, modelId) {
|
|
668
|
+
const config = this.agentConfigs[agent];
|
|
669
|
+
if (config)
|
|
670
|
+
config.model = modelId;
|
|
671
|
+
this.log(`model for ${agent} changed to ${modelId}`);
|
|
672
|
+
}
|
|
673
|
+
effortForAgent(agent) {
|
|
674
|
+
if (agent === 'gemini')
|
|
675
|
+
return null;
|
|
676
|
+
return this.agentConfigs[agent]?.reasoningEffort || 'high';
|
|
677
|
+
}
|
|
678
|
+
setEffortForAgent(agent, effort) {
|
|
679
|
+
const config = this.agentConfigs[agent];
|
|
680
|
+
if (config)
|
|
681
|
+
config.reasoningEffort = effort;
|
|
682
|
+
this.log(`effort for ${agent} changed to ${effort}`);
|
|
683
|
+
}
|
|
684
|
+
getStatusData(chatId) {
|
|
685
|
+
const cs = this.chat(chatId);
|
|
686
|
+
const selectedSession = this.getSelectedSession(cs);
|
|
687
|
+
const selectedTask = this.runningTaskForSession(selectedSession?.key ?? null);
|
|
688
|
+
const fallbackTask = selectedTask || [...this.activeTasks.values()]
|
|
689
|
+
.sort((a, b) => a.startedAt - b.startedAt)[0] || null;
|
|
690
|
+
const model = selectedSession?.modelId || this.modelForAgent(cs.agent);
|
|
691
|
+
const mem = process.memoryUsage();
|
|
692
|
+
return {
|
|
693
|
+
version: VERSION, uptime: Date.now() - this.startedAt,
|
|
694
|
+
memRss: mem.rss, memHeap: mem.heapUsed, pid: process.pid,
|
|
695
|
+
workdir: this.workdir, agent: cs.agent, model, sessionId: cs.sessionId,
|
|
696
|
+
workspacePath: cs.workspacePath ?? null,
|
|
697
|
+
running: fallbackTask, activeTasksCount: this.activeTasks.size, stats: this.stats,
|
|
698
|
+
usage: getUsage({ agent: cs.agent, model }),
|
|
699
|
+
};
|
|
700
|
+
}
|
|
701
|
+
getHostData() {
|
|
702
|
+
const cpus = os.cpus();
|
|
703
|
+
const totalMem = os.totalmem(), freeMem = os.freemem();
|
|
704
|
+
const memory = getHostMemoryUsageData(totalMem, freeMem);
|
|
705
|
+
const cpuUsage = getHostCpuUsageData();
|
|
706
|
+
let disk = null;
|
|
707
|
+
const battery = getHostBatteryData();
|
|
708
|
+
try {
|
|
709
|
+
const df = execSync(`df -h "${this.workdir}" | tail -1`, { encoding: 'utf-8', timeout: 3000 }).trim().split(/\s+/);
|
|
710
|
+
if (df.length >= 5)
|
|
711
|
+
disk = { used: df[2], total: df[1], percent: df[4] };
|
|
712
|
+
}
|
|
713
|
+
catch { }
|
|
714
|
+
let topProcs = [];
|
|
715
|
+
try {
|
|
716
|
+
topProcs = execSync(`ps -eo pid,pcpu,pmem,comm --sort=-pcpu 2>/dev/null | head -6 || ps -eo pid,%cpu,%mem,comm -r 2>/dev/null | head -6`, { encoding: 'utf-8', timeout: 3000 }).trim().split('\n');
|
|
717
|
+
}
|
|
718
|
+
catch { }
|
|
719
|
+
const mem = process.memoryUsage();
|
|
720
|
+
return {
|
|
721
|
+
hostName: getHostDisplayName(),
|
|
722
|
+
cpuModel: cpus[0]?.model || 'unknown', cpuCount: cpus.length,
|
|
723
|
+
cpuUsage,
|
|
724
|
+
totalMem, freeMem, memoryUsed: memory.usedBytes, memoryAvailable: memory.availableBytes, memoryPercent: memory.percent, memorySource: memory.source,
|
|
725
|
+
disk, battery, topProcs,
|
|
726
|
+
selfPid: process.pid, selfRss: mem.rss, selfHeap: mem.heapUsed,
|
|
727
|
+
};
|
|
728
|
+
}
|
|
729
|
+
switchWorkdir(newPath, opts = {}) {
|
|
730
|
+
const old = this.workdir;
|
|
731
|
+
const resolvedPath = path.resolve(newPath.replace(/^~/, process.env.HOME || ''));
|
|
732
|
+
if (opts.persist !== false) {
|
|
733
|
+
setUserWorkdir(resolvedPath, { notify: false });
|
|
734
|
+
}
|
|
735
|
+
else {
|
|
736
|
+
process.env.PIKICLAW_WORKDIR = resolvedPath;
|
|
737
|
+
}
|
|
738
|
+
this.workdir = resolvedPath;
|
|
739
|
+
for (const [, cs] of this.chats) {
|
|
740
|
+
this.resetChatConversation(cs);
|
|
741
|
+
}
|
|
742
|
+
for (const [key, session] of this.sessionStates) {
|
|
743
|
+
if (session.workdir === old && !session.runningTaskIds.size)
|
|
744
|
+
this.sessionStates.delete(key);
|
|
745
|
+
}
|
|
746
|
+
ensureGitignore(resolvedPath);
|
|
747
|
+
initializeProjectSkills(resolvedPath);
|
|
748
|
+
this.log(`switch workdir: ${old} -> ${resolvedPath}`);
|
|
749
|
+
this.afterSwitchWorkdir(old, resolvedPath);
|
|
750
|
+
return old;
|
|
751
|
+
}
|
|
752
|
+
afterSwitchWorkdir(_oldPath, _newPath) { }
|
|
753
|
+
onManagedConfigChange(_config, _opts = {}) { }
|
|
754
|
+
refreshManagedConfig(config, opts = {}) {
|
|
755
|
+
const nextWorkdir = resolveUserWorkdir({ config });
|
|
756
|
+
if (opts.initial) {
|
|
757
|
+
this.workdir = nextWorkdir;
|
|
758
|
+
ensureGitignore(this.workdir);
|
|
759
|
+
initializeProjectSkills(this.workdir);
|
|
760
|
+
}
|
|
761
|
+
else if (nextWorkdir !== this.workdir) {
|
|
762
|
+
this.switchWorkdir(nextWorkdir, { persist: false });
|
|
763
|
+
}
|
|
764
|
+
const nextDefaultAgent = normalizeAgent(String(config.defaultAgent || 'codex').trim().toLowerCase() || 'codex');
|
|
765
|
+
if (opts.initial)
|
|
766
|
+
this.defaultAgent = nextDefaultAgent;
|
|
767
|
+
else if (nextDefaultAgent !== this.defaultAgent)
|
|
768
|
+
this.setDefaultAgent(nextDefaultAgent);
|
|
769
|
+
for (const agent of ['claude', 'codex', 'gemini']) {
|
|
770
|
+
const nextModel = configModelValue(config, agent);
|
|
771
|
+
if (nextModel && this.modelForAgent(agent) !== nextModel) {
|
|
772
|
+
if (opts.initial)
|
|
773
|
+
this.agentConfigs[agent].model = nextModel;
|
|
774
|
+
else
|
|
775
|
+
this.setModelForAgent(agent, nextModel);
|
|
776
|
+
}
|
|
777
|
+
const nextEffort = configReasoningEffortValue(config, agent);
|
|
778
|
+
if (nextEffort && agent !== 'gemini' && this.effortForAgent(agent) !== nextEffort) {
|
|
779
|
+
if (opts.initial)
|
|
780
|
+
this.agentConfigs[agent].reasoningEffort = nextEffort;
|
|
781
|
+
else
|
|
782
|
+
this.setEffortForAgent(agent, nextEffort);
|
|
783
|
+
}
|
|
784
|
+
}
|
|
785
|
+
if (!opts.initial)
|
|
786
|
+
this.onManagedConfigChange(config, opts);
|
|
787
|
+
}
|
|
788
|
+
async runStream(prompt, cs, attachments, onText, systemPrompt, mcpSendFile) {
|
|
789
|
+
const resolvedModel = cs.modelId || this.modelForAgent(cs.agent);
|
|
790
|
+
const agentConfig = this.agentConfigs[cs.agent] || {};
|
|
791
|
+
const extraArgs = agentConfig.extraArgs || [];
|
|
792
|
+
this.log(`[runStream] agent=${cs.agent} session=${cs.sessionId || '(new)'} workdir=${this.workdir} timeout=${this.runTimeout}s attachments=${attachments.length}`);
|
|
793
|
+
this.log(`[runStream] ${cs.agent} config: model=${resolvedModel} extraArgs=[${extraArgs.join(' ')}]`);
|
|
794
|
+
const opts = {
|
|
795
|
+
agent: cs.agent, prompt, workdir: this.workdir, timeout: this.runTimeout,
|
|
796
|
+
sessionId: cs.sessionId, model: null,
|
|
797
|
+
thinkingEffort: agentConfig.reasoningEffort || 'high', onText,
|
|
798
|
+
attachments: attachments.length ? attachments : undefined,
|
|
799
|
+
// codex-specific
|
|
800
|
+
codexModel: cs.agent === 'codex' ? resolvedModel : this.codexModel,
|
|
801
|
+
codexFullAccess: this.codexFullAccess,
|
|
802
|
+
codexDeveloperInstructions: systemPrompt || undefined,
|
|
803
|
+
codexExtraArgs: this.codexExtraArgs.length ? this.codexExtraArgs : undefined,
|
|
804
|
+
codexPrevCumulative: cs.codexCumulative,
|
|
805
|
+
// claude-specific
|
|
806
|
+
claudeModel: cs.agent === 'claude' ? resolvedModel : this.claudeModel,
|
|
807
|
+
claudePermissionMode: this.claudePermissionMode,
|
|
808
|
+
claudeAppendSystemPrompt: systemPrompt || undefined,
|
|
809
|
+
claudeExtraArgs: this.claudeExtraArgs.length ? this.claudeExtraArgs : undefined,
|
|
810
|
+
// gemini-specific
|
|
811
|
+
geminiModel: cs.agent === 'gemini' ? resolvedModel : (this.agentConfigs.gemini?.model || ''),
|
|
812
|
+
geminiExtraArgs: this.agentConfigs.gemini?.extraArgs?.length ? this.agentConfigs.gemini.extraArgs : undefined,
|
|
813
|
+
// MCP bridge
|
|
814
|
+
mcpSendFile,
|
|
815
|
+
};
|
|
816
|
+
const result = await doStream(opts);
|
|
817
|
+
this.stats.totalTurns++;
|
|
818
|
+
if (result.inputTokens)
|
|
819
|
+
this.stats.totalInputTokens += result.inputTokens;
|
|
820
|
+
if (result.outputTokens)
|
|
821
|
+
this.stats.totalOutputTokens += result.outputTokens;
|
|
822
|
+
if (result.cachedInputTokens)
|
|
823
|
+
this.stats.totalCachedTokens += result.cachedInputTokens;
|
|
824
|
+
if (result.codexCumulative)
|
|
825
|
+
cs.codexCumulative = result.codexCumulative;
|
|
826
|
+
if (result.sessionId)
|
|
827
|
+
cs.sessionId = result.sessionId;
|
|
828
|
+
if (result.workspacePath)
|
|
829
|
+
cs.workspacePath = result.workspacePath;
|
|
830
|
+
if (result.model)
|
|
831
|
+
cs.modelId = result.model;
|
|
832
|
+
if ('key' in cs && typeof cs.key === 'string') {
|
|
833
|
+
// If session was promoted from pending, update the runtime key
|
|
834
|
+
const runtime = this.getSessionRuntimeByKey(cs.key, { allowAnyWorkdir: true });
|
|
835
|
+
if (runtime && result.sessionId && runtime.sessionId !== result.sessionId) {
|
|
836
|
+
this.sessionStates.delete(runtime.key);
|
|
837
|
+
runtime.sessionId = result.sessionId;
|
|
838
|
+
runtime.key = this.sessionKey(runtime.agent, result.sessionId);
|
|
839
|
+
this.sessionStates.set(runtime.key, runtime);
|
|
840
|
+
// Update all chats pointing to the old key
|
|
841
|
+
for (const [, chatState] of this.chats) {
|
|
842
|
+
if (chatState.activeSessionKey === cs.key)
|
|
843
|
+
chatState.activeSessionKey = runtime.key;
|
|
844
|
+
}
|
|
845
|
+
}
|
|
846
|
+
if (runtime)
|
|
847
|
+
this.syncSelectedChats(runtime);
|
|
848
|
+
}
|
|
849
|
+
this.log(`[runStream] completed turn=${this.stats.totalTurns} cumulative: in=${fmtTokens(this.stats.totalInputTokens)} out=${fmtTokens(this.stats.totalOutputTokens)} cached=${fmtTokens(this.stats.totalCachedTokens)}`);
|
|
850
|
+
return result;
|
|
851
|
+
}
|
|
852
|
+
startKeepAlive() {
|
|
853
|
+
if (process.platform === 'darwin') {
|
|
854
|
+
if (this.keepAliveProc || this.keepAlivePulseTimer)
|
|
855
|
+
return;
|
|
856
|
+
const bin = whichSync('caffeinate');
|
|
857
|
+
if (bin) {
|
|
858
|
+
this.keepAliveProc = spawn('caffeinate', ['-dis'], { stdio: 'ignore', detached: true });
|
|
859
|
+
this.keepAliveProc.unref();
|
|
860
|
+
this.log(`keep-alive: caffeinate (PID ${this.keepAliveProc.pid})`);
|
|
861
|
+
const pulseUserActivity = () => {
|
|
862
|
+
const pulse = spawn('caffeinate', ['-u', '-t', String(MACOS_USER_ACTIVITY_PULSE_TIMEOUT_S)], {
|
|
863
|
+
stdio: 'ignore',
|
|
864
|
+
detached: true,
|
|
865
|
+
});
|
|
866
|
+
pulse.unref();
|
|
867
|
+
};
|
|
868
|
+
pulseUserActivity();
|
|
869
|
+
this.keepAlivePulseTimer = setInterval(pulseUserActivity, MACOS_USER_ACTIVITY_PULSE_INTERVAL_MS);
|
|
870
|
+
this.keepAlivePulseTimer.unref?.();
|
|
871
|
+
this.log(`keep-alive: macOS user activity pulse every ${MACOS_USER_ACTIVITY_PULSE_INTERVAL_MS / 1000}s`);
|
|
872
|
+
}
|
|
873
|
+
}
|
|
874
|
+
else if (process.platform === 'linux') {
|
|
875
|
+
if (this.keepAliveProc)
|
|
876
|
+
return;
|
|
877
|
+
const bin = whichSync('systemd-inhibit');
|
|
878
|
+
if (bin) {
|
|
879
|
+
this.keepAliveProc = spawn('systemd-inhibit', [
|
|
880
|
+
'--what=idle', '--who=pikiclaw', '--why=AI coding agent running', 'sleep', 'infinity',
|
|
881
|
+
], { stdio: 'ignore', detached: true });
|
|
882
|
+
this.keepAliveProc.unref();
|
|
883
|
+
this.log(`keep-alive: systemd-inhibit (PID ${this.keepAliveProc.pid})`);
|
|
884
|
+
}
|
|
885
|
+
}
|
|
886
|
+
}
|
|
887
|
+
stopKeepAlive() {
|
|
888
|
+
if (this.keepAlivePulseTimer) {
|
|
889
|
+
clearInterval(this.keepAlivePulseTimer);
|
|
890
|
+
this.keepAlivePulseTimer = null;
|
|
891
|
+
}
|
|
892
|
+
if (this.keepAliveProc) {
|
|
893
|
+
terminateProcessTree(this.keepAliveProc, { signal: 'SIGTERM', forceSignal: 'SIGKILL', forceAfterMs: 2000 });
|
|
894
|
+
this.keepAliveProc = null;
|
|
895
|
+
}
|
|
896
|
+
}
|
|
897
|
+
}
|