clawmini 0.0.1 → 0.0.3

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 (103) hide show
  1. package/.github/workflows/ci.yml +59 -0
  2. package/README.md +61 -76
  3. package/dist/adapter-discord/index.d.mts.map +1 -1
  4. package/dist/adapter-discord/index.mjs +13 -4
  5. package/dist/adapter-discord/index.mjs.map +1 -1
  6. package/dist/cli/index.mjs +8 -6
  7. package/dist/cli/index.mjs.map +1 -1
  8. package/dist/cli/lite.mjs +64 -10
  9. package/dist/cli/lite.mjs.map +1 -1
  10. package/dist/daemon/index.mjs +732 -251
  11. package/dist/daemon/index.mjs.map +1 -1
  12. package/dist/{fetch-BjZVyU3Z.mjs → fetch-Cn1XNyiO.mjs} +1 -1
  13. package/dist/{fetch-BjZVyU3Z.mjs.map → fetch-Cn1XNyiO.mjs.map} +1 -1
  14. package/dist/lite-oSYSvaOr.mjs +164 -0
  15. package/dist/lite-oSYSvaOr.mjs.map +1 -0
  16. package/dist/web/_app/immutable/chunks/{COekwvP2.js → 8YNcRyEk.js} +1 -1
  17. package/dist/web/_app/immutable/chunks/{CSvS_NwK.js → DQoygso7.js} +1 -1
  18. package/dist/web/_app/immutable/entry/{app.B-vZe7PN.js → app.DO5eYwVz.js} +2 -2
  19. package/dist/web/_app/immutable/entry/start.D48mVn1m.js +1 -0
  20. package/dist/web/_app/immutable/nodes/{0.B5WFN0zw.js → 0.B-0CcADM.js} +1 -1
  21. package/dist/web/_app/immutable/nodes/{1.D1wtJb2k.js → 1.FixKgvRO.js} +1 -1
  22. package/dist/web/_app/immutable/nodes/{3.BB5wCoBf.js → 3.ncP0xLO6.js} +1 -1
  23. package/dist/web/_app/immutable/nodes/{4.Dr2jvAXK.js → 4.CQYJEgv8.js} +1 -1
  24. package/dist/web/_app/immutable/nodes/{5.BJl7oM3b.js → 5.BpJUN6QH.js} +1 -1
  25. package/dist/web/_app/version.json +1 -1
  26. package/dist/web/index.html +6 -6
  27. package/dist/{workspace-CSgfo_2J.mjs → workspace-DjoNjhW0.mjs} +21 -40
  28. package/dist/workspace-DjoNjhW0.mjs.map +1 -0
  29. package/docs/15_lite_fetch_pending/development_log.md +31 -0
  30. package/docs/15_lite_fetch_pending/notes.md +48 -0
  31. package/docs/15_lite_fetch_pending/prd.md +39 -0
  32. package/docs/15_lite_fetch_pending/questions.md +3 -0
  33. package/docs/15_lite_fetch_pending/tickets.md +42 -0
  34. package/docs/CHECKS.md +2 -2
  35. package/docs/CLI_REFERENCE.md +35 -0
  36. package/docs/guides/sandbox_policies.md +12 -5
  37. package/eslint.config.js +12 -0
  38. package/package.json +3 -2
  39. package/src/adapter-discord/client.ts +1 -1
  40. package/src/adapter-discord/index.ts +22 -5
  41. package/src/cli/client.ts +8 -3
  42. package/src/cli/e2e/adapter-discord.test.ts +2 -2
  43. package/src/cli/e2e/daemon.test.ts +2 -1
  44. package/src/cli/e2e/export-lite-func.test.ts +41 -13
  45. package/src/cli/e2e/fallbacks.test.ts +4 -0
  46. package/src/cli/lite.ts +24 -6
  47. package/src/daemon/api/agent-router.ts +191 -0
  48. package/src/daemon/{router.test.ts → api/index.test.ts} +101 -34
  49. package/src/daemon/api/index.ts +4 -0
  50. package/src/daemon/{router-policy-request.test.ts → api/policy-request.test.ts} +27 -13
  51. package/src/daemon/api/router-utils.ts +159 -0
  52. package/src/daemon/api/trpc.ts +30 -0
  53. package/src/daemon/api/user-router.ts +221 -0
  54. package/src/daemon/index.ts +3 -3
  55. package/src/daemon/message-interruption.test.ts +17 -10
  56. package/src/daemon/message-typing.test.ts +1 -1
  57. package/src/daemon/message.ts +260 -239
  58. package/src/daemon/observation.test.ts +1 -1
  59. package/src/daemon/queue.test.ts +28 -0
  60. package/src/daemon/queue.ts +30 -15
  61. package/src/daemon/request-store.test.ts +4 -4
  62. package/src/daemon/request-store.ts +3 -1
  63. package/src/shared/workspace.ts +4 -5
  64. package/templates/debug/settings.json +5 -0
  65. package/templates/environments/macos/env.json +1 -1
  66. package/templates/environments/macos-proxy/env.json +1 -1
  67. package/templates/gemini-claw/.gemini/hooks/insert-pending.sh +9 -0
  68. package/templates/gemini-claw/.gemini/settings.json +14 -1
  69. package/templates/gemini-claw/.gemini/system.md +2 -0
  70. package/web/.svelte-kit/ambient.d.ts +2 -6
  71. package/web/.svelte-kit/generated/server/internal.js +1 -1
  72. package/web/.svelte-kit/output/client/.vite/manifest.json +29 -29
  73. package/web/.svelte-kit/output/client/_app/immutable/chunks/{COekwvP2.js → 8YNcRyEk.js} +1 -1
  74. package/web/.svelte-kit/output/client/_app/immutable/chunks/{CSvS_NwK.js → DQoygso7.js} +1 -1
  75. package/web/.svelte-kit/output/client/_app/immutable/entry/{app.B-vZe7PN.js → app.DO5eYwVz.js} +2 -2
  76. package/web/.svelte-kit/output/client/_app/immutable/entry/start.D48mVn1m.js +1 -0
  77. package/web/.svelte-kit/output/client/_app/immutable/nodes/{0.B5WFN0zw.js → 0.B-0CcADM.js} +1 -1
  78. package/web/.svelte-kit/output/client/_app/immutable/nodes/{1.D1wtJb2k.js → 1.FixKgvRO.js} +1 -1
  79. package/web/.svelte-kit/output/client/_app/immutable/nodes/{3.BB5wCoBf.js → 3.ncP0xLO6.js} +1 -1
  80. package/web/.svelte-kit/output/client/_app/immutable/nodes/{4.Dr2jvAXK.js → 4.CQYJEgv8.js} +1 -1
  81. package/web/.svelte-kit/output/client/_app/immutable/nodes/{5.BJl7oM3b.js → 5.BpJUN6QH.js} +1 -1
  82. package/web/.svelte-kit/output/client/_app/version.json +1 -1
  83. package/web/.svelte-kit/output/server/chunks/internal.js +1 -1
  84. package/web/.svelte-kit/output/server/manifest-full.js +1 -1
  85. package/web/.svelte-kit/output/server/manifest.js +1 -1
  86. package/web/.svelte-kit/output/server/nodes/0.js +1 -1
  87. package/web/.svelte-kit/output/server/nodes/1.js +1 -1
  88. package/web/.svelte-kit/output/server/nodes/3.js +1 -1
  89. package/web/.svelte-kit/output/server/nodes/4.js +1 -1
  90. package/web/.svelte-kit/output/server/nodes/5.js +1 -1
  91. package/dist/chats-DKgTeU7i.mjs +0 -91
  92. package/dist/chats-DKgTeU7i.mjs.map +0 -1
  93. package/dist/chats-Zd_HXDHx.mjs +0 -29
  94. package/dist/chats-Zd_HXDHx.mjs.map +0 -1
  95. package/dist/fs-B5wW0oaH.mjs +0 -14
  96. package/dist/fs-B5wW0oaH.mjs.map +0 -1
  97. package/dist/lite-Dl7WXyaH.mjs +0 -80
  98. package/dist/lite-Dl7WXyaH.mjs.map +0 -1
  99. package/dist/rolldown-runtime-95iHPtFO.mjs +0 -18
  100. package/dist/web/_app/immutable/entry/start.oP1AgKhs.js +0 -1
  101. package/dist/workspace-CSgfo_2J.mjs.map +0 -1
  102. package/src/daemon/router.ts +0 -510
  103. package/web/.svelte-kit/output/client/_app/immutable/entry/start.oP1AgKhs.js +0 -1
@@ -1,10 +1,10 @@
1
- # Sandbox Policies Guide
1
+ # Configuring Permission Requests
2
2
 
3
3
  The Sandbox Policies feature provides a secure framework for AI agents operating within restricted sandbox environments to request and execute sensitive or network-dependent operations. Using a formal "request-and-approve" workflow, users retain full control via their chat interface while agents gain the ability to perform complex, privileged tasks like sending emails or promoting files to external systems.
4
4
 
5
5
  ## Registering a Policy
6
6
 
7
- Policies are configured centrally in a JSON file located at `.clawmini/policies.json`. This configuration maps an easy-to-use policy command name to the actual script or binary you want to run when the request is approved.
7
+ Policies are configured centrally in a JSON file located at `.clawmini/policies.json`. This configuration maps an easy-to-use policy command name to the actual script or binary you want to run when the request is approved.
8
8
 
9
9
  The framework treats the command execution as a "dumb pipe". It executes the specified script using a secure execution wrapper (bypassing the shell to prevent injection attacks) and interpolates safe, verified file paths.
10
10
 
@@ -33,15 +33,18 @@ Agents running within their environment can interact with the Policies feature u
33
33
 
34
34
  1. **Discovery:**
35
35
  Agents can view available policies and their descriptions:
36
+
36
37
  ```bash
37
38
  clawmini-lite requests list
38
39
  ```
39
40
 
40
41
  2. **Help Documentation:**
41
42
  If a policy is configured with `"allowHelp": true`, agents can query it for help. This securely passes the `--help` flag to the underlying wrapper command and returns the output to the agent:
43
+
42
44
  ```bash
43
45
  clawmini-lite request send-email --help
44
46
  ```
47
+
45
48
  If `"allowHelp"` is missing or set to `false`, the agent will receive an error stating that `--help` is not supported.
46
49
 
47
50
  3. **Submitting a Request:**
@@ -58,19 +61,23 @@ When an agent creates a request, the daemon intercepts it and sends a preview me
58
61
  You can then review and interact with the pending request using the following slash commands in your chat:
59
62
 
60
63
  - **List Pending Requests:**
64
+
61
65
  ```text
62
66
  /pending
63
67
  ```
64
- *Lists all active pending requests that need review.*
68
+
69
+ _Lists all active pending requests that need review._
65
70
 
66
71
  - **Approve a Request:**
72
+
67
73
  ```text
68
74
  /approve <request_id>
69
75
  ```
70
- *Approves the request. The configured script executes securely, and the STDOUT/STDERR results are automatically sent back to the agent in the chat.*
76
+
77
+ _Approves the request. The configured script executes securely, and the STDOUT/STDERR results are automatically sent back to the agent in the chat._
71
78
 
72
79
  - **Reject a Request:**
73
80
  ```text
74
81
  /reject <request_id> [reason]
75
82
  ```
76
- *Rejects the request. You can provide an optional natural language reason (e.g., `/reject 123 Tone needs to be more formal`) to help the agent correct its output and try again.*
83
+ _Rejects the request. You can provide an optional natural language reason (e.g., `/reject 123 Tone needs to be more formal`) to help the agent correct its output and try again._
package/eslint.config.js CHANGED
@@ -34,6 +34,17 @@ export default defineConfig([
34
34
  rules: {
35
35
  '@typescript-eslint/no-explicit-any': 'warn',
36
36
  '@typescript-eslint/no-unused-vars': ['error', { argsIgnorePattern: '^_' }],
37
+ 'no-restricted-syntax': [
38
+ 'error',
39
+ {
40
+ selector: 'ImportExpression',
41
+ message: 'Dynamic imports are not allowed. Please use top-level imports.',
42
+ },
43
+ {
44
+ selector: 'TSImportType',
45
+ message: 'Inline type imports are not allowed. Please use top-level imports.',
46
+ },
47
+ ],
37
48
  'max-lines': ['error', { max: 300, skipBlankLines: true, skipComments: true }],
38
49
  },
39
50
  },
@@ -41,6 +52,7 @@ export default defineConfig([
41
52
  files: ['**/*.test.ts', '**/*.spec.ts'],
42
53
  rules: {
43
54
  'max-lines': 'off',
55
+ 'no-restricted-syntax': 'off',
44
56
  },
45
57
  },
46
58
  eslintConfigPrettier,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "clawmini",
3
- "version": "0.0.1",
3
+ "version": "0.0.3",
4
4
  "type": "module",
5
5
  "workspaces": [
6
6
  "web"
@@ -24,7 +24,8 @@
24
24
  "format": "prettier --write \"src/**/*.ts\" \"web/src/**/*.ts\"",
25
25
  "check": "tsc --noEmit && npm run check -w web",
26
26
  "test": "vitest run && npm run test -w web",
27
- "build:web": "npm run build -w web"
27
+ "build:web": "npm run build -w web",
28
+ "validate": "npm run format:check && npm run lint && npm run check && npm run test"
28
29
  },
29
30
  "dependencies": {
30
31
  "@trpc/client": "^11.12.0",
@@ -1,5 +1,5 @@
1
1
  import { createTRPCClient, httpLink, splitLink, httpSubscriptionLink } from '@trpc/client';
2
- import type { AppRouter } from '../daemon/router.js';
2
+ import type { UserRouter as AppRouter } from '../daemon/api/index.js';
3
3
  import { getSocketPath } from '../shared/workspace.js';
4
4
  import { createUnixSocketFetch } from '../shared/fetch.js';
5
5
  import { createUnixSocketEventSource } from '../shared/event-source.js';
@@ -4,6 +4,7 @@ import { Client, Events, GatewayIntentBits, Partials } from 'discord.js';
4
4
  import { readDiscordConfig, isAuthorized, initDiscordConfig } from './config.js';
5
5
  import { getTRPCClient } from './client.js';
6
6
  import { startDaemonToDiscordForwarder } from './forwarder.js';
7
+ import { getClawminiDir } from '../shared/workspace.js';
7
8
  import fs from 'node:fs/promises';
8
9
  import path from 'node:path';
9
10
 
@@ -62,7 +63,6 @@ export async function main() {
62
63
 
63
64
  const downloadedFiles: string[] = [];
64
65
  if (message.attachments.size > 0) {
65
- const { getClawminiDir } = await import('../shared/workspace.js');
66
66
  const tmpDir = path.join(getClawminiDir(process.cwd()), 'tmp', 'discord');
67
67
  await fs.mkdir(tmpDir, { recursive: true });
68
68
  const maxSizeMB = config.maxAttachmentSizeMB ?? 25;
@@ -141,7 +141,24 @@ export async function main() {
141
141
  }
142
142
  }
143
143
 
144
- main().catch((error) => {
145
- console.error('Unhandled error in Discord Adapter:', error);
146
- process.exit(1);
147
- });
144
+ import { fileURLToPath } from 'node:url';
145
+
146
+ const isMainModule = (() => {
147
+ try {
148
+ if (typeof process === 'undefined' || !process.argv || process.argv.length < 2) return false;
149
+ const argv1 = process.argv[1];
150
+ if (!argv1) return false;
151
+ const p1 = path.resolve(argv1);
152
+ const p2 = path.resolve(fileURLToPath(import.meta.url));
153
+ return p1 === p2;
154
+ } catch {
155
+ return false;
156
+ }
157
+ })();
158
+
159
+ if (isMainModule) {
160
+ main().catch((error) => {
161
+ console.error('Unhandled error in Discord Adapter:', error);
162
+ process.exit(1);
163
+ });
164
+ }
package/src/cli/client.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import { createTRPCClient, httpLink } from '@trpc/client';
2
- import type { AppRouter } from '../daemon/router.js';
2
+ import type { UserRouter as AppRouter } from '../daemon/api/index.js';
3
3
  import { getSocketPath, getClawminiDir } from '../shared/workspace.js';
4
4
  import { createUnixSocketFetch } from '../shared/fetch.js';
5
5
  import { spawn } from 'node:child_process';
@@ -27,8 +27,13 @@ export async function getDaemonClient(options: { autoStart?: boolean } = {}) {
27
27
  });
28
28
  child.unref();
29
29
 
30
- // Wait a moment for the daemon to start and create the socket
31
- await new Promise((resolve) => setTimeout(resolve, 500));
30
+ // Wait up to 5 seconds for the daemon to start and create the socket
31
+ for (let i = 0; i < 50; i++) {
32
+ await new Promise((resolve) => setTimeout(resolve, 100));
33
+ if (fs.existsSync(socketPath)) {
34
+ break;
35
+ }
36
+ }
32
37
 
33
38
  if (!fs.existsSync(socketPath)) {
34
39
  throw new Error('Failed to start daemon.');
@@ -1,7 +1,7 @@
1
1
  import { describe, it, expect, beforeAll, afterAll } from 'vitest';
2
- import path from 'node:path';
3
2
  import { createE2EContext } from './utils.js';
4
3
  import { getTRPCClient } from '../../adapter-discord/client.js';
4
+ import { getSocketPath } from '../../shared/workspace.js';
5
5
 
6
6
  const { runCli, e2eDir, setupE2E, teardownE2E } = createE2EContext('e2e-discord');
7
7
  describe('Discord Adapter Client E2E', () => {
@@ -17,7 +17,7 @@ describe('Discord Adapter Client E2E', () => {
17
17
  }, 30000);
18
18
 
19
19
  it('should successfully connect to the daemon and subscribe to messages', async () => {
20
- const socketPath = path.join(e2eDir, '.clawmini', 'server.sock');
20
+ const socketPath = getSocketPath(e2eDir);
21
21
  const trpc = getTRPCClient({ socketPath });
22
22
 
23
23
  const pingResult = await trpc.ping.query();
@@ -3,6 +3,7 @@ import { spawn } from 'node:child_process';
3
3
  import path from 'node:path';
4
4
  import fs from 'node:fs';
5
5
  import type { ChatMessage } from '../../shared/chats.js';
6
+ import { getSocketPath } from '../../shared/workspace.js';
6
7
  import { createE2EContext } from './utils.js';
7
8
 
8
9
  const { runCli, e2eDir, binPath, setupE2E, teardownE2E } = createE2EContext('e2e-tmp-daemon');
@@ -31,7 +32,7 @@ describe('E2E Daemon and Web Tests', () => {
31
32
 
32
33
  await new Promise((resolve) => setTimeout(resolve, 500));
33
34
 
34
- const socketPath = path.resolve(e2eDir, '.clawmini/daemon.sock');
35
+ const socketPath = getSocketPath(e2eDir);
35
36
  expect(fs.existsSync(socketPath)).toBe(false);
36
37
 
37
38
  const { stdout: stdoutAgain, code: codeAgain } = await runCli(['down']);
@@ -105,18 +105,7 @@ describe('E2E Export Lite Functionality Tests', () => {
105
105
  // 2. Test jobs add
106
106
  const addProcess = spawn(
107
107
  'node',
108
- [
109
- litePath,
110
- 'jobs',
111
- 'add',
112
- 'lite-job',
113
- '--cron',
114
- '* * * * *',
115
- '--message',
116
- 'lite message',
117
- '--chat',
118
- 'lite-chat',
119
- ],
108
+ [litePath, 'jobs', 'add', 'lite-job', '--cron', '* * * * *', '--message', 'lite message'],
120
109
  {
121
110
  env: { ...process.env, CLAW_API_URL: envUrl, CLAW_API_TOKEN: envToken },
122
111
  }
@@ -139,7 +128,7 @@ describe('E2E Export Lite Functionality Tests', () => {
139
128
  expect(listStdout).toContain('* * * * *');
140
129
 
141
130
  // 4. Test jobs delete
142
- const delProcess = spawn('node', [litePath, 'jobs', 'delete', 'lite-job', '-c', 'lite-chat'], {
131
+ const delProcess = spawn('node', [litePath, 'jobs', 'delete', 'lite-job'], {
143
132
  env: { ...process.env, CLAW_API_URL: envUrl, CLAW_API_TOKEN: envToken },
144
133
  });
145
134
  let delStdout = '';
@@ -148,6 +137,45 @@ describe('E2E Export Lite Functionality Tests', () => {
148
137
  await new Promise((resolve) => delProcess.on('close', resolve));
149
138
  expect(delStdout).toContain("Job 'lite-job' deleted successfully.");
150
139
 
140
+ // 5. Test fetch-pending
141
+ const sleeperAgentDir = path.resolve(e2eDir, 'sleeper');
142
+ fs.mkdirSync(sleeperAgentDir, { recursive: true });
143
+ await runCli(['agents', 'add', 'sleeper', '--dir', 'sleeper']);
144
+ const sleeperSettings = path.resolve(e2eDir, '.clawmini/agents/sleeper/settings.json');
145
+ fs.mkdirSync(path.dirname(sleeperSettings), { recursive: true });
146
+ const sleepCommand =
147
+ process.platform === 'win32' ? 'node -e "setTimeout(() => {}, 5000)"' : 'sleep 5';
148
+ fs.writeFileSync(sleeperSettings, JSON.stringify({ commands: { new: sleepCommand } }));
149
+
150
+ await runCli(['chats', 'add', 'sleep-chat']);
151
+ // Start the sleeper agent to block the queue
152
+ await runCli([
153
+ 'messages',
154
+ 'send',
155
+ 'block queue',
156
+ '--chat',
157
+ 'sleep-chat',
158
+ '--agent',
159
+ 'sleeper',
160
+ '--no-wait',
161
+ ]);
162
+
163
+ // Send a pending message that will be queued
164
+ await runCli(['messages', 'send', 'my pending message', '--chat', 'sleep-chat', '--no-wait']);
165
+
166
+ // Fetch the pending message
167
+ const fetchProcess = spawn('node', [litePath, 'fetch-pending'], {
168
+ env: { ...process.env, CLAW_API_URL: envUrl, CLAW_API_TOKEN: envToken },
169
+ });
170
+ let fetchStdout = '';
171
+ fetchProcess.stdout.on('data', (d) => (fetchStdout += d.toString()));
172
+ fetchProcess.stderr.on('data', (d) => (fetchStdout += d.toString()));
173
+ await new Promise((resolve) => fetchProcess.on('close', resolve));
174
+
175
+ expect(fetchStdout).toContain('<message>');
176
+ expect(fetchStdout).toContain('my pending message');
177
+ expect(fetchStdout).toContain('</message>');
178
+
151
179
  await runCli(['down']);
152
180
  fs.writeFileSync(settingsPath, originalSettings);
153
181
  await runCli(['up']);
@@ -40,6 +40,7 @@ describe('E2E Fallbacks Tests', () => {
40
40
  const lines = chatLog
41
41
  .trim()
42
42
  .split('\n')
43
+ .filter((l) => l.trim().length > 0)
43
44
  .map((l) => JSON.parse(l));
44
45
 
45
46
  // Lines: USER, LOG (retry-delay), LOG (success)
@@ -80,6 +81,7 @@ describe('E2E Fallbacks Tests', () => {
80
81
  const lines = chatLog
81
82
  .trim()
82
83
  .split('\n')
84
+ .filter((l) => l.trim().length > 0)
83
85
  .map((l) => JSON.parse(l));
84
86
 
85
87
  const lastLog = lines[lines.length - 1];
@@ -125,6 +127,7 @@ describe('E2E Fallbacks Tests', () => {
125
127
  const lines = chatLog
126
128
  .trim()
127
129
  .split('\n')
130
+ .filter((l) => l.trim().length > 0)
128
131
  .map((l) => JSON.parse(l));
129
132
 
130
133
  expect(
@@ -160,6 +163,7 @@ describe('E2E Fallbacks Tests', () => {
160
163
  const lines = chatLog
161
164
  .trim()
162
165
  .split('\n')
166
+ .filter((l) => l.trim().length > 0)
163
167
  .map((l) => JSON.parse(l));
164
168
 
165
169
  const lastLog = lines[lines.length - 1];
package/src/cli/lite.ts CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  import { Command } from 'commander';
4
4
  import { createTRPCClient, httpLink } from '@trpc/client';
5
- import type { AppRouter } from '../daemon/router.js';
5
+ import type { AgentRouter as AppRouter } from '../daemon/api/index.js';
6
6
  import type { CronJob } from '../shared/config.js';
7
7
 
8
8
  /**
@@ -64,6 +64,25 @@ program
64
64
  }
65
65
  });
66
66
 
67
+ program
68
+ .command('fetch-pending')
69
+ .description('Fetch pending messages and output them as formatted strings')
70
+ .action(async () => {
71
+ try {
72
+ const client = getClient();
73
+ const result = await client.fetchPendingMessages.mutate();
74
+ if (result && result.messages) {
75
+ process.stdout.write(result.messages);
76
+ if (!result.messages.endsWith('\n')) {
77
+ process.stdout.write('\n');
78
+ }
79
+ }
80
+ } catch (err) {
81
+ console.error('Error:', err instanceof Error ? err.message : err);
82
+ process.exit(1);
83
+ }
84
+ });
85
+
67
86
  const jobs = program.command('jobs').description('Manage cron jobs');
68
87
 
69
88
  jobs
@@ -72,7 +91,7 @@ jobs
72
91
  .action(async () => {
73
92
  try {
74
93
  const client = getClient();
75
- const jobsList = await client.listCronJobs.query({});
94
+ const jobsList = await client.listCronJobs.query();
76
95
  console.log(JSON.stringify(jobsList, null, 2));
77
96
  } catch (err) {
78
97
  console.error('Error:', err instanceof Error ? err.message : err);
@@ -129,7 +148,7 @@ jobs
129
148
  }
130
149
 
131
150
  const client = getClient();
132
- await client.addCronJob.mutate({ chatId: options.chat, job });
151
+ await client.addCronJob.mutate({ job });
133
152
  console.log(`Job '${name}' created successfully.`);
134
153
  } catch (err) {
135
154
  console.error('Error:', err instanceof Error ? err.message : err);
@@ -140,11 +159,10 @@ jobs
140
159
  jobs
141
160
  .command('delete <name>')
142
161
  .description('Delete a cron job')
143
- .option('-c, --chat <chatId>', 'Chat ID')
144
- .action(async (name, options) => {
162
+ .action(async (name) => {
145
163
  try {
146
164
  const client = getClient();
147
- const result = await client.deleteCronJob.mutate({ chatId: options.chat, id: name });
165
+ const result = await client.deleteCronJob.mutate({ id: name });
148
166
  if (result && result.deleted) {
149
167
  console.log(`Job '${name}' deleted successfully.`);
150
168
  } else {
@@ -0,0 +1,191 @@
1
+ import { z } from 'zod';
2
+ import { randomUUID } from 'node:crypto';
3
+ import path from 'node:path';
4
+ import { TRPCError } from '@trpc/server';
5
+ import { appendMessage, type CommandLogMessage } from '../chats.js';
6
+ import { executeSafe, generateRequestPreview } from '../policy-utils.js';
7
+ import { getWorkspaceRoot, readPolicies, getClawminiDir } from '../../shared/workspace.js';
8
+ import { PolicyRequestService } from '../policy-request-service.js';
9
+ import { RequestStore } from '../request-store.js';
10
+ import { CronJobSchema } from '../../shared/config.js';
11
+ import { apiProcedure, router } from './trpc.js';
12
+ import { getMessageQueue } from '../queue.js';
13
+ import { formatPendingMessages } from '../message.js';
14
+ import {
15
+ resolveAgentDir,
16
+ validateLogFile,
17
+ listCronJobsShared,
18
+ addCronJobShared,
19
+ deleteCronJobShared,
20
+ } from './router-utils.js';
21
+
22
+ export const logMessage = apiProcedure
23
+ .input(
24
+ z.object({
25
+ message: z.string().optional(),
26
+ files: z.array(z.string()).optional(),
27
+ })
28
+ )
29
+ .mutation(async ({ input, ctx }) => {
30
+ if (!ctx.tokenPayload) throw new TRPCError({ code: 'UNAUTHORIZED', message: 'Missing token' });
31
+ const chatId = ctx.tokenPayload.chatId;
32
+ const timestamp = new Date().toISOString();
33
+ const id = Date.now().toString() + Math.random().toString(36).substring(2, 7);
34
+
35
+ const filePaths: string[] = [];
36
+ if (input.files && input.files.length > 0) {
37
+ const workspaceRoot = getWorkspaceRoot(process.cwd());
38
+ const agentDir = await resolveAgentDir(ctx.tokenPayload?.agentId, workspaceRoot);
39
+
40
+ for (const file of input.files) {
41
+ const validPath = await validateLogFile(file, agentDir, workspaceRoot);
42
+ filePaths.push(validPath);
43
+ }
44
+ }
45
+
46
+ const filesArgStr = filePaths.map((p) => ` --file ${p}`).join('');
47
+ const messageStr = input.message || '';
48
+ const logMsg: CommandLogMessage = {
49
+ id,
50
+ messageId: id,
51
+ role: 'log',
52
+ source: 'router',
53
+ content: messageStr,
54
+ stderr: '',
55
+ timestamp,
56
+ command: `clawmini-lite log${filesArgStr}`,
57
+ cwd: process.cwd(),
58
+ exitCode: 0,
59
+ ...(filePaths.length > 0 ? { files: filePaths } : {}),
60
+ };
61
+
62
+ await appendMessage(chatId, logMsg);
63
+ return { success: true };
64
+ });
65
+
66
+ export const agentListCronJobs = apiProcedure.query(async ({ ctx }) => {
67
+ if (!ctx.tokenPayload) throw new TRPCError({ code: 'UNAUTHORIZED', message: 'Missing token' });
68
+ const chatId = ctx.tokenPayload.chatId;
69
+ return listCronJobsShared(chatId);
70
+ });
71
+
72
+ export const agentAddCronJob = apiProcedure
73
+ .input(z.object({ job: CronJobSchema }))
74
+ .mutation(async ({ input, ctx }) => {
75
+ if (!ctx.tokenPayload) throw new TRPCError({ code: 'UNAUTHORIZED', message: 'Missing token' });
76
+ const chatId = ctx.tokenPayload.chatId;
77
+ const job = { ...input.job, agentId: ctx.tokenPayload.agentId };
78
+ return addCronJobShared(chatId, job);
79
+ });
80
+
81
+ export const agentDeleteCronJob = apiProcedure
82
+ .input(z.object({ id: z.string() }))
83
+ .mutation(async ({ input, ctx }) => {
84
+ if (!ctx.tokenPayload) throw new TRPCError({ code: 'UNAUTHORIZED', message: 'Missing token' });
85
+ const chatId = ctx.tokenPayload.chatId;
86
+ return deleteCronJobShared(chatId, input.id);
87
+ });
88
+
89
+ export const listPolicies = apiProcedure.query(async () => {
90
+ return await readPolicies();
91
+ });
92
+
93
+ export const executePolicyHelp = apiProcedure
94
+ .input(z.object({ commandName: z.string() }))
95
+ .query(async ({ input }) => {
96
+ const config = await readPolicies();
97
+ const policy = config?.policies?.[input.commandName];
98
+
99
+ if (!policy) {
100
+ throw new TRPCError({
101
+ code: 'NOT_FOUND',
102
+ message: `Policy not found: ${input.commandName}`,
103
+ });
104
+ }
105
+
106
+ if (!policy.allowHelp) {
107
+ return { stdout: '', stderr: 'This command does not support --help\n', exitCode: 1 };
108
+ }
109
+
110
+ const fullArgs = [...(policy.args || []), '--help'];
111
+ const { stdout, stderr, exitCode } = await executeSafe(policy.command, fullArgs, {
112
+ cwd: getWorkspaceRoot(),
113
+ });
114
+
115
+ return { stdout, stderr, exitCode };
116
+ });
117
+
118
+ export const createPolicyRequest = apiProcedure
119
+ .input(
120
+ z.object({
121
+ commandName: z.string(),
122
+ args: z.array(z.string()),
123
+ fileMappings: z.record(z.string(), z.string()),
124
+ })
125
+ )
126
+ .mutation(async ({ input, ctx }) => {
127
+ if (!ctx.tokenPayload) throw new TRPCError({ code: 'UNAUTHORIZED', message: 'Missing token' });
128
+ const workspaceRoot = getWorkspaceRoot(process.cwd());
129
+ const snapshotDir = path.join(getClawminiDir(process.cwd()), 'tmp', 'snapshots');
130
+ const store = new RequestStore(process.cwd());
131
+ const agentDir = await resolveAgentDir(ctx.tokenPayload?.agentId, workspaceRoot);
132
+ const service = new PolicyRequestService(store, agentDir, snapshotDir);
133
+
134
+ const chatId = ctx.tokenPayload.chatId;
135
+ const agentId = ctx.tokenPayload.agentId;
136
+
137
+ const request = await service.createRequest(
138
+ input.commandName,
139
+ input.args,
140
+ input.fileMappings,
141
+ chatId,
142
+ agentId
143
+ );
144
+
145
+ const previewContent = await generateRequestPreview(request);
146
+
147
+ const logMsg = {
148
+ id: randomUUID(),
149
+ // TODO: we should store the message ID in the CLAW_API_TOKEN, and extract it here
150
+ messageId: randomUUID(),
151
+ role: 'log' as const,
152
+ source: 'router' as const,
153
+ content: previewContent,
154
+ stderr: '',
155
+ timestamp: new Date().toISOString(),
156
+ command: 'policy-request',
157
+ cwd: process.cwd(),
158
+ exitCode: 0,
159
+ };
160
+
161
+ await appendMessage(chatId, logMsg);
162
+ return request;
163
+ });
164
+
165
+ import { ping } from './user-router.js';
166
+
167
+ export const fetchPendingMessages = apiProcedure.mutation(async ({ ctx }) => {
168
+ const cwd = process.cwd();
169
+ const queue = getMessageQueue(cwd);
170
+ const targetSessionId = ctx.tokenPayload?.sessionId || 'default';
171
+
172
+ const extracted = queue.extractPending((p) => p.sessionId === targetSessionId);
173
+ if (extracted.length === 0) {
174
+ return { messages: '' };
175
+ }
176
+ return { messages: formatPendingMessages(extracted.map((p) => p.text)) };
177
+ });
178
+
179
+ export const agentRouter = router({
180
+ logMessage,
181
+ listCronJobs: agentListCronJobs,
182
+ addCronJob: agentAddCronJob,
183
+ deleteCronJob: agentDeleteCronJob,
184
+ listPolicies,
185
+ executePolicyHelp,
186
+ createPolicyRequest,
187
+ fetchPendingMessages,
188
+ ping,
189
+ });
190
+
191
+ export type AgentRouter = typeof agentRouter;