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
package/dist/daemon/index.mjs
CHANGED
|
@@ -1,9 +1,6 @@
|
|
|
1
|
-
import { C as
|
|
2
|
-
import { n as
|
|
3
|
-
import
|
|
4
|
-
import { n as exportLiteToEnvironment } from "../lite-Dl7WXyaH.mjs";
|
|
5
|
-
import { a as daemonEvents, i as DAEMON_EVENT_TYPING, o as emitTyping, r as DAEMON_EVENT_MESSAGE_APPENDED, t as appendMessage } from "../chats-Zd_HXDHx.mjs";
|
|
6
|
-
import fs from "node:fs";
|
|
1
|
+
import { C as CronJobSchema, S as pathIsInsideDir, _ as readSettings, a as getAgent, b as writeChatSettings, c as getSettingsPath, g as readPolicies, h as readEnvironment, i as getActiveEnvironmentInfo, l as getSocketPath, m as readChatSettings, o as getClawminiDir, p as readAgentSessionSettings, s as getEnvironmentPath, u as getWorkspaceRoot, v as writeAgentSessionSettings, w as SettingsSchema } from "../workspace-DjoNjhW0.mjs";
|
|
2
|
+
import { d as getMessages$1, n as exportLiteToEnvironment, o as appendMessage$1, p as listChats, u as getDefaultChatId } from "../lite-oSYSvaOr.mjs";
|
|
3
|
+
import fs, { constants } from "node:fs";
|
|
7
4
|
import path from "node:path";
|
|
8
5
|
import { execSync, spawn } from "node:child_process";
|
|
9
6
|
import fs$1 from "node:fs/promises";
|
|
@@ -12,9 +9,54 @@ import http from "node:http";
|
|
|
12
9
|
import net from "node:net";
|
|
13
10
|
import { createHTTPHandler } from "@trpc/server/adapters/standalone";
|
|
14
11
|
import { TRPCError, initTRPC } from "@trpc/server";
|
|
15
|
-
import crypto$1 from "node:crypto";
|
|
16
12
|
import schedule from "node-schedule";
|
|
13
|
+
import { EventEmitter, on } from "node:events";
|
|
14
|
+
import crypto$1, { randomBytes, randomUUID } from "node:crypto";
|
|
15
|
+
import fs$2 from "fs/promises";
|
|
16
|
+
import path$1 from "path";
|
|
17
|
+
import { randomInt } from "crypto";
|
|
17
18
|
|
|
19
|
+
//#region src/daemon/api/trpc.ts
|
|
20
|
+
const t = initTRPC.context().create();
|
|
21
|
+
const router = t.router;
|
|
22
|
+
const publicProcedure = t.procedure;
|
|
23
|
+
const apiAuthMiddleware = t.middleware(({ ctx, next }) => {
|
|
24
|
+
if (ctx.isApiServer) {
|
|
25
|
+
if (!ctx.tokenPayload) throw new TRPCError({
|
|
26
|
+
code: "UNAUTHORIZED",
|
|
27
|
+
message: "Missing or invalid token"
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
return next({ ctx: {
|
|
31
|
+
...ctx,
|
|
32
|
+
tokenPayload: ctx.tokenPayload
|
|
33
|
+
} });
|
|
34
|
+
});
|
|
35
|
+
const apiProcedure = t.procedure.use(apiAuthMiddleware);
|
|
36
|
+
|
|
37
|
+
//#endregion
|
|
38
|
+
//#region src/daemon/events.ts
|
|
39
|
+
const daemonEvents = new EventEmitter();
|
|
40
|
+
const DAEMON_EVENT_MESSAGE_APPENDED = "message-appended";
|
|
41
|
+
const DAEMON_EVENT_TYPING = "typing";
|
|
42
|
+
function emitMessageAppended(chatId, message) {
|
|
43
|
+
daemonEvents.emit(DAEMON_EVENT_MESSAGE_APPENDED, {
|
|
44
|
+
chatId,
|
|
45
|
+
message
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
function emitTyping(chatId) {
|
|
49
|
+
daemonEvents.emit(DAEMON_EVENT_TYPING, { chatId });
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
//#endregion
|
|
53
|
+
//#region src/daemon/chats.ts
|
|
54
|
+
async function appendMessage(id, message, startDir = process.cwd()) {
|
|
55
|
+
await appendMessage$1(id, message, startDir);
|
|
56
|
+
emitMessageAppended(id, message);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
//#endregion
|
|
18
60
|
//#region src/daemon/queue.ts
|
|
19
61
|
var Queue = class {
|
|
20
62
|
pending = [];
|
|
@@ -50,35 +92,38 @@ var Queue = class {
|
|
|
50
92
|
this.processNext().catch(() => {});
|
|
51
93
|
}
|
|
52
94
|
}
|
|
53
|
-
abortCurrent() {
|
|
95
|
+
abortCurrent(predicate) {
|
|
54
96
|
if (this.currentController) {
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
97
|
+
if (!predicate || this.currentPayload !== void 0 && predicate(this.currentPayload)) {
|
|
98
|
+
const error = /* @__PURE__ */ new Error("Task aborted");
|
|
99
|
+
error.name = "AbortError";
|
|
100
|
+
this.currentController.abort(error);
|
|
101
|
+
}
|
|
58
102
|
}
|
|
59
103
|
}
|
|
60
104
|
getCurrentPayload() {
|
|
61
105
|
return this.currentPayload;
|
|
62
106
|
}
|
|
63
|
-
clear(reason = "Task cleared") {
|
|
64
|
-
const tasksToClear = [...this.pending];
|
|
65
|
-
this.pending =
|
|
107
|
+
clear(reason = "Task cleared", predicate) {
|
|
108
|
+
const tasksToClear = predicate ? this.pending.filter((p) => p.payload !== void 0 && predicate(p.payload)) : [...this.pending];
|
|
109
|
+
if (predicate) this.pending = this.pending.filter((p) => !(p.payload !== void 0 && predicate(p.payload)));
|
|
110
|
+
else this.pending = [];
|
|
66
111
|
for (const { reject } of tasksToClear) {
|
|
67
112
|
const error = new Error(reason);
|
|
68
113
|
error.name = "AbortError";
|
|
69
114
|
reject(error);
|
|
70
115
|
}
|
|
71
116
|
}
|
|
72
|
-
extractPending() {
|
|
73
|
-
const extracted = this.pending.map((p) => p.payload).filter((p) => p !== void 0);
|
|
74
|
-
this.clear("Task extracted for batching");
|
|
117
|
+
extractPending(predicate) {
|
|
118
|
+
const extracted = this.pending.map((p) => p.payload).filter((p) => p !== void 0 && (!predicate || predicate(p)));
|
|
119
|
+
this.clear("Task extracted for batching", predicate);
|
|
75
120
|
return extracted;
|
|
76
121
|
}
|
|
77
122
|
};
|
|
78
|
-
const
|
|
79
|
-
function
|
|
80
|
-
if (!
|
|
81
|
-
return
|
|
123
|
+
const messageQueues = /* @__PURE__ */ new Map();
|
|
124
|
+
function getMessageQueue(dir) {
|
|
125
|
+
if (!messageQueues.has(dir)) messageQueues.set(dir, new Queue());
|
|
126
|
+
return messageQueues.get(dir);
|
|
82
127
|
}
|
|
83
128
|
|
|
84
129
|
//#endregion
|
|
@@ -154,18 +199,310 @@ const slashStop = createSlashActionRouter("stop", "stop", "Stopping current task
|
|
|
154
199
|
//#region src/daemon/routers/slash-interrupt.ts
|
|
155
200
|
const slashInterrupt = createSlashActionRouter("interrupt", "interrupt", "Interrupting current task...");
|
|
156
201
|
|
|
202
|
+
//#endregion
|
|
203
|
+
//#region src/daemon/request-store.ts
|
|
204
|
+
const PolicyRequestSchema = z.object({
|
|
205
|
+
id: z.string(),
|
|
206
|
+
commandName: z.string(),
|
|
207
|
+
args: z.array(z.string()),
|
|
208
|
+
fileMappings: z.record(z.string(), z.string()),
|
|
209
|
+
state: z.enum([
|
|
210
|
+
"Pending",
|
|
211
|
+
"Approved",
|
|
212
|
+
"Rejected"
|
|
213
|
+
]),
|
|
214
|
+
createdAt: z.number(),
|
|
215
|
+
rejectionReason: z.string().optional(),
|
|
216
|
+
chatId: z.string(),
|
|
217
|
+
agentId: z.string()
|
|
218
|
+
});
|
|
219
|
+
function isENOENT(err) {
|
|
220
|
+
return Boolean(err && typeof err === "object" && "code" in err && err.code === "ENOENT");
|
|
221
|
+
}
|
|
222
|
+
var RequestStore = class {
|
|
223
|
+
baseDir;
|
|
224
|
+
constructor(startDir = process.cwd()) {
|
|
225
|
+
this.baseDir = path$1.join(getClawminiDir(startDir), "tmp", "requests");
|
|
226
|
+
}
|
|
227
|
+
async init() {
|
|
228
|
+
await fs$2.mkdir(this.baseDir, { recursive: true });
|
|
229
|
+
}
|
|
230
|
+
getFilePath(id) {
|
|
231
|
+
return path$1.join(this.baseDir, `${id}.json`);
|
|
232
|
+
}
|
|
233
|
+
async save(request) {
|
|
234
|
+
await this.init();
|
|
235
|
+
const normalizedId = normalizePolicyId(request.id);
|
|
236
|
+
request.id = normalizedId;
|
|
237
|
+
const filePath = this.getFilePath(normalizedId);
|
|
238
|
+
await fs$2.writeFile(filePath, JSON.stringify(request, null, 2), "utf8");
|
|
239
|
+
}
|
|
240
|
+
async load(id) {
|
|
241
|
+
const normalizedId = normalizePolicyId(id);
|
|
242
|
+
const filePath = this.getFilePath(normalizedId);
|
|
243
|
+
try {
|
|
244
|
+
const data = await fs$2.readFile(filePath, "utf8");
|
|
245
|
+
return PolicyRequestSchema.parse(JSON.parse(data));
|
|
246
|
+
} catch (err) {
|
|
247
|
+
if (isENOENT(err)) return null;
|
|
248
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
249
|
+
console.warn(`Failed to parse request file ${filePath}:`, msg);
|
|
250
|
+
return null;
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
async list() {
|
|
254
|
+
await this.init();
|
|
255
|
+
const requests = [];
|
|
256
|
+
try {
|
|
257
|
+
const files = await fs$2.readdir(this.baseDir);
|
|
258
|
+
for (const file of files) {
|
|
259
|
+
if (!file.endsWith(".json")) continue;
|
|
260
|
+
const id = path$1.basename(file, ".json");
|
|
261
|
+
const req = await this.load(id);
|
|
262
|
+
if (req) requests.push(req);
|
|
263
|
+
}
|
|
264
|
+
} catch (err) {
|
|
265
|
+
if (!isENOENT(err)) throw err;
|
|
266
|
+
}
|
|
267
|
+
return requests.sort((a, b) => b.createdAt - a.createdAt);
|
|
268
|
+
}
|
|
269
|
+
};
|
|
270
|
+
function generateRandomAlphaNumericString(length) {
|
|
271
|
+
const characters = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ";
|
|
272
|
+
let result = "";
|
|
273
|
+
for (let i = 0; i < length; i++) result += characters[Math.floor(randomInt(36))];
|
|
274
|
+
return result;
|
|
275
|
+
}
|
|
276
|
+
function normalizePolicyId(id) {
|
|
277
|
+
return id.toLocaleUpperCase().trim();
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
//#endregion
|
|
281
|
+
//#region src/daemon/policy-utils.ts
|
|
282
|
+
const MAX_SNAPSHOT_SIZE = 5 * 1024 * 1024;
|
|
283
|
+
async function createSnapshot(requestedPath, agentDir, snapshotDir) {
|
|
284
|
+
let realAgentDir;
|
|
285
|
+
try {
|
|
286
|
+
realAgentDir = await fs$1.realpath(agentDir);
|
|
287
|
+
} catch (err) {
|
|
288
|
+
throw new Error(`Agent directory not found or cannot be resolved: ${agentDir}`, { cause: err });
|
|
289
|
+
}
|
|
290
|
+
const resolvedRequestedPath = path.resolve(realAgentDir, requestedPath);
|
|
291
|
+
if (!pathIsInsideDir(resolvedRequestedPath, realAgentDir, { allowSameDir: true })) throw new Error(`Security Error: Path resolves outside the allowed agent directory: ${resolvedRequestedPath}`);
|
|
292
|
+
let stat;
|
|
293
|
+
try {
|
|
294
|
+
stat = await fs$1.lstat(resolvedRequestedPath);
|
|
295
|
+
} catch (err) {
|
|
296
|
+
throw new Error(`File not found or cannot be accessed: ${requestedPath}`, { cause: err });
|
|
297
|
+
}
|
|
298
|
+
if (stat.isSymbolicLink()) throw new Error(`Security Error: Symlinks are not allowed: ${requestedPath}`);
|
|
299
|
+
if (!stat.isFile()) throw new Error(`Requested path is not a file: ${requestedPath}`);
|
|
300
|
+
if (stat.size > MAX_SNAPSHOT_SIZE) throw new Error(`File exceeds maximum snapshot size of 5MB: ${requestedPath}`);
|
|
301
|
+
const ext = path.extname(resolvedRequestedPath);
|
|
302
|
+
const base = path.basename(resolvedRequestedPath, ext);
|
|
303
|
+
await fs$1.mkdir(snapshotDir, { recursive: true });
|
|
304
|
+
let snapshotPath;
|
|
305
|
+
while (true) {
|
|
306
|
+
const snapshotFileName = `${base}_${randomBytes(8).toString("hex")}${ext}`;
|
|
307
|
+
snapshotPath = path.join(snapshotDir, snapshotFileName);
|
|
308
|
+
try {
|
|
309
|
+
await fs$1.copyFile(resolvedRequestedPath, snapshotPath, constants.COPYFILE_EXCL);
|
|
310
|
+
break;
|
|
311
|
+
} catch (err) {
|
|
312
|
+
if (err instanceof Error && "code" in err && err.code === "EEXIST") continue;
|
|
313
|
+
throw err;
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
return snapshotPath;
|
|
317
|
+
}
|
|
318
|
+
function interpolateArgs(args, snapshots) {
|
|
319
|
+
return args.map((arg) => {
|
|
320
|
+
let interpolated = arg;
|
|
321
|
+
for (const [key, snapshotPath] of Object.entries(snapshots)) {
|
|
322
|
+
const variable = `{{${key}}}`;
|
|
323
|
+
interpolated = interpolated.replaceAll(variable, snapshotPath);
|
|
324
|
+
}
|
|
325
|
+
return interpolated;
|
|
326
|
+
});
|
|
327
|
+
}
|
|
328
|
+
function executeSafe(command, args, options) {
|
|
329
|
+
return new Promise((resolve) => {
|
|
330
|
+
const p = spawn(command, args, {
|
|
331
|
+
shell: false,
|
|
332
|
+
cwd: options?.cwd,
|
|
333
|
+
env: options?.env
|
|
334
|
+
});
|
|
335
|
+
let stdout = "";
|
|
336
|
+
let stderr = "";
|
|
337
|
+
if (p.stdout) p.stdout.on("data", (data) => {
|
|
338
|
+
stdout += data.toString();
|
|
339
|
+
});
|
|
340
|
+
if (p.stderr) p.stderr.on("data", (data) => {
|
|
341
|
+
stderr += data.toString();
|
|
342
|
+
});
|
|
343
|
+
p.on("close", (code) => {
|
|
344
|
+
resolve({
|
|
345
|
+
stdout,
|
|
346
|
+
stderr,
|
|
347
|
+
exitCode: code ?? 1
|
|
348
|
+
});
|
|
349
|
+
});
|
|
350
|
+
p.on("error", (err) => {
|
|
351
|
+
resolve({
|
|
352
|
+
stdout: "",
|
|
353
|
+
stderr: err.toString(),
|
|
354
|
+
exitCode: 1
|
|
355
|
+
});
|
|
356
|
+
});
|
|
357
|
+
});
|
|
358
|
+
}
|
|
359
|
+
async function generateRequestPreview(request) {
|
|
360
|
+
let previewContent = `Sandbox Policy Request: ${request.commandName}\n`;
|
|
361
|
+
previewContent += `ID: ${request.id}\n`;
|
|
362
|
+
if (request.args.length > 0) previewContent += `Args: ${request.args.join(" ")}\n`;
|
|
363
|
+
for (const [name, snapPath] of Object.entries(request.fileMappings)) {
|
|
364
|
+
previewContent += `File [${name}]:\n`;
|
|
365
|
+
try {
|
|
366
|
+
let content = await fs$1.readFile(snapPath, "utf8");
|
|
367
|
+
if (content.length > 500) content = content.substring(0, 500) + "\n... (truncated)\n";
|
|
368
|
+
previewContent += content;
|
|
369
|
+
} catch (e) {
|
|
370
|
+
previewContent += `<Error reading file: ${e.message}>\n`;
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
previewContent += `\nUse /approve ${request.id} or /reject ${request.id} [reason]`;
|
|
374
|
+
return previewContent;
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
//#endregion
|
|
378
|
+
//#region src/daemon/routers/slash-policies.ts
|
|
379
|
+
async function loadAndValidateRequest(id, state) {
|
|
380
|
+
const store = new RequestStore(getWorkspaceRoot());
|
|
381
|
+
const req = await store.load(id);
|
|
382
|
+
if (!req) return { error: {
|
|
383
|
+
...state,
|
|
384
|
+
message: "",
|
|
385
|
+
reply: `Request not found: ${id}`
|
|
386
|
+
} };
|
|
387
|
+
if (req.chatId && req.chatId !== state.chatId) return { error: {
|
|
388
|
+
...state,
|
|
389
|
+
message: "",
|
|
390
|
+
reply: `Request belongs to a different chat: ${req.chatId}`
|
|
391
|
+
} };
|
|
392
|
+
if (req.state !== "Pending") return { error: {
|
|
393
|
+
...state,
|
|
394
|
+
message: "",
|
|
395
|
+
reply: `Request is not pending: ${id}`
|
|
396
|
+
} };
|
|
397
|
+
return {
|
|
398
|
+
req,
|
|
399
|
+
store
|
|
400
|
+
};
|
|
401
|
+
}
|
|
402
|
+
async function slashPolicies(state) {
|
|
403
|
+
const message = state.message.trim();
|
|
404
|
+
if (message === "/pending") {
|
|
405
|
+
const pending = (await new RequestStore(getWorkspaceRoot()).list()).filter((r) => r.state === "Pending");
|
|
406
|
+
let reply = `Pending Requests (${pending.length}):\n`;
|
|
407
|
+
for (const req of pending) reply += `- ID: ${req.id} | Command: ${req.commandName} ${req.args.join(" ")}\n`;
|
|
408
|
+
return {
|
|
409
|
+
...state,
|
|
410
|
+
reply,
|
|
411
|
+
action: "stop"
|
|
412
|
+
};
|
|
413
|
+
}
|
|
414
|
+
const approveMatch = message.match(/^\/approve\s+([^\s]+)/);
|
|
415
|
+
if (approveMatch) {
|
|
416
|
+
const id = approveMatch[1];
|
|
417
|
+
if (!id) return state;
|
|
418
|
+
const { req, store, error } = await loadAndValidateRequest(id, state);
|
|
419
|
+
if (error) return error;
|
|
420
|
+
if (!req || !store) return state;
|
|
421
|
+
const policy = (await readPolicies())?.policies?.[req.commandName];
|
|
422
|
+
if (!policy) return {
|
|
423
|
+
...state,
|
|
424
|
+
message: "",
|
|
425
|
+
reply: `Policy not found: ${req.commandName}`
|
|
426
|
+
};
|
|
427
|
+
req.state = "Approved";
|
|
428
|
+
await store.save(req);
|
|
429
|
+
const interpolatedArgs = interpolateArgs([...policy.args || [], ...req.args], req.fileMappings);
|
|
430
|
+
const { stdout, stderr, exitCode } = await executeSafe(policy.command, interpolatedArgs, { cwd: getWorkspaceRoot() });
|
|
431
|
+
const commandStr = `${policy.command} ${interpolatedArgs.join(" ")}`;
|
|
432
|
+
const logMsg = {
|
|
433
|
+
id: randomUUID(),
|
|
434
|
+
messageId: state.messageId,
|
|
435
|
+
role: "log",
|
|
436
|
+
source: "router",
|
|
437
|
+
content: `Request ${id} approved and executed.`,
|
|
438
|
+
stderr,
|
|
439
|
+
stdout,
|
|
440
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
441
|
+
command: commandStr,
|
|
442
|
+
cwd: getWorkspaceRoot(),
|
|
443
|
+
exitCode
|
|
444
|
+
};
|
|
445
|
+
await appendMessage(state.chatId, logMsg);
|
|
446
|
+
const agentMessage = `Request ${id} approved.\n\n${wrapInHtml("stdout", stdout)}\n\n${wrapInHtml("stderr", stderr)}\n\nExit Code: ${exitCode}`;
|
|
447
|
+
return {
|
|
448
|
+
...state,
|
|
449
|
+
message: agentMessage,
|
|
450
|
+
reply: `Approved request, running ${req.commandName}`
|
|
451
|
+
};
|
|
452
|
+
}
|
|
453
|
+
const rejectMatch = message.match(/^\/reject\s+([^\s]+)(?:\s+(.*))?/);
|
|
454
|
+
if (rejectMatch) {
|
|
455
|
+
const id = rejectMatch[1];
|
|
456
|
+
if (!id) return state;
|
|
457
|
+
const reason = rejectMatch[2] || "No reason provided";
|
|
458
|
+
const { req, store, error } = await loadAndValidateRequest(id, state);
|
|
459
|
+
if (error) return error;
|
|
460
|
+
if (!req || !store) return state;
|
|
461
|
+
req.state = "Rejected";
|
|
462
|
+
req.rejectionReason = reason;
|
|
463
|
+
await store.save(req);
|
|
464
|
+
const logMsg = {
|
|
465
|
+
id: randomUUID(),
|
|
466
|
+
messageId: state.messageId,
|
|
467
|
+
role: "log",
|
|
468
|
+
source: "router",
|
|
469
|
+
content: `Request ${id} rejected. Reason: ${reason}`,
|
|
470
|
+
stderr: "",
|
|
471
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
472
|
+
command: `policy-request-reject ${id}`,
|
|
473
|
+
cwd: getWorkspaceRoot(),
|
|
474
|
+
exitCode: 1
|
|
475
|
+
};
|
|
476
|
+
await appendMessage(state.chatId, logMsg);
|
|
477
|
+
const agentMessage = `Request ${id} rejected. Reason: ${reason}`;
|
|
478
|
+
return {
|
|
479
|
+
...state,
|
|
480
|
+
message: agentMessage
|
|
481
|
+
};
|
|
482
|
+
}
|
|
483
|
+
return state;
|
|
484
|
+
}
|
|
485
|
+
function wrapInHtml(tag, text) {
|
|
486
|
+
if (text.trim().length === 0) return `<${tag}></${tag}>`;
|
|
487
|
+
return `<${tag}>\n${text.trim()}\n</${tag}>`;
|
|
488
|
+
}
|
|
489
|
+
|
|
157
490
|
//#endregion
|
|
158
491
|
//#region src/daemon/routers.ts
|
|
159
492
|
async function executeRouterPipeline(initialState, routers) {
|
|
160
493
|
let state = { ...initialState };
|
|
161
|
-
for (const router of routers)
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
494
|
+
for (const router of routers) {
|
|
495
|
+
if (state.action === "stop") break;
|
|
496
|
+
if (router === "@clawmini/slash-new") state = slashNew(state);
|
|
497
|
+
else if (router === "@clawmini/slash-command") state = await slashCommand(state);
|
|
498
|
+
else if (router === "@clawmini/slash-stop") state = slashStop(state);
|
|
499
|
+
else if (router === "@clawmini/slash-interrupt") state = slashInterrupt(state);
|
|
500
|
+
else if (router === "@clawmini/slash-policies") state = await slashPolicies(state);
|
|
501
|
+
else try {
|
|
502
|
+
state = await executeCustomRouter(router, state);
|
|
503
|
+
} catch (err) {
|
|
504
|
+
console.error(`Router error [${router}]:`, err);
|
|
505
|
+
}
|
|
169
506
|
}
|
|
170
507
|
return state;
|
|
171
508
|
}
|
|
@@ -296,6 +633,9 @@ function calculateDelay(attempt, baseDelayMs, isFallback = false) {
|
|
|
296
633
|
const delay = baseDelayMs * Math.pow(2, effectiveAttempt - 1);
|
|
297
634
|
return Math.min(delay, 15e3);
|
|
298
635
|
}
|
|
636
|
+
function formatPendingMessages(payloads) {
|
|
637
|
+
return payloads.map((text) => `<message>\n${text}\n</message>`).join("\n\n");
|
|
638
|
+
}
|
|
299
639
|
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
300
640
|
async function resolveSessionState(chatId, cwd, sessionId, overrideAgentId) {
|
|
301
641
|
const chatSettings = await readChatSettings(chatId, cwd);
|
|
@@ -374,9 +714,9 @@ function formatEnvironmentPrefix(prefix, replacements) {
|
|
|
374
714
|
};
|
|
375
715
|
return prefix.replace(/{(WORKSPACE_DIR|AGENT_DIR|ENV_DIR|HOME_DIR|ENV_ARGS)}/g, (match) => map[match] || match);
|
|
376
716
|
}
|
|
377
|
-
async function executeDirectMessage(chatId, state, settings, cwd, runCommand, noWait =
|
|
717
|
+
async function executeDirectMessage(chatId, state, settings, cwd, runCommand, noWait = false, userMessageContent) {
|
|
378
718
|
const userMsg = {
|
|
379
|
-
id: crypto.randomUUID(),
|
|
719
|
+
id: state.messageId ?? crypto.randomUUID(),
|
|
380
720
|
role: "user",
|
|
381
721
|
content: userMessageContent ?? state.message,
|
|
382
722
|
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
@@ -396,18 +736,21 @@ async function executeDirectMessage(chatId, state, settings, cwd, runCommand, no
|
|
|
396
736
|
...state.reply.includes("NO_REPLY_NECESSARY") ? { level: "verbose" } : {}
|
|
397
737
|
});
|
|
398
738
|
if (!state.message.trim() && state.action !== "stop" && state.action !== "interrupt") return;
|
|
399
|
-
const queue =
|
|
739
|
+
const queue = getMessageQueue(cwd);
|
|
400
740
|
if (state.action === "stop") {
|
|
401
741
|
queue.abortCurrent();
|
|
402
742
|
queue.clear();
|
|
403
743
|
return;
|
|
404
744
|
}
|
|
405
745
|
if (state.action === "interrupt") {
|
|
746
|
+
const targetSessionId = state.sessionId || "default";
|
|
747
|
+
const isMatchingSession = (p) => p.sessionId === targetSessionId;
|
|
406
748
|
const currentPayload = queue.getCurrentPayload();
|
|
407
|
-
|
|
408
|
-
const extracted = queue.extractPending();
|
|
409
|
-
|
|
410
|
-
|
|
749
|
+
const currentMatches = currentPayload ? isMatchingSession(currentPayload) : false;
|
|
750
|
+
const extracted = queue.extractPending(isMatchingSession);
|
|
751
|
+
queue.abortCurrent(isMatchingSession);
|
|
752
|
+
const payloads = currentMatches && currentPayload ? [currentPayload, ...extracted] : extracted;
|
|
753
|
+
if (payloads.length > 0) state.message = `${formatPendingMessages(payloads.map((p) => p.text))}\n\n<message>\n${state.message}\n</message>`.trim();
|
|
411
754
|
}
|
|
412
755
|
if (!state.message.trim()) return;
|
|
413
756
|
const routerEnv = state.env ?? {};
|
|
@@ -570,20 +913,29 @@ async function executeDirectMessage(chatId, state, settings, cwd, runCommand, no
|
|
|
570
913
|
if (success) break;
|
|
571
914
|
}
|
|
572
915
|
if (lastLogMsg) await appendMessage(chatId, lastLogMsg);
|
|
573
|
-
},
|
|
574
|
-
|
|
916
|
+
}, {
|
|
917
|
+
text: state.message,
|
|
918
|
+
sessionId: state.sessionId || "default"
|
|
919
|
+
});
|
|
920
|
+
if (!noWait) try {
|
|
921
|
+
await taskPromise;
|
|
922
|
+
} catch (err) {
|
|
923
|
+
if (!(err instanceof Error && err.name === "AbortError")) throw err;
|
|
924
|
+
}
|
|
575
925
|
else taskPromise.catch((err) => {
|
|
576
926
|
if (err.name !== "AbortError") console.error("Task execution error:", err);
|
|
577
927
|
});
|
|
578
928
|
}
|
|
579
|
-
async function getInitialRouterState(chatId, message, cwd = process.cwd(), overrideAgentId, overrideSessionId) {
|
|
929
|
+
async function getInitialRouterState(chatId, message, cwd = process.cwd(), overrideAgentId, overrideSessionId, overrideMessageId) {
|
|
580
930
|
const chatSettings = await readChatSettings(chatId, cwd) ?? {};
|
|
581
931
|
const agentId = overrideAgentId ?? chatSettings.defaultAgent ?? "default";
|
|
932
|
+
const sessionId = overrideSessionId ?? chatSettings.sessions?.[agentId] ?? "default";
|
|
582
933
|
return {
|
|
934
|
+
messageId: overrideMessageId ?? crypto.randomUUID(),
|
|
583
935
|
message,
|
|
584
936
|
chatId,
|
|
585
937
|
agentId,
|
|
586
|
-
sessionId
|
|
938
|
+
sessionId,
|
|
587
939
|
env: {}
|
|
588
940
|
};
|
|
589
941
|
}
|
|
@@ -613,6 +965,7 @@ async function handleUserMessage(chatId, message, settings, cwd = process.cwd(),
|
|
|
613
965
|
}
|
|
614
966
|
if (settingsChanged) await writeChatSettings(chatId, chatSettings, cwd);
|
|
615
967
|
const directState = {
|
|
968
|
+
messageId: finalState.messageId,
|
|
616
969
|
message: finalMessage,
|
|
617
970
|
chatId,
|
|
618
971
|
env: routerEnv
|
|
@@ -778,23 +1131,7 @@ var CronManager = class {
|
|
|
778
1131
|
const cronManager = new CronManager();
|
|
779
1132
|
|
|
780
1133
|
//#endregion
|
|
781
|
-
//#region src/daemon/router.ts
|
|
782
|
-
const t = initTRPC.context().create();
|
|
783
|
-
const router = t.router;
|
|
784
|
-
const publicProcedure = t.procedure;
|
|
785
|
-
const apiAuthMiddleware = t.middleware(({ ctx, next }) => {
|
|
786
|
-
if (ctx.isApiServer) {
|
|
787
|
-
if (!ctx.tokenPayload) throw new TRPCError({
|
|
788
|
-
code: "UNAUTHORIZED",
|
|
789
|
-
message: "Missing or invalid token"
|
|
790
|
-
});
|
|
791
|
-
}
|
|
792
|
-
return next({ ctx: {
|
|
793
|
-
...ctx,
|
|
794
|
-
tokenPayload: ctx.tokenPayload
|
|
795
|
-
} });
|
|
796
|
-
});
|
|
797
|
-
const apiProcedure = t.procedure.use(apiAuthMiddleware);
|
|
1134
|
+
//#region src/daemon/api/router-utils.ts
|
|
798
1135
|
async function getUniquePath(p) {
|
|
799
1136
|
let currentPath = p;
|
|
800
1137
|
let counter = 1;
|
|
@@ -820,16 +1157,6 @@ async function resolveAgentDir(agentId, workspaceRoot) {
|
|
|
820
1157
|
}
|
|
821
1158
|
return workspaceRoot;
|
|
822
1159
|
}
|
|
823
|
-
async function resolveAndCheckChatId(ctx, inputChatId) {
|
|
824
|
-
const chatId = inputChatId ?? (ctx.isApiServer && ctx.tokenPayload ? ctx.tokenPayload.chatId : await getDefaultChatId());
|
|
825
|
-
if (ctx.isApiServer && ctx.tokenPayload) {
|
|
826
|
-
if (ctx.tokenPayload.chatId !== chatId) throw new TRPCError({
|
|
827
|
-
code: "FORBIDDEN",
|
|
828
|
-
message: "Token not authorized for this chat"
|
|
829
|
-
});
|
|
830
|
-
}
|
|
831
|
-
return chatId;
|
|
832
|
-
}
|
|
833
1160
|
async function getAgentFilesDir(agentId, chatId, settings, workspaceRoot) {
|
|
834
1161
|
const chatSettings = await readChatSettings(chatId) ?? {};
|
|
835
1162
|
const targetAgentId = agentId ?? chatSettings.defaultAgent ?? "default";
|
|
@@ -844,8 +1171,6 @@ async function getAgentFilesDir(agentId, chatId, settings, workspaceRoot) {
|
|
|
844
1171
|
return path.resolve(agentDir, agentFilesDir);
|
|
845
1172
|
}
|
|
846
1173
|
async function validateAttachments(files) {
|
|
847
|
-
const { pathIsInsideDir } = await import("../fs-B5wW0oaH.mjs").then((n) => n.t);
|
|
848
|
-
const { getClawminiDir } = await import("../workspace-CSgfo_2J.mjs").then((n) => n._);
|
|
849
1174
|
const tmpDir = path.join(getClawminiDir(process.cwd()), "tmp");
|
|
850
1175
|
for (const file of files) {
|
|
851
1176
|
const absoluteFile = path.resolve(process.cwd(), file);
|
|
@@ -864,7 +1189,6 @@ async function validateAttachments(files) {
|
|
|
864
1189
|
}
|
|
865
1190
|
}
|
|
866
1191
|
async function validateLogFile(file, agentDir, workspaceRoot) {
|
|
867
|
-
const { pathIsInsideDir } = await import("../fs-B5wW0oaH.mjs").then((n) => n.t);
|
|
868
1192
|
const resolvedPath = path.resolve(agentDir, file);
|
|
869
1193
|
if (!pathIsInsideDir(resolvedPath, agentDir, { allowSameDir: true })) throw new TRPCError({
|
|
870
1194
|
code: "BAD_REQUEST",
|
|
@@ -880,192 +1204,349 @@ async function validateLogFile(file, agentDir, workspaceRoot) {
|
|
|
880
1204
|
}
|
|
881
1205
|
return path.relative(workspaceRoot, resolvedPath);
|
|
882
1206
|
}
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
for (const file of files) {
|
|
927
|
-
const fileName = path.basename(file);
|
|
928
|
-
const targetPath = await getUniquePath(path.join(targetDir, fileName));
|
|
929
|
-
try {
|
|
930
|
-
await fs$1.rename(file, targetPath);
|
|
931
|
-
} catch {
|
|
932
|
-
await fs$1.copyFile(file, targetPath);
|
|
933
|
-
await fs$1.unlink(file);
|
|
934
|
-
}
|
|
935
|
-
finalPaths.push(path.relative(agentDir, targetPath));
|
|
936
|
-
}
|
|
937
|
-
const fileList = `Attached files:\n${finalPaths.map((p) => `- ${p}`).join("\n")}`;
|
|
938
|
-
message = message ? `${message}\n\n${fileList}` : fileList;
|
|
939
|
-
}
|
|
940
|
-
await handleUserMessage(chatId, message, settings, void 0, noWait, (args) => runCommand({
|
|
941
|
-
...args,
|
|
942
|
-
logToTerminal: true
|
|
943
|
-
}), sessionId, agentId);
|
|
944
|
-
return { success: true };
|
|
945
|
-
}),
|
|
946
|
-
getMessages: apiProcedure.input(z.object({
|
|
947
|
-
chatId: z.string().optional(),
|
|
948
|
-
limit: z.number().optional()
|
|
949
|
-
})).query(async ({ input, ctx }) => {
|
|
950
|
-
return getMessages(await resolveAndCheckChatId(ctx, input.chatId), input.limit);
|
|
951
|
-
}),
|
|
952
|
-
waitForMessages: apiProcedure.input(z.object({
|
|
953
|
-
chatId: z.string().optional(),
|
|
954
|
-
lastMessageId: z.string().optional()
|
|
955
|
-
})).subscription(async function* ({ input, ctx, signal }) {
|
|
956
|
-
const chatId = await resolveAndCheckChatId(ctx, input.chatId);
|
|
957
|
-
if (input.lastMessageId) {
|
|
958
|
-
const messages = await getMessages(chatId);
|
|
959
|
-
const lastIndex = messages.findIndex((m) => m.id === input.lastMessageId);
|
|
960
|
-
if (lastIndex !== -1 && lastIndex < messages.length - 1) yield messages.slice(lastIndex + 1);
|
|
961
|
-
}
|
|
962
|
-
const { on } = await import("node:events");
|
|
963
|
-
try {
|
|
964
|
-
for await (const [event] of on(daemonEvents, DAEMON_EVENT_MESSAGE_APPENDED, { signal })) if (event.chatId === chatId) yield [event.message];
|
|
965
|
-
} catch (err) {
|
|
966
|
-
if (err instanceof Error && err.name === "AbortError") return;
|
|
967
|
-
throw err;
|
|
968
|
-
}
|
|
969
|
-
}),
|
|
970
|
-
waitForTyping: apiProcedure.input(z.object({ chatId: z.string().optional() })).subscription(async function* ({ input, ctx, signal }) {
|
|
971
|
-
const chatId = await resolveAndCheckChatId(ctx, input.chatId);
|
|
972
|
-
const { on } = await import("node:events");
|
|
973
|
-
try {
|
|
974
|
-
for await (const [event] of on(daemonEvents, DAEMON_EVENT_TYPING, { signal })) if (event.chatId === chatId) yield event;
|
|
975
|
-
} catch (err) {
|
|
976
|
-
if (err instanceof Error && err.name === "AbortError") return;
|
|
977
|
-
throw err;
|
|
978
|
-
}
|
|
979
|
-
}),
|
|
980
|
-
ping: publicProcedure.query(() => {
|
|
981
|
-
return { status: "ok" };
|
|
982
|
-
}),
|
|
983
|
-
shutdown: publicProcedure.mutation(() => {
|
|
984
|
-
setTimeout(() => {
|
|
985
|
-
console.log("Shutting down daemon...");
|
|
986
|
-
process.kill(process.pid, "SIGTERM");
|
|
987
|
-
}, 100);
|
|
988
|
-
return { success: true };
|
|
989
|
-
}),
|
|
990
|
-
logMessage: apiProcedure.input(z.object({
|
|
1207
|
+
async function listCronJobsShared(chatId) {
|
|
1208
|
+
return (await readChatSettings(chatId))?.jobs ?? [];
|
|
1209
|
+
}
|
|
1210
|
+
async function addCronJobShared(chatId, job) {
|
|
1211
|
+
const settings = await readChatSettings(chatId) || {};
|
|
1212
|
+
const cronJobs = settings.jobs ?? [];
|
|
1213
|
+
const existingIndex = cronJobs.findIndex((j) => j.id === job.id);
|
|
1214
|
+
if (existingIndex >= 0) cronJobs[existingIndex] = job;
|
|
1215
|
+
else cronJobs.push(job);
|
|
1216
|
+
settings.jobs = cronJobs;
|
|
1217
|
+
await writeChatSettings(chatId, settings);
|
|
1218
|
+
cronManager.scheduleJob(chatId, job);
|
|
1219
|
+
return { success: true };
|
|
1220
|
+
}
|
|
1221
|
+
async function deleteCronJobShared(chatId, id) {
|
|
1222
|
+
const settings = await readChatSettings(chatId);
|
|
1223
|
+
if (!settings || !settings.jobs) return {
|
|
1224
|
+
success: true,
|
|
1225
|
+
deleted: false
|
|
1226
|
+
};
|
|
1227
|
+
const initialLength = settings.jobs.length;
|
|
1228
|
+
settings.jobs = settings.jobs.filter((j) => j.id !== id);
|
|
1229
|
+
if (settings.jobs.length !== initialLength) {
|
|
1230
|
+
await writeChatSettings(chatId, settings);
|
|
1231
|
+
cronManager.unscheduleJob(chatId, id);
|
|
1232
|
+
return {
|
|
1233
|
+
success: true,
|
|
1234
|
+
deleted: true
|
|
1235
|
+
};
|
|
1236
|
+
}
|
|
1237
|
+
return {
|
|
1238
|
+
success: true,
|
|
1239
|
+
deleted: false
|
|
1240
|
+
};
|
|
1241
|
+
}
|
|
1242
|
+
|
|
1243
|
+
//#endregion
|
|
1244
|
+
//#region src/daemon/api/user-router.ts
|
|
1245
|
+
const sendMessage = apiProcedure.input(z.object({
|
|
1246
|
+
type: z.literal("send-message"),
|
|
1247
|
+
client: z.literal("cli"),
|
|
1248
|
+
data: z.object({
|
|
1249
|
+
message: z.string(),
|
|
991
1250
|
chatId: z.string().optional(),
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1251
|
+
sessionId: z.string().optional(),
|
|
1252
|
+
agentId: z.string().optional(),
|
|
1253
|
+
noWait: z.boolean().optional(),
|
|
1254
|
+
files: z.array(z.string()).optional(),
|
|
1255
|
+
adapter: z.string().optional()
|
|
1256
|
+
})
|
|
1257
|
+
})).mutation(async ({ input }) => {
|
|
1258
|
+
let message = input.data.message;
|
|
1259
|
+
const chatId = input.data.chatId ?? await getDefaultChatId();
|
|
1260
|
+
const noWait = input.data.noWait ?? false;
|
|
1261
|
+
const sessionId = input.data.sessionId;
|
|
1262
|
+
const agentId = input.data.agentId;
|
|
1263
|
+
const settingsPath = getSettingsPath();
|
|
1264
|
+
let settings;
|
|
1265
|
+
try {
|
|
1266
|
+
const settingsStr = await fs$1.readFile(settingsPath, "utf8");
|
|
1267
|
+
settings = JSON.parse(settingsStr);
|
|
1268
|
+
} catch (err) {
|
|
1269
|
+
throw new Error(`Failed to read settings from ${settingsPath}: ${err}`, { cause: err });
|
|
1270
|
+
}
|
|
1271
|
+
const files = input.data.files;
|
|
1272
|
+
if (files && files.length > 0) {
|
|
1273
|
+
const workspaceRoot = getWorkspaceRoot(process.cwd());
|
|
1274
|
+
const chatSettings = await readChatSettings(chatId) ?? {};
|
|
1275
|
+
const agentDir = await resolveAgentDir(agentId ?? chatSettings.defaultAgent ?? "default", workspaceRoot);
|
|
1276
|
+
const absoluteFilesDir = await getAgentFilesDir(agentId, chatId, settings, workspaceRoot);
|
|
1277
|
+
const adapterNamespace = input.data.adapter || "cli";
|
|
1278
|
+
const targetDir = path.join(absoluteFilesDir, adapterNamespace);
|
|
1279
|
+
if (!pathIsInsideDir(targetDir, workspaceRoot, { allowSameDir: true })) throw new TRPCError({
|
|
1280
|
+
code: "BAD_REQUEST",
|
|
1281
|
+
message: "Target directory must be within the workspace."
|
|
1282
|
+
});
|
|
1283
|
+
await validateAttachments(files);
|
|
1284
|
+
await fs$1.mkdir(targetDir, { recursive: true });
|
|
1285
|
+
const finalPaths = [];
|
|
1286
|
+
for (const file of files) {
|
|
1287
|
+
const fileName = path.basename(file);
|
|
1288
|
+
const targetPath = await getUniquePath(path.join(targetDir, fileName));
|
|
1289
|
+
try {
|
|
1290
|
+
await fs$1.rename(file, targetPath);
|
|
1291
|
+
} catch {
|
|
1292
|
+
await fs$1.copyFile(file, targetPath);
|
|
1293
|
+
await fs$1.unlink(file);
|
|
1005
1294
|
}
|
|
1295
|
+
finalPaths.push(path.relative(agentDir, targetPath));
|
|
1006
1296
|
}
|
|
1007
|
-
const
|
|
1008
|
-
|
|
1297
|
+
const fileList = `Attached files:\n${finalPaths.map((p) => `- ${p}`).join("\n")}`;
|
|
1298
|
+
message = message ? `${message}\n\n${fileList}` : fileList;
|
|
1299
|
+
}
|
|
1300
|
+
await handleUserMessage(chatId, message, settings, void 0, noWait, (args) => runCommand({
|
|
1301
|
+
...args,
|
|
1302
|
+
logToTerminal: true
|
|
1303
|
+
}), sessionId, agentId);
|
|
1304
|
+
return { success: true };
|
|
1305
|
+
});
|
|
1306
|
+
const getMessages = apiProcedure.input(z.object({
|
|
1307
|
+
chatId: z.string().optional(),
|
|
1308
|
+
limit: z.number().optional()
|
|
1309
|
+
})).query(async ({ input }) => {
|
|
1310
|
+
return getMessages$1(input.chatId ?? await getDefaultChatId(), input.limit);
|
|
1311
|
+
});
|
|
1312
|
+
const waitForMessages = apiProcedure.input(z.object({
|
|
1313
|
+
chatId: z.string().optional(),
|
|
1314
|
+
lastMessageId: z.string().optional()
|
|
1315
|
+
})).subscription(async function* ({ input, signal }) {
|
|
1316
|
+
const chatId = input.chatId ?? await getDefaultChatId();
|
|
1317
|
+
if (input.lastMessageId) {
|
|
1318
|
+
const messages = await getMessages$1(chatId);
|
|
1319
|
+
const lastIndex = messages.findIndex((m) => m.id === input.lastMessageId);
|
|
1320
|
+
if (lastIndex !== -1 && lastIndex < messages.length - 1) yield messages.slice(lastIndex + 1);
|
|
1321
|
+
}
|
|
1322
|
+
try {
|
|
1323
|
+
for await (const [event] of on(daemonEvents, DAEMON_EVENT_MESSAGE_APPENDED, { signal })) if (event.chatId === chatId) yield [event.message];
|
|
1324
|
+
} catch (err) {
|
|
1325
|
+
if (err instanceof Error && err.name === "AbortError") return;
|
|
1326
|
+
throw err;
|
|
1327
|
+
}
|
|
1328
|
+
});
|
|
1329
|
+
const waitForTyping = apiProcedure.input(z.object({ chatId: z.string().optional() })).subscription(async function* ({ input, signal }) {
|
|
1330
|
+
const chatId = input.chatId ?? await getDefaultChatId();
|
|
1331
|
+
try {
|
|
1332
|
+
for await (const [event] of on(daemonEvents, DAEMON_EVENT_TYPING, { signal })) if (event.chatId === chatId) yield event;
|
|
1333
|
+
} catch (err) {
|
|
1334
|
+
if (err instanceof Error && err.name === "AbortError") return;
|
|
1335
|
+
throw err;
|
|
1336
|
+
}
|
|
1337
|
+
});
|
|
1338
|
+
const ping = publicProcedure.query(() => {
|
|
1339
|
+
return { status: "ok" };
|
|
1340
|
+
});
|
|
1341
|
+
const shutdown = publicProcedure.mutation(() => {
|
|
1342
|
+
setTimeout(() => {
|
|
1343
|
+
console.log("Shutting down daemon...");
|
|
1344
|
+
process.kill(process.pid, "SIGTERM");
|
|
1345
|
+
}, 100);
|
|
1346
|
+
return { success: true };
|
|
1347
|
+
});
|
|
1348
|
+
const userListCronJobs = apiProcedure.input(z.object({ chatId: z.string().optional() })).query(async ({ input }) => {
|
|
1349
|
+
return listCronJobsShared(input.chatId ?? await getDefaultChatId());
|
|
1350
|
+
});
|
|
1351
|
+
const userAddCronJob = apiProcedure.input(z.object({
|
|
1352
|
+
chatId: z.string().optional(),
|
|
1353
|
+
job: CronJobSchema
|
|
1354
|
+
})).mutation(async ({ input }) => {
|
|
1355
|
+
return addCronJobShared(input.chatId ?? await getDefaultChatId(), input.job);
|
|
1356
|
+
});
|
|
1357
|
+
const userDeleteCronJob = apiProcedure.input(z.object({
|
|
1358
|
+
chatId: z.string().optional(),
|
|
1359
|
+
id: z.string()
|
|
1360
|
+
})).mutation(async ({ input }) => {
|
|
1361
|
+
return deleteCronJobShared(input.chatId ?? await getDefaultChatId(), input.id);
|
|
1362
|
+
});
|
|
1363
|
+
const userRouter = router({
|
|
1364
|
+
sendMessage,
|
|
1365
|
+
getMessages,
|
|
1366
|
+
waitForMessages,
|
|
1367
|
+
waitForTyping,
|
|
1368
|
+
ping,
|
|
1369
|
+
shutdown,
|
|
1370
|
+
listCronJobs: userListCronJobs,
|
|
1371
|
+
addCronJob: userAddCronJob,
|
|
1372
|
+
deleteCronJob: userDeleteCronJob
|
|
1373
|
+
});
|
|
1374
|
+
|
|
1375
|
+
//#endregion
|
|
1376
|
+
//#region src/daemon/policy-request-service.ts
|
|
1377
|
+
var PolicyRequestService = class {
|
|
1378
|
+
store;
|
|
1379
|
+
maxPending;
|
|
1380
|
+
agentDir;
|
|
1381
|
+
snapshotDir;
|
|
1382
|
+
constructor(store, agentDir, snapshotDir, maxPending = 100) {
|
|
1383
|
+
this.store = store;
|
|
1384
|
+
this.agentDir = agentDir;
|
|
1385
|
+
this.snapshotDir = snapshotDir;
|
|
1386
|
+
this.maxPending = maxPending;
|
|
1387
|
+
}
|
|
1388
|
+
async createRequest(commandName, args, fileMappings, chatId, agentId) {
|
|
1389
|
+
const allRequests = await this.store.list();
|
|
1390
|
+
if (allRequests.filter((r) => r.state === "Pending").length >= this.maxPending) throw new Error(`Maximum number of pending requests (${this.maxPending}) reached.`);
|
|
1391
|
+
const snapshotMappings = {};
|
|
1392
|
+
for (const [key, requestedPath] of Object.entries(fileMappings)) snapshotMappings[key] = await createSnapshot(requestedPath, this.agentDir, this.snapshotDir);
|
|
1393
|
+
let id = "";
|
|
1394
|
+
do
|
|
1395
|
+
id = generateRandomAlphaNumericString(3);
|
|
1396
|
+
while (allRequests.some((r) => r.id === id));
|
|
1397
|
+
const request = {
|
|
1009
1398
|
id,
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
cwd: process.cwd(),
|
|
1018
|
-
exitCode: 0,
|
|
1019
|
-
...filePaths.length > 0 ? { files: filePaths } : {}
|
|
1399
|
+
commandName,
|
|
1400
|
+
args,
|
|
1401
|
+
fileMappings: snapshotMappings,
|
|
1402
|
+
state: "Pending",
|
|
1403
|
+
createdAt: Date.now(),
|
|
1404
|
+
chatId,
|
|
1405
|
+
agentId
|
|
1020
1406
|
};
|
|
1021
|
-
await
|
|
1022
|
-
return
|
|
1023
|
-
}
|
|
1024
|
-
|
|
1025
|
-
return (
|
|
1026
|
-
}
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
const
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
success: true,
|
|
1050
|
-
deleted: false
|
|
1051
|
-
};
|
|
1052
|
-
const initialLength = settings.jobs.length;
|
|
1053
|
-
settings.jobs = settings.jobs.filter((j) => j.id !== input.id);
|
|
1054
|
-
if (settings.jobs.length !== initialLength) {
|
|
1055
|
-
await writeChatSettings(chatId, settings);
|
|
1056
|
-
cronManager.unscheduleJob(chatId, input.id);
|
|
1057
|
-
return {
|
|
1058
|
-
success: true,
|
|
1059
|
-
deleted: true
|
|
1060
|
-
};
|
|
1407
|
+
await this.store.save(request);
|
|
1408
|
+
return request;
|
|
1409
|
+
}
|
|
1410
|
+
getInterpolatedArgs(request) {
|
|
1411
|
+
return interpolateArgs(request.args, request.fileMappings);
|
|
1412
|
+
}
|
|
1413
|
+
};
|
|
1414
|
+
|
|
1415
|
+
//#endregion
|
|
1416
|
+
//#region src/daemon/api/agent-router.ts
|
|
1417
|
+
const logMessage = apiProcedure.input(z.object({
|
|
1418
|
+
message: z.string().optional(),
|
|
1419
|
+
files: z.array(z.string()).optional()
|
|
1420
|
+
})).mutation(async ({ input, ctx }) => {
|
|
1421
|
+
if (!ctx.tokenPayload) throw new TRPCError({
|
|
1422
|
+
code: "UNAUTHORIZED",
|
|
1423
|
+
message: "Missing token"
|
|
1424
|
+
});
|
|
1425
|
+
const chatId = ctx.tokenPayload.chatId;
|
|
1426
|
+
const timestamp = (/* @__PURE__ */ new Date()).toISOString();
|
|
1427
|
+
const id = Date.now().toString() + Math.random().toString(36).substring(2, 7);
|
|
1428
|
+
const filePaths = [];
|
|
1429
|
+
if (input.files && input.files.length > 0) {
|
|
1430
|
+
const workspaceRoot = getWorkspaceRoot(process.cwd());
|
|
1431
|
+
const agentDir = await resolveAgentDir(ctx.tokenPayload?.agentId, workspaceRoot);
|
|
1432
|
+
for (const file of input.files) {
|
|
1433
|
+
const validPath = await validateLogFile(file, agentDir, workspaceRoot);
|
|
1434
|
+
filePaths.push(validPath);
|
|
1061
1435
|
}
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1436
|
+
}
|
|
1437
|
+
const filesArgStr = filePaths.map((p) => ` --file ${p}`).join("");
|
|
1438
|
+
await appendMessage(chatId, {
|
|
1439
|
+
id,
|
|
1440
|
+
messageId: id,
|
|
1441
|
+
role: "log",
|
|
1442
|
+
source: "router",
|
|
1443
|
+
content: input.message || "",
|
|
1444
|
+
stderr: "",
|
|
1445
|
+
timestamp,
|
|
1446
|
+
command: `clawmini-lite log${filesArgStr}`,
|
|
1447
|
+
cwd: process.cwd(),
|
|
1448
|
+
exitCode: 0,
|
|
1449
|
+
...filePaths.length > 0 ? { files: filePaths } : {}
|
|
1450
|
+
});
|
|
1451
|
+
return { success: true };
|
|
1452
|
+
});
|
|
1453
|
+
const agentListCronJobs = apiProcedure.query(async ({ ctx }) => {
|
|
1454
|
+
if (!ctx.tokenPayload) throw new TRPCError({
|
|
1455
|
+
code: "UNAUTHORIZED",
|
|
1456
|
+
message: "Missing token"
|
|
1457
|
+
});
|
|
1458
|
+
const chatId = ctx.tokenPayload.chatId;
|
|
1459
|
+
return listCronJobsShared(chatId);
|
|
1460
|
+
});
|
|
1461
|
+
const agentAddCronJob = apiProcedure.input(z.object({ job: CronJobSchema })).mutation(async ({ input, ctx }) => {
|
|
1462
|
+
if (!ctx.tokenPayload) throw new TRPCError({
|
|
1463
|
+
code: "UNAUTHORIZED",
|
|
1464
|
+
message: "Missing token"
|
|
1465
|
+
});
|
|
1466
|
+
const chatId = ctx.tokenPayload.chatId;
|
|
1467
|
+
return addCronJobShared(chatId, {
|
|
1468
|
+
...input.job,
|
|
1469
|
+
agentId: ctx.tokenPayload.agentId
|
|
1470
|
+
});
|
|
1471
|
+
});
|
|
1472
|
+
const agentDeleteCronJob = apiProcedure.input(z.object({ id: z.string() })).mutation(async ({ input, ctx }) => {
|
|
1473
|
+
if (!ctx.tokenPayload) throw new TRPCError({
|
|
1474
|
+
code: "UNAUTHORIZED",
|
|
1475
|
+
message: "Missing token"
|
|
1476
|
+
});
|
|
1477
|
+
const chatId = ctx.tokenPayload.chatId;
|
|
1478
|
+
return deleteCronJobShared(chatId, input.id);
|
|
1479
|
+
});
|
|
1480
|
+
const listPolicies = apiProcedure.query(async () => {
|
|
1481
|
+
return await readPolicies();
|
|
1482
|
+
});
|
|
1483
|
+
const executePolicyHelp = apiProcedure.input(z.object({ commandName: z.string() })).query(async ({ input }) => {
|
|
1484
|
+
const policy = (await readPolicies())?.policies?.[input.commandName];
|
|
1485
|
+
if (!policy) throw new TRPCError({
|
|
1486
|
+
code: "NOT_FOUND",
|
|
1487
|
+
message: `Policy not found: ${input.commandName}`
|
|
1488
|
+
});
|
|
1489
|
+
if (!policy.allowHelp) return {
|
|
1490
|
+
stdout: "",
|
|
1491
|
+
stderr: "This command does not support --help\n",
|
|
1492
|
+
exitCode: 1
|
|
1493
|
+
};
|
|
1494
|
+
const fullArgs = [...policy.args || [], "--help"];
|
|
1495
|
+
const { stdout, stderr, exitCode } = await executeSafe(policy.command, fullArgs, { cwd: getWorkspaceRoot() });
|
|
1496
|
+
return {
|
|
1497
|
+
stdout,
|
|
1498
|
+
stderr,
|
|
1499
|
+
exitCode
|
|
1500
|
+
};
|
|
1501
|
+
});
|
|
1502
|
+
const createPolicyRequest = apiProcedure.input(z.object({
|
|
1503
|
+
commandName: z.string(),
|
|
1504
|
+
args: z.array(z.string()),
|
|
1505
|
+
fileMappings: z.record(z.string(), z.string())
|
|
1506
|
+
})).mutation(async ({ input, ctx }) => {
|
|
1507
|
+
if (!ctx.tokenPayload) throw new TRPCError({
|
|
1508
|
+
code: "UNAUTHORIZED",
|
|
1509
|
+
message: "Missing token"
|
|
1510
|
+
});
|
|
1511
|
+
const workspaceRoot = getWorkspaceRoot(process.cwd());
|
|
1512
|
+
const snapshotDir = path.join(getClawminiDir(process.cwd()), "tmp", "snapshots");
|
|
1513
|
+
const service = new PolicyRequestService(new RequestStore(process.cwd()), await resolveAgentDir(ctx.tokenPayload?.agentId, workspaceRoot), snapshotDir);
|
|
1514
|
+
const chatId = ctx.tokenPayload.chatId;
|
|
1515
|
+
const agentId = ctx.tokenPayload.agentId;
|
|
1516
|
+
const request = await service.createRequest(input.commandName, input.args, input.fileMappings, chatId, agentId);
|
|
1517
|
+
const previewContent = await generateRequestPreview(request);
|
|
1518
|
+
await appendMessage(chatId, {
|
|
1519
|
+
id: randomUUID(),
|
|
1520
|
+
messageId: randomUUID(),
|
|
1521
|
+
role: "log",
|
|
1522
|
+
source: "router",
|
|
1523
|
+
content: previewContent,
|
|
1524
|
+
stderr: "",
|
|
1525
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1526
|
+
command: "policy-request",
|
|
1527
|
+
cwd: process.cwd(),
|
|
1528
|
+
exitCode: 0
|
|
1529
|
+
});
|
|
1530
|
+
return request;
|
|
1531
|
+
});
|
|
1532
|
+
const fetchPendingMessages = apiProcedure.mutation(async ({ ctx }) => {
|
|
1533
|
+
const queue = getMessageQueue(process.cwd());
|
|
1534
|
+
const targetSessionId = ctx.tokenPayload?.sessionId || "default";
|
|
1535
|
+
const extracted = queue.extractPending((p) => p.sessionId === targetSessionId);
|
|
1536
|
+
if (extracted.length === 0) return { messages: "" };
|
|
1537
|
+
return { messages: formatPendingMessages(extracted.map((p) => p.text)) };
|
|
1538
|
+
});
|
|
1539
|
+
const agentRouter = router({
|
|
1540
|
+
logMessage,
|
|
1541
|
+
listCronJobs: agentListCronJobs,
|
|
1542
|
+
addCronJob: agentAddCronJob,
|
|
1543
|
+
deleteCronJob: agentDeleteCronJob,
|
|
1544
|
+
listPolicies,
|
|
1545
|
+
executePolicyHelp,
|
|
1546
|
+
createPolicyRequest,
|
|
1547
|
+
fetchPendingMessages,
|
|
1548
|
+
ping
|
|
1067
1549
|
});
|
|
1068
|
-
const appRouter = AppRouter;
|
|
1069
1550
|
|
|
1070
1551
|
//#endregion
|
|
1071
1552
|
//#region src/daemon/index.ts
|
|
@@ -1139,7 +1620,7 @@ async function initDaemon() {
|
|
|
1139
1620
|
readyPromiseResolve = resolve;
|
|
1140
1621
|
});
|
|
1141
1622
|
const handler = createHTTPHandler({
|
|
1142
|
-
router:
|
|
1623
|
+
router: userRouter,
|
|
1143
1624
|
createContext: ({ req, res }) => ({
|
|
1144
1625
|
req,
|
|
1145
1626
|
res,
|
|
@@ -1172,7 +1653,7 @@ async function initDaemon() {
|
|
|
1172
1653
|
let apiServer;
|
|
1173
1654
|
if (apiCtx) {
|
|
1174
1655
|
const apiHandler = createHTTPHandler({
|
|
1175
|
-
router:
|
|
1656
|
+
router: agentRouter,
|
|
1176
1657
|
createContext: ({ req, res }) => {
|
|
1177
1658
|
let tokenPayload = null;
|
|
1178
1659
|
const authHeader = req.headers.authorization;
|