talon-agent 5.2.2 → 5.3.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 (51) hide show
  1. package/package.json +1 -1
  2. package/src/app.ts +78 -0
  3. package/src/backend/codex/mcp-config.ts +1 -1
  4. package/src/backend/openai-agents/mcp-pool.ts +1 -1
  5. package/src/backend/runtime/index.ts +1 -1
  6. package/src/cli/commands/backup.ts +396 -0
  7. package/src/cli/events.ts +14 -0
  8. package/src/cli/index.ts +64 -45
  9. package/src/core/backup/archive/digest.ts +77 -0
  10. package/src/core/backup/archive/tar.ts +567 -0
  11. package/src/core/backup/archive/zstd.ts +31 -0
  12. package/src/core/backup/index.ts +54 -0
  13. package/src/core/backup/plan.ts +273 -0
  14. package/src/core/backup/restore.ts +410 -0
  15. package/src/core/backup/scheduler.ts +357 -0
  16. package/src/core/backup/snapshot.ts +408 -0
  17. package/src/core/backup/status.ts +194 -0
  18. package/src/core/backup/store.ts +312 -0
  19. package/src/core/backup/targets.ts +281 -0
  20. package/src/core/backup/types.ts +96 -0
  21. package/src/core/backup/upload.ts +172 -0
  22. package/src/core/bus/events.ts +45 -1
  23. package/src/core/config/index.ts +52 -0
  24. package/src/core/engine/gateway-actions/backup/index.ts +129 -0
  25. package/src/core/engine/gateway-actions/index.ts +4 -0
  26. package/src/core/mcp-hub/talon-server.ts +1 -1
  27. package/src/core/plugin/actions.ts +34 -0
  28. package/src/core/plugin/index.ts +5 -1
  29. package/src/core/tools/{ops/bridge.ts → bridge.ts} +7 -2
  30. package/src/core/tools/index.ts +2 -0
  31. package/src/core/tools/ops/backup.ts +67 -0
  32. package/src/core/tools/types.ts +2 -1
  33. package/src/core/update/self-update.ts +3 -0
  34. package/src/frontend/discord/callbacks/components/index.ts +3 -0
  35. package/src/frontend/discord/commands/backup.ts +203 -0
  36. package/src/frontend/discord/commands/definitions.ts +35 -0
  37. package/src/frontend/discord/commands/router.ts +3 -0
  38. package/src/frontend/telegram/callbacks/backup.ts +55 -0
  39. package/src/frontend/telegram/callbacks/index.ts +8 -0
  40. package/src/frontend/telegram/commands/backup.ts +209 -0
  41. package/src/frontend/telegram/commands/definitions.ts +4 -0
  42. package/src/frontend/telegram/commands/index.ts +2 -0
  43. package/src/storage/backup/index.ts +82 -0
  44. package/src/storage/backup/repo.ts +164 -0
  45. package/src/storage/db.ts +20 -0
  46. package/src/storage/sql/backups.sql +46 -0
  47. package/src/storage/sql/db.sql +8 -0
  48. package/src/storage/sql/schema.sql +30 -0
  49. package/src/storage/sql/statements.generated.ts +60 -1
  50. package/src/util/log.ts +1 -0
  51. /package/src/core/tools/{ops/mcp-env.ts → mcp-env.ts} +0 -0
package/src/cli/index.ts CHANGED
@@ -14,6 +14,8 @@
14
14
  * - `chat` — terminal chat mode
15
15
  * - `daemon` — start/stop/restart outcome rendering
16
16
  * - `menu` — interactive main menu (no-subcommand default)
17
+ * - `commands/` — one module per command; new commands land here
18
+ * (docs/structure.md worklist item 8)
17
19
  */
18
20
 
19
21
  import pc from "picocolors";
@@ -36,6 +38,7 @@ import { runPluginCommand } from "./plugin.js";
36
38
  import { runSkillCommand } from "./skill.js";
37
39
  import { runMemoryCommand } from "./memory.js";
38
40
  import { mainMenu } from "./menu.js";
41
+ import { runBackupCommand } from "./commands/backup.js";
39
42
 
40
43
  export * from "./context.js";
41
44
  export * from "./config.js";
@@ -58,8 +61,63 @@ const CLI_COMMANDS = [
58
61
  "plugin",
59
62
  "skill",
60
63
  "memory",
64
+ "backup",
61
65
  ];
62
66
 
67
+ /** `talon events [-f] [--history [N]]` → the tail options. */
68
+ function eventsOptions(args: string[]): {
69
+ follow: boolean;
70
+ history?: number;
71
+ } {
72
+ const historyAt = args.findIndex((a) => a === "--history");
73
+ const next = historyAt >= 0 ? Number(args[historyAt + 1]) : NaN;
74
+ return {
75
+ follow: args.includes("-f") || args.includes("--follow"),
76
+ ...(historyAt >= 0
77
+ ? { history: Number.isInteger(next) && next > 0 ? next : 100 }
78
+ : {}),
79
+ };
80
+ }
81
+
82
+ /** The `talon --help` sheet. Its own function: `runCli` is a router. */
83
+ function printHelp(): void {
84
+ printBanner();
85
+ console.log(" Usage: talon [command]\n");
86
+ console.log(" Commands:");
87
+ console.log(` ${pc.cyan("setup")} Guided setup wizard`);
88
+ console.log(` ${pc.cyan("start")} Start as background daemon`);
89
+ console.log(` ${pc.cyan("stop")} Stop the daemon`);
90
+ console.log(` ${pc.cyan("restart")} Restart the daemon`);
91
+ console.log(` ${pc.cyan("run")} Run in foreground (attached)`);
92
+ console.log(` ${pc.cyan("chat")} Terminal chat mode`);
93
+ console.log(` ${pc.cyan("status")} Show bot health`);
94
+ console.log(
95
+ ` ${pc.cyan("ps")} List agent tasks (--all includes journal history)`,
96
+ );
97
+ console.log(` ${pc.cyan("kill")} Abort a killable task by id`);
98
+ console.log(
99
+ ` ${pc.cyan("events")} Tail the event bus (-f follows, --history [N] reads the journal)`,
100
+ );
101
+ console.log(
102
+ ` ${pc.cyan("plugin")} Manage plugins (install/enable/disable)`,
103
+ );
104
+ console.log(
105
+ ` ${pc.cyan("skill")} Manage skills (install/enable/disable)`,
106
+ );
107
+ console.log(
108
+ ` ${pc.cyan("memory")} Read/edit the memory store (list/search/import/render)`,
109
+ );
110
+ console.log(
111
+ ` ${pc.cyan("backup")} Snapshots and checkpoints (now/list/show/pin/restore)`,
112
+ );
113
+ console.log(` ${pc.cyan("config")} View/edit configuration`);
114
+ console.log(` ${pc.cyan("logs")} Tail log file`);
115
+ console.log(` ${pc.cyan("doctor")} Validate environment`);
116
+ console.log(` ${pc.cyan("--version")} Print the package version`);
117
+ console.log();
118
+ console.log(` Run ${pc.cyan("talon")} with no args for interactive menu.\n`);
119
+ }
120
+
63
121
  /** Route a `talon <command>` invocation. Called by the entry point. */
64
122
  export async function runCli(): Promise<void> {
65
123
  const command = process.argv[2];
@@ -105,18 +163,12 @@ export async function runCli(): Promise<void> {
105
163
  case "kill":
106
164
  await killTask(process.argv[3]);
107
165
  break;
108
- case "events": {
109
- const args = process.argv.slice(3);
110
- const historyAt = args.findIndex((a) => a === "--history");
111
- const next = historyAt >= 0 ? Number(args[historyAt + 1]) : NaN;
112
- await showEvents({
113
- follow: args.includes("-f") || args.includes("--follow"),
114
- ...(historyAt >= 0
115
- ? { history: Number.isInteger(next) && next > 0 ? next : 100 }
116
- : {}),
117
- });
166
+ case "events":
167
+ await showEvents(eventsOptions(process.argv.slice(3)));
168
+ break;
169
+ case "backup":
170
+ await runBackupCommand(process.argv.slice(3));
118
171
  break;
119
- }
120
172
  case "plugin":
121
173
  await runPluginCommand(process.argv.slice(3));
122
174
  break;
@@ -133,40 +185,7 @@ export async function runCli(): Promise<void> {
133
185
  }
134
186
  case "--help":
135
187
  case "-h":
136
- printBanner();
137
- console.log(" Usage: talon [command]\n");
138
- console.log(" Commands:");
139
- console.log(` ${pc.cyan("setup")} Guided setup wizard`);
140
- console.log(` ${pc.cyan("start")} Start as background daemon`);
141
- console.log(` ${pc.cyan("stop")} Stop the daemon`);
142
- console.log(` ${pc.cyan("restart")} Restart the daemon`);
143
- console.log(` ${pc.cyan("run")} Run in foreground (attached)`);
144
- console.log(` ${pc.cyan("chat")} Terminal chat mode`);
145
- console.log(` ${pc.cyan("status")} Show bot health`);
146
- console.log(
147
- ` ${pc.cyan("ps")} List agent tasks (--all includes journal history)`,
148
- );
149
- console.log(` ${pc.cyan("kill")} Abort a killable task by id`);
150
- console.log(
151
- ` ${pc.cyan("events")} Tail the event bus (-f follows, --history [N] reads the journal)`,
152
- );
153
- console.log(
154
- ` ${pc.cyan("plugin")} Manage plugins (install/enable/disable)`,
155
- );
156
- console.log(
157
- ` ${pc.cyan("skill")} Manage skills (install/enable/disable)`,
158
- );
159
- console.log(
160
- ` ${pc.cyan("memory")} Read/edit the memory store (list/search/import/render)`,
161
- );
162
- console.log(` ${pc.cyan("config")} View/edit configuration`);
163
- console.log(` ${pc.cyan("logs")} Tail log file`);
164
- console.log(` ${pc.cyan("doctor")} Validate environment`);
165
- console.log(` ${pc.cyan("--version")} Print the package version`);
166
- console.log();
167
- console.log(
168
- ` Run ${pc.cyan("talon")} with no args for interactive menu.\n`,
169
- );
188
+ printHelp();
170
189
  break;
171
190
  case undefined:
172
191
  mainMenu();
@@ -0,0 +1,77 @@
1
+ /**
2
+ * Digests — what makes a snapshot verifiable and the palace part reusable.
3
+ *
4
+ * Two jobs:
5
+ * - `Sha256Tap` sits in the write pipeline and hashes the COMPRESSED
6
+ * bytes on their way to disk, so the manifest's sha256 is produced
7
+ * without a second pass over a multi-GB part. The restore path checks
8
+ * the same digest before it unpacks anything.
9
+ * - `treeHash` fingerprints a directory (path + size + mtime + content
10
+ * digest of every file) so an unchanged memory palace can be carried
11
+ * forward from the previous snapshot instead of recompressed and
12
+ * re-uploaded every six hours.
13
+ */
14
+
15
+ import { createHash } from "node:crypto";
16
+ import { createReadStream } from "node:fs";
17
+ import { Transform, type TransformCallback } from "node:stream";
18
+
19
+ /** Pass-through stream that hashes (and counts) everything crossing it. */
20
+ export class Sha256Tap extends Transform {
21
+ private readonly hash = createHash("sha256");
22
+ private digested: string | null = null;
23
+ private seen = 0;
24
+
25
+ override _transform(
26
+ chunk: Buffer,
27
+ _encoding: BufferEncoding,
28
+ done: TransformCallback,
29
+ ): void {
30
+ this.hash.update(chunk);
31
+ this.seen += chunk.length;
32
+ done(null, chunk);
33
+ }
34
+
35
+ /** Hex digest of everything written. Valid once the stream has finished. */
36
+ digest(): string {
37
+ if (this.digested === null) this.digested = this.hash.digest("hex");
38
+ return this.digested;
39
+ }
40
+
41
+ /** Bytes seen so far — the part's size, without a stat() race. */
42
+ get byteLength(): number {
43
+ return this.seen;
44
+ }
45
+ }
46
+
47
+ /** Streaming sha256 of a file on disk. */
48
+ export async function sha256File(path: string): Promise<string> {
49
+ const hash = createHash("sha256");
50
+ for await (const chunk of createReadStream(path))
51
+ hash.update(chunk as Buffer);
52
+ return hash.digest("hex");
53
+ }
54
+
55
+ /** One file's identity inside a tree fingerprint. */
56
+ export type TreeFile = {
57
+ /** Path relative to the tree root, POSIX separators. */
58
+ path: string;
59
+ size: number;
60
+ /** Modification time in epoch SECONDS (tar's resolution — stable). */
61
+ mtime: number;
62
+ sha256: string;
63
+ };
64
+
65
+ /**
66
+ * Fingerprint a set of files. Sorted by path so the value depends on the
67
+ * content of the tree and not on readdir order.
68
+ */
69
+ export function treeHash(files: readonly TreeFile[]): string {
70
+ const hash = createHash("sha256");
71
+ for (const file of [...files].sort((a, b) =>
72
+ a.path < b.path ? -1 : a.path > b.path ? 1 : 0,
73
+ )) {
74
+ hash.update(`${file.path}\0${file.size}\0${file.mtime}\0${file.sha256}\n`);
75
+ }
76
+ return hash.digest("hex");
77
+ }