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,691 +0,0 @@
1
- import { randomUUID } from "crypto";
2
- import * as fs from "fs";
3
- import * as path from "path";
4
- import { type ChildProcess } from "child_process";
5
- import { type NatsConnection } from "nats";
6
- import { listTasks, parseTaskFile, writeTaskFile, getTaskDir, readTaskStatus, writeTaskStatus, readHistory, deleteHistoryEntry, appendTaskList, removeFromTaskList, isTaskInList, appendHistory, createRunDir, appendRunMessage, getRunDir, writeFollowupStatus, readFollowupStatus, deleteFollowupStatus } from "./task.js";
7
- import { resolvePending, getPending, listPending } from "./pending-requests.js";
8
- import { getPlatform } from "./platform/index.js";
9
- import { spawnCommand } from "./spawn-command.js";
10
- import crossSpawn from "cross-spawn";
11
- import { getAgent } from "./agents/agent.js";
12
- import { validateClient, revokeClient } from "./client-store.js";
13
- import { publishHostEvent } from "./events.js";
14
- import { getLinkedDevice, setLinkedDevice, clearLinkedDevice, clearLinkedDeviceIfMatches } from "./linked-device.js";
15
- import { currentVersion, performUpdate } from "./update-checker.js";
16
- import { parseReportFiles, parseTaskOutcome, stripPalmierMarkers } from "./commands/run.js";
17
- import { clearTaskQueue } from "./event-queues.js";
18
- import { buildLanUrl } from "./network.js";
19
- import type { HostConfig, ParsedTask, RpcMessage, ConversationMessage } from "./types.js";
20
-
21
- export function parseResultFrontmatter(raw: string): Record<string, unknown> {
22
- const fmMatch = raw.match(/^---\n([\s\S]*?)\n---\n([\s\S]*)$/);
23
- if (!fmMatch) return { messages: [] };
24
-
25
- const meta: Record<string, string> = {};
26
- for (const line of fmMatch[1].split("\n")) {
27
- const sep = line.indexOf(": ");
28
- if (sep === -1) continue;
29
- meta[line.slice(0, sep).trim()] = line.slice(sep + 2).trim();
30
- }
31
-
32
- const messages = parseConversationMessages(fmMatch[2]);
33
-
34
- const statusMessages = messages.filter((m: ConversationMessage) => m.role === "status");
35
- const lastStatus = statusMessages[statusMessages.length - 1];
36
- const startedMsg = statusMessages.find((m: ConversationMessage) => m.type === "started");
37
- const terminalStates = ["finished", "failed", "aborted"];
38
- const terminalMsg = [...statusMessages].reverse().find((m: ConversationMessage) => terminalStates.includes(m.type ?? ""));
39
-
40
- const activeStates = ["started", "monitoring", "confirmation"];
41
- let runningState: string | undefined;
42
- if (lastStatus?.type === "monitoring") {
43
- // Show "monitoring" only if no assistant/user message followed it.
44
- const lastStatusIdx = messages.lastIndexOf(lastStatus);
45
- const hasMessageAfter = messages.slice(lastStatusIdx + 1).some((m: ConversationMessage) => m.role === "assistant" || m.role === "user");
46
- runningState = hasMessageAfter ? "started" : "monitoring";
47
- } else if (activeStates.includes(lastStatus?.type ?? "")) {
48
- runningState = terminalMsg ? "followup" : "started";
49
- } else {
50
- runningState = lastStatus?.type;
51
- }
52
-
53
- return {
54
- messages,
55
- task_name: meta.task_name,
56
- agent: meta.agent,
57
- running_state: runningState,
58
- start_time: startedMsg?.time || undefined,
59
- end_time: terminalMsg?.time || undefined,
60
- };
61
- }
62
-
63
- function parseConversationMessages(body: string): ConversationMessage[] {
64
- const delimiterRegex = /<!-- palmier:message\s+(.*?)\s*-->/g;
65
- const messages: ConversationMessage[] = [];
66
- const matches = [...body.matchAll(delimiterRegex)];
67
-
68
- if (matches.length === 0) {
69
- // No delimiters — treat entire body as a single assistant message.
70
- const content = body.trim();
71
- if (content) {
72
- messages.push({ role: "assistant", time: 0, content });
73
- }
74
- return messages;
75
- }
76
-
77
- for (let i = 0; i < matches.length; i++) {
78
- const match = matches[i];
79
- const attrs = match[1];
80
- const start = match.index! + match[0].length;
81
- const end = i + 1 < matches.length ? matches[i + 1].index! : body.length;
82
- const content = body.slice(start, end).trim();
83
-
84
- const role = (parseAttr(attrs, "role") ?? "assistant") as "assistant" | "user";
85
- const time = Number(parseAttr(attrs, "time") ?? "0");
86
- const type = parseAttr(attrs, "type") as ConversationMessage["type"];
87
- const attachmentsRaw = parseAttr(attrs, "attachments");
88
- const attachments = attachmentsRaw ? attachmentsRaw.split(",").map((f) => f.trim()).filter(Boolean) : undefined;
89
-
90
- messages.push({ role, time, content, ...(type ? { type } : {}), ...(attachments ? { attachments } : {}) });
91
- }
92
-
93
- return messages;
94
- }
95
-
96
- function parseAttr(attrs: string, name: string): string | undefined {
97
- const match = attrs.match(new RegExp(`${name}="([^"]*)"`));
98
- return match ? match[1] : undefined;
99
- }
100
-
101
- async function generateName(
102
- projectRoot: string,
103
- userPrompt: string,
104
- agentName: string,
105
- ): Promise<string> {
106
- const prompt = `Generate a concise 3-6 word name for this task. Reply with ONLY the name, nothing else.\n\nTask: ${userPrompt}`;
107
- const agent = getAgent(agentName);
108
- const { command, args, stdin, env: agentEnv } = agent.getPromptCommandLine(prompt);
109
-
110
- try {
111
- const { output } = await spawnCommand(command, args, {
112
- cwd: projectRoot,
113
- timeout: 30_000,
114
- stdin,
115
- ...(agentEnv ? { env: agentEnv } : {}),
116
- });
117
- const name = output.trim().replace(/^["']|["']$/g, "").slice(0, 80);
118
- return name || userPrompt;
119
- } catch {
120
- return userPrompt;
121
- }
122
- }
123
-
124
- /** Active follow-up child processes, keyed by "taskId:runId". */
125
- const activeFollowups = new Map<string, ChildProcess>();
126
-
127
- export function createRpcHandler(config: HostConfig, nc?: NatsConnection) {
128
- function flattenTask(task: ParsedTask) {
129
- const taskDir = getTaskDir(config.projectRoot, task.frontmatter.id);
130
- const status = readTaskStatus(taskDir);
131
- return {
132
- ...task.frontmatter,
133
- status: status ?? undefined,
134
- };
135
- }
136
-
137
- async function handleRpc(request: RpcMessage): Promise<unknown> {
138
- // task.user_input comes from server-originated push responses; it's gated
139
- // by getPending() rather than a client token.
140
- const skipAuth = request.method === "task.user_input";
141
- if (!skipAuth && !request.localhost && (!request.clientToken || !validateClient(request.clientToken))) {
142
- return { error: "Unauthorized" };
143
- }
144
-
145
- switch (request.method) {
146
- case "host.info": {
147
- return {
148
- agents: config.agents ?? [],
149
- version: currentVersion,
150
- host_platform: process.platform,
151
- host_timezone: Intl.DateTimeFormat().resolvedOptions().timeZone,
152
- linked_client_token: getLinkedDevice()?.clientToken ?? null,
153
- pending_prompts: listPending(),
154
- lan_url: buildLanUrl(config.httpPort ?? 7256, config.defaultInterface),
155
- };
156
- }
157
-
158
- case "task.list": {
159
- const tasks = listTasks(config.projectRoot);
160
- return { tasks: tasks.map((task) => flattenTask(task)) };
161
- }
162
-
163
- case "task.get": {
164
- const params = request.params as { id: string };
165
- const taskDir = getTaskDir(config.projectRoot, params.id);
166
- try {
167
- const task = parseTaskFile(taskDir);
168
- return flattenTask(task);
169
- } catch {
170
- return { error: "Task not found" };
171
- }
172
- }
173
-
174
- case "task.create": {
175
- const params = request.params as {
176
- user_prompt: string;
177
- agent: string;
178
- schedule_type?: "crons" | "specific_times" | "on_new_notification" | "on_new_sms";
179
- schedule_values?: string[];
180
- schedule_enabled?: boolean;
181
- requires_confirmation?: boolean;
182
- yolo_mode?: boolean;
183
- foreground_mode?: boolean;
184
- command?: string;
185
- };
186
-
187
- const name = params.user_prompt.length <= 50
188
- ? params.user_prompt
189
- : await generateName(config.projectRoot, params.user_prompt, params.agent);
190
-
191
- const id = randomUUID();
192
- const taskDir = getTaskDir(config.projectRoot, id);
193
- const task: ParsedTask = {
194
- frontmatter: {
195
- id,
196
- name,
197
- user_prompt: params.user_prompt,
198
- agent: params.agent,
199
- schedule_enabled: params.schedule_enabled ?? true,
200
- requires_confirmation: params.requires_confirmation ?? true,
201
- ...(params.schedule_type ? { schedule_type: params.schedule_type } : {}),
202
- ...(params.schedule_values?.length ? { schedule_values: params.schedule_values } : {}),
203
- ...(params.yolo_mode ? { yolo_mode: true } : {}),
204
- ...(params.foreground_mode ? { foreground_mode: true } : {}),
205
- ...(params.command ? { command: params.command } : {}),
206
- },
207
- };
208
-
209
- writeTaskFile(taskDir, task);
210
- appendTaskList(config.projectRoot, id);
211
- getPlatform().installTaskTimer(config, task);
212
-
213
- return flattenTask(task);
214
- }
215
-
216
- case "task.update": {
217
- const params = request.params as {
218
- id: string;
219
- user_prompt?: string;
220
- agent?: string;
221
- schedule_type?: "crons" | "specific_times" | "on_new_notification" | "on_new_sms" | null;
222
- schedule_values?: string[] | null;
223
- schedule_enabled?: boolean;
224
- requires_confirmation?: boolean;
225
- yolo_mode?: boolean;
226
- foreground_mode?: boolean;
227
- command?: string;
228
- };
229
-
230
- const taskDir = getTaskDir(config.projectRoot, params.id);
231
- const existing = parseTaskFile(taskDir);
232
-
233
- const promptChanged = params.user_prompt !== undefined && params.user_prompt !== existing.frontmatter.user_prompt;
234
- const agentChanged = params.agent !== undefined && params.agent !== existing.frontmatter.agent;
235
-
236
- if (params.user_prompt !== undefined) existing.frontmatter.user_prompt = params.user_prompt;
237
- if (params.agent !== undefined) existing.frontmatter.agent = params.agent;
238
- if (params.schedule_type !== undefined) {
239
- if (params.schedule_type) {
240
- existing.frontmatter.schedule_type = params.schedule_type;
241
- } else {
242
- delete existing.frontmatter.schedule_type;
243
- }
244
- }
245
- if (params.schedule_values !== undefined) {
246
- if (params.schedule_values && params.schedule_values.length > 0) {
247
- existing.frontmatter.schedule_values = params.schedule_values;
248
- } else {
249
- delete existing.frontmatter.schedule_values;
250
- }
251
- }
252
- if (params.schedule_enabled !== undefined) existing.frontmatter.schedule_enabled = params.schedule_enabled;
253
- if (params.requires_confirmation !== undefined)
254
- existing.frontmatter.requires_confirmation = params.requires_confirmation;
255
- if (params.yolo_mode !== undefined) {
256
- existing.frontmatter.yolo_mode = params.yolo_mode || undefined;
257
- if (params.yolo_mode) delete existing.frontmatter.permissions;
258
- }
259
- if (params.foreground_mode !== undefined) existing.frontmatter.foreground_mode = params.foreground_mode || undefined;
260
- if (params.command !== undefined) {
261
- if (params.command) {
262
- existing.frontmatter.command = params.command;
263
- } else {
264
- delete existing.frontmatter.command;
265
- }
266
- }
267
-
268
- if (promptChanged || agentChanged) {
269
- existing.frontmatter.name = existing.frontmatter.user_prompt.length <= 50
270
- ? existing.frontmatter.user_prompt
271
- : await generateName(config.projectRoot, existing.frontmatter.user_prompt, existing.frontmatter.agent);
272
- }
273
-
274
- writeTaskFile(taskDir, existing);
275
-
276
- // installTaskTimer overwrites in-place (schtasks /f, systemd unit rewrite)
277
- // without killing a running task process.
278
- getPlatform().installTaskTimer(config, existing);
279
-
280
- return flattenTask(existing);
281
- }
282
-
283
- case "task.delete": {
284
- const params = request.params as { id: string };
285
-
286
- getPlatform().removeTaskTimer(params.id);
287
- clearTaskQueue(params.id);
288
- removeFromTaskList(config.projectRoot, params.id);
289
-
290
- return { ok: true, task_id: params.id };
291
- }
292
-
293
- case "task.run_oneoff": {
294
- const params = request.params as {
295
- user_prompt: string;
296
- agent: string;
297
- requires_confirmation?: boolean;
298
- yolo_mode?: boolean;
299
- foreground_mode?: boolean;
300
- command?: string;
301
- };
302
-
303
- const id = randomUUID();
304
- const taskDir = getTaskDir(config.projectRoot, id);
305
- const name = params.user_prompt.slice(0, 60);
306
- const task: ParsedTask = {
307
- frontmatter: {
308
- id,
309
- name,
310
- user_prompt: params.user_prompt,
311
- agent: params.agent,
312
- schedule_enabled: false,
313
- requires_confirmation: params.requires_confirmation ?? false,
314
- one_off: true,
315
- ...(params.yolo_mode ? { yolo_mode: true } : {}),
316
- ...(params.foreground_mode ? { foreground_mode: true } : {}),
317
- ...(params.command ? { command: params.command } : {}),
318
- },
319
- };
320
-
321
- writeTaskFile(taskDir, task);
322
- // One-off run: do NOT append to tasks.jsonl.
323
-
324
- const runId = createRunDir(taskDir, name, Date.now(), params.agent);
325
- appendHistory(config.projectRoot, { task_id: id, run_id: runId });
326
-
327
- const platform = getPlatform();
328
- platform.installTaskTimer(config, task);
329
- await platform.startTask(id);
330
-
331
- return { ok: true, task_id: id, run_id: runId };
332
- }
333
-
334
- case "task.run": {
335
- const params = request.params as { id: string };
336
- try {
337
- const runTaskDir = getTaskDir(config.projectRoot, params.id);
338
- const platform = getPlatform();
339
-
340
- if (platform.isTaskRunning(params.id)) {
341
- console.log(`[task.run] Task ${params.id} is already running, killing stale process`);
342
- await platform.stopTask(params.id);
343
- }
344
-
345
- const runTask = parseTaskFile(runTaskDir);
346
- const taskRunId = createRunDir(runTaskDir, runTask.frontmatter.name, Date.now(), runTask.frontmatter.agent);
347
- appendHistory(config.projectRoot, { task_id: params.id, run_id: taskRunId });
348
-
349
- await platform.startTask(params.id);
350
- return { ok: true, task_id: params.id, run_id: taskRunId };
351
- } catch (err: unknown) {
352
- const e = err as { stderr?: string; message?: string };
353
- console.error(`task.run failed for ${params.id}: ${e.stderr || e.message}`);
354
- return { error: `Failed to start task: ${e.stderr || e.message}` };
355
- }
356
- }
357
-
358
- case "task.followup": {
359
- const params = request.params as { id: string; run_id: string; message: string };
360
- if (!params.run_id || !params.message?.trim()) {
361
- return { error: "run_id and message are required" };
362
- }
363
- const followupKey = `${params.id}:${params.run_id}`;
364
- if (activeFollowups.has(followupKey)) {
365
- return { error: "A follow-up is already running for this run" };
366
- }
367
-
368
- const followupTaskDir = getTaskDir(config.projectRoot, params.id);
369
- const followupTask = parseTaskFile(followupTaskDir);
370
- const followupRunDir = getRunDir(followupTaskDir, params.run_id);
371
-
372
- appendRunMessage(followupTaskDir, params.run_id, {
373
- role: "user",
374
- time: Date.now(),
375
- content: params.message,
376
- });
377
- appendRunMessage(followupTaskDir, params.run_id, {
378
- role: "status",
379
- time: Date.now(),
380
- content: "",
381
- type: "started",
382
- });
383
- await publishHostEvent(nc, config.hostId, params.id, { event_type: "result-updated", run_id: params.run_id });
384
-
385
- const followupAgent = getAgent(followupTask.frontmatter.agent);
386
- const { command: cmd, args: cmdArgs, stdin, env: followupAgentEnv } = followupAgent.getTaskRunCommandLine(
387
- followupTask, params.message, followupTask.frontmatter.yolo_mode ? "yolo" : followupTask.frontmatter.permissions,
388
- );
389
-
390
- const child = crossSpawn(cmd, cmdArgs, {
391
- cwd: followupRunDir,
392
- stdio: [stdin != null ? "pipe" : "ignore", "pipe", "pipe"],
393
- env: { ...process.env, ...followupAgentEnv },
394
- windowsHide: true,
395
- });
396
- if (stdin != null) child.stdin!.end(stdin);
397
- activeFollowups.set(followupKey, child);
398
- if (child.pid) writeFollowupStatus(followupRunDir, { pid: child.pid, spawned_at: Date.now() });
399
-
400
- const chunks: Buffer[] = [];
401
- child.stdout?.on("data", (d: Buffer) => chunks.push(d));
402
- child.stderr?.on("data", (d: Buffer) => process.stderr.write(d));
403
-
404
- child.on("close", async (code: number | null) => {
405
- activeFollowups.delete(followupKey);
406
- deleteFollowupStatus(followupRunDir);
407
- // stop_followup already wrote the stopped status.
408
- if (child.killed) return;
409
-
410
- const output = Buffer.concat(chunks).toString("utf-8");
411
- const outcome = code !== 0 ? "failed" : parseTaskOutcome(output);
412
- const reportFiles = parseReportFiles(output);
413
-
414
- appendRunMessage(followupTaskDir, params.run_id, {
415
- role: "assistant",
416
- time: Date.now(),
417
- content: stripPalmierMarkers(output),
418
- attachments: reportFiles.length > 0 ? reportFiles : undefined,
419
- });
420
- appendRunMessage(followupTaskDir, params.run_id, {
421
- role: "status",
422
- time: Date.now(),
423
- content: "",
424
- type: outcome,
425
- });
426
- await publishHostEvent(nc, config.hostId, params.id, { event_type: "result-updated", run_id: params.run_id });
427
- });
428
-
429
- child.on("error", async (err: Error) => {
430
- activeFollowups.delete(followupKey);
431
- deleteFollowupStatus(followupRunDir);
432
- console.error(`Follow-up failed for ${followupKey}:`, err);
433
- appendRunMessage(followupTaskDir, params.run_id, {
434
- role: "status",
435
- time: Date.now(),
436
- content: "",
437
- type: "failed",
438
- });
439
- await publishHostEvent(nc, config.hostId, params.id, { event_type: "result-updated", run_id: params.run_id });
440
- });
441
-
442
- return { ok: true, task_id: params.id, run_id: params.run_id };
443
- }
444
-
445
- case "task.stop_followup": {
446
- const params = request.params as { id: string; run_id: string };
447
- if (!params.run_id) {
448
- return { error: "run_id is required" };
449
- }
450
- const stopKey = `${params.id}:${params.run_id}`;
451
- const stopTaskDir = getTaskDir(config.projectRoot, params.id);
452
- const stopRunDir = getRunDir(stopTaskDir, params.run_id);
453
- const child = activeFollowups.get(stopKey);
454
-
455
- let pidToKill: number | undefined = child?.pid;
456
- if (!child) {
457
- // Daemon restarted since spawn — the in-memory handle is gone but
458
- // the child may still be running. Fall back to the persisted PID.
459
- const persisted = readFollowupStatus(stopRunDir);
460
- if (!persisted) return { error: "No active follow-up for this run" };
461
- pidToKill = persisted.pid;
462
- }
463
-
464
- if (pidToKill !== undefined) {
465
- if (process.platform === "win32") {
466
- try {
467
- const { execFileSync } = await import("child_process");
468
- execFileSync("taskkill", ["/pid", String(pidToKill), "/f", "/t"], { windowsHide: true, stdio: "pipe" });
469
- } catch { /* may have already exited */ }
470
- } else if (child) {
471
- child.kill();
472
- } else {
473
- try { process.kill(pidToKill, "SIGTERM"); } catch { /* already dead */ }
474
- }
475
- }
476
-
477
- // child.killed stops the close handler from double-writing the status.
478
- appendRunMessage(stopTaskDir, params.run_id, {
479
- role: "status",
480
- time: Date.now(),
481
- content: "",
482
- type: "stopped",
483
- });
484
- activeFollowups.delete(stopKey);
485
- deleteFollowupStatus(stopRunDir);
486
- await publishHostEvent(nc, config.hostId, params.id, { event_type: "result-updated", run_id: params.run_id });
487
- return { ok: true, task_id: params.id, run_id: params.run_id };
488
- }
489
-
490
- case "task.abort": {
491
- const params = request.params as { id: string };
492
- const abortTaskDir = getTaskDir(config.projectRoot, params.id);
493
- // Read PID before overwriting — stopTask needs it to kill the
494
- // process tree on Windows.
495
- const abortPrevStatus = readTaskStatus(abortTaskDir);
496
- // Write abort status before killing so the dying process's signal
497
- // handler sees this was RPC-initiated and skips publishing.
498
- writeTaskStatus(abortTaskDir, {
499
- running_state: "aborted",
500
- time_stamp: Date.now(),
501
- ...(abortPrevStatus?.pid ? { pid: abortPrevStatus.pid } : {}),
502
- });
503
- try {
504
- const runDirs = fs.readdirSync(abortTaskDir)
505
- .filter((f) => /^\d+$/.test(f) && fs.existsSync(path.join(abortTaskDir, f, "TASKRUN.md")))
506
- .sort();
507
- const latestRunId = runDirs[runDirs.length - 1];
508
- if (latestRunId) {
509
- appendRunMessage(abortTaskDir, latestRunId, {
510
- role: "status",
511
- time: Date.now(),
512
- content: "",
513
- type: "aborted",
514
- });
515
- }
516
- } catch { /* best-effort */ }
517
-
518
- try {
519
- await getPlatform().stopTask(params.id);
520
- } catch (err: unknown) {
521
- const e = err as { stderr?: string; message?: string };
522
- console.error(`task.abort failed for ${params.id}: ${e.stderr || e.message}`);
523
- return { error: `Failed to abort task: ${e.stderr || e.message}` };
524
- }
525
- try {
526
- const aborted = parseTaskFile(abortTaskDir);
527
- if (aborted.frontmatter.one_off) getPlatform().removeTaskTimer(params.id);
528
- } catch { /* best-effort cleanup */ }
529
- const abortPayload: Record<string, unknown> = { event_type: "running-state", running_state: "aborted" };
530
- await publishHostEvent(nc, config.hostId, params.id, abortPayload);
531
- return { ok: true, task_id: params.id };
532
- }
533
-
534
- case "task.status": {
535
- const params = request.params as { id: string };
536
- const taskDir = getTaskDir(config.projectRoot, params.id);
537
- const status = readTaskStatus(taskDir);
538
- if (!status) {
539
- return { task_id: params.id, error: "No status found" };
540
- }
541
- return { task_id: params.id, ...status };
542
- }
543
-
544
- case "task.result": {
545
- const params = request.params as { id: string; run_id: string };
546
- if (!params.run_id) {
547
- return { error: "run_id is required" };
548
- }
549
- const taskrunPath = path.join(config.projectRoot, "tasks", params.id, params.run_id, "TASKRUN.md");
550
-
551
- try {
552
- const raw = fs.readFileSync(taskrunPath, "utf-8");
553
- const meta = parseResultFrontmatter(raw);
554
- return { task_id: params.id, ...meta };
555
- } catch {
556
- return { task_id: params.id, error: "Run not found" };
557
- }
558
- }
559
-
560
- case "task.reports": {
561
- const params = request.params as { id: string; run_id: string; report_files: string[] };
562
- if (!params.run_id || !Array.isArray(params.report_files) || params.report_files.length === 0) {
563
- return { error: "run_id and report_files are required" };
564
- }
565
- const ALLOWED_EXT = [".md", ".txt", ".png", ".jpg", ".jpeg", ".gif", ".svg", ".webp"];
566
- const IMAGE_EXT = [".png", ".jpg", ".jpeg", ".gif", ".svg", ".webp"];
567
- const reports: Array<{ file: string; content?: string; data_url?: string; error?: string }> = [];
568
- const runDir = path.join(config.projectRoot, "tasks", params.id, params.run_id);
569
- for (const file of params.report_files) {
570
- const ext = path.extname(file).toLowerCase();
571
- if (!ALLOWED_EXT.includes(ext)) {
572
- reports.push({ file, error: `unsupported file type: ${ext}` });
573
- continue;
574
- }
575
- const basename = path.basename(file);
576
- if (basename !== file) {
577
- reports.push({ file, error: "must be a plain filename" });
578
- continue;
579
- }
580
- const reportPath = path.join(runDir, basename);
581
- try {
582
- if (IMAGE_EXT.includes(ext)) {
583
- const buf = fs.readFileSync(reportPath);
584
- const mime = ext === ".svg" ? "image/svg+xml" : `image/${ext.slice(1).replace("jpg", "jpeg")}`;
585
- reports.push({ file, data_url: `data:${mime};base64,${buf.toString("base64")}` });
586
- } else {
587
- const content = fs.readFileSync(reportPath, "utf-8");
588
- reports.push({ file, content });
589
- }
590
- } catch {
591
- reports.push({ file, error: "Report file not found" });
592
- }
593
- }
594
- return { task_id: params.id, reports };
595
- }
596
-
597
- case "task.user_input": {
598
- const params = request.params as { id: string; value: string[] };
599
-
600
- const pending = getPending(params.id);
601
- if (!pending) {
602
- return { ok: false, error: "not pending" };
603
- }
604
-
605
- const resolved = resolvePending(params.id, params.value);
606
- console.log(`[task.user_input] ${params.id} → ${params.value}`);
607
- return { ok: resolved };
608
- }
609
-
610
- case "taskrun.list": {
611
- const params = request.params as { offset?: number; limit?: number; task_id?: string };
612
- const { entries, total } = readHistory(config.projectRoot, {
613
- offset: params.offset ?? 0,
614
- limit: params.limit ?? 10,
615
- task_id: params.task_id,
616
- });
617
-
618
- const enriched = entries.map((entry) => {
619
- const taskrunPath = path.join(config.projectRoot, "tasks", entry.task_id, entry.run_id, "TASKRUN.md");
620
- try {
621
- const raw = fs.readFileSync(taskrunPath, "utf-8");
622
- const meta = parseResultFrontmatter(raw);
623
- const { messages: _, ...rest } = meta;
624
- return { ...entry, ...rest };
625
- } catch {
626
- return { ...entry, error: "Run not found" };
627
- }
628
- });
629
-
630
- return { entries: enriched, total };
631
- }
632
-
633
- case "taskrun.delete": {
634
- const params = request.params as { task_id: string; run_id: string };
635
- if (!params.task_id || !params.run_id) {
636
- return { error: "task_id and run_id are required" };
637
- }
638
- const deleteTaskDir = getTaskDir(config.projectRoot, params.task_id);
639
-
640
- const deleted = deleteHistoryEntry(config.projectRoot, params.task_id, params.run_id);
641
- if (!deleted) {
642
- return { error: "History entry not found" };
643
- }
644
-
645
- const { total: remainingRuns } = readHistory(config.projectRoot, { task_id: params.task_id, limit: 1 });
646
- if (remainingRuns === 0 && !isTaskInList(config.projectRoot, params.task_id)) {
647
- try { getPlatform().removeTaskTimer(params.task_id); } catch { /* best-effort */ }
648
- clearTaskQueue(params.task_id);
649
- try { fs.rmSync(deleteTaskDir, { recursive: true, force: true }); } catch { /* best-effort */ }
650
- }
651
-
652
- return { ok: true, task_id: params.task_id, run_id: params.run_id };
653
- }
654
-
655
- case "host.update": {
656
- const error = await performUpdate();
657
- if (error) return { error };
658
- return { ok: true };
659
- }
660
-
661
- case "device.link": {
662
- const params = request.params as { fcmToken: string };
663
- if (!params.fcmToken) return { error: "fcmToken is required" };
664
- const clientToken = request.clientToken ?? "";
665
- if (!clientToken) return { error: "Unauthorized" };
666
- setLinkedDevice(clientToken, params.fcmToken);
667
- return { ok: true };
668
- }
669
-
670
- case "device.unlink": {
671
- const clientToken = request.clientToken ?? "";
672
- const current = getLinkedDevice();
673
- if (current?.clientToken === clientToken) clearLinkedDevice();
674
- return { ok: true };
675
- }
676
-
677
- case "clients.revoke_self": {
678
- const clientToken = request.clientToken ?? "";
679
- if (!clientToken) return { error: "Unauthorized" };
680
- clearLinkedDeviceIfMatches(clientToken);
681
- revokeClient(clientToken);
682
- return { ok: true };
683
- }
684
-
685
- default:
686
- return { error: `Unknown method: ${request.method}` };
687
- }
688
- }
689
-
690
- return handleRpc;
691
- }