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
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
export function hasConfiguredChannelToken(config, channel, tokenOverride) {
|
|
2
|
+
switch (channel) {
|
|
3
|
+
case 'telegram':
|
|
4
|
+
return !!(config.telegramBotToken || tokenOverride);
|
|
5
|
+
case 'feishu':
|
|
6
|
+
return !!(config.feishuAppId || tokenOverride);
|
|
7
|
+
case 'whatsapp':
|
|
8
|
+
return !!tokenOverride;
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
export function resolveConfiguredChannels(opts) {
|
|
12
|
+
const rawChannels = String(opts.explicitChannels || '').trim();
|
|
13
|
+
if (rawChannels) {
|
|
14
|
+
return rawChannels.split(',').map(channel => channel.trim().toLowerCase()).filter(Boolean);
|
|
15
|
+
}
|
|
16
|
+
if (opts.config.channels?.length)
|
|
17
|
+
return opts.config.channels;
|
|
18
|
+
const detected = [];
|
|
19
|
+
if (hasConfiguredChannelToken(opts.config, 'feishu', opts.tokenOverride))
|
|
20
|
+
detected.push('feishu');
|
|
21
|
+
if (hasConfiguredChannelToken(opts.config, 'telegram', opts.tokenOverride))
|
|
22
|
+
detected.push('telegram');
|
|
23
|
+
return detected;
|
|
24
|
+
}
|
package/dist/cli.js
ADDED
|
@@ -0,0 +1,484 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* cli.ts — CLI entry point for pikiclaw.
|
|
4
|
+
*/
|
|
5
|
+
import { spawn } from 'node:child_process';
|
|
6
|
+
import path from 'node:path';
|
|
7
|
+
import { VERSION, envBool } from './bot.js';
|
|
8
|
+
import { TelegramBot } from './bot-telegram.js';
|
|
9
|
+
import { hasConfiguredChannelToken, resolveConfiguredChannels } from './cli-channels.js';
|
|
10
|
+
import { listAgents } from './code-agent.js';
|
|
11
|
+
import { startDashboard } from './dashboard.js';
|
|
12
|
+
import { buildSetupGuide, collectSetupState, hasReadyAgent, isSetupReady } from './onboarding.js';
|
|
13
|
+
import { buildRestartCommand, clearRestartStateFile, consumeRestartStateFile, createRestartStateFilePath, PROCESS_RESTART_EXIT_CODE, requestProcessRestart, } from './process-control.js';
|
|
14
|
+
import { runSetupWizard } from './setup-wizard.js';
|
|
15
|
+
import { applyUserConfig, loadUserConfig, startUserConfigSync } from './user-config.js';
|
|
16
|
+
/* ── Daemon (watchdog) mode ─────────────────────────────────────────── */
|
|
17
|
+
const DAEMON_RESTART_DELAY_MS = 3_000;
|
|
18
|
+
const DAEMON_MAX_RESTART_DELAY_MS = 60_000;
|
|
19
|
+
const DAEMON_RAPID_CRASH_WINDOW_MS = 10_000;
|
|
20
|
+
function daemonLog(msg) {
|
|
21
|
+
const ts = new Date().toTimeString().slice(0, 8);
|
|
22
|
+
process.stdout.write(`[daemon ${ts}] ${msg}\n`);
|
|
23
|
+
}
|
|
24
|
+
/** Args that are daemon-specific and should not be forwarded to the child. */
|
|
25
|
+
const DAEMON_STRIP_ARGS = new Set(['--daemon', '--no-daemon']);
|
|
26
|
+
/**
|
|
27
|
+
* Runs the bot as a supervised child process. On non-zero exit the child is
|
|
28
|
+
* restarted with exponential back-off. A clean exit (code 0) stops the daemon.
|
|
29
|
+
* Restart requests use a dedicated exit code and are respawned immediately.
|
|
30
|
+
*/
|
|
31
|
+
async function runDaemon(userArgs) {
|
|
32
|
+
// Forward user's CLI args (strip daemon-related flags).
|
|
33
|
+
const forwardedArgs = userArgs.filter(a => !DAEMON_STRIP_ARGS.has(a));
|
|
34
|
+
const restartCmd = process.env.PIKICLAW_RESTART_CMD;
|
|
35
|
+
const restartStateFile = createRestartStateFilePath(process.pid);
|
|
36
|
+
let restartDelay = DAEMON_RESTART_DELAY_MS;
|
|
37
|
+
let attempt = 0;
|
|
38
|
+
let nextRestartEnv = {};
|
|
39
|
+
const spawnChild = (extraEnv = {}) => {
|
|
40
|
+
clearRestartStateFile(restartStateFile);
|
|
41
|
+
const { bin, args } = buildRestartCommand(forwardedArgs, restartCmd);
|
|
42
|
+
daemonLog(`exec: ${bin} ${args.join(' ')}`);
|
|
43
|
+
return spawn(bin, args, {
|
|
44
|
+
stdio: 'inherit',
|
|
45
|
+
env: {
|
|
46
|
+
...process.env,
|
|
47
|
+
...extraEnv,
|
|
48
|
+
PIKICLAW_DAEMON_CHILD: '1',
|
|
49
|
+
PIKICLAW_RESTART_STATE_FILE: restartStateFile,
|
|
50
|
+
npm_config_yes: 'true',
|
|
51
|
+
},
|
|
52
|
+
});
|
|
53
|
+
};
|
|
54
|
+
// eslint-disable-next-line no-constant-condition
|
|
55
|
+
while (true) {
|
|
56
|
+
attempt++;
|
|
57
|
+
daemonLog(`starting child process (attempt #${attempt})`);
|
|
58
|
+
const child = spawnChild(nextRestartEnv);
|
|
59
|
+
nextRestartEnv = {};
|
|
60
|
+
daemonLog(`child running (pid=${child.pid})`);
|
|
61
|
+
const startedAt = Date.now();
|
|
62
|
+
let shutdownSignal = null;
|
|
63
|
+
// Forward termination and restart signals to the active child.
|
|
64
|
+
const forwardShutdownSignal = (sig) => {
|
|
65
|
+
shutdownSignal = sig;
|
|
66
|
+
child.kill(sig);
|
|
67
|
+
};
|
|
68
|
+
const forwardRestartSignal = () => {
|
|
69
|
+
child.kill('SIGUSR2');
|
|
70
|
+
};
|
|
71
|
+
process.on('SIGINT', forwardShutdownSignal);
|
|
72
|
+
process.on('SIGTERM', forwardShutdownSignal);
|
|
73
|
+
process.on('SIGUSR2', forwardRestartSignal);
|
|
74
|
+
const code = await new Promise(resolve => {
|
|
75
|
+
child.on('exit', (c) => resolve(c));
|
|
76
|
+
});
|
|
77
|
+
process.removeListener('SIGINT', forwardShutdownSignal);
|
|
78
|
+
process.removeListener('SIGTERM', forwardShutdownSignal);
|
|
79
|
+
process.removeListener('SIGUSR2', forwardRestartSignal);
|
|
80
|
+
if (shutdownSignal) {
|
|
81
|
+
const exitCode = shutdownSignal === 'SIGINT' ? 130 : 143;
|
|
82
|
+
daemonLog(`received ${shutdownSignal}, daemon stopping`);
|
|
83
|
+
process.exit(exitCode);
|
|
84
|
+
}
|
|
85
|
+
if (code === PROCESS_RESTART_EXIT_CODE) {
|
|
86
|
+
nextRestartEnv = consumeRestartStateFile(restartStateFile);
|
|
87
|
+
restartDelay = DAEMON_RESTART_DELAY_MS;
|
|
88
|
+
daemonLog('child requested restart, respawning immediately');
|
|
89
|
+
continue;
|
|
90
|
+
}
|
|
91
|
+
// Clean exit → stop daemon.
|
|
92
|
+
if (code === 0 || code === null) {
|
|
93
|
+
daemonLog(`child exited cleanly (code=${code}), daemon stopping`);
|
|
94
|
+
process.exit(0);
|
|
95
|
+
}
|
|
96
|
+
// Exponential back-off for rapid crashes.
|
|
97
|
+
const uptime = Date.now() - startedAt;
|
|
98
|
+
if (uptime > DAEMON_RAPID_CRASH_WINDOW_MS) {
|
|
99
|
+
restartDelay = DAEMON_RESTART_DELAY_MS; // reset if it ran for a while
|
|
100
|
+
}
|
|
101
|
+
else {
|
|
102
|
+
restartDelay = Math.min(restartDelay * 2, DAEMON_MAX_RESTART_DELAY_MS);
|
|
103
|
+
}
|
|
104
|
+
daemonLog(`child crashed (code=${code}, uptime=${Math.round(uptime / 1000)}s), restarting in ${Math.round(restartDelay / 1000)}s...`);
|
|
105
|
+
await new Promise(resolve => setTimeout(resolve, restartDelay));
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
function parseArgs(argv) {
|
|
109
|
+
const args = {
|
|
110
|
+
token: null, agent: null, model: null, workdir: null,
|
|
111
|
+
fullAccess: null, safeMode: false, allowedIds: null,
|
|
112
|
+
timeout: null, version: false, help: false, doctor: false, setup: false,
|
|
113
|
+
noDashboard: false, dashboardPort: null, daemon: true,
|
|
114
|
+
};
|
|
115
|
+
const it = argv[Symbol.iterator]();
|
|
116
|
+
for (const arg of it) {
|
|
117
|
+
switch (arg) {
|
|
118
|
+
case '-t':
|
|
119
|
+
case '--token':
|
|
120
|
+
args.token = it.next().value;
|
|
121
|
+
break;
|
|
122
|
+
case '-a':
|
|
123
|
+
case '--agent':
|
|
124
|
+
args.agent = it.next().value;
|
|
125
|
+
break;
|
|
126
|
+
case '-m':
|
|
127
|
+
case '--model':
|
|
128
|
+
args.model = it.next().value;
|
|
129
|
+
break;
|
|
130
|
+
case '-w':
|
|
131
|
+
case '--workdir':
|
|
132
|
+
args.workdir = it.next().value;
|
|
133
|
+
break;
|
|
134
|
+
case '--full-access':
|
|
135
|
+
args.fullAccess = true;
|
|
136
|
+
break;
|
|
137
|
+
case '--safe-mode':
|
|
138
|
+
args.safeMode = true;
|
|
139
|
+
break;
|
|
140
|
+
case '--allowed-ids':
|
|
141
|
+
args.allowedIds = it.next().value;
|
|
142
|
+
break;
|
|
143
|
+
case '--timeout':
|
|
144
|
+
args.timeout = parseInt(it.next().value ?? '', 10);
|
|
145
|
+
break;
|
|
146
|
+
case '--doctor':
|
|
147
|
+
args.doctor = true;
|
|
148
|
+
break;
|
|
149
|
+
case '--setup':
|
|
150
|
+
args.setup = true;
|
|
151
|
+
break;
|
|
152
|
+
case '--no-dashboard':
|
|
153
|
+
args.noDashboard = true;
|
|
154
|
+
break;
|
|
155
|
+
case '--dashboard-port':
|
|
156
|
+
args.dashboardPort = parseInt(it.next().value ?? '', 10);
|
|
157
|
+
break;
|
|
158
|
+
case '--daemon':
|
|
159
|
+
args.daemon = true;
|
|
160
|
+
break;
|
|
161
|
+
case '--no-daemon':
|
|
162
|
+
args.daemon = false;
|
|
163
|
+
break;
|
|
164
|
+
case '-v':
|
|
165
|
+
case '--version':
|
|
166
|
+
args.version = true;
|
|
167
|
+
break;
|
|
168
|
+
case '-h':
|
|
169
|
+
case '--help':
|
|
170
|
+
args.help = true;
|
|
171
|
+
break;
|
|
172
|
+
default:
|
|
173
|
+
if (arg.startsWith('-')) {
|
|
174
|
+
process.stderr.write(`Unknown option: ${arg}\n`);
|
|
175
|
+
process.exit(1);
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
return args;
|
|
180
|
+
}
|
|
181
|
+
export async function main() {
|
|
182
|
+
// ── MCP server mode: launched by agent CLI via --mcp-config ──
|
|
183
|
+
if (process.argv.includes('--mcp-serve')) {
|
|
184
|
+
await import('./mcp-session-server.js');
|
|
185
|
+
return;
|
|
186
|
+
}
|
|
187
|
+
const args = parseArgs(process.argv.slice(2));
|
|
188
|
+
let userConfig = loadUserConfig();
|
|
189
|
+
if (args.version) {
|
|
190
|
+
process.stdout.write(`pikiclaw ${VERSION}\n`);
|
|
191
|
+
process.exit(0);
|
|
192
|
+
}
|
|
193
|
+
// Daemon mode (default): become a watchdog that supervises the real bot process.
|
|
194
|
+
// The child is spawned via `npx pikiclaw@latest` so restarts always pull latest code.
|
|
195
|
+
// Use --no-daemon to disable.
|
|
196
|
+
if (args.daemon && !process.env.PIKICLAW_DAEMON_CHILD) {
|
|
197
|
+
await runDaemon(process.argv.slice(2));
|
|
198
|
+
}
|
|
199
|
+
const processLog = (message) => {
|
|
200
|
+
const ts = new Date().toTimeString().slice(0, 8);
|
|
201
|
+
process.stdout.write(`[pikiclaw ${ts}] ${message}\n`);
|
|
202
|
+
};
|
|
203
|
+
const onSigusr2 = () => {
|
|
204
|
+
processLog('SIGUSR2 received, restarting...');
|
|
205
|
+
void requestProcessRestart({ log: processLog });
|
|
206
|
+
};
|
|
207
|
+
process.on('SIGUSR2', onSigusr2);
|
|
208
|
+
process.once('exit', () => {
|
|
209
|
+
process.off('SIGUSR2', onSigusr2);
|
|
210
|
+
});
|
|
211
|
+
const configOverrides = {};
|
|
212
|
+
if (args.agent)
|
|
213
|
+
configOverrides.defaultAgent = args.agent;
|
|
214
|
+
if (args.workdir)
|
|
215
|
+
process.env.PIKICLAW_WORKDIR = path.resolve(args.workdir);
|
|
216
|
+
// Apply config early so managed env vars are populated from setting.json.
|
|
217
|
+
applyUserConfig({ ...userConfig, ...configOverrides }, undefined, { overwrite: true, clearMissing: true });
|
|
218
|
+
const effectiveConfig = () => ({ ...userConfig, ...configOverrides });
|
|
219
|
+
// Resolve channels from config / auto-detect from tokens
|
|
220
|
+
let channels = resolveConfiguredChannels({
|
|
221
|
+
config: effectiveConfig(),
|
|
222
|
+
tokenOverride: args.token,
|
|
223
|
+
});
|
|
224
|
+
// Primary channel used for setup wizard / doctor checks (feishu preferred)
|
|
225
|
+
let channel = channels[0] || 'feishu';
|
|
226
|
+
const tokenProvided = channels.length > 0 && hasConfiguredChannelToken(effectiveConfig(), channel, args.token);
|
|
227
|
+
if (args.help) {
|
|
228
|
+
process.stdout.write(`pikiclaw v${VERSION} — Run local coding agents through IM.
|
|
229
|
+
|
|
230
|
+
Run a bot that forwards IM messages to a local AI coding agent
|
|
231
|
+
(Claude Code or Codex CLI), streams responses in real-time, and manages
|
|
232
|
+
sessions, models, and workdirs.
|
|
233
|
+
|
|
234
|
+
Channels are auto-detected from configured tokens. If both Feishu and
|
|
235
|
+
Telegram tokens are present, both channels launch simultaneously.
|
|
236
|
+
|
|
237
|
+
Usage:
|
|
238
|
+
npx pikiclaw # auto-detect from config/env
|
|
239
|
+
npx pikiclaw -w ~/project # set working directory
|
|
240
|
+
|
|
241
|
+
Options:
|
|
242
|
+
-t, --token <token> Channel auth token (env: PIKICLAW_TOKEN)
|
|
243
|
+
-a, --agent <agent> AI agent: claude | codex [default: codex]
|
|
244
|
+
-m, --model <model> Default model, switchable in chat via /models
|
|
245
|
+
-w, --workdir <dir> Working directory for the agent [default: current process cwd]
|
|
246
|
+
--full-access Codex full-access + Claude bypassPermissions [default]
|
|
247
|
+
--safe-mode Use safer agent permission modes
|
|
248
|
+
--allowed-ids <id,id> Comma-separated chat/user ID whitelist
|
|
249
|
+
--timeout <seconds> Max seconds per agent request [default: 1800]
|
|
250
|
+
--doctor Run setup checks and exit
|
|
251
|
+
--setup Run the interactive setup wizard
|
|
252
|
+
--no-daemon Disable watchdog (auto-restart on crash is ON by default)
|
|
253
|
+
--no-dashboard Skip the web dashboard
|
|
254
|
+
--dashboard-port <port> Dashboard port [default: 3939]
|
|
255
|
+
-v, --version Print version
|
|
256
|
+
-h, --help Print this help
|
|
257
|
+
|
|
258
|
+
Environment variables (general):
|
|
259
|
+
PIKICLAW_TOKEN Channel auth token (same as -t, channel-agnostic)
|
|
260
|
+
DEFAULT_AGENT Default agent (same as -a)
|
|
261
|
+
PIKICLAW_WORKDIR Working directory (same as -w)
|
|
262
|
+
PIKICLAW_TIMEOUT Timeout in seconds (same as --timeout)
|
|
263
|
+
PIKICLAW_ALLOWED_IDS Comma-separated chat/user ID whitelist
|
|
264
|
+
PIKICLAW_FULL_ACCESS Default full-access behavior (true/false)
|
|
265
|
+
|
|
266
|
+
Environment variables (Telegram):
|
|
267
|
+
TELEGRAM_BOT_TOKEN Telegram bot token (from @BotFather)
|
|
268
|
+
TELEGRAM_ALLOWED_CHAT_IDS Comma-separated allowed Telegram chat IDs
|
|
269
|
+
|
|
270
|
+
Environment variables (per agent):
|
|
271
|
+
CLAUDE_MODEL Claude model name
|
|
272
|
+
CLAUDE_PERMISSION_MODE Permission mode (default: bypassPermissions)
|
|
273
|
+
CLAUDE_EXTRA_ARGS Extra CLI args for claude
|
|
274
|
+
CODEX_MODEL Codex model name
|
|
275
|
+
CODEX_REASONING_EFFORT Reasoning effort (default: xhigh)
|
|
276
|
+
CODEX_FULL_ACCESS Full-access mode (default: true)
|
|
277
|
+
CODEX_EXTRA_ARGS Extra CLI args for codex
|
|
278
|
+
|
|
279
|
+
Bot commands (available once running):
|
|
280
|
+
/sessions List or switch coding sessions
|
|
281
|
+
/agents List or switch AI agents
|
|
282
|
+
/models List or switch models
|
|
283
|
+
/status Bot status, uptime, and token usage
|
|
284
|
+
/host Host machine info (CPU, memory, disk, battery)
|
|
285
|
+
/switch Browse and change working directory
|
|
286
|
+
/restart Restart with latest version
|
|
287
|
+
|
|
288
|
+
Environment variables (Feishu):
|
|
289
|
+
FEISHU_APP_ID Feishu app ID (from Feishu Open Platform)
|
|
290
|
+
FEISHU_APP_SECRET Feishu app secret
|
|
291
|
+
FEISHU_DOMAIN API domain (default: https://open.feishu.cn)
|
|
292
|
+
FEISHU_ALLOWED_CHAT_IDS Comma-separated allowed Feishu chat IDs
|
|
293
|
+
|
|
294
|
+
Notes:
|
|
295
|
+
- whatsapp is planned but not implemented yet.
|
|
296
|
+
- --safe-mode delegates to the agent's own permission model; it does not add
|
|
297
|
+
a pikiclaw-specific approval workflow.
|
|
298
|
+
|
|
299
|
+
Prerequisites: Node.js >= 18, and at least one agent CLI installed (claude or codex).
|
|
300
|
+
Docs: https://github.com/xiaotonng/pikiclaw
|
|
301
|
+
`);
|
|
302
|
+
process.exit(0);
|
|
303
|
+
}
|
|
304
|
+
const listStartupAgents = () => listAgents().agents;
|
|
305
|
+
const listVerboseAgents = () => listAgents({ includeVersion: true }).agents;
|
|
306
|
+
const setupState = collectSetupState({
|
|
307
|
+
agents: args.doctor ? listVerboseAgents() : listStartupAgents(),
|
|
308
|
+
channel,
|
|
309
|
+
tokenProvided,
|
|
310
|
+
});
|
|
311
|
+
const canPromptInteractively = !!(process.stdin.isTTY && process.stdout.isTTY);
|
|
312
|
+
// ── Doctor mode: quick check and exit ──
|
|
313
|
+
if (args.doctor) {
|
|
314
|
+
const guide = buildSetupGuide(setupState, VERSION, { doctor: true });
|
|
315
|
+
const ready = isSetupReady(setupState);
|
|
316
|
+
if (ready)
|
|
317
|
+
process.stdout.write(`${guide}\nSetup looks ready.\n`);
|
|
318
|
+
else
|
|
319
|
+
process.stderr.write(guide);
|
|
320
|
+
process.exit(ready ? 0 : 1);
|
|
321
|
+
}
|
|
322
|
+
// ── Dashboard mode (default) ──
|
|
323
|
+
// If config is incomplete or first-time: open dashboard for configuration.
|
|
324
|
+
// If config is ready: open dashboard + start bot channels.
|
|
325
|
+
const useDashboard = !args.noDashboard && !args.setup;
|
|
326
|
+
let dashboard = null;
|
|
327
|
+
const noChannelsDetected = channels.length === 0;
|
|
328
|
+
const needsSetup = noChannelsDetected || !tokenProvided || !hasReadyAgent(setupState);
|
|
329
|
+
if (useDashboard) {
|
|
330
|
+
// Start dashboard — always. If config is incomplete, it serves as the setup UI.
|
|
331
|
+
dashboard = await startDashboard({
|
|
332
|
+
port: args.dashboardPort || 3939,
|
|
333
|
+
open: true,
|
|
334
|
+
});
|
|
335
|
+
if (needsSetup) {
|
|
336
|
+
// Dashboard is showing the config page. Wait until configuration becomes ready,
|
|
337
|
+
// then continue startup without requiring a manual restart.
|
|
338
|
+
const ts = new Date().toTimeString().slice(0, 8);
|
|
339
|
+
process.stdout.write(`[pikiclaw ${ts}] waiting for configuration via dashboard...\n`);
|
|
340
|
+
process.stdout.write(`[pikiclaw ${ts}] configure at ${dashboard.url}; startup will continue automatically once ready.\n`);
|
|
341
|
+
while (true) {
|
|
342
|
+
await new Promise(resolve => setTimeout(resolve, 1_000));
|
|
343
|
+
userConfig = loadUserConfig();
|
|
344
|
+
channels = resolveConfiguredChannels({
|
|
345
|
+
config: { ...userConfig, ...configOverrides },
|
|
346
|
+
tokenOverride: args.token,
|
|
347
|
+
});
|
|
348
|
+
channel = channels[0] || 'feishu';
|
|
349
|
+
const nextSetupState = collectSetupState({
|
|
350
|
+
agents: listStartupAgents(),
|
|
351
|
+
channel,
|
|
352
|
+
tokenProvided: channels.length > 0 && hasConfiguredChannelToken({ ...userConfig, ...configOverrides }, channel, args.token),
|
|
353
|
+
});
|
|
354
|
+
const nextNeedsSetup = channels.length === 0
|
|
355
|
+
|| !hasReadyAgent(nextSetupState);
|
|
356
|
+
if (!nextNeedsSetup)
|
|
357
|
+
break;
|
|
358
|
+
}
|
|
359
|
+
const resumeTs = new Date().toTimeString().slice(0, 8);
|
|
360
|
+
process.stdout.write(`[pikiclaw ${resumeTs}] configuration detected, starting bot channels...\n`);
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
else if (args.setup) {
|
|
364
|
+
// Explicit --setup: use the terminal-based wizard
|
|
365
|
+
if (!canPromptInteractively) {
|
|
366
|
+
process.stderr.write('--setup requires an interactive terminal.\n');
|
|
367
|
+
process.exit(1);
|
|
368
|
+
}
|
|
369
|
+
const wizard = await runSetupWizard({
|
|
370
|
+
version: VERSION,
|
|
371
|
+
channel,
|
|
372
|
+
argsAgent: args.agent || userConfig.defaultAgent || null,
|
|
373
|
+
currentToken: args.token || userConfig.telegramBotToken || null,
|
|
374
|
+
initialState: setupState,
|
|
375
|
+
listAgents: listVerboseAgents,
|
|
376
|
+
});
|
|
377
|
+
if (!wizard.completed)
|
|
378
|
+
process.exit(1);
|
|
379
|
+
userConfig = loadUserConfig();
|
|
380
|
+
}
|
|
381
|
+
else if (needsSetup) {
|
|
382
|
+
// --no-dashboard and needs setup: show guide and exit
|
|
383
|
+
process.stdout.write(buildSetupGuide(setupState, VERSION));
|
|
384
|
+
process.exit(0);
|
|
385
|
+
}
|
|
386
|
+
// Re-resolve channels after wizard/dashboard may have changed configuration.
|
|
387
|
+
channels = resolveConfiguredChannels({
|
|
388
|
+
config: effectiveConfig(),
|
|
389
|
+
tokenOverride: args.token,
|
|
390
|
+
});
|
|
391
|
+
channel = channels[0] || 'feishu';
|
|
392
|
+
const refreshedTokenProvided = channels.length > 0;
|
|
393
|
+
if (!refreshedTokenProvided) {
|
|
394
|
+
const refreshedSetupState = collectSetupState({
|
|
395
|
+
agents: listStartupAgents(),
|
|
396
|
+
channel,
|
|
397
|
+
tokenProvided: false,
|
|
398
|
+
});
|
|
399
|
+
process.stdout.write(buildSetupGuide(refreshedSetupState, VERSION));
|
|
400
|
+
process.exit(0);
|
|
401
|
+
}
|
|
402
|
+
const refreshedSetupState = collectSetupState({
|
|
403
|
+
agents: listStartupAgents(),
|
|
404
|
+
channel,
|
|
405
|
+
tokenProvided: refreshedTokenProvided,
|
|
406
|
+
});
|
|
407
|
+
if (!hasReadyAgent(refreshedSetupState)) {
|
|
408
|
+
process.stderr.write(buildSetupGuide(refreshedSetupState, VERSION, { doctor: true }));
|
|
409
|
+
process.exit(1);
|
|
410
|
+
}
|
|
411
|
+
const runtimeConfig = { ...userConfig, ...configOverrides };
|
|
412
|
+
if (args.token) {
|
|
413
|
+
if (channel === 'telegram')
|
|
414
|
+
runtimeConfig.telegramBotToken = args.token;
|
|
415
|
+
else if (channel === 'feishu') {
|
|
416
|
+
const [appId, ...rest] = args.token.split(':');
|
|
417
|
+
runtimeConfig.feishuAppId = appId;
|
|
418
|
+
runtimeConfig.feishuAppSecret = rest.join(':');
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
if (args.allowedIds && channel === 'telegram')
|
|
422
|
+
runtimeConfig.telegramAllowedChatIds = args.allowedIds;
|
|
423
|
+
applyUserConfig(runtimeConfig, undefined, { overwrite: true, clearMissing: true });
|
|
424
|
+
if (args.model) {
|
|
425
|
+
const ag = args.agent || runtimeConfig.defaultAgent || 'codex';
|
|
426
|
+
if (ag === 'codex')
|
|
427
|
+
process.env.CODEX_MODEL = args.model;
|
|
428
|
+
else if (ag === 'gemini')
|
|
429
|
+
process.env.GEMINI_MODEL = args.model;
|
|
430
|
+
else
|
|
431
|
+
process.env.CLAUDE_MODEL = args.model;
|
|
432
|
+
}
|
|
433
|
+
if (args.timeout != null)
|
|
434
|
+
process.env.PIKICLAW_TIMEOUT = String(args.timeout);
|
|
435
|
+
if (args.safeMode) {
|
|
436
|
+
process.env.CODEX_FULL_ACCESS = 'false';
|
|
437
|
+
process.env.CLAUDE_PERMISSION_MODE = 'default';
|
|
438
|
+
}
|
|
439
|
+
else if (args.fullAccess || envBool('PIKICLAW_FULL_ACCESS', true)) {
|
|
440
|
+
process.env.CODEX_FULL_ACCESS = 'true';
|
|
441
|
+
process.env.CLAUDE_PERMISSION_MODE = 'bypassPermissions';
|
|
442
|
+
}
|
|
443
|
+
const stopUserConfigSync = startUserConfigSync({
|
|
444
|
+
overrides: runtimeConfig,
|
|
445
|
+
log: message => {
|
|
446
|
+
const ts = new Date().toTimeString().slice(0, 8);
|
|
447
|
+
process.stdout.write(`[pikiclaw ${ts}] ${message}\n`);
|
|
448
|
+
},
|
|
449
|
+
});
|
|
450
|
+
process.once('exit', stopUserConfigSync);
|
|
451
|
+
// dispatch to channel-specific bot(s) — launch all channels concurrently
|
|
452
|
+
async function launchChannel(ch) {
|
|
453
|
+
switch (ch) {
|
|
454
|
+
case 'telegram': {
|
|
455
|
+
const bot = new TelegramBot();
|
|
456
|
+
// Attach bot to dashboard for runtime monitoring
|
|
457
|
+
if (dashboard)
|
|
458
|
+
dashboard.attachBot(bot);
|
|
459
|
+
await bot.run();
|
|
460
|
+
break;
|
|
461
|
+
}
|
|
462
|
+
case 'feishu': {
|
|
463
|
+
const { FeishuBot } = await import('./bot-feishu.js');
|
|
464
|
+
const bot = new FeishuBot();
|
|
465
|
+
if (dashboard)
|
|
466
|
+
dashboard.attachBot(bot);
|
|
467
|
+
await bot.run();
|
|
468
|
+
break;
|
|
469
|
+
}
|
|
470
|
+
case 'whatsapp':
|
|
471
|
+
process.stderr.write('WhatsApp channel is not yet implemented. Coming soon.\n');
|
|
472
|
+
break;
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
if (channels.length === 1) {
|
|
476
|
+
await launchChannel(channels[0]);
|
|
477
|
+
}
|
|
478
|
+
else {
|
|
479
|
+
const ts = new Date().toTimeString().slice(0, 8);
|
|
480
|
+
process.stdout.write(`[pikiclaw ${ts}] launching channels: ${channels.join(', ')}\n`);
|
|
481
|
+
await Promise.all(channels.map(ch => launchChannel(ch)));
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
main().catch(err => { console.error(err); process.exit(1); });
|