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.
Files changed (55) hide show
  1. package/README.md +30 -2
  2. package/package.json +1 -1
  3. package/src/backend/codex/factory.ts +2 -0
  4. package/src/backend/codex/handler/message.ts +10 -0
  5. package/src/backend/kilo/factory.ts +2 -0
  6. package/src/backend/kilo/handler/message.ts +10 -0
  7. package/src/backend/openai-agents/factory.ts +2 -0
  8. package/src/backend/openai-agents/handler/message.ts +10 -0
  9. package/src/backend/opencode/factory.ts +2 -0
  10. package/src/backend/opencode/handler/message.ts +10 -0
  11. package/src/backend/shared/index.ts +4 -0
  12. package/src/backend/shared/turn-interrupt.ts +61 -0
  13. package/src/bootstrap.ts +5 -0
  14. package/src/cli/daemon-api.ts +2 -1
  15. package/src/cli/events.ts +47 -12
  16. package/src/cli/fs.ts +138 -0
  17. package/src/cli/index.ts +47 -7
  18. package/src/cli/install-sources.ts +106 -0
  19. package/src/cli/plugin-entries.ts +104 -0
  20. package/src/cli/plugin.ts +494 -0
  21. package/src/cli/skill.ts +252 -0
  22. package/src/cli/tasks.ts +85 -20
  23. package/src/core/engine/gateway-actions/index.ts +3 -0
  24. package/src/core/engine/gateway-actions/native.ts +151 -24
  25. package/src/core/engine/gateway-actions/plugins.ts +36 -19
  26. package/src/core/engine/gateway-actions/vfs.ts +69 -0
  27. package/src/core/engine/gateway.ts +52 -0
  28. package/src/core/plugin/loader.ts +14 -2
  29. package/src/core/plugin/types.ts +4 -0
  30. package/src/core/tasks/table.ts +1 -1
  31. package/src/core/tasks/types.ts +6 -2
  32. package/src/core/tools/index.ts +2 -0
  33. package/src/core/tools/native.ts +19 -6
  34. package/src/core/tools/types.ts +2 -1
  35. package/src/core/tools/vfs.ts +61 -0
  36. package/src/core/vfs/index.ts +113 -0
  37. package/src/core/vfs/mounts/files.ts +154 -0
  38. package/src/core/vfs/mounts/plugins.ts +72 -0
  39. package/src/core/vfs/mounts/proc.ts +125 -0
  40. package/src/core/vfs/types.ts +103 -0
  41. package/src/core/vfs/vfs.ts +237 -0
  42. package/src/core/weaver/weaver.ts +40 -3
  43. package/src/frontend/native/discovery.ts +3 -0
  44. package/src/frontend/native/index.ts +10 -1
  45. package/src/frontend/native/server.ts +65 -6
  46. package/src/frontend/native/tls.ts +313 -0
  47. package/src/storage/journal.ts +91 -0
  48. package/src/storage/repositories/journal-repo.ts +38 -0
  49. package/src/storage/skill-store.ts +118 -5
  50. package/src/storage/sql/journal.sql +16 -0
  51. package/src/storage/sql/schema.sql +15 -0
  52. package/src/storage/sql/statements.generated.ts +24 -1
  53. package/src/util/config.ts +16 -0
  54. package/src/util/log.ts +1 -0
  55. 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
- await showEvents(
112
- process.argv[3] === "-f" || process.argv[3] === "--follow",
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(` ${pc.cyan("ps")} List agent tasks (task table)`);
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
+ }