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.
- package/.github/workflows/ci.yml +59 -0
- package/README.md +61 -76
- package/dist/adapter-discord/index.d.mts.map +1 -1
- package/dist/adapter-discord/index.mjs +13 -4
- package/dist/adapter-discord/index.mjs.map +1 -1
- package/dist/cli/index.mjs +8 -6
- package/dist/cli/index.mjs.map +1 -1
- package/dist/cli/lite.mjs +64 -10
- package/dist/cli/lite.mjs.map +1 -1
- package/dist/daemon/index.mjs +732 -251
- package/dist/daemon/index.mjs.map +1 -1
- package/dist/{fetch-BjZVyU3Z.mjs → fetch-Cn1XNyiO.mjs} +1 -1
- package/dist/{fetch-BjZVyU3Z.mjs.map → fetch-Cn1XNyiO.mjs.map} +1 -1
- package/dist/lite-oSYSvaOr.mjs +164 -0
- package/dist/lite-oSYSvaOr.mjs.map +1 -0
- package/dist/web/_app/immutable/chunks/{COekwvP2.js → 8YNcRyEk.js} +1 -1
- package/dist/web/_app/immutable/chunks/{CSvS_NwK.js → DQoygso7.js} +1 -1
- package/dist/web/_app/immutable/entry/{app.B-vZe7PN.js → app.DO5eYwVz.js} +2 -2
- package/dist/web/_app/immutable/entry/start.D48mVn1m.js +1 -0
- package/dist/web/_app/immutable/nodes/{0.B5WFN0zw.js → 0.B-0CcADM.js} +1 -1
- package/dist/web/_app/immutable/nodes/{1.D1wtJb2k.js → 1.FixKgvRO.js} +1 -1
- package/dist/web/_app/immutable/nodes/{3.BB5wCoBf.js → 3.ncP0xLO6.js} +1 -1
- package/dist/web/_app/immutable/nodes/{4.Dr2jvAXK.js → 4.CQYJEgv8.js} +1 -1
- package/dist/web/_app/immutable/nodes/{5.BJl7oM3b.js → 5.BpJUN6QH.js} +1 -1
- package/dist/web/_app/version.json +1 -1
- package/dist/web/index.html +6 -6
- package/dist/{workspace-CSgfo_2J.mjs → workspace-DjoNjhW0.mjs} +21 -40
- package/dist/workspace-DjoNjhW0.mjs.map +1 -0
- package/docs/15_lite_fetch_pending/development_log.md +31 -0
- package/docs/15_lite_fetch_pending/notes.md +48 -0
- package/docs/15_lite_fetch_pending/prd.md +39 -0
- package/docs/15_lite_fetch_pending/questions.md +3 -0
- package/docs/15_lite_fetch_pending/tickets.md +42 -0
- package/docs/CHECKS.md +2 -2
- package/docs/CLI_REFERENCE.md +35 -0
- package/docs/guides/sandbox_policies.md +12 -5
- package/eslint.config.js +12 -0
- package/package.json +3 -2
- package/src/adapter-discord/client.ts +1 -1
- package/src/adapter-discord/index.ts +22 -5
- package/src/cli/client.ts +8 -3
- package/src/cli/e2e/adapter-discord.test.ts +2 -2
- package/src/cli/e2e/daemon.test.ts +2 -1
- package/src/cli/e2e/export-lite-func.test.ts +41 -13
- package/src/cli/e2e/fallbacks.test.ts +4 -0
- package/src/cli/lite.ts +24 -6
- package/src/daemon/api/agent-router.ts +191 -0
- package/src/daemon/{router.test.ts → api/index.test.ts} +101 -34
- package/src/daemon/api/index.ts +4 -0
- package/src/daemon/{router-policy-request.test.ts → api/policy-request.test.ts} +27 -13
- package/src/daemon/api/router-utils.ts +159 -0
- package/src/daemon/api/trpc.ts +30 -0
- package/src/daemon/api/user-router.ts +221 -0
- package/src/daemon/index.ts +3 -3
- package/src/daemon/message-interruption.test.ts +17 -10
- package/src/daemon/message-typing.test.ts +1 -1
- package/src/daemon/message.ts +260 -239
- package/src/daemon/observation.test.ts +1 -1
- package/src/daemon/queue.test.ts +28 -0
- package/src/daemon/queue.ts +30 -15
- package/src/daemon/request-store.test.ts +4 -4
- package/src/daemon/request-store.ts +3 -1
- package/src/shared/workspace.ts +4 -5
- package/templates/debug/settings.json +5 -0
- package/templates/environments/macos/env.json +1 -1
- package/templates/environments/macos-proxy/env.json +1 -1
- package/templates/gemini-claw/.gemini/hooks/insert-pending.sh +9 -0
- package/templates/gemini-claw/.gemini/settings.json +14 -1
- package/templates/gemini-claw/.gemini/system.md +2 -0
- package/web/.svelte-kit/ambient.d.ts +2 -6
- package/web/.svelte-kit/generated/server/internal.js +1 -1
- package/web/.svelte-kit/output/client/.vite/manifest.json +29 -29
- package/web/.svelte-kit/output/client/_app/immutable/chunks/{COekwvP2.js → 8YNcRyEk.js} +1 -1
- package/web/.svelte-kit/output/client/_app/immutable/chunks/{CSvS_NwK.js → DQoygso7.js} +1 -1
- package/web/.svelte-kit/output/client/_app/immutable/entry/{app.B-vZe7PN.js → app.DO5eYwVz.js} +2 -2
- package/web/.svelte-kit/output/client/_app/immutable/entry/start.D48mVn1m.js +1 -0
- package/web/.svelte-kit/output/client/_app/immutable/nodes/{0.B5WFN0zw.js → 0.B-0CcADM.js} +1 -1
- package/web/.svelte-kit/output/client/_app/immutable/nodes/{1.D1wtJb2k.js → 1.FixKgvRO.js} +1 -1
- package/web/.svelte-kit/output/client/_app/immutable/nodes/{3.BB5wCoBf.js → 3.ncP0xLO6.js} +1 -1
- package/web/.svelte-kit/output/client/_app/immutable/nodes/{4.Dr2jvAXK.js → 4.CQYJEgv8.js} +1 -1
- package/web/.svelte-kit/output/client/_app/immutable/nodes/{5.BJl7oM3b.js → 5.BpJUN6QH.js} +1 -1
- package/web/.svelte-kit/output/client/_app/version.json +1 -1
- package/web/.svelte-kit/output/server/chunks/internal.js +1 -1
- package/web/.svelte-kit/output/server/manifest-full.js +1 -1
- package/web/.svelte-kit/output/server/manifest.js +1 -1
- package/web/.svelte-kit/output/server/nodes/0.js +1 -1
- package/web/.svelte-kit/output/server/nodes/1.js +1 -1
- package/web/.svelte-kit/output/server/nodes/3.js +1 -1
- package/web/.svelte-kit/output/server/nodes/4.js +1 -1
- package/web/.svelte-kit/output/server/nodes/5.js +1 -1
- package/dist/chats-DKgTeU7i.mjs +0 -91
- package/dist/chats-DKgTeU7i.mjs.map +0 -1
- package/dist/chats-Zd_HXDHx.mjs +0 -29
- package/dist/chats-Zd_HXDHx.mjs.map +0 -1
- package/dist/fs-B5wW0oaH.mjs +0 -14
- package/dist/fs-B5wW0oaH.mjs.map +0 -1
- package/dist/lite-Dl7WXyaH.mjs +0 -80
- package/dist/lite-Dl7WXyaH.mjs.map +0 -1
- package/dist/rolldown-runtime-95iHPtFO.mjs +0 -18
- package/dist/web/_app/immutable/entry/start.oP1AgKhs.js +0 -1
- package/dist/workspace-CSgfo_2J.mjs.map +0 -1
- package/src/daemon/router.ts +0 -510
- package/web/.svelte-kit/output/client/_app/immutable/entry/start.oP1AgKhs.js +0 -1
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
#
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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/
|
|
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
|
-
|
|
145
|
-
|
|
146
|
-
|
|
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/
|
|
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
|
|
31
|
-
|
|
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 =
|
|
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 =
|
|
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'
|
|
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/
|
|
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({
|
|
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
|
-
.
|
|
144
|
-
.action(async (name, options) => {
|
|
162
|
+
.action(async (name) => {
|
|
145
163
|
try {
|
|
146
164
|
const client = getClient();
|
|
147
|
-
const result = await client.deleteCronJob.mutate({
|
|
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;
|