talon-agent 1.49.0 → 1.51.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/README.md +30 -2
- package/package.json +1 -1
- package/src/backend/codex/factory.ts +2 -0
- package/src/backend/codex/handler/message.ts +10 -0
- package/src/backend/kilo/factory.ts +2 -0
- package/src/backend/kilo/handler/message.ts +10 -0
- package/src/backend/openai-agents/factory.ts +2 -0
- package/src/backend/openai-agents/handler/message.ts +10 -0
- package/src/backend/opencode/factory.ts +2 -0
- package/src/backend/opencode/handler/message.ts +10 -0
- package/src/backend/shared/index.ts +4 -0
- package/src/backend/shared/turn-interrupt.ts +61 -0
- package/src/bootstrap.ts +5 -0
- package/src/cli/daemon-api.ts +2 -1
- package/src/cli/events.ts +47 -12
- package/src/cli/fs.ts +138 -0
- package/src/cli/index.ts +47 -7
- package/src/cli/install-sources.ts +106 -0
- package/src/cli/plugin-entries.ts +104 -0
- package/src/cli/plugin.ts +494 -0
- package/src/cli/skill.ts +252 -0
- package/src/cli/tasks.ts +85 -20
- package/src/core/engine/gateway-actions/index.ts +3 -0
- package/src/core/engine/gateway-actions/native.ts +151 -24
- package/src/core/engine/gateway-actions/plugins.ts +36 -19
- package/src/core/engine/gateway-actions/vfs.ts +69 -0
- package/src/core/engine/gateway.ts +52 -0
- package/src/core/plugin/loader.ts +14 -2
- package/src/core/plugin/types.ts +4 -0
- package/src/core/tasks/table.ts +1 -1
- package/src/core/tasks/types.ts +6 -2
- package/src/core/tools/index.ts +2 -0
- package/src/core/tools/native.ts +19 -6
- package/src/core/tools/types.ts +2 -1
- package/src/core/tools/vfs.ts +61 -0
- package/src/core/vfs/index.ts +113 -0
- package/src/core/vfs/mounts/files.ts +154 -0
- package/src/core/vfs/mounts/plugins.ts +72 -0
- package/src/core/vfs/mounts/proc.ts +125 -0
- package/src/core/vfs/types.ts +103 -0
- package/src/core/vfs/vfs.ts +237 -0
- package/src/core/weaver/weaver.ts +40 -3
- package/src/frontend/native/discovery.ts +3 -0
- package/src/frontend/native/index.ts +10 -1
- package/src/frontend/native/server.ts +65 -6
- package/src/frontend/native/tls.ts +313 -0
- package/src/storage/journal.ts +91 -0
- package/src/storage/repositories/journal-repo.ts +38 -0
- package/src/storage/skill-store.ts +118 -5
- package/src/storage/sql/journal.sql +16 -0
- package/src/storage/sql/schema.sql +15 -0
- package/src/storage/sql/statements.generated.ts +24 -1
- package/src/util/config.ts +16 -0
- package/src/util/log.ts +1 -0
- package/src/util/paths.ts +5 -0
package/src/cli/index.ts
CHANGED
|
@@ -32,6 +32,9 @@ import { startChat } from "./chat.js";
|
|
|
32
32
|
import { daemonStart, daemonStop, daemonRestart } from "./daemon.js";
|
|
33
33
|
import { showTasks, killTask } from "./tasks.js";
|
|
34
34
|
import { showEvents } from "./events.js";
|
|
35
|
+
import { showLs, showCat } from "./fs.js";
|
|
36
|
+
import { runPluginCommand } from "./plugin.js";
|
|
37
|
+
import { runSkillCommand } from "./skill.js";
|
|
35
38
|
import { mainMenu } from "./menu.js";
|
|
36
39
|
|
|
37
40
|
export * from "./context.js";
|
|
@@ -60,6 +63,10 @@ const CLI_COMMANDS = [
|
|
|
60
63
|
"ps",
|
|
61
64
|
"kill",
|
|
62
65
|
"events",
|
|
66
|
+
"ls",
|
|
67
|
+
"cat",
|
|
68
|
+
"plugin",
|
|
69
|
+
"skill",
|
|
63
70
|
];
|
|
64
71
|
|
|
65
72
|
/** Route a `talon <command>` invocation. Called by the entry point. */
|
|
@@ -102,15 +109,34 @@ export async function runCli(): Promise<void> {
|
|
|
102
109
|
runDoctor();
|
|
103
110
|
break;
|
|
104
111
|
case "ps":
|
|
105
|
-
await showTasks();
|
|
112
|
+
await showTasks(process.argv[3] === "--all" || process.argv[3] === "-a");
|
|
106
113
|
break;
|
|
107
114
|
case "kill":
|
|
108
115
|
await killTask(process.argv[3]);
|
|
109
116
|
break;
|
|
110
|
-
case "events":
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
);
|
|
117
|
+
case "events": {
|
|
118
|
+
const args = process.argv.slice(3);
|
|
119
|
+
const historyAt = args.findIndex((a) => a === "--history");
|
|
120
|
+
const next = historyAt >= 0 ? Number(args[historyAt + 1]) : NaN;
|
|
121
|
+
await showEvents({
|
|
122
|
+
follow: args.includes("-f") || args.includes("--follow"),
|
|
123
|
+
...(historyAt >= 0
|
|
124
|
+
? { history: Number.isInteger(next) && next > 0 ? next : 100 }
|
|
125
|
+
: {}),
|
|
126
|
+
});
|
|
127
|
+
break;
|
|
128
|
+
}
|
|
129
|
+
case "ls":
|
|
130
|
+
await showLs(process.argv[3]);
|
|
131
|
+
break;
|
|
132
|
+
case "cat":
|
|
133
|
+
await showCat(process.argv[3]);
|
|
134
|
+
break;
|
|
135
|
+
case "plugin":
|
|
136
|
+
await runPluginCommand(process.argv.slice(3));
|
|
137
|
+
break;
|
|
138
|
+
case "skill":
|
|
139
|
+
await runSkillCommand(process.argv.slice(3));
|
|
114
140
|
break;
|
|
115
141
|
case "--version":
|
|
116
142
|
case "-v": {
|
|
@@ -129,10 +155,24 @@ export async function runCli(): Promise<void> {
|
|
|
129
155
|
console.log(` ${pc.cyan("run")} Run in foreground (attached)`);
|
|
130
156
|
console.log(` ${pc.cyan("chat")} Terminal chat mode`);
|
|
131
157
|
console.log(` ${pc.cyan("status")} Show bot health`);
|
|
132
|
-
console.log(
|
|
158
|
+
console.log(
|
|
159
|
+
` ${pc.cyan("ps")} List agent tasks (--all includes journal history)`,
|
|
160
|
+
);
|
|
133
161
|
console.log(` ${pc.cyan("kill")} Abort a killable task by id`);
|
|
134
162
|
console.log(
|
|
135
|
-
` ${pc.cyan("events")} Tail the event bus (-f follows)`,
|
|
163
|
+
` ${pc.cyan("events")} Tail the event bus (-f follows, --history [N] reads the journal)`,
|
|
164
|
+
);
|
|
165
|
+
console.log(
|
|
166
|
+
` ${pc.cyan("ls")} List the talon:// namespace (ls proc/tasks)`,
|
|
167
|
+
);
|
|
168
|
+
console.log(
|
|
169
|
+
` ${pc.cyan("cat")} Read a talon:// file (cat proc/events)`,
|
|
170
|
+
);
|
|
171
|
+
console.log(
|
|
172
|
+
` ${pc.cyan("plugin")} Manage plugins (install/enable/disable)`,
|
|
173
|
+
);
|
|
174
|
+
console.log(
|
|
175
|
+
` ${pc.cyan("skill")} Manage skills (install/enable/disable)`,
|
|
136
176
|
);
|
|
137
177
|
console.log(` ${pc.cyan("config")} View/edit configuration`);
|
|
138
178
|
console.log(` ${pc.cyan("logs")} Tail log file`);
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Install-source resolution shared by `talon plugin install` and
|
|
3
|
+
* `talon skill install`.
|
|
4
|
+
*
|
|
5
|
+
* One grammar for both commands, checked in order:
|
|
6
|
+
*
|
|
7
|
+
* 1. an existing local path → { kind: "local" }
|
|
8
|
+
* 2. a git URL (scheme, `git@`, or `.git`) → { kind: "git" }
|
|
9
|
+
* 3. `owner/repo[/subpath]` shorthand → { kind: "git" } on github.com
|
|
10
|
+
* 4. anything else → { kind: "other" } — the caller
|
|
11
|
+
* decides (plugins treat it as an npm spec, skills reject it)
|
|
12
|
+
*
|
|
13
|
+
* Cloning always uses `--depth=1` (installs never need history) and spawns
|
|
14
|
+
* `git`/`npm` via cross-spawn, which resolves the `.cmd`/`.exe` shims on
|
|
15
|
+
* Windows — never assume a POSIX shell here.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import crossSpawn from "cross-spawn";
|
|
19
|
+
import { existsSync, mkdtempSync, rmSync } from "node:fs";
|
|
20
|
+
import { tmpdir } from "node:os";
|
|
21
|
+
import { join, resolve } from "node:path";
|
|
22
|
+
|
|
23
|
+
export type ResolvedSource =
|
|
24
|
+
| { kind: "local"; dir: string }
|
|
25
|
+
| { kind: "git"; url: string; subpath?: string }
|
|
26
|
+
| { kind: "other"; raw: string };
|
|
27
|
+
|
|
28
|
+
const GIT_URL_RE = /^(https?|git|ssh):\/\//;
|
|
29
|
+
/** `owner/repo` or `owner/repo/sub/path` — never an npm scope (`@…`). */
|
|
30
|
+
const GITHUB_SHORTHAND_RE =
|
|
31
|
+
/^([A-Za-z0-9_.-]+)\/([A-Za-z0-9_.-]+)((?:\/[^\s/]+)*)$/;
|
|
32
|
+
|
|
33
|
+
export function resolveSource(raw: string): ResolvedSource {
|
|
34
|
+
const trimmed = raw.trim();
|
|
35
|
+
if (existsSync(resolve(trimmed))) {
|
|
36
|
+
return { kind: "local", dir: resolve(trimmed) };
|
|
37
|
+
}
|
|
38
|
+
if (
|
|
39
|
+
GIT_URL_RE.test(trimmed) ||
|
|
40
|
+
trimmed.startsWith("git@") ||
|
|
41
|
+
trimmed.endsWith(".git")
|
|
42
|
+
) {
|
|
43
|
+
return { kind: "git", url: trimmed };
|
|
44
|
+
}
|
|
45
|
+
if (!trimmed.startsWith("@")) {
|
|
46
|
+
const match = GITHUB_SHORTHAND_RE.exec(trimmed);
|
|
47
|
+
if (match) {
|
|
48
|
+
const [, owner, repo, rest] = match;
|
|
49
|
+
return {
|
|
50
|
+
kind: "git",
|
|
51
|
+
url: `https://github.com/${owner}/${repo}.git`,
|
|
52
|
+
...(rest ? { subpath: rest.slice(1) } : {}),
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
return { kind: "other", raw: trimmed };
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export type CommandOutcome = { ok: true } | { ok: false; error: string };
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Run a tool from PATH synchronously. `inherit` streams output to the
|
|
63
|
+
* terminal (npm installs); otherwise stderr is captured for the error.
|
|
64
|
+
*/
|
|
65
|
+
export function runTool(
|
|
66
|
+
tool: string,
|
|
67
|
+
args: string[],
|
|
68
|
+
options: { cwd?: string; inherit?: boolean } = {},
|
|
69
|
+
): CommandOutcome {
|
|
70
|
+
const result = crossSpawn.sync(tool, args, {
|
|
71
|
+
cwd: options.cwd,
|
|
72
|
+
stdio: options.inherit ? "inherit" : ["ignore", "ignore", "pipe"],
|
|
73
|
+
});
|
|
74
|
+
if (result.error) {
|
|
75
|
+
const missing = (result.error as NodeJS.ErrnoException).code === "ENOENT";
|
|
76
|
+
return {
|
|
77
|
+
ok: false,
|
|
78
|
+
error: missing
|
|
79
|
+
? `${tool} is not installed or not on PATH`
|
|
80
|
+
: result.error.message,
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
if (result.status !== 0) {
|
|
84
|
+
const stderr = result.stderr?.toString().trim();
|
|
85
|
+
return {
|
|
86
|
+
ok: false,
|
|
87
|
+
error: stderr || `${tool} ${args[0]} exited with ${result.status}`,
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
return { ok: true };
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export type CloneOutcome =
|
|
94
|
+
{ ok: true; dir: string; cleanup: () => void } | { ok: false; error: string };
|
|
95
|
+
|
|
96
|
+
/** Shallow-clone into a fresh temp directory. Caller must run `cleanup`. */
|
|
97
|
+
export function cloneShallow(url: string): CloneOutcome {
|
|
98
|
+
const dir = mkdtempSync(join(tmpdir(), "talon-install-"));
|
|
99
|
+
const cleanup = () => rmSync(dir, { recursive: true, force: true });
|
|
100
|
+
const outcome = runTool("git", ["clone", "--depth=1", url, dir]);
|
|
101
|
+
if (!outcome.ok) {
|
|
102
|
+
cleanup();
|
|
103
|
+
return { ok: false, error: `Clone failed: ${outcome.error}` };
|
|
104
|
+
}
|
|
105
|
+
return { ok: true, dir, cleanup };
|
|
106
|
+
}
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure helpers over the config `plugins` array for the `talon plugin`
|
|
3
|
+
* command group. The CLI edits config.json as JSON — entries here are the
|
|
4
|
+
* on-disk shape (`core/plugin/types.ts` PluginEntry), not loaded plugins.
|
|
5
|
+
* No filesystem or process access — everything is testable data-in/data-out.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
/** On-disk plugin entry — path-based module or standalone MCP server. */
|
|
9
|
+
export type PluginEntryJson = {
|
|
10
|
+
path?: string;
|
|
11
|
+
config?: Record<string, unknown>;
|
|
12
|
+
name?: string;
|
|
13
|
+
command?: string;
|
|
14
|
+
args?: string[];
|
|
15
|
+
env?: Record<string, string>;
|
|
16
|
+
enabled?: boolean;
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Built-in plugins toggled by their own config sections (`github.enabled`,
|
|
21
|
+
* …) rather than `plugins[]` entries — see core/plugin/builtins.ts.
|
|
22
|
+
*/
|
|
23
|
+
export const BUILTIN_PLUGINS = [
|
|
24
|
+
"github",
|
|
25
|
+
"mempalace",
|
|
26
|
+
"mem0",
|
|
27
|
+
"playwright",
|
|
28
|
+
] as const;
|
|
29
|
+
|
|
30
|
+
export function isBuiltinPlugin(name: string): boolean {
|
|
31
|
+
return (BUILTIN_PLUGINS as readonly string[]).includes(name);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** Split a config path on either separator — Windows configs carry `\`. */
|
|
35
|
+
function pathSegments(path: string): string[] {
|
|
36
|
+
return path.split(/[\\/]+/).filter(Boolean);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Display name for an entry: the MCP name, or for path entries the package
|
|
41
|
+
* name — segments after the last `node_modules` (preserving `@scope/pkg`),
|
|
42
|
+
* else the folder basename.
|
|
43
|
+
*/
|
|
44
|
+
export function entryDisplayName(entry: PluginEntryJson): string {
|
|
45
|
+
if (entry.name) return entry.name;
|
|
46
|
+
if (!entry.path) return "(invalid entry)";
|
|
47
|
+
const segments = pathSegments(entry.path);
|
|
48
|
+
const nm = segments.lastIndexOf("node_modules");
|
|
49
|
+
if (nm >= 0 && nm < segments.length - 1) {
|
|
50
|
+
return segments.slice(nm + 1).join("/");
|
|
51
|
+
}
|
|
52
|
+
return segments[segments.length - 1] ?? entry.path;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** Does `token` identify this entry? Display name, basename, or full path. */
|
|
56
|
+
export function entryMatches(entry: PluginEntryJson, token: string): boolean {
|
|
57
|
+
if (entryDisplayName(entry) === token) return true;
|
|
58
|
+
if (!entry.path) return false;
|
|
59
|
+
const segments = pathSegments(entry.path);
|
|
60
|
+
return entry.path === token || segments[segments.length - 1] === token;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export function findEntryIndex(
|
|
64
|
+
entries: PluginEntryJson[],
|
|
65
|
+
token: string,
|
|
66
|
+
): number {
|
|
67
|
+
return entries.findIndex((entry) => entryMatches(entry, token));
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Index of an entry with the same identity as `candidate` (same module
|
|
72
|
+
* path or same MCP name) — the duplicate an install must replace.
|
|
73
|
+
*/
|
|
74
|
+
export function findConflictIndex(
|
|
75
|
+
entries: PluginEntryJson[],
|
|
76
|
+
candidate: PluginEntryJson,
|
|
77
|
+
): number {
|
|
78
|
+
return entries.findIndex((entry) =>
|
|
79
|
+
candidate.path !== undefined
|
|
80
|
+
? entry.path === candidate.path
|
|
81
|
+
: entry.name !== undefined && entry.name === candidate.name,
|
|
82
|
+
);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Copy of `entry` with the enabled state applied. Enabled is the default,
|
|
87
|
+
* so enabling removes the key rather than writing `enabled: true`.
|
|
88
|
+
*/
|
|
89
|
+
export function withEnabled(
|
|
90
|
+
entry: PluginEntryJson,
|
|
91
|
+
enabled: boolean,
|
|
92
|
+
): PluginEntryJson {
|
|
93
|
+
const { enabled: _drop, ...rest } = entry;
|
|
94
|
+
return enabled ? rest : { ...rest, enabled: false };
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Package name of an npm spec, version stripped: `pkg@1.2` → `pkg`,
|
|
99
|
+
* `@scope/pkg@^1` → `@scope/pkg`. A leading `@` is the scope, not a version.
|
|
100
|
+
*/
|
|
101
|
+
export function npmSpecName(spec: string): string {
|
|
102
|
+
const at = spec.lastIndexOf("@");
|
|
103
|
+
return at > 0 ? spec.slice(0, at) : spec;
|
|
104
|
+
}
|