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
package/src/sms-store.ts DELETED
@@ -1,28 +0,0 @@
1
- export interface SmsMessage {
2
- id: string;
3
- sender: string;
4
- body: string;
5
- timestamp: number;
6
- receivedAt: number;
7
- }
8
-
9
- const MAX_MESSAGES = 50;
10
- const messages: SmsMessage[] = [];
11
- const listeners = new Set<() => void>();
12
-
13
- export function addSmsMessage(m: SmsMessage): void {
14
- messages.push(m);
15
- if (messages.length > MAX_MESSAGES) {
16
- messages.shift();
17
- }
18
- for (const cb of listeners) cb();
19
- }
20
-
21
- export function getSmsMessages(): SmsMessage[] {
22
- return [...messages];
23
- }
24
-
25
- export function onSmsChanged(cb: () => void): () => void {
26
- listeners.add(cb);
27
- return () => { listeners.delete(cb); };
28
- }
@@ -1,123 +0,0 @@
1
- import crossSpawn from "cross-spawn";
2
- import { execFileSync, type ChildProcess } from "child_process";
3
-
4
- /** Kill a child process and its entire tree on Windows; plain kill elsewhere. */
5
- function treeKill(child: ChildProcess): void {
6
- if (process.platform === "win32" && child.pid) {
7
- try {
8
- execFileSync("taskkill", ["/pid", String(child.pid), "/f", "/t"], { windowsHide: true, stdio: "pipe" });
9
- return;
10
- } catch { /* fall through */ }
11
- }
12
- child.kill();
13
- }
14
-
15
- export interface SpawnStreamingOptions {
16
- cwd: string;
17
- env?: Record<string, string>;
18
- }
19
-
20
- /**
21
- * Spawn with shell interpretation for piped commands like "tail -f log | grep".
22
- * Returns the ChildProcess with stdout piped so the caller can read it directly
23
- * (contrast with spawnCommand which buffers). stdin is "pipe" (held open, not
24
- * written to): some long-running commands exit on stdin EOF. Agent CLIs like
25
- * `claude -p` conversely hang on an open stdin, which is why spawnCommand
26
- * defaults to "ignore".
27
- */
28
- export function spawnStreamingCommand(
29
- command: string,
30
- opts: SpawnStreamingOptions,
31
- ): ChildProcess {
32
- return crossSpawn(command, [], {
33
- cwd: opts.cwd,
34
- stdio: ["pipe", "pipe", "pipe"],
35
- shell: true,
36
- env: opts.env ? { ...process.env, ...opts.env } : undefined,
37
- windowsHide: true,
38
- });
39
- }
40
-
41
- export interface SpawnCommandOptions {
42
- cwd: string;
43
- env?: Record<string, string>;
44
- timeout?: number;
45
- /** Echo stdout to process.stdout (useful for journald logging). */
46
- echoStdout?: boolean;
47
- /** Resolve with output even on non-zero exit (instead of rejecting). */
48
- resolveOnFailure?: boolean;
49
- /** If provided, write this string to the process's stdin and then close the pipe. */
50
- stdin?: string;
51
- /** Called on each chunk of output (stdout + stderr combined). */
52
- onData?: (chunk: string) => void;
53
- }
54
-
55
- /**
56
- * cross-spawn resolves .cmd shims and escapes args on Windows without shell:true
57
- * (which mishandles special characters). stdin defaults to "ignore" because
58
- * tools like `claude -p` hang on an open stdin pipe; pass opts.stdin to write
59
- * a string and then close the pipe.
60
- */
61
- export interface SpawnCommandResult {
62
- output: string;
63
- exitCode: number | null;
64
- }
65
-
66
- export function spawnCommand(
67
- command: string,
68
- args: string[],
69
- opts: SpawnCommandOptions,
70
- ): Promise<SpawnCommandResult> {
71
- return new Promise<SpawnCommandResult>((resolve, reject) => {
72
- // cmd.exe can't handle literal newlines in arguments.
73
- const finalArgs = process.platform === "win32"
74
- ? args.map((a) => a.replace(/[\r\n]+/g, " "))
75
- : args;
76
- const truncate = (s: string, max = 100) => s.length > max ? s.slice(0, max) + "..." : s;
77
- const displayArgs = finalArgs.map((arg) => truncate(arg));
78
-
79
- console.log(`[spawn] ${command} ${displayArgs.join(" ")}`);
80
-
81
- const child = crossSpawn(command, finalArgs, {
82
- cwd: opts.cwd,
83
- stdio: [opts.stdin != null ? "pipe" : "ignore", "pipe", "pipe"],
84
- env: opts.env ? { ...process.env, ...opts.env } : undefined,
85
- windowsHide: true,
86
- });
87
-
88
- if (opts.stdin != null) {
89
- child.stdin!.end(opts.stdin);
90
- }
91
-
92
- const chunks: Buffer[] = [];
93
- child.stdout!.on("data", (d: Buffer) => {
94
- chunks.push(d);
95
- if (opts.echoStdout) process.stdout.write(d);
96
- if (opts.onData) opts.onData(d.toString("utf-8"));
97
- });
98
- child.stderr!.on("data", (d: Buffer) => {
99
- process.stderr.write(d);
100
- if (opts.onData) opts.onData(d.toString("utf-8"));
101
- });
102
-
103
- let timer: ReturnType<typeof setTimeout> | undefined;
104
- if (opts.timeout) {
105
- timer = setTimeout(() => {
106
- treeKill(child);
107
- reject(new Error("command timed out"));
108
- }, opts.timeout);
109
- }
110
-
111
- child.on("close", (code: number | null) => {
112
- if (timer) clearTimeout(timer);
113
- const output = Buffer.concat(chunks).toString("utf-8");
114
- if (code === 0 || opts.resolveOnFailure) resolve({ output, exitCode: code });
115
- else reject(new Error(`process exited with code ${code}`));
116
- });
117
-
118
- child.on("error", (err: Error) => {
119
- if (timer) clearTimeout(timer);
120
- reject(err);
121
- });
122
- });
123
- }
package/src/task.ts DELETED
@@ -1,343 +0,0 @@
1
- import * as fs from "fs";
2
- import * as path from "path";
3
- import { parse as parseYaml, stringify as stringifyYaml } from "yaml";
4
- import type { ParsedTask, TaskFrontmatter, TaskStatus, HistoryEntry, ConversationMessage } from "./types.js";
5
-
6
- export function parseTaskFile(taskDir: string): ParsedTask {
7
- const filePath = path.join(taskDir, "TASK.md");
8
-
9
- if (!fs.existsSync(filePath)) {
10
- throw new Error(`TASK.md not found at: ${filePath}`);
11
- }
12
-
13
- const content = fs.readFileSync(filePath, "utf-8");
14
- return parseTaskContent(content);
15
- }
16
-
17
- export function parseTaskContent(content: string): ParsedTask {
18
- const fmRegex = /^---\r?\n([\s\S]*?)\r?\n---\r?\n?([\s\S]*)$/;
19
- const match = content.match(fmRegex);
20
-
21
- if (!match) {
22
- throw new Error("TASK.md is missing valid YAML frontmatter delimiters (---)");
23
- }
24
-
25
- const frontmatter = parseYaml(match[1]) as TaskFrontmatter;
26
-
27
- if (!frontmatter.id) {
28
- throw new Error("TASK.md frontmatter must include at least: id");
29
- }
30
-
31
- frontmatter.name ??= frontmatter.user_prompt?.slice(0, 60) ?? "";
32
- frontmatter.agent ??= "claude";
33
- frontmatter.schedule_enabled ??= true;
34
-
35
- return { frontmatter };
36
- }
37
-
38
- export function writeTaskFile(taskDir: string, task: ParsedTask): void {
39
- fs.mkdirSync(taskDir, { recursive: true });
40
-
41
- const yamlStr = stringifyYaml(task.frontmatter).trim();
42
- const content = `---\n${yamlStr}\n---\n`;
43
-
44
- const filePath = path.join(taskDir, "TASK.md");
45
- fs.writeFileSync(filePath, content, "utf-8");
46
- }
47
-
48
- export function appendTaskList(projectRoot: string, taskId: string): void {
49
- const listPath = path.join(projectRoot, "tasks.jsonl");
50
- fs.appendFileSync(listPath, JSON.stringify({ task_id: taskId }) + "\n", "utf-8");
51
- }
52
-
53
- export function isTaskInList(projectRoot: string, taskId: string): boolean {
54
- const listPath = path.join(projectRoot, "tasks.jsonl");
55
- if (!fs.existsSync(listPath)) return false;
56
-
57
- const lines = fs.readFileSync(listPath, "utf-8").split("\n").filter(Boolean);
58
- for (const line of lines) {
59
- try {
60
- if ((JSON.parse(line) as { task_id: string }).task_id === taskId) return true;
61
- } catch { /* skip malformed */ }
62
- }
63
- return false;
64
- }
65
-
66
- export function removeFromTaskList(projectRoot: string, taskId: string): boolean {
67
- const listPath = path.join(projectRoot, "tasks.jsonl");
68
- if (!fs.existsSync(listPath)) return false;
69
-
70
- const lines = fs.readFileSync(listPath, "utf-8").split("\n").filter(Boolean);
71
- let found = false;
72
- const remaining: string[] = [];
73
-
74
- for (const line of lines) {
75
- try {
76
- const entry = JSON.parse(line) as { task_id: string };
77
- if (entry.task_id === taskId) {
78
- found = true;
79
- continue;
80
- }
81
- } catch { /* keep malformed lines */ }
82
- remaining.push(line);
83
- }
84
-
85
- if (!found) return false;
86
- fs.writeFileSync(listPath, remaining.length > 0 ? remaining.join("\n") + "\n" : "", "utf-8");
87
- return true;
88
- }
89
-
90
- export function listTasks(projectRoot: string): ParsedTask[] {
91
- const listPath = path.join(projectRoot, "tasks.jsonl");
92
- if (!fs.existsSync(listPath)) return [];
93
-
94
- const lines = fs.readFileSync(listPath, "utf-8").split("\n").filter(Boolean);
95
- const tasks: ParsedTask[] = [];
96
-
97
- for (const line of lines) {
98
- let taskId: string;
99
- try {
100
- taskId = (JSON.parse(line) as { task_id: string }).task_id;
101
- } catch { continue; }
102
-
103
- const taskDir = getTaskDir(projectRoot, taskId);
104
- try {
105
- tasks.push(parseTaskFile(taskDir));
106
- } catch (err) {
107
- console.error(`Warning: failed to parse task ${taskId}: ${err}`);
108
- }
109
- }
110
-
111
- return tasks.reverse();
112
- }
113
-
114
- export function getTaskDir(projectRoot: string, taskId: string): string {
115
- return path.join(projectRoot, "tasks", taskId);
116
- }
117
-
118
- export function writeTaskStatus(taskDir: string, status: TaskStatus): void {
119
- const filePath = path.join(taskDir, "status.json");
120
- fs.writeFileSync(filePath, JSON.stringify(status), "utf-8");
121
- }
122
-
123
- export function readTaskStatus(taskDir: string): TaskStatus | undefined {
124
- const filePath = path.join(taskDir, "status.json");
125
- try {
126
- return JSON.parse(fs.readFileSync(filePath, "utf-8")) as TaskStatus;
127
- } catch {
128
- return undefined;
129
- }
130
- }
131
-
132
- export interface FollowupStatus {
133
- pid: number;
134
- spawned_at: number;
135
- }
136
-
137
- export function writeFollowupStatus(runDir: string, status: FollowupStatus): void {
138
- fs.writeFileSync(path.join(runDir, "followup.json"), JSON.stringify(status), "utf-8");
139
- }
140
-
141
- export function readFollowupStatus(runDir: string): FollowupStatus | undefined {
142
- try {
143
- return JSON.parse(fs.readFileSync(path.join(runDir, "followup.json"), "utf-8")) as FollowupStatus;
144
- } catch {
145
- return undefined;
146
- }
147
- }
148
-
149
- export function deleteFollowupStatus(runDir: string): void {
150
- try { fs.unlinkSync(path.join(runDir, "followup.json")); } catch { /* ignore */ }
151
- }
152
-
153
- /** Returns the run ID (timestamp string used as directory name). */
154
- export function createRunDir(
155
- taskDir: string,
156
- taskName: string,
157
- startTime: number,
158
- agent?: string,
159
- ): string {
160
- const runId = String(startTime);
161
- const runDir = path.join(taskDir, runId);
162
- fs.mkdirSync(runDir, { recursive: true });
163
- const agentLine = agent ? `\nagent: ${agent}` : "";
164
- const content = `---\ntask_name: ${taskName}${agentLine}\n---\n\n`;
165
- fs.writeFileSync(path.join(runDir, "TASKRUN.md"), content, "utf-8");
166
- return runId;
167
- }
168
-
169
- export function getRunDir(taskDir: string, runId: string): string {
170
- return path.join(taskDir, runId);
171
- }
172
-
173
- export function appendRunMessage(
174
- taskDir: string,
175
- runId: string,
176
- msg: ConversationMessage,
177
- ): void {
178
- const attrs = [`role="${msg.role}"`, `time="${msg.time}"`];
179
- if (msg.type) attrs.push(`type="${msg.type}"`);
180
- if (msg.attachments?.length) attrs.push(`attachments="${msg.attachments.join(",")}"`);
181
-
182
- const delimiter = `<!-- palmier:message ${attrs.join(" ")} -->`;
183
- const entry = `${delimiter}\n\n${msg.content}\n\n`;
184
- fs.appendFileSync(path.join(taskDir, runId, "TASKRUN.md"), entry, "utf-8");
185
- }
186
-
187
- export function beginStreamingMessage(
188
- taskDir: string,
189
- runId: string,
190
- time: number,
191
- ): StreamingMessageWriter {
192
- const filePath = path.join(taskDir, runId, "TASKRUN.md");
193
- const delimiter = `<!-- palmier:message role="assistant" time="${time}" -->`;
194
- fs.appendFileSync(filePath, `${delimiter}\n\n`, "utf-8");
195
- return new StreamingMessageWriter(filePath);
196
- }
197
-
198
- export class StreamingMessageWriter {
199
- constructor(private filePath: string) {}
200
-
201
- /** Append a chunk of content to the current message. */
202
- write(chunk: string): void {
203
- fs.appendFileSync(this.filePath, chunk, "utf-8");
204
- }
205
-
206
- /** Finalize the message. If attachments are provided, rewrites the last assistant delimiter to include them. */
207
- end(attachments?: string[]): void {
208
- fs.appendFileSync(this.filePath, "\n\n", "utf-8");
209
- if (attachments?.length) {
210
- const raw = fs.readFileSync(this.filePath, "utf-8");
211
- // spliceUserMessage may have created a newer assistant delimiter.
212
- const pattern = /<!-- palmier:message role="assistant" time="\d+" -->/g;
213
- let lastMatch: RegExpExecArray | null = null;
214
- let m;
215
- while ((m = pattern.exec(raw)) !== null) lastMatch = m;
216
- if (lastMatch) {
217
- const before = raw.slice(0, lastMatch.index);
218
- const after = raw.slice(lastMatch.index + lastMatch[0].length);
219
- const updated = before + `${lastMatch[0].slice(0, -4)} attachments="${attachments.join(",")}" -->` + after;
220
- fs.writeFileSync(this.filePath, updated, "utf-8");
221
- }
222
- }
223
- }
224
- }
225
-
226
- /**
227
- * Splice a user message into a running assistant stream: close the current
228
- * assistant block, write the user message, open a new assistant block. Direct
229
- * appends only, so an existing StreamingMessageWriter keeps working — its
230
- * subsequent chunks land in the new block.
231
- */
232
- export function spliceUserMessage(
233
- taskDir: string,
234
- runId: string,
235
- userMsg: ConversationMessage,
236
- /** Optional text to append to the current assistant block before ending it. */
237
- assistantAppend?: string,
238
- ): void {
239
- const filePath = path.join(taskDir, runId, "TASKRUN.md");
240
- if (assistantAppend) {
241
- fs.appendFileSync(filePath, assistantAppend, "utf-8");
242
- }
243
- fs.appendFileSync(filePath, "\n\n", "utf-8");
244
- appendRunMessage(taskDir, runId, userMsg);
245
- const delimiter = `<!-- palmier:message role="assistant" time="${Date.now()}" -->`;
246
- fs.appendFileSync(filePath, `${delimiter}\n\n`, "utf-8");
247
- }
248
-
249
- export function readRunMessages(taskDir: string, runId: string): ConversationMessage[] {
250
- const raw = fs.readFileSync(path.join(taskDir, runId, "TASKRUN.md"), "utf-8");
251
- const fmMatch = raw.match(/^---\n[\s\S]*?\n---\n([\s\S]*)$/);
252
- if (!fmMatch) return [];
253
-
254
- const body = fmMatch[1];
255
- const delimiterRegex = /<!-- palmier:message\s+(.*?)\s*-->/g;
256
- const matches = [...body.matchAll(delimiterRegex)];
257
- if (matches.length === 0) return [];
258
-
259
- const messages: ConversationMessage[] = [];
260
- for (let i = 0; i < matches.length; i++) {
261
- const match = matches[i];
262
- const attrs = match[1];
263
- const start = match.index! + match[0].length;
264
- const end = i + 1 < matches.length ? matches[i + 1].index! : body.length;
265
- const content = body.slice(start, end).trim();
266
-
267
- const roleAttr = attrs.match(/role="([^"]*)"/)?.[1] ?? "assistant";
268
- const timeAttr = attrs.match(/time="([^"]*)"/)?.[1] ?? "0";
269
- const typeAttr = attrs.match(/type="([^"]*)"/)?.[1];
270
- const attachmentsAttr = attrs.match(/attachments="([^"]*)"/)?.[1];
271
-
272
- messages.push({
273
- role: roleAttr as ConversationMessage["role"],
274
- time: Number(timeAttr),
275
- content,
276
- ...(typeAttr ? { type: typeAttr as ConversationMessage["type"] } : {}),
277
- ...(attachmentsAttr ? { attachments: attachmentsAttr.split(",").map((f) => f.trim()).filter(Boolean) } : {}),
278
- });
279
- }
280
- return messages;
281
- }
282
-
283
- export function appendHistory(projectRoot: string, entry: HistoryEntry): void {
284
- const historyPath = path.join(projectRoot, "history.jsonl");
285
- fs.appendFileSync(historyPath, JSON.stringify(entry) + "\n", "utf-8");
286
- }
287
-
288
- export function deleteHistoryEntry(
289
- projectRoot: string,
290
- taskId: string,
291
- runId: string,
292
- ): boolean {
293
- const historyPath = path.join(projectRoot, "history.jsonl");
294
- if (!fs.existsSync(historyPath)) return false;
295
-
296
- const lines = fs.readFileSync(historyPath, "utf-8").split("\n").filter(Boolean);
297
- let found = false;
298
- const remaining: string[] = [];
299
-
300
- for (const line of lines) {
301
- try {
302
- const entry = JSON.parse(line) as HistoryEntry;
303
- if (entry.task_id === taskId && entry.run_id === runId) {
304
- found = true;
305
- continue;
306
- }
307
- } catch { /* keep malformed lines */ }
308
- remaining.push(line);
309
- }
310
-
311
- if (!found) return false;
312
-
313
- fs.writeFileSync(historyPath, remaining.length > 0 ? remaining.join("\n") + "\n" : "", "utf-8");
314
-
315
- const runDir = path.join(projectRoot, "tasks", taskId, runId);
316
- if (fs.existsSync(runDir)) {
317
- fs.rmSync(runDir, { recursive: true, force: true });
318
- }
319
-
320
- return true;
321
- }
322
-
323
- /** Returns entries most-recent-first. */
324
- export function readHistory(
325
- projectRoot: string,
326
- opts: { offset?: number; limit?: number; task_id?: string },
327
- ): { entries: HistoryEntry[]; total: number } {
328
- const historyPath = path.join(projectRoot, "history.jsonl");
329
- if (!fs.existsSync(historyPath)) return { entries: [], total: 0 };
330
-
331
- const lines = fs.readFileSync(historyPath, "utf-8").split("\n").filter(Boolean);
332
- let all: HistoryEntry[] = [];
333
- for (const line of lines) {
334
- try { all.push(JSON.parse(line)); } catch { /* skip malformed */ }
335
- }
336
- all.reverse();
337
- if (opts.task_id) {
338
- all = all.filter((e) => e.task_id === opts.task_id);
339
- }
340
- const offset = opts.offset ?? 0;
341
- const limit = opts.limit ?? 10;
342
- return { entries: all.slice(offset, offset + limit), total: all.length };
343
- }