reeboot 1.0.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 (110) hide show
  1. package/README.md +361 -0
  2. package/container/Dockerfile +48 -0
  3. package/container/entrypoint.sh +8 -0
  4. package/dist/agent-runner/index.d.ts +9 -0
  5. package/dist/agent-runner/index.d.ts.map +1 -0
  6. package/dist/agent-runner/index.js +21 -0
  7. package/dist/agent-runner/index.js.map +1 -0
  8. package/dist/agent-runner/interface.d.ts +56 -0
  9. package/dist/agent-runner/interface.d.ts.map +1 -0
  10. package/dist/agent-runner/interface.js +5 -0
  11. package/dist/agent-runner/interface.js.map +1 -0
  12. package/dist/agent-runner/pi-runner.d.ts +41 -0
  13. package/dist/agent-runner/pi-runner.d.ts.map +1 -0
  14. package/dist/agent-runner/pi-runner.js +162 -0
  15. package/dist/agent-runner/pi-runner.js.map +1 -0
  16. package/dist/channels/interface.d.ts +63 -0
  17. package/dist/channels/interface.d.ts.map +1 -0
  18. package/dist/channels/interface.js +33 -0
  19. package/dist/channels/interface.js.map +1 -0
  20. package/dist/channels/registry.d.ts +30 -0
  21. package/dist/channels/registry.d.ts.map +1 -0
  22. package/dist/channels/registry.js +71 -0
  23. package/dist/channels/registry.js.map +1 -0
  24. package/dist/channels/signal.d.ts +51 -0
  25. package/dist/channels/signal.d.ts.map +1 -0
  26. package/dist/channels/signal.js +263 -0
  27. package/dist/channels/signal.js.map +1 -0
  28. package/dist/channels/web.d.ts +35 -0
  29. package/dist/channels/web.d.ts.map +1 -0
  30. package/dist/channels/web.js +65 -0
  31. package/dist/channels/web.js.map +1 -0
  32. package/dist/channels/whatsapp.d.ts +25 -0
  33. package/dist/channels/whatsapp.d.ts.map +1 -0
  34. package/dist/channels/whatsapp.js +150 -0
  35. package/dist/channels/whatsapp.js.map +1 -0
  36. package/dist/config.d.ts +366 -0
  37. package/dist/config.d.ts.map +1 -0
  38. package/dist/config.js +140 -0
  39. package/dist/config.js.map +1 -0
  40. package/dist/context.d.ts +69 -0
  41. package/dist/context.d.ts.map +1 -0
  42. package/dist/context.js +166 -0
  43. package/dist/context.js.map +1 -0
  44. package/dist/credential-proxy.d.ts +25 -0
  45. package/dist/credential-proxy.d.ts.map +1 -0
  46. package/dist/credential-proxy.js +96 -0
  47. package/dist/credential-proxy.js.map +1 -0
  48. package/dist/daemon.d.ts +25 -0
  49. package/dist/daemon.d.ts.map +1 -0
  50. package/dist/daemon.js +138 -0
  51. package/dist/daemon.js.map +1 -0
  52. package/dist/db/index.d.ts +23 -0
  53. package/dist/db/index.d.ts.map +1 -0
  54. package/dist/db/index.js +113 -0
  55. package/dist/db/index.js.map +1 -0
  56. package/dist/db/schema.d.ts +408 -0
  57. package/dist/db/schema.d.ts.map +1 -0
  58. package/dist/db/schema.js +55 -0
  59. package/dist/db/schema.js.map +1 -0
  60. package/dist/doctor.d.ts +23 -0
  61. package/dist/doctor.d.ts.map +1 -0
  62. package/dist/doctor.js +217 -0
  63. package/dist/doctor.js.map +1 -0
  64. package/dist/extensions/loader.d.ts +19 -0
  65. package/dist/extensions/loader.d.ts.map +1 -0
  66. package/dist/extensions/loader.js +124 -0
  67. package/dist/extensions/loader.js.map +1 -0
  68. package/dist/index.d.ts +3 -0
  69. package/dist/index.d.ts.map +1 -0
  70. package/dist/index.js +561 -0
  71. package/dist/index.js.map +1 -0
  72. package/dist/orchestrator.d.ts +60 -0
  73. package/dist/orchestrator.d.ts.map +1 -0
  74. package/dist/orchestrator.js +313 -0
  75. package/dist/orchestrator.js.map +1 -0
  76. package/dist/packages.d.ts +21 -0
  77. package/dist/packages.d.ts.map +1 -0
  78. package/dist/packages.js +116 -0
  79. package/dist/packages.js.map +1 -0
  80. package/dist/scheduler-registry.d.ts +8 -0
  81. package/dist/scheduler-registry.d.ts.map +1 -0
  82. package/dist/scheduler-registry.js +14 -0
  83. package/dist/scheduler-registry.js.map +1 -0
  84. package/dist/scheduler.d.ts +60 -0
  85. package/dist/scheduler.d.ts.map +1 -0
  86. package/dist/scheduler.js +143 -0
  87. package/dist/scheduler.js.map +1 -0
  88. package/dist/server.d.ts +18 -0
  89. package/dist/server.d.ts.map +1 -0
  90. package/dist/server.js +489 -0
  91. package/dist/server.js.map +1 -0
  92. package/dist/setup-wizard.d.ts +12 -0
  93. package/dist/setup-wizard.d.ts.map +1 -0
  94. package/dist/setup-wizard.js +163 -0
  95. package/dist/setup-wizard.js.map +1 -0
  96. package/extensions/confirm-destructive.ts +59 -0
  97. package/extensions/custom-compaction.ts +114 -0
  98. package/extensions/protected-paths.ts +30 -0
  99. package/extensions/sandbox/index.ts +317 -0
  100. package/extensions/sandbox/package-lock.json +92 -0
  101. package/extensions/sandbox/package.json +19 -0
  102. package/extensions/scheduler-tool.ts +65 -0
  103. package/extensions/session-name.ts +27 -0
  104. package/extensions/token-meter.ts +55 -0
  105. package/package.json +68 -0
  106. package/skills/send-message/SKILL.md +27 -0
  107. package/skills/web-search/SKILL.md +32 -0
  108. package/templates/global-agents.md +23 -0
  109. package/templates/main-agents.md +28 -0
  110. package/webchat/index.html +421 -0
@@ -0,0 +1,317 @@
1
+ /**
2
+ * Sandbox Extension - OS-level sandboxing for bash commands
3
+ *
4
+ * Uses @anthropic-ai/sandbox-runtime to enforce filesystem and network
5
+ * restrictions on bash commands at the OS level (sandbox-exec on macOS,
6
+ * bubblewrap on Linux).
7
+ *
8
+ * Config files (merged, project takes precedence):
9
+ * - ~/.pi/agent/sandbox.json (global)
10
+ * - <cwd>/.pi/sandbox.json (project-local)
11
+ *
12
+ * Example .pi/sandbox.json:
13
+ * ```json
14
+ * {
15
+ * "enabled": true,
16
+ * "network": {
17
+ * "allowedDomains": ["github.com", "*.github.com"],
18
+ * "deniedDomains": []
19
+ * },
20
+ * "filesystem": {
21
+ * "denyRead": ["~/.ssh", "~/.aws"],
22
+ * "allowWrite": [".", "/tmp"],
23
+ * "denyWrite": [".env"]
24
+ * }
25
+ * }
26
+ * ```
27
+ *
28
+ * Usage:
29
+ * - `pi -e ./sandbox` - sandbox enabled with default/config settings
30
+ * - `pi -e ./sandbox --no-sandbox` - disable sandboxing
31
+ * - `/sandbox` - show current sandbox configuration
32
+ *
33
+ * Setup:
34
+ * 1. Copy sandbox/ directory to ~/.pi/agent/extensions/
35
+ * 2. Run `npm install` in ~/.pi/agent/extensions/sandbox/
36
+ *
37
+ * Linux also requires: bubblewrap, socat, ripgrep
38
+ */
39
+
40
+ import { spawn } from "node:child_process";
41
+ import { existsSync, readFileSync } from "node:fs";
42
+ import { join } from "node:path";
43
+ import { SandboxManager, type SandboxRuntimeConfig } from "@anthropic-ai/sandbox-runtime";
44
+ import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
45
+ import { type BashOperations, createBashTool, getAgentDir } from "@mariozechner/pi-coding-agent";
46
+
47
+ interface SandboxConfig extends SandboxRuntimeConfig {
48
+ enabled?: boolean;
49
+ }
50
+
51
+ const DEFAULT_CONFIG: SandboxConfig = {
52
+ enabled: true,
53
+ network: {
54
+ allowedDomains: [
55
+ "npmjs.org",
56
+ "*.npmjs.org",
57
+ "registry.npmjs.org",
58
+ "registry.yarnpkg.com",
59
+ "pypi.org",
60
+ "*.pypi.org",
61
+ "github.com",
62
+ "*.github.com",
63
+ "api.github.com",
64
+ "raw.githubusercontent.com",
65
+ ],
66
+ deniedDomains: [],
67
+ },
68
+ filesystem: {
69
+ denyRead: ["~/.ssh", "~/.aws", "~/.gnupg"],
70
+ allowWrite: [".", "/tmp"],
71
+ denyWrite: [".env", ".env.*", "*.pem", "*.key"],
72
+ },
73
+ };
74
+
75
+ function loadConfig(cwd: string): SandboxConfig {
76
+ const projectConfigPath = join(cwd, ".pi", "sandbox.json");
77
+ const globalConfigPath = join(getAgentDir(), "extensions", "sandbox.json");
78
+
79
+ let globalConfig: Partial<SandboxConfig> = {};
80
+ let projectConfig: Partial<SandboxConfig> = {};
81
+
82
+ if (existsSync(globalConfigPath)) {
83
+ try {
84
+ globalConfig = JSON.parse(readFileSync(globalConfigPath, "utf-8"));
85
+ } catch (e) {
86
+ console.error(`Warning: Could not parse ${globalConfigPath}: ${e}`);
87
+ }
88
+ }
89
+
90
+ if (existsSync(projectConfigPath)) {
91
+ try {
92
+ projectConfig = JSON.parse(readFileSync(projectConfigPath, "utf-8"));
93
+ } catch (e) {
94
+ console.error(`Warning: Could not parse ${projectConfigPath}: ${e}`);
95
+ }
96
+ }
97
+
98
+ return deepMerge(deepMerge(DEFAULT_CONFIG, globalConfig), projectConfig);
99
+ }
100
+
101
+ function deepMerge(base: SandboxConfig, overrides: Partial<SandboxConfig>): SandboxConfig {
102
+ const result: SandboxConfig = { ...base };
103
+
104
+ if (overrides.enabled !== undefined) result.enabled = overrides.enabled;
105
+ if (overrides.network) {
106
+ result.network = { ...base.network, ...overrides.network };
107
+ }
108
+ if (overrides.filesystem) {
109
+ result.filesystem = { ...base.filesystem, ...overrides.filesystem };
110
+ }
111
+
112
+ const extOverrides = overrides as {
113
+ ignoreViolations?: Record<string, string[]>;
114
+ enableWeakerNestedSandbox?: boolean;
115
+ };
116
+ const extResult = result as { ignoreViolations?: Record<string, string[]>; enableWeakerNestedSandbox?: boolean };
117
+
118
+ if (extOverrides.ignoreViolations) {
119
+ extResult.ignoreViolations = extOverrides.ignoreViolations;
120
+ }
121
+ if (extOverrides.enableWeakerNestedSandbox !== undefined) {
122
+ extResult.enableWeakerNestedSandbox = extOverrides.enableWeakerNestedSandbox;
123
+ }
124
+
125
+ return result;
126
+ }
127
+
128
+ function createSandboxedBashOps(): BashOperations {
129
+ return {
130
+ async exec(command, cwd, { onData, signal, timeout }) {
131
+ if (!existsSync(cwd)) {
132
+ throw new Error(`Working directory does not exist: ${cwd}`);
133
+ }
134
+
135
+ const wrappedCommand = await SandboxManager.wrapWithSandbox(command);
136
+
137
+ return new Promise((resolve, reject) => {
138
+ const child = spawn("bash", ["-c", wrappedCommand], {
139
+ cwd,
140
+ detached: true,
141
+ stdio: ["ignore", "pipe", "pipe"],
142
+ });
143
+
144
+ let timedOut = false;
145
+ let timeoutHandle: NodeJS.Timeout | undefined;
146
+
147
+ if (timeout !== undefined && timeout > 0) {
148
+ timeoutHandle = setTimeout(() => {
149
+ timedOut = true;
150
+ if (child.pid) {
151
+ try {
152
+ process.kill(-child.pid, "SIGKILL");
153
+ } catch {
154
+ child.kill("SIGKILL");
155
+ }
156
+ }
157
+ }, timeout * 1000);
158
+ }
159
+
160
+ child.stdout?.on("data", onData);
161
+ child.stderr?.on("data", onData);
162
+
163
+ child.on("error", (err) => {
164
+ if (timeoutHandle) clearTimeout(timeoutHandle);
165
+ reject(err);
166
+ });
167
+
168
+ const onAbort = () => {
169
+ if (child.pid) {
170
+ try {
171
+ process.kill(-child.pid, "SIGKILL");
172
+ } catch {
173
+ child.kill("SIGKILL");
174
+ }
175
+ }
176
+ };
177
+
178
+ signal?.addEventListener("abort", onAbort, { once: true });
179
+
180
+ child.on("close", (code) => {
181
+ if (timeoutHandle) clearTimeout(timeoutHandle);
182
+ signal?.removeEventListener("abort", onAbort);
183
+
184
+ if (signal?.aborted) {
185
+ reject(new Error("aborted"));
186
+ } else if (timedOut) {
187
+ reject(new Error(`timeout:${timeout}`));
188
+ } else {
189
+ resolve({ exitCode: code });
190
+ }
191
+ });
192
+ });
193
+ },
194
+ };
195
+ }
196
+
197
+ export default function (pi: ExtensionAPI) {
198
+ pi.registerFlag("no-sandbox", {
199
+ description: "Disable OS-level sandboxing for bash commands",
200
+ type: "boolean",
201
+ default: false,
202
+ });
203
+
204
+ const localCwd = process.cwd();
205
+ const localBash = createBashTool(localCwd);
206
+
207
+ let sandboxEnabled = false;
208
+ let sandboxInitialized = false;
209
+
210
+ pi.registerTool({
211
+ ...localBash,
212
+ label: "bash (sandboxed)",
213
+ async execute(id, params, signal, onUpdate, _ctx) {
214
+ if (!sandboxEnabled || !sandboxInitialized) {
215
+ return localBash.execute(id, params, signal, onUpdate);
216
+ }
217
+
218
+ const sandboxedBash = createBashTool(localCwd, {
219
+ operations: createSandboxedBashOps(),
220
+ });
221
+ return sandboxedBash.execute(id, params, signal, onUpdate);
222
+ },
223
+ });
224
+
225
+ pi.on("user_bash", () => {
226
+ if (!sandboxEnabled || !sandboxInitialized) return;
227
+ return { operations: createSandboxedBashOps() };
228
+ });
229
+
230
+ pi.on("session_start", async (_event, ctx) => {
231
+ const noSandbox = pi.getFlag("no-sandbox") as boolean;
232
+
233
+ if (noSandbox) {
234
+ sandboxEnabled = false;
235
+ ctx.ui.notify("Sandbox disabled via --no-sandbox", "warning");
236
+ return;
237
+ }
238
+
239
+ const config = loadConfig(ctx.cwd);
240
+
241
+ if (!config.enabled) {
242
+ sandboxEnabled = false;
243
+ ctx.ui.notify("Sandbox disabled via config", "info");
244
+ return;
245
+ }
246
+
247
+ const platform = process.platform;
248
+ if (platform !== "darwin" && platform !== "linux") {
249
+ sandboxEnabled = false;
250
+ ctx.ui.notify(`Sandbox not supported on ${platform}`, "warning");
251
+ return;
252
+ }
253
+
254
+ try {
255
+ const configExt = config as unknown as {
256
+ ignoreViolations?: Record<string, string[]>;
257
+ enableWeakerNestedSandbox?: boolean;
258
+ };
259
+
260
+ await SandboxManager.initialize({
261
+ network: config.network,
262
+ filesystem: config.filesystem,
263
+ ignoreViolations: configExt.ignoreViolations,
264
+ enableWeakerNestedSandbox: configExt.enableWeakerNestedSandbox,
265
+ });
266
+
267
+ sandboxEnabled = true;
268
+ sandboxInitialized = true;
269
+
270
+ const networkCount = config.network?.allowedDomains?.length ?? 0;
271
+ const writeCount = config.filesystem?.allowWrite?.length ?? 0;
272
+ ctx.ui.setStatus(
273
+ "sandbox",
274
+ ctx.ui.theme.fg("accent", `🔒 Sandbox: ${networkCount} domains, ${writeCount} write paths`),
275
+ );
276
+ ctx.ui.notify("Sandbox initialized", "info");
277
+ } catch (err) {
278
+ sandboxEnabled = false;
279
+ ctx.ui.notify(`Sandbox initialization failed: ${err instanceof Error ? err.message : err}`, "error");
280
+ }
281
+ });
282
+
283
+ pi.on("session_shutdown", async () => {
284
+ if (sandboxInitialized) {
285
+ try {
286
+ await SandboxManager.reset();
287
+ } catch {
288
+ // Ignore cleanup errors
289
+ }
290
+ }
291
+ });
292
+
293
+ pi.registerCommand("sandbox", {
294
+ description: "Show sandbox configuration",
295
+ handler: async (_args, ctx) => {
296
+ if (!sandboxEnabled) {
297
+ ctx.ui.notify("Sandbox is disabled", "info");
298
+ return;
299
+ }
300
+
301
+ const config = loadConfig(ctx.cwd);
302
+ const lines = [
303
+ "Sandbox Configuration:",
304
+ "",
305
+ "Network:",
306
+ ` Allowed: ${config.network?.allowedDomains?.join(", ") || "(none)"}`,
307
+ ` Denied: ${config.network?.deniedDomains?.join(", ") || "(none)"}`,
308
+ "",
309
+ "Filesystem:",
310
+ ` Deny Read: ${config.filesystem?.denyRead?.join(", ") || "(none)"}`,
311
+ ` Allow Write: ${config.filesystem?.allowWrite?.join(", ") || "(none)"}`,
312
+ ` Deny Write: ${config.filesystem?.denyWrite?.join(", ") || "(none)"}`,
313
+ ];
314
+ ctx.ui.notify(lines.join("\n"), "info");
315
+ },
316
+ });
317
+ }
@@ -0,0 +1,92 @@
1
+ {
2
+ "name": "pi-extension-sandbox",
3
+ "version": "1.0.0",
4
+ "lockfileVersion": 3,
5
+ "requires": true,
6
+ "packages": {
7
+ "": {
8
+ "name": "pi-extension-sandbox",
9
+ "version": "1.0.0",
10
+ "dependencies": {
11
+ "@anthropic-ai/sandbox-runtime": "^0.0.26"
12
+ }
13
+ },
14
+ "node_modules/@anthropic-ai/sandbox-runtime": {
15
+ "version": "0.0.26",
16
+ "resolved": "https://registry.npmjs.org/@anthropic-ai/sandbox-runtime/-/sandbox-runtime-0.0.26.tgz",
17
+ "integrity": "sha512-DYV5LSsVMnzq0lbfaYMSpxZPUMAx4+hy343dRss+pVCLIfF62qOhxpYfZ5TmOk1GTDQm5f9wPprMNSStmnsV4w==",
18
+ "license": "Apache-2.0",
19
+ "dependencies": {
20
+ "@pondwader/socks5-server": "^1.0.10",
21
+ "@types/lodash-es": "^4.17.12",
22
+ "commander": "^12.1.0",
23
+ "lodash-es": "^4.17.21",
24
+ "shell-quote": "^1.8.3",
25
+ "zod": "^3.24.1"
26
+ },
27
+ "bin": {
28
+ "srt": "dist/cli.js"
29
+ },
30
+ "engines": {
31
+ "node": ">=18.0.0"
32
+ }
33
+ },
34
+ "node_modules/@pondwader/socks5-server": {
35
+ "version": "1.0.10",
36
+ "resolved": "https://registry.npmjs.org/@pondwader/socks5-server/-/socks5-server-1.0.10.tgz",
37
+ "integrity": "sha512-bQY06wzzR8D2+vVCUoBsr5QS2U6UgPUQRmErNwtsuI6vLcyRKkafjkr3KxbtGFf9aBBIV2mcvlsKD1UYaIV+sg==",
38
+ "license": "MIT"
39
+ },
40
+ "node_modules/@types/lodash": {
41
+ "version": "4.17.23",
42
+ "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.17.23.tgz",
43
+ "integrity": "sha512-RDvF6wTulMPjrNdCoYRC8gNR880JNGT8uB+REUpC2Ns4pRqQJhGz90wh7rgdXDPpCczF3VGktDuFGVnz8zP7HA==",
44
+ "license": "MIT"
45
+ },
46
+ "node_modules/@types/lodash-es": {
47
+ "version": "4.17.12",
48
+ "resolved": "https://registry.npmjs.org/@types/lodash-es/-/lodash-es-4.17.12.tgz",
49
+ "integrity": "sha512-0NgftHUcV4v34VhXm8QBSftKVXtbkBG3ViCjs6+eJ5a6y6Mi/jiFGPc1sC7QK+9BFhWrURE3EOggmWaSxL9OzQ==",
50
+ "license": "MIT",
51
+ "dependencies": {
52
+ "@types/lodash": "*"
53
+ }
54
+ },
55
+ "node_modules/commander": {
56
+ "version": "12.1.0",
57
+ "resolved": "https://registry.npmjs.org/commander/-/commander-12.1.0.tgz",
58
+ "integrity": "sha512-Vw8qHK3bZM9y/P10u3Vib8o/DdkvA2OtPtZvD871QKjy74Wj1WSKFILMPRPSdUSx5RFK1arlJzEtA4PkFgnbuA==",
59
+ "license": "MIT",
60
+ "engines": {
61
+ "node": ">=18"
62
+ }
63
+ },
64
+ "node_modules/lodash-es": {
65
+ "version": "4.17.22",
66
+ "resolved": "https://registry.npmjs.org/lodash-es/-/lodash-es-4.17.22.tgz",
67
+ "integrity": "sha512-XEawp1t0gxSi9x01glktRZ5HDy0HXqrM0x5pXQM98EaI0NxO6jVM7omDOxsuEo5UIASAnm2bRp1Jt/e0a2XU8Q==",
68
+ "license": "MIT"
69
+ },
70
+ "node_modules/shell-quote": {
71
+ "version": "1.8.3",
72
+ "resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.8.3.tgz",
73
+ "integrity": "sha512-ObmnIF4hXNg1BqhnHmgbDETF8dLPCggZWBjkQfhZpbszZnYur5DUljTcCHii5LC3J5E0yeO/1LIMyH+UvHQgyw==",
74
+ "license": "MIT",
75
+ "engines": {
76
+ "node": ">= 0.4"
77
+ },
78
+ "funding": {
79
+ "url": "https://github.com/sponsors/ljharb"
80
+ }
81
+ },
82
+ "node_modules/zod": {
83
+ "version": "3.25.76",
84
+ "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz",
85
+ "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==",
86
+ "license": "MIT",
87
+ "funding": {
88
+ "url": "https://github.com/sponsors/colinhacks"
89
+ }
90
+ }
91
+ }
92
+ }
@@ -0,0 +1,19 @@
1
+ {
2
+ "name": "pi-extension-sandbox",
3
+ "private": true,
4
+ "version": "1.0.0",
5
+ "type": "module",
6
+ "scripts": {
7
+ "clean": "echo 'nothing to clean'",
8
+ "build": "echo 'nothing to build'",
9
+ "check": "echo 'nothing to check'"
10
+ },
11
+ "pi": {
12
+ "extensions": [
13
+ "./index.ts"
14
+ ]
15
+ },
16
+ "dependencies": {
17
+ "@anthropic-ai/sandbox-runtime": "^0.0.26"
18
+ }
19
+ }
@@ -0,0 +1,65 @@
1
+ /**
2
+ * Scheduler Tool Extension
3
+ *
4
+ * Registers schedule_task, list_tasks, cancel_task tools backed by SQLite.
5
+ * Uses getDb() for DB access and integrates with the Scheduler singleton
6
+ * (injected via pi's extension context or resolved from the global registry).
7
+ */
8
+
9
+ import { Type } from '@sinclair/typebox';
10
+ import type { ExtensionAPI } from '@mariozechner/pi-coding-agent';
11
+
12
+ export default function (pi: ExtensionAPI) {
13
+ // Lazily resolve DB and scheduler to avoid circular imports
14
+ function getTools() {
15
+ // Dynamic requires deferred to avoid startup issues
16
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
17
+ const { getDb } = require('../src/db/index.js') as typeof import('../src/db/index.js');
18
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
19
+ const { createSchedulerTools } = require('../src/scheduler.js') as typeof import('../src/scheduler.js');
20
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
21
+ const { globalScheduler } = require('../src/scheduler-registry.js') as typeof import('../src/scheduler-registry.js');
22
+
23
+ const db = getDb();
24
+ return createSchedulerTools(db, globalScheduler);
25
+ }
26
+
27
+ pi.registerTool({
28
+ name: 'schedule_task',
29
+ label: 'Schedule Task',
30
+ description: 'Schedule a recurring task. Provide a cron expression, a prompt, and optionally a contextId.',
31
+ parameters: Type.Object({
32
+ schedule: Type.String({ description: 'Cron expression (e.g. "0 9 * * 1-5" for weekdays at 9am)' }),
33
+ prompt: Type.String({ description: 'Prompt to dispatch to the agent on schedule' }),
34
+ contextId: Type.Optional(Type.String({ description: 'Context to run in (default: main)' })),
35
+ }),
36
+ execute: async (_id, params) => {
37
+ const tools = getTools();
38
+ return tools.schedule_task(params);
39
+ },
40
+ });
41
+
42
+ pi.registerTool({
43
+ name: 'list_tasks',
44
+ label: 'List Tasks',
45
+ description: 'List all scheduled tasks with their id, schedule, prompt, contextId, enabled status and last run time.',
46
+ parameters: Type.Object({}),
47
+ execute: async () => {
48
+ const tools = getTools();
49
+ return tools.list_tasks({} as Record<string, never>);
50
+ },
51
+ });
52
+
53
+ pi.registerTool({
54
+ name: 'cancel_task',
55
+ label: 'Cancel Task',
56
+ description: 'Cancel and delete a scheduled task by its ID.',
57
+ parameters: Type.Object({
58
+ task_id: Type.String({ description: 'Task ID to cancel (from list_tasks)' }),
59
+ }),
60
+ execute: async (_id, params) => {
61
+ const tools = getTools();
62
+ return tools.cancel_task(params);
63
+ },
64
+ });
65
+ }
@@ -0,0 +1,27 @@
1
+ /**
2
+ * Session naming example.
3
+ *
4
+ * Shows setSessionName/getSessionName to give sessions friendly names
5
+ * that appear in the session selector instead of the first message.
6
+ *
7
+ * Usage: /session-name [name] - set or show session name
8
+ */
9
+
10
+ import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
11
+
12
+ export default function (pi: ExtensionAPI) {
13
+ pi.registerCommand("session-name", {
14
+ description: "Set or show session name (usage: /session-name [new name])",
15
+ handler: async (args, ctx) => {
16
+ const name = args.trim();
17
+
18
+ if (name) {
19
+ pi.setSessionName(name);
20
+ ctx.ui.notify(`Session named: ${name}`, "info");
21
+ } else {
22
+ const current = pi.getSessionName();
23
+ ctx.ui.notify(current ? `Session: ${current}` : "No session name set", "info");
24
+ }
25
+ },
26
+ });
27
+ }
@@ -0,0 +1,55 @@
1
+ /**
2
+ * Token Meter Extension
3
+ *
4
+ * Subscribes to agent_end events and inserts a usage row into the SQLite
5
+ * `usage` table via getDb() from the reeboot DB module.
6
+ *
7
+ * The context_id is derived from the cwd (basename of the context workspace).
8
+ */
9
+
10
+ import type { ExtensionAPI } from '@mariozechner/pi-coding-agent';
11
+
12
+ export default function (pi: ExtensionAPI) {
13
+ pi.on('agent_end', async (event, ctx) => {
14
+ // Extract the last assistant message to get token usage
15
+ const messages = event.messages as any[];
16
+ let inputTokens = 0;
17
+ let outputTokens = 0;
18
+ let modelId = '';
19
+
20
+ for (let i = messages.length - 1; i >= 0; i--) {
21
+ const m = messages[i];
22
+ if (m.role === 'assistant') {
23
+ if (m.usage) {
24
+ inputTokens = m.usage.inputTokens ?? 0;
25
+ outputTokens = m.usage.outputTokens ?? 0;
26
+ }
27
+ if (m.model) {
28
+ modelId = typeof m.model === 'string' ? m.model : (m.model.id ?? '');
29
+ }
30
+ break;
31
+ }
32
+ }
33
+
34
+ if (inputTokens === 0 && outputTokens === 0) return;
35
+
36
+ // Derive context_id from the context workspace path (basename)
37
+ const path = await import('path');
38
+ const contextId = path.basename(ctx.cwd);
39
+
40
+ try {
41
+ // Dynamic import to avoid circular deps when extension is loaded outside reeboot
42
+ const { getDb } = await import('reeboot/src/db/index.js').catch(() =>
43
+ // fallback: try relative path resolution at runtime
44
+ import('../src/db/index.js')
45
+ );
46
+ const db = getDb();
47
+ db.prepare(
48
+ `INSERT INTO usage (context_id, input_tokens, output_tokens, model)
49
+ VALUES (?, ?, ?, ?)`
50
+ ).run(contextId, inputTokens, outputTokens, modelId);
51
+ } catch {
52
+ // DB may not be available in test/standalone contexts — silently skip
53
+ }
54
+ });
55
+ }
package/package.json ADDED
@@ -0,0 +1,68 @@
1
+ {
2
+ "name": "reeboot",
3
+ "version": "1.0.0",
4
+ "description": "Personal AI agent running locally, reachable via WhatsApp, Signal, or WebChat",
5
+ "license": "MIT",
6
+ "type": "module",
7
+ "bin": {
8
+ "reeboot": "./dist/index.js"
9
+ },
10
+ "main": "./dist/index.js",
11
+ "exports": {
12
+ ".": "./dist/index.js",
13
+ "./channels": "./dist/channels/interface.js"
14
+ },
15
+ "files": [
16
+ "dist/",
17
+ "extensions/",
18
+ "skills/",
19
+ "templates/",
20
+ "container/",
21
+ "webchat/"
22
+ ],
23
+ "engines": {
24
+ "node": ">=22"
25
+ },
26
+ "keywords": [
27
+ "ai",
28
+ "agent",
29
+ "whatsapp",
30
+ "signal",
31
+ "personal-ai",
32
+ "llm"
33
+ ],
34
+ "scripts": {
35
+ "build": "tsc",
36
+ "dev": "tsx src/index.ts",
37
+ "test": "vitest",
38
+ "test:ui": "vitest --ui"
39
+ },
40
+ "dependencies": {
41
+ "@fastify/static": "^9.0.0",
42
+ "@fastify/websocket": "^11.2.0",
43
+ "@mariozechner/pi-coding-agent": "latest",
44
+ "@types/ws": "^8.18.1",
45
+ "@whiskeysockets/baileys": "6.7.21",
46
+ "better-sqlite3": "^11.6.0",
47
+ "commander": "^12.1.0",
48
+ "drizzle-kit": "^0.20.0",
49
+ "drizzle-orm": "^0.31.0",
50
+ "fastify": "^5.0.0",
51
+ "inquirer": "^12.0.0",
52
+ "nanoid": "^5.0.0",
53
+ "node-cron": "^4.2.1",
54
+ "pino": "^9.0.0",
55
+ "pino-pretty": "^11.0.0",
56
+ "qrcode-terminal": "^0.12.0",
57
+ "ws": "^8.19.0",
58
+ "zod": "^3.23.0"
59
+ },
60
+ "devDependencies": {
61
+ "@types/better-sqlite3": "^7.6.0",
62
+ "@types/node": "^20.11.0",
63
+ "@types/node-cron": "^3.0.11",
64
+ "tsx": "^4.7.0",
65
+ "typescript": "^5.4.0",
66
+ "vitest": "^1.3.0"
67
+ }
68
+ }
@@ -0,0 +1,27 @@
1
+ ---
2
+ name: send-message
3
+ description: Send a message to a contact via WhatsApp or Signal. Use when the user asks you to send a message to someone via a messaging channel.
4
+ ---
5
+
6
+ # Send Message Skill
7
+
8
+ Use this skill when the user wants to send a message to a contact via a messaging channel (WhatsApp, Signal).
9
+
10
+ ## Instructions
11
+
12
+ 1. Identify the target channel (WhatsApp or Signal) and recipient
13
+ 2. Confirm the message content with the user if not explicitly stated
14
+ 3. Use the appropriate channel tool to send the message
15
+ 4. Report success or failure back to the user
16
+
17
+ ## When to Use
18
+
19
+ - "Send a WhatsApp message to Alice saying..."
20
+ - "Text Bob on Signal that..."
21
+ - "Forward this to my team on WhatsApp"
22
+
23
+ ## Important
24
+
25
+ - Always confirm the recipient and message content before sending
26
+ - Do not send messages without explicit user instruction
27
+ - Report delivery status or errors clearly