clawmini 0.0.1 → 0.0.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (103) hide show
  1. package/.github/workflows/ci.yml +59 -0
  2. package/README.md +61 -76
  3. package/dist/adapter-discord/index.d.mts.map +1 -1
  4. package/dist/adapter-discord/index.mjs +13 -4
  5. package/dist/adapter-discord/index.mjs.map +1 -1
  6. package/dist/cli/index.mjs +8 -6
  7. package/dist/cli/index.mjs.map +1 -1
  8. package/dist/cli/lite.mjs +64 -10
  9. package/dist/cli/lite.mjs.map +1 -1
  10. package/dist/daemon/index.mjs +732 -251
  11. package/dist/daemon/index.mjs.map +1 -1
  12. package/dist/{fetch-BjZVyU3Z.mjs → fetch-Cn1XNyiO.mjs} +1 -1
  13. package/dist/{fetch-BjZVyU3Z.mjs.map → fetch-Cn1XNyiO.mjs.map} +1 -1
  14. package/dist/lite-oSYSvaOr.mjs +164 -0
  15. package/dist/lite-oSYSvaOr.mjs.map +1 -0
  16. package/dist/web/_app/immutable/chunks/{COekwvP2.js → 8YNcRyEk.js} +1 -1
  17. package/dist/web/_app/immutable/chunks/{CSvS_NwK.js → DQoygso7.js} +1 -1
  18. package/dist/web/_app/immutable/entry/{app.B-vZe7PN.js → app.DO5eYwVz.js} +2 -2
  19. package/dist/web/_app/immutable/entry/start.D48mVn1m.js +1 -0
  20. package/dist/web/_app/immutable/nodes/{0.B5WFN0zw.js → 0.B-0CcADM.js} +1 -1
  21. package/dist/web/_app/immutable/nodes/{1.D1wtJb2k.js → 1.FixKgvRO.js} +1 -1
  22. package/dist/web/_app/immutable/nodes/{3.BB5wCoBf.js → 3.ncP0xLO6.js} +1 -1
  23. package/dist/web/_app/immutable/nodes/{4.Dr2jvAXK.js → 4.CQYJEgv8.js} +1 -1
  24. package/dist/web/_app/immutable/nodes/{5.BJl7oM3b.js → 5.BpJUN6QH.js} +1 -1
  25. package/dist/web/_app/version.json +1 -1
  26. package/dist/web/index.html +6 -6
  27. package/dist/{workspace-CSgfo_2J.mjs → workspace-DjoNjhW0.mjs} +21 -40
  28. package/dist/workspace-DjoNjhW0.mjs.map +1 -0
  29. package/docs/15_lite_fetch_pending/development_log.md +31 -0
  30. package/docs/15_lite_fetch_pending/notes.md +48 -0
  31. package/docs/15_lite_fetch_pending/prd.md +39 -0
  32. package/docs/15_lite_fetch_pending/questions.md +3 -0
  33. package/docs/15_lite_fetch_pending/tickets.md +42 -0
  34. package/docs/CHECKS.md +2 -2
  35. package/docs/CLI_REFERENCE.md +35 -0
  36. package/docs/guides/sandbox_policies.md +12 -5
  37. package/eslint.config.js +12 -0
  38. package/package.json +3 -2
  39. package/src/adapter-discord/client.ts +1 -1
  40. package/src/adapter-discord/index.ts +22 -5
  41. package/src/cli/client.ts +8 -3
  42. package/src/cli/e2e/adapter-discord.test.ts +2 -2
  43. package/src/cli/e2e/daemon.test.ts +2 -1
  44. package/src/cli/e2e/export-lite-func.test.ts +41 -13
  45. package/src/cli/e2e/fallbacks.test.ts +4 -0
  46. package/src/cli/lite.ts +24 -6
  47. package/src/daemon/api/agent-router.ts +191 -0
  48. package/src/daemon/{router.test.ts → api/index.test.ts} +101 -34
  49. package/src/daemon/api/index.ts +4 -0
  50. package/src/daemon/{router-policy-request.test.ts → api/policy-request.test.ts} +27 -13
  51. package/src/daemon/api/router-utils.ts +159 -0
  52. package/src/daemon/api/trpc.ts +30 -0
  53. package/src/daemon/api/user-router.ts +221 -0
  54. package/src/daemon/index.ts +3 -3
  55. package/src/daemon/message-interruption.test.ts +17 -10
  56. package/src/daemon/message-typing.test.ts +1 -1
  57. package/src/daemon/message.ts +260 -239
  58. package/src/daemon/observation.test.ts +1 -1
  59. package/src/daemon/queue.test.ts +28 -0
  60. package/src/daemon/queue.ts +30 -15
  61. package/src/daemon/request-store.test.ts +4 -4
  62. package/src/daemon/request-store.ts +3 -1
  63. package/src/shared/workspace.ts +4 -5
  64. package/templates/debug/settings.json +5 -0
  65. package/templates/environments/macos/env.json +1 -1
  66. package/templates/environments/macos-proxy/env.json +1 -1
  67. package/templates/gemini-claw/.gemini/hooks/insert-pending.sh +9 -0
  68. package/templates/gemini-claw/.gemini/settings.json +14 -1
  69. package/templates/gemini-claw/.gemini/system.md +2 -0
  70. package/web/.svelte-kit/ambient.d.ts +2 -6
  71. package/web/.svelte-kit/generated/server/internal.js +1 -1
  72. package/web/.svelte-kit/output/client/.vite/manifest.json +29 -29
  73. package/web/.svelte-kit/output/client/_app/immutable/chunks/{COekwvP2.js → 8YNcRyEk.js} +1 -1
  74. package/web/.svelte-kit/output/client/_app/immutable/chunks/{CSvS_NwK.js → DQoygso7.js} +1 -1
  75. package/web/.svelte-kit/output/client/_app/immutable/entry/{app.B-vZe7PN.js → app.DO5eYwVz.js} +2 -2
  76. package/web/.svelte-kit/output/client/_app/immutable/entry/start.D48mVn1m.js +1 -0
  77. package/web/.svelte-kit/output/client/_app/immutable/nodes/{0.B5WFN0zw.js → 0.B-0CcADM.js} +1 -1
  78. package/web/.svelte-kit/output/client/_app/immutable/nodes/{1.D1wtJb2k.js → 1.FixKgvRO.js} +1 -1
  79. package/web/.svelte-kit/output/client/_app/immutable/nodes/{3.BB5wCoBf.js → 3.ncP0xLO6.js} +1 -1
  80. package/web/.svelte-kit/output/client/_app/immutable/nodes/{4.Dr2jvAXK.js → 4.CQYJEgv8.js} +1 -1
  81. package/web/.svelte-kit/output/client/_app/immutable/nodes/{5.BJl7oM3b.js → 5.BpJUN6QH.js} +1 -1
  82. package/web/.svelte-kit/output/client/_app/version.json +1 -1
  83. package/web/.svelte-kit/output/server/chunks/internal.js +1 -1
  84. package/web/.svelte-kit/output/server/manifest-full.js +1 -1
  85. package/web/.svelte-kit/output/server/manifest.js +1 -1
  86. package/web/.svelte-kit/output/server/nodes/0.js +1 -1
  87. package/web/.svelte-kit/output/server/nodes/1.js +1 -1
  88. package/web/.svelte-kit/output/server/nodes/3.js +1 -1
  89. package/web/.svelte-kit/output/server/nodes/4.js +1 -1
  90. package/web/.svelte-kit/output/server/nodes/5.js +1 -1
  91. package/dist/chats-DKgTeU7i.mjs +0 -91
  92. package/dist/chats-DKgTeU7i.mjs.map +0 -1
  93. package/dist/chats-Zd_HXDHx.mjs +0 -29
  94. package/dist/chats-Zd_HXDHx.mjs.map +0 -1
  95. package/dist/fs-B5wW0oaH.mjs +0 -14
  96. package/dist/fs-B5wW0oaH.mjs.map +0 -1
  97. package/dist/lite-Dl7WXyaH.mjs +0 -80
  98. package/dist/lite-Dl7WXyaH.mjs.map +0 -1
  99. package/dist/rolldown-runtime-95iHPtFO.mjs +0 -18
  100. package/dist/web/_app/immutable/entry/start.oP1AgKhs.js +0 -1
  101. package/dist/workspace-CSgfo_2J.mjs.map +0 -1
  102. package/src/daemon/router.ts +0 -510
  103. package/web/.svelte-kit/output/client/_app/immutable/entry/start.oP1AgKhs.js +0 -1
@@ -1,9 +1,6 @@
1
- import { C as SettingsSchema, S as CronJobSchema, a as getAgent, b as writeChatSettings, c as getSettingsPath, g as readSettings, 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 } from "../workspace-CSgfo_2J.mjs";
2
- import { n as pathIsInsideDir } from "../fs-B5wW0oaH.mjs";
3
- import { l as listChats, o as getDefaultChatId, s as getMessages } from "../chats-DKgTeU7i.mjs";
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
- const error = /* @__PURE__ */ new Error("Task aborted");
56
- error.name = "AbortError";
57
- this.currentController.abort(error);
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 directoryQueues = /* @__PURE__ */ new Map();
79
- function getQueue(dir) {
80
- if (!directoryQueues.has(dir)) directoryQueues.set(dir, new Queue());
81
- return directoryQueues.get(dir);
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) if (router === "@clawmini/slash-new") state = slashNew(state);
162
- else if (router === "@clawmini/slash-command") state = await slashCommand(state);
163
- else if (router === "@clawmini/slash-stop") state = slashStop(state);
164
- else if (router === "@clawmini/slash-interrupt") state = slashInterrupt(state);
165
- else try {
166
- state = await executeCustomRouter(router, state);
167
- } catch (err) {
168
- console.error(`Router error [${router}]:`, err);
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 = true, userMessageContent) {
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 = getQueue(cwd);
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
- queue.abortCurrent();
408
- const extracted = queue.extractPending();
409
- const payloads = currentPayload ? [currentPayload, ...extracted] : extracted;
410
- if (payloads.length > 0) state.message = `${payloads.map((text) => `<message>\n${text}\n</message>`).join("\n\n")}\n\n<message>\n${state.message}\n</message>`.trim();
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
- }, state.message);
574
- if (!noWait) await taskPromise;
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: overrideSessionId ?? chatSettings.sessions?.[agentId] ?? "default",
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
- const AppRouter = router({
884
- sendMessage: apiProcedure.input(z.object({
885
- type: z.literal("send-message"),
886
- client: z.literal("cli"),
887
- data: z.object({
888
- message: z.string(),
889
- chatId: z.string().optional(),
890
- sessionId: z.string().optional(),
891
- agentId: z.string().optional(),
892
- noWait: z.boolean().optional(),
893
- files: z.array(z.string()).optional(),
894
- adapter: z.string().optional()
895
- })
896
- })).mutation(async ({ input, ctx }) => {
897
- let message = input.data.message;
898
- const chatId = await resolveAndCheckChatId(ctx, input.data.chatId);
899
- const noWait = input.data.noWait ?? false;
900
- const sessionId = input.data.sessionId;
901
- const agentId = input.data.agentId;
902
- const settingsPath = getSettingsPath();
903
- let settings;
904
- try {
905
- const settingsStr = await fs$1.readFile(settingsPath, "utf8");
906
- settings = JSON.parse(settingsStr);
907
- } catch (err) {
908
- throw new Error(`Failed to read settings from ${settingsPath}: ${err}`, { cause: err });
909
- }
910
- const files = input.data.files;
911
- if (files && files.length > 0) {
912
- const workspaceRoot = getWorkspaceRoot(process.cwd());
913
- const chatSettings = await readChatSettings(chatId) ?? {};
914
- const agentDir = await resolveAgentDir(agentId ?? chatSettings.defaultAgent ?? "default", workspaceRoot);
915
- const absoluteFilesDir = await getAgentFilesDir(agentId, chatId, settings, workspaceRoot);
916
- const adapterNamespace = input.data.adapter || "cli";
917
- const targetDir = path.join(absoluteFilesDir, adapterNamespace);
918
- const { pathIsInsideDir } = await import("../fs-B5wW0oaH.mjs").then((n) => n.t);
919
- if (!pathIsInsideDir(targetDir, workspaceRoot, { allowSameDir: true })) throw new TRPCError({
920
- code: "BAD_REQUEST",
921
- message: "Target directory must be within the workspace."
922
- });
923
- await validateAttachments(files);
924
- await fs$1.mkdir(targetDir, { recursive: true });
925
- const finalPaths = [];
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
- message: z.string().optional(),
993
- files: z.array(z.string()).optional()
994
- })).mutation(async ({ input, ctx }) => {
995
- const chatId = await resolveAndCheckChatId(ctx, input.chatId);
996
- const timestamp = (/* @__PURE__ */ new Date()).toISOString();
997
- const id = Date.now().toString() + Math.random().toString(36).substring(2, 7);
998
- const filePaths = [];
999
- if (input.files && input.files.length > 0) {
1000
- const workspaceRoot = getWorkspaceRoot(process.cwd());
1001
- const agentDir = await resolveAgentDir(ctx.tokenPayload?.agentId, workspaceRoot);
1002
- for (const file of input.files) {
1003
- const validPath = await validateLogFile(file, agentDir, workspaceRoot);
1004
- filePaths.push(validPath);
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 filesArgStr = filePaths.map((p) => ` --file ${p}`).join("");
1008
- const logMsg = {
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
- messageId: id,
1011
- role: "log",
1012
- source: "router",
1013
- content: input.message || "",
1014
- stderr: "",
1015
- timestamp,
1016
- command: `clawmini-lite log${filesArgStr}`,
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 import("../chats-Zd_HXDHx.mjs").then((n) => n.n).then((m) => m.appendMessage(chatId, logMsg));
1022
- return { success: true };
1023
- }),
1024
- listCronJobs: apiProcedure.input(z.object({ chatId: z.string().optional() })).query(async ({ input, ctx }) => {
1025
- return (await readChatSettings(await resolveAndCheckChatId(ctx, input.chatId)))?.jobs ?? [];
1026
- }),
1027
- addCronJob: apiProcedure.input(z.object({
1028
- chatId: z.string().optional(),
1029
- job: CronJobSchema
1030
- })).mutation(async ({ input, ctx }) => {
1031
- const chatId = await resolveAndCheckChatId(ctx, input.chatId);
1032
- const settings = await readChatSettings(chatId) || {};
1033
- const cronJobs = settings.jobs ?? [];
1034
- const existingIndex = cronJobs.findIndex((j) => j.id === input.job.id);
1035
- if (existingIndex >= 0) cronJobs[existingIndex] = input.job;
1036
- else cronJobs.push(input.job);
1037
- settings.jobs = cronJobs;
1038
- await writeChatSettings(chatId, settings);
1039
- cronManager.scheduleJob(chatId, input.job);
1040
- return { success: true };
1041
- }),
1042
- deleteCronJob: apiProcedure.input(z.object({
1043
- chatId: z.string().optional(),
1044
- id: z.string()
1045
- })).mutation(async ({ input, ctx }) => {
1046
- const chatId = await resolveAndCheckChatId(ctx, input.chatId);
1047
- const settings = await readChatSettings(chatId);
1048
- if (!settings || !settings.jobs) return {
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
- return {
1063
- success: true,
1064
- deleted: false
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: appRouter,
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: appRouter,
1656
+ router: agentRouter,
1176
1657
  createContext: ({ req, res }) => {
1177
1658
  let tokenPayload = null;
1178
1659
  const authHeader = req.headers.authorization;