talon-agent 1.0.0 → 1.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +1 -0
- package/package.json +15 -11
- package/prompts/dream.md +7 -3
- package/prompts/heartbeat.md +30 -0
- package/prompts/identity.md +1 -0
- package/prompts/teams.md +3 -0
- package/prompts/telegram.md +1 -0
- package/src/__tests__/chat-settings.test.ts +108 -2
- package/src/__tests__/cleanup-registry.test.ts +58 -0
- package/src/__tests__/config.test.ts +118 -52
- package/src/__tests__/cron-store-extended.test.ts +661 -0
- package/src/__tests__/cron-store.test.ts +145 -11
- package/src/__tests__/daily-log.test.ts +224 -13
- package/src/__tests__/dispatcher.test.ts +424 -23
- package/src/__tests__/dream.test.ts +1028 -0
- package/src/__tests__/errors-extended.test.ts +428 -0
- package/src/__tests__/errors.test.ts +95 -3
- package/src/__tests__/fuzz.test.ts +87 -15
- package/src/__tests__/gateway-actions.test.ts +1174 -433
- package/src/__tests__/gateway-http.test.ts +210 -19
- package/src/__tests__/gateway-retry.test.ts +359 -0
- package/src/__tests__/gateway-withRetry-extended.test.ts +343 -0
- package/src/__tests__/graph.test.ts +830 -0
- package/src/__tests__/handlers-stream.test.ts +208 -0
- package/src/__tests__/handlers.test.ts +2539 -70
- package/src/__tests__/heartbeat.test.ts +364 -0
- package/src/__tests__/history-extended.test.ts +775 -0
- package/src/__tests__/history-persistence.test.ts +74 -19
- package/src/__tests__/history.test.ts +113 -79
- package/src/__tests__/integration.test.ts +43 -8
- package/src/__tests__/log-init.test.ts +129 -0
- package/src/__tests__/log.test.ts +23 -5
- package/src/__tests__/media-index.test.ts +317 -35
- package/src/__tests__/plugin.test.ts +314 -0
- package/src/__tests__/prompt-builder-extended.test.ts +296 -0
- package/src/__tests__/prompt-builder.test.ts +44 -9
- package/src/__tests__/sessions.test.ts +258 -4
- package/src/__tests__/storage-save-errors.test.ts +342 -0
- package/src/__tests__/teams-frontend.test.ts +526 -31
- package/src/__tests__/telegram-formatting.test.ts +82 -0
- package/src/__tests__/terminal-commands.test.ts +208 -1
- package/src/__tests__/terminal-renderer.test.ts +223 -0
- package/src/__tests__/time.test.ts +107 -0
- package/src/__tests__/workspace-migrate.test.ts +256 -0
- package/src/__tests__/workspace.test.ts +63 -1
- package/src/backend/claude-sdk/tools.ts +64 -18
- package/src/bootstrap.ts +14 -14
- package/src/cli.ts +440 -125
- package/src/core/cron.ts +20 -5
- package/src/core/dispatcher.ts +27 -9
- package/src/core/dream.ts +79 -24
- package/src/core/errors.ts +12 -2
- package/src/core/gateway-actions.ts +182 -46
- package/src/core/gateway.ts +93 -41
- package/src/core/heartbeat.ts +515 -0
- package/src/core/plugin.ts +1 -1
- package/src/core/prompt-builder.ts +1 -4
- package/src/core/pulse.ts +4 -3
- package/src/frontend/teams/actions.ts +3 -1
- package/src/frontend/teams/formatting.ts +47 -8
- package/src/frontend/teams/graph.ts +35 -11
- package/src/frontend/teams/index.ts +155 -57
- package/src/frontend/teams/tools.ts +4 -6
- package/src/frontend/telegram/actions.ts +358 -82
- package/src/frontend/telegram/admin.ts +162 -72
- package/src/frontend/telegram/callbacks.ts +16 -10
- package/src/frontend/telegram/commands.ts +37 -21
- package/src/frontend/telegram/formatting.ts +2 -4
- package/src/frontend/telegram/handlers.ts +262 -66
- package/src/frontend/telegram/index.ts +39 -14
- package/src/frontend/telegram/middleware.ts +14 -4
- package/src/frontend/telegram/userbot.ts +16 -4
- package/src/frontend/terminal/renderer.ts +1 -4
- package/src/index.ts +28 -4
- package/src/storage/chat-settings.ts +32 -9
- package/src/storage/cron-store.ts +53 -11
- package/src/storage/daily-log.ts +72 -19
- package/src/storage/history.ts +39 -21
- package/src/storage/media-index.ts +37 -12
- package/src/storage/sessions.ts +3 -2
- package/src/util/cleanup-registry.ts +34 -0
- package/src/util/config.ts +85 -23
- package/src/util/log.ts +47 -17
- package/src/util/paths.ts +10 -0
- package/src/util/time.ts +29 -6
- package/src/util/watchdog.ts +5 -1
- package/src/util/workspace.ts +51 -10
package/src/cli.ts
CHANGED
|
@@ -14,7 +14,14 @@
|
|
|
14
14
|
|
|
15
15
|
import * as p from "@clack/prompts";
|
|
16
16
|
import pc from "picocolors";
|
|
17
|
-
import {
|
|
17
|
+
import {
|
|
18
|
+
existsSync,
|
|
19
|
+
readFileSync,
|
|
20
|
+
mkdirSync,
|
|
21
|
+
watchFile,
|
|
22
|
+
writeFileSync,
|
|
23
|
+
unlinkSync,
|
|
24
|
+
} from "node:fs";
|
|
18
25
|
import { resolve } from "node:path";
|
|
19
26
|
import writeFileAtomic from "write-file-atomic";
|
|
20
27
|
import { dirs, files as pathFiles } from "./util/paths.js";
|
|
@@ -65,7 +72,9 @@ function loadConfig(): Config {
|
|
|
65
72
|
if (existsSync(CONFIG_FILE)) {
|
|
66
73
|
return { ...DEFAULTS, ...JSON.parse(readFileSync(CONFIG_FILE, "utf-8")) };
|
|
67
74
|
}
|
|
68
|
-
} catch {
|
|
75
|
+
} catch {
|
|
76
|
+
/* corrupt */
|
|
77
|
+
}
|
|
69
78
|
return { ...DEFAULTS };
|
|
70
79
|
}
|
|
71
80
|
|
|
@@ -83,7 +92,9 @@ function maskToken(token: string | undefined): string {
|
|
|
83
92
|
}
|
|
84
93
|
|
|
85
94
|
function isConfigured(config: Config): boolean {
|
|
86
|
-
const fes = Array.isArray(config.frontend)
|
|
95
|
+
const fes = Array.isArray(config.frontend)
|
|
96
|
+
? config.frontend
|
|
97
|
+
: [config.frontend];
|
|
87
98
|
return fes.every((fe) => {
|
|
88
99
|
if (fe === "telegram") return !!config.botToken;
|
|
89
100
|
if (fe === "terminal") return true;
|
|
@@ -99,19 +110,33 @@ async function runSetup(): Promise<void> {
|
|
|
99
110
|
p.intro(pc.inverse(" Setup Wizard "));
|
|
100
111
|
|
|
101
112
|
const config = loadConfig();
|
|
102
|
-
const existingFrontends = Array.isArray(config.frontend)
|
|
113
|
+
const existingFrontends = Array.isArray(config.frontend)
|
|
114
|
+
? config.frontend
|
|
115
|
+
: [config.frontend || "telegram"];
|
|
103
116
|
|
|
104
117
|
const frontendSelection = await p.multiselect({
|
|
105
118
|
message: "Frontend platforms (space to toggle, enter to confirm)",
|
|
106
119
|
initialValues: existingFrontends,
|
|
107
120
|
options: [
|
|
108
|
-
{
|
|
109
|
-
|
|
110
|
-
|
|
121
|
+
{
|
|
122
|
+
value: "telegram",
|
|
123
|
+
label: `Telegram ${pc.dim("\u2014 bot via @BotFather")}`,
|
|
124
|
+
},
|
|
125
|
+
{
|
|
126
|
+
value: "terminal",
|
|
127
|
+
label: `Terminal ${pc.dim("\u2014 local CLI chat")}`,
|
|
128
|
+
},
|
|
129
|
+
{
|
|
130
|
+
value: "teams",
|
|
131
|
+
label: `Teams ${pc.dim("\u2014 Microsoft Teams via Power Automate")}`,
|
|
132
|
+
},
|
|
111
133
|
],
|
|
112
134
|
required: true,
|
|
113
135
|
});
|
|
114
|
-
if (p.isCancel(frontendSelection)) {
|
|
136
|
+
if (p.isCancel(frontendSelection)) {
|
|
137
|
+
p.cancel("Cancelled.");
|
|
138
|
+
process.exit(0);
|
|
139
|
+
}
|
|
115
140
|
const selectedFrontends = frontendSelection as string[];
|
|
116
141
|
|
|
117
142
|
let botToken: string | undefined;
|
|
@@ -129,32 +154,56 @@ async function runSetup(): Promise<void> {
|
|
|
129
154
|
if (!v.includes(":")) return "Invalid format";
|
|
130
155
|
},
|
|
131
156
|
});
|
|
132
|
-
if (p.isCancel(token)) {
|
|
157
|
+
if (p.isCancel(token)) {
|
|
158
|
+
p.cancel("Cancelled.");
|
|
159
|
+
process.exit(0);
|
|
160
|
+
}
|
|
133
161
|
botToken = token;
|
|
134
162
|
|
|
135
|
-
adminId = await p.text({
|
|
163
|
+
adminId = (await p.text({
|
|
136
164
|
message: "Your Telegram user ID",
|
|
137
165
|
placeholder: "optional \u2014 message @userinfobot to find yours",
|
|
138
166
|
initialValue: config.adminUserId ? String(config.adminUserId) : "",
|
|
139
|
-
}) as string;
|
|
140
|
-
if (p.isCancel(adminId)) {
|
|
167
|
+
})) as string;
|
|
168
|
+
if (p.isCancel(adminId)) {
|
|
169
|
+
p.cancel("Cancelled.");
|
|
170
|
+
process.exit(0);
|
|
171
|
+
}
|
|
141
172
|
|
|
142
173
|
const wantUserbot = await p.confirm({
|
|
143
174
|
message: "Set up userbot for full history access?",
|
|
144
175
|
initialValue: !!(config.apiId && config.apiHash),
|
|
145
176
|
});
|
|
146
|
-
if (p.isCancel(wantUserbot)) {
|
|
177
|
+
if (p.isCancel(wantUserbot)) {
|
|
178
|
+
p.cancel("Cancelled.");
|
|
179
|
+
process.exit(0);
|
|
180
|
+
}
|
|
147
181
|
|
|
148
182
|
if (wantUserbot) {
|
|
149
|
-
p.note(
|
|
183
|
+
p.note(
|
|
184
|
+
"Get these from https://my.telegram.org \u2192 API development tools",
|
|
185
|
+
"Telegram API credentials",
|
|
186
|
+
);
|
|
150
187
|
const id = await p.text({
|
|
151
|
-
message: "API ID",
|
|
188
|
+
message: "API ID",
|
|
189
|
+
placeholder: "12345678",
|
|
152
190
|
initialValue: config.apiId ? String(config.apiId) : "",
|
|
153
|
-
validate: (v) => {
|
|
191
|
+
validate: (v) => {
|
|
192
|
+
if (v && isNaN(parseInt(v, 10))) return "Must be a number";
|
|
193
|
+
},
|
|
154
194
|
});
|
|
155
|
-
if (p.isCancel(id)) {
|
|
156
|
-
|
|
157
|
-
|
|
195
|
+
if (p.isCancel(id)) {
|
|
196
|
+
p.cancel("Cancelled.");
|
|
197
|
+
process.exit(0);
|
|
198
|
+
}
|
|
199
|
+
const hash = await p.text({
|
|
200
|
+
message: "API Hash",
|
|
201
|
+
initialValue: config.apiHash || "",
|
|
202
|
+
});
|
|
203
|
+
if (p.isCancel(hash)) {
|
|
204
|
+
p.cancel("Cancelled.");
|
|
205
|
+
process.exit(0);
|
|
206
|
+
}
|
|
158
207
|
if (id) apiId = parseInt(id, 10);
|
|
159
208
|
if (hash) apiHash = hash as string;
|
|
160
209
|
}
|
|
@@ -168,8 +217,8 @@ async function runSetup(): Promise<void> {
|
|
|
168
217
|
if (selectedFrontends.includes("teams")) {
|
|
169
218
|
p.note(
|
|
170
219
|
"Set up two Power Automate workflows in Teams:\n" +
|
|
171
|
-
|
|
172
|
-
|
|
220
|
+
"1. Send: 'Post to a channel when a webhook request is received' — copy the URL below\n" +
|
|
221
|
+
"2. Receive: 'When a new channel message is added' → HTTP POST to your Talon endpoint",
|
|
173
222
|
"Teams Setup",
|
|
174
223
|
);
|
|
175
224
|
|
|
@@ -179,39 +228,57 @@ async function runSetup(): Promise<void> {
|
|
|
179
228
|
initialValue: config.teamsWebhookUrl || undefined,
|
|
180
229
|
validate: (v) => {
|
|
181
230
|
if (!v) return "Webhook URL is required";
|
|
182
|
-
try {
|
|
231
|
+
try {
|
|
232
|
+
new URL(v);
|
|
233
|
+
} catch {
|
|
234
|
+
return "Must be a valid URL";
|
|
235
|
+
}
|
|
183
236
|
},
|
|
184
237
|
});
|
|
185
|
-
if (p.isCancel(url)) {
|
|
238
|
+
if (p.isCancel(url)) {
|
|
239
|
+
p.cancel("Cancelled.");
|
|
240
|
+
process.exit(0);
|
|
241
|
+
}
|
|
186
242
|
teamsWebhookUrl = url;
|
|
187
243
|
|
|
188
|
-
const secret = await p.text({
|
|
244
|
+
const secret = (await p.text({
|
|
189
245
|
message: "Webhook secret for inbound verification",
|
|
190
246
|
placeholder: "optional — shared secret to verify incoming webhooks",
|
|
191
247
|
initialValue: config.teamsWebhookSecret || "",
|
|
192
|
-
}) as string;
|
|
193
|
-
if (p.isCancel(secret)) {
|
|
248
|
+
})) as string;
|
|
249
|
+
if (p.isCancel(secret)) {
|
|
250
|
+
p.cancel("Cancelled.");
|
|
251
|
+
process.exit(0);
|
|
252
|
+
}
|
|
194
253
|
if (secret) teamsWebhookSecret = secret;
|
|
195
254
|
|
|
196
255
|
const port = await p.text({
|
|
197
256
|
message: "Webhook receiver port",
|
|
198
257
|
placeholder: "19878",
|
|
199
|
-
initialValue: config.teamsWebhookPort
|
|
258
|
+
initialValue: config.teamsWebhookPort
|
|
259
|
+
? String(config.teamsWebhookPort)
|
|
260
|
+
: "19878",
|
|
200
261
|
validate: (v) => {
|
|
201
262
|
if (!v) return "Port is required";
|
|
202
263
|
const n = parseInt(v, 10);
|
|
203
264
|
if (isNaN(n) || n < 1024 || n > 65535) return "Port must be 1024-65535";
|
|
204
265
|
},
|
|
205
266
|
});
|
|
206
|
-
if (p.isCancel(port)) {
|
|
267
|
+
if (p.isCancel(port)) {
|
|
268
|
+
p.cancel("Cancelled.");
|
|
269
|
+
process.exit(0);
|
|
270
|
+
}
|
|
207
271
|
teamsWebhookPort = parseInt(port as string, 10);
|
|
208
272
|
|
|
209
|
-
const botName = await p.text({
|
|
273
|
+
const botName = (await p.text({
|
|
210
274
|
message: "Bot display name in Teams (for echo loop prevention)",
|
|
211
275
|
placeholder: "optional — e.g. 'Talon Bot'",
|
|
212
276
|
initialValue: config.teamsBotDisplayName || "",
|
|
213
|
-
}) as string;
|
|
214
|
-
if (p.isCancel(botName)) {
|
|
277
|
+
})) as string;
|
|
278
|
+
if (p.isCancel(botName)) {
|
|
279
|
+
p.cancel("Cancelled.");
|
|
280
|
+
process.exit(0);
|
|
281
|
+
}
|
|
215
282
|
if (botName) teamsBotDisplayName = botName;
|
|
216
283
|
}
|
|
217
284
|
|
|
@@ -219,18 +286,35 @@ async function runSetup(): Promise<void> {
|
|
|
219
286
|
message: "Default model",
|
|
220
287
|
initialValue: config.model,
|
|
221
288
|
options: [
|
|
222
|
-
{
|
|
223
|
-
|
|
224
|
-
|
|
289
|
+
{
|
|
290
|
+
value: "claude-sonnet-4-6",
|
|
291
|
+
label: `Sonnet 4.6 ${pc.dim("\u2014 fast, balanced")}`,
|
|
292
|
+
},
|
|
293
|
+
{
|
|
294
|
+
value: "claude-opus-4-6",
|
|
295
|
+
label: `Opus 4.6 ${pc.dim("\u2014 smartest")}`,
|
|
296
|
+
},
|
|
297
|
+
{
|
|
298
|
+
value: "claude-haiku-4-5",
|
|
299
|
+
label: `Haiku 4.5 ${pc.dim("\u2014 fastest, cheapest")}`,
|
|
300
|
+
},
|
|
225
301
|
],
|
|
226
302
|
});
|
|
227
|
-
if (p.isCancel(model)) {
|
|
303
|
+
if (p.isCancel(model)) {
|
|
304
|
+
p.cancel("Cancelled.");
|
|
305
|
+
process.exit(0);
|
|
306
|
+
}
|
|
228
307
|
|
|
229
|
-
const pulse = !selectedFrontends.every((f) => f === "terminal")
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
308
|
+
const pulse = !selectedFrontends.every((f) => f === "terminal")
|
|
309
|
+
? await p.confirm({
|
|
310
|
+
message: "Enable pulse? (periodic group engagement)",
|
|
311
|
+
initialValue: config.pulse,
|
|
312
|
+
})
|
|
313
|
+
: false;
|
|
314
|
+
if (p.isCancel(pulse)) {
|
|
315
|
+
p.cancel("Cancelled.");
|
|
316
|
+
process.exit(0);
|
|
317
|
+
}
|
|
234
318
|
|
|
235
319
|
// ── Claude binary path ──
|
|
236
320
|
const claudeBinaryInput = await p.text({
|
|
@@ -238,11 +322,15 @@ async function runSetup(): Promise<void> {
|
|
|
238
322
|
placeholder: "leave empty for default (claude)",
|
|
239
323
|
initialValue: config.claudeBinary || "",
|
|
240
324
|
});
|
|
241
|
-
if (p.isCancel(claudeBinaryInput)) {
|
|
325
|
+
if (p.isCancel(claudeBinaryInput)) {
|
|
326
|
+
p.cancel("Cancelled.");
|
|
327
|
+
process.exit(0);
|
|
328
|
+
}
|
|
242
329
|
const claudeBinary = (claudeBinaryInput as string).trim() || undefined;
|
|
243
330
|
|
|
244
331
|
const newConfig: Config = {
|
|
245
|
-
frontend:
|
|
332
|
+
frontend:
|
|
333
|
+
selectedFrontends.length === 1 ? selectedFrontends[0] : selectedFrontends,
|
|
246
334
|
botToken: selectedFrontends.includes("telegram") ? botToken : undefined,
|
|
247
335
|
claudeBinary,
|
|
248
336
|
model: model as string,
|
|
@@ -250,14 +338,23 @@ async function runSetup(): Promise<void> {
|
|
|
250
338
|
pulse: pulse as boolean,
|
|
251
339
|
pulseIntervalMs: config.pulseIntervalMs,
|
|
252
340
|
adminUserId: adminId ? parseInt(adminId, 10) || undefined : undefined,
|
|
253
|
-
apiId,
|
|
341
|
+
apiId,
|
|
342
|
+
apiHash,
|
|
254
343
|
maxMessageLength: config.maxMessageLength,
|
|
255
344
|
plugins: config.plugins,
|
|
256
345
|
// Teams
|
|
257
|
-
teamsWebhookUrl: selectedFrontends.includes("teams")
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
346
|
+
teamsWebhookUrl: selectedFrontends.includes("teams")
|
|
347
|
+
? teamsWebhookUrl
|
|
348
|
+
: undefined,
|
|
349
|
+
teamsWebhookSecret: selectedFrontends.includes("teams")
|
|
350
|
+
? teamsWebhookSecret
|
|
351
|
+
: undefined,
|
|
352
|
+
teamsWebhookPort: selectedFrontends.includes("teams")
|
|
353
|
+
? teamsWebhookPort
|
|
354
|
+
: undefined,
|
|
355
|
+
teamsBotDisplayName: selectedFrontends.includes("teams")
|
|
356
|
+
? teamsBotDisplayName
|
|
357
|
+
: undefined,
|
|
261
358
|
};
|
|
262
359
|
|
|
263
360
|
const s = p.spinner();
|
|
@@ -268,7 +365,9 @@ async function runSetup(): Promise<void> {
|
|
|
268
365
|
p.outro(`Run ${pc.cyan(pc.bold("talon start"))} to launch Talon`);
|
|
269
366
|
|
|
270
367
|
if (selectedFrontends.includes("telegram") && apiId && apiHash) {
|
|
271
|
-
console.log(
|
|
368
|
+
console.log(
|
|
369
|
+
` ${pc.yellow("!")} Run ${pc.cyan("npx tsx src/login.ts")} to authenticate the userbot first.\n`,
|
|
370
|
+
);
|
|
272
371
|
}
|
|
273
372
|
}
|
|
274
373
|
|
|
@@ -279,11 +378,15 @@ async function showStatus(): Promise<void> {
|
|
|
279
378
|
try {
|
|
280
379
|
const resp = await fetch(HEALTH_URL, { signal: AbortSignal.timeout(2000) });
|
|
281
380
|
if (resp.ok) {
|
|
282
|
-
const h = await resp.json() as Record<string, unknown>;
|
|
381
|
+
const h = (await resp.json()) as Record<string, unknown>;
|
|
283
382
|
const ok = h.ok as boolean;
|
|
284
|
-
console.log(
|
|
383
|
+
console.log(
|
|
384
|
+
` ${ok ? pc.green("\u25CF") : pc.yellow("\u25CF")} ${pc.bold("Running")} ${ok ? pc.green("healthy") : pc.yellow("degraded")}`,
|
|
385
|
+
);
|
|
285
386
|
console.log();
|
|
286
|
-
console.log(
|
|
387
|
+
console.log(
|
|
388
|
+
` ${pc.dim("Uptime")} ${formatUptime(h.uptime as number)}`,
|
|
389
|
+
);
|
|
287
390
|
console.log(` ${pc.dim("Memory")} ${h.memory} MB`);
|
|
288
391
|
console.log(` ${pc.dim("Sessions")} ${h.sessions}`);
|
|
289
392
|
console.log(` ${pc.dim("Messages")} ${h.messages}`);
|
|
@@ -292,18 +395,30 @@ async function showStatus(): Promise<void> {
|
|
|
292
395
|
console.log(` ${pc.dim("Last active")} ${h.lastActivity}\n`);
|
|
293
396
|
return;
|
|
294
397
|
}
|
|
295
|
-
} catch {
|
|
398
|
+
} catch {
|
|
399
|
+
/* not running */
|
|
400
|
+
}
|
|
296
401
|
|
|
297
402
|
console.log(` ${pc.red("\u25CF")} ${pc.bold("Stopped")}\n`);
|
|
298
403
|
if (existsSync(CONFIG_FILE)) {
|
|
299
404
|
const config = loadConfig();
|
|
300
|
-
const fes = Array.isArray(config.frontend)
|
|
405
|
+
const fes = Array.isArray(config.frontend)
|
|
406
|
+
? config.frontend
|
|
407
|
+
: [config.frontend];
|
|
301
408
|
console.log(` ${pc.dim("Frontend")} ${fes.join(", ")}`);
|
|
302
|
-
if (fes.includes("telegram"))
|
|
303
|
-
|
|
409
|
+
if (fes.includes("telegram"))
|
|
410
|
+
console.log(
|
|
411
|
+
` ${pc.dim("Token")} ${config.botToken ? pc.green("configured") : pc.red("not set")}`,
|
|
412
|
+
);
|
|
413
|
+
if (fes.includes("teams"))
|
|
414
|
+
console.log(
|
|
415
|
+
` ${pc.dim("Teams")} ${config.teamsWebhookUrl ? pc.green("configured") : pc.red("not set")}`,
|
|
416
|
+
);
|
|
304
417
|
console.log(` ${pc.dim("Model")} ${config.model}`);
|
|
305
418
|
console.log(` ${pc.dim("Config")} ${pc.dim(CONFIG_FILE)}\n`);
|
|
306
|
-
console.log(
|
|
419
|
+
console.log(
|
|
420
|
+
` Start with ${pc.cyan("talon start")} or ${pc.cyan("talon chat")}\n`,
|
|
421
|
+
);
|
|
307
422
|
} else {
|
|
308
423
|
console.log(` Run ${pc.cyan("talon setup")} to get started.\n`);
|
|
309
424
|
}
|
|
@@ -319,58 +434,117 @@ function formatUptime(seconds: number): string {
|
|
|
319
434
|
|
|
320
435
|
async function viewConfig(): Promise<void> {
|
|
321
436
|
printBanner();
|
|
322
|
-
if (!existsSync(CONFIG_FILE)) {
|
|
437
|
+
if (!existsSync(CONFIG_FILE)) {
|
|
438
|
+
console.log(` No config found. Running setup...\n`);
|
|
439
|
+
await runSetup();
|
|
440
|
+
return;
|
|
441
|
+
}
|
|
323
442
|
const config = loadConfig();
|
|
324
443
|
p.intro(pc.inverse(" Configuration "));
|
|
325
444
|
console.log();
|
|
326
445
|
console.log(` ${pc.dim("File")} ${pc.dim(CONFIG_FILE)}`);
|
|
327
|
-
const fes = Array.isArray(config.frontend)
|
|
446
|
+
const fes = Array.isArray(config.frontend)
|
|
447
|
+
? config.frontend
|
|
448
|
+
: [config.frontend];
|
|
328
449
|
console.log(` ${pc.dim("Frontend")} ${fes.join(", ")}`);
|
|
329
450
|
if (fes.includes("telegram")) {
|
|
330
|
-
console.log(
|
|
331
|
-
|
|
332
|
-
|
|
451
|
+
console.log(
|
|
452
|
+
` ${pc.dim("Bot token")} ${maskToken(config.botToken)}`,
|
|
453
|
+
);
|
|
454
|
+
console.log(
|
|
455
|
+
` ${pc.dim("Admin")} ${config.adminUserId || pc.dim("not set")}`,
|
|
456
|
+
);
|
|
457
|
+
console.log(
|
|
458
|
+
` ${pc.dim("Userbot")} ${config.apiId ? pc.green("configured") : pc.dim("not set")}`,
|
|
459
|
+
);
|
|
333
460
|
}
|
|
334
461
|
if (fes.includes("teams")) {
|
|
335
|
-
console.log(
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
console.log(
|
|
462
|
+
console.log(
|
|
463
|
+
` ${pc.dim("Teams webhook")} ${config.teamsWebhookUrl ? pc.green("configured") : pc.red("not set")}`,
|
|
464
|
+
);
|
|
465
|
+
console.log(
|
|
466
|
+
` ${pc.dim("Teams secret")} ${config.teamsWebhookSecret ? pc.green("set") : pc.dim("not set")}`,
|
|
467
|
+
);
|
|
468
|
+
console.log(
|
|
469
|
+
` ${pc.dim("Teams port")} ${config.teamsWebhookPort || 19878}`,
|
|
470
|
+
);
|
|
471
|
+
console.log(
|
|
472
|
+
` ${pc.dim("Teams bot name")} ${config.teamsBotDisplayName || pc.dim("not set")}`,
|
|
473
|
+
);
|
|
339
474
|
}
|
|
340
|
-
if (config.claudeBinary)
|
|
475
|
+
if (config.claudeBinary)
|
|
476
|
+
console.log(
|
|
477
|
+
` ${pc.dim("Claude binary")} ${pc.green(config.claudeBinary)}`,
|
|
478
|
+
);
|
|
341
479
|
console.log(` ${pc.dim("Model")} ${config.model}`);
|
|
342
480
|
console.log(` ${pc.dim("Concurrency")} ${config.concurrency}`);
|
|
343
|
-
console.log(
|
|
344
|
-
|
|
481
|
+
console.log(
|
|
482
|
+
` ${pc.dim("Pulse")} ${config.pulse ? pc.green("on") : pc.dim("off")} ${pc.dim(`(${Math.round(config.pulseIntervalMs / 60000)}m)`)}`,
|
|
483
|
+
);
|
|
484
|
+
if (config.plugins && config.plugins.length > 0)
|
|
485
|
+
console.log(
|
|
486
|
+
` ${pc.dim("Plugins")} ${config.plugins.length} loaded`,
|
|
487
|
+
);
|
|
345
488
|
console.log();
|
|
346
|
-
const action = await p.select({
|
|
489
|
+
const action = await p.select({
|
|
490
|
+
message: "Action",
|
|
491
|
+
options: [
|
|
492
|
+
{ value: "edit", label: "Edit", hint: "re-run setup wizard" },
|
|
493
|
+
{ value: "done", label: "Done" },
|
|
494
|
+
],
|
|
495
|
+
});
|
|
347
496
|
if (action === "edit") await runSetup();
|
|
348
497
|
}
|
|
349
498
|
|
|
350
499
|
// ── Log viewer ──────────────────────────────────────────────────────────────
|
|
351
500
|
|
|
352
|
-
const LEVEL_LABELS: Record<number, string> = {
|
|
501
|
+
const LEVEL_LABELS: Record<number, string> = {
|
|
502
|
+
10: pc.dim("TRC"),
|
|
503
|
+
20: pc.dim("DBG"),
|
|
504
|
+
30: pc.blue("INF"),
|
|
505
|
+
40: pc.yellow("WRN"),
|
|
506
|
+
50: pc.red("ERR"),
|
|
507
|
+
60: pc.bgRed(pc.white("FTL")),
|
|
508
|
+
};
|
|
353
509
|
|
|
354
510
|
function formatLogLine(line: string): string {
|
|
355
511
|
try {
|
|
356
512
|
const obj = JSON.parse(line);
|
|
357
513
|
const level = LEVEL_LABELS[obj.level as number] ?? pc.dim("???");
|
|
358
|
-
const time = pc.dim(
|
|
359
|
-
|
|
514
|
+
const time = pc.dim(
|
|
515
|
+
new Date(obj.time as number).toTimeString().slice(0, 8),
|
|
516
|
+
);
|
|
517
|
+
const comp = pc.cyan(((obj.component as string) ?? "?").padEnd(10));
|
|
360
518
|
return ` ${time} ${level} ${comp} ${obj.msg}${obj.err ? pc.red(` (${obj.err})`) : ""}`;
|
|
361
|
-
} catch {
|
|
519
|
+
} catch {
|
|
520
|
+
return ` ${line}`;
|
|
521
|
+
}
|
|
362
522
|
}
|
|
363
523
|
|
|
364
524
|
async function tailLogs(): Promise<void> {
|
|
365
525
|
printBanner();
|
|
366
|
-
if (!existsSync(LOG_FILE)) {
|
|
367
|
-
|
|
526
|
+
if (!existsSync(LOG_FILE)) {
|
|
527
|
+
console.log(
|
|
528
|
+
` No log file. Start the bot first: ${pc.cyan("talon start")}\n`,
|
|
529
|
+
);
|
|
530
|
+
return;
|
|
531
|
+
}
|
|
532
|
+
console.log(
|
|
533
|
+
` ${pc.dim("Tailing")} ${pc.dim(LOG_FILE)}\n ${pc.dim("Press Ctrl+C to stop")}\n`,
|
|
534
|
+
);
|
|
368
535
|
const content = readFileSync(LOG_FILE, "utf-8");
|
|
369
536
|
const lines = content.trim().split("\n");
|
|
370
537
|
for (const line of lines.slice(-30)) console.log(formatLogLine(line));
|
|
371
538
|
let lastSize = lines.length;
|
|
372
539
|
watchFile(LOG_FILE, { interval: 500 }, () => {
|
|
373
|
-
try {
|
|
540
|
+
try {
|
|
541
|
+
const nl = readFileSync(LOG_FILE, "utf-8").trim().split("\n");
|
|
542
|
+
for (let i = lastSize; i < nl.length; i++)
|
|
543
|
+
console.log(formatLogLine(nl[i]));
|
|
544
|
+
lastSize = nl.length;
|
|
545
|
+
} catch {
|
|
546
|
+
/* ignore */
|
|
547
|
+
}
|
|
374
548
|
});
|
|
375
549
|
await new Promise(() => {});
|
|
376
550
|
}
|
|
@@ -382,15 +556,32 @@ async function runDoctor(): Promise<void> {
|
|
|
382
556
|
console.log(` ${pc.bold("Environment check")}\n`);
|
|
383
557
|
let issues = 0;
|
|
384
558
|
const major = parseInt(process.versions.node.split(".")[0], 10);
|
|
385
|
-
console.log(
|
|
559
|
+
console.log(
|
|
560
|
+
major >= 22
|
|
561
|
+
? ` ${pc.green("\u2713")} Node.js ${process.versions.node}`
|
|
562
|
+
: ` ${pc.red("\u2717")} Node.js ${process.versions.node} ${pc.dim("(need >=22)")}`,
|
|
563
|
+
);
|
|
386
564
|
if (major < 22) issues++;
|
|
387
565
|
if (existsSync(CONFIG_FILE)) {
|
|
388
566
|
const config = loadConfig();
|
|
389
|
-
const fes = Array.isArray(config.frontend)
|
|
390
|
-
|
|
567
|
+
const fes = Array.isArray(config.frontend)
|
|
568
|
+
? config.frontend
|
|
569
|
+
: [config.frontend];
|
|
570
|
+
console.log(
|
|
571
|
+
isConfigured(config)
|
|
572
|
+
? ` ${pc.green("\u2713")} Frontend: ${fes.join(", ")} (configured)`
|
|
573
|
+
: ` ${pc.red("\u2717")} Frontend not fully configured`,
|
|
574
|
+
);
|
|
391
575
|
if (!isConfigured(config)) issues++;
|
|
392
|
-
} else {
|
|
393
|
-
|
|
576
|
+
} else {
|
|
577
|
+
console.log(` ${pc.red("\u2717")} No config file`);
|
|
578
|
+
issues++;
|
|
579
|
+
}
|
|
580
|
+
console.log(
|
|
581
|
+
existsSync(dirs.root)
|
|
582
|
+
? ` ${pc.green("\u2713")} Workspace: ${pc.dim(dirs.root)}`
|
|
583
|
+
: ` ${pc.yellow("!")} Workspace missing`,
|
|
584
|
+
);
|
|
394
585
|
try {
|
|
395
586
|
const { execSync } = await import("node:child_process");
|
|
396
587
|
const doctorConfig = existsSync(CONFIG_FILE) ? loadConfig() : undefined;
|
|
@@ -399,18 +590,36 @@ async function runDoctor(): Promise<void> {
|
|
|
399
590
|
const cmd = process.platform === "win32" ? "where" : "which";
|
|
400
591
|
try {
|
|
401
592
|
execSync(`${cmd} ${doctorConfig.claudeBinary}`, { stdio: "pipe" });
|
|
402
|
-
console.log(
|
|
593
|
+
console.log(
|
|
594
|
+
` ${pc.green("\u2713")} Claude Code binary: ${pc.dim(doctorConfig.claudeBinary)}`,
|
|
595
|
+
);
|
|
403
596
|
} catch {
|
|
404
|
-
console.log(
|
|
597
|
+
console.log(
|
|
598
|
+
` ${pc.red("\u2717")} Claude Code binary not found: ${pc.dim(doctorConfig.claudeBinary)}`,
|
|
599
|
+
);
|
|
405
600
|
issues++;
|
|
406
601
|
}
|
|
407
602
|
} else {
|
|
408
|
-
execSync(process.platform === "win32" ? "where claude" : "which claude", {
|
|
603
|
+
execSync(process.platform === "win32" ? "where claude" : "which claude", {
|
|
604
|
+
stdio: "pipe",
|
|
605
|
+
});
|
|
409
606
|
console.log(` ${pc.green("\u2713")} Claude Code installed`);
|
|
410
607
|
}
|
|
411
|
-
} catch {
|
|
412
|
-
|
|
413
|
-
|
|
608
|
+
} catch {
|
|
609
|
+
console.log(` ${pc.red("\u2717")} Claude Code not found`);
|
|
610
|
+
issues++;
|
|
611
|
+
}
|
|
612
|
+
try {
|
|
613
|
+
const resp = await fetch(HEALTH_URL, { signal: AbortSignal.timeout(2000) });
|
|
614
|
+
if (resp.ok) console.log(` ${pc.green("\u2713")} Bot is running`);
|
|
615
|
+
} catch {
|
|
616
|
+
console.log(` ${pc.dim("-")} Bot is not running`);
|
|
617
|
+
}
|
|
618
|
+
console.log(
|
|
619
|
+
issues === 0
|
|
620
|
+
? `\n ${pc.green("All checks passed.")}\n`
|
|
621
|
+
: `\n ${pc.yellow(`${issues} issue(s) found.`)}\n`,
|
|
622
|
+
);
|
|
414
623
|
}
|
|
415
624
|
|
|
416
625
|
// ── Terminal chat ───────────────────────────────────────────────────────────
|
|
@@ -418,13 +627,15 @@ async function runDoctor(): Promise<void> {
|
|
|
418
627
|
async function startChat(): Promise<void> {
|
|
419
628
|
process.env.TALON_QUIET = "1";
|
|
420
629
|
|
|
421
|
-
const { bootstrap, initBackendAndDispatcher } =
|
|
630
|
+
const { bootstrap, initBackendAndDispatcher } =
|
|
631
|
+
await import("./bootstrap.js");
|
|
422
632
|
const { flushSessions } = await import("./storage/sessions.js");
|
|
423
633
|
const { flushChatSettings } = await import("./storage/chat-settings.js");
|
|
424
634
|
const { flushCronJobs } = await import("./storage/cron-store.js");
|
|
425
635
|
const { flushHistory } = await import("./storage/history.js");
|
|
426
636
|
const { flushMediaIndex } = await import("./storage/media-index.js");
|
|
427
|
-
const { createTerminalFrontend } =
|
|
637
|
+
const { createTerminalFrontend } =
|
|
638
|
+
await import("./frontend/terminal/index.js");
|
|
428
639
|
const { Gateway } = await import("./core/gateway.js");
|
|
429
640
|
|
|
430
641
|
const { config } = await bootstrap({ frontendNames: ["terminal"] });
|
|
@@ -461,23 +672,52 @@ async function mainMenu(): Promise<void> {
|
|
|
461
672
|
printBanner();
|
|
462
673
|
if (!existsSync(CONFIG_FILE) || !isConfigured(loadConfig())) {
|
|
463
674
|
p.intro(pc.inverse(" Welcome to Talon "));
|
|
464
|
-
p.note(
|
|
675
|
+
p.note(
|
|
676
|
+
"Talon is an agentic AI harness.\nSupports Telegram and Terminal.\nLet's get you set up.",
|
|
677
|
+
"First time?",
|
|
678
|
+
);
|
|
465
679
|
await runSetup();
|
|
466
680
|
return;
|
|
467
681
|
}
|
|
468
682
|
|
|
469
683
|
let running = false;
|
|
470
|
-
try {
|
|
684
|
+
try {
|
|
685
|
+
const resp = await fetch(HEALTH_URL, { signal: AbortSignal.timeout(1000) });
|
|
686
|
+
running = resp.ok;
|
|
687
|
+
} catch {
|
|
688
|
+
/* not running */
|
|
689
|
+
}
|
|
471
690
|
const config = loadConfig();
|
|
472
|
-
const statusDot = running
|
|
473
|
-
|
|
474
|
-
|
|
691
|
+
const statusDot = running
|
|
692
|
+
? `${pc.green("\u25CF")} running`
|
|
693
|
+
: `${pc.red("\u25CF")} stopped`;
|
|
694
|
+
const fes = Array.isArray(config.frontend)
|
|
695
|
+
? config.frontend
|
|
696
|
+
: [config.frontend];
|
|
697
|
+
const frontendLabel = fes
|
|
698
|
+
.map((f) =>
|
|
699
|
+
f === "telegram" ? "Telegram" : f === "teams" ? "Teams" : "Terminal",
|
|
700
|
+
)
|
|
701
|
+
.join(" + ");
|
|
475
702
|
|
|
476
703
|
const action = await p.select({
|
|
477
704
|
message: `Talon ${statusDot} ${pc.dim(`(${frontendLabel})`)}`,
|
|
478
705
|
options: [
|
|
479
|
-
...(!running
|
|
480
|
-
|
|
706
|
+
...(!running
|
|
707
|
+
? [
|
|
708
|
+
{
|
|
709
|
+
value: "start" as const,
|
|
710
|
+
label: `Start ${frontendLabel}`,
|
|
711
|
+
hint: "background daemon",
|
|
712
|
+
},
|
|
713
|
+
]
|
|
714
|
+
: []),
|
|
715
|
+
...(running
|
|
716
|
+
? [
|
|
717
|
+
{ value: "restart" as const, label: "Restart" },
|
|
718
|
+
{ value: "stop" as const, label: "Stop" },
|
|
719
|
+
]
|
|
720
|
+
: []),
|
|
481
721
|
{ value: "chat", label: "Chat in terminal", hint: "talk to Talon here" },
|
|
482
722
|
{ value: "status", label: "Status", hint: "health and stats" },
|
|
483
723
|
{ value: "config", label: "Config", hint: "view or edit" },
|
|
@@ -487,14 +727,31 @@ async function mainMenu(): Promise<void> {
|
|
|
487
727
|
});
|
|
488
728
|
if (p.isCancel(action)) process.exit(0);
|
|
489
729
|
switch (action) {
|
|
490
|
-
case "start":
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
case "
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
case "
|
|
497
|
-
|
|
730
|
+
case "start":
|
|
731
|
+
await daemonStart();
|
|
732
|
+
break;
|
|
733
|
+
case "stop":
|
|
734
|
+
daemonStop();
|
|
735
|
+
break;
|
|
736
|
+
case "restart":
|
|
737
|
+
await daemonRestart();
|
|
738
|
+
break;
|
|
739
|
+
case "chat":
|
|
740
|
+
process.chdir(PKG_ROOT);
|
|
741
|
+
await startChat();
|
|
742
|
+
break;
|
|
743
|
+
case "status":
|
|
744
|
+
await showStatus();
|
|
745
|
+
break;
|
|
746
|
+
case "config":
|
|
747
|
+
await viewConfig();
|
|
748
|
+
break;
|
|
749
|
+
case "logs":
|
|
750
|
+
await tailLogs();
|
|
751
|
+
break;
|
|
752
|
+
case "setup":
|
|
753
|
+
await runSetup();
|
|
754
|
+
break;
|
|
498
755
|
}
|
|
499
756
|
}
|
|
500
757
|
|
|
@@ -508,19 +765,30 @@ function readPid(): number | null {
|
|
|
508
765
|
const pid = parseInt(readFileSync(PID_FILE, "utf-8").trim(), 10);
|
|
509
766
|
if (!isNaN(pid) && pid > 0) return pid;
|
|
510
767
|
}
|
|
511
|
-
} catch {
|
|
768
|
+
} catch {
|
|
769
|
+
/* corrupt */
|
|
770
|
+
}
|
|
512
771
|
return null;
|
|
513
772
|
}
|
|
514
773
|
|
|
515
774
|
function isProcessRunning(pid: number): boolean {
|
|
516
|
-
try {
|
|
775
|
+
try {
|
|
776
|
+
process.kill(pid, 0);
|
|
777
|
+
return true;
|
|
778
|
+
} catch {
|
|
779
|
+
return false;
|
|
780
|
+
}
|
|
517
781
|
}
|
|
518
782
|
|
|
519
783
|
async function daemonStart(): Promise<void> {
|
|
520
784
|
const existingPid = readPid();
|
|
521
785
|
if (existingPid && isProcessRunning(existingPid)) {
|
|
522
|
-
console.log(
|
|
523
|
-
|
|
786
|
+
console.log(
|
|
787
|
+
` ${pc.yellow("!")} Talon is already running (PID ${existingPid})`,
|
|
788
|
+
);
|
|
789
|
+
console.log(
|
|
790
|
+
` Use ${pc.cyan("talon restart")} to restart, or ${pc.cyan("talon stop")} to stop.\n`,
|
|
791
|
+
);
|
|
524
792
|
return;
|
|
525
793
|
}
|
|
526
794
|
|
|
@@ -529,7 +797,14 @@ async function daemonStart(): Promise<void> {
|
|
|
529
797
|
|
|
530
798
|
// Spawn detached process with stdio piped to /dev/null
|
|
531
799
|
// Use node with tsx's ESM loader to avoid .cmd wrapper issues on Windows
|
|
532
|
-
const tsxImport = resolve(
|
|
800
|
+
const tsxImport = resolve(
|
|
801
|
+
PKG_ROOT,
|
|
802
|
+
"node_modules",
|
|
803
|
+
"tsx",
|
|
804
|
+
"dist",
|
|
805
|
+
"esm",
|
|
806
|
+
"index.mjs",
|
|
807
|
+
);
|
|
533
808
|
const child = spawn(process.execPath, ["--import", tsxImport, entryScript], {
|
|
534
809
|
cwd: PKG_ROOT,
|
|
535
810
|
detached: true,
|
|
@@ -554,13 +829,21 @@ function daemonStop(): boolean {
|
|
|
554
829
|
const pid = readPid();
|
|
555
830
|
if (!pid || !isProcessRunning(pid)) {
|
|
556
831
|
console.log(` ${pc.dim("●")} Talon is not running\n`);
|
|
557
|
-
try {
|
|
832
|
+
try {
|
|
833
|
+
unlinkSync(PID_FILE);
|
|
834
|
+
} catch {
|
|
835
|
+
/* ok */
|
|
836
|
+
}
|
|
558
837
|
return false;
|
|
559
838
|
}
|
|
560
839
|
|
|
561
840
|
process.kill(pid, "SIGTERM");
|
|
562
841
|
console.log(` ${pc.red("●")} Talon stopped (PID ${pid})`);
|
|
563
|
-
try {
|
|
842
|
+
try {
|
|
843
|
+
unlinkSync(PID_FILE);
|
|
844
|
+
} catch {
|
|
845
|
+
/* ok */
|
|
846
|
+
}
|
|
564
847
|
return true;
|
|
565
848
|
}
|
|
566
849
|
|
|
@@ -577,17 +860,43 @@ async function daemonRestart(): Promise<void> {
|
|
|
577
860
|
|
|
578
861
|
const command = process.argv[2];
|
|
579
862
|
switch (command) {
|
|
580
|
-
case "setup":
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
case "
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
case "
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
case "
|
|
590
|
-
|
|
863
|
+
case "setup":
|
|
864
|
+
runSetup();
|
|
865
|
+
break;
|
|
866
|
+
case "status":
|
|
867
|
+
showStatus();
|
|
868
|
+
break;
|
|
869
|
+
case "config":
|
|
870
|
+
viewConfig();
|
|
871
|
+
break;
|
|
872
|
+
case "logs":
|
|
873
|
+
tailLogs();
|
|
874
|
+
break;
|
|
875
|
+
case "start":
|
|
876
|
+
printBanner();
|
|
877
|
+
await daemonStart();
|
|
878
|
+
break;
|
|
879
|
+
case "stop":
|
|
880
|
+
printBanner();
|
|
881
|
+
daemonStop();
|
|
882
|
+
break;
|
|
883
|
+
case "restart":
|
|
884
|
+
printBanner();
|
|
885
|
+
await daemonRestart();
|
|
886
|
+
break;
|
|
887
|
+
case "run":
|
|
888
|
+
process.chdir(PKG_ROOT);
|
|
889
|
+
import("./index.js");
|
|
890
|
+
break;
|
|
891
|
+
case "chat":
|
|
892
|
+
process.chdir(PKG_ROOT);
|
|
893
|
+
startChat();
|
|
894
|
+
break;
|
|
895
|
+
case "doctor":
|
|
896
|
+
runDoctor();
|
|
897
|
+
break;
|
|
898
|
+
case "--help":
|
|
899
|
+
case "-h":
|
|
591
900
|
printBanner();
|
|
592
901
|
console.log(" Usage: talon [command]\n");
|
|
593
902
|
console.log(" Commands:");
|
|
@@ -602,10 +911,16 @@ switch (command) {
|
|
|
602
911
|
console.log(` ${pc.cyan("logs")} Tail log file`);
|
|
603
912
|
console.log(` ${pc.cyan("doctor")} Validate environment`);
|
|
604
913
|
console.log();
|
|
605
|
-
console.log(
|
|
914
|
+
console.log(
|
|
915
|
+
` Run ${pc.cyan("talon")} with no args for interactive menu.\n`,
|
|
916
|
+
);
|
|
917
|
+
break;
|
|
918
|
+
case undefined:
|
|
919
|
+
mainMenu();
|
|
606
920
|
break;
|
|
607
|
-
case undefined: mainMenu(); break;
|
|
608
921
|
default:
|
|
609
|
-
console.error(
|
|
922
|
+
console.error(
|
|
923
|
+
` Unknown command: ${command}\n Run ${pc.cyan("talon --help")} for usage.\n`,
|
|
924
|
+
);
|
|
610
925
|
process.exit(1);
|
|
611
926
|
}
|