agent-shell-chat 1.2.2
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 +199 -0
- package/dist/bin/agent-shell.d.ts +15 -0
- package/dist/bin/agent-shell.d.ts.map +1 -0
- package/dist/bin/agent-shell.js +816 -0
- package/dist/bin/agent-shell.js.map +1 -0
- package/dist/package.json +54 -0
- package/dist/src/acp/agent-manager.d.ts +22 -0
- package/dist/src/acp/agent-manager.d.ts.map +1 -0
- package/dist/src/acp/agent-manager.js +79 -0
- package/dist/src/acp/agent-manager.js.map +1 -0
- package/dist/src/acp/client.d.ts +64 -0
- package/dist/src/acp/client.d.ts.map +1 -0
- package/dist/src/acp/client.js +265 -0
- package/dist/src/acp/client.js.map +1 -0
- package/dist/src/acp/session.d.ts +81 -0
- package/dist/src/acp/session.d.ts.map +1 -0
- package/dist/src/acp/session.js +339 -0
- package/dist/src/acp/session.js.map +1 -0
- package/dist/src/adapter/inbound.d.ts +39 -0
- package/dist/src/adapter/inbound.d.ts.map +1 -0
- package/dist/src/adapter/inbound.js +264 -0
- package/dist/src/adapter/inbound.js.map +1 -0
- package/dist/src/bridge.d.ts +115 -0
- package/dist/src/bridge.d.ts.map +1 -0
- package/dist/src/bridge.js +969 -0
- package/dist/src/bridge.js.map +1 -0
- package/dist/src/config.d.ts +155 -0
- package/dist/src/config.d.ts.map +1 -0
- package/dist/src/config.js +265 -0
- package/dist/src/config.js.map +1 -0
- package/dist/src/index.d.ts +9 -0
- package/dist/src/index.d.ts.map +1 -0
- package/dist/src/index.js +7 -0
- package/dist/src/index.js.map +1 -0
- package/dist/src/inject/monitor.d.ts +24 -0
- package/dist/src/inject/monitor.d.ts.map +1 -0
- package/dist/src/inject/monitor.js +149 -0
- package/dist/src/inject/monitor.js.map +1 -0
- package/dist/src/inject/queue.d.ts +13 -0
- package/dist/src/inject/queue.d.ts.map +1 -0
- package/dist/src/inject/queue.js +35 -0
- package/dist/src/inject/queue.js.map +1 -0
- package/dist/src/inject/types.d.ts +10 -0
- package/dist/src/inject/types.d.ts.map +1 -0
- package/dist/src/inject/types.js +2 -0
- package/dist/src/inject/types.js.map +1 -0
- package/dist/src/storage/accounts.d.ts +43 -0
- package/dist/src/storage/accounts.d.ts.map +1 -0
- package/dist/src/storage/accounts.js +289 -0
- package/dist/src/storage/accounts.js.map +1 -0
- package/dist/src/storage/runtime.d.ts +23 -0
- package/dist/src/storage/runtime.d.ts.map +1 -0
- package/dist/src/storage/runtime.js +104 -0
- package/dist/src/storage/runtime.js.map +1 -0
- package/dist/src/storage/state.d.ts +17 -0
- package/dist/src/storage/state.d.ts.map +1 -0
- package/dist/src/storage/state.js +78 -0
- package/dist/src/storage/state.js.map +1 -0
- package/dist/src/telemetry/index.d.ts +33 -0
- package/dist/src/telemetry/index.d.ts.map +1 -0
- package/dist/src/telemetry/index.js +167 -0
- package/dist/src/telemetry/index.js.map +1 -0
- package/dist/src/weixin/api.d.ts +50 -0
- package/dist/src/weixin/api.d.ts.map +1 -0
- package/dist/src/weixin/api.js +90 -0
- package/dist/src/weixin/api.js.map +1 -0
- package/dist/src/weixin/auth.d.ts +26 -0
- package/dist/src/weixin/auth.d.ts.map +1 -0
- package/dist/src/weixin/auth.js +103 -0
- package/dist/src/weixin/auth.js.map +1 -0
- package/dist/src/weixin/media.d.ts +24 -0
- package/dist/src/weixin/media.d.ts.map +1 -0
- package/dist/src/weixin/media.js +64 -0
- package/dist/src/weixin/media.js.map +1 -0
- package/dist/src/weixin/monitor.d.ts +16 -0
- package/dist/src/weixin/monitor.d.ts.map +1 -0
- package/dist/src/weixin/monitor.js +113 -0
- package/dist/src/weixin/monitor.js.map +1 -0
- package/dist/src/weixin/send.d.ts +28 -0
- package/dist/src/weixin/send.d.ts.map +1 -0
- package/dist/src/weixin/send.js +162 -0
- package/dist/src/weixin/send.js.map +1 -0
- package/dist/src/weixin/types.d.ts +149 -0
- package/dist/src/weixin/types.d.ts.map +1 -0
- package/dist/src/weixin/types.js +33 -0
- package/dist/src/weixin/types.js.map +1 -0
- package/package.json +54 -0
|
@@ -0,0 +1,816 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* agent-shell CLI entry point.
|
|
4
|
+
*
|
|
5
|
+
* Usage:
|
|
6
|
+
* agent-shell --agent "claude code"
|
|
7
|
+
* agent-shell --agent "gemini" --cwd /path/to/project
|
|
8
|
+
* agent-shell --agent "npx tsx ./agent.ts" --login
|
|
9
|
+
* agent-shell --agent "claude code" --daemon
|
|
10
|
+
* agent-shell stop
|
|
11
|
+
* agent-shell status
|
|
12
|
+
* agent-shell inject --text "今日 AI 资讯"
|
|
13
|
+
*/
|
|
14
|
+
import fs from "node:fs";
|
|
15
|
+
import path from "node:path";
|
|
16
|
+
import { spawn } from "node:child_process";
|
|
17
|
+
import { createInterface } from "node:readline/promises";
|
|
18
|
+
import qrcodeTerminal from "qrcode-terminal";
|
|
19
|
+
import { AgentShellBridge } from "../src/bridge.js";
|
|
20
|
+
import { defaultConfig, defaultStorageDir, listBuiltInAgents, resolveAgentSelection, validateCommandAliases, validateInstanceName, } from "../src/config.js";
|
|
21
|
+
import { queueInjectedMessage } from "../src/inject/queue.js";
|
|
22
|
+
import { DEFAULT_INJECTION_TARGET } from "../src/inject/types.js";
|
|
23
|
+
import { discoverAccounts, formatAccountName, registerAccount, resolveAccount, validateAccountName, } from "../src/storage/accounts.js";
|
|
24
|
+
import { claimRuntime, inspectRuntime, releaseRuntime, } from "../src/storage/runtime.js";
|
|
25
|
+
import { loadToken, login } from "../src/weixin/auth.js";
|
|
26
|
+
import { initTelemetry, trackEvent, trackException, shutdownTelemetry, } from "../src/telemetry/index.js";
|
|
27
|
+
import packageJson from "../package.json" with { type: "json" };
|
|
28
|
+
function usage() {
|
|
29
|
+
const presets = listBuiltInAgents()
|
|
30
|
+
.map(({ id }) => id)
|
|
31
|
+
.join(", ");
|
|
32
|
+
console.log(`
|
|
33
|
+
agent-shell v${packageJson.version} — Bridge WeChat to any ACP-compatible AI agent
|
|
34
|
+
|
|
35
|
+
Usage:
|
|
36
|
+
agent-shell --agent <preset|command> [options]
|
|
37
|
+
agent-shell agents List built-in agent presets
|
|
38
|
+
agent-shell accounts List saved WeChat accounts
|
|
39
|
+
agent-shell login Log in and save a WeChat account
|
|
40
|
+
agent-shell inject --text <text> Inject a local message into the daemon
|
|
41
|
+
agent-shell stop Select and stop a running account
|
|
42
|
+
agent-shell status List account process status
|
|
43
|
+
|
|
44
|
+
Options:
|
|
45
|
+
--agent <value> Built-in preset name or raw agent command
|
|
46
|
+
Presets: ${presets}
|
|
47
|
+
Examples: "copilot", "claude", "npx tsx ./agent.ts"
|
|
48
|
+
--cwd <dir> Working directory for agent (default: current dir)
|
|
49
|
+
--login Force re-login (new QR code)
|
|
50
|
+
--daemon Run in background after login
|
|
51
|
+
--account <value> Saved account name, accountId, or name-accountId
|
|
52
|
+
--account-name <n> Name for a newly added account (required without a TTY)
|
|
53
|
+
--config <file> Config file path (JSON)
|
|
54
|
+
--instance <name> Run as a named, isolated instance.
|
|
55
|
+
Storage, token, daemon pid/log, and telemetry id are
|
|
56
|
+
scoped to ~/.agent-shell/instances/<name>/.
|
|
57
|
+
Lets you run multiple bridges side by side, each with
|
|
58
|
+
its own WeChat account and project cwd.
|
|
59
|
+
--inbox-dir <path> Directory to save binary files received from WeChat
|
|
60
|
+
(default: <storage.dir>/inbox). The agent sees the
|
|
61
|
+
saved absolute path in the prompt so it can read the
|
|
62
|
+
file directly.
|
|
63
|
+
--no-inbox Disable saving received files. The agent will only
|
|
64
|
+
see a "[Received file: name, N bytes]" notice and
|
|
65
|
+
will not be able to read the file content.
|
|
66
|
+
--idle-timeout <m> Session idle timeout in minutes (default: 1440)
|
|
67
|
+
Use 0 to disable idle cleanup
|
|
68
|
+
--max-sessions <n> Max concurrent user sessions (default: 10)
|
|
69
|
+
--hide-thoughts Do not forward agent thinking to WeChat (default: forwarded)
|
|
70
|
+
--show-diffs Forward ACP file diffs to WeChat (default: hidden)
|
|
71
|
+
--auto-send-media <mode>
|
|
72
|
+
Auto-send media mode: off, tagged, all (default: tagged)
|
|
73
|
+
all - send all file references in replies
|
|
74
|
+
tagged - only send @send: tagged files
|
|
75
|
+
off - never auto-send media
|
|
76
|
+
--system-prompt <text>
|
|
77
|
+
System prompt injected at the start of each new session
|
|
78
|
+
(e.g. language preference, communication conventions)
|
|
79
|
+
--text <text> Message text for "inject"
|
|
80
|
+
--file <path> Read injected message text from a file
|
|
81
|
+
--to <target> Injection target (default: ${DEFAULT_INJECTION_TARGET})
|
|
82
|
+
--context-token <t> Override stored context token for "inject"
|
|
83
|
+
-v, --verbose Verbose logging
|
|
84
|
+
-V, --version Print version and exit
|
|
85
|
+
-h, --help Show this help
|
|
86
|
+
`);
|
|
87
|
+
}
|
|
88
|
+
async function handleInject(config, args) {
|
|
89
|
+
if (!config.storage.injectDir) {
|
|
90
|
+
throw new Error("storage.injectDir is not configured");
|
|
91
|
+
}
|
|
92
|
+
if (!args.injectText && !args.injectFile) {
|
|
93
|
+
throw new Error('inject requires --text <text> or --file <path>');
|
|
94
|
+
}
|
|
95
|
+
if (args.injectText && args.injectFile) {
|
|
96
|
+
throw new Error("inject accepts only one of --text or --file");
|
|
97
|
+
}
|
|
98
|
+
const text = args.injectFile
|
|
99
|
+
? fs.readFileSync(path.resolve(args.injectFile), "utf-8")
|
|
100
|
+
: args.injectText;
|
|
101
|
+
const { job, filePath } = await queueInjectedMessage({
|
|
102
|
+
injectDir: config.storage.injectDir,
|
|
103
|
+
text,
|
|
104
|
+
target: args.injectTo,
|
|
105
|
+
contextToken: args.injectContextToken,
|
|
106
|
+
});
|
|
107
|
+
console.log(`Queued injection ${job.id}`);
|
|
108
|
+
console.log(`Target: ${job.target}`);
|
|
109
|
+
console.log(`File: ${filePath}`);
|
|
110
|
+
console.log("It will be processed by any running agent-shell instance using the same storage directory.");
|
|
111
|
+
}
|
|
112
|
+
function parseArgs(argv) {
|
|
113
|
+
const result = {
|
|
114
|
+
forceLogin: false,
|
|
115
|
+
daemon: false,
|
|
116
|
+
disableInbox: false,
|
|
117
|
+
hideThoughts: true,
|
|
118
|
+
showDiffs: false,
|
|
119
|
+
verbose: false,
|
|
120
|
+
version: false,
|
|
121
|
+
help: false,
|
|
122
|
+
};
|
|
123
|
+
const args = argv.slice(2);
|
|
124
|
+
let i = 0;
|
|
125
|
+
// Check for subcommand
|
|
126
|
+
if (args[0] && !args[0].startsWith("-")) {
|
|
127
|
+
result.command = args[0];
|
|
128
|
+
i = 1;
|
|
129
|
+
}
|
|
130
|
+
while (i < args.length) {
|
|
131
|
+
const arg = args[i];
|
|
132
|
+
switch (arg) {
|
|
133
|
+
case "--agent":
|
|
134
|
+
result.agent = args[++i];
|
|
135
|
+
break;
|
|
136
|
+
case "--cwd":
|
|
137
|
+
result.cwd = args[++i];
|
|
138
|
+
break;
|
|
139
|
+
case "--login":
|
|
140
|
+
result.forceLogin = true;
|
|
141
|
+
break;
|
|
142
|
+
case "--daemon":
|
|
143
|
+
result.daemon = true;
|
|
144
|
+
break;
|
|
145
|
+
case "--config":
|
|
146
|
+
result.configFile = args[++i];
|
|
147
|
+
break;
|
|
148
|
+
case "--account":
|
|
149
|
+
result.account = args[++i];
|
|
150
|
+
break;
|
|
151
|
+
case "--account-name":
|
|
152
|
+
result.accountName = args[++i];
|
|
153
|
+
break;
|
|
154
|
+
case "--instance":
|
|
155
|
+
result.instance = args[++i];
|
|
156
|
+
break;
|
|
157
|
+
case "--inbox-dir":
|
|
158
|
+
result.inboxDir = args[++i];
|
|
159
|
+
break;
|
|
160
|
+
case "--no-inbox":
|
|
161
|
+
result.disableInbox = true;
|
|
162
|
+
break;
|
|
163
|
+
case "--idle-timeout":
|
|
164
|
+
result.idleTimeout = parseInt(args[++i], 10);
|
|
165
|
+
break;
|
|
166
|
+
case "--max-sessions":
|
|
167
|
+
result.maxSessions = parseInt(args[++i], 10);
|
|
168
|
+
break;
|
|
169
|
+
case "--text":
|
|
170
|
+
result.injectText = args[++i];
|
|
171
|
+
break;
|
|
172
|
+
case "--file":
|
|
173
|
+
result.injectFile = args[++i];
|
|
174
|
+
break;
|
|
175
|
+
case "--to":
|
|
176
|
+
result.injectTo = args[++i];
|
|
177
|
+
break;
|
|
178
|
+
case "--context-token":
|
|
179
|
+
result.injectContextToken = args[++i];
|
|
180
|
+
break;
|
|
181
|
+
case "--hide-thoughts":
|
|
182
|
+
result.hideThoughts = true;
|
|
183
|
+
break;
|
|
184
|
+
case "--show-diffs":
|
|
185
|
+
result.showDiffs = true;
|
|
186
|
+
break;
|
|
187
|
+
case "--auto-send-media":
|
|
188
|
+
result.autoSendMedia = args[++i];
|
|
189
|
+
break;
|
|
190
|
+
case "--system-prompt":
|
|
191
|
+
result.systemPrompt = args[++i];
|
|
192
|
+
break;
|
|
193
|
+
case "-v":
|
|
194
|
+
case "--verbose":
|
|
195
|
+
result.verbose = true;
|
|
196
|
+
break;
|
|
197
|
+
case "-V":
|
|
198
|
+
case "--version":
|
|
199
|
+
result.version = true;
|
|
200
|
+
break;
|
|
201
|
+
case "-h":
|
|
202
|
+
case "--help":
|
|
203
|
+
result.help = true;
|
|
204
|
+
break;
|
|
205
|
+
default:
|
|
206
|
+
if (arg?.startsWith("-")) {
|
|
207
|
+
console.error(`Unknown option: ${arg}`);
|
|
208
|
+
process.exit(1);
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
i++;
|
|
212
|
+
}
|
|
213
|
+
return result;
|
|
214
|
+
}
|
|
215
|
+
function loadConfigFile(filePath) {
|
|
216
|
+
const content = fs.readFileSync(filePath, "utf-8");
|
|
217
|
+
return JSON.parse(content);
|
|
218
|
+
}
|
|
219
|
+
function handleAgents(config) {
|
|
220
|
+
console.log("Built-in ACP agent presets:\n");
|
|
221
|
+
for (const { id, preset } of listBuiltInAgents(config.agents)) {
|
|
222
|
+
const commandLine = [preset.command, ...preset.args].join(" ");
|
|
223
|
+
console.log(`${id.padEnd(10)} ${commandLine}`);
|
|
224
|
+
if (preset.description) {
|
|
225
|
+
console.log(` ${preset.description}`);
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
function handleAccounts(rootDir) {
|
|
230
|
+
const accounts = discoverAccounts(rootDir);
|
|
231
|
+
if (accounts.length === 0) {
|
|
232
|
+
console.log("No saved WeChat accounts.");
|
|
233
|
+
return;
|
|
234
|
+
}
|
|
235
|
+
console.log("Saved WeChat accounts:\n");
|
|
236
|
+
accounts.forEach((account, index) => {
|
|
237
|
+
console.log(`${String(index + 1).padStart(2)}. ${formatAccountName(account.record)}`);
|
|
238
|
+
});
|
|
239
|
+
}
|
|
240
|
+
async function handleStop(rootDir, selector, fallback) {
|
|
241
|
+
const accounts = discoverAccounts(rootDir);
|
|
242
|
+
let candidates = fallback
|
|
243
|
+
? [fallback]
|
|
244
|
+
: selector
|
|
245
|
+
? [resolveAccount(rootDir, selector)]
|
|
246
|
+
: accounts;
|
|
247
|
+
candidates = candidates.filter((account) => inspectRuntime(account.storageDir, true).running);
|
|
248
|
+
if (candidates.length === 0) {
|
|
249
|
+
console.log(selector ? "Selected account is not running." : "No account process is running.");
|
|
250
|
+
return;
|
|
251
|
+
}
|
|
252
|
+
const account = candidates.length === 1
|
|
253
|
+
? candidates[0]
|
|
254
|
+
: await selectAccount(candidates, "Select an account process to stop");
|
|
255
|
+
const status = inspectRuntime(account.storageDir, true);
|
|
256
|
+
if (!status.running || !status.pid) {
|
|
257
|
+
console.log(`${formatAccountName(account.record)} is not running.`);
|
|
258
|
+
return;
|
|
259
|
+
}
|
|
260
|
+
try {
|
|
261
|
+
process.kill(status.pid, "SIGTERM");
|
|
262
|
+
console.log(`Stop signal sent to ${formatAccountName(account.record)} (PID ${status.pid}).`);
|
|
263
|
+
}
|
|
264
|
+
catch (err) {
|
|
265
|
+
if (err.code !== "ESRCH") {
|
|
266
|
+
throw err;
|
|
267
|
+
}
|
|
268
|
+
releaseRuntime(account.storageDir, status.pid);
|
|
269
|
+
console.log(`Cleaned stale process record for ${formatAccountName(account.record)}.`);
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
function handleStatus(rootDir, selector, fallback) {
|
|
273
|
+
const accounts = fallback
|
|
274
|
+
? [fallback]
|
|
275
|
+
: selector
|
|
276
|
+
? [resolveAccount(rootDir, selector)]
|
|
277
|
+
: discoverAccounts(rootDir);
|
|
278
|
+
if (accounts.length === 0) {
|
|
279
|
+
console.log("No saved WeChat accounts.");
|
|
280
|
+
return;
|
|
281
|
+
}
|
|
282
|
+
printAccountStatuses(accounts);
|
|
283
|
+
}
|
|
284
|
+
function printAccountStatuses(accounts) {
|
|
285
|
+
const rows = accounts.map((account) => {
|
|
286
|
+
const status = inspectRuntime(account.storageDir, true);
|
|
287
|
+
const runtime = status.runtime;
|
|
288
|
+
return {
|
|
289
|
+
account: formatAccountName(account.record),
|
|
290
|
+
pid: status.running ? String(status.pid) : "-",
|
|
291
|
+
agent: runtime?.agent ?? "-",
|
|
292
|
+
state: status.running ? "running" : "stopped",
|
|
293
|
+
uptime: runtime && status.running ? formatDuration(Date.now() - Date.parse(runtime.startedAt)) : "-",
|
|
294
|
+
};
|
|
295
|
+
});
|
|
296
|
+
const widths = {
|
|
297
|
+
account: Math.max("ACCOUNT".length, ...rows.map((row) => row.account.length)),
|
|
298
|
+
pid: Math.max("PID".length, ...rows.map((row) => row.pid.length)),
|
|
299
|
+
agent: Math.max("AGENT".length, ...rows.map((row) => row.agent.length)),
|
|
300
|
+
state: Math.max("STATE".length, ...rows.map((row) => row.state.length)),
|
|
301
|
+
};
|
|
302
|
+
console.log(`${"ACCOUNT".padEnd(widths.account)} ${"PID".padEnd(widths.pid)} ` +
|
|
303
|
+
`${"AGENT".padEnd(widths.agent)} ${"STATE".padEnd(widths.state)} UPTIME`);
|
|
304
|
+
for (const row of rows) {
|
|
305
|
+
console.log(`${row.account.padEnd(widths.account)} ${row.pid.padEnd(widths.pid)} ` +
|
|
306
|
+
`${row.agent.padEnd(widths.agent)} ${row.state.padEnd(widths.state)} ${row.uptime}`);
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
function daemonize(config, runtime) {
|
|
310
|
+
const logFile = config.daemon.logFile;
|
|
311
|
+
fs.mkdirSync(path.dirname(logFile), { recursive: true });
|
|
312
|
+
const out = fs.openSync(logFile, "a");
|
|
313
|
+
const err = fs.openSync(logFile, "a");
|
|
314
|
+
// Re-run ourselves with --no-daemon (internal flag) as a detached process
|
|
315
|
+
const args = process.argv.slice(1).filter((a) => a !== "--daemon" && a !== "--login");
|
|
316
|
+
if (!args.includes("--account") && !args.includes("--instance")) {
|
|
317
|
+
args.push("--account", runtime.accountKey);
|
|
318
|
+
}
|
|
319
|
+
const child = spawn(process.execPath, args, {
|
|
320
|
+
detached: true,
|
|
321
|
+
stdio: ["ignore", out, err],
|
|
322
|
+
env: { ...process.env, AGENT_SHELL_DAEMON: "1" },
|
|
323
|
+
windowsHide: true,
|
|
324
|
+
});
|
|
325
|
+
if (!child.pid) {
|
|
326
|
+
child.kill();
|
|
327
|
+
throw new Error("Failed to start daemon process.");
|
|
328
|
+
}
|
|
329
|
+
try {
|
|
330
|
+
claimRuntime(config.storage.dir, {
|
|
331
|
+
...runtime,
|
|
332
|
+
pid: child.pid,
|
|
333
|
+
daemon: true,
|
|
334
|
+
startedAt: new Date().toISOString(),
|
|
335
|
+
});
|
|
336
|
+
}
|
|
337
|
+
catch (error) {
|
|
338
|
+
child.kill();
|
|
339
|
+
throw error;
|
|
340
|
+
}
|
|
341
|
+
child.unref();
|
|
342
|
+
console.log(`Daemon started (PID ${child.pid})`);
|
|
343
|
+
console.log(`Logs: ${logFile}`);
|
|
344
|
+
process.exit(0);
|
|
345
|
+
}
|
|
346
|
+
async function selectAccount(accounts, prompt) {
|
|
347
|
+
if (accounts.length === 0) {
|
|
348
|
+
throw new Error("No saved WeChat accounts.");
|
|
349
|
+
}
|
|
350
|
+
if (!process.stdin.isTTY || !process.stdout.isTTY) {
|
|
351
|
+
throw new Error("Account selection requires a TTY. Specify --account <name|accountId>.");
|
|
352
|
+
}
|
|
353
|
+
console.log(`\n${prompt}:`);
|
|
354
|
+
accounts.forEach((account, index) => {
|
|
355
|
+
console.log(` ${index + 1}. ${formatAccountName(account.record)}`);
|
|
356
|
+
});
|
|
357
|
+
const answer = await ask(`Choose 1-${accounts.length}: `);
|
|
358
|
+
const index = Number.parseInt(answer, 10) - 1;
|
|
359
|
+
if (!Number.isInteger(index) || !accounts[index]) {
|
|
360
|
+
throw new Error("Invalid account selection.");
|
|
361
|
+
}
|
|
362
|
+
return accounts[index];
|
|
363
|
+
}
|
|
364
|
+
async function ask(prompt) {
|
|
365
|
+
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
366
|
+
try {
|
|
367
|
+
return (await rl.question(prompt)).trim();
|
|
368
|
+
}
|
|
369
|
+
finally {
|
|
370
|
+
rl.close();
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
function formatDuration(durationMs) {
|
|
374
|
+
const totalSeconds = Math.max(0, Math.floor(durationMs / 1000));
|
|
375
|
+
const hours = Math.floor(totalSeconds / 3600);
|
|
376
|
+
const minutes = Math.floor((totalSeconds % 3600) / 60);
|
|
377
|
+
const seconds = totalSeconds % 60;
|
|
378
|
+
return [hours, minutes, seconds].map((value) => String(value).padStart(2, "0")).join(":");
|
|
379
|
+
}
|
|
380
|
+
function renderQrInTerminal(url) {
|
|
381
|
+
qrcodeTerminal.generate(url, { small: true }, (qr) => {
|
|
382
|
+
console.log(qr);
|
|
383
|
+
});
|
|
384
|
+
}
|
|
385
|
+
async function loginAndRegisterAccount(params) {
|
|
386
|
+
const accounts = discoverAccounts(params.rootDir);
|
|
387
|
+
if (accounts.length > 0) {
|
|
388
|
+
console.log("Saved WeChat accounts and process status:\n");
|
|
389
|
+
printAccountStatuses(accounts);
|
|
390
|
+
}
|
|
391
|
+
else {
|
|
392
|
+
console.log("No saved WeChat accounts.");
|
|
393
|
+
}
|
|
394
|
+
let overwrite = params.selected;
|
|
395
|
+
let accountName = params.accountName;
|
|
396
|
+
if (!overwrite && !accountName) {
|
|
397
|
+
if (!process.stdin.isTTY || !process.stdout.isTTY) {
|
|
398
|
+
throw new Error("Login selection requires a TTY. Use --account to overwrite or --account-name to add.");
|
|
399
|
+
}
|
|
400
|
+
console.log("\nLogin action:");
|
|
401
|
+
console.log(" A. Add a new account");
|
|
402
|
+
accounts.forEach((account, index) => {
|
|
403
|
+
console.log(` ${index + 1}. Overwrite ${formatAccountName(account.record)}`);
|
|
404
|
+
});
|
|
405
|
+
const choice = await ask(accounts.length > 0
|
|
406
|
+
? `Choose A or 1-${accounts.length}: `
|
|
407
|
+
: "Choose A to add a new account: ");
|
|
408
|
+
if (/^a(dd)?$/i.test(choice)) {
|
|
409
|
+
accountName = validateAccountName(await ask("Enter a name for the new account: "));
|
|
410
|
+
}
|
|
411
|
+
else {
|
|
412
|
+
const index = Number.parseInt(choice, 10) - 1;
|
|
413
|
+
if (!Number.isInteger(index) || !accounts[index]) {
|
|
414
|
+
throw new Error("Invalid login action.");
|
|
415
|
+
}
|
|
416
|
+
overwrite = accounts[index];
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
if (overwrite) {
|
|
420
|
+
const status = inspectRuntime(overwrite.storageDir, true);
|
|
421
|
+
if (status.running) {
|
|
422
|
+
throw new Error(`Cannot overwrite ${formatAccountName(overwrite.record)} while it is running (PID ${status.pid}).`);
|
|
423
|
+
}
|
|
424
|
+
accountName = accountName
|
|
425
|
+
? validateAccountName(accountName)
|
|
426
|
+
: overwrite.record.accountName;
|
|
427
|
+
console.log(`\nSelected overwrite: ${formatAccountName(overwrite.record)}`);
|
|
428
|
+
}
|
|
429
|
+
else {
|
|
430
|
+
accountName = validateAccountName(accountName ?? "");
|
|
431
|
+
console.log(`\nSelected add: ${accountName}`);
|
|
432
|
+
}
|
|
433
|
+
console.log("Starting QR login...");
|
|
434
|
+
const token = await login({
|
|
435
|
+
baseUrl: params.config.wechat.baseUrl,
|
|
436
|
+
botType: params.config.wechat.botType,
|
|
437
|
+
storageDir: params.rootDir,
|
|
438
|
+
persist: false,
|
|
439
|
+
log: (message) => console.log(message),
|
|
440
|
+
renderQrUrl: renderQrInTerminal,
|
|
441
|
+
});
|
|
442
|
+
const sameAccount = accounts.find((account) => account.record.accountId === token.accountId);
|
|
443
|
+
if (sameAccount && sameAccount.record.accountKey !== overwrite?.record.accountKey) {
|
|
444
|
+
throw new Error(`The scanned WeChat account is already saved as ${formatAccountName(sameAccount.record)}. ` +
|
|
445
|
+
"Run login again and choose that account to overwrite.");
|
|
446
|
+
}
|
|
447
|
+
const registered = registerAccount({
|
|
448
|
+
rootDir: params.rootDir,
|
|
449
|
+
token,
|
|
450
|
+
accountName,
|
|
451
|
+
overwriteAccountKey: overwrite?.record.accountKey,
|
|
452
|
+
});
|
|
453
|
+
console.log(`Saved account: ${formatAccountName(registered.record)}`);
|
|
454
|
+
console.log(`Token directory: ${registered.storageDir}`);
|
|
455
|
+
return registered;
|
|
456
|
+
}
|
|
457
|
+
function applyAccountStorage(config, account) {
|
|
458
|
+
config.storage.instance = account.record.accountKey;
|
|
459
|
+
config.storage.dir = account.storageDir;
|
|
460
|
+
config.daemon.logFile = path.join(account.storageDir, "agent-shell.log");
|
|
461
|
+
config.daemon.pidFile = path.join(account.storageDir, "daemon.pid");
|
|
462
|
+
}
|
|
463
|
+
function runtimeForAccount(account, config, pid, daemon) {
|
|
464
|
+
return {
|
|
465
|
+
pid,
|
|
466
|
+
accountKey: account.record.accountKey,
|
|
467
|
+
accountName: account.record.accountName,
|
|
468
|
+
accountId: account.record.accountId,
|
|
469
|
+
agent: config.agent.preset ?? config.agent.command,
|
|
470
|
+
cwd: config.agent.cwd,
|
|
471
|
+
startedAt: new Date().toISOString(),
|
|
472
|
+
daemon,
|
|
473
|
+
};
|
|
474
|
+
}
|
|
475
|
+
function accountForLegacyStorage(storageDir, name) {
|
|
476
|
+
const token = loadToken(storageDir);
|
|
477
|
+
const runtime = inspectRuntime(storageDir).runtime;
|
|
478
|
+
const now = new Date().toISOString();
|
|
479
|
+
return {
|
|
480
|
+
record: {
|
|
481
|
+
accountKey: name,
|
|
482
|
+
accountName: token?.accountName ?? runtime?.accountName ?? name,
|
|
483
|
+
accountId: token?.accountId ?? runtime?.accountId ?? "unknown",
|
|
484
|
+
userId: token?.userId ?? "unknown",
|
|
485
|
+
storageDir: ".",
|
|
486
|
+
createdAt: token?.savedAt ?? runtime?.startedAt ?? now,
|
|
487
|
+
updatedAt: token?.savedAt ?? runtime?.startedAt ?? now,
|
|
488
|
+
},
|
|
489
|
+
storageDir,
|
|
490
|
+
};
|
|
491
|
+
}
|
|
492
|
+
async function main() {
|
|
493
|
+
const args = parseArgs(process.argv);
|
|
494
|
+
if (args.version) {
|
|
495
|
+
console.log(packageJson.version);
|
|
496
|
+
process.exit(0);
|
|
497
|
+
}
|
|
498
|
+
if (args.help) {
|
|
499
|
+
usage();
|
|
500
|
+
process.exit(0);
|
|
501
|
+
}
|
|
502
|
+
if (args.instance !== undefined) {
|
|
503
|
+
try {
|
|
504
|
+
validateInstanceName(args.instance);
|
|
505
|
+
}
|
|
506
|
+
catch (err) {
|
|
507
|
+
console.error(`Error: ${err.message}`);
|
|
508
|
+
process.exit(1);
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
if (args.instance && args.account) {
|
|
512
|
+
console.error("Error: --instance and --account cannot be used together.");
|
|
513
|
+
process.exit(1);
|
|
514
|
+
}
|
|
515
|
+
const config = defaultConfig({ instance: args.instance });
|
|
516
|
+
const accountRoot = defaultStorageDir();
|
|
517
|
+
let selectedAccount;
|
|
518
|
+
// Load config file if specified
|
|
519
|
+
let configFileSetInboxDir = false;
|
|
520
|
+
let configFileSetStateFile = false;
|
|
521
|
+
let configFileSetInjectDir = false;
|
|
522
|
+
if (args.configFile) {
|
|
523
|
+
const fileConfig = loadConfigFile(args.configFile);
|
|
524
|
+
Object.assign(config.wechat, fileConfig.wechat ?? {});
|
|
525
|
+
Object.assign(config.agent, fileConfig.agent ?? {});
|
|
526
|
+
Object.assign(config.agents, fileConfig.agents ?? {});
|
|
527
|
+
Object.assign(config.session, fileConfig.session ?? {});
|
|
528
|
+
Object.assign(config.daemon, fileConfig.daemon ?? {});
|
|
529
|
+
if (Object.prototype.hasOwnProperty.call(fileConfig, "commandAliases")) {
|
|
530
|
+
// Assign the raw value (even if malformed) so the post-merge
|
|
531
|
+
// validateCommandAliases() below can reject it with a clean error.
|
|
532
|
+
config.commandAliases = fileConfig.commandAliases;
|
|
533
|
+
}
|
|
534
|
+
// Track whether the user explicitly set inboxDir so we don't
|
|
535
|
+
// overwrite their choice with a re-derived default below. We check
|
|
536
|
+
// before Object.assign because checking after can't distinguish
|
|
537
|
+
// "user wrote inboxDir: null to disable" from "user didn't write it".
|
|
538
|
+
if (fileConfig.storage &&
|
|
539
|
+
Object.prototype.hasOwnProperty.call(fileConfig.storage, "inboxDir")) {
|
|
540
|
+
configFileSetInboxDir = true;
|
|
541
|
+
}
|
|
542
|
+
if (fileConfig.storage &&
|
|
543
|
+
Object.prototype.hasOwnProperty.call(fileConfig.storage, "stateFile")) {
|
|
544
|
+
configFileSetStateFile = true;
|
|
545
|
+
}
|
|
546
|
+
if (fileConfig.storage &&
|
|
547
|
+
Object.prototype.hasOwnProperty.call(fileConfig.storage, "injectDir")) {
|
|
548
|
+
configFileSetInjectDir = true;
|
|
549
|
+
}
|
|
550
|
+
Object.assign(config.storage, fileConfig.storage ?? {});
|
|
551
|
+
}
|
|
552
|
+
if (args.command === "agents") {
|
|
553
|
+
handleAgents(config);
|
|
554
|
+
return;
|
|
555
|
+
}
|
|
556
|
+
if (args.command === "accounts") {
|
|
557
|
+
handleAccounts(accountRoot);
|
|
558
|
+
return;
|
|
559
|
+
}
|
|
560
|
+
const configuredStorageDir = args.instance
|
|
561
|
+
? defaultStorageDir(args.instance)
|
|
562
|
+
: path.resolve(config.storage.dir);
|
|
563
|
+
const legacyCommandAccount = !args.account && path.resolve(configuredStorageDir) !== path.resolve(accountRoot)
|
|
564
|
+
? accountForLegacyStorage(configuredStorageDir, args.instance ?? "custom")
|
|
565
|
+
: undefined;
|
|
566
|
+
if (args.command === "status") {
|
|
567
|
+
handleStatus(accountRoot, args.account ?? args.instance, legacyCommandAccount);
|
|
568
|
+
return;
|
|
569
|
+
}
|
|
570
|
+
if (args.command === "stop") {
|
|
571
|
+
await handleStop(accountRoot, args.account ?? args.instance, legacyCommandAccount);
|
|
572
|
+
return;
|
|
573
|
+
}
|
|
574
|
+
// CLI --instance always wins over config-file storage.dir so users can
|
|
575
|
+
// run a config in multiple isolated instances without editing the file.
|
|
576
|
+
if (args.instance) {
|
|
577
|
+
config.storage.instance = args.instance;
|
|
578
|
+
config.storage.dir = defaultStorageDir(args.instance);
|
|
579
|
+
config.daemon.logFile = path.join(config.storage.dir, "agent-shell.log");
|
|
580
|
+
config.daemon.pidFile = path.join(config.storage.dir, "daemon.pid");
|
|
581
|
+
}
|
|
582
|
+
const customStorageMode = !args.account &&
|
|
583
|
+
(Boolean(args.instance) || path.resolve(config.storage.dir) !== path.resolve(accountRoot));
|
|
584
|
+
if (!customStorageMode) {
|
|
585
|
+
const accounts = discoverAccounts(accountRoot);
|
|
586
|
+
if (args.account) {
|
|
587
|
+
selectedAccount = resolveAccount(accountRoot, args.account);
|
|
588
|
+
}
|
|
589
|
+
if (args.command === "login" || args.forceLogin) {
|
|
590
|
+
selectedAccount = await loginAndRegisterAccount({
|
|
591
|
+
config,
|
|
592
|
+
rootDir: accountRoot,
|
|
593
|
+
selected: selectedAccount,
|
|
594
|
+
accountName: args.accountName,
|
|
595
|
+
});
|
|
596
|
+
args.forceLogin = false;
|
|
597
|
+
}
|
|
598
|
+
else if (accounts.length === 0) {
|
|
599
|
+
if (args.command === "inject") {
|
|
600
|
+
throw new Error("No saved account is available for injection. Run `agent-shell login` first.");
|
|
601
|
+
}
|
|
602
|
+
selectedAccount = await loginAndRegisterAccount({
|
|
603
|
+
config,
|
|
604
|
+
rootDir: accountRoot,
|
|
605
|
+
accountName: args.accountName,
|
|
606
|
+
});
|
|
607
|
+
}
|
|
608
|
+
else {
|
|
609
|
+
if (!process.env.AGENT_SHELL_DAEMON) {
|
|
610
|
+
console.log("Saved WeChat accounts and process status:\n");
|
|
611
|
+
printAccountStatuses(accounts);
|
|
612
|
+
}
|
|
613
|
+
if (!selectedAccount) {
|
|
614
|
+
selectedAccount = await selectAccount(accounts, "Select a WeChat account to start");
|
|
615
|
+
}
|
|
616
|
+
}
|
|
617
|
+
applyAccountStorage(config, selectedAccount);
|
|
618
|
+
}
|
|
619
|
+
else {
|
|
620
|
+
const token = loadToken(config.storage.dir);
|
|
621
|
+
if (token) {
|
|
622
|
+
selectedAccount = {
|
|
623
|
+
record: {
|
|
624
|
+
accountKey: args.instance ?? "custom",
|
|
625
|
+
accountName: token.accountName ?? args.instance ?? "custom",
|
|
626
|
+
accountId: token.accountId,
|
|
627
|
+
userId: token.userId,
|
|
628
|
+
storageDir: ".",
|
|
629
|
+
createdAt: token.savedAt,
|
|
630
|
+
updatedAt: token.savedAt,
|
|
631
|
+
},
|
|
632
|
+
storageDir: config.storage.dir,
|
|
633
|
+
};
|
|
634
|
+
}
|
|
635
|
+
}
|
|
636
|
+
if (args.command === "login") {
|
|
637
|
+
return;
|
|
638
|
+
}
|
|
639
|
+
// Resolve the final inbox directory. Precedence (highest first):
|
|
640
|
+
// 1. --no-inbox (explicit disable)
|
|
641
|
+
// 2. --inbox-dir <path> (explicit CLI override)
|
|
642
|
+
// 3. config.storage.inboxDir explicitly set in the config file
|
|
643
|
+
// (relative paths are resolved against cwd)
|
|
644
|
+
// 4. Default: <storage.dir>/inbox, re-derived from whatever the
|
|
645
|
+
// final storage.dir is. This is what keeps a config file that
|
|
646
|
+
// only sets storage.dir consistent with the documented
|
|
647
|
+
// "default: <storage.dir>/inbox", and also covers the
|
|
648
|
+
// --instance case for free.
|
|
649
|
+
if (args.disableInbox) {
|
|
650
|
+
config.storage.inboxDir = null;
|
|
651
|
+
}
|
|
652
|
+
else if (args.inboxDir) {
|
|
653
|
+
config.storage.inboxDir = path.resolve(args.inboxDir);
|
|
654
|
+
}
|
|
655
|
+
else if (configFileSetInboxDir) {
|
|
656
|
+
if (config.storage.inboxDir && !path.isAbsolute(config.storage.inboxDir)) {
|
|
657
|
+
config.storage.inboxDir = path.resolve(config.storage.inboxDir);
|
|
658
|
+
}
|
|
659
|
+
}
|
|
660
|
+
else {
|
|
661
|
+
config.storage.inboxDir = path.join(config.storage.dir, "inbox");
|
|
662
|
+
}
|
|
663
|
+
if (configFileSetStateFile) {
|
|
664
|
+
if (config.storage.stateFile && !path.isAbsolute(config.storage.stateFile)) {
|
|
665
|
+
config.storage.stateFile = path.resolve(config.storage.stateFile);
|
|
666
|
+
}
|
|
667
|
+
}
|
|
668
|
+
else {
|
|
669
|
+
config.storage.stateFile = path.join(config.storage.dir, "state.json");
|
|
670
|
+
}
|
|
671
|
+
if (configFileSetInjectDir) {
|
|
672
|
+
if (config.storage.injectDir && !path.isAbsolute(config.storage.injectDir)) {
|
|
673
|
+
config.storage.injectDir = path.resolve(config.storage.injectDir);
|
|
674
|
+
}
|
|
675
|
+
}
|
|
676
|
+
else {
|
|
677
|
+
config.storage.injectDir = path.join(config.storage.dir, "inject");
|
|
678
|
+
}
|
|
679
|
+
try {
|
|
680
|
+
validateCommandAliases(config.commandAliases);
|
|
681
|
+
}
|
|
682
|
+
catch (err) {
|
|
683
|
+
console.error(`Error: ${err.message}`);
|
|
684
|
+
process.exit(1);
|
|
685
|
+
}
|
|
686
|
+
// Handle storage-scoped subcommands
|
|
687
|
+
if (args.command === "inject") {
|
|
688
|
+
await handleInject(config, args);
|
|
689
|
+
return;
|
|
690
|
+
}
|
|
691
|
+
const agentSelection = args.agent ?? config.agent.preset;
|
|
692
|
+
// Require preset or raw command
|
|
693
|
+
if (!agentSelection && !config.agent.command) {
|
|
694
|
+
console.error("Error: --agent is required\n");
|
|
695
|
+
usage();
|
|
696
|
+
process.exit(1);
|
|
697
|
+
}
|
|
698
|
+
if (agentSelection) {
|
|
699
|
+
const resolvedAgent = resolveAgentSelection(agentSelection, config.agents);
|
|
700
|
+
config.agent.preset = resolvedAgent.id;
|
|
701
|
+
config.agent.command = resolvedAgent.command;
|
|
702
|
+
config.agent.args = resolvedAgent.args;
|
|
703
|
+
if (resolvedAgent.env) {
|
|
704
|
+
config.agent.env = { ...(config.agent.env ?? {}), ...resolvedAgent.env };
|
|
705
|
+
}
|
|
706
|
+
}
|
|
707
|
+
if (args.cwd)
|
|
708
|
+
config.agent.cwd = path.resolve(args.cwd);
|
|
709
|
+
if (args.idleTimeout !== undefined) {
|
|
710
|
+
if (!Number.isFinite(args.idleTimeout) || args.idleTimeout < 0) {
|
|
711
|
+
console.error("Error: invalid --idle-timeout value");
|
|
712
|
+
console.error('Use a non-negative integer minute value, where "0" means unlimited.');
|
|
713
|
+
process.exit(1);
|
|
714
|
+
}
|
|
715
|
+
config.session.idleTimeoutMs = args.idleTimeout * 60_000;
|
|
716
|
+
}
|
|
717
|
+
if (args.maxSessions)
|
|
718
|
+
config.session.maxConcurrentUsers = args.maxSessions;
|
|
719
|
+
if (args.hideThoughts)
|
|
720
|
+
config.agent.showThoughts = false;
|
|
721
|
+
if (args.showDiffs)
|
|
722
|
+
config.agent.showDiffs = true;
|
|
723
|
+
if (args.autoSendMedia !== undefined) {
|
|
724
|
+
const mode = args.autoSendMedia.toLowerCase();
|
|
725
|
+
if (mode !== "off" && mode !== "tagged" && mode !== "all") {
|
|
726
|
+
console.error(`Error: invalid --auto-send-media value "${args.autoSendMedia}". Must be: off, tagged, or all.`);
|
|
727
|
+
process.exit(1);
|
|
728
|
+
}
|
|
729
|
+
config.agent.autoSendMedia = mode;
|
|
730
|
+
}
|
|
731
|
+
if (args.systemPrompt !== undefined) {
|
|
732
|
+
config.agent.systemPrompt = args.systemPrompt;
|
|
733
|
+
}
|
|
734
|
+
config.daemon.enabled = args.daemon;
|
|
735
|
+
if (!selectedAccount) {
|
|
736
|
+
const token = loadToken(config.storage.dir);
|
|
737
|
+
if (token) {
|
|
738
|
+
selectedAccount = {
|
|
739
|
+
record: {
|
|
740
|
+
accountKey: config.storage.instance ?? "custom",
|
|
741
|
+
accountName: token.accountName ?? config.storage.instance ?? "custom",
|
|
742
|
+
accountId: token.accountId,
|
|
743
|
+
userId: token.userId,
|
|
744
|
+
storageDir: ".",
|
|
745
|
+
createdAt: token.savedAt,
|
|
746
|
+
updatedAt: token.savedAt,
|
|
747
|
+
},
|
|
748
|
+
storageDir: config.storage.dir,
|
|
749
|
+
};
|
|
750
|
+
}
|
|
751
|
+
}
|
|
752
|
+
if (!selectedAccount) {
|
|
753
|
+
throw new Error("No saved account is available. Run `agent-shell login` first.");
|
|
754
|
+
}
|
|
755
|
+
const runtime = runtimeForAccount(selectedAccount, config, process.pid, args.daemon);
|
|
756
|
+
// Handle daemon mode
|
|
757
|
+
if (args.daemon && !process.env.AGENT_SHELL_DAEMON) {
|
|
758
|
+
daemonize(config, runtime);
|
|
759
|
+
}
|
|
760
|
+
const ownsRuntime = !process.env.AGENT_SHELL_DAEMON;
|
|
761
|
+
if (ownsRuntime) {
|
|
762
|
+
claimRuntime(config.storage.dir, runtime);
|
|
763
|
+
}
|
|
764
|
+
// Initialize telemetry. No-op when AGENT_SHELL_TELEMETRY=0/false/off.
|
|
765
|
+
initTelemetry({
|
|
766
|
+
version: packageJson.version,
|
|
767
|
+
storageDir: config.storage.dir,
|
|
768
|
+
agentPreset: config.agent.preset ?? "raw",
|
|
769
|
+
daemon: config.daemon.enabled,
|
|
770
|
+
});
|
|
771
|
+
trackEvent("app.start", {
|
|
772
|
+
agentPreset: config.agent.preset ?? "raw",
|
|
773
|
+
daemon: config.daemon.enabled,
|
|
774
|
+
});
|
|
775
|
+
const startedAt = Date.now();
|
|
776
|
+
// Create and start bridge
|
|
777
|
+
const bridge = new AgentShellBridge(config, (msg) => {
|
|
778
|
+
const ts = new Date().toISOString().substring(11, 19);
|
|
779
|
+
console.log(`[${ts}] ${msg}`);
|
|
780
|
+
});
|
|
781
|
+
// Handle graceful shutdown
|
|
782
|
+
const shutdown = async (reason) => {
|
|
783
|
+
trackEvent("app.stop", { reason, uptimeSec: Math.round((Date.now() - startedAt) / 1000) });
|
|
784
|
+
await bridge.stop();
|
|
785
|
+
await shutdownTelemetry();
|
|
786
|
+
releaseRuntime(config.storage.dir, process.pid);
|
|
787
|
+
process.exit(reason === "error" ? 1 : 0);
|
|
788
|
+
};
|
|
789
|
+
process.on("SIGINT", () => void shutdown("signal"));
|
|
790
|
+
process.on("SIGTERM", () => void shutdown("signal"));
|
|
791
|
+
try {
|
|
792
|
+
await bridge.start({
|
|
793
|
+
forceLogin: args.forceLogin,
|
|
794
|
+
renderQrUrl: renderQrInTerminal,
|
|
795
|
+
});
|
|
796
|
+
releaseRuntime(config.storage.dir, process.pid);
|
|
797
|
+
}
|
|
798
|
+
catch (err) {
|
|
799
|
+
if (err.message === "aborted") {
|
|
800
|
+
// Normal shutdown
|
|
801
|
+
}
|
|
802
|
+
else {
|
|
803
|
+
trackException(err, "main");
|
|
804
|
+
trackEvent("app.stop", { reason: "error", uptimeSec: Math.round((Date.now() - startedAt) / 1000) });
|
|
805
|
+
await shutdownTelemetry();
|
|
806
|
+
releaseRuntime(config.storage.dir, process.pid);
|
|
807
|
+
console.error(`Fatal: ${String(err)}`);
|
|
808
|
+
process.exit(1);
|
|
809
|
+
}
|
|
810
|
+
}
|
|
811
|
+
}
|
|
812
|
+
main().catch((err) => {
|
|
813
|
+
console.error(`Fatal: ${String(err)}`);
|
|
814
|
+
process.exit(1);
|
|
815
|
+
});
|
|
816
|
+
//# sourceMappingURL=agent-shell.js.map
|