@suzuke/agend 0.0.1 → 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (202) hide show
  1. package/README.md +557 -1
  2. package/README.zh-TW.md +504 -0
  3. package/dist/access-path.d.ts +7 -0
  4. package/dist/access-path.js +12 -0
  5. package/dist/access-path.js.map +1 -0
  6. package/dist/approval/approval-server.d.ts +30 -0
  7. package/dist/approval/approval-server.js +156 -0
  8. package/dist/approval/approval-server.js.map +1 -0
  9. package/dist/approval/tmux-prompt-detector.d.ts +34 -0
  10. package/dist/approval/tmux-prompt-detector.js +264 -0
  11. package/dist/approval/tmux-prompt-detector.js.map +1 -0
  12. package/dist/backend/approval-strategy.d.ts +14 -0
  13. package/dist/backend/approval-strategy.js +2 -0
  14. package/dist/backend/approval-strategy.js.map +1 -0
  15. package/dist/backend/claude-code.d.ts +13 -0
  16. package/dist/backend/claude-code.js +114 -0
  17. package/dist/backend/claude-code.js.map +1 -0
  18. package/dist/backend/codex.d.ts +10 -0
  19. package/dist/backend/codex.js +58 -0
  20. package/dist/backend/codex.js.map +1 -0
  21. package/dist/backend/factory.d.ts +2 -0
  22. package/dist/backend/factory.js +19 -0
  23. package/dist/backend/factory.js.map +1 -0
  24. package/dist/backend/gemini-cli.d.ts +10 -0
  25. package/dist/backend/gemini-cli.js +68 -0
  26. package/dist/backend/gemini-cli.js.map +1 -0
  27. package/dist/backend/hook-based-approval.d.ts +20 -0
  28. package/dist/backend/hook-based-approval.js +41 -0
  29. package/dist/backend/hook-based-approval.js.map +1 -0
  30. package/dist/backend/index.d.ts +6 -0
  31. package/dist/backend/index.js +6 -0
  32. package/dist/backend/index.js.map +1 -0
  33. package/dist/backend/opencode.d.ts +10 -0
  34. package/dist/backend/opencode.js +63 -0
  35. package/dist/backend/opencode.js.map +1 -0
  36. package/dist/backend/types.d.ts +26 -0
  37. package/dist/backend/types.js +2 -0
  38. package/dist/backend/types.js.map +1 -0
  39. package/dist/channel/access-manager.d.ts +18 -0
  40. package/dist/channel/access-manager.js +149 -0
  41. package/dist/channel/access-manager.js.map +1 -0
  42. package/dist/channel/adapters/discord.d.ts +45 -0
  43. package/dist/channel/adapters/discord.js +366 -0
  44. package/dist/channel/adapters/discord.js.map +1 -0
  45. package/dist/channel/adapters/telegram.d.ts +58 -0
  46. package/dist/channel/adapters/telegram.js +569 -0
  47. package/dist/channel/adapters/telegram.js.map +1 -0
  48. package/dist/channel/attachment-handler.d.ts +15 -0
  49. package/dist/channel/attachment-handler.js +55 -0
  50. package/dist/channel/attachment-handler.js.map +1 -0
  51. package/dist/channel/factory.d.ts +12 -0
  52. package/dist/channel/factory.js +38 -0
  53. package/dist/channel/factory.js.map +1 -0
  54. package/dist/channel/ipc-bridge.d.ts +26 -0
  55. package/dist/channel/ipc-bridge.js +170 -0
  56. package/dist/channel/ipc-bridge.js.map +1 -0
  57. package/dist/channel/mcp-server.d.ts +10 -0
  58. package/dist/channel/mcp-server.js +196 -0
  59. package/dist/channel/mcp-server.js.map +1 -0
  60. package/dist/channel/mcp-tools.d.ts +909 -0
  61. package/dist/channel/mcp-tools.js +346 -0
  62. package/dist/channel/mcp-tools.js.map +1 -0
  63. package/dist/channel/message-bus.d.ts +17 -0
  64. package/dist/channel/message-bus.js +86 -0
  65. package/dist/channel/message-bus.js.map +1 -0
  66. package/dist/channel/message-queue.d.ts +39 -0
  67. package/dist/channel/message-queue.js +248 -0
  68. package/dist/channel/message-queue.js.map +1 -0
  69. package/dist/channel/tool-router.d.ts +6 -0
  70. package/dist/channel/tool-router.js +69 -0
  71. package/dist/channel/tool-router.js.map +1 -0
  72. package/dist/channel/tool-tracker.d.ts +13 -0
  73. package/dist/channel/tool-tracker.js +58 -0
  74. package/dist/channel/tool-tracker.js.map +1 -0
  75. package/dist/channel/types.d.ts +116 -0
  76. package/dist/channel/types.js +2 -0
  77. package/dist/channel/types.js.map +1 -0
  78. package/dist/cli.d.ts +2 -0
  79. package/dist/cli.js +782 -0
  80. package/dist/cli.js.map +1 -0
  81. package/dist/config.d.ts +8 -0
  82. package/dist/config.js +85 -0
  83. package/dist/config.js.map +1 -0
  84. package/dist/container-manager.d.ts +24 -0
  85. package/dist/container-manager.js +148 -0
  86. package/dist/container-manager.js.map +1 -0
  87. package/dist/context-guardian.d.ts +29 -0
  88. package/dist/context-guardian.js +123 -0
  89. package/dist/context-guardian.js.map +1 -0
  90. package/dist/cost-guard.d.ts +21 -0
  91. package/dist/cost-guard.js +113 -0
  92. package/dist/cost-guard.js.map +1 -0
  93. package/dist/daemon-entry.d.ts +1 -0
  94. package/dist/daemon-entry.js +29 -0
  95. package/dist/daemon-entry.js.map +1 -0
  96. package/dist/daemon.d.ts +88 -0
  97. package/dist/daemon.js +820 -0
  98. package/dist/daemon.js.map +1 -0
  99. package/dist/daily-summary.d.ts +13 -0
  100. package/dist/daily-summary.js +55 -0
  101. package/dist/daily-summary.js.map +1 -0
  102. package/dist/db.d.ts +10 -0
  103. package/dist/db.js +43 -0
  104. package/dist/db.js.map +1 -0
  105. package/dist/event-log.d.ts +22 -0
  106. package/dist/event-log.js +66 -0
  107. package/dist/event-log.js.map +1 -0
  108. package/dist/export-import.d.ts +2 -0
  109. package/dist/export-import.js +110 -0
  110. package/dist/export-import.js.map +1 -0
  111. package/dist/fleet-context.d.ts +36 -0
  112. package/dist/fleet-context.js +4 -0
  113. package/dist/fleet-context.js.map +1 -0
  114. package/dist/fleet-manager.d.ts +115 -0
  115. package/dist/fleet-manager.js +1742 -0
  116. package/dist/fleet-manager.js.map +1 -0
  117. package/dist/fleet-system-prompt.d.ts +11 -0
  118. package/dist/fleet-system-prompt.js +60 -0
  119. package/dist/fleet-system-prompt.js.map +1 -0
  120. package/dist/hang-detector.d.ts +16 -0
  121. package/dist/hang-detector.js +53 -0
  122. package/dist/hang-detector.js.map +1 -0
  123. package/dist/index.d.ts +8 -0
  124. package/dist/index.js +6 -0
  125. package/dist/index.js.map +1 -0
  126. package/dist/install-recorder.d.ts +30 -0
  127. package/dist/install-recorder.js +159 -0
  128. package/dist/install-recorder.js.map +1 -0
  129. package/dist/logger.d.ts +3 -0
  130. package/dist/logger.js +63 -0
  131. package/dist/logger.js.map +1 -0
  132. package/dist/meeting/orchestrator.d.ts +30 -0
  133. package/dist/meeting/orchestrator.js +355 -0
  134. package/dist/meeting/orchestrator.js.map +1 -0
  135. package/dist/meeting/prompt-builder.d.ts +12 -0
  136. package/dist/meeting/prompt-builder.js +96 -0
  137. package/dist/meeting/prompt-builder.js.map +1 -0
  138. package/dist/meeting/role-assigner.d.ts +2 -0
  139. package/dist/meeting/role-assigner.js +25 -0
  140. package/dist/meeting/role-assigner.js.map +1 -0
  141. package/dist/meeting/types.d.ts +21 -0
  142. package/dist/meeting/types.js +2 -0
  143. package/dist/meeting/types.js.map +1 -0
  144. package/dist/meeting-manager.d.ts +10 -0
  145. package/dist/meeting-manager.js +38 -0
  146. package/dist/meeting-manager.js.map +1 -0
  147. package/dist/memory-layer.d.ts +13 -0
  148. package/dist/memory-layer.js +44 -0
  149. package/dist/memory-layer.js.map +1 -0
  150. package/dist/plugin/agend/.claude-plugin/plugin.json +5 -0
  151. package/dist/plugin/agend/.mcp.json +9 -0
  152. package/dist/plugin/ccd-channel/.claude-plugin/plugin.json +5 -0
  153. package/dist/plugin/ccd-channel/.mcp.json +9 -0
  154. package/dist/process-manager.d.ts +31 -0
  155. package/dist/process-manager.js +264 -0
  156. package/dist/process-manager.js.map +1 -0
  157. package/dist/scheduler/db.d.ts +16 -0
  158. package/dist/scheduler/db.js +132 -0
  159. package/dist/scheduler/db.js.map +1 -0
  160. package/dist/scheduler/db.test.d.ts +1 -0
  161. package/dist/scheduler/db.test.js +92 -0
  162. package/dist/scheduler/db.test.js.map +1 -0
  163. package/dist/scheduler/index.d.ts +4 -0
  164. package/dist/scheduler/index.js +4 -0
  165. package/dist/scheduler/index.js.map +1 -0
  166. package/dist/scheduler/scheduler.d.ts +25 -0
  167. package/dist/scheduler/scheduler.js +119 -0
  168. package/dist/scheduler/scheduler.js.map +1 -0
  169. package/dist/scheduler/scheduler.test.d.ts +1 -0
  170. package/dist/scheduler/scheduler.test.js +119 -0
  171. package/dist/scheduler/scheduler.test.js.map +1 -0
  172. package/dist/scheduler/types.d.ts +47 -0
  173. package/dist/scheduler/types.js +7 -0
  174. package/dist/scheduler/types.js.map +1 -0
  175. package/dist/service-installer.d.ts +14 -0
  176. package/dist/service-installer.js +91 -0
  177. package/dist/service-installer.js.map +1 -0
  178. package/dist/setup-wizard.d.ts +14 -0
  179. package/dist/setup-wizard.js +517 -0
  180. package/dist/setup-wizard.js.map +1 -0
  181. package/dist/stt.d.ts +10 -0
  182. package/dist/stt.js +33 -0
  183. package/dist/stt.js.map +1 -0
  184. package/dist/tmux-manager.d.ts +22 -0
  185. package/dist/tmux-manager.js +131 -0
  186. package/dist/tmux-manager.js.map +1 -0
  187. package/dist/topic-commands.d.ts +22 -0
  188. package/dist/topic-commands.js +176 -0
  189. package/dist/topic-commands.js.map +1 -0
  190. package/dist/transcript-monitor.d.ts +21 -0
  191. package/dist/transcript-monitor.js +149 -0
  192. package/dist/transcript-monitor.js.map +1 -0
  193. package/dist/types.d.ts +153 -0
  194. package/dist/types.js +2 -0
  195. package/dist/types.js.map +1 -0
  196. package/dist/webhook-emitter.d.ts +15 -0
  197. package/dist/webhook-emitter.js +41 -0
  198. package/dist/webhook-emitter.js.map +1 -0
  199. package/package.json +60 -4
  200. package/templates/launchd.plist.ejs +29 -0
  201. package/templates/systemd.service.ejs +15 -0
  202. package/index.js +0 -1
package/dist/cli.js ADDED
@@ -0,0 +1,782 @@
1
+ #!/usr/bin/env node
2
+ import { Command } from "commander";
3
+ import { join, dirname } from "node:path";
4
+ import { SchedulerDb } from "./scheduler/db.js";
5
+ import { Cron } from "croner";
6
+ import { existsSync, readFileSync, writeFileSync, unlinkSync, readdirSync, rmSync, } from "node:fs";
7
+ import { homedir } from "node:os";
8
+ import { fileURLToPath } from "node:url";
9
+ import { execSync } from "node:child_process";
10
+ const __filename = fileURLToPath(import.meta.url);
11
+ const __dirname = dirname(__filename);
12
+ const DATA_DIR = join(homedir(), ".agend");
13
+ const FLEET_CONFIG_PATH = join(DATA_DIR, "fleet.yaml");
14
+ const program = new Command();
15
+ // Read version from package.json at build time
16
+ const pkgVersion = (() => {
17
+ try {
18
+ const pkgPath = join(__dirname, "..", "package.json");
19
+ return JSON.parse(readFileSync(pkgPath, "utf-8")).version ?? "0.0.0";
20
+ }
21
+ catch {
22
+ return "0.0.0";
23
+ }
24
+ })();
25
+ program
26
+ .name("ccd")
27
+ .description("Claude Channel Daemon")
28
+ .version(pkgVersion);
29
+ function signalFleetReload() {
30
+ const pidPath = join(DATA_DIR, "fleet.pid");
31
+ try {
32
+ const pid = parseInt(readFileSync(pidPath, "utf-8").trim(), 10);
33
+ process.kill(pid, "SIGHUP");
34
+ console.log("Fleet manager notified to reload schedules.");
35
+ }
36
+ catch {
37
+ console.log("Fleet manager not running. Schedules will be loaded on next start.");
38
+ }
39
+ }
40
+ // === Fleet commands ===
41
+ const fleet = program.command("fleet").description("Fleet management");
42
+ fleet
43
+ .command("start")
44
+ .description("Start fleet or specific instance")
45
+ .argument("[instance]", "Specific instance to start")
46
+ .action(async (instance) => {
47
+ const { FleetManager } = await import("./fleet-manager.js");
48
+ const fm = new FleetManager(DATA_DIR);
49
+ if (instance) {
50
+ const config = fm.loadConfig(FLEET_CONFIG_PATH);
51
+ const inst = config.instances[instance];
52
+ if (!inst) {
53
+ console.error(`Instance "${instance}" not found in fleet config`);
54
+ process.exit(1);
55
+ }
56
+ const topicMode = config.channel?.mode === "topic";
57
+ await fm.startInstance(instance, inst, topicMode);
58
+ }
59
+ else {
60
+ await fm.startAll(FLEET_CONFIG_PATH);
61
+ }
62
+ console.log("Fleet started");
63
+ // Keep process alive + clean shutdown on Ctrl+C
64
+ const shutdown = async () => {
65
+ console.log("\nStopping fleet...");
66
+ await fm.stopAll();
67
+ process.exit(0);
68
+ };
69
+ process.on("SIGINT", shutdown);
70
+ process.on("SIGTERM", shutdown);
71
+ process.on("uncaughtException", async (err) => {
72
+ console.error("Uncaught exception:", err);
73
+ await fm.stopAll().catch(() => { });
74
+ process.exit(1);
75
+ });
76
+ process.on("unhandledRejection", async (err) => {
77
+ const msg = err instanceof Error ? err.message : String(err);
78
+ // 409 = another bot poller exists — adapter handles retry, don't crash
79
+ if (msg.includes("409") && msg.includes("getUpdates")) {
80
+ console.error("Bot polling conflict (409) — retrying...");
81
+ return;
82
+ }
83
+ console.error("Unhandled rejection:", err);
84
+ await fm.stopAll().catch(() => { });
85
+ process.exit(1);
86
+ });
87
+ });
88
+ fleet
89
+ .command("stop")
90
+ .description("Stop fleet or specific instance")
91
+ .argument("[instance]", "Specific instance to stop")
92
+ .action(async (instance) => {
93
+ if (instance) {
94
+ const { FleetManager } = await import("./fleet-manager.js");
95
+ const fm = new FleetManager(DATA_DIR);
96
+ await fm.stopInstance(instance);
97
+ console.log("Stopped");
98
+ }
99
+ else {
100
+ const pidPath = join(DATA_DIR, "fleet.pid");
101
+ if (!existsSync(pidPath)) {
102
+ console.error("Fleet is not running (no PID file found)");
103
+ process.exit(1);
104
+ }
105
+ const pid = parseInt(readFileSync(pidPath, "utf-8").trim(), 10);
106
+ try {
107
+ process.kill(pid, "SIGTERM");
108
+ }
109
+ catch {
110
+ console.error("Failed to send SIGTERM (process may have already exited)");
111
+ try {
112
+ unlinkSync(pidPath);
113
+ }
114
+ catch { }
115
+ process.exit(1);
116
+ }
117
+ // Wait for process exit (up to 10 seconds)
118
+ const deadline = Date.now() + 10_000;
119
+ while (Date.now() < deadline) {
120
+ try {
121
+ process.kill(pid, 0);
122
+ await new Promise(r => setTimeout(r, 200));
123
+ }
124
+ catch {
125
+ // Process exited
126
+ console.log("Fleet stopped");
127
+ return;
128
+ }
129
+ }
130
+ console.warn("Warning: fleet process still running after 10s");
131
+ }
132
+ });
133
+ fleet
134
+ .command("restart")
135
+ .description("Graceful restart: wait for instances to idle, then restart")
136
+ .option("--reload", "Full process restart to load new code")
137
+ .action(async (opts) => {
138
+ const pidPath = join(DATA_DIR, "fleet.pid");
139
+ if (!existsSync(pidPath)) {
140
+ console.error("Fleet is not running (no PID file found)");
141
+ process.exit(1);
142
+ }
143
+ const pid = parseInt(readFileSync(pidPath, "utf-8").trim(), 10);
144
+ if (opts.reload) {
145
+ // Check if managed by launchd — if so, just signal and let launchd restart
146
+ let managedByLaunchd = false;
147
+ try {
148
+ const ppid = parseInt(execSync(`ps -o ppid= -p ${pid}`).toString().trim(), 10);
149
+ managedByLaunchd = ppid === 1;
150
+ }
151
+ catch { /* ignore */ }
152
+ try {
153
+ process.kill(pid, "SIGUSR1");
154
+ }
155
+ catch {
156
+ console.error("Failed to send reload signal (process may have exited)");
157
+ process.exit(1);
158
+ }
159
+ if (managedByLaunchd) {
160
+ console.log("Fleet is managed by launchd — sent reload signal.");
161
+ console.log("launchd will automatically restart with new code.");
162
+ // Wait briefly for old process to exit, then confirm new one started
163
+ const deadline = Date.now() + 6 * 60 * 1000;
164
+ while (Date.now() < deadline) {
165
+ try {
166
+ process.kill(pid, 0);
167
+ await new Promise(r => setTimeout(r, 500));
168
+ }
169
+ catch {
170
+ break;
171
+ }
172
+ }
173
+ // Wait for launchd to start new process
174
+ await new Promise(r => setTimeout(r, 2000));
175
+ if (existsSync(pidPath)) {
176
+ const newPid = parseInt(readFileSync(pidPath, "utf-8").trim(), 10);
177
+ if (newPid !== pid) {
178
+ console.log(`New fleet process started (PID ${newPid})`);
179
+ }
180
+ }
181
+ return;
182
+ }
183
+ console.log("Full restart signal sent — waiting for fleet to stop...");
184
+ // Wait for old process to exit (up to 6 minutes: 5 min idle wait + buffer)
185
+ const deadline = Date.now() + 6 * 60 * 1000;
186
+ while (Date.now() < deadline) {
187
+ try {
188
+ process.kill(pid, 0); // check if alive
189
+ await new Promise(r => setTimeout(r, 500));
190
+ }
191
+ catch {
192
+ break; // process exited
193
+ }
194
+ }
195
+ // Verify it actually exited
196
+ try {
197
+ process.kill(pid, 0);
198
+ console.error("Old fleet process still running after timeout — aborting");
199
+ process.exit(1);
200
+ }
201
+ catch {
202
+ // Good, it exited
203
+ }
204
+ console.log("Old fleet stopped. Starting with new code...");
205
+ // Start new fleet in this process (new Node.js process = new code loaded)
206
+ const { FleetManager } = await import("./fleet-manager.js");
207
+ const fm = new FleetManager(DATA_DIR);
208
+ await fm.startAll(FLEET_CONFIG_PATH);
209
+ console.log("Fleet restarted with new code");
210
+ // Keep process alive (same as fleet start)
211
+ const shutdown = async () => {
212
+ console.log("\nStopping fleet...");
213
+ await fm.stopAll();
214
+ process.exit(0);
215
+ };
216
+ process.on("SIGINT", shutdown);
217
+ process.on("SIGTERM", shutdown);
218
+ process.on("uncaughtException", async (err) => {
219
+ console.error("Uncaught exception:", err);
220
+ await fm.stopAll().catch(() => { });
221
+ process.exit(1);
222
+ });
223
+ process.on("unhandledRejection", async (err) => {
224
+ const msg = err instanceof Error ? err.message : String(err);
225
+ if (msg.includes("409") && msg.includes("getUpdates")) {
226
+ console.error("Bot polling conflict (409) — retrying...");
227
+ return;
228
+ }
229
+ console.error("Unhandled rejection:", err);
230
+ await fm.stopAll().catch(() => { });
231
+ process.exit(1);
232
+ });
233
+ }
234
+ else {
235
+ // Instance-only restart (existing behavior)
236
+ try {
237
+ process.kill(pid, "SIGUSR2");
238
+ console.log("Graceful restart signal sent — fleet will restart when all instances are idle");
239
+ }
240
+ catch {
241
+ console.error("Failed to send restart signal (process may have exited)");
242
+ process.exit(1);
243
+ }
244
+ }
245
+ });
246
+ fleet
247
+ .command("status")
248
+ .description("Show fleet status")
249
+ .action(async () => {
250
+ const { FleetManager } = await import("./fleet-manager.js");
251
+ const fm = new FleetManager(DATA_DIR);
252
+ const config = fm.loadConfig(FLEET_CONFIG_PATH);
253
+ const names = Object.keys(config.instances);
254
+ const nameWidth = Math.max(20, ...names.map(n => n.length + 2));
255
+ console.log("Instance".padEnd(nameWidth) + "Status".padEnd(10) + "Context".padEnd(10) + "Cost".padEnd(10) + "Topic");
256
+ console.log("\u2500".repeat(nameWidth + 40));
257
+ for (const [name, inst] of Object.entries(config.instances)) {
258
+ const status = fm.getInstanceStatus(name);
259
+ const topic = inst.topic_id ? `#${inst.topic_id}` : "(DM)";
260
+ // Read statusline.json for context usage and cost
261
+ let contextStr = "-";
262
+ let costStr = "-";
263
+ const statusFile = join(DATA_DIR, "instances", name, "statusline.json");
264
+ try {
265
+ if (existsSync(statusFile)) {
266
+ const data = JSON.parse(readFileSync(statusFile, "utf-8"));
267
+ if (data.context_window?.used_percentage != null) {
268
+ contextStr = `${Math.round(data.context_window.used_percentage)}%`;
269
+ }
270
+ if (data.cost?.total_cost_usd != null) {
271
+ costStr = `$${data.cost.total_cost_usd.toFixed(2)}`;
272
+ }
273
+ }
274
+ }
275
+ catch { /* ignore read errors */ }
276
+ console.log(name.padEnd(nameWidth) +
277
+ status.padEnd(10) +
278
+ contextStr.padEnd(10) +
279
+ costStr.padEnd(10) +
280
+ topic);
281
+ }
282
+ });
283
+ fleet
284
+ .command("logs")
285
+ .description("Show instance logs")
286
+ .argument("<instance>", "Instance name")
287
+ .option("-n, --lines <count>", "Number of lines to show", "50")
288
+ .action((instance, opts) => {
289
+ const logPath = join(DATA_DIR, "instances", instance, "daemon.log");
290
+ if (!existsSync(logPath)) {
291
+ console.error(`No logs found for instance "${instance}"`);
292
+ process.exit(1);
293
+ }
294
+ const content = readFileSync(logPath, "utf-8");
295
+ const lines = content.trim().split("\n");
296
+ const n = parseInt(opts.lines, 10);
297
+ console.log(lines.slice(-n).join("\n"));
298
+ });
299
+ fleet
300
+ .command("history")
301
+ .description("Show fleet event history")
302
+ .option("--instance <name>", "Filter by instance name")
303
+ .option("--type <type>", "Filter by event type")
304
+ .option("--since <date>", "Filter events since date (ISO format)")
305
+ .option("--limit <n>", "Number of events to show", "50")
306
+ .option("--json", "Output as JSON")
307
+ .action(async (opts) => {
308
+ const { EventLog } = await import("./event-log.js");
309
+ const evLog = new EventLog(join(DATA_DIR, "events.db"));
310
+ try {
311
+ const rows = evLog.query({
312
+ instance: opts.instance,
313
+ type: opts.type,
314
+ since: opts.since,
315
+ limit: parseInt(opts.limit, 10),
316
+ });
317
+ if (opts.json) {
318
+ console.log(JSON.stringify(rows, null, 2));
319
+ return;
320
+ }
321
+ if (rows.length === 0) {
322
+ console.log("No events found.");
323
+ return;
324
+ }
325
+ const instWidth = Math.max(20, ...rows.map(r => r.instance_name.length + 2));
326
+ console.log("Time".padEnd(22) + "Instance".padEnd(instWidth) + "Type".padEnd(25) + "Payload");
327
+ console.log("\u2500".repeat(22 + instWidth + 25 + 23));
328
+ for (const r of rows) {
329
+ const payloadStr = r.payload != null ? JSON.stringify(r.payload) : "";
330
+ console.log(r.created_at.padEnd(22) +
331
+ r.instance_name.padEnd(instWidth) +
332
+ r.event_type.padEnd(25) +
333
+ payloadStr);
334
+ }
335
+ }
336
+ finally {
337
+ evLog.close();
338
+ }
339
+ });
340
+ fleet
341
+ .command("cleanup")
342
+ .description("Remove orphaned instance directories not in fleet.yaml")
343
+ .option("--dry-run", "List orphans without deleting")
344
+ .action(async (opts) => {
345
+ const { FleetManager } = await import("./fleet-manager.js");
346
+ const fm = new FleetManager(DATA_DIR);
347
+ const config = fm.loadConfig(FLEET_CONFIG_PATH);
348
+ const configuredNames = new Set(Object.keys(config.instances));
349
+ const instancesDir = join(DATA_DIR, "instances");
350
+ if (!existsSync(instancesDir)) {
351
+ console.log("No instances directory.");
352
+ return;
353
+ }
354
+ const dirs = readdirSync(instancesDir).filter(d => !configuredNames.has(d));
355
+ if (dirs.length === 0) {
356
+ console.log("No orphaned directories.");
357
+ return;
358
+ }
359
+ console.log(`Found ${dirs.length} orphaned instance directories:`);
360
+ for (const d of dirs)
361
+ console.log(` ${d}`);
362
+ if (opts.dryRun)
363
+ return;
364
+ for (const d of dirs) {
365
+ rmSync(join(instancesDir, d), { recursive: true, force: true });
366
+ console.log(` Removed: ${d}`);
367
+ }
368
+ console.log(`Cleaned up ${dirs.length} directories.`);
369
+ // Clean stale files from active instances
370
+ const staleFiles = ["memory.db", "sandbox-bash"];
371
+ let staleCount = 0;
372
+ for (const name of configuredNames) {
373
+ const instDir = join(instancesDir, name);
374
+ for (const f of staleFiles) {
375
+ const p = join(instDir, f);
376
+ if (existsSync(p)) {
377
+ if (!opts.dryRun)
378
+ rmSync(p, { force: true });
379
+ staleCount++;
380
+ }
381
+ }
382
+ }
383
+ if (staleCount > 0)
384
+ console.log(`Removed ${staleCount} stale files (memory.db, sandbox-bash).`);
385
+ });
386
+ // === Topic commands ===
387
+ const topic = program.command("topic").description("Topic binding management");
388
+ topic
389
+ .command("list")
390
+ .description("List topic bindings")
391
+ .action(async () => {
392
+ const { loadFleetConfig } = await import("./config.js");
393
+ const config = loadFleetConfig(FLEET_CONFIG_PATH);
394
+ let found = false;
395
+ for (const [name, inst] of Object.entries(config.instances)) {
396
+ if (inst.topic_id != null) {
397
+ console.log(`${name} \u2192 topic #${inst.topic_id}`);
398
+ found = true;
399
+ }
400
+ }
401
+ if (!found) {
402
+ console.log("No topic bindings configured");
403
+ }
404
+ });
405
+ topic
406
+ .command("bind")
407
+ .description("Bind an instance to a topic")
408
+ .argument("<instance>", "Instance name")
409
+ .argument("<topic-id>", "Topic ID")
410
+ .action(async (instance, topicId) => {
411
+ const { loadFleetConfig } = await import("./config.js");
412
+ const yaml = await import("js-yaml");
413
+ const config = loadFleetConfig(FLEET_CONFIG_PATH);
414
+ if (!config.instances[instance]) {
415
+ console.error(`Instance "${instance}" not found in fleet config`);
416
+ process.exit(1);
417
+ }
418
+ config.instances[instance].topic_id = parseInt(topicId, 10);
419
+ writeFileSync(FLEET_CONFIG_PATH, yaml.dump(config));
420
+ console.log(`Bound ${instance} \u2192 topic #${topicId}`);
421
+ });
422
+ topic
423
+ .command("unbind")
424
+ .description("Unbind an instance from its topic")
425
+ .argument("<instance>", "Instance name")
426
+ .action(async (instance) => {
427
+ const { loadFleetConfig } = await import("./config.js");
428
+ const yaml = await import("js-yaml");
429
+ const config = loadFleetConfig(FLEET_CONFIG_PATH);
430
+ if (!config.instances[instance]) {
431
+ console.error(`Instance "${instance}" not found in fleet config`);
432
+ process.exit(1);
433
+ }
434
+ delete config.instances[instance].topic_id;
435
+ writeFileSync(FLEET_CONFIG_PATH, yaml.dump(config));
436
+ console.log(`Unbound ${instance} from topic`);
437
+ });
438
+ // === Access commands ===
439
+ const access = program
440
+ .command("access")
441
+ .description("Access control for instances");
442
+ async function resolveAccessPath(instance) {
443
+ const { loadFleetConfig } = await import("./config.js");
444
+ const { resolveAccessPathFromConfig } = await import("./access-path.js");
445
+ const config = loadFleetConfig(FLEET_CONFIG_PATH);
446
+ const inst = config.instances[instance];
447
+ return resolveAccessPathFromConfig(DATA_DIR, instance, config.channel);
448
+ }
449
+ access
450
+ .command("lock")
451
+ .description("Lock instance access")
452
+ .argument("<instance>", "Instance name")
453
+ .action(async (instance) => {
454
+ const { AccessManager } = await import("./channel/access-manager.js");
455
+ const instanceDir = join(DATA_DIR, "instances", instance);
456
+ if (!existsSync(instanceDir)) {
457
+ console.error(`Instance "${instance}" not found`);
458
+ process.exit(1);
459
+ }
460
+ const statePath = await resolveAccessPath(instance);
461
+ const am = new AccessManager({ mode: "locked", allowed_users: [], max_pending_codes: 5, code_expiry_minutes: 10 }, statePath);
462
+ am.setMode("locked");
463
+ console.log(`${instance}: locked`);
464
+ });
465
+ access
466
+ .command("unlock")
467
+ .description("Unlock instance access")
468
+ .argument("<instance>", "Instance name")
469
+ .action(async (instance) => {
470
+ const { AccessManager } = await import("./channel/access-manager.js");
471
+ const instanceDir = join(DATA_DIR, "instances", instance);
472
+ if (!existsSync(instanceDir)) {
473
+ console.error(`Instance "${instance}" not found`);
474
+ process.exit(1);
475
+ }
476
+ const statePath = await resolveAccessPath(instance);
477
+ const am = new AccessManager({ mode: "pairing", allowed_users: [], max_pending_codes: 5, code_expiry_minutes: 10 }, statePath);
478
+ am.setMode("pairing");
479
+ console.log(`${instance}: unlocked`);
480
+ });
481
+ access
482
+ .command("list")
483
+ .description("List allowed users for an instance")
484
+ .argument("<instance>", "Instance name")
485
+ .action(async (instance) => {
486
+ const { AccessManager } = await import("./channel/access-manager.js");
487
+ const instanceDir = join(DATA_DIR, "instances", instance);
488
+ if (!existsSync(instanceDir)) {
489
+ console.error(`Instance "${instance}" not found`);
490
+ process.exit(1);
491
+ }
492
+ const statePath = await resolveAccessPath(instance);
493
+ const am = new AccessManager({ mode: "pairing", allowed_users: [], max_pending_codes: 5, code_expiry_minutes: 10 }, statePath);
494
+ const users = am.getAllowedUsers();
495
+ if (users.length === 0) {
496
+ console.log(`${instance}: no allowed users`);
497
+ }
498
+ else {
499
+ console.log(`${instance} allowed users:`);
500
+ for (const uid of users) {
501
+ console.log(` - ${uid}`);
502
+ }
503
+ }
504
+ });
505
+ access
506
+ .command("remove")
507
+ .description("Remove a user from allowed list")
508
+ .argument("<instance>", "Instance name")
509
+ .argument("<user-id>", "User ID to remove")
510
+ .action(async (instance, userId) => {
511
+ const { AccessManager } = await import("./channel/access-manager.js");
512
+ const instanceDir = join(DATA_DIR, "instances", instance);
513
+ if (!existsSync(instanceDir)) {
514
+ console.error(`Instance "${instance}" not found`);
515
+ process.exit(1);
516
+ }
517
+ const statePath = await resolveAccessPath(instance);
518
+ const am = new AccessManager({ mode: "pairing", allowed_users: [], max_pending_codes: 5, code_expiry_minutes: 10 }, statePath);
519
+ am.removeUser(parseInt(userId, 10));
520
+ console.log(`${instance}: removed user ${userId}`);
521
+ });
522
+ access
523
+ .command("pair")
524
+ .description("Generate a pairing code for a user")
525
+ .argument("<instance>", "Instance name")
526
+ .argument("<user-id>", "Telegram user ID requesting pairing")
527
+ .action(async (instance, userId) => {
528
+ const { AccessManager } = await import("./channel/access-manager.js");
529
+ const instanceDir = join(DATA_DIR, "instances", instance);
530
+ if (!existsSync(instanceDir)) {
531
+ console.error(`Instance "${instance}" not found`);
532
+ process.exit(1);
533
+ }
534
+ const statePath = await resolveAccessPath(instance);
535
+ const am = new AccessManager({ mode: "pairing", allowed_users: [], max_pending_codes: 5, code_expiry_minutes: 10 }, statePath);
536
+ const code = am.generateCode(parseInt(userId, 10));
537
+ console.log(`${instance}: pairing code = ${code}`);
538
+ console.log("Share this code with the user. It expires in 10 minutes.");
539
+ });
540
+ // === Install/Uninstall ===
541
+ program
542
+ .command("install")
543
+ .description("Install as system service")
544
+ .option("--activate", "Stop manual fleet and load the service immediately")
545
+ .action(async (opts) => {
546
+ const { installService, activateService, detectPlatform } = await import("./service-installer.js");
547
+ const execPath = process.argv[1];
548
+ const svcPath = installService({
549
+ label: "com.agend.fleet",
550
+ execPath,
551
+ path: process.env.PATH,
552
+ workingDirectory: DATA_DIR,
553
+ logPath: join(DATA_DIR, "fleet.log"),
554
+ });
555
+ console.log(`Service installed at: ${svcPath}`);
556
+ if (opts.activate) {
557
+ const pidPath = join(DATA_DIR, "fleet.pid");
558
+ activateService(svcPath, pidPath);
559
+ console.log("Service activated.");
560
+ }
561
+ else {
562
+ const plat = detectPlatform();
563
+ if (plat === "macos") {
564
+ console.log(`Run: launchctl load ${svcPath}`);
565
+ }
566
+ else {
567
+ console.log(`Run: systemctl --user enable --now com.agend.fleet`);
568
+ }
569
+ }
570
+ });
571
+ program
572
+ .command("uninstall")
573
+ .description("Remove system service")
574
+ .action(async () => {
575
+ const { uninstallService } = await import("./service-installer.js");
576
+ const removed = uninstallService("com.agend.fleet");
577
+ if (removed) {
578
+ console.log("Service uninstalled");
579
+ }
580
+ else {
581
+ console.log("No service found to uninstall");
582
+ }
583
+ });
584
+ program
585
+ .command("init")
586
+ .description("Interactive setup wizard")
587
+ .action(async () => {
588
+ const { runSetupWizard } = await import("./setup-wizard.js");
589
+ await runSetupWizard();
590
+ });
591
+ // === Schedule commands ===
592
+ const schedule = program.command("schedule").description("Manage scheduled tasks");
593
+ schedule
594
+ .command("list")
595
+ .description("List all schedules")
596
+ .option("--target <instance>", "Filter by target instance")
597
+ .option("--json", "Output as JSON")
598
+ .action((opts) => {
599
+ const db = new SchedulerDb(join(DATA_DIR, "scheduler.db"));
600
+ try {
601
+ const schedules = db.list(opts.target);
602
+ if (opts.json) {
603
+ console.log(JSON.stringify(schedules, null, 2));
604
+ return;
605
+ }
606
+ if (schedules.length === 0) {
607
+ console.log("No schedules found.");
608
+ return;
609
+ }
610
+ console.log("ID\t\t\t\t\tLabel\t\t\tCron\t\tTarget\tEnabled\tLast Status");
611
+ for (const s of schedules) {
612
+ console.log(`${s.id}\t${s.label ?? "-"}\t${s.cron}\t${s.target}\t${s.enabled ? "✅" : "❌"}\t${s.last_status ?? "-"}`);
613
+ }
614
+ }
615
+ finally {
616
+ db.close();
617
+ }
618
+ });
619
+ schedule
620
+ .command("add")
621
+ .description("Add a new schedule")
622
+ .requiredOption("--cron <expr>", "Cron expression")
623
+ .requiredOption("--target <instance>", "Target instance")
624
+ .requiredOption("--message <text>", "Message to send on trigger")
625
+ .option("--label <text>", "Human-readable name")
626
+ .option("--timezone <tz>", "IANA timezone", "Asia/Taipei")
627
+ .action((opts) => {
628
+ // Validate cron expression
629
+ try {
630
+ new Cron(opts.cron, { timezone: opts.timezone });
631
+ }
632
+ catch (err) {
633
+ console.error(`Invalid cron expression: ${err.message}`);
634
+ process.exit(1);
635
+ }
636
+ const db = new SchedulerDb(join(DATA_DIR, "scheduler.db"));
637
+ try {
638
+ const s = db.create({
639
+ cron: opts.cron,
640
+ message: opts.message,
641
+ source: opts.target,
642
+ target: opts.target,
643
+ reply_chat_id: "",
644
+ reply_thread_id: null,
645
+ label: opts.label,
646
+ timezone: opts.timezone,
647
+ });
648
+ console.log(`Created schedule ${s.id}`);
649
+ signalFleetReload();
650
+ }
651
+ finally {
652
+ db.close();
653
+ }
654
+ });
655
+ schedule
656
+ .command("update")
657
+ .description("Update an existing schedule")
658
+ .argument("<id>", "Schedule ID")
659
+ .option("--cron <expr>", "New cron expression")
660
+ .option("--message <text>", "New message")
661
+ .option("--target <instance>", "New target instance")
662
+ .option("--label <text>", "New label")
663
+ .option("--timezone <tz>", "New timezone")
664
+ .option("--enabled <bool>", "Enable/disable (true/false)")
665
+ .action((id, opts) => {
666
+ const db = new SchedulerDb(join(DATA_DIR, "scheduler.db"));
667
+ try {
668
+ const params = {};
669
+ if (opts.cron)
670
+ params.cron = opts.cron;
671
+ if (opts.message)
672
+ params.message = opts.message;
673
+ if (opts.target)
674
+ params.target = opts.target;
675
+ if (opts.label)
676
+ params.label = opts.label;
677
+ if (opts.timezone)
678
+ params.timezone = opts.timezone;
679
+ if (opts.enabled !== undefined)
680
+ params.enabled = opts.enabled === "true";
681
+ db.update(id, params);
682
+ console.log(`Updated schedule ${id}`);
683
+ signalFleetReload();
684
+ }
685
+ finally {
686
+ db.close();
687
+ }
688
+ });
689
+ schedule
690
+ .command("delete")
691
+ .description("Delete a schedule")
692
+ .argument("<id>", "Schedule ID")
693
+ .action((id) => {
694
+ const db = new SchedulerDb(join(DATA_DIR, "scheduler.db"));
695
+ try {
696
+ db.delete(id);
697
+ console.log(`Deleted schedule ${id}`);
698
+ signalFleetReload();
699
+ }
700
+ finally {
701
+ db.close();
702
+ }
703
+ });
704
+ schedule
705
+ .command("enable")
706
+ .description("Enable a schedule")
707
+ .argument("<id>", "Schedule ID")
708
+ .action((id) => {
709
+ const db = new SchedulerDb(join(DATA_DIR, "scheduler.db"));
710
+ try {
711
+ db.update(id, { enabled: true });
712
+ console.log(`Enabled schedule ${id}`);
713
+ signalFleetReload();
714
+ }
715
+ finally {
716
+ db.close();
717
+ }
718
+ });
719
+ schedule
720
+ .command("disable")
721
+ .description("Disable a schedule")
722
+ .argument("<id>", "Schedule ID")
723
+ .action((id) => {
724
+ const db = new SchedulerDb(join(DATA_DIR, "scheduler.db"));
725
+ try {
726
+ db.update(id, { enabled: false });
727
+ console.log(`Disabled schedule ${id}`);
728
+ signalFleetReload();
729
+ }
730
+ finally {
731
+ db.close();
732
+ }
733
+ });
734
+ schedule
735
+ .command("history")
736
+ .description("Show schedule run history")
737
+ .argument("<id>", "Schedule ID")
738
+ .option("--limit <n>", "Number of runs to show", "20")
739
+ .action((id, opts) => {
740
+ const db = new SchedulerDb(join(DATA_DIR, "scheduler.db"));
741
+ try {
742
+ const runs = db.getRuns(id, parseInt(opts.limit, 10));
743
+ if (runs.length === 0) {
744
+ console.log("No runs found.");
745
+ return;
746
+ }
747
+ console.log("Time\t\t\tStatus\t\t\tDetail");
748
+ for (const r of runs) {
749
+ console.log(`${r.triggered_at}\t${r.status}\t${r.detail ?? ""}`);
750
+ }
751
+ }
752
+ finally {
753
+ db.close();
754
+ }
755
+ });
756
+ schedule
757
+ .command("trigger")
758
+ .description("Manually trigger a schedule")
759
+ .argument("<id>", "Schedule ID")
760
+ .action((id) => {
761
+ console.log("Manual trigger requires fleet manager running. Use the Telegram interface instead.");
762
+ });
763
+ // === Export / Import ===
764
+ program
765
+ .command("export")
766
+ .description("Export configuration for migration to another device")
767
+ .argument("[output]", "Output file path")
768
+ .option("--full", "Include all instance data (not just config)")
769
+ .action(async (output, opts) => {
770
+ const { exportConfig } = await import("./export-import.js");
771
+ await exportConfig(DATA_DIR, output, opts?.full ?? false);
772
+ });
773
+ program
774
+ .command("import")
775
+ .description("Import configuration from an export file")
776
+ .argument("<file>", "Path to export tarball")
777
+ .action(async (file) => {
778
+ const { importConfig } = await import("./export-import.js");
779
+ await importConfig(DATA_DIR, file);
780
+ });
781
+ program.parse();
782
+ //# sourceMappingURL=cli.js.map