palmier 0.6.0 → 0.6.2

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 (110) hide show
  1. package/.github/workflows/publish.yml +15 -2
  2. package/CLAUDE.md +2 -2
  3. package/DISCLAIMER.md +36 -0
  4. package/README.md +76 -87
  5. package/dist/agents/agent-instructions.md +1 -1
  6. package/dist/agents/agent.d.ts +2 -0
  7. package/dist/agents/agent.js +21 -0
  8. package/dist/agents/aider.d.ts +9 -0
  9. package/dist/agents/aider.js +32 -0
  10. package/dist/agents/cursor.d.ts +9 -0
  11. package/dist/agents/cursor.js +35 -0
  12. package/dist/agents/deepagents.d.ts +9 -0
  13. package/dist/agents/deepagents.js +35 -0
  14. package/dist/agents/droid.d.ts +9 -0
  15. package/dist/agents/droid.js +32 -0
  16. package/dist/agents/goose.d.ts +9 -0
  17. package/dist/agents/goose.js +32 -0
  18. package/dist/agents/opencode.d.ts +9 -0
  19. package/dist/agents/opencode.js +35 -0
  20. package/dist/agents/openhands.d.ts +9 -0
  21. package/dist/agents/openhands.js +35 -0
  22. package/dist/commands/pair.d.ts +1 -1
  23. package/dist/commands/pair.js +1 -1
  24. package/dist/commands/run.js +2 -2
  25. package/dist/pwa/apple-touch-icon.png +0 -0
  26. package/dist/pwa/assets/index-ByhOhTz1.js +118 -0
  27. package/dist/pwa/assets/index-_AmC1Rkn.css +1 -0
  28. package/dist/pwa/assets/plus-jakarta-sans-latin-ext-wght-normal-DmpS2jIq.woff2 +0 -0
  29. package/dist/pwa/assets/plus-jakarta-sans-latin-wght-normal-eXO_dkmS.woff2 +0 -0
  30. package/dist/pwa/assets/plus-jakarta-sans-vietnamese-wght-normal-qRpaaN48.woff2 +0 -0
  31. package/dist/pwa/favicon.ico +0 -0
  32. package/dist/pwa/index.html +17 -0
  33. package/dist/pwa/manifest.webmanifest +1 -0
  34. package/dist/pwa/pwa-192x192.png +0 -0
  35. package/dist/pwa/pwa-512x512.png +0 -0
  36. package/dist/pwa/registerSW.js +1 -0
  37. package/dist/pwa/service-worker.js +2 -0
  38. package/dist/rpc-handler.d.ts +4 -0
  39. package/dist/rpc-handler.js +5 -4
  40. package/dist/transports/http-transport.js +29 -41
  41. package/package.json +2 -2
  42. package/palmier-server/.github/workflows/ci.yml +21 -0
  43. package/palmier-server/.github/workflows/deploy.yml +38 -0
  44. package/palmier-server/CLAUDE.md +13 -0
  45. package/palmier-server/PRODUCTION.md +355 -0
  46. package/palmier-server/README.md +187 -0
  47. package/palmier-server/nats.conf +15 -0
  48. package/palmier-server/package.json +8 -0
  49. package/palmier-server/pnpm-lock.yaml +6597 -0
  50. package/palmier-server/pnpm-workspace.yaml +3 -0
  51. package/palmier-server/pwa/index.html +16 -0
  52. package/palmier-server/pwa/logo/logo-prompt.md +28 -0
  53. package/palmier-server/pwa/logo/logo_20260330.png +0 -0
  54. package/palmier-server/pwa/package.json +30 -0
  55. package/palmier-server/pwa/public/apple-touch-icon.png +0 -0
  56. package/palmier-server/pwa/public/favicon.ico +0 -0
  57. package/palmier-server/pwa/public/pwa-192x192.png +0 -0
  58. package/palmier-server/pwa/public/pwa-512x512.png +0 -0
  59. package/palmier-server/pwa/src/App.css +2387 -0
  60. package/palmier-server/pwa/src/App.tsx +21 -0
  61. package/palmier-server/pwa/src/agentLabels.ts +11 -0
  62. package/palmier-server/pwa/src/api.ts +61 -0
  63. package/palmier-server/pwa/src/components/HostMenu.tsx +289 -0
  64. package/palmier-server/pwa/src/components/PlanDialog.tsx +41 -0
  65. package/palmier-server/pwa/src/components/RunDetailView.tsx +293 -0
  66. package/palmier-server/pwa/src/components/RunsView.tsx +254 -0
  67. package/palmier-server/pwa/src/components/TabBar.tsx +31 -0
  68. package/palmier-server/pwa/src/components/TaskCard.tsx +213 -0
  69. package/palmier-server/pwa/src/components/TaskForm.tsx +580 -0
  70. package/palmier-server/pwa/src/components/TaskListView.tsx +415 -0
  71. package/palmier-server/pwa/src/constants.ts +2 -0
  72. package/palmier-server/pwa/src/contexts/HostConnectionContext.tsx +313 -0
  73. package/palmier-server/pwa/src/contexts/HostStoreContext.tsx +135 -0
  74. package/palmier-server/pwa/src/formatTime.ts +10 -0
  75. package/palmier-server/pwa/src/hooks/useBackClose.ts +75 -0
  76. package/palmier-server/pwa/src/hooks/useMediaQuery.ts +17 -0
  77. package/palmier-server/pwa/src/hooks/usePushSubscription.ts +75 -0
  78. package/palmier-server/pwa/src/main.tsx +14 -0
  79. package/palmier-server/pwa/src/pages/Dashboard.tsx +223 -0
  80. package/palmier-server/pwa/src/pages/PairHost.tsx +178 -0
  81. package/palmier-server/pwa/src/service-worker.ts +139 -0
  82. package/palmier-server/pwa/src/types.ts +79 -0
  83. package/palmier-server/pwa/src/vite-env.d.ts +11 -0
  84. package/palmier-server/pwa/tsconfig.json +21 -0
  85. package/palmier-server/pwa/tsconfig.node.json +19 -0
  86. package/palmier-server/pwa/vite.config.ts +47 -0
  87. package/palmier-server/server/.env.example +16 -0
  88. package/palmier-server/server/package.json +33 -0
  89. package/palmier-server/server/src/db.ts +34 -0
  90. package/palmier-server/server/src/index.ts +219 -0
  91. package/palmier-server/server/src/nats.ts +25 -0
  92. package/palmier-server/server/src/push.ts +68 -0
  93. package/palmier-server/server/src/routes/hosts.ts +45 -0
  94. package/palmier-server/server/src/routes/push.ts +100 -0
  95. package/palmier-server/server/tsconfig.json +20 -0
  96. package/palmier-server/spec.md +415 -0
  97. package/src/agents/agent-instructions.md +1 -1
  98. package/src/agents/agent.ts +23 -0
  99. package/src/agents/aider.ts +37 -0
  100. package/src/agents/cursor.ts +38 -0
  101. package/src/agents/deepagents.ts +38 -0
  102. package/src/agents/droid.ts +37 -0
  103. package/src/agents/goose.ts +35 -0
  104. package/src/agents/opencode.ts +38 -0
  105. package/src/agents/openhands.ts +38 -0
  106. package/src/commands/pair.ts +1 -1
  107. package/src/commands/run.ts +2 -2
  108. package/src/rpc-handler.ts +5 -4
  109. package/src/transports/http-transport.ts +31 -43
  110. package/test/result-state.test.ts +110 -0
@@ -0,0 +1,21 @@
1
+ import { Routes, Route } from "react-router-dom";
2
+ import { HostStoreProvider } from "./contexts/HostStoreContext";
3
+ import { HostConnectionProvider } from "./contexts/HostConnectionContext";
4
+ import Dashboard from "./pages/Dashboard";
5
+ import PairHost from "./pages/PairHost";
6
+
7
+ export default function App() {
8
+ return (
9
+ <HostStoreProvider>
10
+ <HostConnectionProvider>
11
+ <Routes>
12
+ <Route path="/" element={<Dashboard />} />
13
+ <Route path="/runs" element={<Dashboard />} />
14
+ <Route path="/runs/:taskId" element={<Dashboard />} />
15
+ <Route path="/runs/:taskId/:runId" element={<Dashboard />} />
16
+ <Route path="/pair" element={<PairHost />} />
17
+ </Routes>
18
+ </HostConnectionProvider>
19
+ </HostStoreProvider>
20
+ );
21
+ }
@@ -0,0 +1,11 @@
1
+ import type { AgentInfo } from "./types";
2
+
3
+ const labels: Record<string, string> = {};
4
+
5
+ export function setAgentLabels(agents: AgentInfo[]): void {
6
+ for (const a of agents) labels[a.key] = a.label;
7
+ }
8
+
9
+ export function getAgentLabel(key: string): string {
10
+ return labels[key] ?? key;
11
+ }
@@ -0,0 +1,61 @@
1
+ async function request<T>(
2
+ method: string,
3
+ path: string,
4
+ body?: unknown,
5
+ token?: string
6
+ ): Promise<T> {
7
+ console.log(`[API] ${method} ${path}`);
8
+ const headers: Record<string, string> = {
9
+ "Content-Type": "application/json",
10
+ };
11
+ if (token) {
12
+ headers["Authorization"] = `Bearer ${token}`;
13
+ }
14
+ const res = await fetch(path, {
15
+ method,
16
+ headers,
17
+ body: body != null ? JSON.stringify(body) : undefined,
18
+ });
19
+ if (!res.ok) {
20
+ const text = await res.text();
21
+ let message: string;
22
+ try {
23
+ const json = JSON.parse(text);
24
+ message = json.error ?? json.message ?? text;
25
+ } catch {
26
+ message = text;
27
+ }
28
+ console.error(`[API] ${method} ${path} failed:`, res.status, message);
29
+ throw new Error(message || `Request failed with status ${res.status}`);
30
+ }
31
+ console.log(`[API] ${method} ${path} ->`, res.status);
32
+ return res.json() as Promise<T>;
33
+ }
34
+
35
+ export function apiPost<T = unknown>(
36
+ path: string,
37
+ body: unknown,
38
+ token?: string
39
+ ): Promise<T> {
40
+ return request<T>("POST", path, body, token);
41
+ }
42
+
43
+ export function apiGet<T = unknown>(path: string, token?: string): Promise<T> {
44
+ return request<T>("GET", path, undefined, token);
45
+ }
46
+
47
+ export function apiPatch<T = unknown>(
48
+ path: string,
49
+ body: unknown,
50
+ token?: string
51
+ ): Promise<T> {
52
+ return request<T>("PATCH", path, body, token);
53
+ }
54
+
55
+ export function apiDelete<T = unknown>(
56
+ path: string,
57
+ body?: unknown,
58
+ token?: string
59
+ ): Promise<T> {
60
+ return request<T>("DELETE", path, body, token);
61
+ }
@@ -0,0 +1,289 @@
1
+ import { useState, useEffect, useRef, useCallback } from "react";
2
+ import { createPortal } from "react-dom";
3
+ import { useNavigate } from "react-router-dom";
4
+ import { useHostStore } from "../contexts/HostStoreContext";
5
+ import { useMediaQuery } from "../hooks/useMediaQuery";
6
+
7
+ /** LAN mode: PWA is served by palmier serve (marker injected into HTML). */
8
+ const isLanMode = !!(window as any).__PALMIER_SERVE__;
9
+
10
+ interface HostMenuProps {
11
+ daemonVersion?: string | null;
12
+ }
13
+
14
+ export default function HostMenu({ daemonVersion }: HostMenuProps) {
15
+ const { pairedHosts, activeHostId, setActiveHostId, removePairedHost, renamePairedHost } = useHostStore();
16
+ const navigate = useNavigate();
17
+ const isDesktop = useMediaQuery("(min-width: 768px)");
18
+
19
+ const [visible, setVisible] = useState(false);
20
+ const [closing, setClosing] = useState(false);
21
+ const [renamingId, setRenamingId] = useState<string | null>(null);
22
+ const [renameValue, setRenameValue] = useState("");
23
+ const [confirmingDeleteId, setConfirmingDeleteId] = useState<string | null>(null);
24
+ const drawerRef = useRef<HTMLDivElement>(null);
25
+ const renameInputRef = useRef<HTMLInputElement>(null);
26
+
27
+ // In LAN mode, there's only one host — no picker/pairing needed
28
+
29
+ const close = useCallback(() => {
30
+ setClosing(true);
31
+ }, []);
32
+
33
+ function handleAnimationEnd() {
34
+ if (closing) {
35
+ setVisible(false);
36
+ setClosing(false);
37
+ }
38
+ }
39
+
40
+ function openDrawer() {
41
+ setClosing(false);
42
+ setVisible(true);
43
+ }
44
+
45
+ function startRename(hostId: string, currentName: string) {
46
+ setRenamingId(hostId);
47
+ setRenameValue(currentName);
48
+ }
49
+
50
+ function submitRename() {
51
+ if (renamingId && renameValue.trim()) {
52
+ renamePairedHost(renamingId, renameValue.trim());
53
+ }
54
+ setRenamingId(null);
55
+ setRenameValue("");
56
+ }
57
+
58
+ function cancelRename() {
59
+ setRenamingId(null);
60
+ setRenameValue("");
61
+ }
62
+
63
+ useEffect(() => {
64
+ if (renamingId && renameInputRef.current) {
65
+ renameInputRef.current.focus();
66
+ renameInputRef.current.select();
67
+ }
68
+ }, [renamingId]);
69
+
70
+ useEffect(() => {
71
+ if (!visible || closing) return;
72
+ function handleKey(e: KeyboardEvent) {
73
+ if (e.key === "Escape") close();
74
+ }
75
+ document.addEventListener("keydown", handleKey);
76
+ return () => {
77
+ document.removeEventListener("keydown", handleKey);
78
+ };
79
+ }, [visible, closing, close]);
80
+
81
+ const drawerContent = (
82
+ <>
83
+ <div className="drawer-header">
84
+ <span className="drawer-title">Palmier</span>
85
+ {!isDesktop && (
86
+ <button
87
+ className="drawer-close-btn"
88
+ onClick={close}
89
+ aria-label="Close menu"
90
+ >
91
+ <svg width="16" height="16" viewBox="0 0 16 16" fill="none">
92
+ <path d="M4 4L12 12M12 4L4 12" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" />
93
+ </svg>
94
+ </button>
95
+ )}
96
+ </div>
97
+
98
+ {!isLanMode && pairedHosts.length > 0 && (
99
+ <div className="drawer-section">
100
+ <h3 className="drawer-section-label">Hosts</h3>
101
+ <div className="host-picker-inline">
102
+ <div className="host-picker-list" role="listbox">
103
+ {pairedHosts.map((host) => {
104
+ const isActive = host.hostId === activeHostId;
105
+ const isRenaming = renamingId === host.hostId;
106
+ const displayName = host.name || host.hostId.slice(0, 8);
107
+ return (
108
+ <div key={host.hostId} className="host-picker-item-wrapper">
109
+ <div
110
+ className={`host-picker-item ${isActive ? "host-picker-item-active" : ""}`}
111
+ onClick={() => {
112
+ if (isRenaming) return;
113
+ if (!isActive) { setActiveHostId(host.hostId); if (!isDesktop) close(); }
114
+ }}
115
+ role="option"
116
+ aria-selected={isActive}
117
+ >
118
+ {isRenaming ? (
119
+ <input
120
+ ref={renameInputRef}
121
+ className="form-input host-picker-rename-input"
122
+ type="text"
123
+ value={renameValue}
124
+ onChange={(e) => setRenameValue(e.target.value)}
125
+ onKeyDown={(e) => {
126
+ if (e.key === "Enter") submitRename();
127
+ if (e.key === "Escape") cancelRename();
128
+ }}
129
+ onBlur={submitRename}
130
+ onClick={(e) => e.stopPropagation()}
131
+ />
132
+ ) : (
133
+ <span className="host-picker-item-name">
134
+ {displayName}
135
+ </span>
136
+ )}
137
+ <div className="host-picker-item-actions">
138
+ {isActive && !isRenaming && (
139
+ <button
140
+ className="host-picker-edit-btn"
141
+ onClick={(e) => {
142
+ e.stopPropagation();
143
+ startRename(host.hostId, displayName);
144
+ }}
145
+ aria-label="Rename host"
146
+ >
147
+ <svg width="13" height="13" viewBox="0 0 13 13" fill="none">
148
+ <path
149
+ d="M9.5 1.5L11.5 3.5M1.5 11.5L2 9L9 2L11 4L4 11L1.5 11.5Z"
150
+ stroke="currentColor"
151
+ strokeWidth="1.2"
152
+ strokeLinecap="round"
153
+ strokeLinejoin="round"
154
+ />
155
+ </svg>
156
+ </button>
157
+ )}
158
+ {!isActive && (
159
+ <button
160
+ className="host-picker-delete-btn"
161
+ onClick={(e) => {
162
+ e.stopPropagation();
163
+ setConfirmingDeleteId(host.hostId);
164
+ }}
165
+ aria-label={`Unpair ${displayName}`}
166
+ >
167
+ <svg width="14" height="14" viewBox="0 0 14 14" fill="none">
168
+ <path
169
+ d="M4 5.5V11C4 11.2761 4.22386 11.5 4.5 11.5H9.5C9.77614 11.5 10 11.2761 10 11V5.5M3 4H11M5.5 4V3C5.5 2.72386 5.72386 2.5 6 2.5H8C8.27614 2.5 8.5 2.72386 8.5 3V4M6.5 6.5V9.5M7.5 6.5V9.5"
170
+ stroke="currentColor"
171
+ strokeWidth="1.2"
172
+ strokeLinecap="round"
173
+ strokeLinejoin="round"
174
+ />
175
+ </svg>
176
+ </button>
177
+ )}
178
+ </div>
179
+ </div>
180
+ </div>
181
+ );
182
+ })}
183
+ </div>
184
+ </div>
185
+ </div>
186
+ )}
187
+
188
+ {!isLanMode && (<>
189
+ <div className="drawer-divider" />
190
+ <div className="drawer-section">
191
+ <button
192
+ className="btn btn-primary btn-full"
193
+ onClick={() => { navigate("/pair"); if (!isDesktop) close(); }}
194
+ >
195
+ Pair New Host
196
+ </button>
197
+ </div>
198
+ </>)}
199
+
200
+ <div className="drawer-footer">
201
+ {daemonVersion && (
202
+ <div className="drawer-version">
203
+ Palmier v{daemonVersion}
204
+ </div>
205
+ )}
206
+ <div className="drawer-legal">
207
+ <a href="https://www.palmier.me/terms" target="_blank" rel="noopener noreferrer">Terms</a>
208
+ <span className="drawer-legal-sep">·</span>
209
+ <a href="https://www.palmier.me/privacy" target="_blank" rel="noopener noreferrer">Privacy</a>
210
+ </div>
211
+ </div>
212
+ </>
213
+ );
214
+
215
+ const deleteModal = confirmingDeleteId && createPortal(
216
+ <div className="confirm-modal-overlay" onClick={() => setConfirmingDeleteId(null)}>
217
+ <div className="confirm-modal" onClick={(e) => e.stopPropagation()}>
218
+ <h2 className="confirm-modal-title">Delete host?</h2>
219
+ <p className="confirm-modal-message">
220
+ "{pairedHosts.find((h) => h.hostId === confirmingDeleteId)?.name || confirmingDeleteId.slice(0, 8)}" will be unpaired from this device.
221
+ </p>
222
+ <div className="confirm-modal-actions">
223
+ <button
224
+ className="btn btn-secondary"
225
+ onClick={() => setConfirmingDeleteId(null)}
226
+ >
227
+ Cancel
228
+ </button>
229
+ <button
230
+ className="btn btn-danger"
231
+ onClick={() => {
232
+ removePairedHost(confirmingDeleteId);
233
+ setConfirmingDeleteId(null);
234
+ }}
235
+ >
236
+ Delete
237
+ </button>
238
+ </div>
239
+ </div>
240
+ </div>,
241
+ document.body
242
+ );
243
+
244
+ // Desktop: persistent inline sidebar
245
+ if (isDesktop) {
246
+ return (
247
+ <>
248
+ <div className="drawer-panel drawer-panel-desktop" ref={drawerRef}>
249
+ {drawerContent}
250
+ </div>
251
+ {deleteModal}
252
+ </>
253
+ );
254
+ }
255
+
256
+ // Mobile: hamburger + slide-out drawer
257
+ return (
258
+ <>
259
+ <button
260
+ className="hamburger-btn"
261
+ onClick={openDrawer}
262
+ aria-label="Open menu"
263
+ >
264
+ <svg width="20" height="20" viewBox="0 0 20 20" fill="none">
265
+ <path d="M3 5H17M3 10H17M3 15H17" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" />
266
+ </svg>
267
+ </button>
268
+
269
+ {visible && createPortal(
270
+ <div
271
+ className={`drawer-overlay ${closing ? "drawer-overlay-closing" : ""}`}
272
+ onClick={close}
273
+ onAnimationEnd={handleAnimationEnd}
274
+ >
275
+ <div
276
+ className={`drawer-panel ${closing ? "drawer-panel-closing" : ""}`}
277
+ ref={drawerRef}
278
+ onClick={(e) => e.stopPropagation()}
279
+ >
280
+ {drawerContent}
281
+ </div>
282
+ </div>,
283
+ document.body
284
+ )}
285
+
286
+ {deleteModal}
287
+ </>
288
+ );
289
+ }
@@ -0,0 +1,41 @@
1
+ import Markdown from "react-markdown";
2
+ import remarkGfm from "remark-gfm";
3
+ import type { RequiredPermission } from "../types";
4
+
5
+ interface PlanDialogProps {
6
+ body: string;
7
+ permissions?: RequiredPermission[];
8
+ }
9
+
10
+ export default function PlanDialog({ body, permissions }: PlanDialogProps) {
11
+ return (
12
+ <div className="plan-dialog">
13
+ <h2>Task Execution Plan</h2>
14
+ <div className="plan-dialog-scroll">
15
+ {body ? (
16
+ <div className="plan-preview"><Markdown remarkPlugins={[remarkGfm]}>{body}</Markdown></div>
17
+ ) : (
18
+ <p className="plan-empty">No execution plan generated for this task. Your task description will be used directly.</p>
19
+ )}
20
+ {permissions && permissions.length > 0 && (
21
+ <div className="permissions-section">
22
+ <h3>Granted Permissions</h3>
23
+ <ul className="permissions-list">
24
+ {permissions.map((p, i) => (
25
+ <li key={i} className="permission-item">
26
+ <span className="permission-tool">{p.name}</span>
27
+ <span className="permission-desc">{p.description}</span>
28
+ </li>
29
+ ))}
30
+ </ul>
31
+ </div>
32
+ )}
33
+ </div>
34
+ <div className="plan-dialog-actions">
35
+ <button className="btn btn-secondary" onClick={() => history.back()}>
36
+ Back
37
+ </button>
38
+ </div>
39
+ </div>
40
+ );
41
+ }