talon-agent 3.0.0 → 3.0.1
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/package.json +2 -1
- package/src/cli/config.ts +4 -2
- package/src/cli/menu.ts +4 -2
- package/src/core/doctor.ts +105 -10
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "talon-agent",
|
|
3
|
-
"version": "3.0.
|
|
3
|
+
"version": "3.0.1",
|
|
4
4
|
"description": "Multi-frontend AI agent with full tool access, streaming, cron jobs, and plugin system",
|
|
5
5
|
"author": "Dylan Neve",
|
|
6
6
|
"license": "MIT",
|
|
@@ -116,6 +116,7 @@
|
|
|
116
116
|
"liquidjs": "^10.27.0",
|
|
117
117
|
"marked": "^18.0.0",
|
|
118
118
|
"mem0ai": "^3.0.13",
|
|
119
|
+
"openai": "^6.46.0",
|
|
119
120
|
"p-retry": "^8.0.0",
|
|
120
121
|
"picocolors": "^1.1.1",
|
|
121
122
|
"pino": "^10.3.1",
|
package/src/cli/config.ts
CHANGED
|
@@ -97,9 +97,11 @@ export function isConfigured(config: Config): boolean {
|
|
|
97
97
|
: [config.frontend];
|
|
98
98
|
return fes.every((fe) => {
|
|
99
99
|
if (fe === "telegram") return !!config.botToken;
|
|
100
|
-
if (fe === "terminal") return true;
|
|
101
100
|
if (fe === "teams") return !!config.teamsWebhookUrl;
|
|
102
101
|
if (fe === "discord") return !!config.discord?.botToken;
|
|
103
|
-
|
|
102
|
+
// terminal (stdio) and native (the client bridge) carry no
|
|
103
|
+
// credentials; unknown names fail closed so a typo'd frontend sends
|
|
104
|
+
// the user to setup instead of a daemon that can't start.
|
|
105
|
+
return fe === "terminal" || fe === "native";
|
|
104
106
|
});
|
|
105
107
|
}
|
package/src/cli/menu.ts
CHANGED
|
@@ -21,7 +21,7 @@ export async function mainMenu(): Promise<void> {
|
|
|
21
21
|
if (!existsSync(CONFIG_FILE) || !isConfigured(loadConfig())) {
|
|
22
22
|
p.intro(pc.inverse(" Welcome to Talon "));
|
|
23
23
|
p.note(
|
|
24
|
-
"Talon is an agentic AI harness.\
|
|
24
|
+
"Talon is an agentic AI harness.\nRuns on Telegram, Discord, Teams, Terminal,\nand the native client bridge.\nLet's get you set up.",
|
|
25
25
|
"First time?",
|
|
26
26
|
);
|
|
27
27
|
await runSetup();
|
|
@@ -44,7 +44,9 @@ export async function mainMenu(): Promise<void> {
|
|
|
44
44
|
? "Teams"
|
|
45
45
|
: f === "discord"
|
|
46
46
|
? "Discord"
|
|
47
|
-
: "
|
|
47
|
+
: f === "native"
|
|
48
|
+
? "Native"
|
|
49
|
+
: "Terminal",
|
|
48
50
|
)
|
|
49
51
|
.join(" + ");
|
|
50
52
|
|
package/src/core/doctor.ts
CHANGED
|
@@ -12,6 +12,7 @@
|
|
|
12
12
|
*/
|
|
13
13
|
|
|
14
14
|
import { existsSync } from "node:fs";
|
|
15
|
+
import { stat } from "node:fs/promises";
|
|
15
16
|
import { execFileSync } from "node:child_process";
|
|
16
17
|
import { NATIVE_MODULES } from "../native/registry.js";
|
|
17
18
|
import { dirs } from "../util/paths.js";
|
|
@@ -103,20 +104,107 @@ export async function checkNativeModules(): Promise<NativeModuleCheck[]> {
|
|
|
103
104
|
return results;
|
|
104
105
|
}
|
|
105
106
|
|
|
106
|
-
/**
|
|
107
|
-
|
|
107
|
+
/**
|
|
108
|
+
* The configured frontends that are missing their required credentials.
|
|
109
|
+
* `terminal` (stdio) and `native` (the client bridge) carry none, so they
|
|
110
|
+
* are always configured; a name doctor doesn't recognize stays a failure
|
|
111
|
+
* (fail-closed) so a typo'd frontend is surfaced instead of blessed —
|
|
112
|
+
* but it is *named* in the report either way, never a bare "not fully
|
|
113
|
+
* configured" that leaves the operator guessing which entry is at fault.
|
|
114
|
+
*/
|
|
115
|
+
function unconfiguredFrontends(config: DoctorConfigSlice): string[] {
|
|
108
116
|
const fes = Array.isArray(config.frontend)
|
|
109
117
|
? config.frontend
|
|
110
118
|
: [config.frontend];
|
|
111
|
-
return fes.
|
|
112
|
-
if (fe === "telegram") return
|
|
113
|
-
if (fe === "
|
|
114
|
-
if (fe === "
|
|
115
|
-
|
|
116
|
-
return false;
|
|
119
|
+
return fes.filter((fe) => {
|
|
120
|
+
if (fe === "telegram") return !config.botToken;
|
|
121
|
+
if (fe === "teams") return !config.teamsWebhookUrl;
|
|
122
|
+
if (fe === "discord") return !config.discord?.botToken;
|
|
123
|
+
return fe !== "terminal" && fe !== "native";
|
|
117
124
|
});
|
|
118
125
|
}
|
|
119
126
|
|
|
127
|
+
/** How long a namespace probe may take before the mount counts as wedged. */
|
|
128
|
+
const NS_PROBE_TIMEOUT_MS = 3_000;
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Probe one path without trusting it to answer: a healthy dir stats in
|
|
132
|
+
* microseconds, a stale FUSE mount from a dead daemon answers ENOTCONN,
|
|
133
|
+
* and a live-but-stuck daemon's mount HANGS the syscall — so the probe
|
|
134
|
+
* races a timeout and a hang counts as wedged rather than wedging
|
|
135
|
+
* doctor itself.
|
|
136
|
+
*/
|
|
137
|
+
async function probeNsPath(
|
|
138
|
+
path: string,
|
|
139
|
+
): Promise<"ok" | "missing" | "hang" | string> {
|
|
140
|
+
const result = await Promise.race([
|
|
141
|
+
stat(path).then(
|
|
142
|
+
() => "ok" as const,
|
|
143
|
+
(err) => {
|
|
144
|
+
const code = (err as NodeJS.ErrnoException).code;
|
|
145
|
+
return code === "ENOENT" ? ("missing" as const) : (code ?? "error");
|
|
146
|
+
},
|
|
147
|
+
),
|
|
148
|
+
new Promise<"hang">((resolve) => {
|
|
149
|
+
setTimeout(() => resolve("hang"), NS_PROBE_TIMEOUT_MS).unref();
|
|
150
|
+
}),
|
|
151
|
+
]);
|
|
152
|
+
return result;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* The namespace mountpoint (~/.talon/ns) from the outside. Doctor runs
|
|
157
|
+
* in its own process, so it can't read the daemon's in-memory FUSE
|
|
158
|
+
* status — but every failure mode shows up in the filesystem itself:
|
|
159
|
+
* missing means no daemon has booted yet, ENOTCONN or a hanging syscall
|
|
160
|
+
* means a FUSE mount wedged by a dead or stuck daemon (every consumer
|
|
161
|
+
* of the namespace is broken until it's detached), and a healthy root
|
|
162
|
+
* either has live views (proc/ answers) or is the plain symlink farm.
|
|
163
|
+
*/
|
|
164
|
+
async function checkNamespaceDir(): Promise<DoctorCheck> {
|
|
165
|
+
const root = await probeNsPath(dirs.ns);
|
|
166
|
+
if (root === "missing") {
|
|
167
|
+
return {
|
|
168
|
+
label: "Namespace dir not built yet",
|
|
169
|
+
status: "info",
|
|
170
|
+
detail: `${dirs.ns} appears at first daemon boot`,
|
|
171
|
+
};
|
|
172
|
+
}
|
|
173
|
+
if (root !== "ok") {
|
|
174
|
+
return {
|
|
175
|
+
label:
|
|
176
|
+
root === "hang"
|
|
177
|
+
? "Namespace dir wedged (syscalls hang)"
|
|
178
|
+
: `Namespace dir wedged (${root})`,
|
|
179
|
+
status: "fail",
|
|
180
|
+
detail: `stale FUSE mount at ${dirs.ns} — detach with: fusermount3 -uz ${dirs.ns} (or: umount -l ${dirs.ns})`,
|
|
181
|
+
};
|
|
182
|
+
}
|
|
183
|
+
// Root answers. proc/ is served over the FUSE bridge, so it gets its
|
|
184
|
+
// own hang-guarded probe: present = live views, absent = symlink farm.
|
|
185
|
+
const proc = await probeNsPath(`${dirs.ns}/proc`);
|
|
186
|
+
if (proc === "ok") {
|
|
187
|
+
return {
|
|
188
|
+
label: `Namespace dir: ${dirs.ns}`,
|
|
189
|
+
status: "ok",
|
|
190
|
+
detail: "live views mounted (proc/ answering)",
|
|
191
|
+
};
|
|
192
|
+
}
|
|
193
|
+
if (proc === "missing") {
|
|
194
|
+
return {
|
|
195
|
+
label: `Namespace dir: ${dirs.ns}`,
|
|
196
|
+
status: "ok",
|
|
197
|
+
detail: "symlink farm (no live views — daemon off or FUSE not mounted)",
|
|
198
|
+
};
|
|
199
|
+
}
|
|
200
|
+
return {
|
|
201
|
+
label: `Namespace live views unhealthy (proc/ ${proc === "hang" ? "hangs" : proc})`,
|
|
202
|
+
status: "warn",
|
|
203
|
+
detail: `file mounts still work; detach the mount with: fusermount3 -uz ${dirs.ns}`,
|
|
204
|
+
issue: true,
|
|
205
|
+
};
|
|
206
|
+
}
|
|
207
|
+
|
|
120
208
|
function binaryOnPath(name: string): boolean {
|
|
121
209
|
try {
|
|
122
210
|
const lookupCmd = process.platform === "win32" ? "where" : "which";
|
|
@@ -327,14 +415,19 @@ export async function collectDoctorReport(opts: {
|
|
|
327
415
|
const fes = Array.isArray(opts.config.frontend)
|
|
328
416
|
? opts.config.frontend
|
|
329
417
|
: [opts.config.frontend];
|
|
418
|
+
const missing = unconfiguredFrontends(opts.config);
|
|
330
419
|
checks.push(
|
|
331
|
-
|
|
420
|
+
missing.length === 0
|
|
332
421
|
? {
|
|
333
422
|
label: `Frontend: ${fes.join(", ")}`,
|
|
334
423
|
status: "ok",
|
|
335
424
|
detail: "configured",
|
|
336
425
|
}
|
|
337
|
-
: {
|
|
426
|
+
: {
|
|
427
|
+
label: `Frontend not fully configured: ${missing.join(", ")}`,
|
|
428
|
+
status: "fail",
|
|
429
|
+
detail: "missing credentials in talon.json (or unknown frontend)",
|
|
430
|
+
},
|
|
338
431
|
);
|
|
339
432
|
} else {
|
|
340
433
|
checks.push({ label: "No config file", status: "fail" });
|
|
@@ -346,6 +439,8 @@ export async function collectDoctorReport(opts: {
|
|
|
346
439
|
: { label: "Workspace missing", status: "warn" },
|
|
347
440
|
);
|
|
348
441
|
|
|
442
|
+
checks.push(await checkNamespaceDir());
|
|
443
|
+
|
|
349
444
|
// The warden is optional by design (built per-arch, absent on plain
|
|
350
445
|
// npm installs), so a missing binary is informational — triggers run
|
|
351
446
|
// on the in-process TS path. When present, report the live version.
|