pikiloop 0.4.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/LICENSE +21 -0
- package/README.md +353 -0
- package/README.v2.md +287 -0
- package/README.zh-CN.md +352 -0
- package/dashboard/dist/assets/AgentTab-UZPIhlkr.js +1 -0
- package/dashboard/dist/assets/DirBrowser-Ckcmi-Pi.js +1 -0
- package/dashboard/dist/assets/ExtensionsTab-KZhEDrdu.js +1 -0
- package/dashboard/dist/assets/IMAccessTab-Bd_IY1GQ.js +1 -0
- package/dashboard/dist/assets/Modal-CTeL0y7P.js +1 -0
- package/dashboard/dist/assets/Modals-axftHasy.js +1 -0
- package/dashboard/dist/assets/Select-C8tOdPhe.js +1 -0
- package/dashboard/dist/assets/SessionPanel-C1geSRxw.js +1 -0
- package/dashboard/dist/assets/SystemTab-DBDkaPiO.js +1 -0
- package/dashboard/dist/assets/anthropic-BAdojD7P.ico +0 -0
- package/dashboard/dist/assets/codex-DYadqqp0.png +0 -0
- package/dashboard/dist/assets/deepseek-BeYNZEk0.ico +0 -0
- package/dashboard/dist/assets/doubao-DloFDuFR.png +0 -0
- package/dashboard/dist/assets/feishu-C4OMrjCW.ico +0 -0
- package/dashboard/dist/assets/gemini-BYkEpiWr.svg +1 -0
- package/dashboard/dist/assets/hermes-BAarh-tH.png +0 -0
- package/dashboard/dist/assets/index-CpM4CqZJ.js +23 -0
- package/dashboard/dist/assets/index-DXSohzrE.js +3 -0
- package/dashboard/dist/assets/index-reSbuley.css +1 -0
- package/dashboard/dist/assets/markdown-DxQYQFeH.js +29 -0
- package/dashboard/dist/assets/minimax-PuEGTfrF.ico +0 -0
- package/dashboard/dist/assets/mlx-DhWwjtMw.png +0 -0
- package/dashboard/dist/assets/ollama-Bt9O-2K_.png +0 -0
- package/dashboard/dist/assets/openrouter-CsJ_bD5Q.ico +0 -0
- package/dashboard/dist/assets/playwright-BldPFZgC.ico +0 -0
- package/dashboard/dist/assets/qwen-xykkX0_y.png +0 -0
- package/dashboard/dist/assets/react-vendor-C7Sl8SE7.js +9 -0
- package/dashboard/dist/assets/router-DHISdpPk.js +3 -0
- package/dashboard/dist/assets/shared-BIP_4k4I.js +1 -0
- package/dashboard/dist/favicon.svg +28 -0
- package/dashboard/dist/index.html +17 -0
- package/dist/agent/acp-client.js +261 -0
- package/dist/agent/auto-update.js +432 -0
- package/dist/agent/await-resume.js +50 -0
- package/dist/agent/cli/auth.js +325 -0
- package/dist/agent/cli/catalog.js +40 -0
- package/dist/agent/cli/detector.js +136 -0
- package/dist/agent/cli/index.js +7 -0
- package/dist/agent/cli/registry.js +33 -0
- package/dist/agent/driver.js +39 -0
- package/dist/agent/drivers/claude-tui.js +2297 -0
- package/dist/agent/drivers/claude.js +2689 -0
- package/dist/agent/drivers/codex.js +2210 -0
- package/dist/agent/drivers/gemini.js +1059 -0
- package/dist/agent/drivers/hermes.js +795 -0
- package/dist/agent/goal.js +274 -0
- package/dist/agent/handover.js +130 -0
- package/dist/agent/images.js +355 -0
- package/dist/agent/index.js +50 -0
- package/dist/agent/mcp/bridge.js +791 -0
- package/dist/agent/mcp/extensions.js +637 -0
- package/dist/agent/mcp/oauth.js +353 -0
- package/dist/agent/mcp/registry.js +119 -0
- package/dist/agent/mcp/session-server.js +229 -0
- package/dist/agent/mcp/tools/ask-user.js +113 -0
- package/dist/agent/mcp/tools/await-resume.js +77 -0
- package/dist/agent/mcp/tools/goal.js +144 -0
- package/dist/agent/mcp/tools/types.js +12 -0
- package/dist/agent/mcp/tools/workspace.js +212 -0
- package/dist/agent/npm.js +31 -0
- package/dist/agent/session.js +1206 -0
- package/dist/agent/skill-installer.js +160 -0
- package/dist/agent/skills.js +257 -0
- package/dist/agent/stream.js +743 -0
- package/dist/agent/types.js +13 -0
- package/dist/agent/utils.js +687 -0
- package/dist/bot/bot.js +2499 -0
- package/dist/bot/command-ui.js +633 -0
- package/dist/bot/commands.js +513 -0
- package/dist/bot/headless-bot.js +36 -0
- package/dist/bot/host.js +192 -0
- package/dist/bot/human-loop.js +168 -0
- package/dist/bot/menu.js +48 -0
- package/dist/bot/orchestration.js +79 -0
- package/dist/bot/render-shared.js +309 -0
- package/dist/bot/session-hub.js +361 -0
- package/dist/bot/session-status.js +55 -0
- package/dist/bot/streaming.js +309 -0
- package/dist/browser-profile.js +579 -0
- package/dist/browser-supervisor.js +249 -0
- package/dist/catalog/cli-tools.js +421 -0
- package/dist/catalog/index.js +21 -0
- package/dist/catalog/local-models.js +94 -0
- package/dist/catalog/mcp-servers.js +315 -0
- package/dist/catalog/skill-repos.js +173 -0
- package/dist/channels/base.js +55 -0
- package/dist/channels/dingtalk/bot.js +549 -0
- package/dist/channels/dingtalk/channel.js +268 -0
- package/dist/channels/discord/bot.js +552 -0
- package/dist/channels/discord/channel.js +245 -0
- package/dist/channels/feishu/bot.js +1275 -0
- package/dist/channels/feishu/channel.js +911 -0
- package/dist/channels/feishu/markdown.js +91 -0
- package/dist/channels/feishu/render.js +619 -0
- package/dist/channels/health.js +109 -0
- package/dist/channels/slack/bot.js +554 -0
- package/dist/channels/slack/channel.js +283 -0
- package/dist/channels/states.js +6 -0
- package/dist/channels/telegram/bot.js +1310 -0
- package/dist/channels/telegram/channel.js +820 -0
- package/dist/channels/telegram/directory.js +111 -0
- package/dist/channels/telegram/live-preview.js +220 -0
- package/dist/channels/telegram/render.js +384 -0
- package/dist/channels/wecom/bot.js +558 -0
- package/dist/channels/wecom/channel.js +479 -0
- package/dist/channels/weixin/api.js +520 -0
- package/dist/channels/weixin/bot.js +1000 -0
- package/dist/channels/weixin/channel.js +222 -0
- package/dist/cli/autostart.js +262 -0
- package/dist/cli/channel-supervisor.js +313 -0
- package/dist/cli/channels.js +54 -0
- package/dist/cli/main.js +726 -0
- package/dist/cli/onboarding.js +227 -0
- package/dist/cli/run.js +308 -0
- package/dist/cli/setup-wizard.js +235 -0
- package/dist/core/config/runtime-config.js +201 -0
- package/dist/core/config/user-config.js +510 -0
- package/dist/core/config/validation.js +521 -0
- package/dist/core/constants.js +400 -0
- package/dist/core/git.js +145 -0
- package/dist/core/legacy-compat.js +60 -0
- package/dist/core/logging.js +101 -0
- package/dist/core/platform.js +59 -0
- package/dist/core/process-control.js +315 -0
- package/dist/core/secrets/index.js +42 -0
- package/dist/core/secrets/inline-seal.js +60 -0
- package/dist/core/secrets/ref.js +33 -0
- package/dist/core/secrets/resolver.js +65 -0
- package/dist/core/secrets/store.js +63 -0
- package/dist/core/utils.js +233 -0
- package/dist/core/version.js +15 -0
- package/dist/dashboard/platform.js +219 -0
- package/dist/dashboard/routes/agents.js +450 -0
- package/dist/dashboard/routes/cli.js +174 -0
- package/dist/dashboard/routes/config.js +523 -0
- package/dist/dashboard/routes/extensions.js +745 -0
- package/dist/dashboard/routes/local-models.js +290 -0
- package/dist/dashboard/routes/models.js +324 -0
- package/dist/dashboard/routes/sessions.js +838 -0
- package/dist/dashboard/runtime.js +410 -0
- package/dist/dashboard/server.js +237 -0
- package/dist/dashboard/session-control.js +347 -0
- package/dist/model/catalog.js +104 -0
- package/dist/model/index.js +20 -0
- package/dist/model/injector.js +272 -0
- package/dist/model/provider-models.js +112 -0
- package/dist/model/store.js +212 -0
- package/dist/model/types.js +13 -0
- package/dist/model/validation.js +203 -0
- package/package.json +82 -0
|
@@ -0,0 +1,552 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Discord bot orchestration.
|
|
3
|
+
*
|
|
4
|
+
* Mirrors the Slack/Weixin command surface (/help, /status, /agent, /models, …)
|
|
5
|
+
* over Discord channels. Bot replies as a regular message reply so the thread
|
|
6
|
+
* UI keeps the conversation grouped under the trigger message.
|
|
7
|
+
*/
|
|
8
|
+
import fs from 'node:fs';
|
|
9
|
+
import os from 'node:os';
|
|
10
|
+
import path from 'node:path';
|
|
11
|
+
import { Bot, buildPrompt, fmtUptime, fmtBytes, formatGitStatusLine, normalizeAgent, parseAllowedChatIds, } from '../../bot/bot.js';
|
|
12
|
+
import { BOT_SHUTDOWN_FORCE_EXIT_MS, buildSessionTaskId } from '../../bot/orchestration.js';
|
|
13
|
+
import { shutdownAllDrivers } from '../../agent/driver.js';
|
|
14
|
+
import { expandTilde } from '../../core/platform.js';
|
|
15
|
+
import { registerProcessRuntime, requestProcessRestart, } from '../../core/process-control.js';
|
|
16
|
+
import { getStatusDataAsync, getHostDataSync, getAgentsListData, getSkillsListData, getModelsListData, getSessionsPageData, getStartData, getWorkspacesData, } from '../../bot/commands.js';
|
|
17
|
+
import { DiscordChannel } from './channel.js';
|
|
18
|
+
import { getActiveUserConfig } from '../../core/config/user-config.js';
|
|
19
|
+
const SHUTDOWN_EXIT_CODE = {
|
|
20
|
+
SIGINT: 130,
|
|
21
|
+
SIGTERM: 143,
|
|
22
|
+
};
|
|
23
|
+
function describeError(error) {
|
|
24
|
+
return error instanceof Error ? error.message : String(error ?? 'unknown error');
|
|
25
|
+
}
|
|
26
|
+
export class DiscordBot extends Bot {
|
|
27
|
+
botToken;
|
|
28
|
+
channel;
|
|
29
|
+
nextTaskId = 1;
|
|
30
|
+
shutdownInFlight = false;
|
|
31
|
+
shutdownExitCode = null;
|
|
32
|
+
shutdownForceExitTimer = null;
|
|
33
|
+
signalHandlers = {};
|
|
34
|
+
processRuntimeCleanup = null;
|
|
35
|
+
constructor() {
|
|
36
|
+
super();
|
|
37
|
+
const config = getActiveUserConfig();
|
|
38
|
+
if (process.env.DISCORD_ALLOWED_CHANNEL_IDS) {
|
|
39
|
+
for (const id of parseAllowedChatIds(process.env.DISCORD_ALLOWED_CHANNEL_IDS))
|
|
40
|
+
this.allowedChatIds.add(id);
|
|
41
|
+
}
|
|
42
|
+
this.botToken = String(config.discordBotToken || process.env.DISCORD_BOT_TOKEN || '').trim();
|
|
43
|
+
if (!this.botToken) {
|
|
44
|
+
throw new Error('Missing Discord credentials. Configure discordBotToken.');
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
requestStop() {
|
|
48
|
+
super.requestStop();
|
|
49
|
+
try {
|
|
50
|
+
this.channel?.disconnect();
|
|
51
|
+
}
|
|
52
|
+
catch { }
|
|
53
|
+
}
|
|
54
|
+
onManagedConfigChange(config, opts = {}) {
|
|
55
|
+
const next = String(config.discordBotToken || process.env.DISCORD_BOT_TOKEN || '').trim();
|
|
56
|
+
if (next && next !== this.botToken) {
|
|
57
|
+
this.botToken = next;
|
|
58
|
+
if (!opts.initial)
|
|
59
|
+
this.log('discord botToken reloaded from setting.json');
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
installSignalHandlers() {
|
|
63
|
+
this.removeSignalHandlers();
|
|
64
|
+
const onSigint = () => this.beginShutdown('SIGINT');
|
|
65
|
+
const onSigterm = () => this.beginShutdown('SIGTERM');
|
|
66
|
+
this.signalHandlers = { SIGINT: onSigint, SIGTERM: onSigterm };
|
|
67
|
+
process.once('SIGINT', onSigint);
|
|
68
|
+
process.once('SIGTERM', onSigterm);
|
|
69
|
+
}
|
|
70
|
+
removeSignalHandlers() {
|
|
71
|
+
for (const signal of Object.keys(this.signalHandlers)) {
|
|
72
|
+
const handler = this.signalHandlers[signal];
|
|
73
|
+
if (handler)
|
|
74
|
+
process.off(signal, handler);
|
|
75
|
+
}
|
|
76
|
+
this.signalHandlers = {};
|
|
77
|
+
}
|
|
78
|
+
clearShutdownForceExitTimer() {
|
|
79
|
+
if (!this.shutdownForceExitTimer)
|
|
80
|
+
return;
|
|
81
|
+
clearTimeout(this.shutdownForceExitTimer);
|
|
82
|
+
this.shutdownForceExitTimer = null;
|
|
83
|
+
}
|
|
84
|
+
cleanupRuntimeForExit() {
|
|
85
|
+
try {
|
|
86
|
+
this.channel.disconnect();
|
|
87
|
+
}
|
|
88
|
+
catch { }
|
|
89
|
+
this.stopKeepAlive();
|
|
90
|
+
shutdownAllDrivers();
|
|
91
|
+
}
|
|
92
|
+
beginShutdown(signal) {
|
|
93
|
+
if (this.shutdownInFlight)
|
|
94
|
+
return;
|
|
95
|
+
this.shutdownInFlight = true;
|
|
96
|
+
this.shutdownExitCode = SHUTDOWN_EXIT_CODE[signal];
|
|
97
|
+
this.log(`${signal}, shutting down...`);
|
|
98
|
+
this.cleanupRuntimeForExit();
|
|
99
|
+
this.clearShutdownForceExitTimer();
|
|
100
|
+
this.shutdownForceExitTimer = setTimeout(() => {
|
|
101
|
+
this.log(`shutdown still pending after ${Math.floor(BOT_SHUTDOWN_FORCE_EXIT_MS / 1000)}s, forcing exit`);
|
|
102
|
+
process.exit(this.shutdownExitCode ?? 1);
|
|
103
|
+
}, BOT_SHUTDOWN_FORCE_EXIT_MS);
|
|
104
|
+
this.shutdownForceExitTimer.unref?.();
|
|
105
|
+
}
|
|
106
|
+
resolveSession(chatId, title, files) {
|
|
107
|
+
return this.ensureSessionForChat(chatId, title, files);
|
|
108
|
+
}
|
|
109
|
+
async handleCommand(text, ctx) {
|
|
110
|
+
const [rawCommand, ...rest] = text.trim().slice(1).split(/\s+/);
|
|
111
|
+
const command = rawCommand?.toLowerCase() || '';
|
|
112
|
+
const args = rest.join(' ').trim();
|
|
113
|
+
switch (command) {
|
|
114
|
+
case 'help':
|
|
115
|
+
await ctx.reply([
|
|
116
|
+
'/help - Show commands',
|
|
117
|
+
'/new - New session',
|
|
118
|
+
'/status - Session status',
|
|
119
|
+
'/host - Host system info',
|
|
120
|
+
'/agent [codex|claude|gemini] - Switch agent',
|
|
121
|
+
'/models [name|#] - Switch model',
|
|
122
|
+
'/mode [plan|code] - Toggle plan mode (claude only)',
|
|
123
|
+
'/switch [path] - Change workdir',
|
|
124
|
+
'/workspaces [#] - Pick saved workspace',
|
|
125
|
+
'/sessions [new|#] - List/switch sessions',
|
|
126
|
+
'/skills - List project skills',
|
|
127
|
+
'/stop - Stop current task',
|
|
128
|
+
'/restart - Restart pikiloop',
|
|
129
|
+
].join('\n'));
|
|
130
|
+
return true;
|
|
131
|
+
case 'new':
|
|
132
|
+
this.resetConversationForChat(ctx.chatId);
|
|
133
|
+
await ctx.reply('Started a new session.');
|
|
134
|
+
return true;
|
|
135
|
+
case 'status':
|
|
136
|
+
await this.cmdStatus(ctx);
|
|
137
|
+
return true;
|
|
138
|
+
case 'host':
|
|
139
|
+
await this.cmdHost(ctx);
|
|
140
|
+
return true;
|
|
141
|
+
case 'agent':
|
|
142
|
+
await this.cmdAgent(ctx, args);
|
|
143
|
+
return true;
|
|
144
|
+
case 'models':
|
|
145
|
+
await this.cmdModels(ctx, args);
|
|
146
|
+
return true;
|
|
147
|
+
case 'mode':
|
|
148
|
+
await this.cmdMode(ctx, args);
|
|
149
|
+
return true;
|
|
150
|
+
case 'switch':
|
|
151
|
+
await this.cmdSwitch(ctx, args);
|
|
152
|
+
return true;
|
|
153
|
+
case 'workspaces':
|
|
154
|
+
await this.cmdWorkspaces(ctx, args);
|
|
155
|
+
return true;
|
|
156
|
+
case 'sessions':
|
|
157
|
+
await this.cmdSessions(ctx, args);
|
|
158
|
+
return true;
|
|
159
|
+
case 'skills':
|
|
160
|
+
await this.cmdSkills(ctx);
|
|
161
|
+
return true;
|
|
162
|
+
case 'stop':
|
|
163
|
+
await this.cmdStop(ctx);
|
|
164
|
+
return true;
|
|
165
|
+
case 'restart':
|
|
166
|
+
await this.cmdRestart(ctx);
|
|
167
|
+
return true;
|
|
168
|
+
case 'start':
|
|
169
|
+
await this.cmdStart(ctx);
|
|
170
|
+
return true;
|
|
171
|
+
default: return false;
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
async cmdStart(ctx) {
|
|
175
|
+
const d = getStartData(this, ctx.chatId);
|
|
176
|
+
const lines = [`pikiloop v${d.version}`, `Workdir: ${d.workdir}`, '', `Agent: ${d.agent}`];
|
|
177
|
+
for (const a of d.agentDetails) {
|
|
178
|
+
const parts = [` ${a.agent}: ${a.model}`];
|
|
179
|
+
if (a.effort)
|
|
180
|
+
parts[0] += ` (effort: ${a.effort})`;
|
|
181
|
+
lines.push(parts[0]);
|
|
182
|
+
}
|
|
183
|
+
lines.push('', 'Ready. Send a message to start.');
|
|
184
|
+
await ctx.reply(lines.join('\n'));
|
|
185
|
+
}
|
|
186
|
+
async cmdStatus(ctx) {
|
|
187
|
+
const d = await getStatusDataAsync(this, ctx.chatId);
|
|
188
|
+
const gitLine = formatGitStatusLine(d.git);
|
|
189
|
+
const lines = [
|
|
190
|
+
`pikiloop v${d.version}`,
|
|
191
|
+
`Uptime: ${fmtUptime(d.uptime)}`,
|
|
192
|
+
`PID: ${d.pid} | RSS: ${fmtBytes(d.memRss)} | Heap: ${fmtBytes(d.memHeap)}`,
|
|
193
|
+
`Workdir: ${d.workdir}`,
|
|
194
|
+
...(gitLine ? [`Git: ${gitLine}`] : []),
|
|
195
|
+
'',
|
|
196
|
+
`Agent: ${d.agent}`,
|
|
197
|
+
`Model: ${d.model || '-'}`,
|
|
198
|
+
`Session: ${d.sessionId ? d.sessionId.slice(0, 16) : '(new)'}`,
|
|
199
|
+
`Tasks: ${d.activeTasksCount}`,
|
|
200
|
+
];
|
|
201
|
+
if (d.running)
|
|
202
|
+
lines.push(`Running: ${fmtUptime(Date.now() - d.running.startedAt)}`);
|
|
203
|
+
await ctx.reply(lines.join('\n'));
|
|
204
|
+
}
|
|
205
|
+
async cmdHost(ctx) {
|
|
206
|
+
const d = getHostDataSync(this);
|
|
207
|
+
const lines = [`Host: ${d.hostName}`, `CPU: ${d.cpuModel} x${d.cpuCount}`];
|
|
208
|
+
if (d.cpuUsage) {
|
|
209
|
+
lines.push(`CPU Usage: ${d.cpuUsage.usedPercent.toFixed(1)}% (user ${d.cpuUsage.userPercent.toFixed(1)}%, sys ${d.cpuUsage.sysPercent.toFixed(1)}%)`);
|
|
210
|
+
}
|
|
211
|
+
lines.push(`Memory: ${fmtBytes(d.memoryUsed)} / ${fmtBytes(d.totalMem)} (${d.memoryPercent.toFixed(0)}%)`, `Available: ${fmtBytes(d.memoryAvailable)}`);
|
|
212
|
+
if (d.battery)
|
|
213
|
+
lines.push(`Battery: ${d.battery.percent} (${d.battery.state})`);
|
|
214
|
+
if (d.disk)
|
|
215
|
+
lines.push(`Disk: ${d.disk.used} / ${d.disk.total} (${d.disk.percent})`);
|
|
216
|
+
lines.push(`Process: PID ${d.selfPid} | RSS ${fmtBytes(d.selfRss)} | Heap ${fmtBytes(d.selfHeap)}`);
|
|
217
|
+
if (d.topProcs.length > 1) {
|
|
218
|
+
lines.push('', 'Top Processes:');
|
|
219
|
+
lines.push(...d.topProcs);
|
|
220
|
+
}
|
|
221
|
+
await ctx.reply(lines.join('\n'));
|
|
222
|
+
}
|
|
223
|
+
async cmdAgent(ctx, args) {
|
|
224
|
+
if (!args) {
|
|
225
|
+
const d = getAgentsListData(this, ctx.chatId);
|
|
226
|
+
const current = d.agents.find(a => a.isCurrent);
|
|
227
|
+
const lines = [];
|
|
228
|
+
lines.push(`Current: ${current ? current.label : d.currentAgent}`, '');
|
|
229
|
+
for (const a of d.agents) {
|
|
230
|
+
const tick = a.installed ? '✓' : '✗';
|
|
231
|
+
const head = a.versionShort ? `${tick} ${a.label} · v${a.versionShort}` : `${tick} ${a.label}`;
|
|
232
|
+
lines.push(a.isCurrent ? `${head} ← current` : head);
|
|
233
|
+
if (a.boundProvider && a.boundModel)
|
|
234
|
+
lines.push(` └ ${a.boundProvider} / ${a.boundModel}`);
|
|
235
|
+
}
|
|
236
|
+
const ids = d.agents.filter(a => a.installed).map(a => a.agent).join('|');
|
|
237
|
+
lines.push('', `Switch: /agent ${ids || 'codex|claude|gemini|hermes'}`);
|
|
238
|
+
await ctx.reply(lines.join('\n'));
|
|
239
|
+
return;
|
|
240
|
+
}
|
|
241
|
+
try {
|
|
242
|
+
const agent = normalizeAgent(args);
|
|
243
|
+
this.switchAgentForChat(ctx.chatId, agent);
|
|
244
|
+
await ctx.reply(`Agent switched to ${agent}.`);
|
|
245
|
+
}
|
|
246
|
+
catch {
|
|
247
|
+
await ctx.reply('Unknown agent. Use: /agent codex|claude|gemini');
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
async cmdModels(ctx, args) {
|
|
251
|
+
const d = await getModelsListData(this, ctx.chatId);
|
|
252
|
+
if (args) {
|
|
253
|
+
const idx = parseInt(args, 10);
|
|
254
|
+
let modelId = null;
|
|
255
|
+
if (!isNaN(idx) && idx >= 1 && idx <= d.models.length)
|
|
256
|
+
modelId = d.models[idx - 1].id;
|
|
257
|
+
else {
|
|
258
|
+
const match = d.models.find(m => m.id === args || m.alias === args);
|
|
259
|
+
if (match)
|
|
260
|
+
modelId = match.id;
|
|
261
|
+
}
|
|
262
|
+
if (modelId) {
|
|
263
|
+
this.switchModelForChat(ctx.chatId, modelId);
|
|
264
|
+
await ctx.reply(`Model switched to ${modelId}.`);
|
|
265
|
+
return;
|
|
266
|
+
}
|
|
267
|
+
await ctx.reply(`Unknown model: ${args}`);
|
|
268
|
+
return;
|
|
269
|
+
}
|
|
270
|
+
const lines = [`Current: ${d.currentModel}`, ''];
|
|
271
|
+
d.models.forEach((m, i) => {
|
|
272
|
+
const alias = m.alias ? ` (${m.alias})` : '';
|
|
273
|
+
const mark = m.isCurrent ? ' ←' : '';
|
|
274
|
+
lines.push(`${i + 1}. ${m.id}${alias}${mark}`);
|
|
275
|
+
});
|
|
276
|
+
if (d.effort) {
|
|
277
|
+
lines.push('', `Effort: ${d.effort.current}`);
|
|
278
|
+
for (const lv of d.effort.levels) {
|
|
279
|
+
const mark = lv.isCurrent ? ' ←' : '';
|
|
280
|
+
lines.push(` ${lv.id} - ${lv.label}${mark}`);
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
lines.push('', 'Usage: /models <name|number>');
|
|
284
|
+
await ctx.reply(lines.join('\n'));
|
|
285
|
+
}
|
|
286
|
+
async cmdMode(ctx, args) {
|
|
287
|
+
if (this.chat(ctx.chatId).agent !== 'claude') {
|
|
288
|
+
await ctx.reply('Mode toggle is only available for Claude agent.');
|
|
289
|
+
return;
|
|
290
|
+
}
|
|
291
|
+
const isPlan = this.agentConfigs.claude.permissionMode === 'plan';
|
|
292
|
+
if (args === 'plan') {
|
|
293
|
+
this.switchPermissionModeForChat(ctx.chatId, 'plan');
|
|
294
|
+
await ctx.reply('Mode: Plan (read-only)');
|
|
295
|
+
}
|
|
296
|
+
else if (args === 'code') {
|
|
297
|
+
this.switchPermissionModeForChat(ctx.chatId, 'bypassPermissions');
|
|
298
|
+
await ctx.reply('Mode: Code (full access)');
|
|
299
|
+
}
|
|
300
|
+
else {
|
|
301
|
+
await ctx.reply(`Current: ${isPlan ? 'Plan (read-only)' : 'Code (full access)'}\n\nUsage: /mode plan|code`);
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
async cmdSwitch(ctx, args) {
|
|
305
|
+
const wd = this.chatWorkdir(ctx.chatId);
|
|
306
|
+
if (args) {
|
|
307
|
+
const resolvedPath = path.resolve(expandTilde(args));
|
|
308
|
+
if (!fs.existsSync(resolvedPath) || !fs.statSync(resolvedPath).isDirectory()) {
|
|
309
|
+
await ctx.reply(`Not a valid directory: ${resolvedPath}`);
|
|
310
|
+
return;
|
|
311
|
+
}
|
|
312
|
+
const oldPath = this.switchWorkdir(resolvedPath);
|
|
313
|
+
await ctx.reply(`Workdir switched:\n${oldPath}\n→ ${resolvedPath}`);
|
|
314
|
+
return;
|
|
315
|
+
}
|
|
316
|
+
const savedCount = getWorkspacesData(this, ctx.chatId).workspaces.length;
|
|
317
|
+
const hint = savedCount > 0
|
|
318
|
+
? `\n\nTip: ${savedCount} saved workspace${savedCount === 1 ? '' : 's'} — use /workspaces to pick one.`
|
|
319
|
+
: '';
|
|
320
|
+
await ctx.reply(`Current workdir: ${wd}\n\nUsage: /switch <path>${hint}`);
|
|
321
|
+
}
|
|
322
|
+
async cmdWorkspaces(ctx, args) {
|
|
323
|
+
const data = getWorkspacesData(this, ctx.chatId);
|
|
324
|
+
if (data.workspaces.length === 0) {
|
|
325
|
+
await ctx.reply('No saved workspaces yet. Add them from the dashboard, or use /switch <path>.');
|
|
326
|
+
return;
|
|
327
|
+
}
|
|
328
|
+
const trimmed = args.trim();
|
|
329
|
+
if (trimmed) {
|
|
330
|
+
const idx = parseInt(trimmed, 10);
|
|
331
|
+
if (Number.isNaN(idx) || idx < 1 || idx > data.workspaces.length) {
|
|
332
|
+
await ctx.reply(`Workspace #${trimmed} not found. Use /workspaces to list.`);
|
|
333
|
+
return;
|
|
334
|
+
}
|
|
335
|
+
const ws = data.workspaces[idx - 1];
|
|
336
|
+
if (!ws.exists) {
|
|
337
|
+
await ctx.reply(`Workspace path is missing on disk:\n${ws.path}`);
|
|
338
|
+
return;
|
|
339
|
+
}
|
|
340
|
+
const oldPath = this.switchWorkdir(ws.path);
|
|
341
|
+
await ctx.reply(`Workdir switched:\n${oldPath}\n→ ${ws.path}`);
|
|
342
|
+
return;
|
|
343
|
+
}
|
|
344
|
+
const lines = ['Saved workspaces:', `Current: ${data.currentWorkdir}`, ''];
|
|
345
|
+
data.workspaces.forEach((ws, i) => {
|
|
346
|
+
const marker = ws.isCurrent ? '✓' : ws.exists ? ' ' : '⚠';
|
|
347
|
+
lines.push(`${marker} ${i + 1}. ${ws.name}`);
|
|
348
|
+
lines.push(` ${ws.path}`);
|
|
349
|
+
});
|
|
350
|
+
lines.push('', 'Usage: /workspaces <number> to switch.');
|
|
351
|
+
await ctx.reply(lines.join('\n'));
|
|
352
|
+
}
|
|
353
|
+
async cmdSessions(ctx, args) {
|
|
354
|
+
const arg = args.trim().toLowerCase();
|
|
355
|
+
if (arg === 'new') {
|
|
356
|
+
this.resetConversationForChat(ctx.chatId);
|
|
357
|
+
await ctx.reply('Started a new session.');
|
|
358
|
+
return;
|
|
359
|
+
}
|
|
360
|
+
const idx = parseInt(arg, 10);
|
|
361
|
+
if (!isNaN(idx) && idx >= 1) {
|
|
362
|
+
const d = await getSessionsPageData(this, ctx.chatId, 0, 100);
|
|
363
|
+
const target = d.sessions[idx - 1];
|
|
364
|
+
if (target) {
|
|
365
|
+
const result = await this.fetchSessions(undefined, this.chatWorkdir(ctx.chatId));
|
|
366
|
+
const session = result.sessions.find(s => s.sessionId === target.key);
|
|
367
|
+
if (session) {
|
|
368
|
+
this.resumeSessionForChat(ctx.chatId, session);
|
|
369
|
+
await ctx.reply(`Switched to [${session.agent}] ${target.title}`);
|
|
370
|
+
}
|
|
371
|
+
else {
|
|
372
|
+
await ctx.reply('Session not found.');
|
|
373
|
+
}
|
|
374
|
+
return;
|
|
375
|
+
}
|
|
376
|
+
await ctx.reply(`Session #${idx} not found.`);
|
|
377
|
+
return;
|
|
378
|
+
}
|
|
379
|
+
const d = await getSessionsPageData(this, ctx.chatId, 0, 10);
|
|
380
|
+
if (!d.sessions.length) {
|
|
381
|
+
await ctx.reply('No sessions found.');
|
|
382
|
+
return;
|
|
383
|
+
}
|
|
384
|
+
const lines = [`Sessions (${d.total}):`, ''];
|
|
385
|
+
d.sessions.forEach((s, i) => {
|
|
386
|
+
const mark = s.isCurrent ? ' ←' : '';
|
|
387
|
+
const running = s.isRunning ? ' [running]' : '';
|
|
388
|
+
lines.push(`${i + 1}. [${s.agent}] ${s.title} · ${s.time}${mark}${running}`);
|
|
389
|
+
});
|
|
390
|
+
lines.push('', 'Usage: /sessions new | /sessions <#>');
|
|
391
|
+
await ctx.reply(lines.join('\n'));
|
|
392
|
+
}
|
|
393
|
+
async cmdSkills(ctx) {
|
|
394
|
+
const d = getSkillsListData(this, ctx.chatId);
|
|
395
|
+
if (!d.skills.length) {
|
|
396
|
+
await ctx.reply('No project skills found.');
|
|
397
|
+
return;
|
|
398
|
+
}
|
|
399
|
+
const lines = [`Skills (${d.agent}):`, ''];
|
|
400
|
+
for (const s of d.skills) {
|
|
401
|
+
const desc = s.description ? ` - ${s.description}` : '';
|
|
402
|
+
lines.push(`/${s.command} (${s.label})${desc}`);
|
|
403
|
+
}
|
|
404
|
+
await ctx.reply(lines.join('\n'));
|
|
405
|
+
}
|
|
406
|
+
async cmdStop(ctx) {
|
|
407
|
+
const session = this.selectedSession(ctx.chatId);
|
|
408
|
+
if (!session) {
|
|
409
|
+
await ctx.reply('No active session to stop.');
|
|
410
|
+
return;
|
|
411
|
+
}
|
|
412
|
+
const { interrupted, cancelledQueued } = this.stopTasksForSession(session.key);
|
|
413
|
+
if (!interrupted && cancelledQueued === 0) {
|
|
414
|
+
await ctx.reply('No running or queued work for the current session.');
|
|
415
|
+
return;
|
|
416
|
+
}
|
|
417
|
+
const parts = [];
|
|
418
|
+
if (interrupted)
|
|
419
|
+
parts.push('interrupted current run');
|
|
420
|
+
if (cancelledQueued > 0)
|
|
421
|
+
parts.push(`cancelled ${cancelledQueued} queued task(s)`);
|
|
422
|
+
await ctx.reply(`Stopped: ${parts.join(', ')}.`);
|
|
423
|
+
}
|
|
424
|
+
async cmdRestart(ctx) {
|
|
425
|
+
await ctx.reply('Restarting pikiloop...');
|
|
426
|
+
void requestProcessRestart({ log: msg => this.log(msg) });
|
|
427
|
+
}
|
|
428
|
+
createMcpSendFile(chatId) {
|
|
429
|
+
return async (filePath) => {
|
|
430
|
+
try {
|
|
431
|
+
await this.channel.send(chatId, `Artifact ready: ${path.basename(filePath)}\n${filePath}`);
|
|
432
|
+
return { ok: true };
|
|
433
|
+
}
|
|
434
|
+
catch (error) {
|
|
435
|
+
return { ok: false, error: describeError(error) };
|
|
436
|
+
}
|
|
437
|
+
};
|
|
438
|
+
}
|
|
439
|
+
async sendResult(chatId, result) {
|
|
440
|
+
const text = result.ok
|
|
441
|
+
? (result.message.trim() || 'Task finished.')
|
|
442
|
+
: ['Task failed.', result.error || result.message || 'Unknown error.'].filter(Boolean).join('\n');
|
|
443
|
+
await this.channel.send(chatId, text);
|
|
444
|
+
}
|
|
445
|
+
async handleMessage(msg, ctx) {
|
|
446
|
+
const text = msg.text.trim();
|
|
447
|
+
if (text.startsWith('/') && await this.handleCommand(text, ctx))
|
|
448
|
+
return;
|
|
449
|
+
if (!text && !msg.files.length) {
|
|
450
|
+
await ctx.reply('Send some text — file uploads are not yet supported on the Discord channel.');
|
|
451
|
+
return;
|
|
452
|
+
}
|
|
453
|
+
const session = this.resolveSession(ctx.chatId, text, msg.files);
|
|
454
|
+
const prompt = buildPrompt(text, msg.files);
|
|
455
|
+
const taskId = buildSessionTaskId(session, this.nextTaskId++);
|
|
456
|
+
this.beginTask({
|
|
457
|
+
taskId,
|
|
458
|
+
chatId: ctx.chatId,
|
|
459
|
+
agent: session.agent,
|
|
460
|
+
sessionKey: session.key,
|
|
461
|
+
prompt,
|
|
462
|
+
startedAt: Date.now(),
|
|
463
|
+
sourceMessageId: ctx.messageId,
|
|
464
|
+
});
|
|
465
|
+
this.emitStreamQueued(session.key, taskId);
|
|
466
|
+
void this.queueSessionTask(session, async () => {
|
|
467
|
+
const abortController = new AbortController();
|
|
468
|
+
const task = this.markTaskRunning(taskId, () => abortController.abort());
|
|
469
|
+
if (task?.cancelled) {
|
|
470
|
+
this.emitStreamCancelled(taskId, session.key);
|
|
471
|
+
this.finishTask(taskId);
|
|
472
|
+
return;
|
|
473
|
+
}
|
|
474
|
+
this.emitStreamStart(taskId, session);
|
|
475
|
+
let typingTimer = null;
|
|
476
|
+
try {
|
|
477
|
+
await this.channel.sendTyping(ctx.chatId).catch(() => { });
|
|
478
|
+
typingTimer = setInterval(() => {
|
|
479
|
+
void this.channel.sendTyping(ctx.chatId).catch(() => { });
|
|
480
|
+
}, 7_000);
|
|
481
|
+
typingTimer.unref?.();
|
|
482
|
+
const result = await this.runStream(prompt, session, msg.files, (text, thinking, activity, meta, plan) => {
|
|
483
|
+
this.emitStreamText(taskId, session.key, text, thinking, activity, meta, plan);
|
|
484
|
+
}, undefined, this.createMcpSendFile(ctx.chatId), abortController.signal, this.createInteractionHandler(ctx.chatId, taskId));
|
|
485
|
+
this.emitStreamDone(taskId, session.key, {
|
|
486
|
+
sessionId: result.sessionId || session.sessionId,
|
|
487
|
+
incomplete: !!result.incomplete,
|
|
488
|
+
...(result.ok ? {} : { error: result.error || result.message }),
|
|
489
|
+
});
|
|
490
|
+
await this.sendResult(ctx.chatId, result);
|
|
491
|
+
}
|
|
492
|
+
catch (error) {
|
|
493
|
+
this.emitStreamDone(taskId, session.key, {
|
|
494
|
+
sessionId: session.sessionId,
|
|
495
|
+
incomplete: true,
|
|
496
|
+
error: describeError(error),
|
|
497
|
+
});
|
|
498
|
+
await ctx.reply(`Error: ${describeError(error)}`);
|
|
499
|
+
}
|
|
500
|
+
finally {
|
|
501
|
+
if (typingTimer)
|
|
502
|
+
clearInterval(typingTimer);
|
|
503
|
+
this.finishTask(taskId);
|
|
504
|
+
this.syncSelectedChats(session);
|
|
505
|
+
}
|
|
506
|
+
}, taskId).catch(error => {
|
|
507
|
+
this.finishTask(taskId);
|
|
508
|
+
this.log(`discord queue execution failed: ${describeError(error)}`);
|
|
509
|
+
});
|
|
510
|
+
}
|
|
511
|
+
async run() {
|
|
512
|
+
const tmpDir = path.join(os.tmpdir(), 'pikiloop');
|
|
513
|
+
fs.mkdirSync(tmpDir, { recursive: true });
|
|
514
|
+
this.channel = new DiscordChannel({
|
|
515
|
+
botToken: this.botToken,
|
|
516
|
+
workdir: tmpDir,
|
|
517
|
+
allowedChatIds: this.allowedChatIds.size ? new Set([...this.allowedChatIds].map(value => String(value))) : undefined,
|
|
518
|
+
});
|
|
519
|
+
this.processRuntimeCleanup?.();
|
|
520
|
+
this.processRuntimeCleanup = registerProcessRuntime({
|
|
521
|
+
label: 'discord',
|
|
522
|
+
getActiveTaskCount: () => this.activeTasks.size,
|
|
523
|
+
prepareForRestart: () => this.cleanupRuntimeForExit(),
|
|
524
|
+
});
|
|
525
|
+
this.installSignalHandlers();
|
|
526
|
+
try {
|
|
527
|
+
const bot = await this.channel.connect();
|
|
528
|
+
this.connected = true;
|
|
529
|
+
this.log(`bot: ${bot.displayName} (id=${bot.id})`);
|
|
530
|
+
for (const agent of this.fetchAgents().agents) {
|
|
531
|
+
this.log(`agent ${agent.agent}: ${agent.path || 'NOT FOUND'}`);
|
|
532
|
+
}
|
|
533
|
+
this.log(`config: agent=${this.defaultAgent} workdir=${this.workdir} timeout=${this.runTimeout}s`);
|
|
534
|
+
this.channel.onMessage((msg, ctx) => this.handleMessage(msg, ctx));
|
|
535
|
+
this.channel.onError(error => this.log(`error: ${describeError(error)}`));
|
|
536
|
+
this.startKeepAlive();
|
|
537
|
+
this.log('✓ Discord connected, gateway listening — ready to receive messages');
|
|
538
|
+
await this.channel.listen();
|
|
539
|
+
this.stopKeepAlive();
|
|
540
|
+
this.log('stopped');
|
|
541
|
+
}
|
|
542
|
+
finally {
|
|
543
|
+
this.stopKeepAlive();
|
|
544
|
+
this.clearShutdownForceExitTimer();
|
|
545
|
+
this.removeSignalHandlers();
|
|
546
|
+
this.processRuntimeCleanup?.();
|
|
547
|
+
this.processRuntimeCleanup = null;
|
|
548
|
+
if (this.shutdownInFlight)
|
|
549
|
+
process.exit(this.shutdownExitCode ?? 1);
|
|
550
|
+
}
|
|
551
|
+
}
|
|
552
|
+
}
|