@matthias-hausberger/beige 0.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.
Files changed (194) hide show
  1. package/LICENSE.md +8 -0
  2. package/README.md +183 -0
  3. package/dist/channels/registry.d.ts +14 -0
  4. package/dist/channels/registry.d.ts.map +1 -0
  5. package/dist/channels/registry.js +14 -0
  6. package/dist/channels/registry.js.map +1 -0
  7. package/dist/channels/telegram.d.ts +92 -0
  8. package/dist/channels/telegram.d.ts.map +1 -0
  9. package/dist/channels/telegram.js +469 -0
  10. package/dist/channels/telegram.js.map +1 -0
  11. package/dist/channels/tui.d.ts +8 -0
  12. package/dist/channels/tui.d.ts.map +1 -0
  13. package/dist/channels/tui.js +574 -0
  14. package/dist/channels/tui.js.map +1 -0
  15. package/dist/cli.d.ts +23 -0
  16. package/dist/cli.d.ts.map +1 -0
  17. package/dist/cli.js +571 -0
  18. package/dist/cli.js.map +1 -0
  19. package/dist/config/loader.d.ts +6 -0
  20. package/dist/config/loader.d.ts.map +1 -0
  21. package/dist/config/loader.js +103 -0
  22. package/dist/config/loader.js.map +1 -0
  23. package/dist/config/loader.spec.d.ts +2 -0
  24. package/dist/config/loader.spec.d.ts.map +1 -0
  25. package/dist/config/loader.spec.js +195 -0
  26. package/dist/config/loader.spec.js.map +1 -0
  27. package/dist/config/schema.d.ts +107 -0
  28. package/dist/config/schema.d.ts.map +1 -0
  29. package/dist/config/schema.js +42 -0
  30. package/dist/config/schema.js.map +1 -0
  31. package/dist/config/schema.spec.d.ts +2 -0
  32. package/dist/config/schema.spec.d.ts.map +1 -0
  33. package/dist/config/schema.spec.js +180 -0
  34. package/dist/config/schema.spec.js.map +1 -0
  35. package/dist/gateway/agent-manager.d.ts +138 -0
  36. package/dist/gateway/agent-manager.d.ts.map +1 -0
  37. package/dist/gateway/agent-manager.js +532 -0
  38. package/dist/gateway/agent-manager.js.map +1 -0
  39. package/dist/gateway/api.d.ts +43 -0
  40. package/dist/gateway/api.d.ts.map +1 -0
  41. package/dist/gateway/api.js +256 -0
  42. package/dist/gateway/api.js.map +1 -0
  43. package/dist/gateway/api.spec.d.ts +2 -0
  44. package/dist/gateway/api.spec.d.ts.map +1 -0
  45. package/dist/gateway/api.spec.js +256 -0
  46. package/dist/gateway/api.spec.js.map +1 -0
  47. package/dist/gateway/audit.d.ts +38 -0
  48. package/dist/gateway/audit.d.ts.map +1 -0
  49. package/dist/gateway/audit.js +82 -0
  50. package/dist/gateway/audit.js.map +1 -0
  51. package/dist/gateway/audit.spec.d.ts +2 -0
  52. package/dist/gateway/audit.spec.d.ts.map +1 -0
  53. package/dist/gateway/audit.spec.js +212 -0
  54. package/dist/gateway/audit.spec.js.map +1 -0
  55. package/dist/gateway/gateway.d.ts +27 -0
  56. package/dist/gateway/gateway.d.ts.map +1 -0
  57. package/dist/gateway/gateway.js +158 -0
  58. package/dist/gateway/gateway.js.map +1 -0
  59. package/dist/gateway/policy.d.ts +27 -0
  60. package/dist/gateway/policy.d.ts.map +1 -0
  61. package/dist/gateway/policy.js +40 -0
  62. package/dist/gateway/policy.js.map +1 -0
  63. package/dist/gateway/policy.spec.d.ts +2 -0
  64. package/dist/gateway/policy.spec.d.ts.map +1 -0
  65. package/dist/gateway/policy.spec.js +121 -0
  66. package/dist/gateway/policy.spec.js.map +1 -0
  67. package/dist/gateway/provider-health.d.ts +83 -0
  68. package/dist/gateway/provider-health.d.ts.map +1 -0
  69. package/dist/gateway/provider-health.js +219 -0
  70. package/dist/gateway/provider-health.js.map +1 -0
  71. package/dist/gateway/provider-health.spec.d.ts +2 -0
  72. package/dist/gateway/provider-health.spec.d.ts.map +1 -0
  73. package/dist/gateway/provider-health.spec.js +278 -0
  74. package/dist/gateway/provider-health.spec.js.map +1 -0
  75. package/dist/gateway/session-settings.d.ts +62 -0
  76. package/dist/gateway/session-settings.d.ts.map +1 -0
  77. package/dist/gateway/session-settings.js +91 -0
  78. package/dist/gateway/session-settings.js.map +1 -0
  79. package/dist/gateway/session-settings.spec.d.ts +2 -0
  80. package/dist/gateway/session-settings.spec.d.ts.map +1 -0
  81. package/dist/gateway/session-settings.spec.js +141 -0
  82. package/dist/gateway/session-settings.spec.js.map +1 -0
  83. package/dist/gateway/sessions.d.ts +68 -0
  84. package/dist/gateway/sessions.d.ts.map +1 -0
  85. package/dist/gateway/sessions.js +177 -0
  86. package/dist/gateway/sessions.js.map +1 -0
  87. package/dist/gateway/sessions.spec.d.ts +2 -0
  88. package/dist/gateway/sessions.spec.d.ts.map +1 -0
  89. package/dist/gateway/sessions.spec.js +190 -0
  90. package/dist/gateway/sessions.spec.js.map +1 -0
  91. package/dist/index.d.ts +11 -0
  92. package/dist/index.d.ts.map +1 -0
  93. package/dist/index.js +10 -0
  94. package/dist/index.js.map +1 -0
  95. package/dist/install.d.ts +39 -0
  96. package/dist/install.d.ts.map +1 -0
  97. package/dist/install.js +144 -0
  98. package/dist/install.js.map +1 -0
  99. package/dist/sandbox/manager.d.ts +63 -0
  100. package/dist/sandbox/manager.d.ts.map +1 -0
  101. package/dist/sandbox/manager.js +294 -0
  102. package/dist/sandbox/manager.js.map +1 -0
  103. package/dist/skills/index.d.ts +2 -0
  104. package/dist/skills/index.d.ts.map +1 -0
  105. package/dist/skills/index.js +2 -0
  106. package/dist/skills/index.js.map +1 -0
  107. package/dist/skills/registry.d.ts +11 -0
  108. package/dist/skills/registry.d.ts.map +1 -0
  109. package/dist/skills/registry.js +86 -0
  110. package/dist/skills/registry.js.map +1 -0
  111. package/dist/skills/registry.spec.d.ts +2 -0
  112. package/dist/skills/registry.spec.d.ts.map +1 -0
  113. package/dist/skills/registry.spec.js +220 -0
  114. package/dist/skills/registry.spec.js.map +1 -0
  115. package/dist/socket/protocol.d.ts +21 -0
  116. package/dist/socket/protocol.d.ts.map +1 -0
  117. package/dist/socket/protocol.js +7 -0
  118. package/dist/socket/protocol.js.map +1 -0
  119. package/dist/socket/protocol.spec.d.ts +2 -0
  120. package/dist/socket/protocol.spec.d.ts.map +1 -0
  121. package/dist/socket/protocol.spec.js +135 -0
  122. package/dist/socket/protocol.spec.js.map +1 -0
  123. package/dist/socket/server.d.ts +21 -0
  124. package/dist/socket/server.d.ts.map +1 -0
  125. package/dist/socket/server.js +133 -0
  126. package/dist/socket/server.js.map +1 -0
  127. package/dist/socket/server.spec.d.ts +2 -0
  128. package/dist/socket/server.spec.d.ts.map +1 -0
  129. package/dist/socket/server.spec.js +333 -0
  130. package/dist/socket/server.spec.js.map +1 -0
  131. package/dist/test/fixtures.d.ts +47 -0
  132. package/dist/test/fixtures.d.ts.map +1 -0
  133. package/dist/test/fixtures.js +144 -0
  134. package/dist/test/fixtures.js.map +1 -0
  135. package/dist/toolkit/index.d.ts +4 -0
  136. package/dist/toolkit/index.d.ts.map +1 -0
  137. package/dist/toolkit/index.js +4 -0
  138. package/dist/toolkit/index.js.map +1 -0
  139. package/dist/toolkit/installer.d.ts +26 -0
  140. package/dist/toolkit/installer.d.ts.map +1 -0
  141. package/dist/toolkit/installer.js +247 -0
  142. package/dist/toolkit/installer.js.map +1 -0
  143. package/dist/toolkit/registry.d.ts +19 -0
  144. package/dist/toolkit/registry.d.ts.map +1 -0
  145. package/dist/toolkit/registry.js +119 -0
  146. package/dist/toolkit/registry.js.map +1 -0
  147. package/dist/toolkit/registry.spec.d.ts +2 -0
  148. package/dist/toolkit/registry.spec.d.ts.map +1 -0
  149. package/dist/toolkit/registry.spec.js +194 -0
  150. package/dist/toolkit/registry.spec.js.map +1 -0
  151. package/dist/toolkit/schema.d.ts +61 -0
  152. package/dist/toolkit/schema.d.ts.map +1 -0
  153. package/dist/toolkit/schema.js +116 -0
  154. package/dist/toolkit/schema.js.map +1 -0
  155. package/dist/toolkit/schema.spec.d.ts +2 -0
  156. package/dist/toolkit/schema.spec.d.ts.map +1 -0
  157. package/dist/toolkit/schema.spec.js +202 -0
  158. package/dist/toolkit/schema.spec.js.map +1 -0
  159. package/dist/tools/core.d.ts +10 -0
  160. package/dist/tools/core.d.ts.map +1 -0
  161. package/dist/tools/core.js +246 -0
  162. package/dist/tools/core.js.map +1 -0
  163. package/dist/tools/core.spec.d.ts +2 -0
  164. package/dist/tools/core.spec.d.ts.map +1 -0
  165. package/dist/tools/core.spec.js +315 -0
  166. package/dist/tools/core.spec.js.map +1 -0
  167. package/dist/tools/registry.d.ts +15 -0
  168. package/dist/tools/registry.d.ts.map +1 -0
  169. package/dist/tools/registry.js +62 -0
  170. package/dist/tools/registry.js.map +1 -0
  171. package/dist/tools/registry.spec.d.ts +2 -0
  172. package/dist/tools/registry.spec.d.ts.map +1 -0
  173. package/dist/tools/registry.spec.js +228 -0
  174. package/dist/tools/registry.spec.js.map +1 -0
  175. package/dist/tools/runner.d.ts +25 -0
  176. package/dist/tools/runner.d.ts.map +1 -0
  177. package/dist/tools/runner.js +35 -0
  178. package/dist/tools/runner.js.map +1 -0
  179. package/dist/tools/runner.spec.d.ts +2 -0
  180. package/dist/tools/runner.spec.d.ts.map +1 -0
  181. package/dist/tools/runner.spec.js +129 -0
  182. package/dist/tools/runner.spec.js.map +1 -0
  183. package/dist/types/session.d.ts +8 -0
  184. package/dist/types/session.d.ts.map +1 -0
  185. package/dist/types/session.js +23 -0
  186. package/dist/types/session.js.map +1 -0
  187. package/package.json +76 -0
  188. package/tools/README.md +1 -0
  189. package/tools/kv/README.md +150 -0
  190. package/tools/kv/index.ts +149 -0
  191. package/tools/kv/tool.json +23 -0
  192. package/tools/message/README.md +53 -0
  193. package/tools/message/index.ts +183 -0
  194. package/tools/message/tool.json +11 -0
package/package.json ADDED
@@ -0,0 +1,76 @@
1
+ {
2
+ "name": "@matthias-hausberger/beige",
3
+ "version": "0.0.1",
4
+ "description": "Secure sandboxed agent system",
5
+ "publishConfig": {
6
+ "access": "public"
7
+ },
8
+ "type": "module",
9
+ "main": "dist/index.js",
10
+ "types": "dist/index.d.ts",
11
+ "exports": {
12
+ ".": {
13
+ "types": "./dist/index.d.ts",
14
+ "import": "./dist/index.js"
15
+ }
16
+ },
17
+ "bin": {
18
+ "beige": "dist/cli.js"
19
+ },
20
+ "files": [
21
+ "dist",
22
+ "tools"
23
+ ],
24
+ "scripts": {
25
+ "beige": "tsx src/cli.ts",
26
+ "build": "tsc",
27
+ "start": "node dist/cli.js",
28
+ "build:sandbox": "docker build -t beige-sandbox:latest ./sandbox",
29
+ "test": "vitest run",
30
+ "test:watch": "vitest",
31
+ "test:coverage": "vitest run --coverage",
32
+ "docs:dev": "cd docs && mintlify dev --port 3333",
33
+ "docs:build": "cd docs && mintlify build",
34
+ "docs:deploy": "cd docs && mintlify deploy"
35
+ },
36
+ "dependencies": {
37
+ "@mariozechner/pi-ai": "^0.56.1",
38
+ "@mariozechner/pi-coding-agent": "latest",
39
+ "@sinclair/typebox": "^0.34.0",
40
+ "dockerode": "^4.0.5",
41
+ "grammy": "^1.35.0",
42
+ "json5": "^2.2.3",
43
+ "tar": "^7.5.11"
44
+ },
45
+ "devDependencies": {
46
+ "@types/dockerode": "^3.3.38",
47
+ "@types/node": "^22.0.0",
48
+ "@types/tar": "^7.0.87",
49
+ "@vitest/coverage-v8": "^4.0.18",
50
+ "mintlify": "^4.2.417",
51
+ "tsx": "^4.19.0",
52
+ "typescript": "^5.7.0",
53
+ "vitest": "^4.0.18"
54
+ },
55
+ "engines": {
56
+ "node": ">=22.0.0"
57
+ },
58
+ "keywords": [
59
+ "ai",
60
+ "agent",
61
+ "sandbox",
62
+ "docker",
63
+ "llm",
64
+ "claude",
65
+ "anthropic"
66
+ ],
67
+ "repository": {
68
+ "type": "git",
69
+ "url": "git+https://github.com/matthias-hausberger/beige.git"
70
+ },
71
+ "bugs": {
72
+ "url": "https://github.com/matthias-hausberger/beige/issues"
73
+ },
74
+ "homepage": "https://github.com/matthias-hausberger/beige#readme",
75
+ "license": "MIT"
76
+ }
@@ -0,0 +1 @@
1
+ This folder showcases examples for tools. They are NOT added to an agent by default and NOT used.
@@ -0,0 +1,150 @@
1
+ # KV Tool
2
+
3
+ Simple key-value store. Data persists on the gateway host at `~/.beige/data/kv.json` across sessions and gateway restarts.
4
+
5
+ ## Usage
6
+
7
+ ```sh
8
+ /tools/bin/kv set <key> <value> # Store a value
9
+ /tools/bin/kv get <key> # Retrieve a value
10
+ /tools/bin/kv del <key> # Delete a key
11
+ /tools/bin/kv list # List all keys with their values
12
+ ```
13
+
14
+ ## Examples
15
+
16
+ ### Basic Operations
17
+
18
+ ```sh
19
+ # Store a travel note
20
+ /tools/bin/kv set trip:paris "Flying March 15, Hotel Lumiere"
21
+
22
+ # Retrieve it
23
+ /tools/bin/kv get trip:paris
24
+ # → Flying March 15, Hotel Lumiere
25
+
26
+ # List all stored keys
27
+ /tools/bin/kv list
28
+ # → trip:paris = Flying March 15, Hotel Lumiere
29
+
30
+ # Delete
31
+ /tools/bin/kv del trip:paris
32
+ # → Deleted: trip:paris
33
+ ```
34
+
35
+ ### Using Keys with Namespaces
36
+
37
+ ```sh
38
+ # Organize with colon-separated namespaces
39
+ /tools/bin/kv set user:alice:email "alice@example.com"
40
+ /tools/bin/kv set user:bob:email "bob@example.com"
41
+ /tools/bin/kv set config:timezone "Europe/Berlin"
42
+
43
+ # List shows all keys
44
+ /tools/bin/kv list
45
+ # → user:alice:email = alice@example.com
46
+ # → user:bob:email = bob@example.com
47
+ # → config:timezone = Europe/Berlin
48
+ ```
49
+
50
+ ### Storing JSON Data
51
+
52
+ ```sh
53
+ # Store structured data as JSON string
54
+ /tools/bin/kv set project:config '{"name": "beige", "version": "0.1.0"}'
55
+
56
+ # Retrieve and parse with jq
57
+ /tools/bin/kv get project:config | jq -r '.name'
58
+ # → beige
59
+ ```
60
+
61
+ ### Error Handling
62
+
63
+ ```sh
64
+ # Getting a non-existent key
65
+ /tools/bin/kv get nonexistent
66
+ # → Key not found: nonexistent
67
+ # (exit code: 1)
68
+
69
+ # Deleting a non-existent key
70
+ /tools/bin/kv del nonexistent
71
+ # → Key not found: nonexistent
72
+ # (exit code: 1)
73
+ ```
74
+
75
+ ## Access Control
76
+
77
+ The commands available to an agent are controlled via the tool's `config` block in `config.json5`.
78
+ Two optional fields let you allowlist and/or denylist specific commands:
79
+
80
+ | Config field | Type | Default | Description |
81
+ |---|---|---|---|
82
+ | `allowCommands` | `string \| string[]` | all commands | Only these commands are permitted. |
83
+ | `denyCommands` | `string \| string[]` | *(none)* | These commands are always blocked. Deny beats allow. |
84
+
85
+ **Example — read-only agent** (can only `get` and `list`):
86
+
87
+ ```json5
88
+ tools: {
89
+ "kv-readonly": {
90
+ path: "./tools/kv",
91
+ target: "gateway",
92
+ config: {
93
+ allowCommands: ["get", "list"],
94
+ },
95
+ },
96
+ },
97
+ agents: {
98
+ reader: { tools: ["kv-readonly"] },
99
+ },
100
+ ```
101
+
102
+ **Example — write-only agent** (can `set` and `del`, cannot read):
103
+
104
+ ```json5
105
+ tools: {
106
+ "kv-writeonly": {
107
+ path: "./tools/kv",
108
+ target: "gateway",
109
+ config: {
110
+ allowCommands: ["set", "del"],
111
+ },
112
+ },
113
+ },
114
+ ```
115
+
116
+ **Example — deny a single command on an otherwise full-access tool** (no `del`):
117
+
118
+ ```json5
119
+ tools: {
120
+ "kv-nodelete": {
121
+ path: "./tools/kv",
122
+ target: "gateway",
123
+ config: {
124
+ denyCommands: ["del"],
125
+ },
126
+ },
127
+ },
128
+ ```
129
+
130
+ When a denied command is called, the tool exits with code `1` and prints a clear error:
131
+
132
+ ```
133
+ Permission denied: command 'del' is not allowed for this agent.
134
+ Permitted commands: set, get, list
135
+ ```
136
+
137
+ ## Notes
138
+
139
+ - Keys and values are strings.
140
+ - Values with spaces must be passed as a single argument (the tool joins all args after the key).
141
+ - Data is stored as JSON on the gateway host at `~/.beige/data/kv.json`. Agents cannot access the raw storage file directly — they must use the tool commands.
142
+ - The same physical KV store is shared across all agents. Use `allowCommands` / `denyCommands` to restrict which operations each agent can perform, not which keys it can see.
143
+ - If you need key-level isolation between agents, create multiple tool instances with different configs (future enhancement).
144
+
145
+ ## Implementation Details
146
+
147
+ - **Target**: Gateway (runs on the host, not in the sandbox)
148
+ - **Storage**: `~/.beige/data/kv.json`
149
+ - **Protocol**: Tool launcher calls back to gateway via Unix socket
150
+ - **Type**: See `tools/kv/index.ts` for the handler implementation
@@ -0,0 +1,149 @@
1
+ import { readFileSync, writeFileSync, mkdirSync } from "fs";
2
+ import { resolve } from "path";
3
+ import { homedir } from "os";
4
+
5
+ // ToolHandler type is defined here inline so this file is self-contained.
6
+ // It can be installed anywhere (e.g. ~/.beige/tools/kv/) without needing the
7
+ // beige source tree.
8
+ type ToolHandler = (args: string[], config?: Record<string, unknown>) => Promise<{ output: string; exitCode: number }>;
9
+
10
+ /** All commands the KV tool exposes. */
11
+ const ALL_COMMANDS = ["set", "get", "del", "list"] as const;
12
+ type KVCommand = (typeof ALL_COMMANDS)[number];
13
+
14
+ /**
15
+ * Resolve which commands are permitted given the tool config.
16
+ *
17
+ * Config fields (both optional, strings or arrays of strings):
18
+ * allowCommands — whitelist; only these commands are permitted.
19
+ * Defaults to all commands when absent.
20
+ * denyCommands — blacklist; these commands are always blocked,
21
+ * even if present in allowCommands.
22
+ *
23
+ * Precedence: deny beats allow.
24
+ */
25
+ function resolveAllowedCommands(config: Record<string, unknown>): Set<KVCommand> {
26
+ const toArray = (value: unknown): string[] => {
27
+ if (Array.isArray(value)) return value.map(String);
28
+ if (typeof value === "string") return [value];
29
+ return [];
30
+ };
31
+
32
+ const allowed = new Set<KVCommand>(
33
+ config.allowCommands !== undefined
34
+ ? (toArray(config.allowCommands).filter((c) =>
35
+ (ALL_COMMANDS as readonly string[]).includes(c)
36
+ ) as KVCommand[])
37
+ : ALL_COMMANDS
38
+ );
39
+
40
+ for (const cmd of toArray(config.denyCommands)) {
41
+ allowed.delete(cmd as KVCommand);
42
+ }
43
+
44
+ return allowed;
45
+ }
46
+
47
+ /**
48
+ * KV Tool — Simple key-value store that persists to disk.
49
+ * Executes on the gateway host.
50
+ *
51
+ * Commands:
52
+ * set <key> <value> — Store a value
53
+ * get <key> — Retrieve a value
54
+ * del <key> — Delete a key
55
+ * list — List all keys
56
+ *
57
+ * Config:
58
+ * allowCommands — only permit these commands (default: all)
59
+ * denyCommands — always block these commands (deny beats allow)
60
+ */
61
+ export function createHandler(config: Record<string, unknown>): ToolHandler {
62
+ const storePath = resolve(homedir(), ".beige", "data", "kv.json");
63
+ mkdirSync(resolve(homedir(), ".beige", "data"), { recursive: true });
64
+
65
+ const allowedCommands = resolveAllowedCommands(config);
66
+
67
+ function loadStore(): Record<string, string> {
68
+ try {
69
+ return JSON.parse(readFileSync(storePath, "utf-8"));
70
+ } catch {
71
+ return {};
72
+ }
73
+ }
74
+
75
+ function saveStore(store: Record<string, string>): void {
76
+ writeFileSync(storePath, JSON.stringify(store, null, 2));
77
+ }
78
+
79
+ return async (args: string[]) => {
80
+ const command = args[0];
81
+
82
+ // Access-control check — runs before any business logic.
83
+ if (command && !allowedCommands.has(command as KVCommand)) {
84
+ const permitted = [...allowedCommands].join(", ") || "(none)";
85
+ return {
86
+ output: `Permission denied: command '${command}' is not allowed for this agent.\nPermitted commands: ${permitted}`,
87
+ exitCode: 1,
88
+ };
89
+ }
90
+
91
+ switch (command) {
92
+ case "set": {
93
+ const key = args[1];
94
+ const value = args.slice(2).join(" ");
95
+ if (!key || !value) {
96
+ return { output: "Usage: kv set <key> <value>", exitCode: 1 };
97
+ }
98
+ const store = loadStore();
99
+ store[key] = value;
100
+ saveStore(store);
101
+ return { output: `OK: ${key} = ${value}`, exitCode: 0 };
102
+ }
103
+
104
+ case "get": {
105
+ const key = args[1];
106
+ if (!key) {
107
+ return { output: "Usage: kv get <key>", exitCode: 1 };
108
+ }
109
+ const store = loadStore();
110
+ if (key in store) {
111
+ return { output: store[key], exitCode: 0 };
112
+ }
113
+ return { output: `Key not found: ${key}`, exitCode: 1 };
114
+ }
115
+
116
+ case "del": {
117
+ const key = args[1];
118
+ if (!key) {
119
+ return { output: "Usage: kv del <key>", exitCode: 1 };
120
+ }
121
+ const store = loadStore();
122
+ if (key in store) {
123
+ delete store[key];
124
+ saveStore(store);
125
+ return { output: `Deleted: ${key}`, exitCode: 0 };
126
+ }
127
+ return { output: `Key not found: ${key}`, exitCode: 1 };
128
+ }
129
+
130
+ case "list": {
131
+ const store = loadStore();
132
+ const keys = Object.keys(store);
133
+ if (keys.length === 0) {
134
+ return { output: "(empty)", exitCode: 0 };
135
+ }
136
+ return {
137
+ output: keys.map((k) => `${k} = ${store[k]}`).join("\n"),
138
+ exitCode: 0,
139
+ };
140
+ }
141
+
142
+ default:
143
+ return {
144
+ output: `Unknown command: ${command}\nUsage: kv <set|get|del|list> [args...]`,
145
+ exitCode: 1,
146
+ };
147
+ }
148
+ };
149
+ }
@@ -0,0 +1,23 @@
1
+ {
2
+ "name": "kv",
3
+ "description": "Simple key-value store. Store and retrieve values by key. Data persists across sessions.",
4
+ "commands": [
5
+ "set <key> <value> — Store a value",
6
+ "get <key> — Retrieve a value",
7
+ "del <key> — Delete a key",
8
+ "list — List all keys"
9
+ ],
10
+ "target": "gateway",
11
+ "config": {
12
+ "allowCommands": {
13
+ "type": "string | string[]",
14
+ "description": "Whitelist of permitted commands. Defaults to all commands when absent.",
15
+ "example": ["get", "list"]
16
+ },
17
+ "denyCommands": {
18
+ "type": "string | string[]",
19
+ "description": "Blacklist of blocked commands. Takes precedence over allowCommands.",
20
+ "example": ["del"]
21
+ }
22
+ }
23
+ }
@@ -0,0 +1,53 @@
1
+ # Message Tool
2
+
3
+ Send messages to users through configured channels (currently Telegram only).
4
+
5
+ ## Commands
6
+
7
+ ### Send to Current Session
8
+
9
+ Send a message to the current session's channel/chat/thread:
10
+
11
+ ```bash
12
+ /tools/bin/message --to-current-session -- Hello! This is a reply.
13
+ ```
14
+
15
+ This only works when called from within an active LLM session. If called from a standalone script, you'll get an error and should use explicit targeting instead.
16
+
17
+ ### Send to Specific Telegram Chat
18
+
19
+ Send to a specific chat (proactive messaging):
20
+
21
+ ```bash
22
+ /tools/bin/message telegram 123456789 -- Hello! This is a proactive message.
23
+ ```
24
+
25
+ Send to a specific thread in a Telegram forum:
26
+
27
+ ```bash
28
+ /tools/bin/message telegram 123456789 42 -- Hello from the thread!
29
+ ```
30
+
31
+ ### With Formatting
32
+
33
+ Use `--parse-mode` for formatted messages (Markdown or HTML):
34
+
35
+ ```bash
36
+ # Markdown
37
+ /tools/bin/message telegram 123456789 -- --parse-mode markdown -- **Bold** and _italic_ text
38
+
39
+ # HTML
40
+ /tools/bin/message telegram 123456789 -- --parse-mode html -- <b>Bold</b> and <i>italic</i> text
41
+ ```
42
+
43
+ Note: Telegram uses MarkdownV2 syntax. See [Telegram's formatting docs](https://core.telegram.org/bots/api#markdownv2-style) for details.
44
+
45
+ ## Error Handling
46
+
47
+ - **No session context**: When using `--to-current-session` from a script, you'll get an error with guidance to use explicit targeting.
48
+ - **Unsupported channel**: If the current session's channel doesn't support messaging (e.g., TUI), you'll get an error.
49
+ - **Channel not available**: If Telegram isn't enabled in the gateway config, explicit Telegram commands will fail.
50
+
51
+ ## Long Messages
52
+
53
+ Messages longer than Telegram's 4096 character limit are automatically split into multiple messages.
@@ -0,0 +1,183 @@
1
+ type ToolHandler = (
2
+ args: string[],
3
+ config?: Record<string, unknown>,
4
+ sessionContext?: SessionContext
5
+ ) => Promise<{ output: string; exitCode: number }>;
6
+
7
+ interface SessionContext {
8
+ sessionKey: string;
9
+ channel: string;
10
+ chatId?: string;
11
+ threadId?: string;
12
+ }
13
+
14
+ interface ToolHandlerContext {
15
+ channelRegistry?: ChannelRegistry;
16
+ }
17
+
18
+ interface ChannelRegistry {
19
+ get(channel: string): ChannelAdapter | undefined;
20
+ has(channel: string): boolean;
21
+ }
22
+
23
+ interface ChannelAdapter {
24
+ sendMessage(
25
+ chatId: string,
26
+ threadId: string | undefined,
27
+ text: string,
28
+ options?: { parseMode?: "html" | "markdown" }
29
+ ): Promise<void>;
30
+ supportsMessaging(): boolean;
31
+ }
32
+
33
+ interface ParsedCommand {
34
+ action: "current" | "telegram";
35
+ chatId?: string;
36
+ threadId?: string;
37
+ parseMode?: "html" | "markdown";
38
+ text: string;
39
+ }
40
+
41
+ function parseArgs(args: string[]): ParsedCommand | { error: string } {
42
+ if (args.length === 0) {
43
+ return { error: "No arguments provided. Usage: message --to-current-session -- <text> | message telegram <chatId> [-- <threadId>] -- [--parse-mode <mode>] -- <text>" };
44
+ }
45
+
46
+ if (args[0] === "--to-current-session") {
47
+ const doubleDashIndex = args.indexOf("--");
48
+ if (doubleDashIndex === -1 || doubleDashIndex === args.length - 1) {
49
+ return { error: "Missing message text after --. Usage: message --to-current-session -- <text>" };
50
+ }
51
+ const textParts: string[] = [];
52
+ let i = doubleDashIndex + 1;
53
+ while (i < args.length) {
54
+ textParts.push(args[i]);
55
+ i++;
56
+ }
57
+ return {
58
+ action: "current",
59
+ text: textParts.join(" "),
60
+ };
61
+ }
62
+
63
+ if (args[0] === "telegram") {
64
+ if (args.length < 3) {
65
+ return { error: "Missing arguments. Usage: message telegram <chatId> [-- <threadId>] -- [--parse-mode <mode>] -- <text>" };
66
+ }
67
+
68
+ const chatId = args[1];
69
+ let threadId: string | undefined;
70
+ let parseMode: "html" | "markdown" | undefined;
71
+ let textStartIndex = 2;
72
+
73
+ // Look for threadId (must be before the final --)
74
+ const lastDoubleDash = args.lastIndexOf("--");
75
+ if (lastDoubleDash === -1 || lastDoubleDash === args.length - 1) {
76
+ return { error: "Missing message text after --. Usage: message telegram <chatId> [-- <threadId>] -- [--parse-mode <mode>] -- <text>" };
77
+ }
78
+
79
+ // Check for --parse-mode before the final --
80
+ for (let i = 2; i < lastDoubleDash; i++) {
81
+ if (args[i] === "--parse-mode" && i + 1 < lastDoubleDash) {
82
+ const mode = args[i + 1];
83
+ if (mode === "html" || mode === "markdown") {
84
+ parseMode = mode;
85
+ } else {
86
+ return { error: `Invalid parse mode: ${mode}. Must be 'html' or 'markdown'.` };
87
+ }
88
+ } else if (!isNaN(Number(args[i])) && !threadId && args[i - 1] !== "--parse-mode") {
89
+ // If it's a number and we haven't set threadId yet, it might be threadId
90
+ threadId = args[i];
91
+ }
92
+ }
93
+
94
+ const text = args.slice(lastDoubleDash + 1).join(" ");
95
+ if (!text) {
96
+ return { error: "Missing message text after --" };
97
+ }
98
+
99
+ return {
100
+ action: "telegram",
101
+ chatId,
102
+ threadId,
103
+ parseMode,
104
+ text,
105
+ };
106
+ }
107
+
108
+ return { error: `Unknown action: ${args[0]}. Use '--to-current-session' or 'telegram'.` };
109
+ }
110
+
111
+ export function createHandler(config: Record<string, unknown>, context: ToolHandlerContext): ToolHandler {
112
+ const channelRegistry = context.channelRegistry;
113
+
114
+ return async (args: string[], _config?: Record<string, unknown>, sessionContext?: SessionContext) => {
115
+ const parsed = parseArgs(args);
116
+ if ("error" in parsed) {
117
+ return { output: `Error: ${parsed.error}`, exitCode: 1 };
118
+ }
119
+
120
+ if (parsed.action === "current") {
121
+ if (!sessionContext) {
122
+ return {
123
+ output: "Error: No session context available. This command must be run from within an active LLM session, not from a standalone script.\n\nUse explicit targeting instead: message telegram <chatId> -- <text>",
124
+ exitCode: 1,
125
+ };
126
+ }
127
+
128
+ const adapter = channelRegistry?.get(sessionContext.channel);
129
+ if (!adapter) {
130
+ return {
131
+ output: `Error: Channel '${sessionContext.channel}' is not available or not registered.`,
132
+ exitCode: 1,
133
+ };
134
+ }
135
+
136
+ if (!adapter.supportsMessaging()) {
137
+ return {
138
+ output: `Error: The current session's channel ('${sessionContext.channel}') doesn't support messaging.`,
139
+ exitCode: 1,
140
+ };
141
+ }
142
+
143
+ if (!sessionContext.chatId) {
144
+ return {
145
+ output: `Error: The current session does not have a chat ID. Cannot send message.`,
146
+ exitCode: 1,
147
+ };
148
+ }
149
+
150
+ try {
151
+ await adapter.sendMessage(sessionContext.chatId, sessionContext.threadId, parsed.text);
152
+ return { output: "Message sent successfully.", exitCode: 0 };
153
+ } catch (err) {
154
+ const msg = err instanceof Error ? err.message : String(err);
155
+ return { output: `Error sending message: ${msg}`, exitCode: 1 };
156
+ }
157
+ }
158
+
159
+ if (parsed.action === "telegram") {
160
+ if (!parsed.chatId) {
161
+ return { output: "Error: Missing chat ID for Telegram message.", exitCode: 1 };
162
+ }
163
+
164
+ const adapter = channelRegistry?.get("telegram");
165
+ if (!adapter) {
166
+ return {
167
+ output: "Error: Telegram channel is not available. Make sure Telegram is enabled in the gateway config.",
168
+ exitCode: 1,
169
+ };
170
+ }
171
+
172
+ try {
173
+ await adapter.sendMessage(parsed.chatId, parsed.threadId, parsed.text, { parseMode: parsed.parseMode });
174
+ return { output: "Message sent successfully to Telegram.", exitCode: 0 };
175
+ } catch (err) {
176
+ const msg = err instanceof Error ? err.message : String(err);
177
+ return { output: `Error sending message to Telegram: ${msg}`, exitCode: 1 };
178
+ }
179
+ }
180
+
181
+ return { output: "Error: Unknown action.", exitCode: 1 };
182
+ };
183
+ }
@@ -0,0 +1,11 @@
1
+ {
2
+ "name": "message",
3
+ "description": "Send messages to users through configured channels (Telegram). Can reply to the current session or send proactively to any chat.",
4
+ "commands": [
5
+ "--to-current-session -- <text> — Send to the current session's channel/chat/thread",
6
+ "telegram <chatId> -- <text> — Send to a specific Telegram chat",
7
+ "telegram <chatId> <threadId> -- <text> — Send to a specific Telegram thread",
8
+ "telegram <chatId> -- --parse-mode <mode> -- <text> — Send with formatting (html|markdown)"
9
+ ],
10
+ "target": "gateway"
11
+ }