clawmini 0.0.2 → 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 +4 -2
- 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 +7 -6
- package/dist/cli/index.mjs.map +1 -1
- package/dist/cli/lite.mjs +16 -10
- package/dist/cli/lite.mjs.map +1 -1
- package/dist/daemon/index.mjs +590 -401
- 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/{CAZeqksE.js → 8YNcRyEk.js} +1 -1
- package/dist/web/_app/immutable/chunks/{B3YcEpQV.js → DQoygso7.js} +1 -1
- package/dist/web/_app/immutable/entry/{app.ZuicLpkH.js → app.DO5eYwVz.js} +2 -2
- package/dist/web/_app/immutable/entry/start.D48mVn1m.js +1 -0
- package/dist/web/_app/immutable/nodes/{0.BB1CjKco.js → 0.B-0CcADM.js} +1 -1
- package/dist/web/_app/immutable/nodes/{1.CdSgEHu9.js → 1.FixKgvRO.js} +1 -1
- package/{web/.svelte-kit/output/client/_app/immutable/nodes/3.CKp7Wkn8.js → dist/web/_app/immutable/nodes/3.ncP0xLO6.js} +1 -1
- package/dist/web/_app/immutable/nodes/{4.FyeoMY-Y.js → 4.CQYJEgv8.js} +1 -1
- package/dist/web/_app/immutable/nodes/{5.D6mVN7l7.js → 5.BpJUN6QH.js} +1 -1
- package/dist/web/_app/version.json +1 -1
- package/dist/web/index.html +6 -6
- package/dist/{workspace-BC1ahx4R.mjs → workspace-DjoNjhW0.mjs} +12 -42
- 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/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/generated/server/internal.js +1 -1
- package/web/.svelte-kit/output/client/.vite/manifest.json +26 -26
- package/web/.svelte-kit/output/client/_app/immutable/chunks/{CAZeqksE.js → 8YNcRyEk.js} +1 -1
- package/web/.svelte-kit/output/client/_app/immutable/chunks/{B3YcEpQV.js → DQoygso7.js} +1 -1
- package/web/.svelte-kit/output/client/_app/immutable/entry/{app.ZuicLpkH.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.BB1CjKco.js → 0.B-0CcADM.js} +1 -1
- package/web/.svelte-kit/output/client/_app/immutable/nodes/{1.CdSgEHu9.js → 1.FixKgvRO.js} +1 -1
- package/{dist/web/_app/immutable/nodes/3.CKp7Wkn8.js → web/.svelte-kit/output/client/_app/immutable/nodes/3.ncP0xLO6.js} +1 -1
- package/web/.svelte-kit/output/client/_app/immutable/nodes/{4.FyeoMY-Y.js → 4.CQYJEgv8.js} +1 -1
- package/web/.svelte-kit/output/client/_app/immutable/nodes/{5.D6mVN7l7.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-BcbxvPlj.mjs +0 -29
- package/dist/chats-BcbxvPlj.mjs.map +0 -1
- package/dist/chats-CpRQrNHj.mjs +0 -91
- package/dist/chats-CpRQrNHj.mjs.map +0 -1
- package/dist/fs-B5wW0oaH.mjs +0 -14
- package/dist/fs-B5wW0oaH.mjs.map +0 -1
- package/dist/lite-DBUuHsX0.mjs +0 -80
- package/dist/lite-DBUuHsX0.mjs.map +0 -1
- package/dist/policy-utils-BvfOK6Ih.mjs +0 -114
- package/dist/policy-utils-BvfOK6Ih.mjs.map +0 -1
- package/dist/rolldown-runtime-95iHPtFO.mjs +0 -18
- package/dist/web/_app/immutable/entry/start.DuQwh4Nz.js +0 -1
- package/dist/workspace-BC1ahx4R.mjs.map +0 -1
- package/src/daemon/router.ts +0 -510
- package/web/.svelte-kit/output/client/_app/immutable/entry/start.DuQwh4Nz.js +0 -1
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
/* eslint-disable @typescript-eslint/no-explicit-any */
|
|
2
2
|
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
3
|
-
import {
|
|
4
|
-
|
|
5
|
-
import * as
|
|
6
|
-
import
|
|
7
|
-
import
|
|
3
|
+
import { userRouter, agentRouter } from './index.js';
|
|
4
|
+
// No merged router to avoid duplicate key errors.
|
|
5
|
+
import * as workspace from '../../shared/workspace.js';
|
|
6
|
+
import * as chats from '../../shared/chats.js';
|
|
7
|
+
import type { CronJob } from '../../shared/config.js';
|
|
8
|
+
import * as message from '../message.js';
|
|
9
|
+
import { getMessageQueue } from '../queue.js';
|
|
8
10
|
import * as fs from 'node:fs/promises';
|
|
9
11
|
import path from 'node:path';
|
|
10
12
|
|
|
@@ -32,12 +34,16 @@ vi.mock('node:fs/promises', async (importOriginal) => {
|
|
|
32
34
|
};
|
|
33
35
|
});
|
|
34
36
|
|
|
35
|
-
vi.mock('
|
|
36
|
-
|
|
37
|
-
|
|
37
|
+
vi.mock('../message.js', async (importOriginal) => {
|
|
38
|
+
const actual = await importOriginal<typeof import('../message.js')>();
|
|
39
|
+
return {
|
|
40
|
+
...actual,
|
|
41
|
+
handleUserMessage: vi.fn(),
|
|
42
|
+
};
|
|
43
|
+
});
|
|
38
44
|
|
|
39
|
-
vi.mock('
|
|
40
|
-
const actual = await importOriginal<typeof import('
|
|
45
|
+
vi.mock('../../shared/workspace.js', async (importOriginal) => {
|
|
46
|
+
const actual = await importOriginal<typeof import('../../shared/workspace.js')>();
|
|
41
47
|
return {
|
|
42
48
|
...actual,
|
|
43
49
|
readChatSettings: vi.fn(),
|
|
@@ -46,14 +52,14 @@ vi.mock('../shared/workspace.js', async (importOriginal) => {
|
|
|
46
52
|
getAgent: vi.fn(),
|
|
47
53
|
getWorkspaceRoot: vi.fn().mockReturnValue(process.cwd()),
|
|
48
54
|
getActiveEnvironmentName: vi.fn().mockResolvedValue(null),
|
|
49
|
-
|
|
55
|
+
getActiveEnvironmentInfo: vi.fn().mockResolvedValue(null),
|
|
50
56
|
getEnvironmentPath: vi.fn().mockReturnValue(''),
|
|
51
57
|
readEnvironment: vi.fn().mockResolvedValue(null),
|
|
52
58
|
};
|
|
53
59
|
});
|
|
54
60
|
|
|
55
|
-
vi.mock('
|
|
56
|
-
const actual = await importOriginal<typeof import('
|
|
61
|
+
vi.mock('../../shared/chats.js', async (importOriginal) => {
|
|
62
|
+
const actual = await importOriginal<typeof import('../../shared/chats.js')>();
|
|
57
63
|
return {
|
|
58
64
|
...actual,
|
|
59
65
|
getDefaultChatId: vi.fn(),
|
|
@@ -77,7 +83,7 @@ describe('Daemon TRPC Router', () => {
|
|
|
77
83
|
vi.mocked(chats.getDefaultChatId).mockResolvedValue('default-chat');
|
|
78
84
|
vi.mocked(workspace.readChatSettings).mockResolvedValue({});
|
|
79
85
|
|
|
80
|
-
const caller =
|
|
86
|
+
const caller = userRouter.createCaller({});
|
|
81
87
|
const jobs = await caller.listCronJobs({});
|
|
82
88
|
expect(jobs).toEqual([]);
|
|
83
89
|
expect(workspace.readChatSettings).toHaveBeenCalledWith('default-chat');
|
|
@@ -87,7 +93,7 @@ describe('Daemon TRPC Router', () => {
|
|
|
87
93
|
vi.mocked(chats.getDefaultChatId).mockResolvedValue('default-chat');
|
|
88
94
|
vi.mocked(workspace.readChatSettings).mockResolvedValue({ jobs: [mockJob] });
|
|
89
95
|
|
|
90
|
-
const caller =
|
|
96
|
+
const caller = userRouter.createCaller({});
|
|
91
97
|
const jobs = await caller.listCronJobs({ chatId: 'custom-chat' });
|
|
92
98
|
expect(jobs).toEqual([mockJob]);
|
|
93
99
|
expect(workspace.readChatSettings).toHaveBeenCalledWith('custom-chat');
|
|
@@ -97,7 +103,7 @@ describe('Daemon TRPC Router', () => {
|
|
|
97
103
|
vi.mocked(chats.getDefaultChatId).mockResolvedValue('default-chat');
|
|
98
104
|
vi.mocked(workspace.readChatSettings).mockResolvedValue({});
|
|
99
105
|
|
|
100
|
-
const caller =
|
|
106
|
+
const caller = userRouter.createCaller({});
|
|
101
107
|
const result = await caller.addCronJob({ job: mockJob });
|
|
102
108
|
|
|
103
109
|
expect(result.success).toBe(true);
|
|
@@ -110,7 +116,7 @@ describe('Daemon TRPC Router', () => {
|
|
|
110
116
|
vi.mocked(chats.getDefaultChatId).mockResolvedValue('default-chat');
|
|
111
117
|
vi.mocked(workspace.readChatSettings).mockResolvedValue({ jobs: [mockJob] });
|
|
112
118
|
|
|
113
|
-
const caller =
|
|
119
|
+
const caller = userRouter.createCaller({});
|
|
114
120
|
const updatedJob = { ...mockJob, message: 'updated' };
|
|
115
121
|
const result = await caller.addCronJob({ job: updatedJob });
|
|
116
122
|
|
|
@@ -124,7 +130,7 @@ describe('Daemon TRPC Router', () => {
|
|
|
124
130
|
vi.mocked(chats.getDefaultChatId).mockResolvedValue('default-chat');
|
|
125
131
|
vi.mocked(workspace.readChatSettings).mockResolvedValue({ jobs: [mockJob] });
|
|
126
132
|
|
|
127
|
-
const caller =
|
|
133
|
+
const caller = userRouter.createCaller({});
|
|
128
134
|
const result = await caller.deleteCronJob({ id: 'job-1' });
|
|
129
135
|
|
|
130
136
|
expect(result.success).toBe(true);
|
|
@@ -136,7 +142,7 @@ describe('Daemon TRPC Router', () => {
|
|
|
136
142
|
vi.mocked(chats.getDefaultChatId).mockResolvedValue('default-chat');
|
|
137
143
|
vi.mocked(workspace.readChatSettings).mockResolvedValue({ jobs: [mockJob] });
|
|
138
144
|
|
|
139
|
-
const caller =
|
|
145
|
+
const caller = userRouter.createCaller({});
|
|
140
146
|
const result = await caller.deleteCronJob({ id: 'non-existent' });
|
|
141
147
|
|
|
142
148
|
expect(result.success).toBe(true);
|
|
@@ -150,7 +156,7 @@ describe('Daemon TRPC Router', () => {
|
|
|
150
156
|
vi.mocked(chats.getDefaultChatId).mockResolvedValue('default-chat');
|
|
151
157
|
vi.mocked((fs as any).default.readFile).mockResolvedValue('{}');
|
|
152
158
|
|
|
153
|
-
const caller =
|
|
159
|
+
const caller = userRouter.createCaller({});
|
|
154
160
|
await caller.sendMessage({
|
|
155
161
|
type: 'send-message',
|
|
156
162
|
client: 'cli',
|
|
@@ -178,7 +184,7 @@ describe('Daemon TRPC Router', () => {
|
|
|
178
184
|
vi.mocked((fs as any).default.rename).mockResolvedValue(undefined);
|
|
179
185
|
vi.mocked((fs as any).default.access).mockResolvedValue(undefined);
|
|
180
186
|
|
|
181
|
-
const caller =
|
|
187
|
+
const caller = userRouter.createCaller({});
|
|
182
188
|
await caller.sendMessage({
|
|
183
189
|
type: 'send-message',
|
|
184
190
|
client: 'cli',
|
|
@@ -216,7 +222,7 @@ describe('Daemon TRPC Router', () => {
|
|
|
216
222
|
.mockRejectedValue(new Error('not found'));
|
|
217
223
|
vi.mocked((fs as any).default.rename).mockResolvedValue(undefined);
|
|
218
224
|
|
|
219
|
-
const caller =
|
|
225
|
+
const caller = userRouter.createCaller({});
|
|
220
226
|
await caller.sendMessage({
|
|
221
227
|
type: 'send-message',
|
|
222
228
|
client: 'cli',
|
|
@@ -243,7 +249,7 @@ describe('Daemon TRPC Router', () => {
|
|
|
243
249
|
vi.mocked(chats.getDefaultChatId).mockResolvedValue('default-chat');
|
|
244
250
|
vi.mocked((fs as any).default.readFile).mockResolvedValue('{}');
|
|
245
251
|
|
|
246
|
-
const caller =
|
|
252
|
+
const caller = userRouter.createCaller({});
|
|
247
253
|
await expect(
|
|
248
254
|
caller.sendMessage({
|
|
249
255
|
type: 'send-message',
|
|
@@ -262,7 +268,7 @@ describe('Daemon TRPC Router', () => {
|
|
|
262
268
|
vi.mocked((fs as any).default.readFile).mockResolvedValue('{}');
|
|
263
269
|
vi.mocked((fs as any).default.access).mockRejectedValue(new Error('ENOENT'));
|
|
264
270
|
|
|
265
|
-
const caller =
|
|
271
|
+
const caller = userRouter.createCaller({});
|
|
266
272
|
await expect(
|
|
267
273
|
caller.sendMessage({
|
|
268
274
|
type: 'send-message',
|
|
@@ -282,9 +288,11 @@ describe('Daemon TRPC Router', () => {
|
|
|
282
288
|
vi.mocked(chats.getDefaultChatId).mockResolvedValue('default-chat');
|
|
283
289
|
vi.mocked(chats.appendMessage).mockResolvedValue(undefined);
|
|
284
290
|
|
|
285
|
-
const caller =
|
|
291
|
+
const caller = agentRouter.createCaller({
|
|
292
|
+
isApiServer: true,
|
|
293
|
+
tokenPayload: { agentId: 'default', chatId: 'default-chat' },
|
|
294
|
+
} as any);
|
|
286
295
|
const result = await caller.logMessage({
|
|
287
|
-
chatId: 'default-chat',
|
|
288
296
|
message: 'Test log',
|
|
289
297
|
});
|
|
290
298
|
|
|
@@ -304,9 +312,11 @@ describe('Daemon TRPC Router', () => {
|
|
|
304
312
|
vi.mocked(chats.appendMessage).mockResolvedValue(undefined);
|
|
305
313
|
vi.mocked((fs as any).default.access).mockResolvedValue(undefined);
|
|
306
314
|
|
|
307
|
-
const caller =
|
|
315
|
+
const caller = agentRouter.createCaller({
|
|
316
|
+
isApiServer: true,
|
|
317
|
+
tokenPayload: { agentId: 'default', chatId: 'default-chat' },
|
|
318
|
+
} as any);
|
|
308
319
|
const result = await caller.logMessage({
|
|
309
|
-
chatId: 'default-chat',
|
|
310
320
|
message: 'Test log with file',
|
|
311
321
|
files: ['attachments/discord/image.png'],
|
|
312
322
|
});
|
|
@@ -325,10 +335,12 @@ describe('Daemon TRPC Router', () => {
|
|
|
325
335
|
it('should reject file path with directory traversal (..)', async () => {
|
|
326
336
|
vi.mocked(chats.getDefaultChatId).mockResolvedValue('default-chat');
|
|
327
337
|
|
|
328
|
-
const caller =
|
|
338
|
+
const caller = agentRouter.createCaller({
|
|
339
|
+
isApiServer: true,
|
|
340
|
+
tokenPayload: { agentId: 'default', chatId: 'default-chat' },
|
|
341
|
+
} as any);
|
|
329
342
|
await expect(
|
|
330
343
|
caller.logMessage({
|
|
331
|
-
chatId: 'default-chat',
|
|
332
344
|
message: 'Malicious log',
|
|
333
345
|
files: ['../secret.txt'],
|
|
334
346
|
})
|
|
@@ -338,10 +350,12 @@ describe('Daemon TRPC Router', () => {
|
|
|
338
350
|
it('should reject file path with absolute path', async () => {
|
|
339
351
|
vi.mocked(chats.getDefaultChatId).mockResolvedValue('default-chat');
|
|
340
352
|
|
|
341
|
-
const caller =
|
|
353
|
+
const caller = agentRouter.createCaller({
|
|
354
|
+
isApiServer: true,
|
|
355
|
+
tokenPayload: { agentId: 'default', chatId: 'default-chat' },
|
|
356
|
+
} as any);
|
|
342
357
|
await expect(
|
|
343
358
|
caller.logMessage({
|
|
344
|
-
chatId: 'default-chat',
|
|
345
359
|
message: 'Malicious log',
|
|
346
360
|
files: ['/etc/passwd'],
|
|
347
361
|
})
|
|
@@ -353,7 +367,7 @@ describe('Daemon TRPC Router', () => {
|
|
|
353
367
|
it('waitForTyping should yield typing events for the correct chatId', async () => {
|
|
354
368
|
vi.mocked(chats.getDefaultChatId).mockResolvedValue('default-chat');
|
|
355
369
|
|
|
356
|
-
const caller =
|
|
370
|
+
const caller = userRouter.createCaller({});
|
|
357
371
|
const iterable = await caller.waitForTyping({ chatId: 'default-chat' });
|
|
358
372
|
const iterator = iterable[Symbol.asyncIterator]();
|
|
359
373
|
|
|
@@ -365,7 +379,7 @@ describe('Daemon TRPC Router', () => {
|
|
|
365
379
|
if (e2.value) events.push(e2.value);
|
|
366
380
|
})();
|
|
367
381
|
|
|
368
|
-
const { daemonEvents, DAEMON_EVENT_TYPING } = await import('
|
|
382
|
+
const { daemonEvents, DAEMON_EVENT_TYPING } = await import('../events.js');
|
|
369
383
|
await new Promise((resolve) => setTimeout(resolve, 10));
|
|
370
384
|
|
|
371
385
|
daemonEvents.emit(DAEMON_EVENT_TYPING, { chatId: 'default-chat' });
|
|
@@ -377,4 +391,57 @@ describe('Daemon TRPC Router', () => {
|
|
|
377
391
|
expect(events).toEqual([{ chatId: 'default-chat' }, { chatId: 'default-chat' }]);
|
|
378
392
|
});
|
|
379
393
|
});
|
|
394
|
+
|
|
395
|
+
describe('fetchPendingMessages', () => {
|
|
396
|
+
let queue: ReturnType<typeof getMessageQueue>;
|
|
397
|
+
beforeEach(() => {
|
|
398
|
+
queue = getMessageQueue(process.cwd());
|
|
399
|
+
queue.clear();
|
|
400
|
+
});
|
|
401
|
+
|
|
402
|
+
it('should extract pending messages from queue matching the session and format them', async () => {
|
|
403
|
+
let resolveFirstTask: () => void;
|
|
404
|
+
const firstTaskPromise = new Promise<void>((r) => {
|
|
405
|
+
resolveFirstTask = r;
|
|
406
|
+
});
|
|
407
|
+
|
|
408
|
+
// The first task will start and block, leaving the others in pending
|
|
409
|
+
queue.enqueue(
|
|
410
|
+
async () => {
|
|
411
|
+
await firstTaskPromise;
|
|
412
|
+
},
|
|
413
|
+
{ text: 'Task 1', sessionId: 's1' }
|
|
414
|
+
);
|
|
415
|
+
|
|
416
|
+
// These will stay in pending
|
|
417
|
+
const p2 = queue.enqueue(async () => {}, { text: 'Task 2', sessionId: 's1' });
|
|
418
|
+
const p3 = queue.enqueue(async () => {}, { text: 'Task 3', sessionId: 's1' });
|
|
419
|
+
const p4 = queue.enqueue(async () => {}, { text: 'Task 4', sessionId: 's2' });
|
|
420
|
+
|
|
421
|
+
// We expect them to throw AbortError when extracted
|
|
422
|
+
p2.catch(() => {});
|
|
423
|
+
p3.catch(() => {});
|
|
424
|
+
p4.catch(() => {});
|
|
425
|
+
|
|
426
|
+
const caller = agentRouter.createCaller({
|
|
427
|
+
tokenPayload: { sessionId: 's1', chatId: 'c1', agentId: 'a1', timestamp: 123 },
|
|
428
|
+
});
|
|
429
|
+
const result = await caller.fetchPendingMessages();
|
|
430
|
+
|
|
431
|
+
expect(result.messages).toBe(
|
|
432
|
+
'<message>\nTask 2\n</message>\n\n<message>\nTask 3\n</message>'
|
|
433
|
+
);
|
|
434
|
+
expect(queue.extractPending((p) => p.sessionId === 's2')).toEqual([
|
|
435
|
+
{ text: 'Task 4', sessionId: 's2' },
|
|
436
|
+
]);
|
|
437
|
+
|
|
438
|
+
resolveFirstTask!(); // cleanup
|
|
439
|
+
});
|
|
440
|
+
|
|
441
|
+
it('should return empty string if no pending messages', async () => {
|
|
442
|
+
const caller = agentRouter.createCaller({});
|
|
443
|
+
const result = await caller.fetchPendingMessages();
|
|
444
|
+
expect(result.messages).toBe('');
|
|
445
|
+
});
|
|
446
|
+
});
|
|
380
447
|
});
|
|
@@ -1,19 +1,19 @@
|
|
|
1
1
|
/* eslint-disable @typescript-eslint/no-explicit-any */
|
|
2
2
|
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
3
|
-
import { appRouter } from './
|
|
4
|
-
import * as chats from '
|
|
3
|
+
import { agentRouter as appRouter } from './index.js';
|
|
4
|
+
import * as chats from '../../shared/chats.js';
|
|
5
5
|
|
|
6
|
-
vi.mock('
|
|
6
|
+
vi.mock('../../shared/chats.js', () => ({
|
|
7
7
|
getDefaultChatId: vi.fn().mockResolvedValue('default-chat'),
|
|
8
8
|
appendMessage: vi.fn().mockResolvedValue(undefined),
|
|
9
9
|
}));
|
|
10
10
|
|
|
11
|
-
vi.mock('
|
|
11
|
+
vi.mock('../../shared/workspace.js', () => ({
|
|
12
12
|
getWorkspaceRoot: vi.fn().mockReturnValue('/mock/workspace'),
|
|
13
13
|
getClawminiDir: vi.fn().mockReturnValue('/mock/.clawmini'),
|
|
14
14
|
}));
|
|
15
15
|
|
|
16
|
-
vi.mock('
|
|
16
|
+
vi.mock('../policy-request-service.js', () => {
|
|
17
17
|
return {
|
|
18
18
|
PolicyRequestService: class {
|
|
19
19
|
async createRequest() {
|
|
@@ -39,12 +39,23 @@ const { mockReadFile } = vi.hoisted(() => {
|
|
|
39
39
|
return { mockReadFile: vi.fn() };
|
|
40
40
|
});
|
|
41
41
|
|
|
42
|
-
vi.mock('node:fs/promises', () =>
|
|
43
|
-
|
|
42
|
+
vi.mock('node:fs/promises', async (importOriginal) => {
|
|
43
|
+
const actual = await importOriginal<typeof import('node:fs/promises')>();
|
|
44
|
+
return {
|
|
45
|
+
...actual,
|
|
46
|
+
default: {
|
|
47
|
+
...actual,
|
|
48
|
+
readFile: mockReadFile,
|
|
49
|
+
mkdir: vi.fn(),
|
|
50
|
+
readdir: vi.fn().mockResolvedValue([]),
|
|
51
|
+
realpath: vi.fn().mockImplementation((p) => Promise.resolve(p)),
|
|
52
|
+
},
|
|
44
53
|
readFile: mockReadFile,
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
54
|
+
mkdir: vi.fn(),
|
|
55
|
+
readdir: vi.fn().mockResolvedValue([]),
|
|
56
|
+
realpath: vi.fn().mockImplementation((p) => Promise.resolve(p)),
|
|
57
|
+
};
|
|
58
|
+
});
|
|
48
59
|
|
|
49
60
|
describe('createPolicyRequest preview message', () => {
|
|
50
61
|
beforeEach(() => {
|
|
@@ -52,7 +63,10 @@ describe('createPolicyRequest preview message', () => {
|
|
|
52
63
|
});
|
|
53
64
|
|
|
54
65
|
it('should create a request and append a preview message truncating long files', async () => {
|
|
55
|
-
const caller = appRouter.createCaller({
|
|
66
|
+
const caller = appRouter.createCaller({
|
|
67
|
+
isApiServer: true,
|
|
68
|
+
tokenPayload: { agentId: 'default', chatId: 'default-chat' },
|
|
69
|
+
} as any);
|
|
56
70
|
|
|
57
71
|
// file1 is short, file2 is long
|
|
58
72
|
const shortContent = 'Hello world!';
|
|
@@ -68,8 +82,8 @@ describe('createPolicyRequest preview message', () => {
|
|
|
68
82
|
commandName: 'test-cmd',
|
|
69
83
|
args: ['arg1', 'arg2'],
|
|
70
84
|
fileMappings: {
|
|
71
|
-
file1: '/
|
|
72
|
-
file2: '/
|
|
85
|
+
file1: '/mock/workspace/file1',
|
|
86
|
+
file2: '/mock/workspace/file2',
|
|
73
87
|
},
|
|
74
88
|
});
|
|
75
89
|
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
import fs from 'node:fs/promises';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { TRPCError } from '@trpc/server';
|
|
4
|
+
import { pathIsInsideDir } from '../../shared/utils/fs.js';
|
|
5
|
+
import {
|
|
6
|
+
getAgent,
|
|
7
|
+
getClawminiDir,
|
|
8
|
+
readChatSettings,
|
|
9
|
+
writeChatSettings,
|
|
10
|
+
} from '../../shared/workspace.js';
|
|
11
|
+
import { cronManager } from '../cron.js';
|
|
12
|
+
import type { z } from 'zod';
|
|
13
|
+
import type { CronJobSchema } from '../../shared/config.js';
|
|
14
|
+
|
|
15
|
+
export async function getUniquePath(p: string): Promise<string> {
|
|
16
|
+
let currentPath = p;
|
|
17
|
+
let counter = 1;
|
|
18
|
+
while (true) {
|
|
19
|
+
try {
|
|
20
|
+
await fs.stat(currentPath);
|
|
21
|
+
const ext = path.extname(p);
|
|
22
|
+
const base = path.basename(p, ext);
|
|
23
|
+
currentPath = path.join(path.dirname(p), `${base}-${counter}${ext}`);
|
|
24
|
+
counter++;
|
|
25
|
+
} catch {
|
|
26
|
+
return currentPath;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export async function resolveAgentDir(
|
|
32
|
+
agentId: string | undefined | null,
|
|
33
|
+
workspaceRoot: string
|
|
34
|
+
): Promise<string> {
|
|
35
|
+
if (agentId && agentId !== 'default') {
|
|
36
|
+
try {
|
|
37
|
+
const agent = await getAgent(agentId, workspaceRoot);
|
|
38
|
+
if (agent && agent.directory) {
|
|
39
|
+
return path.resolve(workspaceRoot, agent.directory);
|
|
40
|
+
}
|
|
41
|
+
} catch (err: unknown) {
|
|
42
|
+
console.warn(`Could not load custom agent '${agentId}' for resolving directory:`, err);
|
|
43
|
+
}
|
|
44
|
+
return path.resolve(workspaceRoot, agentId);
|
|
45
|
+
}
|
|
46
|
+
return workspaceRoot;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export async function getAgentFilesDir(
|
|
50
|
+
agentId: string | undefined,
|
|
51
|
+
chatId: string,
|
|
52
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
53
|
+
settings: any,
|
|
54
|
+
workspaceRoot: string
|
|
55
|
+
): Promise<string> {
|
|
56
|
+
const chatSettings = (await readChatSettings(chatId)) ?? {};
|
|
57
|
+
const targetAgentId = agentId ?? chatSettings.defaultAgent ?? 'default';
|
|
58
|
+
let agentFilesDir = settings?.defaultAgent?.files || './attachments';
|
|
59
|
+
const agentDir = await resolveAgentDir(targetAgentId, workspaceRoot);
|
|
60
|
+
|
|
61
|
+
if (targetAgentId !== 'default') {
|
|
62
|
+
try {
|
|
63
|
+
const customAgent = await getAgent(targetAgentId, workspaceRoot);
|
|
64
|
+
if (customAgent?.files) {
|
|
65
|
+
agentFilesDir = customAgent.files;
|
|
66
|
+
}
|
|
67
|
+
} catch (err: unknown) {
|
|
68
|
+
console.warn(
|
|
69
|
+
`Could not load custom agent '${targetAgentId}' for resolving files directory:`,
|
|
70
|
+
err
|
|
71
|
+
);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
return path.resolve(agentDir, agentFilesDir);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export async function validateAttachments(files: string[]): Promise<void> {
|
|
79
|
+
const tmpDir = path.join(getClawminiDir(process.cwd()), 'tmp');
|
|
80
|
+
|
|
81
|
+
for (const file of files) {
|
|
82
|
+
const absoluteFile = path.resolve(process.cwd(), file);
|
|
83
|
+
if (!pathIsInsideDir(absoluteFile, tmpDir)) {
|
|
84
|
+
throw new TRPCError({
|
|
85
|
+
code: 'BAD_REQUEST',
|
|
86
|
+
message: 'File must be inside the temporary directory.',
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
try {
|
|
90
|
+
await fs.access(absoluteFile);
|
|
91
|
+
} catch {
|
|
92
|
+
throw new TRPCError({
|
|
93
|
+
code: 'BAD_REQUEST',
|
|
94
|
+
message: `File does not exist: ${file}`,
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
export async function validateLogFile(
|
|
101
|
+
file: string,
|
|
102
|
+
agentDir: string,
|
|
103
|
+
workspaceRoot: string
|
|
104
|
+
): Promise<string> {
|
|
105
|
+
const resolvedPath = path.resolve(agentDir, file);
|
|
106
|
+
|
|
107
|
+
if (!pathIsInsideDir(resolvedPath, agentDir, { allowSameDir: true })) {
|
|
108
|
+
throw new TRPCError({
|
|
109
|
+
code: 'BAD_REQUEST',
|
|
110
|
+
message: 'File must be within the agent workspace.',
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
try {
|
|
115
|
+
await fs.access(resolvedPath);
|
|
116
|
+
} catch {
|
|
117
|
+
throw new TRPCError({
|
|
118
|
+
code: 'BAD_REQUEST',
|
|
119
|
+
message: `File does not exist: ${file}`,
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
return path.relative(workspaceRoot, resolvedPath);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
export async function listCronJobsShared(chatId: string) {
|
|
127
|
+
const settings = await readChatSettings(chatId);
|
|
128
|
+
return settings?.jobs ?? [];
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
export async function addCronJobShared(chatId: string, job: z.infer<typeof CronJobSchema>) {
|
|
132
|
+
const settings = (await readChatSettings(chatId)) || {};
|
|
133
|
+
const cronJobs = settings.jobs ?? [];
|
|
134
|
+
const existingIndex = cronJobs.findIndex((j) => j.id === job.id);
|
|
135
|
+
if (existingIndex >= 0) {
|
|
136
|
+
cronJobs[existingIndex] = job;
|
|
137
|
+
} else {
|
|
138
|
+
cronJobs.push(job);
|
|
139
|
+
}
|
|
140
|
+
settings.jobs = cronJobs;
|
|
141
|
+
await writeChatSettings(chatId, settings);
|
|
142
|
+
cronManager.scheduleJob(chatId, job);
|
|
143
|
+
return { success: true };
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
export async function deleteCronJobShared(chatId: string, id: string) {
|
|
147
|
+
const settings = await readChatSettings(chatId);
|
|
148
|
+
if (!settings || !settings.jobs) {
|
|
149
|
+
return { success: true, deleted: false };
|
|
150
|
+
}
|
|
151
|
+
const initialLength = settings.jobs.length;
|
|
152
|
+
settings.jobs = settings.jobs.filter((j) => j.id !== id);
|
|
153
|
+
if (settings.jobs.length !== initialLength) {
|
|
154
|
+
await writeChatSettings(chatId, settings);
|
|
155
|
+
cronManager.unscheduleJob(chatId, id);
|
|
156
|
+
return { success: true, deleted: true };
|
|
157
|
+
}
|
|
158
|
+
return { success: true, deleted: false };
|
|
159
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { initTRPC, TRPCError } from '@trpc/server';
|
|
2
|
+
import type { IncomingMessage, ServerResponse } from 'node:http';
|
|
3
|
+
import type { TokenPayload } from '../auth.js';
|
|
4
|
+
|
|
5
|
+
export interface Context {
|
|
6
|
+
req?: IncomingMessage | undefined;
|
|
7
|
+
res?: ServerResponse | undefined;
|
|
8
|
+
isApiServer?: boolean | undefined;
|
|
9
|
+
tokenPayload?: TokenPayload | null | undefined;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
const t = initTRPC.context<Context>().create();
|
|
13
|
+
export const router = t.router;
|
|
14
|
+
export const publicProcedure = t.procedure;
|
|
15
|
+
|
|
16
|
+
const apiAuthMiddleware = t.middleware(({ ctx, next }) => {
|
|
17
|
+
if (ctx.isApiServer) {
|
|
18
|
+
if (!ctx.tokenPayload) {
|
|
19
|
+
throw new TRPCError({ code: 'UNAUTHORIZED', message: 'Missing or invalid token' });
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
return next({
|
|
23
|
+
ctx: {
|
|
24
|
+
...ctx,
|
|
25
|
+
tokenPayload: ctx.tokenPayload,
|
|
26
|
+
},
|
|
27
|
+
});
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
export const apiProcedure = t.procedure.use(apiAuthMiddleware);
|