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.
Files changed (88) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +199 -0
  3. package/dist/bin/agent-shell.d.ts +15 -0
  4. package/dist/bin/agent-shell.d.ts.map +1 -0
  5. package/dist/bin/agent-shell.js +816 -0
  6. package/dist/bin/agent-shell.js.map +1 -0
  7. package/dist/package.json +54 -0
  8. package/dist/src/acp/agent-manager.d.ts +22 -0
  9. package/dist/src/acp/agent-manager.d.ts.map +1 -0
  10. package/dist/src/acp/agent-manager.js +79 -0
  11. package/dist/src/acp/agent-manager.js.map +1 -0
  12. package/dist/src/acp/client.d.ts +64 -0
  13. package/dist/src/acp/client.d.ts.map +1 -0
  14. package/dist/src/acp/client.js +265 -0
  15. package/dist/src/acp/client.js.map +1 -0
  16. package/dist/src/acp/session.d.ts +81 -0
  17. package/dist/src/acp/session.d.ts.map +1 -0
  18. package/dist/src/acp/session.js +339 -0
  19. package/dist/src/acp/session.js.map +1 -0
  20. package/dist/src/adapter/inbound.d.ts +39 -0
  21. package/dist/src/adapter/inbound.d.ts.map +1 -0
  22. package/dist/src/adapter/inbound.js +264 -0
  23. package/dist/src/adapter/inbound.js.map +1 -0
  24. package/dist/src/bridge.d.ts +115 -0
  25. package/dist/src/bridge.d.ts.map +1 -0
  26. package/dist/src/bridge.js +969 -0
  27. package/dist/src/bridge.js.map +1 -0
  28. package/dist/src/config.d.ts +155 -0
  29. package/dist/src/config.d.ts.map +1 -0
  30. package/dist/src/config.js +265 -0
  31. package/dist/src/config.js.map +1 -0
  32. package/dist/src/index.d.ts +9 -0
  33. package/dist/src/index.d.ts.map +1 -0
  34. package/dist/src/index.js +7 -0
  35. package/dist/src/index.js.map +1 -0
  36. package/dist/src/inject/monitor.d.ts +24 -0
  37. package/dist/src/inject/monitor.d.ts.map +1 -0
  38. package/dist/src/inject/monitor.js +149 -0
  39. package/dist/src/inject/monitor.js.map +1 -0
  40. package/dist/src/inject/queue.d.ts +13 -0
  41. package/dist/src/inject/queue.d.ts.map +1 -0
  42. package/dist/src/inject/queue.js +35 -0
  43. package/dist/src/inject/queue.js.map +1 -0
  44. package/dist/src/inject/types.d.ts +10 -0
  45. package/dist/src/inject/types.d.ts.map +1 -0
  46. package/dist/src/inject/types.js +2 -0
  47. package/dist/src/inject/types.js.map +1 -0
  48. package/dist/src/storage/accounts.d.ts +43 -0
  49. package/dist/src/storage/accounts.d.ts.map +1 -0
  50. package/dist/src/storage/accounts.js +289 -0
  51. package/dist/src/storage/accounts.js.map +1 -0
  52. package/dist/src/storage/runtime.d.ts +23 -0
  53. package/dist/src/storage/runtime.d.ts.map +1 -0
  54. package/dist/src/storage/runtime.js +104 -0
  55. package/dist/src/storage/runtime.js.map +1 -0
  56. package/dist/src/storage/state.d.ts +17 -0
  57. package/dist/src/storage/state.d.ts.map +1 -0
  58. package/dist/src/storage/state.js +78 -0
  59. package/dist/src/storage/state.js.map +1 -0
  60. package/dist/src/telemetry/index.d.ts +33 -0
  61. package/dist/src/telemetry/index.d.ts.map +1 -0
  62. package/dist/src/telemetry/index.js +167 -0
  63. package/dist/src/telemetry/index.js.map +1 -0
  64. package/dist/src/weixin/api.d.ts +50 -0
  65. package/dist/src/weixin/api.d.ts.map +1 -0
  66. package/dist/src/weixin/api.js +90 -0
  67. package/dist/src/weixin/api.js.map +1 -0
  68. package/dist/src/weixin/auth.d.ts +26 -0
  69. package/dist/src/weixin/auth.d.ts.map +1 -0
  70. package/dist/src/weixin/auth.js +103 -0
  71. package/dist/src/weixin/auth.js.map +1 -0
  72. package/dist/src/weixin/media.d.ts +24 -0
  73. package/dist/src/weixin/media.d.ts.map +1 -0
  74. package/dist/src/weixin/media.js +64 -0
  75. package/dist/src/weixin/media.js.map +1 -0
  76. package/dist/src/weixin/monitor.d.ts +16 -0
  77. package/dist/src/weixin/monitor.d.ts.map +1 -0
  78. package/dist/src/weixin/monitor.js +113 -0
  79. package/dist/src/weixin/monitor.js.map +1 -0
  80. package/dist/src/weixin/send.d.ts +28 -0
  81. package/dist/src/weixin/send.d.ts.map +1 -0
  82. package/dist/src/weixin/send.js +162 -0
  83. package/dist/src/weixin/send.js.map +1 -0
  84. package/dist/src/weixin/types.d.ts +149 -0
  85. package/dist/src/weixin/types.d.ts.map +1 -0
  86. package/dist/src/weixin/types.js +33 -0
  87. package/dist/src/weixin/types.js.map +1 -0
  88. 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