palmier 0.9.6 → 0.9.8

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 (255) hide show
  1. package/README.md +28 -13
  2. package/dist/agents/agent.d.ts +0 -1
  3. package/dist/agents/agent.js +0 -1
  4. package/dist/agents/aider.d.ts +0 -1
  5. package/dist/agents/aider.js +0 -1
  6. package/dist/agents/claude.d.ts +0 -1
  7. package/dist/agents/claude.js +0 -1
  8. package/dist/agents/cline.d.ts +0 -1
  9. package/dist/agents/cline.js +0 -1
  10. package/dist/agents/codex.d.ts +0 -1
  11. package/dist/agents/codex.js +0 -1
  12. package/dist/agents/copilot.d.ts +0 -1
  13. package/dist/agents/copilot.js +0 -1
  14. package/dist/agents/cursor.d.ts +0 -1
  15. package/dist/agents/cursor.js +0 -1
  16. package/dist/agents/deepagents.d.ts +0 -1
  17. package/dist/agents/deepagents.js +0 -1
  18. package/dist/agents/droid.d.ts +0 -1
  19. package/dist/agents/droid.js +0 -1
  20. package/dist/agents/gemini.d.ts +0 -1
  21. package/dist/agents/gemini.js +0 -1
  22. package/dist/agents/goose.d.ts +0 -1
  23. package/dist/agents/goose.js +0 -1
  24. package/dist/agents/hermes.d.ts +0 -1
  25. package/dist/agents/hermes.js +0 -1
  26. package/dist/agents/kimi.d.ts +0 -1
  27. package/dist/agents/kimi.js +0 -1
  28. package/dist/agents/kiro.d.ts +0 -1
  29. package/dist/agents/kiro.js +0 -1
  30. package/dist/agents/openclaw.d.ts +0 -1
  31. package/dist/agents/openclaw.js +0 -1
  32. package/dist/agents/opencode.d.ts +0 -1
  33. package/dist/agents/opencode.js +0 -1
  34. package/dist/agents/qoder.d.ts +0 -1
  35. package/dist/agents/qoder.js +0 -1
  36. package/dist/agents/qwen.d.ts +0 -1
  37. package/dist/agents/qwen.js +0 -1
  38. package/dist/agents/shared-prompt.d.ts +0 -1
  39. package/dist/agents/shared-prompt.js +0 -1
  40. package/dist/client-store.d.ts +0 -1
  41. package/dist/client-store.js +0 -1
  42. package/dist/commands/clients.d.ts +0 -1
  43. package/dist/commands/clients.js +0 -1
  44. package/dist/commands/info.d.ts +0 -1
  45. package/dist/commands/info.js +0 -1
  46. package/dist/commands/init.d.ts +0 -1
  47. package/dist/commands/init.js +1 -2
  48. package/dist/commands/pair.d.ts +0 -1
  49. package/dist/commands/pair.js +0 -1
  50. package/dist/commands/restart.d.ts +0 -1
  51. package/dist/commands/restart.js +0 -1
  52. package/dist/commands/run.d.ts +0 -1
  53. package/dist/commands/run.js +19 -3
  54. package/dist/commands/serve.d.ts +0 -1
  55. package/dist/commands/serve.js +0 -1
  56. package/dist/commands/uninstall.d.ts +0 -1
  57. package/dist/commands/uninstall.js +0 -1
  58. package/dist/config.d.ts +0 -1
  59. package/dist/config.js +0 -1
  60. package/dist/event-queues.d.ts +0 -1
  61. package/dist/event-queues.js +0 -1
  62. package/dist/events.d.ts +0 -1
  63. package/dist/events.js +0 -1
  64. package/dist/index.d.ts +0 -1
  65. package/dist/index.js +0 -1
  66. package/dist/linked-device.d.ts +0 -1
  67. package/dist/linked-device.js +0 -1
  68. package/dist/mcp-handler.d.ts +0 -1
  69. package/dist/mcp-handler.js +0 -1
  70. package/dist/mcp-tools.d.ts +0 -1
  71. package/dist/mcp-tools.js +0 -1
  72. package/dist/nats-client.d.ts +0 -1
  73. package/dist/nats-client.js +0 -1
  74. package/dist/network.d.ts +0 -1
  75. package/dist/network.js +0 -1
  76. package/dist/notification-store.d.ts +0 -1
  77. package/dist/notification-store.js +0 -1
  78. package/dist/pending-requests.d.ts +0 -1
  79. package/dist/pending-requests.js +0 -1
  80. package/dist/platform/index.d.ts +0 -1
  81. package/dist/platform/index.js +0 -1
  82. package/dist/platform/linux.d.ts +0 -1
  83. package/dist/platform/linux.js +0 -1
  84. package/dist/platform/macos.d.ts +0 -1
  85. package/dist/platform/macos.js +0 -1
  86. package/dist/platform/platform.d.ts +0 -1
  87. package/dist/platform/platform.js +0 -1
  88. package/dist/platform/windows.d.ts +0 -1
  89. package/dist/platform/windows.js +0 -1
  90. package/dist/pwa/assets/{index-MLEFUP3r.js → index-DWvRAUiy.js} +31 -31
  91. package/dist/pwa/assets/{web-B1sKCc7e.js → web-C4iZbqTC.js} +1 -1
  92. package/dist/pwa/assets/{web-ETD-8ZHd.js → web-CBFqJGX6.js} +1 -1
  93. package/dist/pwa/assets/{web-B4xEa6WO.js → web-DL4uXOpS.js} +1 -1
  94. package/dist/pwa/index.html +2 -2
  95. package/dist/rpc-handler.d.ts +0 -1
  96. package/dist/rpc-handler.js +0 -1
  97. package/dist/sms-store.d.ts +0 -1
  98. package/dist/sms-store.js +0 -1
  99. package/dist/spawn-command.d.ts +0 -1
  100. package/dist/spawn-command.js +0 -1
  101. package/dist/task.d.ts +0 -1
  102. package/dist/task.js +0 -1
  103. package/dist/transports/http-transport.d.ts +0 -1
  104. package/dist/transports/http-transport.js +0 -1
  105. package/dist/transports/nats-transport.d.ts +0 -1
  106. package/dist/transports/nats-transport.js +0 -1
  107. package/dist/types.d.ts +0 -1
  108. package/dist/types.js +0 -1
  109. package/dist/update-checker.d.ts +0 -1
  110. package/dist/update-checker.js +0 -1
  111. package/package.json +11 -1
  112. package/.github/workflows/ci.yml +0 -16
  113. package/.github/workflows/publish.yml +0 -37
  114. package/CLAUDE.md +0 -22
  115. package/dist/pwa/apple-touch-icon.png +0 -0
  116. package/dist/pwa/manifest.webmanifest +0 -1
  117. package/dist/pwa/pwa-192x192.png +0 -0
  118. package/dist/pwa/pwa-512x512.png +0 -0
  119. package/dist/pwa/registerSW.js +0 -1
  120. package/dist/pwa/service-worker.js +0 -2
  121. package/palmier-server/.github/workflows/ci.yml +0 -21
  122. package/palmier-server/.github/workflows/deploy.yml +0 -38
  123. package/palmier-server/CLAUDE.md +0 -17
  124. package/palmier-server/PRODUCTION.md +0 -358
  125. package/palmier-server/README.md +0 -231
  126. package/palmier-server/nats.conf +0 -19
  127. package/palmier-server/package.json +0 -15
  128. package/palmier-server/pnpm-lock.yaml +0 -7639
  129. package/palmier-server/pnpm-workspace.yaml +0 -3
  130. package/palmier-server/pwa/index.html +0 -16
  131. package/palmier-server/pwa/logo/logo_20260421.png +0 -0
  132. package/palmier-server/pwa/package.json +0 -34
  133. package/palmier-server/pwa/public/apple-touch-icon.png +0 -0
  134. package/palmier-server/pwa/public/favicon.ico +0 -0
  135. package/palmier-server/pwa/public/pwa-192x192.png +0 -0
  136. package/palmier-server/pwa/public/pwa-512x512.png +0 -0
  137. package/palmier-server/pwa/src/App.css +0 -3012
  138. package/palmier-server/pwa/src/App.tsx +0 -59
  139. package/palmier-server/pwa/src/agentLabels.ts +0 -11
  140. package/palmier-server/pwa/src/api.ts +0 -67
  141. package/palmier-server/pwa/src/components/CapabilityToggles.tsx +0 -170
  142. package/palmier-server/pwa/src/components/ConnectionStatusIcon.tsx +0 -113
  143. package/palmier-server/pwa/src/components/HostMenu.tsx +0 -429
  144. package/palmier-server/pwa/src/components/PermissionsDialog.tsx +0 -34
  145. package/palmier-server/pwa/src/components/PullToRefreshIndicator.tsx +0 -46
  146. package/palmier-server/pwa/src/components/RunDetailView.tsx +0 -343
  147. package/palmier-server/pwa/src/components/SessionComposer.tsx +0 -157
  148. package/palmier-server/pwa/src/components/SessionsView.tsx +0 -326
  149. package/palmier-server/pwa/src/components/SwipeToDeleteRow.tsx +0 -170
  150. package/palmier-server/pwa/src/components/TabBar.tsx +0 -40
  151. package/palmier-server/pwa/src/components/TaskCard.tsx +0 -255
  152. package/palmier-server/pwa/src/components/TaskForm.tsx +0 -766
  153. package/palmier-server/pwa/src/components/TasksView.tsx +0 -179
  154. package/palmier-server/pwa/src/constants.ts +0 -2
  155. package/palmier-server/pwa/src/contexts/HostConnectionContext.tsx +0 -432
  156. package/palmier-server/pwa/src/contexts/HostStoreContext.tsx +0 -124
  157. package/palmier-server/pwa/src/draftGuard.ts +0 -24
  158. package/palmier-server/pwa/src/formatTime.ts +0 -44
  159. package/palmier-server/pwa/src/hooks/useBackClose.ts +0 -75
  160. package/palmier-server/pwa/src/hooks/useMediaQuery.ts +0 -17
  161. package/palmier-server/pwa/src/hooks/usePullToRefresh.ts +0 -102
  162. package/palmier-server/pwa/src/hooks/usePushSubscription.ts +0 -77
  163. package/palmier-server/pwa/src/main.tsx +0 -14
  164. package/palmier-server/pwa/src/native/Device.ts +0 -49
  165. package/palmier-server/pwa/src/pages/Dashboard.tsx +0 -542
  166. package/palmier-server/pwa/src/pages/PairHost.tsx +0 -232
  167. package/palmier-server/pwa/src/pages/PairSetup.tsx +0 -134
  168. package/palmier-server/pwa/src/service-worker.ts +0 -142
  169. package/palmier-server/pwa/src/types.ts +0 -75
  170. package/palmier-server/pwa/src/vite-env.d.ts +0 -11
  171. package/palmier-server/pwa/tsconfig.json +0 -21
  172. package/palmier-server/pwa/tsconfig.node.json +0 -19
  173. package/palmier-server/pwa/vite.config.ts +0 -47
  174. package/palmier-server/server/.env.example +0 -20
  175. package/palmier-server/server/package.json +0 -36
  176. package/palmier-server/server/src/db.ts +0 -44
  177. package/palmier-server/server/src/fcm.ts +0 -74
  178. package/palmier-server/server/src/index.ts +0 -688
  179. package/palmier-server/server/src/nats-jwt.ts +0 -299
  180. package/palmier-server/server/src/nats-setup.ts +0 -48
  181. package/palmier-server/server/src/nats.ts +0 -33
  182. package/palmier-server/server/src/notify.ts +0 -34
  183. package/palmier-server/server/src/push.ts +0 -68
  184. package/palmier-server/server/src/routes/device.ts +0 -224
  185. package/palmier-server/server/src/routes/fcm.ts +0 -64
  186. package/palmier-server/server/src/routes/hosts.ts +0 -56
  187. package/palmier-server/server/src/routes/push.ts +0 -101
  188. package/palmier-server/server/tsconfig.json +0 -20
  189. package/palmier-server/spec.md +0 -533
  190. package/src/agents/agent-instructions.md +0 -28
  191. package/src/agents/agent.ts +0 -114
  192. package/src/agents/aider.ts +0 -35
  193. package/src/agents/claude.ts +0 -39
  194. package/src/agents/cline.ts +0 -35
  195. package/src/agents/codex.ts +0 -40
  196. package/src/agents/copilot.ts +0 -37
  197. package/src/agents/cursor.ts +0 -36
  198. package/src/agents/deepagents.ts +0 -36
  199. package/src/agents/droid.ts +0 -35
  200. package/src/agents/gemini.ts +0 -43
  201. package/src/agents/goose.ts +0 -33
  202. package/src/agents/hermes.ts +0 -36
  203. package/src/agents/kimi.ts +0 -35
  204. package/src/agents/kiro.ts +0 -36
  205. package/src/agents/openclaw.ts +0 -29
  206. package/src/agents/opencode.ts +0 -36
  207. package/src/agents/qoder.ts +0 -36
  208. package/src/agents/qwen.ts +0 -32
  209. package/src/agents/shared-prompt.ts +0 -30
  210. package/src/client-store.ts +0 -68
  211. package/src/commands/clients.ts +0 -29
  212. package/src/commands/info.ts +0 -29
  213. package/src/commands/init.ts +0 -165
  214. package/src/commands/pair.ts +0 -137
  215. package/src/commands/restart.ts +0 -6
  216. package/src/commands/run.ts +0 -608
  217. package/src/commands/serve.ts +0 -211
  218. package/src/commands/uninstall.ts +0 -9
  219. package/src/config.ts +0 -36
  220. package/src/cross-spawn.d.ts +0 -5
  221. package/src/event-queues.ts +0 -41
  222. package/src/events.ts +0 -29
  223. package/src/index.ts +0 -111
  224. package/src/linked-device.ts +0 -52
  225. package/src/mcp-handler.ts +0 -200
  226. package/src/mcp-tools.ts +0 -839
  227. package/src/nats-client.ts +0 -19
  228. package/src/network.ts +0 -96
  229. package/src/notification-store.ts +0 -30
  230. package/src/pending-requests.ts +0 -73
  231. package/src/platform/index.ts +0 -20
  232. package/src/platform/linux.ts +0 -296
  233. package/src/platform/macos.ts +0 -329
  234. package/src/platform/platform.ts +0 -31
  235. package/src/platform/windows.ts +0 -299
  236. package/src/rpc-handler.ts +0 -691
  237. package/src/sms-store.ts +0 -28
  238. package/src/spawn-command.ts +0 -123
  239. package/src/task.ts +0 -343
  240. package/src/transports/http-transport.ts +0 -478
  241. package/src/transports/nats-transport.ts +0 -76
  242. package/src/types.ts +0 -89
  243. package/src/update-checker.ts +0 -40
  244. package/test/agent-instructions.test.ts +0 -209
  245. package/test/agent-output-parsing.test.ts +0 -74
  246. package/test/linux-cron.test.ts +0 -41
  247. package/test/macos-plist.test.ts +0 -112
  248. package/test/notification-store.test.ts +0 -57
  249. package/test/pairing.test.ts +0 -35
  250. package/test/result-state.test.ts +0 -110
  251. package/test/task-parsing.test.ts +0 -82
  252. package/test/taskrun-messages.test.ts +0 -224
  253. package/test/tsconfig.json +0 -9
  254. package/test/windows-xml.test.ts +0 -89
  255. package/tsconfig.json +0 -19
@@ -1,608 +0,0 @@
1
- import * as fs from "fs";
2
- import * as path from "path";
3
- import * as readline from "readline";
4
- import { spawnCommand, spawnStreamingCommand } from "../spawn-command.js";
5
- import { loadConfig } from "../config.js";
6
- import { connectNats } from "../nats-client.js";
7
- import { parseTaskFile, getTaskDir, writeTaskFile, writeTaskStatus, readTaskStatus, appendHistory, createRunDir, appendRunMessage, readRunMessages, getRunDir, beginStreamingMessage } from "../task.js";
8
- import { getAgent } from "../agents/agent.js";
9
- import { getPlatform } from "../platform/index.js";
10
- import { TASK_SUCCESS_MARKER, TASK_FAILURE_MARKER, TASK_REPORT_PREFIX, TASK_PERMISSION_PREFIX } from "../agents/shared-prompt.js";
11
- import type { AgentTool } from "../agents/agent.js";
12
- import { publishHostEvent } from "../events.js";
13
- import type { HostConfig, ParsedTask, TaskRunningState, RequiredPermission } from "../types.js";
14
- import type { NatsConnection } from "nats";
15
-
16
- interface InvocationContext {
17
- agent: AgentTool;
18
- task: ParsedTask;
19
- taskDir: string;
20
- runId: string;
21
- guiEnv: Record<string, string>;
22
- nc: NatsConnection | undefined;
23
- config: HostConfig;
24
- taskId: string;
25
- /** Mutable — accumulates across invocations within a run. */
26
- transientPermissions: RequiredPermission[];
27
- }
28
-
29
- interface InvocationResult {
30
- outcome: TaskRunningState;
31
- }
32
-
33
- /**
34
- * Invoke the agent CLI in a continuation loop to handle permission requests.
35
- * `invokeTask` is the ParsedTask whose prompt is passed to the agent (in
36
- * command-triggered mode this is the per-line augmented task).
37
- */
38
- async function invokeAgentWithRetries(
39
- ctx: InvocationContext,
40
- invokeTask: ParsedTask,
41
- ): Promise<InvocationResult> {
42
- // eslint-disable-next-line no-constant-condition
43
- while (true) {
44
- const writer = beginStreamingMessage(ctx.taskDir, ctx.runId, Date.now());
45
- let lineBuf = "";
46
- let notifyPending = false;
47
- let notifyTimer: ReturnType<typeof setTimeout> | undefined;
48
-
49
- function throttledNotify() {
50
- if (notifyPending) return;
51
- notifyPending = true;
52
- notifyTimer = setTimeout(() => {
53
- notifyPending = false;
54
- publishHostEvent(ctx.nc, ctx.config.hostId, ctx.taskId, { event_type: "result-updated", run_id: ctx.runId });
55
- }, 500);
56
- }
57
-
58
- const { command, args, stdin, env: agentEnv } = ctx.agent.getTaskRunCommandLine(
59
- invokeTask, undefined, ctx.task.frontmatter.yolo_mode ? "yolo" : ctx.transientPermissions,
60
- );
61
- const result = await spawnCommand(command, args, {
62
- cwd: getRunDir(ctx.taskDir, ctx.runId),
63
- env: { ...ctx.guiEnv, ...agentEnv, PALMIER_RUN_DIR: getRunDir(ctx.taskDir, ctx.runId), PALMIER_HTTP_PORT: String(ctx.config.httpPort ?? 7256) },
64
- echoStdout: true,
65
- resolveOnFailure: true,
66
- stdin,
67
- onData: (chunk) => {
68
- lineBuf += chunk;
69
- const lines = lineBuf.split("\n");
70
- lineBuf = lines.pop() ?? "";
71
- const filtered = lines.filter((l) => !l.startsWith("[PALMIER"));
72
- if (filtered.length > 0) {
73
- writer.write(filtered.join("\n") + "\n");
74
- throttledNotify();
75
- }
76
- },
77
- });
78
-
79
- if (notifyTimer) clearTimeout(notifyTimer);
80
-
81
- const outcome: TaskRunningState = result.exitCode !== 0 ? "failed" : parseTaskOutcome(result.output);
82
- const reportFiles = parseReportFiles(result.output);
83
- const requiredPermissions = parsePermissions(result.output);
84
-
85
- if (lineBuf && !lineBuf.startsWith("[PALMIER")) {
86
- writer.write(lineBuf);
87
- }
88
-
89
- if (requiredPermissions.length > 0) {
90
- const permLines = requiredPermissions.map((p) => `- **${p.name}** ${p.description}`).join("\n");
91
- writer.write(`\n\n**Permissions requested:**\n${permLines}\n`);
92
- }
93
-
94
- writer.end(reportFiles.length > 0 ? reportFiles : undefined);
95
- await publishHostEvent(ctx.nc, ctx.config.hostId, ctx.taskId, { event_type: "result-updated", run_id: ctx.runId });
96
-
97
- if (reportFiles.length > 0) {
98
- await publishHostEvent(ctx.nc, ctx.config.hostId, ctx.taskId, {
99
- event_type: "report-generated",
100
- run_id: ctx.runId,
101
- name: ctx.task.frontmatter.name,
102
- report_files: reportFiles,
103
- });
104
- }
105
-
106
- if (requiredPermissions.length > 0) {
107
- const response = await requestPermission(ctx.config, ctx.task, ctx.taskDir, requiredPermissions);
108
-
109
- if (response === "aborted") {
110
- await appendAndNotify(ctx, {
111
- role: "user",
112
- time: Date.now(),
113
- content: "Deny & Abort Task",
114
- type: "permission",
115
- });
116
- return { outcome: "failed" };
117
- }
118
-
119
- const newPerms = requiredPermissions.filter(
120
- (rp) => !ctx.task.frontmatter.permissions?.some((ep) => ep.name === rp.name)
121
- && !ctx.transientPermissions.some((ep) => ep.name === rp.name),
122
- );
123
-
124
- await appendAndNotify(ctx, {
125
- role: "user",
126
- time: Date.now(),
127
- content: response === "granted_all" ? "Allow Always" : "Allow Once",
128
- type: "permission",
129
- });
130
-
131
- if (response === "granted_all") {
132
- ctx.task.frontmatter.permissions = [...(ctx.task.frontmatter.permissions ?? []), ...newPerms];
133
- invokeTask.frontmatter.permissions = ctx.task.frontmatter.permissions;
134
- writeTaskFile(ctx.taskDir, ctx.task);
135
- } else {
136
- ctx.transientPermissions = [...ctx.transientPermissions, ...newPerms];
137
- }
138
-
139
- // Retry with the new permissions if the agent failed.
140
- if (outcome === "failed") {
141
- continue;
142
- }
143
- }
144
-
145
- return { outcome };
146
- }
147
- }
148
-
149
- export function stripPalmierMarkers(output: string): string {
150
- return output.split("\n").filter((l) => !l.startsWith("[PALMIER")).join("\n").trim();
151
- }
152
-
153
- async function appendAndNotify(
154
- ctx: InvocationContext,
155
- msg: Parameters<typeof appendRunMessage>[2],
156
- ): Promise<void> {
157
- appendRunMessage(ctx.taskDir, ctx.runId, msg);
158
- await publishHostEvent(ctx.nc, ctx.config.hostId, ctx.taskId, { event_type: "result-updated", run_id: ctx.runId });
159
- }
160
-
161
- /** The latest run dir with no status messages yet — freshly created by the RPC handler. */
162
- function findLatestPendingRunId(taskDir: string): string | null {
163
- const dirs = fs.readdirSync(taskDir)
164
- .filter((f) => /^\d+$/.test(f) && fs.existsSync(path.join(taskDir, f, "TASKRUN.md")))
165
- .sort();
166
- if (dirs.length === 0) return null;
167
- const latest = dirs[dirs.length - 1];
168
- const messages = readRunMessages(taskDir, latest);
169
- const hasStatus = messages.some((m) => m.role === "status");
170
- return hasStatus ? null : latest;
171
- }
172
-
173
- /**
174
- * If the RPC handler already wrote "aborted" (via task.abort), respect that
175
- * instead of overwriting with the process's own outcome.
176
- */
177
- function resolveOutcome(taskDir: string, outcome: TaskRunningState): TaskRunningState {
178
- const current = readTaskStatus(taskDir);
179
- if (current?.running_state === "aborted") return "aborted";
180
- return outcome;
181
- }
182
-
183
- export async function runCommand(taskId: string): Promise<void> {
184
- const config = loadConfig();
185
- const taskDir = getTaskDir(config.projectRoot, taskId);
186
- const task = parseTaskFile(taskDir);
187
- console.log(`Running task: ${taskId}`);
188
-
189
- let nc: NatsConnection | undefined;
190
- const taskName = task.frontmatter.name;
191
-
192
- const existingRunId = findLatestPendingRunId(taskDir);
193
- const runId = existingRunId ?? createRunDir(taskDir, taskName, Date.now(), task.frontmatter.agent);
194
- if (!existingRunId) {
195
- appendHistory(config.projectRoot, { task_id: taskId, run_id: runId });
196
- }
197
-
198
- const cleanup = async () => {
199
- if (nc && !nc.isClosed()) {
200
- await nc.drain();
201
- }
202
- if (task.frontmatter.one_off) {
203
- try { getPlatform().removeTaskTimer(taskId); } catch { /* best-effort */ }
204
- }
205
- };
206
-
207
- try {
208
- nc = await connectNats(config);
209
-
210
- await publishTaskEvent(nc, config, taskDir, taskId, "started", taskName, runId);
211
- appendRunMessage(taskDir, runId, { role: "status", time: Date.now(), content: "", type: "started" });
212
- await publishHostEvent(nc, config.hostId, taskId, { event_type: "result-updated", run_id: runId });
213
-
214
- if (task.frontmatter.requires_confirmation) {
215
- const confirmed = await requestConfirmation(config, task, taskDir);
216
- const confirmPrompt = `**Task Confirmation**\n\nRun task "${taskName || task.frontmatter.user_prompt}"?`;
217
- appendRunMessage(taskDir, runId, { role: "assistant", time: Date.now(), content: confirmPrompt, type: "confirmation" });
218
- await publishHostEvent(nc, config.hostId, taskId, { event_type: "result-updated", run_id: runId });
219
-
220
- if (!confirmed) {
221
- console.log("Task aborted by user.");
222
- appendRunMessage(taskDir, runId, { role: "user", time: Date.now(), content: "Aborted", type: "confirmation" });
223
- appendRunMessage(taskDir, runId, { role: "status", time: Date.now(), content: "", type: "aborted" });
224
- await publishTaskEvent(nc, config, taskDir, taskId, "aborted", taskName, runId);
225
- await cleanup();
226
- return;
227
- }
228
- console.log("Task confirmed by user.");
229
- appendRunMessage(taskDir, runId, { role: "user", time: Date.now(), content: "Confirmed", type: "confirmation" });
230
- appendRunMessage(taskDir, runId, { role: "status", time: Date.now(), content: "", type: "confirmation" });
231
- await publishHostEvent(nc, config.hostId, taskId, { event_type: "result-updated", run_id: runId });
232
- }
233
-
234
- const guiEnv = getPlatform().getGuiEnv();
235
- const agent = getAgent(task.frontmatter.agent);
236
- const ctx: InvocationContext = {
237
- agent, task, taskDir, runId, guiEnv, nc, config, taskId,
238
- transientPermissions: [],
239
- };
240
-
241
- if (task.frontmatter.command) {
242
- const result = await runCommandTriggeredMode(ctx);
243
- const outcome = resolveOutcome(taskDir, result.outcome);
244
- appendRunMessage(taskDir, runId, { role: "status", time: Date.now(), content: "", type: outcome });
245
- await publishTaskEvent(nc, config, taskDir, taskId, outcome, taskName, runId);
246
- console.log(`Task ${taskId} completed (command-triggered).`);
247
- } else if (task.frontmatter.schedule_type === "on_new_notification"
248
- || task.frontmatter.schedule_type === "on_new_sms") {
249
- const result = await runEventTriggeredMode(ctx);
250
- const outcome = resolveOutcome(taskDir, result.outcome);
251
- appendRunMessage(taskDir, runId, { role: "status", time: Date.now(), content: "", type: outcome });
252
- await publishTaskEvent(nc, config, taskDir, taskId, outcome, taskName, runId);
253
- console.log(`Task ${taskId} completed (event-triggered).`);
254
- } else {
255
- await appendAndNotify(ctx, {
256
- role: "user",
257
- time: Date.now(),
258
- content: task.frontmatter.user_prompt,
259
- });
260
-
261
- const result = await invokeAgentWithRetries(ctx, task);
262
- const outcome = resolveOutcome(taskDir, result.outcome);
263
- appendRunMessage(taskDir, runId, { role: "status", time: Date.now(), content: "", type: outcome });
264
- await publishTaskEvent(nc, config, taskDir, taskId, outcome, taskName, runId);
265
- console.log(`Task ${taskId} completed.`);
266
- }
267
- } catch (err) {
268
- console.error(`Task ${taskId} failed:`, err);
269
- const outcome = resolveOutcome(taskDir, "failed");
270
- const errorMsg = err instanceof Error ? err.message : String(err);
271
- appendRunMessage(taskDir, runId, {
272
- role: "assistant",
273
- time: Date.now(),
274
- content: errorMsg,
275
- });
276
- appendRunMessage(taskDir, runId, { role: "status", time: Date.now(), content: "", type: outcome });
277
- await publishTaskEvent(nc, config, taskDir, taskId, outcome, taskName, runId);
278
- process.exitCode = 1;
279
- } finally {
280
- await cleanup();
281
- }
282
- }
283
-
284
- const MAX_QUEUE_SIZE = 100;
285
- const MAX_LOG_ENTRIES = 1000;
286
- /** Max input line length (chars). Long emails can take up to 200k chars. */
287
- const MAX_LINE_LENGTH = 200_000;
288
-
289
- /**
290
- * Spawn a long-running shell command and invoke the agent CLI once per stdout
291
- * line, with the user's prompt augmented by that line. Sequential with a
292
- * bounded queue.
293
- */
294
- async function runCommandTriggeredMode(
295
- ctx: InvocationContext,
296
- ): Promise<{ outcome: TaskRunningState; endTime: number }> {
297
- const commandStr = ctx.task.frontmatter.command!;
298
- console.log(`[command-triggered] Spawning: ${commandStr}`);
299
-
300
- appendRunMessage(ctx.taskDir, ctx.runId, { role: "status", time: Date.now(), content: "", type: "monitoring" });
301
- await publishHostEvent(ctx.nc, ctx.config.hostId, ctx.taskId, { event_type: "result-updated", run_id: ctx.runId });
302
-
303
- const child = spawnStreamingCommand(commandStr, {
304
- cwd: getRunDir(ctx.taskDir, ctx.runId),
305
- env: { ...ctx.guiEnv, PALMIER_RUN_DIR: getRunDir(ctx.taskDir, ctx.runId), PALMIER_HTTP_PORT: String(ctx.config.httpPort ?? 7256) },
306
- });
307
-
308
- let linesProcessed = 0;
309
- let invocationsSucceeded = 0;
310
- let invocationsFailed = 0;
311
-
312
- const lineQueue: string[] = [];
313
- let processing = false;
314
- let commandExited = false;
315
- let resolveWhenDone: (() => void) | undefined;
316
-
317
- const logPath = path.join(getRunDir(ctx.taskDir, ctx.runId), "command-output.log");
318
- function appendLog(line: string, agentOutput: string, outcome: string) {
319
- const entry = `[${new Date().toISOString()}] (${outcome}) input: ${line}\n${agentOutput}\n---\n`;
320
- fs.appendFileSync(logPath, entry, "utf-8");
321
-
322
- try {
323
- const content = fs.readFileSync(logPath, "utf-8");
324
- const entries = content.split("\n---\n").filter(Boolean);
325
- if (entries.length > MAX_LOG_ENTRIES) {
326
- const trimmed = entries.slice(-MAX_LOG_ENTRIES).join("\n---\n") + "\n---\n";
327
- fs.writeFileSync(logPath, trimmed, "utf-8");
328
- }
329
- } catch { /* ignore trim errors */ }
330
- }
331
-
332
- async function processLine(line: string): Promise<void> {
333
- linesProcessed++;
334
- if (line.length > MAX_LINE_LENGTH) {
335
- console.warn(`[command-triggered] Skipping line #${linesProcessed}: ${line.length} chars exceeds limit`);
336
- invocationsFailed++;
337
- appendLog(line.slice(0, 200) + "...(truncated)", "", "skipped");
338
- return;
339
- }
340
- console.log(`[command-triggered] Processing line #${linesProcessed}: ${line}`);
341
-
342
- const perLinePrompt = `${ctx.task.frontmatter.user_prompt}\n\nProcess this input:\n${line}`;
343
- const perLineTask: ParsedTask = {
344
- frontmatter: { ...ctx.task.frontmatter, user_prompt: perLinePrompt },
345
- };
346
-
347
- const result = await invokeAgentWithRetries(ctx, perLineTask);
348
- if (result.outcome === "finished") {
349
- invocationsSucceeded++;
350
- } else {
351
- invocationsFailed++;
352
- }
353
- appendLog(line, "", result.outcome);
354
-
355
- // Signal "waiting for more input" in the UI.
356
- appendRunMessage(ctx.taskDir, ctx.runId, { role: "status", time: Date.now(), content: "", type: "monitoring" });
357
- await publishHostEvent(ctx.nc, ctx.config.hostId, ctx.taskId, { event_type: "result-updated", run_id: ctx.runId });
358
- }
359
-
360
- async function drainQueue(): Promise<void> {
361
- if (processing) return;
362
- processing = true;
363
- try {
364
- while (lineQueue.length > 0) {
365
- const line = lineQueue.shift()!;
366
- await processLine(line);
367
- }
368
- } finally {
369
- processing = false;
370
- if (commandExited && lineQueue.length === 0 && resolveWhenDone) {
371
- resolveWhenDone();
372
- }
373
- }
374
- }
375
-
376
- const rl = readline.createInterface({ input: child.stdout! });
377
- rl.on("line", (line: string) => {
378
- if (!line.trim()) return;
379
- if (lineQueue.length >= MAX_QUEUE_SIZE) {
380
- console.warn(`[command-triggered] Queue full, dropping oldest line.`);
381
- lineQueue.shift();
382
- }
383
- lineQueue.push(line);
384
- drainQueue().catch((err) => {
385
- console.error(`[command-triggered] Error processing line:`, err);
386
- invocationsFailed++;
387
- });
388
- });
389
-
390
- let stderrBuf = "";
391
- child.stderr?.on("data", (d: Buffer) => {
392
- const chunk = d.toString();
393
- stderrBuf += chunk;
394
- process.stderr.write(d);
395
- });
396
-
397
- const exitCode = await new Promise<number | null>((resolve) => {
398
- child.on("close", (code: number | null) => {
399
- commandExited = true;
400
- rl.close();
401
- resolve(code);
402
- });
403
- child.on("error", (err: Error) => {
404
- console.error(`[command-triggered] Command error:`, err);
405
- stderrBuf += err.message;
406
- commandExited = true;
407
- rl.close();
408
- resolve(1);
409
- });
410
- });
411
-
412
- if (lineQueue.length > 0 || processing) {
413
- await new Promise<void>((resolve) => {
414
- resolveWhenDone = resolve;
415
- drainQueue();
416
- });
417
- }
418
-
419
- const endTime = Date.now();
420
-
421
- if (exitCode !== 0) {
422
- const errorDetail = stderrBuf.trim() || `Command exited with code ${exitCode}`;
423
- appendRunMessage(ctx.taskDir, ctx.runId, {
424
- role: "status",
425
- time: endTime,
426
- content: errorDetail,
427
- type: "error",
428
- });
429
- await publishHostEvent(ctx.nc, ctx.config.hostId, ctx.taskId, { event_type: "result-updated", run_id: ctx.runId });
430
- return { outcome: "failed", endTime };
431
- }
432
-
433
- return { outcome: "finished", endTime };
434
- }
435
-
436
- /**
437
- * Drain the daemon-owned per-task event queue via /task-event/pop, invoking
438
- * the agent once per event. The run process holds no NATS subscription — the
439
- * daemon owns that and atomically clears the active flag on empty pop so it
440
- * can fire a fresh run on the next incoming event.
441
- */
442
- async function runEventTriggeredMode(
443
- ctx: InvocationContext,
444
- ): Promise<{ outcome: TaskRunningState; endTime: number }> {
445
- const scheduleType = ctx.task.frontmatter.schedule_type!;
446
- const label = scheduleType === "on_new_notification" ? "notification" : "SMS";
447
- const port = ctx.config.httpPort ?? 7256;
448
- const popUrl = `http://localhost:${port}/task-event/pop?taskId=${encodeURIComponent(ctx.taskId)}`;
449
-
450
- console.log(`[event-triggered] Draining ${label} queue`);
451
- appendRunMessage(ctx.taskDir, ctx.runId, { role: "status", time: Date.now(), content: "", type: "monitoring" });
452
- await publishHostEvent(ctx.nc, ctx.config.hostId, ctx.taskId, { event_type: "result-updated", run_id: ctx.runId });
453
-
454
- let eventsProcessed = 0;
455
- try {
456
- // eslint-disable-next-line no-constant-condition
457
- while (true) {
458
- const res = await fetch(popUrl, { method: "POST" });
459
- if (!res.ok) throw new Error(`pop-event failed: ${res.status} ${res.statusText}`);
460
- const body = await res.json() as { event?: string; empty?: true };
461
- if (body.empty || !body.event) break;
462
-
463
- eventsProcessed++;
464
- console.log(`[event-triggered] Processing ${label} #${eventsProcessed}`);
465
-
466
- const perEventPrompt = `${ctx.task.frontmatter.user_prompt}\n\nProcess this new ${label}:\n${body.event}`;
467
- const perEventTask: ParsedTask = {
468
- frontmatter: { ...ctx.task.frontmatter, user_prompt: perEventPrompt },
469
- };
470
-
471
- await invokeAgentWithRetries(ctx, perEventTask);
472
-
473
- appendRunMessage(ctx.taskDir, ctx.runId, { role: "status", time: Date.now(), content: "", type: "monitoring" });
474
- await publishHostEvent(ctx.nc, ctx.config.hostId, ctx.taskId, { event_type: "result-updated", run_id: ctx.runId });
475
- }
476
- } catch (err) {
477
- const errorMsg = err instanceof Error ? err.message : String(err);
478
- appendRunMessage(ctx.taskDir, ctx.runId, { role: "status", time: Date.now(), content: errorMsg, type: "error" });
479
- await publishHostEvent(ctx.nc, ctx.config.hostId, ctx.taskId, { event_type: "result-updated", run_id: ctx.runId });
480
- return { outcome: "failed", endTime: Date.now() };
481
- }
482
-
483
- return { outcome: "finished", endTime: Date.now() };
484
- }
485
-
486
- async function publishTaskEvent(
487
- nc: NatsConnection | undefined,
488
- config: HostConfig,
489
- taskDir: string,
490
- taskId: string,
491
- eventType: TaskRunningState,
492
- taskName?: string,
493
- runId?: string,
494
- ): Promise<void> {
495
- writeTaskStatus(taskDir, {
496
- running_state: eventType,
497
- time_stamp: Date.now(),
498
- ...(eventType === "started" ? { pid: process.pid } : {}),
499
- });
500
-
501
- const payload: Record<string, unknown> = { event_type: "running-state", running_state: eventType };
502
- if (taskName) payload.name = taskName;
503
- if (runId) payload.run_id = runId;
504
- await publishHostEvent(nc, config.hostId, taskId, payload);
505
- }
506
-
507
-
508
- async function requestPermission(
509
- config: HostConfig,
510
- task: ParsedTask,
511
- taskDir: string,
512
- requiredPermissions: RequiredPermission[],
513
- ): Promise<"granted" | "granted_all" | "aborted"> {
514
- const port = config.httpPort ?? 7256;
515
- const res = await fetch(`http://localhost:${port}/request-permission`, {
516
- method: "POST",
517
- headers: { "Content-Type": "application/json" },
518
- body: JSON.stringify({
519
- taskId: task.frontmatter.id,
520
- taskName: task.frontmatter.name,
521
- permissions: requiredPermissions,
522
- }),
523
- });
524
- const body = await res.json() as { response?: string; error?: string };
525
- const response = body.response as "granted" | "granted_all" | "aborted" | undefined;
526
- if (!response || !["granted", "granted_all", "aborted"].includes(response)) {
527
- throw new Error(`Permission request failed: ${body.error ?? `unexpected response: ${JSON.stringify(body)}`}`);
528
- }
529
- writeTaskStatus(taskDir, {
530
- running_state: response === "aborted" ? "aborted" : "started",
531
- time_stamp: Date.now(),
532
- });
533
- return response;
534
- }
535
-
536
-
537
- async function requestConfirmation(
538
- config: HostConfig,
539
- task: ParsedTask,
540
- taskDir: string,
541
- ): Promise<boolean> {
542
- const port = config.httpPort ?? 7256;
543
- const res = await fetch(`http://localhost:${port}/request-confirmation?taskId=${encodeURIComponent(task.frontmatter.id)}`, {
544
- method: "POST",
545
- headers: { "Content-Type": "application/json" },
546
- body: JSON.stringify({ description: `Run task "${task.frontmatter.name || task.frontmatter.id}"?` }),
547
- });
548
- const body = await res.json() as { confirmed?: boolean; error?: string };
549
- if (typeof body.confirmed !== "boolean") {
550
- throw new Error(`Confirmation request failed: ${body.error ?? `unexpected response: ${JSON.stringify(body)}`}`);
551
- }
552
- const { confirmed } = body;
553
- writeTaskStatus(taskDir, {
554
- running_state: confirmed ? "started" : "aborted",
555
- time_stamp: Date.now(),
556
- });
557
- return confirmed;
558
- }
559
-
560
- const ALLOWED_REPORT_EXT = [".md", ".txt", ".png", ".jpg", ".jpeg", ".gif", ".svg", ".webp"];
561
-
562
- export function parseReportFiles(output: string): string[] {
563
- const regex = new RegExp(`^\\${TASK_REPORT_PREFIX}\\s+(.+)$`, "gm");
564
- const files: string[] = [];
565
- let match;
566
- while ((match = regex.exec(output)) !== null) {
567
- const name = match[1].trim();
568
- // Skip placeholder examples echoed from the prompt (e.g. "<filename>").
569
- if (!name || name.startsWith("<")) continue;
570
- const ext = name.lastIndexOf(".") >= 0 ? name.slice(name.lastIndexOf(".")).toLowerCase() : "";
571
- if (!ALLOWED_REPORT_EXT.includes(ext)) continue;
572
- files.push(name);
573
- }
574
- return files;
575
- }
576
-
577
- export function parsePermissions(output: string): RequiredPermission[] {
578
- const regex = new RegExp(`^\\${TASK_PERMISSION_PREFIX}\\s+(.+)$`, "gm");
579
- const perms: RequiredPermission[] = [];
580
- let match;
581
- while ((match = regex.exec(output)) !== null) {
582
- const raw = match[1].trim();
583
- // Skip placeholder examples echoed from the prompt (e.g. "<tool_name> | <description>").
584
- if (raw.startsWith("<")) continue;
585
- const sep = raw.indexOf("|");
586
- if (sep !== -1) {
587
- perms.push({ name: raw.slice(0, sep).trim(), description: raw.slice(sep + 1).trim() });
588
- } else {
589
- perms.push({ name: raw, description: "" });
590
- }
591
- }
592
- return perms;
593
- }
594
-
595
- /** Falls back to "finished" if no success/failure marker is found. */
596
- export function parseTaskOutcome(output: string): TaskRunningState {
597
- const lastChunk = output.slice(-500);
598
- const regex = new RegExp(`^\\${TASK_FAILURE_MARKER}$|^\\${TASK_SUCCESS_MARKER}$`, "gm");
599
- let last: string | null = null;
600
- let match;
601
- while ((match = regex.exec(lastChunk)) !== null) {
602
- last = match[0];
603
- }
604
- if (last === TASK_FAILURE_MARKER) return "failed";
605
- if (last === TASK_SUCCESS_MARKER) return "finished";
606
- return "finished";
607
- }
608
-