@parall/daemon 1.28.0 → 1.28.1
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.
- package/bundle/manifest.json +22 -0
- package/bundle/parall-claude-agent.js +5685 -0
- package/bundle/parall-codex-agent.js +6640 -0
- package/bundle/parall-daemon.js +2786 -0
- package/bundle/parall-openclaw-agent.js +220 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +277 -0
- package/dist/config.d.ts +2 -0
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +43 -2
- package/dist/index.js +12 -1
- package/dist/supervisor.d.ts +6 -0
- package/dist/supervisor.d.ts.map +1 -1
- package/dist/supervisor.js +80 -8
- package/package.json +16 -8
- package/src/config.ts +0 -146
- package/src/index.ts +0 -132
- package/src/runtimes.ts +0 -91
- package/src/supervisor.ts +0 -480
|
@@ -0,0 +1,2786 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// ts/sdk/dist/constants.js
|
|
4
|
+
var API_BASE = "/api/v1";
|
|
5
|
+
var WIKI_BASE = "/wiki/v1";
|
|
6
|
+
var ENDPOINTS = {
|
|
7
|
+
// Auth
|
|
8
|
+
AUTH_REGISTER: `${API_BASE}/auth/register`,
|
|
9
|
+
AUTH_LOGIN: `${API_BASE}/auth/login`,
|
|
10
|
+
AUTH_REFRESH: `${API_BASE}/auth/refresh`,
|
|
11
|
+
AUTH_LOGOUT: `${API_BASE}/auth/logout`,
|
|
12
|
+
AUTH_CHANGE_PASSWORD: `${API_BASE}/auth/change-password`,
|
|
13
|
+
AUTH_CHECK_EMAIL: `${API_BASE}/auth/check-email`,
|
|
14
|
+
AUTH_VERIFY_EMAIL: `${API_BASE}/auth/verify-email`,
|
|
15
|
+
AUTH_RESEND_CODE: `${API_BASE}/auth/resend-code`,
|
|
16
|
+
// Users
|
|
17
|
+
USERS_ME: `${API_BASE}/users/me`,
|
|
18
|
+
USER_AVATAR: `${API_BASE}/users/me/avatar`,
|
|
19
|
+
USER: (id) => `${API_BASE}/users/${id}`,
|
|
20
|
+
// WebSocket ticket
|
|
21
|
+
WS_TICKET: `${API_BASE}/ws/ticket`,
|
|
22
|
+
// Organizations (global)
|
|
23
|
+
ORGS: `${API_BASE}/orgs`,
|
|
24
|
+
// Org-scoped
|
|
25
|
+
ORG: (orgId) => `${API_BASE}/orgs/${orgId}`,
|
|
26
|
+
ORG_MEMBERS: (orgId) => `${API_BASE}/orgs/${orgId}/members`,
|
|
27
|
+
ORG_MEMBERS_ONLINE: (orgId) => `${API_BASE}/orgs/${orgId}/members/online`,
|
|
28
|
+
ORG_MEMBER: (orgId, userId) => `${API_BASE}/orgs/${orgId}/members/${userId}`,
|
|
29
|
+
ORG_MEMBER_CHATS: (orgId, memberId) => `${API_BASE}/orgs/${orgId}/members/${memberId}/chats`,
|
|
30
|
+
ORG_MEMBER_TASKS: (orgId, memberId) => `${API_BASE}/orgs/${orgId}/members/${memberId}/tasks`,
|
|
31
|
+
// Direct messages (org-scoped, atomic find-or-create + send)
|
|
32
|
+
DM: (orgId) => `${API_BASE}/orgs/${orgId}/dm`,
|
|
33
|
+
// Onboarding
|
|
34
|
+
SEED_ONBOARDING_DM: (orgId) => `${API_BASE}/orgs/${orgId}/seed-onboarding-dm`,
|
|
35
|
+
DISMISS_ONBOARDING: (orgId) => `${API_BASE}/orgs/${orgId}/dismiss-onboarding`,
|
|
36
|
+
// Chats (org-scoped)
|
|
37
|
+
CHATS: (orgId) => `${API_BASE}/orgs/${orgId}/chats`,
|
|
38
|
+
CHATS_DISCOVERABLE: (orgId) => `${API_BASE}/orgs/${orgId}/chats/discoverable`,
|
|
39
|
+
CHAT: (orgId, chatId) => `${API_BASE}/orgs/${orgId}/chats/${chatId}`,
|
|
40
|
+
CHAT_JOIN: (orgId, chatId) => `${API_BASE}/orgs/${orgId}/chats/${chatId}/join`,
|
|
41
|
+
CHAT_ARCHIVE: (orgId, chatId) => `${API_BASE}/orgs/${orgId}/chats/${chatId}/archive`,
|
|
42
|
+
CHAT_RESTORE: (orgId, chatId) => `${API_BASE}/orgs/${orgId}/chats/${chatId}/restore`,
|
|
43
|
+
CHAT_MEMBERS: (orgId, chatId) => `${API_BASE}/orgs/${orgId}/chats/${chatId}/members`,
|
|
44
|
+
CHAT_MEMBER: (orgId, chatId, userId) => `${API_BASE}/orgs/${orgId}/chats/${chatId}/members/${userId}`,
|
|
45
|
+
CHAT_TRANSFER_OWNERSHIP: (orgId, chatId) => `${API_BASE}/orgs/${orgId}/chats/${chatId}/transfer-ownership`,
|
|
46
|
+
CHAT_MESSAGES: (orgId, chatId) => `${API_BASE}/orgs/${orgId}/chats/${chatId}/messages`,
|
|
47
|
+
// Messages (global, by message ID)
|
|
48
|
+
MESSAGE: (id) => `${API_BASE}/messages/${id}`,
|
|
49
|
+
MESSAGE_PATCHES: (id) => `${API_BASE}/messages/${id}/patches`,
|
|
50
|
+
MESSAGE_REPLIES: (id) => `${API_BASE}/messages/${id}/replies`,
|
|
51
|
+
// Upload (org-scoped)
|
|
52
|
+
UPLOAD_PRESIGN: (orgId) => `${API_BASE}/orgs/${orgId}/upload/presign`,
|
|
53
|
+
UPLOAD_COMPLETE: (orgId) => `${API_BASE}/orgs/${orgId}/upload/complete`,
|
|
54
|
+
FILE: (id) => `${API_BASE}/files/${id}`,
|
|
55
|
+
// Approval requests (org-scoped)
|
|
56
|
+
APPROVAL_REQUESTS: (orgId) => `${API_BASE}/orgs/${orgId}/approval-requests`,
|
|
57
|
+
// Approvals (global)
|
|
58
|
+
APPROVAL: (id) => `${API_BASE}/approvals/${id}`,
|
|
59
|
+
APPROVAL_DECIDE: (id) => `${API_BASE}/approvals/${id}/decide`,
|
|
60
|
+
APPROVAL_CANCEL: (id) => `${API_BASE}/approvals/${id}/cancel`,
|
|
61
|
+
APPROVALS_PENDING: `${API_BASE}/approvals/pending`,
|
|
62
|
+
APPROVALS_ACTIONS: `${API_BASE}/approvals/actions`,
|
|
63
|
+
// Agents (org-scoped)
|
|
64
|
+
AGENTS: (orgId) => `${API_BASE}/orgs/${orgId}/agents`,
|
|
65
|
+
AGENT: (orgId, agentId) => `${API_BASE}/orgs/${orgId}/agents/${agentId}`,
|
|
66
|
+
AGENT_API_KEYS: (orgId, agentId) => `${API_BASE}/orgs/${orgId}/agents/${agentId}/api-keys`,
|
|
67
|
+
AGENT_API_KEY: (orgId, agentId, key) => `${API_BASE}/orgs/${orgId}/agents/${agentId}/api-keys/${key}`,
|
|
68
|
+
AGENT_AVATAR: (orgId, agentId) => `${API_BASE}/orgs/${orgId}/agents/${agentId}/avatar`,
|
|
69
|
+
AGENT_ACTIVITY: (orgId, agentId) => `${API_BASE}/orgs/${orgId}/agents/${agentId}/activity`,
|
|
70
|
+
AGENT_MONITOR: (orgId, agentId) => `${API_BASE}/orgs/${orgId}/agents/${agentId}/monitor`,
|
|
71
|
+
AGENT_ME: (orgId) => `${API_BASE}/orgs/${orgId}/agents/me`,
|
|
72
|
+
AGENT_SESSIONS: (orgId, agentId) => `${API_BASE}/orgs/${orgId}/agents/${agentId}/sessions`,
|
|
73
|
+
AGENT_SESSION: (orgId, agentId, sessionId) => `${API_BASE}/orgs/${orgId}/agents/${agentId}/sessions/${sessionId}`,
|
|
74
|
+
AGENT_SESSION_STEPS: (orgId, agentId, sessionId) => `${API_BASE}/orgs/${orgId}/agents/${agentId}/sessions/${sessionId}/steps`,
|
|
75
|
+
AGENT_SESSION_STEP: (orgId, agentId, sessionId, stepId) => `${API_BASE}/orgs/${orgId}/agents/${agentId}/sessions/${sessionId}/steps/${stepId}`,
|
|
76
|
+
AGENT_TASKS: (orgId, agentId) => `${API_BASE}/orgs/${orgId}/agents/${agentId}/tasks`,
|
|
77
|
+
AGENT_RUNTIME_AUTH: (orgId, agentId) => `${API_BASE}/orgs/${orgId}/agents/${agentId}/runtime-auth`,
|
|
78
|
+
AGENT_RUNTIME_AUTH_SESSIONS: (orgId, agentId) => `${API_BASE}/orgs/${orgId}/agents/${agentId}/runtime-auth-sessions`,
|
|
79
|
+
AGENT_RUNTIME_AUTH_SESSION_COMPLETE: (orgId, agentId, sessionId) => `${API_BASE}/orgs/${orgId}/agents/${agentId}/runtime-auth-sessions/${sessionId}/complete`,
|
|
80
|
+
AGENT_RUNTIME: (orgId, agentId) => `${API_BASE}/orgs/${orgId}/agents/${agentId}/runtime`,
|
|
81
|
+
AGENT_RUNTIME_UPGRADE: (orgId, agentId) => `${API_BASE}/orgs/${orgId}/agents/${agentId}/runtime/upgrade`,
|
|
82
|
+
AGENT_RUNTIME_AVAILABLE_TAGS: (orgId, agentId) => `${API_BASE}/orgs/${orgId}/agents/${agentId}/runtime/available-tags`,
|
|
83
|
+
AGENT_RUNTIME_RELEASE: (orgId, agentId, tag) => `${API_BASE}/orgs/${orgId}/agents/${agentId}/runtime/releases/${encodeURIComponent(tag)}`,
|
|
84
|
+
AGENT_PROVIDER_CONFIG: (orgId, agentId) => `${API_BASE}/orgs/${orgId}/agents/${agentId}/provider-config`,
|
|
85
|
+
// Machines (org-scoped)
|
|
86
|
+
MACHINES: (orgId) => `${API_BASE}/orgs/${orgId}/machines`,
|
|
87
|
+
MACHINE: (orgId, machineId) => `${API_BASE}/orgs/${orgId}/machines/${machineId}`,
|
|
88
|
+
MACHINE_START: (orgId, machineId) => `${API_BASE}/orgs/${orgId}/machines/${machineId}/start`,
|
|
89
|
+
MACHINE_STOP: (orgId, machineId) => `${API_BASE}/orgs/${orgId}/machines/${machineId}/stop`,
|
|
90
|
+
MACHINE_STATUS: (orgId, machineId) => `${API_BASE}/orgs/${orgId}/machines/${machineId}/status`,
|
|
91
|
+
MACHINE_LOGS: (orgId, machineId) => `${API_BASE}/orgs/${orgId}/machines/${machineId}/logs`,
|
|
92
|
+
MACHINE_SPEC: (orgId, machineId) => `${API_BASE}/orgs/${orgId}/machines/${machineId}/spec`,
|
|
93
|
+
MACHINE_RESTART: (orgId, machineId) => `${API_BASE}/orgs/${orgId}/machines/${machineId}/restart`,
|
|
94
|
+
MACHINE_RESTART_ALL: (orgId) => `${API_BASE}/orgs/${orgId}/machines/restart-all`,
|
|
95
|
+
// Daemon-mode Machine management (org-scoped, user auth).
|
|
96
|
+
// POST creates a new daemon-mode Machine (daemon_mode=true always).
|
|
97
|
+
// Attach/Detach bind an agent to/from a daemon Machine.
|
|
98
|
+
MACHINE_ATTACH_AGENT: (orgId, machineId, agentId) => `${API_BASE}/orgs/${orgId}/machines/${machineId}/agents/${agentId}`,
|
|
99
|
+
MACHINE_DETACH_AGENT: (orgId, machineId, agentId) => `${API_BASE}/orgs/${orgId}/machines/${machineId}/agents/${agentId}`,
|
|
100
|
+
MACHINE_RUNTIME_AUTH: (orgId, machineId) => `${API_BASE}/orgs/${orgId}/machines/${machineId}/runtime-auth`,
|
|
101
|
+
MACHINE_KEYS: (orgId, machineId) => `${API_BASE}/orgs/${orgId}/machines/${machineId}/keys`,
|
|
102
|
+
MACHINE_KEY: (orgId, machineId, keyId) => `${API_BASE}/orgs/${orgId}/machines/${machineId}/keys/${keyId}`,
|
|
103
|
+
// Machine self-control-plane (mck_-scoped). The bearer token implicitly
|
|
104
|
+
// identifies the Machine, so there is no `:mid` URL parameter — these are
|
|
105
|
+
// "self" routes called by the daemon for its own host.
|
|
106
|
+
MACHINES_ME: `${API_BASE}/machines/me`,
|
|
107
|
+
MACHINES_ME_AGENTS: `${API_BASE}/machines/me/agents`,
|
|
108
|
+
MACHINES_ME_HEALTH: `${API_BASE}/machines/me/health`,
|
|
109
|
+
MACHINES_ME_AGENT_LAUNCH_CREDENTIAL: (agentId) => `${API_BASE}/machines/me/agents/${agentId}/launch-credential`,
|
|
110
|
+
MACHINES_ME_WS_TICKET: `${API_BASE}/machines/me/ws/ticket`,
|
|
111
|
+
// Tasks (org-scoped)
|
|
112
|
+
TASKS: (orgId) => `${API_BASE}/orgs/${orgId}/tasks`,
|
|
113
|
+
TASK: (orgId, taskId) => `${API_BASE}/orgs/${orgId}/tasks/${taskId}`,
|
|
114
|
+
TASK_ARCHIVE: (orgId, taskId) => `${API_BASE}/orgs/${orgId}/tasks/${taskId}/archive`,
|
|
115
|
+
TASK_RESTORE: (orgId, taskId) => `${API_BASE}/orgs/${orgId}/tasks/${taskId}/restore`,
|
|
116
|
+
TASK_WATCH: (orgId, taskId) => `${API_BASE}/orgs/${orgId}/tasks/${taskId}/watch`,
|
|
117
|
+
TASK_WATCHERS: (orgId, taskId) => `${API_BASE}/orgs/${orgId}/tasks/${taskId}/watchers`,
|
|
118
|
+
TASK_SUBTASKS: (orgId, taskId) => `${API_BASE}/orgs/${orgId}/tasks/${taskId}/subtasks`,
|
|
119
|
+
TASK_RELATIONS: (orgId, taskId) => `${API_BASE}/orgs/${orgId}/tasks/${taskId}/relations`,
|
|
120
|
+
TASK_RELATION: (orgId, taskId, relId) => `${API_BASE}/orgs/${orgId}/tasks/${taskId}/relations/${relId}`,
|
|
121
|
+
TASK_RELATIONS_BY_TARGET: (orgId) => `${API_BASE}/orgs/${orgId}/task-relations`,
|
|
122
|
+
TASK_COMMENTS: (orgId, taskId) => `${API_BASE}/orgs/${orgId}/tasks/${taskId}/comments`,
|
|
123
|
+
TASK_COMMENT: (orgId, taskId, commentId) => `${API_BASE}/orgs/${orgId}/tasks/${taskId}/comments/${commentId}`,
|
|
124
|
+
TASK_ACTIVITIES: (orgId, taskId) => `${API_BASE}/orgs/${orgId}/tasks/${taskId}/activities`,
|
|
125
|
+
// Projects (org-scoped)
|
|
126
|
+
PROJECTS: (orgId) => `${API_BASE}/orgs/${orgId}/projects`,
|
|
127
|
+
PROJECT: (orgId, projectId) => `${API_BASE}/orgs/${orgId}/projects/${projectId}`,
|
|
128
|
+
// Schedules (org-scoped, platform time trigger primitive)
|
|
129
|
+
SCHEDULES: (orgId) => `${API_BASE}/orgs/${orgId}/schedules`,
|
|
130
|
+
SCHEDULE: (orgId, id) => `${API_BASE}/orgs/${orgId}/schedules/${id}`,
|
|
131
|
+
SCHEDULE_PAUSE: (orgId, id) => `${API_BASE}/orgs/${orgId}/schedules/${id}/pause`,
|
|
132
|
+
SCHEDULE_RESUME: (orgId, id) => `${API_BASE}/orgs/${orgId}/schedules/${id}/resume`,
|
|
133
|
+
SCHEDULE_CANCEL: (orgId, id) => `${API_BASE}/orgs/${orgId}/schedules/${id}/cancel`,
|
|
134
|
+
SCHEDULE_RUNS: (orgId, id) => `${API_BASE}/orgs/${orgId}/schedules/${id}/runs`,
|
|
135
|
+
SCHEDULE_RUN: (orgId, runId) => `${API_BASE}/orgs/${orgId}/schedule_runs/${runId}`,
|
|
136
|
+
// Invitations (org-scoped, admin)
|
|
137
|
+
ORG_INVITATIONS: (orgId) => `${API_BASE}/orgs/${orgId}/invitations`,
|
|
138
|
+
ORG_INVITATION: (orgId, invId) => `${API_BASE}/orgs/${orgId}/invitations/${invId}`,
|
|
139
|
+
ORG_INVITATION_RESEND: (orgId, invId) => `${API_BASE}/orgs/${orgId}/invitations/${invId}/resend`,
|
|
140
|
+
// Invitations (invitee)
|
|
141
|
+
MY_INVITATIONS: `${API_BASE}/invitations/pending`,
|
|
142
|
+
INVITATION_ACCEPT: (id) => `${API_BASE}/invitations/${id}/accept`,
|
|
143
|
+
INVITATION_DECLINE: (id) => `${API_BASE}/invitations/${id}/decline`,
|
|
144
|
+
INVITATION_BY_TOKEN: (token) => `${API_BASE}/invitations/by-token/${token}`,
|
|
145
|
+
// Wikis (org-scoped, served by wiki-service)
|
|
146
|
+
WIKIS: (orgId) => `${WIKI_BASE}/orgs/${orgId}/wikis`,
|
|
147
|
+
WIKI: (orgId, wikiId) => `${WIKI_BASE}/orgs/${orgId}/wikis/${wikiId}`,
|
|
148
|
+
WIKI_TREE: (orgId, wikiId) => `${WIKI_BASE}/orgs/${orgId}/wikis/${wikiId}/tree`,
|
|
149
|
+
WIKI_BLOB: (orgId, wikiId) => `${WIKI_BASE}/orgs/${orgId}/wikis/${wikiId}/blob`,
|
|
150
|
+
WIKI_NODE_SECTIONS: (orgId, wikiId) => `${WIKI_BASE}/orgs/${orgId}/wikis/${wikiId}/node-sections`,
|
|
151
|
+
WIKI_SEARCH: (orgId, wikiId) => `${WIKI_BASE}/orgs/${orgId}/wikis/${wikiId}/search`,
|
|
152
|
+
WIKI_PAGE_INDEX: (orgId, wikiId) => `${WIKI_BASE}/orgs/${orgId}/wikis/${wikiId}/page-index`,
|
|
153
|
+
WIKI_REFS: (orgId, wikiId) => `${WIKI_BASE}/orgs/${orgId}/wikis/${wikiId}/refs`,
|
|
154
|
+
WIKI_REFS_CHECK: (orgId, wikiId) => `${WIKI_BASE}/orgs/${orgId}/wikis/${wikiId}/refs/check`,
|
|
155
|
+
WIKI_ANCHOR_STATUS: (orgId, wikiId) => `${WIKI_BASE}/orgs/${orgId}/wikis/${wikiId}/anchor-status`,
|
|
156
|
+
WIKI_CHANGESETS: (orgId, wikiId) => `${WIKI_BASE}/orgs/${orgId}/wikis/${wikiId}/changesets`,
|
|
157
|
+
WIKI_CHANGESET: (orgId, wikiId, changesetId) => `${WIKI_BASE}/orgs/${orgId}/wikis/${wikiId}/changesets/${changesetId}`,
|
|
158
|
+
WIKI_CHANGESET_DIFF: (orgId, wikiId, changesetId) => `${WIKI_BASE}/orgs/${orgId}/wikis/${wikiId}/changesets/${changesetId}/diff`,
|
|
159
|
+
WIKI_CHANGESET_MERGE: (orgId, wikiId, changesetId) => `${WIKI_BASE}/orgs/${orgId}/wikis/${wikiId}/changesets/${changesetId}/merge`,
|
|
160
|
+
WIKI_CHANGESET_CLOSE: (orgId, wikiId, changesetId) => `${WIKI_BASE}/orgs/${orgId}/wikis/${wikiId}/changesets/${changesetId}/close`,
|
|
161
|
+
WIKI_CHANGESET_FILES: (orgId, wikiId, changesetId) => `${WIKI_BASE}/orgs/${orgId}/wikis/${wikiId}/changesets/${changesetId}/files`,
|
|
162
|
+
// Wiki Binary Upload / Delete (Phase 2 of wiki-file-storage-design)
|
|
163
|
+
WIKI_UPLOADS: (orgId, wikiId) => `${WIKI_BASE}/orgs/${orgId}/wikis/${wikiId}/uploads`,
|
|
164
|
+
WIKI_FILES: (orgId, wikiId) => `${WIKI_BASE}/orgs/${orgId}/wikis/${wikiId}/files`,
|
|
165
|
+
WIKI_FILE_PREVIEW_URL: (orgId, wikiId) => `${WIKI_BASE}/orgs/${orgId}/wikis/${wikiId}/files/preview-url`,
|
|
166
|
+
// Wiki Path Scopes (AFCS ACL)
|
|
167
|
+
WIKI_PATH_SCOPES: (orgId, wikiId) => `${WIKI_BASE}/orgs/${orgId}/wikis/${wikiId}/path-scopes`,
|
|
168
|
+
WIKI_PATH_SCOPE: (orgId, wikiId, scopeId) => `${WIKI_BASE}/orgs/${orgId}/wikis/${wikiId}/path-scopes/${scopeId}`,
|
|
169
|
+
WIKI_ACCESS_STATUS: (orgId, wikiId) => `${WIKI_BASE}/orgs/${orgId}/wikis/${wikiId}/access-status`,
|
|
170
|
+
WIKI_ACCESS_REQUESTS: (orgId, wikiId) => `${WIKI_BASE}/orgs/${orgId}/wikis/${wikiId}/access-requests`,
|
|
171
|
+
// Wiki History (commits, file commits, blame)
|
|
172
|
+
WIKI_COMMITS: (orgId, wikiId) => `${WIKI_BASE}/orgs/${orgId}/wikis/${wikiId}/commits`,
|
|
173
|
+
WIKI_FILE_COMMITS: (orgId, wikiId) => `${WIKI_BASE}/orgs/${orgId}/wikis/${wikiId}/file-commits`,
|
|
174
|
+
WIKI_BLAME: (orgId, wikiId) => `${WIKI_BASE}/orgs/${orgId}/wikis/${wikiId}/blame`,
|
|
175
|
+
// Wiki Operations (audit log)
|
|
176
|
+
WIKI_OPERATIONS: (orgId, wikiId) => `${WIKI_BASE}/orgs/${orgId}/wikis/${wikiId}/operations`,
|
|
177
|
+
WIKI_OPERATION_REVERT: (orgId, wikiId, opId) => `${WIKI_BASE}/orgs/${orgId}/wikis/${wikiId}/operations/${opId}/revert`,
|
|
178
|
+
// Unified Comments (org-scoped, api-server)
|
|
179
|
+
COMMENTS: (orgId) => `${API_BASE}/orgs/${orgId}/comments`,
|
|
180
|
+
COMMENT: (orgId, commentId) => `${API_BASE}/orgs/${orgId}/comments/${commentId}`,
|
|
181
|
+
// Inbox (org-scoped)
|
|
182
|
+
INBOX: (orgId) => `${API_BASE}/orgs/${orgId}/inbox`,
|
|
183
|
+
INBOX_UNREAD_COUNT: (orgId) => `${API_BASE}/orgs/${orgId}/inbox/unread-count`,
|
|
184
|
+
INBOX_ITEM_READ: (orgId, id) => `${API_BASE}/orgs/${orgId}/inbox/${id}/read`,
|
|
185
|
+
INBOX_ITEM_UNREAD: (orgId, id) => `${API_BASE}/orgs/${orgId}/inbox/${id}/unread`,
|
|
186
|
+
INBOX_ITEM_ARCHIVE: (orgId, id) => `${API_BASE}/orgs/${orgId}/inbox/${id}/archive`,
|
|
187
|
+
// Snooze/Unsnooze deferred until un-snooze cron worker is implemented
|
|
188
|
+
INBOX_MARK_ALL_READ: (orgId) => `${API_BASE}/orgs/${orgId}/inbox/mark-all-read`,
|
|
189
|
+
INBOX_ARCHIVE_ALL: (orgId) => `${API_BASE}/orgs/${orgId}/inbox/archive-all`,
|
|
190
|
+
INBOX_ITEM: (orgId, id) => `${API_BASE}/orgs/${orgId}/inbox/${id}`,
|
|
191
|
+
INBOX_ACK: (orgId) => `${API_BASE}/orgs/${orgId}/inbox/ack`,
|
|
192
|
+
// Dispatch (agent event delivery)
|
|
193
|
+
DISPATCH: (orgId) => `${API_BASE}/orgs/${orgId}/dispatch`,
|
|
194
|
+
DISPATCH_PENDING_COUNT: (orgId) => `${API_BASE}/orgs/${orgId}/dispatch/pending-count`,
|
|
195
|
+
DISPATCH_ACK: (orgId) => `${API_BASE}/orgs/${orgId}/dispatch/ack`,
|
|
196
|
+
DISPATCH_ACK_BY_ID: (orgId, id) => `${API_BASE}/orgs/${orgId}/dispatch/${id}/ack`,
|
|
197
|
+
// Unread
|
|
198
|
+
UNREAD: `${API_BASE}/me/unread`,
|
|
199
|
+
ORG_UNREAD: (orgId) => `${API_BASE}/orgs/${orgId}/unread`,
|
|
200
|
+
CHAT_READ: (orgId, chatId) => `${API_BASE}/orgs/${orgId}/chats/${chatId}/read`,
|
|
201
|
+
// References (org-scoped)
|
|
202
|
+
REFS_RESOLVE: (orgId) => `${API_BASE}/orgs/${orgId}/refs/resolve`,
|
|
203
|
+
REFS_BACKLINKS: (orgId) => `${API_BASE}/orgs/${orgId}/refs/backlinks`,
|
|
204
|
+
REFS_CHECK: (orgId) => `${API_BASE}/orgs/${orgId}/refs/check`,
|
|
205
|
+
// Platform config (agent-scoped, not org-scoped)
|
|
206
|
+
PLATFORM_CONFIG: `${API_BASE}/agents/platform-config`,
|
|
207
|
+
// Push notifications
|
|
208
|
+
PUSH_SUBSCRIBE: `${API_BASE}/push/subscribe`,
|
|
209
|
+
PUSH_UNSUBSCRIBE: `${API_BASE}/push/unsubscribe`,
|
|
210
|
+
PUSH_VAPID_KEY: `${API_BASE}/push/vapid-key`,
|
|
211
|
+
// Notification preferences
|
|
212
|
+
NOTIFICATION_PREFERENCES: `${API_BASE}/notification-preferences`,
|
|
213
|
+
// Feature flags (org-scoped, server-evaluated)
|
|
214
|
+
FEATURE_FLAGS: (orgId) => `${API_BASE}/orgs/${orgId}/feature-flags`,
|
|
215
|
+
// Billing & Credits (org-scoped)
|
|
216
|
+
BILLING: (orgId) => `${API_BASE}/orgs/${orgId}/billing`,
|
|
217
|
+
BILLING_TRANSACTIONS: (orgId) => `${API_BASE}/orgs/${orgId}/billing/transactions`,
|
|
218
|
+
BILLING_CHECKOUT: (orgId) => `${API_BASE}/orgs/${orgId}/billing/checkout`,
|
|
219
|
+
BILLING_AUTO_RELOAD: (orgId) => `${API_BASE}/orgs/${orgId}/billing/auto-reload`,
|
|
220
|
+
BILLING_SETUP_INTENT: (orgId) => `${API_BASE}/orgs/${orgId}/billing/setup-intent`,
|
|
221
|
+
COMPUTE_PRICING: () => `${API_BASE}/billing/compute-pricing`
|
|
222
|
+
// ADMIN_GRANTS intentionally NOT exported here — it sits under the
|
|
223
|
+
// unauthenticated `/internal/admin/*` surface and must not bleed into the
|
|
224
|
+
// public SDK. Admin dashboard hits the URL directly from its own client.
|
|
225
|
+
};
|
|
226
|
+
var WS_EVENTS = {
|
|
227
|
+
// Client -> Server
|
|
228
|
+
PING: "ping",
|
|
229
|
+
TYPING: "typing",
|
|
230
|
+
WATCH: "watch",
|
|
231
|
+
AGENT_HEARTBEAT: "agent.heartbeat",
|
|
232
|
+
// Server -> Client
|
|
233
|
+
HELLO: "hello",
|
|
234
|
+
PONG: "pong",
|
|
235
|
+
WATCHING: "watching",
|
|
236
|
+
MESSAGE_NEW: "message.new",
|
|
237
|
+
MESSAGE_PATCH: "message.patch",
|
|
238
|
+
MESSAGE_EDIT: "message.edit",
|
|
239
|
+
MESSAGE_DELETE: "message.delete",
|
|
240
|
+
TYPING_UPDATE: "typing.update",
|
|
241
|
+
CHAT_UPDATE: "chat.update",
|
|
242
|
+
CHAT_DELETED: "chat.deleted",
|
|
243
|
+
APPROVAL_UPDATE: "approval.update",
|
|
244
|
+
// deprecated
|
|
245
|
+
CARD_UPDATE: "card.update",
|
|
246
|
+
RECOVERY_OVERFLOW: "recovery.overflow",
|
|
247
|
+
MEMBERSHIP_CHANGED: "membership.changed",
|
|
248
|
+
TASK_CREATED: "task.created",
|
|
249
|
+
TASK_UPDATED: "task.updated",
|
|
250
|
+
TASK_DELETED: "task.deleted",
|
|
251
|
+
TASK_COMMENT_CREATED: "task.comment.created",
|
|
252
|
+
TASK_COMMENT_UPDATED: "task.comment.updated",
|
|
253
|
+
TASK_COMMENT_DELETED: "task.comment.deleted",
|
|
254
|
+
PROJECT_CREATED: "project.created",
|
|
255
|
+
PROJECT_UPDATED: "project.updated",
|
|
256
|
+
PROJECT_DELETED: "project.deleted",
|
|
257
|
+
AGENT_SESSION_NEW: "agent_session.new",
|
|
258
|
+
AGENT_SESSION_UPDATE: "agent_session.update",
|
|
259
|
+
AGENT_STEP_NEW: "agent_step.new",
|
|
260
|
+
INVITATION_NEW: "invitation.new",
|
|
261
|
+
INVITATION_ACCEPTED: "invitation.accepted",
|
|
262
|
+
INVITATION_DECLINED: "invitation.declined",
|
|
263
|
+
INVITATION_REVOKED: "invitation.revoked",
|
|
264
|
+
AGENT_CONFIG_UPDATE: "agent_config.update",
|
|
265
|
+
PRESENCE_UPDATE: "presence.update",
|
|
266
|
+
WIKI_CHANGESET_CREATED: "wiki.changeset.created",
|
|
267
|
+
WIKI_CHANGESET_UPDATED: "wiki.changeset.updated",
|
|
268
|
+
COMMENT_CREATED: "comment.created",
|
|
269
|
+
COMMENT_UPDATED: "comment.updated",
|
|
270
|
+
COMMENT_DELETED: "comment.deleted",
|
|
271
|
+
NOTIFICATION_ALERT: "notification.alert",
|
|
272
|
+
INBOX_NEW: "inbox.new",
|
|
273
|
+
INBOX_UPDATE: "inbox.update",
|
|
274
|
+
INBOX_BULK_UPDATE: "inbox.bulk_update",
|
|
275
|
+
READ_POSITION_UPDATED: "read_position.updated",
|
|
276
|
+
DISPATCH_NEW: "dispatch.new",
|
|
277
|
+
SCHEDULE_CREATED: "schedule.created",
|
|
278
|
+
SCHEDULE_UPDATED: "schedule.updated",
|
|
279
|
+
SCHEDULE_DELETED: "schedule.deleted",
|
|
280
|
+
SCHEDULE_FIRED: "schedule.fired",
|
|
281
|
+
BILLING_BALANCE_UPDATED: "billing.balance_updated",
|
|
282
|
+
BILLING_LOW_BALANCE: "billing.low_balance",
|
|
283
|
+
BILLING_MACHINE_STOPPED: "billing.machine_stopped",
|
|
284
|
+
BILLING_INSUFFICIENT: "billing.insufficient",
|
|
285
|
+
USER_UPDATED: "user.updated",
|
|
286
|
+
MACHINE_HELLO: "machine.hello",
|
|
287
|
+
MACHINE_AGENT_ATTACHED: "machine.agent.attached",
|
|
288
|
+
MACHINE_AGENT_DETACHED: "machine.agent.detached",
|
|
289
|
+
MACHINE_STOP: "machine.stop"
|
|
290
|
+
};
|
|
291
|
+
|
|
292
|
+
// ts/sdk/dist/client.js
|
|
293
|
+
var ParallClient = class _ParallClient {
|
|
294
|
+
baseUrl;
|
|
295
|
+
token;
|
|
296
|
+
onTokenExpired;
|
|
297
|
+
getRefreshToken;
|
|
298
|
+
setTokens;
|
|
299
|
+
refreshPromise = null;
|
|
300
|
+
swimlaneName;
|
|
301
|
+
/** Auth endpoints excluded from automatic 401 refresh to prevent recursion. */
|
|
302
|
+
static AUTH_PATHS = /* @__PURE__ */ new Set([
|
|
303
|
+
"/auth/login",
|
|
304
|
+
"/auth/register",
|
|
305
|
+
"/auth/refresh",
|
|
306
|
+
"/auth/logout",
|
|
307
|
+
"/auth/verify-email",
|
|
308
|
+
"/auth/resend-code",
|
|
309
|
+
"/auth/check-email"
|
|
310
|
+
]);
|
|
311
|
+
/** Proactive refresh when token expires within this window (seconds). */
|
|
312
|
+
static REFRESH_THRESHOLD_S = 5 * 60;
|
|
313
|
+
static normalizeFetchError(err) {
|
|
314
|
+
if (typeof DOMException !== "undefined" && err instanceof DOMException && (err.name === "TimeoutError" || err.name === "AbortError")) {
|
|
315
|
+
return new ApiError(0, "Request timed out", "REQUEST_TIMEOUT");
|
|
316
|
+
}
|
|
317
|
+
const apiError = new ApiError(0, "Network request failed", "NETWORK_ERROR");
|
|
318
|
+
if (err instanceof Error && err.message && err.message !== "Failed to fetch") {
|
|
319
|
+
apiError.extras = { cause: err.message };
|
|
320
|
+
}
|
|
321
|
+
return apiError;
|
|
322
|
+
}
|
|
323
|
+
/** Build headers common to all requests (auth, swimlane). */
|
|
324
|
+
buildHeaders(extra) {
|
|
325
|
+
const headers = {
|
|
326
|
+
"Content-Type": "application/json",
|
|
327
|
+
...extra
|
|
328
|
+
};
|
|
329
|
+
if (this.token) {
|
|
330
|
+
headers["Authorization"] = `Bearer ${this.token}`;
|
|
331
|
+
}
|
|
332
|
+
if (this.swimlaneName) {
|
|
333
|
+
headers["X-Prll-Swimlane"] = this.swimlaneName;
|
|
334
|
+
}
|
|
335
|
+
return headers;
|
|
336
|
+
}
|
|
337
|
+
constructor(options = {}) {
|
|
338
|
+
this.baseUrl = options.baseUrl ?? "";
|
|
339
|
+
this.token = options.token ?? null;
|
|
340
|
+
this.onTokenExpired = options.onTokenExpired;
|
|
341
|
+
this.getRefreshToken = options.getRefreshToken;
|
|
342
|
+
this.setTokens = options.setTokens;
|
|
343
|
+
this.swimlaneName = options.swimlaneName;
|
|
344
|
+
}
|
|
345
|
+
setToken(token) {
|
|
346
|
+
this.token = token;
|
|
347
|
+
}
|
|
348
|
+
getToken() {
|
|
349
|
+
return this.token;
|
|
350
|
+
}
|
|
351
|
+
/** Decode the `exp` claim from a JWT without verifying the signature. */
|
|
352
|
+
static decodeJwtExp(token) {
|
|
353
|
+
try {
|
|
354
|
+
const parts = token.split(".");
|
|
355
|
+
if (parts.length !== 3)
|
|
356
|
+
return null;
|
|
357
|
+
const b64 = parts[1].replace(/-/g, "+").replace(/_/g, "/");
|
|
358
|
+
const padded = b64.padEnd(b64.length + (4 - b64.length % 4) % 4, "=");
|
|
359
|
+
const payload = JSON.parse(atob(padded));
|
|
360
|
+
return typeof payload.exp === "number" ? payload.exp : null;
|
|
361
|
+
} catch {
|
|
362
|
+
return null;
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
/**
|
|
366
|
+
* Proactive refresh: if the current access token expires within
|
|
367
|
+
* REFRESH_THRESHOLD_S, refresh it **before** sending the request.
|
|
368
|
+
* No-op when the token is still fresh, missing, or un-parseable.
|
|
369
|
+
*/
|
|
370
|
+
async ensureFreshToken(path4) {
|
|
371
|
+
if (!this.token || !this.getRefreshToken)
|
|
372
|
+
return;
|
|
373
|
+
const pathSuffix = path4.replace(/^\/api\/v1/, "");
|
|
374
|
+
if (_ParallClient.AUTH_PATHS.has(pathSuffix))
|
|
375
|
+
return;
|
|
376
|
+
const exp = _ParallClient.decodeJwtExp(this.token);
|
|
377
|
+
if (exp === null)
|
|
378
|
+
return;
|
|
379
|
+
if (exp - Date.now() / 1e3 > _ParallClient.REFRESH_THRESHOLD_S)
|
|
380
|
+
return;
|
|
381
|
+
await this.tryRefresh();
|
|
382
|
+
}
|
|
383
|
+
/**
|
|
384
|
+
* Single-flight token refresh. Multiple concurrent 401s share one promise
|
|
385
|
+
* so only one refresh request is made.
|
|
386
|
+
*/
|
|
387
|
+
async tryRefresh() {
|
|
388
|
+
const rt = this.getRefreshToken?.();
|
|
389
|
+
if (!rt)
|
|
390
|
+
return false;
|
|
391
|
+
if (!this.refreshPromise) {
|
|
392
|
+
this.refreshPromise = this.refreshToken(rt);
|
|
393
|
+
}
|
|
394
|
+
try {
|
|
395
|
+
const tokens = await this.refreshPromise;
|
|
396
|
+
this.setToken(tokens.access_token);
|
|
397
|
+
this.setTokens?.(tokens);
|
|
398
|
+
return true;
|
|
399
|
+
} catch {
|
|
400
|
+
return false;
|
|
401
|
+
} finally {
|
|
402
|
+
this.refreshPromise = null;
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
async request(method, path4, body, query, retried = false, opts) {
|
|
406
|
+
if (!retried) {
|
|
407
|
+
await this.ensureFreshToken(path4);
|
|
408
|
+
}
|
|
409
|
+
let url = `${this.baseUrl}${path4}`;
|
|
410
|
+
if (query) {
|
|
411
|
+
const params = new URLSearchParams();
|
|
412
|
+
for (const [key, value] of Object.entries(query)) {
|
|
413
|
+
if (value !== void 0) {
|
|
414
|
+
params.set(key, String(value));
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
const qs = params.toString();
|
|
418
|
+
if (qs)
|
|
419
|
+
url += `?${qs}`;
|
|
420
|
+
}
|
|
421
|
+
const headers = this.buildHeaders();
|
|
422
|
+
let res;
|
|
423
|
+
try {
|
|
424
|
+
res = await fetch(url, {
|
|
425
|
+
method,
|
|
426
|
+
headers,
|
|
427
|
+
body: body ? JSON.stringify(body) : void 0,
|
|
428
|
+
signal: AbortSignal.timeout(opts?.timeoutMs ?? 15e3)
|
|
429
|
+
});
|
|
430
|
+
} catch (err) {
|
|
431
|
+
throw _ParallClient.normalizeFetchError(err);
|
|
432
|
+
}
|
|
433
|
+
if (res.status === 401) {
|
|
434
|
+
const pathSuffix = path4.replace(/^\/api\/v1/, "");
|
|
435
|
+
const isAuthPath = _ParallClient.AUTH_PATHS.has(pathSuffix);
|
|
436
|
+
if (!retried && !isAuthPath && this.getRefreshToken) {
|
|
437
|
+
const refreshed = await this.tryRefresh();
|
|
438
|
+
if (refreshed) {
|
|
439
|
+
return this.request(method, path4, body, query, true, opts);
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
if (this.onTokenExpired && !isAuthPath) {
|
|
443
|
+
this.onTokenExpired();
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
if (!res.ok) {
|
|
447
|
+
const rawErrorBody = await res.json().catch(() => ({}));
|
|
448
|
+
const errorBody = rawErrorBody !== null && typeof rawErrorBody === "object" ? rawErrorBody : {};
|
|
449
|
+
const errorObj = errorBody?.error && typeof errorBody.error === "object" ? errorBody.error : void 0;
|
|
450
|
+
const errMsg = errorObj?.message ?? (typeof errorBody?.error === "string" ? errorBody.error : void 0);
|
|
451
|
+
const errCode = errorObj?.code ?? (typeof errorBody?.code === "string" ? errorBody.code : void 0);
|
|
452
|
+
const apiError = new ApiError(res.status, errMsg ?? res.statusText, errCode);
|
|
453
|
+
const { error: _e, code: _c, message: _m, ...extras } = errorBody;
|
|
454
|
+
if (Object.keys(extras).length > 0)
|
|
455
|
+
apiError.extras = extras;
|
|
456
|
+
throw apiError;
|
|
457
|
+
}
|
|
458
|
+
if (res.status === 204)
|
|
459
|
+
return void 0;
|
|
460
|
+
if (res.status === 202) {
|
|
461
|
+
const text = await res.text();
|
|
462
|
+
if (!text)
|
|
463
|
+
return void 0;
|
|
464
|
+
return JSON.parse(text);
|
|
465
|
+
}
|
|
466
|
+
return res.json();
|
|
467
|
+
}
|
|
468
|
+
/**
|
|
469
|
+
* Multipart upload variant of `request`. Same auth / refresh / error
|
|
470
|
+
* handling, but lets the caller hand us a prepared `FormData` (file +
|
|
471
|
+
* text fields) and skips the JSON content-type. Used by
|
|
472
|
+
* uploadWikiFile / uploadWikiFileToChangeset — wiki binary uploads can
|
|
473
|
+
* hit the 100 MiB cap, so a longer 5-minute timeout is used so a
|
|
474
|
+
* 50 MiB blob on a slow connection doesn't get chopped at 15 s.
|
|
475
|
+
*/
|
|
476
|
+
async multipartRequest(method, path4, body, retried = false) {
|
|
477
|
+
if (!retried) {
|
|
478
|
+
await this.ensureFreshToken(path4);
|
|
479
|
+
}
|
|
480
|
+
const { "Content-Type": _drop, ...headers } = this.buildHeaders();
|
|
481
|
+
void _drop;
|
|
482
|
+
let res;
|
|
483
|
+
try {
|
|
484
|
+
res = await fetch(`${this.baseUrl}${path4}`, {
|
|
485
|
+
method,
|
|
486
|
+
headers,
|
|
487
|
+
body,
|
|
488
|
+
signal: AbortSignal.timeout(5 * 60 * 1e3)
|
|
489
|
+
});
|
|
490
|
+
} catch (err) {
|
|
491
|
+
throw _ParallClient.normalizeFetchError(err);
|
|
492
|
+
}
|
|
493
|
+
if (res.status === 401) {
|
|
494
|
+
const pathSuffix = path4.replace(/^\/api\/v1/, "");
|
|
495
|
+
const isAuthPath = _ParallClient.AUTH_PATHS.has(pathSuffix);
|
|
496
|
+
if (!retried && !isAuthPath && this.getRefreshToken) {
|
|
497
|
+
const refreshed = await this.tryRefresh();
|
|
498
|
+
if (refreshed) {
|
|
499
|
+
return this.multipartRequest(method, path4, body, true);
|
|
500
|
+
}
|
|
501
|
+
}
|
|
502
|
+
if (this.onTokenExpired && !isAuthPath) {
|
|
503
|
+
this.onTokenExpired();
|
|
504
|
+
}
|
|
505
|
+
}
|
|
506
|
+
if (!res.ok) {
|
|
507
|
+
const rawErrorBody = await res.json().catch(() => ({}));
|
|
508
|
+
const errorBody = rawErrorBody !== null && typeof rawErrorBody === "object" ? rawErrorBody : {};
|
|
509
|
+
const errorObj = errorBody?.error && typeof errorBody.error === "object" ? errorBody.error : void 0;
|
|
510
|
+
const errMsg = errorObj?.message ?? (typeof errorBody?.error === "string" ? errorBody.error : void 0);
|
|
511
|
+
const errCode = errorObj?.code ?? (typeof errorBody?.code === "string" ? errorBody.code : void 0);
|
|
512
|
+
const apiError = new ApiError(res.status, errMsg ?? res.statusText, errCode);
|
|
513
|
+
const { error: _e, code: _c, message: _m, ...extras } = errorBody;
|
|
514
|
+
if (Object.keys(extras).length > 0)
|
|
515
|
+
apiError.extras = extras;
|
|
516
|
+
throw apiError;
|
|
517
|
+
}
|
|
518
|
+
if (res.status === 204)
|
|
519
|
+
return void 0;
|
|
520
|
+
return res.json();
|
|
521
|
+
}
|
|
522
|
+
// ---- Auth ----
|
|
523
|
+
async register(req) {
|
|
524
|
+
return this.request("POST", ENDPOINTS.AUTH_REGISTER, req);
|
|
525
|
+
}
|
|
526
|
+
async login(req) {
|
|
527
|
+
return this.request("POST", ENDPOINTS.AUTH_LOGIN, req);
|
|
528
|
+
}
|
|
529
|
+
async refreshToken(refreshToken) {
|
|
530
|
+
return this.request("POST", ENDPOINTS.AUTH_REFRESH, { refresh_token: refreshToken });
|
|
531
|
+
}
|
|
532
|
+
async logout() {
|
|
533
|
+
return this.request("POST", ENDPOINTS.AUTH_LOGOUT);
|
|
534
|
+
}
|
|
535
|
+
async changePassword(req) {
|
|
536
|
+
return this.request("POST", ENDPOINTS.AUTH_CHANGE_PASSWORD, req);
|
|
537
|
+
}
|
|
538
|
+
async checkEmail(email) {
|
|
539
|
+
return this.request("POST", ENDPOINTS.AUTH_CHECK_EMAIL, { email });
|
|
540
|
+
}
|
|
541
|
+
async verifyEmail(email, code) {
|
|
542
|
+
return this.request("POST", ENDPOINTS.AUTH_VERIFY_EMAIL, { email, code });
|
|
543
|
+
}
|
|
544
|
+
async resendCode(email) {
|
|
545
|
+
return this.request("POST", ENDPOINTS.AUTH_RESEND_CODE, { email });
|
|
546
|
+
}
|
|
547
|
+
// ---- WebSocket ----
|
|
548
|
+
async getWsTicket() {
|
|
549
|
+
return this.request("POST", ENDPOINTS.WS_TICKET);
|
|
550
|
+
}
|
|
551
|
+
// ---- Users ----
|
|
552
|
+
async getMe() {
|
|
553
|
+
return this.request("GET", ENDPOINTS.USERS_ME);
|
|
554
|
+
}
|
|
555
|
+
async updateMe(data) {
|
|
556
|
+
return this.request("PATCH", ENDPOINTS.USERS_ME, data);
|
|
557
|
+
}
|
|
558
|
+
async deleteAccount() {
|
|
559
|
+
return this.request("DELETE", ENDPOINTS.USERS_ME);
|
|
560
|
+
}
|
|
561
|
+
async uploadAvatar(file) {
|
|
562
|
+
const fd = new FormData();
|
|
563
|
+
fd.append("file", file);
|
|
564
|
+
return this.multipartRequest("POST", ENDPOINTS.USER_AVATAR, fd);
|
|
565
|
+
}
|
|
566
|
+
async deleteAvatar() {
|
|
567
|
+
return this.request("DELETE", ENDPOINTS.USER_AVATAR);
|
|
568
|
+
}
|
|
569
|
+
async uploadAgentAvatar(orgId, agentId, file) {
|
|
570
|
+
const fd = new FormData();
|
|
571
|
+
fd.append("file", file);
|
|
572
|
+
return this.multipartRequest("POST", ENDPOINTS.AGENT_AVATAR(orgId, agentId), fd);
|
|
573
|
+
}
|
|
574
|
+
async deleteAgentAvatar(orgId, agentId) {
|
|
575
|
+
return this.request("DELETE", ENDPOINTS.AGENT_AVATAR(orgId, agentId));
|
|
576
|
+
}
|
|
577
|
+
async getUser(id) {
|
|
578
|
+
return this.request("GET", ENDPOINTS.USER(id));
|
|
579
|
+
}
|
|
580
|
+
// ---- Organizations ----
|
|
581
|
+
async createOrg(data) {
|
|
582
|
+
return this.request("POST", ENDPOINTS.ORGS, data);
|
|
583
|
+
}
|
|
584
|
+
async getOrgs() {
|
|
585
|
+
const res = await this.request("GET", ENDPOINTS.ORGS);
|
|
586
|
+
return res.data;
|
|
587
|
+
}
|
|
588
|
+
async getOrg(orgId) {
|
|
589
|
+
return this.request("GET", ENDPOINTS.ORG(orgId));
|
|
590
|
+
}
|
|
591
|
+
/**
|
|
592
|
+
* Update org settings. Owner/admin-only on the server; non-admin callers
|
|
593
|
+
* get 403. Smart routing: pass strategy ('llm' | 'agent' | '') and
|
|
594
|
+
* agent_id to configure; omit to leave unchanged.
|
|
595
|
+
*/
|
|
596
|
+
async updateOrg(orgId, data) {
|
|
597
|
+
return this.request("PATCH", ENDPOINTS.ORG(orgId), data);
|
|
598
|
+
}
|
|
599
|
+
async deleteOrg(orgId) {
|
|
600
|
+
return this.request("DELETE", ENDPOINTS.ORG(orgId));
|
|
601
|
+
}
|
|
602
|
+
async getOrgMembers(orgId) {
|
|
603
|
+
const res = await this.request("GET", ENDPOINTS.ORG_MEMBERS(orgId));
|
|
604
|
+
return res.data;
|
|
605
|
+
}
|
|
606
|
+
async getOnlineMembers(orgId) {
|
|
607
|
+
const res = await this.request("GET", ENDPOINTS.ORG_MEMBERS_ONLINE(orgId));
|
|
608
|
+
return res.user_ids ?? [];
|
|
609
|
+
}
|
|
610
|
+
async removeOrgMember(orgId, userId) {
|
|
611
|
+
return this.request("DELETE", ENDPOINTS.ORG_MEMBER(orgId, userId));
|
|
612
|
+
}
|
|
613
|
+
async updateOrgMember(orgId, userId, role) {
|
|
614
|
+
return this.request("PATCH", ENDPOINTS.ORG_MEMBER(orgId, userId), { role });
|
|
615
|
+
}
|
|
616
|
+
// Member activity surfaces — chats the member participates in within this org,
|
|
617
|
+
// ordered by most recent activity. Powers the member profile Activity tab.
|
|
618
|
+
async getMemberChats(orgId, memberId, params) {
|
|
619
|
+
const q = {};
|
|
620
|
+
if (params?.cursor)
|
|
621
|
+
q.cursor = params.cursor;
|
|
622
|
+
if (params?.limit)
|
|
623
|
+
q.limit = String(params.limit);
|
|
624
|
+
return this.request("GET", ENDPOINTS.ORG_MEMBER_CHATS(orgId, memberId), void 0, q);
|
|
625
|
+
}
|
|
626
|
+
// Pending tasks (todo + in_progress) assigned to a member. Powers both
|
|
627
|
+
// member profile Activity and the agent startup catch-up flow.
|
|
628
|
+
async getMemberTasks(orgId, memberId, params) {
|
|
629
|
+
const q = {};
|
|
630
|
+
if (params?.cursor)
|
|
631
|
+
q.cursor = params.cursor;
|
|
632
|
+
if (params?.limit)
|
|
633
|
+
q.limit = String(params.limit);
|
|
634
|
+
return this.request("GET", ENDPOINTS.ORG_MEMBER_TASKS(orgId, memberId), void 0, q);
|
|
635
|
+
}
|
|
636
|
+
// ---- Invitations ----
|
|
637
|
+
async createInvitation(orgId, email, role) {
|
|
638
|
+
return this.request("POST", ENDPOINTS.ORG_INVITATIONS(orgId), { email, role });
|
|
639
|
+
}
|
|
640
|
+
async getOrgInvitations(orgId) {
|
|
641
|
+
const res = await this.request("GET", ENDPOINTS.ORG_INVITATIONS(orgId));
|
|
642
|
+
return res.data;
|
|
643
|
+
}
|
|
644
|
+
async revokeInvitation(orgId, invId) {
|
|
645
|
+
return this.request("DELETE", ENDPOINTS.ORG_INVITATION(orgId, invId));
|
|
646
|
+
}
|
|
647
|
+
async resendInvitation(orgId, invId) {
|
|
648
|
+
return this.request("POST", ENDPOINTS.ORG_INVITATION_RESEND(orgId, invId));
|
|
649
|
+
}
|
|
650
|
+
async getMyInvitations() {
|
|
651
|
+
const res = await this.request("GET", ENDPOINTS.MY_INVITATIONS);
|
|
652
|
+
return res.data;
|
|
653
|
+
}
|
|
654
|
+
async acceptInvitation(id) {
|
|
655
|
+
return this.request("POST", ENDPOINTS.INVITATION_ACCEPT(id));
|
|
656
|
+
}
|
|
657
|
+
async declineInvitation(id) {
|
|
658
|
+
return this.request("POST", ENDPOINTS.INVITATION_DECLINE(id));
|
|
659
|
+
}
|
|
660
|
+
async getInvitationByToken(token) {
|
|
661
|
+
return this.request("GET", ENDPOINTS.INVITATION_BY_TOKEN(token));
|
|
662
|
+
}
|
|
663
|
+
// ---- Direct Messages (org-scoped) ----
|
|
664
|
+
async sendDirectMessage(orgId, req) {
|
|
665
|
+
return this.request("POST", ENDPOINTS.DM(orgId), req);
|
|
666
|
+
}
|
|
667
|
+
/** Create the onboarding agent's DM and seed the intro message.
|
|
668
|
+
* Call after confirming the agent is online so the chat only appears
|
|
669
|
+
* once the agent can actually respond. */
|
|
670
|
+
async seedOnboardingDM(orgId) {
|
|
671
|
+
return this.request("POST", ENDPOINTS.SEED_ONBOARDING_DM(orgId));
|
|
672
|
+
}
|
|
673
|
+
/** Record that the user has dismissed the onboarding popup for this org.
|
|
674
|
+
* Once dismissed, the popup never auto-shows again. */
|
|
675
|
+
async dismissOnboarding(orgId) {
|
|
676
|
+
await this.request("POST", ENDPOINTS.DISMISS_ONBOARDING(orgId));
|
|
677
|
+
}
|
|
678
|
+
// ---- Chats (org-scoped) ----
|
|
679
|
+
async createChat(orgId, req) {
|
|
680
|
+
return this.request("POST", ENDPOINTS.CHATS(orgId), req);
|
|
681
|
+
}
|
|
682
|
+
async getChats(orgId, params) {
|
|
683
|
+
return this.request("GET", ENDPOINTS.CHATS(orgId), void 0, params);
|
|
684
|
+
}
|
|
685
|
+
async getChat(orgId, chatId) {
|
|
686
|
+
return this.request("GET", ENDPOINTS.CHAT(orgId, chatId));
|
|
687
|
+
}
|
|
688
|
+
async updateChat(orgId, chatId, data) {
|
|
689
|
+
return this.request("PATCH", ENDPOINTS.CHAT(orgId, chatId), data);
|
|
690
|
+
}
|
|
691
|
+
async getDiscoverableChats(orgId, params) {
|
|
692
|
+
const res = await this.request("GET", ENDPOINTS.CHATS_DISCOVERABLE(orgId), void 0, params);
|
|
693
|
+
return res.data;
|
|
694
|
+
}
|
|
695
|
+
async joinChat(orgId, chatId) {
|
|
696
|
+
return this.request("POST", ENDPOINTS.CHAT_JOIN(orgId, chatId));
|
|
697
|
+
}
|
|
698
|
+
async deleteChat(orgId, chatId, opts) {
|
|
699
|
+
return this.request("DELETE", ENDPOINTS.CHAT(orgId, chatId), void 0, opts?.force ? { force: "true" } : void 0);
|
|
700
|
+
}
|
|
701
|
+
async archiveChat(orgId, chatId) {
|
|
702
|
+
return this.request("POST", ENDPOINTS.CHAT_ARCHIVE(orgId, chatId));
|
|
703
|
+
}
|
|
704
|
+
async restoreChat(orgId, chatId) {
|
|
705
|
+
return this.request("POST", ENDPOINTS.CHAT_RESTORE(orgId, chatId));
|
|
706
|
+
}
|
|
707
|
+
// ---- Chat Members ----
|
|
708
|
+
async getChatMembers(orgId, chatId) {
|
|
709
|
+
const res = await this.request("GET", ENDPOINTS.CHAT_MEMBERS(orgId, chatId));
|
|
710
|
+
return res.data;
|
|
711
|
+
}
|
|
712
|
+
async addChatMember(orgId, chatId, userId, role) {
|
|
713
|
+
return this.request("POST", ENDPOINTS.CHAT_MEMBERS(orgId, chatId), { user_id: userId, role });
|
|
714
|
+
}
|
|
715
|
+
async removeChatMember(orgId, chatId, userId) {
|
|
716
|
+
return this.request("DELETE", ENDPOINTS.CHAT_MEMBER(orgId, chatId, userId));
|
|
717
|
+
}
|
|
718
|
+
async updateChatMemberRole(orgId, chatId, userId, role) {
|
|
719
|
+
return this.request("PATCH", ENDPOINTS.CHAT_MEMBER(orgId, chatId, userId), { role });
|
|
720
|
+
}
|
|
721
|
+
async updateChatMember(orgId, chatId, userId, req) {
|
|
722
|
+
return this.request("PATCH", ENDPOINTS.CHAT_MEMBER(orgId, chatId, userId), req);
|
|
723
|
+
}
|
|
724
|
+
async transferChatOwnership(orgId, chatId, req) {
|
|
725
|
+
return this.request("POST", ENDPOINTS.CHAT_TRANSFER_OWNERSHIP(orgId, chatId), req);
|
|
726
|
+
}
|
|
727
|
+
// ---- Messages ----
|
|
728
|
+
async sendMessage(orgId, chatId, req) {
|
|
729
|
+
return this.request("POST", ENDPOINTS.CHAT_MESSAGES(orgId, chatId), req);
|
|
730
|
+
}
|
|
731
|
+
async getMessages(orgId, chatId, params) {
|
|
732
|
+
return this.request("GET", ENDPOINTS.CHAT_MESSAGES(orgId, chatId), void 0, params);
|
|
733
|
+
}
|
|
734
|
+
async getMessage(id) {
|
|
735
|
+
return this.request("GET", ENDPOINTS.MESSAGE(id));
|
|
736
|
+
}
|
|
737
|
+
async editMessage(id, content) {
|
|
738
|
+
return this.request("PATCH", ENDPOINTS.MESSAGE(id), { content });
|
|
739
|
+
}
|
|
740
|
+
async deleteMessage(id) {
|
|
741
|
+
return this.request("DELETE", ENDPOINTS.MESSAGE(id));
|
|
742
|
+
}
|
|
743
|
+
async patchMessage(id, req) {
|
|
744
|
+
return this.request("POST", ENDPOINTS.MESSAGE_PATCHES(id), req);
|
|
745
|
+
}
|
|
746
|
+
async getMessageReplies(id, params) {
|
|
747
|
+
return this.request("GET", ENDPOINTS.MESSAGE_REPLIES(id), void 0, params);
|
|
748
|
+
}
|
|
749
|
+
// ---- File Upload ----
|
|
750
|
+
async getUploadPresignUrl(orgId, req) {
|
|
751
|
+
return this.request("POST", ENDPOINTS.UPLOAD_PRESIGN(orgId), req);
|
|
752
|
+
}
|
|
753
|
+
async completeUpload(orgId, attachmentId) {
|
|
754
|
+
return this.request("POST", ENDPOINTS.UPLOAD_COMPLETE(orgId), { attachment_id: attachmentId });
|
|
755
|
+
}
|
|
756
|
+
async getFileUrl(id) {
|
|
757
|
+
return this.request("GET", ENDPOINTS.FILE(id));
|
|
758
|
+
}
|
|
759
|
+
// ---- Approvals ----
|
|
760
|
+
async getApproval(id) {
|
|
761
|
+
return this.request("GET", ENDPOINTS.APPROVAL(id));
|
|
762
|
+
}
|
|
763
|
+
async requestApproval(orgId, req) {
|
|
764
|
+
return this.request("POST", ENDPOINTS.APPROVAL_REQUESTS(orgId), req);
|
|
765
|
+
}
|
|
766
|
+
async decideApproval(id, decision) {
|
|
767
|
+
return this.request("POST", ENDPOINTS.APPROVAL_DECIDE(id), { decision });
|
|
768
|
+
}
|
|
769
|
+
async cancelApproval(id) {
|
|
770
|
+
return this.request("POST", ENDPOINTS.APPROVAL_CANCEL(id));
|
|
771
|
+
}
|
|
772
|
+
async getPendingApprovals() {
|
|
773
|
+
const res = await this.request("GET", ENDPOINTS.APPROVALS_PENDING);
|
|
774
|
+
return res.data;
|
|
775
|
+
}
|
|
776
|
+
async getApprovableActions() {
|
|
777
|
+
const res = await this.request("GET", ENDPOINTS.APPROVALS_ACTIONS);
|
|
778
|
+
return res.data;
|
|
779
|
+
}
|
|
780
|
+
// ---- Agents (org-scoped) ----
|
|
781
|
+
async createAgent(orgId, req, opts) {
|
|
782
|
+
return this.request("POST", ENDPOINTS.AGENTS(orgId), req, void 0, false, opts);
|
|
783
|
+
}
|
|
784
|
+
async getAgents(orgId) {
|
|
785
|
+
const res = await this.request("GET", ENDPOINTS.AGENTS(orgId));
|
|
786
|
+
return res.data;
|
|
787
|
+
}
|
|
788
|
+
async updateAgent(orgId, agentId, data) {
|
|
789
|
+
return this.request("PATCH", ENDPOINTS.AGENT(orgId, agentId), data);
|
|
790
|
+
}
|
|
791
|
+
/**
|
|
792
|
+
* Read the redacted per-agent provider config. Admin-only — the
|
|
793
|
+
* agents list deliberately omits `provider_config` to avoid leaking
|
|
794
|
+
* topology metadata (base URL → provider / tenant) to non-admins,
|
|
795
|
+
* so this dedicated endpoint is the only read path.
|
|
796
|
+
*/
|
|
797
|
+
async getAgentProviderConfig(orgId, agentId) {
|
|
798
|
+
return this.request("GET", ENDPOINTS.AGENT_PROVIDER_CONFIG(orgId, agentId));
|
|
799
|
+
}
|
|
800
|
+
/**
|
|
801
|
+
* Update per-agent credential / endpoint overrides. Admin-only on the
|
|
802
|
+
* server side. Env changes only take effect after the operator
|
|
803
|
+
* recreates the agent Pod — this call just persists to the DB.
|
|
804
|
+
*/
|
|
805
|
+
async updateAgentProviderConfig(orgId, agentId, req) {
|
|
806
|
+
return this.request("PATCH", ENDPOINTS.AGENT_PROVIDER_CONFIG(orgId, agentId), req);
|
|
807
|
+
}
|
|
808
|
+
async deleteAgent(orgId, agentId) {
|
|
809
|
+
return this.request("DELETE", ENDPOINTS.AGENT(orgId, agentId));
|
|
810
|
+
}
|
|
811
|
+
async createAgentApiKey(orgId, agentId) {
|
|
812
|
+
return this.request("POST", ENDPOINTS.AGENT_API_KEYS(orgId, agentId));
|
|
813
|
+
}
|
|
814
|
+
async revokeAgentApiKey(orgId, agentId, key) {
|
|
815
|
+
return this.request("DELETE", ENDPOINTS.AGENT_API_KEY(orgId, agentId, key));
|
|
816
|
+
}
|
|
817
|
+
async getAgentActivity(orgId, agentId, params) {
|
|
818
|
+
const res = await this.request("GET", ENDPOINTS.AGENT_ACTIVITY(orgId, agentId), void 0, params);
|
|
819
|
+
return res.data;
|
|
820
|
+
}
|
|
821
|
+
async getAgentMonitor(orgId, agentId) {
|
|
822
|
+
const res = await this.request("GET", ENDPOINTS.AGENT_MONITOR(orgId, agentId));
|
|
823
|
+
return res.data;
|
|
824
|
+
}
|
|
825
|
+
// ---- Agent Bootstrap (self) ----
|
|
826
|
+
async getAgentMe(orgId) {
|
|
827
|
+
return this.request("GET", ENDPOINTS.AGENT_ME(orgId));
|
|
828
|
+
}
|
|
829
|
+
// ---- Agent Sessions (org-scoped) ----
|
|
830
|
+
async createAgentSession(orgId, agentId, req) {
|
|
831
|
+
return this.request("POST", ENDPOINTS.AGENT_SESSIONS(orgId, agentId), req);
|
|
832
|
+
}
|
|
833
|
+
/**
|
|
834
|
+
* List agent sessions.
|
|
835
|
+
* @param params.status - Comma-separated status filter (e.g., `'open'`).
|
|
836
|
+
*/
|
|
837
|
+
async getAgentSessions(orgId, agentId, params) {
|
|
838
|
+
const res = await this.request("GET", ENDPOINTS.AGENT_SESSIONS(orgId, agentId), void 0, params);
|
|
839
|
+
return res.data;
|
|
840
|
+
}
|
|
841
|
+
async getAgentSession(orgId, agentId, sessionId) {
|
|
842
|
+
return this.request("GET", ENDPOINTS.AGENT_SESSION(orgId, agentId, sessionId));
|
|
843
|
+
}
|
|
844
|
+
async updateAgentSession(orgId, agentId, sessionId, req) {
|
|
845
|
+
return this.request("PATCH", ENDPOINTS.AGENT_SESSION(orgId, agentId, sessionId), req);
|
|
846
|
+
}
|
|
847
|
+
async createAgentStep(orgId, agentId, sessionId, req) {
|
|
848
|
+
return this.request("POST", ENDPOINTS.AGENT_SESSION_STEPS(orgId, agentId, sessionId), req);
|
|
849
|
+
}
|
|
850
|
+
async getAgentSessionSteps(orgId, agentId, sessionId, params) {
|
|
851
|
+
const res = await this.request("GET", ENDPOINTS.AGENT_SESSION_STEPS(orgId, agentId, sessionId), void 0, params);
|
|
852
|
+
return res.data;
|
|
853
|
+
}
|
|
854
|
+
async getAgentSessionStep(orgId, agentId, sessionId, stepId) {
|
|
855
|
+
return this.request("GET", ENDPOINTS.AGENT_SESSION_STEP(orgId, agentId, sessionId, stepId));
|
|
856
|
+
}
|
|
857
|
+
// ---- Agent runtime auth (hosted Claude OAuth) ----
|
|
858
|
+
async getAgentRuntimeAuth(orgId, agentId) {
|
|
859
|
+
return this.request("GET", ENDPOINTS.AGENT_RUNTIME_AUTH(orgId, agentId));
|
|
860
|
+
}
|
|
861
|
+
async startAgentRuntimeAuthSession(orgId, agentId, req = {}) {
|
|
862
|
+
return this.request("POST", ENDPOINTS.AGENT_RUNTIME_AUTH_SESSIONS(orgId, agentId), req);
|
|
863
|
+
}
|
|
864
|
+
async completeAgentRuntimeAuthSession(orgId, agentId, sessionId, req) {
|
|
865
|
+
return this.request("POST", ENDPOINTS.AGENT_RUNTIME_AUTH_SESSION_COMPLETE(orgId, agentId, sessionId), req);
|
|
866
|
+
}
|
|
867
|
+
async disconnectAgentRuntimeAuth(orgId, agentId) {
|
|
868
|
+
return this.request("DELETE", ENDPOINTS.AGENT_RUNTIME_AUTH(orgId, agentId));
|
|
869
|
+
}
|
|
870
|
+
// ---- Agent runtime update mode (hot-update) ----
|
|
871
|
+
async getAgentRuntime(orgId, agentId) {
|
|
872
|
+
return this.request("GET", ENDPOINTS.AGENT_RUNTIME(orgId, agentId));
|
|
873
|
+
}
|
|
874
|
+
async setAgentRuntimeMode(orgId, agentId, req) {
|
|
875
|
+
return this.request("PATCH", ENDPOINTS.AGENT_RUNTIME(orgId, agentId), req);
|
|
876
|
+
}
|
|
877
|
+
async upgradeAgentRuntime(orgId, agentId) {
|
|
878
|
+
return this.request("POST", ENDPOINTS.AGENT_RUNTIME_UPGRADE(orgId, agentId));
|
|
879
|
+
}
|
|
880
|
+
async listAgentRuntimeTags(orgId, agentId) {
|
|
881
|
+
return this.request("GET", ENDPOINTS.AGENT_RUNTIME_AVAILABLE_TAGS(orgId, agentId));
|
|
882
|
+
}
|
|
883
|
+
async getAgentRuntimeRelease(orgId, agentId, tag) {
|
|
884
|
+
return this.request("GET", ENDPOINTS.AGENT_RUNTIME_RELEASE(orgId, agentId, tag));
|
|
885
|
+
}
|
|
886
|
+
/** Fetch all pending tasks (todo/in_progress) assigned to an agent. Pages automatically. */
|
|
887
|
+
async getAgentTasks(orgId, agentId) {
|
|
888
|
+
const all = [];
|
|
889
|
+
let cursor;
|
|
890
|
+
do {
|
|
891
|
+
const params = { limit: "100" };
|
|
892
|
+
if (cursor)
|
|
893
|
+
params.cursor = cursor;
|
|
894
|
+
const res = await this.request("GET", ENDPOINTS.AGENT_TASKS(orgId, agentId), void 0, params);
|
|
895
|
+
all.push(...res.data);
|
|
896
|
+
cursor = res.has_more ? res.next_cursor : void 0;
|
|
897
|
+
} while (cursor);
|
|
898
|
+
return all;
|
|
899
|
+
}
|
|
900
|
+
// ---- Machines (org-scoped) ----
|
|
901
|
+
async getMachines(orgId) {
|
|
902
|
+
const res = await this.request("GET", ENDPOINTS.MACHINES(orgId));
|
|
903
|
+
return res.data;
|
|
904
|
+
}
|
|
905
|
+
async getMachine(orgId, machineId) {
|
|
906
|
+
return this.request("GET", ENDPOINTS.MACHINE(orgId, machineId));
|
|
907
|
+
}
|
|
908
|
+
async deleteMachine(orgId, machineId) {
|
|
909
|
+
return this.request("DELETE", ENDPOINTS.MACHINE(orgId, machineId));
|
|
910
|
+
}
|
|
911
|
+
async startMachine(orgId, machineId) {
|
|
912
|
+
return this.request("POST", ENDPOINTS.MACHINE_START(orgId, machineId));
|
|
913
|
+
}
|
|
914
|
+
async stopMachine(orgId, machineId) {
|
|
915
|
+
return this.request("POST", ENDPOINTS.MACHINE_STOP(orgId, machineId));
|
|
916
|
+
}
|
|
917
|
+
async getMachineStatus(orgId, machineId) {
|
|
918
|
+
return this.request("GET", ENDPOINTS.MACHINE_STATUS(orgId, machineId));
|
|
919
|
+
}
|
|
920
|
+
async getMachineLogs(orgId, machineId, lines = 100) {
|
|
921
|
+
return this.request("GET", ENDPOINTS.MACHINE_LOGS(orgId, machineId), void 0, { lines });
|
|
922
|
+
}
|
|
923
|
+
async restartMachine(orgId, machineId) {
|
|
924
|
+
return this.request("POST", ENDPOINTS.MACHINE_RESTART(orgId, machineId));
|
|
925
|
+
}
|
|
926
|
+
async restartAllMachines(orgId) {
|
|
927
|
+
return this.request("POST", ENDPOINTS.MACHINE_RESTART_ALL(orgId));
|
|
928
|
+
}
|
|
929
|
+
// ---- Daemon-mode Machine management (org-scoped, user auth) ----
|
|
930
|
+
/**
|
|
931
|
+
* Create a new daemon-mode Machine. Returns the Machine row + one-shot
|
|
932
|
+
* mck_ token. The UI uses this when the user clicks "Create Workspace".
|
|
933
|
+
*/
|
|
934
|
+
async createMachine(orgId, opts) {
|
|
935
|
+
return this.request("POST", ENDPOINTS.MACHINES(orgId), opts);
|
|
936
|
+
}
|
|
937
|
+
/**
|
|
938
|
+
* Returns only daemon_mode=true machines (non-terminated). Backs the
|
|
939
|
+
* "Workspaces" UI list. Legacy 1:1 machines are excluded.
|
|
940
|
+
*/
|
|
941
|
+
async getDaemonMachines(orgId) {
|
|
942
|
+
const all = await this.getMachines(orgId);
|
|
943
|
+
return all.filter((m) => m.daemon_mode && m.status !== "terminated");
|
|
944
|
+
}
|
|
945
|
+
/** Attach an agent to a daemon-mode Machine. */
|
|
946
|
+
async attachAgent(orgId, machineId, agentId, opts) {
|
|
947
|
+
return this.request("POST", ENDPOINTS.MACHINE_ATTACH_AGENT(orgId, machineId, agentId), opts);
|
|
948
|
+
}
|
|
949
|
+
/** Detach an agent from a daemon-mode Machine. */
|
|
950
|
+
async detachAgent(orgId, machineId, agentId) {
|
|
951
|
+
return this.request("DELETE", ENDPOINTS.MACHINE_DETACH_AGENT(orgId, machineId, agentId));
|
|
952
|
+
}
|
|
953
|
+
/** Get machine-level runtime auth state. */
|
|
954
|
+
async getMachineRuntimeAuth(orgId, machineId) {
|
|
955
|
+
return this.request("GET", ENDPOINTS.MACHINE_RUNTIME_AUTH(orgId, machineId));
|
|
956
|
+
}
|
|
957
|
+
// ---- Machine self-control-plane (mck_-scoped) ----
|
|
958
|
+
//
|
|
959
|
+
// The four methods below are intended to be called from a daemon-mode
|
|
960
|
+
// Machine using its `mck_*` bearer as the API key. They identify the
|
|
961
|
+
// Machine implicitly via the bearer; there is no `machineId` parameter.
|
|
962
|
+
// Calling them with a JWT or `agk_` will get a 401 from the server's
|
|
963
|
+
// RequireMachineAuth middleware.
|
|
964
|
+
/** `GET /machines/me` — returns the calling Machine's own row. */
|
|
965
|
+
async getMachineSelf() {
|
|
966
|
+
return this.request("GET", ENDPOINTS.MACHINES_ME);
|
|
967
|
+
}
|
|
968
|
+
/**
|
|
969
|
+
* `GET /machines/me/agents` — enumerate the AgentProfiles attached to
|
|
970
|
+
* the calling Machine. The supervisor uses this list to decide which
|
|
971
|
+
* per-agent runtime subprocesses to keep alive.
|
|
972
|
+
*/
|
|
973
|
+
async listAttachedAgents() {
|
|
974
|
+
const res = await this.request("GET", ENDPOINTS.MACHINES_ME_AGENTS);
|
|
975
|
+
return res.data;
|
|
976
|
+
}
|
|
977
|
+
/**
|
|
978
|
+
* `POST /machines/me/health` — bump the Machine's `updated_at` to now.
|
|
979
|
+
* Body is intentionally empty; the server ignores any payload. The
|
|
980
|
+
* daemon should call this on a fixed cadence (e.g. every 30s) so an
|
|
981
|
+
* external observer can detect a wedged supervisor.
|
|
982
|
+
*/
|
|
983
|
+
async postMachineHeartbeat() {
|
|
984
|
+
return this.request("POST", ENDPOINTS.MACHINES_ME_HEALTH);
|
|
985
|
+
}
|
|
986
|
+
/**
|
|
987
|
+
* `POST /machines/me/agents/{agentId}/launch-credential` — mint a
|
|
988
|
+
* short-lived `agk_*` for one of this Machine's attached agents. The
|
|
989
|
+
* daemon uses the returned `api_key` as `PRLL_API_KEY` for that
|
|
990
|
+
* agent's subprocess. Server returns 404 if the agent isn't
|
|
991
|
+
* attached to the calling Machine, or 410-equivalent if the agent
|
|
992
|
+
* was suspended.
|
|
993
|
+
*/
|
|
994
|
+
async mintLaunchCredential(agentId) {
|
|
995
|
+
return this.request("POST", ENDPOINTS.MACHINES_ME_AGENT_LAUNCH_CREDENTIAL(agentId));
|
|
996
|
+
}
|
|
997
|
+
async getMachineWsTicket() {
|
|
998
|
+
return this.request("POST", ENDPOINTS.MACHINES_ME_WS_TICKET);
|
|
999
|
+
}
|
|
1000
|
+
async resizeMachine(orgId, machineId, spec) {
|
|
1001
|
+
return this.request("PATCH", ENDPOINTS.MACHINE_SPEC(orgId, machineId), spec);
|
|
1002
|
+
}
|
|
1003
|
+
/** Create a new machine key. Returns the raw key string (shown once) + metadata. */
|
|
1004
|
+
async createMachineKey(orgId, machineId, name) {
|
|
1005
|
+
return this.request("POST", ENDPOINTS.MACHINE_KEYS(orgId, machineId), name ? { name } : void 0);
|
|
1006
|
+
}
|
|
1007
|
+
/** Revoke (soft-delete) a machine key by ID. */
|
|
1008
|
+
async revokeMachineKey(orgId, machineId, keyId) {
|
|
1009
|
+
return this.request("DELETE", ENDPOINTS.MACHINE_KEY(orgId, machineId, keyId));
|
|
1010
|
+
}
|
|
1011
|
+
// Wiki mount/token methods removed — wiki-service handles all wiki endpoints.
|
|
1012
|
+
// ---- Unread ----
|
|
1013
|
+
async getUnreadCounts(orgId) {
|
|
1014
|
+
const endpoint = orgId ? ENDPOINTS.ORG_UNREAD(orgId) : ENDPOINTS.UNREAD;
|
|
1015
|
+
const res = await this.request("GET", endpoint);
|
|
1016
|
+
return res.data;
|
|
1017
|
+
}
|
|
1018
|
+
async markRead(orgId, chatId, messageId) {
|
|
1019
|
+
return this.request("POST", ENDPOINTS.CHAT_READ(orgId, chatId), { message_id: messageId });
|
|
1020
|
+
}
|
|
1021
|
+
// ---- Inbox ----
|
|
1022
|
+
async getInbox(orgId, params) {
|
|
1023
|
+
return this.request("GET", ENDPOINTS.INBOX(orgId), void 0, params);
|
|
1024
|
+
}
|
|
1025
|
+
async getInboxUnreadCount(orgId) {
|
|
1026
|
+
return this.request("GET", ENDPOINTS.INBOX_UNREAD_COUNT(orgId));
|
|
1027
|
+
}
|
|
1028
|
+
async markInboxRead(orgId, id) {
|
|
1029
|
+
return this.request("PATCH", ENDPOINTS.INBOX_ITEM_READ(orgId, id));
|
|
1030
|
+
}
|
|
1031
|
+
async markInboxUnread(orgId, id) {
|
|
1032
|
+
return this.request("PATCH", ENDPOINTS.INBOX_ITEM_UNREAD(orgId, id));
|
|
1033
|
+
}
|
|
1034
|
+
async archiveInboxItem(orgId, id) {
|
|
1035
|
+
return this.request("PATCH", ENDPOINTS.INBOX_ITEM_ARCHIVE(orgId, id));
|
|
1036
|
+
}
|
|
1037
|
+
// snoozeInboxItem / unsnoozeInboxItem deferred until un-snooze cron worker is implemented
|
|
1038
|
+
async markAllInboxRead(orgId) {
|
|
1039
|
+
return this.request("POST", ENDPOINTS.INBOX_MARK_ALL_READ(orgId));
|
|
1040
|
+
}
|
|
1041
|
+
async archiveAllInbox(orgId) {
|
|
1042
|
+
return this.request("POST", ENDPOINTS.INBOX_ARCHIVE_ALL(orgId));
|
|
1043
|
+
}
|
|
1044
|
+
/** Mark an inbox item as read by its source (source_type + source_id) rather than inbox item ID. */
|
|
1045
|
+
async ackInbox(orgId, source) {
|
|
1046
|
+
return this.request("POST", ENDPOINTS.INBOX_ACK(orgId), source);
|
|
1047
|
+
}
|
|
1048
|
+
async deleteInboxItem(orgId, id) {
|
|
1049
|
+
return this.request("DELETE", ENDPOINTS.INBOX_ITEM(orgId, id));
|
|
1050
|
+
}
|
|
1051
|
+
// ---- Dispatch (agent event delivery) ----
|
|
1052
|
+
async getDispatch(orgId, params) {
|
|
1053
|
+
return this.request("GET", ENDPOINTS.DISPATCH(orgId), void 0, params);
|
|
1054
|
+
}
|
|
1055
|
+
async getDispatchPendingCount(orgId) {
|
|
1056
|
+
return this.request("GET", ENDPOINTS.DISPATCH_PENDING_COUNT(orgId));
|
|
1057
|
+
}
|
|
1058
|
+
async ackDispatch(orgId, source) {
|
|
1059
|
+
return this.request("POST", ENDPOINTS.DISPATCH_ACK(orgId), source);
|
|
1060
|
+
}
|
|
1061
|
+
async ackDispatchByID(orgId, id) {
|
|
1062
|
+
return this.request("POST", ENDPOINTS.DISPATCH_ACK_BY_ID(orgId, id));
|
|
1063
|
+
}
|
|
1064
|
+
// ---- Platform Config ----
|
|
1065
|
+
/**
|
|
1066
|
+
* Fetch platform config for the authenticated agent.
|
|
1067
|
+
* Pass currentVersion to enable conditional fetch (If-None-Match / 304).
|
|
1068
|
+
* Returns null when the server responds with 304 (config unchanged).
|
|
1069
|
+
*/
|
|
1070
|
+
async getPlatformConfig(currentVersion) {
|
|
1071
|
+
const url = `${this.baseUrl}${ENDPOINTS.PLATFORM_CONFIG}`;
|
|
1072
|
+
const extra = {};
|
|
1073
|
+
if (currentVersion !== void 0) {
|
|
1074
|
+
extra["If-None-Match"] = currentVersion;
|
|
1075
|
+
}
|
|
1076
|
+
const headers = this.buildHeaders(extra);
|
|
1077
|
+
let res;
|
|
1078
|
+
try {
|
|
1079
|
+
res = await fetch(url, {
|
|
1080
|
+
method: "GET",
|
|
1081
|
+
headers,
|
|
1082
|
+
signal: AbortSignal.timeout(15e3)
|
|
1083
|
+
});
|
|
1084
|
+
} catch (err) {
|
|
1085
|
+
throw _ParallClient.normalizeFetchError(err);
|
|
1086
|
+
}
|
|
1087
|
+
if (res.status === 304)
|
|
1088
|
+
return null;
|
|
1089
|
+
if (!res.ok) {
|
|
1090
|
+
const rawErrorBody = await res.json().catch(() => ({}));
|
|
1091
|
+
const errorBody = rawErrorBody !== null && typeof rawErrorBody === "object" ? rawErrorBody : {};
|
|
1092
|
+
const errorObj = errorBody?.error && typeof errorBody.error === "object" ? errorBody.error : void 0;
|
|
1093
|
+
const errMsg = errorObj?.message ?? (typeof errorBody?.error === "string" ? errorBody.error : void 0);
|
|
1094
|
+
const errCode = errorObj?.code ?? (typeof errorBody?.code === "string" ? errorBody.code : void 0);
|
|
1095
|
+
const apiError = new ApiError(res.status, errMsg ?? res.statusText, errCode);
|
|
1096
|
+
const { error: _e, code: _c, message: _m, ...extras } = errorBody;
|
|
1097
|
+
if (Object.keys(extras).length > 0)
|
|
1098
|
+
apiError.extras = extras;
|
|
1099
|
+
throw apiError;
|
|
1100
|
+
}
|
|
1101
|
+
return res.json();
|
|
1102
|
+
}
|
|
1103
|
+
// ---- Tasks (org-scoped) ----
|
|
1104
|
+
async createTask(orgId, data) {
|
|
1105
|
+
return this.request("POST", ENDPOINTS.TASKS(orgId), data);
|
|
1106
|
+
}
|
|
1107
|
+
async getTasks(orgId, params) {
|
|
1108
|
+
return this.request("GET", ENDPOINTS.TASKS(orgId), void 0, params);
|
|
1109
|
+
}
|
|
1110
|
+
async getTask(orgId, taskId) {
|
|
1111
|
+
return this.request("GET", ENDPOINTS.TASK(orgId, taskId));
|
|
1112
|
+
}
|
|
1113
|
+
async updateTask(orgId, taskId, data) {
|
|
1114
|
+
return this.request("PATCH", ENDPOINTS.TASK(orgId, taskId), data);
|
|
1115
|
+
}
|
|
1116
|
+
async deleteTask(orgId, taskId, opts) {
|
|
1117
|
+
return this.request("DELETE", ENDPOINTS.TASK(orgId, taskId), void 0, opts?.force ? { force: "true" } : void 0);
|
|
1118
|
+
}
|
|
1119
|
+
async archiveTask(orgId, taskId) {
|
|
1120
|
+
return this.request("POST", ENDPOINTS.TASK_ARCHIVE(orgId, taskId));
|
|
1121
|
+
}
|
|
1122
|
+
async restoreTask(orgId, taskId) {
|
|
1123
|
+
return this.request("POST", ENDPOINTS.TASK_RESTORE(orgId, taskId));
|
|
1124
|
+
}
|
|
1125
|
+
async watchTask(orgId, taskId) {
|
|
1126
|
+
return this.request("POST", ENDPOINTS.TASK_WATCH(orgId, taskId));
|
|
1127
|
+
}
|
|
1128
|
+
async unwatchTask(orgId, taskId) {
|
|
1129
|
+
return this.request("DELETE", ENDPOINTS.TASK_WATCH(orgId, taskId));
|
|
1130
|
+
}
|
|
1131
|
+
async getTaskWatchers(orgId, taskId) {
|
|
1132
|
+
const res = await this.request("GET", ENDPOINTS.TASK_WATCHERS(orgId, taskId));
|
|
1133
|
+
return res.data;
|
|
1134
|
+
}
|
|
1135
|
+
async getSubtasks(orgId, taskId) {
|
|
1136
|
+
const res = await this.request("GET", ENDPOINTS.TASK_SUBTASKS(orgId, taskId));
|
|
1137
|
+
return res.data;
|
|
1138
|
+
}
|
|
1139
|
+
async createTaskRelation(orgId, taskId, data) {
|
|
1140
|
+
return this.request("POST", ENDPOINTS.TASK_RELATIONS(orgId, taskId), data);
|
|
1141
|
+
}
|
|
1142
|
+
async getTaskRelations(orgId, taskId) {
|
|
1143
|
+
const res = await this.request("GET", ENDPOINTS.TASK_RELATIONS(orgId, taskId));
|
|
1144
|
+
return res.data;
|
|
1145
|
+
}
|
|
1146
|
+
async listTaskRelationsByTarget(orgId, params) {
|
|
1147
|
+
const query = new URLSearchParams({
|
|
1148
|
+
target_type: params.target_type,
|
|
1149
|
+
target_id: params.target_id
|
|
1150
|
+
});
|
|
1151
|
+
const res = await this.request("GET", `${ENDPOINTS.TASK_RELATIONS_BY_TARGET(orgId)}?${query.toString()}`);
|
|
1152
|
+
return res.data;
|
|
1153
|
+
}
|
|
1154
|
+
async deleteTaskRelation(orgId, taskId, relationId) {
|
|
1155
|
+
return this.request("DELETE", ENDPOINTS.TASK_RELATION(orgId, taskId, relationId));
|
|
1156
|
+
}
|
|
1157
|
+
// ---- Task Comments ----
|
|
1158
|
+
async getTaskComments(orgId, taskId, params) {
|
|
1159
|
+
return this.request("GET", ENDPOINTS.TASK_COMMENTS(orgId, taskId), void 0, params);
|
|
1160
|
+
}
|
|
1161
|
+
async getTaskComment(orgId, taskId, commentId) {
|
|
1162
|
+
return this.request("GET", ENDPOINTS.TASK_COMMENT(orgId, taskId, commentId));
|
|
1163
|
+
}
|
|
1164
|
+
async createTaskComment(orgId, taskId, data) {
|
|
1165
|
+
return this.request("POST", ENDPOINTS.TASK_COMMENTS(orgId, taskId), data);
|
|
1166
|
+
}
|
|
1167
|
+
async updateTaskComment(orgId, taskId, commentId, data) {
|
|
1168
|
+
return this.request("PATCH", ENDPOINTS.TASK_COMMENT(orgId, taskId, commentId), data);
|
|
1169
|
+
}
|
|
1170
|
+
async deleteTaskComment(orgId, taskId, commentId) {
|
|
1171
|
+
return this.request("DELETE", ENDPOINTS.TASK_COMMENT(orgId, taskId, commentId));
|
|
1172
|
+
}
|
|
1173
|
+
// ---- Task Activities ----
|
|
1174
|
+
async getTaskActivities(orgId, taskId, params) {
|
|
1175
|
+
return this.request("GET", ENDPOINTS.TASK_ACTIVITIES(orgId, taskId), void 0, params);
|
|
1176
|
+
}
|
|
1177
|
+
// ---- Projects (org-scoped) ----
|
|
1178
|
+
async createProject(orgId, data) {
|
|
1179
|
+
return this.request("POST", ENDPOINTS.PROJECTS(orgId), data);
|
|
1180
|
+
}
|
|
1181
|
+
async getProjects(orgId) {
|
|
1182
|
+
const res = await this.request("GET", ENDPOINTS.PROJECTS(orgId));
|
|
1183
|
+
return res.data;
|
|
1184
|
+
}
|
|
1185
|
+
async getProject(orgId, projectId) {
|
|
1186
|
+
return this.request("GET", ENDPOINTS.PROJECT(orgId, projectId));
|
|
1187
|
+
}
|
|
1188
|
+
async updateProject(orgId, projectId, data) {
|
|
1189
|
+
return this.request("PATCH", ENDPOINTS.PROJECT(orgId, projectId), data);
|
|
1190
|
+
}
|
|
1191
|
+
async deleteProject(orgId, projectId) {
|
|
1192
|
+
return this.request("DELETE", ENDPOINTS.PROJECT(orgId, projectId));
|
|
1193
|
+
}
|
|
1194
|
+
// ---- Schedules (org-scoped) ----
|
|
1195
|
+
async createSchedule(orgId, input) {
|
|
1196
|
+
return this.request("POST", ENDPOINTS.SCHEDULES(orgId), input);
|
|
1197
|
+
}
|
|
1198
|
+
async listSchedules(orgId, filters) {
|
|
1199
|
+
return this.request("GET", ENDPOINTS.SCHEDULES(orgId), void 0, filters);
|
|
1200
|
+
}
|
|
1201
|
+
async getSchedule(orgId, scheduleId) {
|
|
1202
|
+
return this.request("GET", ENDPOINTS.SCHEDULE(orgId, scheduleId));
|
|
1203
|
+
}
|
|
1204
|
+
async updateSchedule(orgId, scheduleId, patch) {
|
|
1205
|
+
return this.request("PATCH", ENDPOINTS.SCHEDULE(orgId, scheduleId), patch);
|
|
1206
|
+
}
|
|
1207
|
+
async pauseSchedule(orgId, scheduleId) {
|
|
1208
|
+
return this.request("POST", ENDPOINTS.SCHEDULE_PAUSE(orgId, scheduleId));
|
|
1209
|
+
}
|
|
1210
|
+
async resumeSchedule(orgId, scheduleId) {
|
|
1211
|
+
return this.request("POST", ENDPOINTS.SCHEDULE_RESUME(orgId, scheduleId));
|
|
1212
|
+
}
|
|
1213
|
+
async cancelSchedule(orgId, scheduleId) {
|
|
1214
|
+
return this.request("POST", ENDPOINTS.SCHEDULE_CANCEL(orgId, scheduleId));
|
|
1215
|
+
}
|
|
1216
|
+
async deleteSchedule(orgId, scheduleId) {
|
|
1217
|
+
return this.request("DELETE", ENDPOINTS.SCHEDULE(orgId, scheduleId));
|
|
1218
|
+
}
|
|
1219
|
+
async listScheduleRuns(orgId, scheduleId, filters) {
|
|
1220
|
+
return this.request("GET", ENDPOINTS.SCHEDULE_RUNS(orgId, scheduleId), void 0, filters);
|
|
1221
|
+
}
|
|
1222
|
+
async getScheduleRun(orgId, runId) {
|
|
1223
|
+
return this.request("GET", ENDPOINTS.SCHEDULE_RUN(orgId, runId));
|
|
1224
|
+
}
|
|
1225
|
+
// ---- Wikis (org-scoped) ----
|
|
1226
|
+
async createWiki(orgId, data) {
|
|
1227
|
+
return this.request("POST", ENDPOINTS.WIKIS(orgId), data);
|
|
1228
|
+
}
|
|
1229
|
+
async getWikis(orgId) {
|
|
1230
|
+
const res = await this.request("GET", ENDPOINTS.WIKIS(orgId));
|
|
1231
|
+
return res.data;
|
|
1232
|
+
}
|
|
1233
|
+
async getWiki(orgId, wikiId) {
|
|
1234
|
+
return this.request("GET", ENDPOINTS.WIKI(orgId, wikiId));
|
|
1235
|
+
}
|
|
1236
|
+
async getWikiTree(orgId, wikiId, params) {
|
|
1237
|
+
return this.request("GET", ENDPOINTS.WIKI_TREE(orgId, wikiId), void 0, params);
|
|
1238
|
+
}
|
|
1239
|
+
async getWikiBlob(orgId, wikiId, params) {
|
|
1240
|
+
return this.request("GET", ENDPOINTS.WIKI_BLOB(orgId, wikiId), void 0, params);
|
|
1241
|
+
}
|
|
1242
|
+
async getWikiNodeSections(orgId, wikiId, params) {
|
|
1243
|
+
return this.request("GET", ENDPOINTS.WIKI_NODE_SECTIONS(orgId, wikiId), void 0, params);
|
|
1244
|
+
}
|
|
1245
|
+
async searchWiki(orgId, wikiId, params) {
|
|
1246
|
+
return this.request("GET", ENDPOINTS.WIKI_SEARCH(orgId, wikiId), void 0, params);
|
|
1247
|
+
}
|
|
1248
|
+
async getWikiPageIndex(orgId, wikiId, params) {
|
|
1249
|
+
return this.request("GET", ENDPOINTS.WIKI_PAGE_INDEX(orgId, wikiId), void 0, params);
|
|
1250
|
+
}
|
|
1251
|
+
async getWikiRefs(orgId, wikiId, params) {
|
|
1252
|
+
return this.request("GET", ENDPOINTS.WIKI_REFS(orgId, wikiId), void 0, params);
|
|
1253
|
+
}
|
|
1254
|
+
async checkWikiRefs(orgId, wikiId, params) {
|
|
1255
|
+
return this.request("GET", ENDPOINTS.WIKI_REFS_CHECK(orgId, wikiId), void 0, params);
|
|
1256
|
+
}
|
|
1257
|
+
// Anchor-status: batch blob-SHA compare for inline-comment outdated judging.
|
|
1258
|
+
// Refs the caller cannot read are silently dropped from the response.
|
|
1259
|
+
async getWikiAnchorStatus(orgId, wikiId, body) {
|
|
1260
|
+
return this.request("POST", ENDPOINTS.WIKI_ANCHOR_STATUS(orgId, wikiId), body);
|
|
1261
|
+
}
|
|
1262
|
+
async createWikiChangeset(orgId, wikiId, data) {
|
|
1263
|
+
return normalizeWikiChangeset(await this.request("POST", ENDPOINTS.WIKI_CHANGESETS(orgId, wikiId), data));
|
|
1264
|
+
}
|
|
1265
|
+
async getWikiChangesets(orgId, wikiId) {
|
|
1266
|
+
const res = await this.request("GET", ENDPOINTS.WIKI_CHANGESETS(orgId, wikiId));
|
|
1267
|
+
return res.data.map(normalizeWikiChangeset);
|
|
1268
|
+
}
|
|
1269
|
+
async getWikiChangeset(orgId, wikiId, changesetId) {
|
|
1270
|
+
return normalizeWikiChangeset(await this.request("GET", ENDPOINTS.WIKI_CHANGESET(orgId, wikiId, changesetId)));
|
|
1271
|
+
}
|
|
1272
|
+
async updateWikiChangeset(orgId, wikiId, changesetId, data) {
|
|
1273
|
+
return normalizeWikiChangeset(await this.request("PATCH", ENDPOINTS.WIKI_CHANGESET(orgId, wikiId, changesetId), data));
|
|
1274
|
+
}
|
|
1275
|
+
async getWikiChangesetDiff(orgId, wikiId, changesetId) {
|
|
1276
|
+
return this.request("GET", ENDPOINTS.WIKI_CHANGESET_DIFF(orgId, wikiId, changesetId));
|
|
1277
|
+
}
|
|
1278
|
+
async mergeWikiChangeset(orgId, wikiId, changesetId) {
|
|
1279
|
+
return normalizeWikiChangeset(await this.request("POST", ENDPOINTS.WIKI_CHANGESET_MERGE(orgId, wikiId, changesetId)));
|
|
1280
|
+
}
|
|
1281
|
+
async closeWikiChangeset(orgId, wikiId, changesetId) {
|
|
1282
|
+
return normalizeWikiChangeset(await this.request("POST", ENDPOINTS.WIKI_CHANGESET_CLOSE(orgId, wikiId, changesetId)));
|
|
1283
|
+
}
|
|
1284
|
+
// ---- Wiki Binary Upload / Delete (Phase 2 of wiki-file-storage-design) ----
|
|
1285
|
+
/**
|
|
1286
|
+
* Upload a binary file directly to the wiki's default branch (maintain
|
|
1287
|
+
* scope). Size ≤ 1 MiB lands as an inline Git blob; > 1 MiB lands as a
|
|
1288
|
+
* Git-LFS pointer backed by S3. Text files return 422 — they must go
|
|
1289
|
+
* through createWikiChangeset / updateWikiChangeset.
|
|
1290
|
+
*/
|
|
1291
|
+
async uploadWikiFile(orgId, wikiId, params) {
|
|
1292
|
+
const fd = new FormData();
|
|
1293
|
+
fd.append("path", params.path);
|
|
1294
|
+
fd.append("file", params.file);
|
|
1295
|
+
if (params.message)
|
|
1296
|
+
fd.append("message", params.message);
|
|
1297
|
+
return this.multipartRequest("POST", ENDPOINTS.WIKI_UPLOADS(orgId, wikiId), fd);
|
|
1298
|
+
}
|
|
1299
|
+
/**
|
|
1300
|
+
* Upload a binary file into a changeset's feature branch (read scope +
|
|
1301
|
+
* author-only). Used by reader flow: propose a changeset, attach
|
|
1302
|
+
* binary, PATCH markdown that references it. On merge the binary
|
|
1303
|
+
* squashes into the default branch.
|
|
1304
|
+
*/
|
|
1305
|
+
async uploadWikiFileToChangeset(orgId, wikiId, changesetId, params) {
|
|
1306
|
+
const fd = new FormData();
|
|
1307
|
+
fd.append("path", params.path);
|
|
1308
|
+
fd.append("file", params.file);
|
|
1309
|
+
if (params.message)
|
|
1310
|
+
fd.append("message", params.message);
|
|
1311
|
+
return this.multipartRequest("POST", ENDPOINTS.WIKI_CHANGESET_FILES(orgId, wikiId, changesetId), fd);
|
|
1312
|
+
}
|
|
1313
|
+
/** Remove a binary file from the default branch. Text files must use a
|
|
1314
|
+
* changeset delete action. The blob stays reachable via git history. */
|
|
1315
|
+
async deleteWikiFile(orgId, wikiId, params) {
|
|
1316
|
+
await this.request("DELETE", ENDPOINTS.WIKI_FILES(orgId, wikiId), void 0, { path: params.path, message: params.message });
|
|
1317
|
+
}
|
|
1318
|
+
/**
|
|
1319
|
+
* Sign a short-lived (~5-min) URL the browser can paste into a media
|
|
1320
|
+
* element src. ACL is evaluated here; the returned URL is a capability
|
|
1321
|
+
* token — don't leak it.
|
|
1322
|
+
*/
|
|
1323
|
+
async getWikiFilePreviewUrl(orgId, wikiId, params) {
|
|
1324
|
+
return this.request("POST", ENDPOINTS.WIKI_FILE_PREVIEW_URL(orgId, wikiId), params);
|
|
1325
|
+
}
|
|
1326
|
+
// ---- Wiki Path Scopes (AFCS ACL) ----
|
|
1327
|
+
async getWikiPathScopes(orgId, wikiId) {
|
|
1328
|
+
const res = await this.request("GET", ENDPOINTS.WIKI_PATH_SCOPES(orgId, wikiId));
|
|
1329
|
+
return res.data;
|
|
1330
|
+
}
|
|
1331
|
+
async createWikiPathScope(orgId, wikiId, data) {
|
|
1332
|
+
return this.request("POST", ENDPOINTS.WIKI_PATH_SCOPES(orgId, wikiId), data);
|
|
1333
|
+
}
|
|
1334
|
+
async deleteWikiPathScope(orgId, wikiId, scopeId) {
|
|
1335
|
+
await this.request("DELETE", ENDPOINTS.WIKI_PATH_SCOPE(orgId, wikiId, scopeId));
|
|
1336
|
+
}
|
|
1337
|
+
async getWikiAccessStatus(orgId, wikiId, path4) {
|
|
1338
|
+
return this.request("GET", ENDPOINTS.WIKI_ACCESS_STATUS(orgId, wikiId), void 0, path4 ? { path: path4 } : void 0);
|
|
1339
|
+
}
|
|
1340
|
+
async createWikiAccessRequest(orgId, wikiId, data) {
|
|
1341
|
+
await this.request("POST", ENDPOINTS.WIKI_ACCESS_REQUESTS(orgId, wikiId), data);
|
|
1342
|
+
}
|
|
1343
|
+
// ---- Wiki History (commits, file commits, blame) ----
|
|
1344
|
+
async getWikiCommits(orgId, wikiId, params) {
|
|
1345
|
+
return this.request("GET", ENDPOINTS.WIKI_COMMITS(orgId, wikiId), void 0, params);
|
|
1346
|
+
}
|
|
1347
|
+
async getWikiFileCommits(orgId, wikiId, path4, params) {
|
|
1348
|
+
return this.request("GET", ENDPOINTS.WIKI_FILE_COMMITS(orgId, wikiId), void 0, { path: path4, ...params });
|
|
1349
|
+
}
|
|
1350
|
+
async getWikiBlame(orgId, wikiId, path4, ref) {
|
|
1351
|
+
return this.request("GET", ENDPOINTS.WIKI_BLAME(orgId, wikiId), void 0, { path: path4, ref });
|
|
1352
|
+
}
|
|
1353
|
+
// ---- Wiki Operations (audit log) ----
|
|
1354
|
+
async getWikiOperations(orgId, wikiId, params) {
|
|
1355
|
+
return this.request("GET", ENDPOINTS.WIKI_OPERATIONS(orgId, wikiId), void 0, params);
|
|
1356
|
+
}
|
|
1357
|
+
async revertWikiOperation(orgId, wikiId, opId) {
|
|
1358
|
+
return this.request("POST", ENDPOINTS.WIKI_OPERATION_REVERT(orgId, wikiId, opId));
|
|
1359
|
+
}
|
|
1360
|
+
// ---- Unified Comments ----
|
|
1361
|
+
async getComments(orgId, params) {
|
|
1362
|
+
return this.request("GET", ENDPOINTS.COMMENTS(orgId), void 0, params);
|
|
1363
|
+
}
|
|
1364
|
+
async getComment(orgId, commentId) {
|
|
1365
|
+
return this.request("GET", ENDPOINTS.COMMENT(orgId, commentId));
|
|
1366
|
+
}
|
|
1367
|
+
async createComment(orgId, data) {
|
|
1368
|
+
return this.request("POST", ENDPOINTS.COMMENTS(orgId), data);
|
|
1369
|
+
}
|
|
1370
|
+
async updateComment(orgId, commentId, data) {
|
|
1371
|
+
return this.request("PATCH", ENDPOINTS.COMMENT(orgId, commentId), data);
|
|
1372
|
+
}
|
|
1373
|
+
async deleteComment(orgId, commentId) {
|
|
1374
|
+
return this.request("DELETE", ENDPOINTS.COMMENT(orgId, commentId));
|
|
1375
|
+
}
|
|
1376
|
+
// ---- References ----
|
|
1377
|
+
async resolveRefs(orgId, refs, options) {
|
|
1378
|
+
const body = { refs };
|
|
1379
|
+
if (options?.sourceMessageId) {
|
|
1380
|
+
body.source_message_id = options.sourceMessageId;
|
|
1381
|
+
}
|
|
1382
|
+
return this.request("POST", ENDPOINTS.REFS_RESOLVE(orgId), body);
|
|
1383
|
+
}
|
|
1384
|
+
async getBacklinks(orgId, params) {
|
|
1385
|
+
return this.request("GET", ENDPOINTS.REFS_BACKLINKS(orgId), void 0, params);
|
|
1386
|
+
}
|
|
1387
|
+
async checkBrokenRefs(orgId) {
|
|
1388
|
+
return this.request("GET", ENDPOINTS.REFS_CHECK(orgId));
|
|
1389
|
+
}
|
|
1390
|
+
// ---- Push Notifications ----
|
|
1391
|
+
async subscribePush(body) {
|
|
1392
|
+
return this.request("POST", ENDPOINTS.PUSH_SUBSCRIBE, body);
|
|
1393
|
+
}
|
|
1394
|
+
async unsubscribePush(token) {
|
|
1395
|
+
return this.request("DELETE", ENDPOINTS.PUSH_UNSUBSCRIBE, { token });
|
|
1396
|
+
}
|
|
1397
|
+
async getVapidKey() {
|
|
1398
|
+
return this.request("GET", ENDPOINTS.PUSH_VAPID_KEY);
|
|
1399
|
+
}
|
|
1400
|
+
// ---- Notification Preferences ----
|
|
1401
|
+
async getNotificationPreferences() {
|
|
1402
|
+
return this.request("GET", ENDPOINTS.NOTIFICATION_PREFERENCES);
|
|
1403
|
+
}
|
|
1404
|
+
async updateNotificationPreferences(prefs) {
|
|
1405
|
+
return this.request("PATCH", ENDPOINTS.NOTIFICATION_PREFERENCES, { prefs });
|
|
1406
|
+
}
|
|
1407
|
+
// ---- Feature Flags (org-scoped, server-evaluated) ----
|
|
1408
|
+
async getFeatureFlags(orgId) {
|
|
1409
|
+
return this.request("GET", ENDPOINTS.FEATURE_FLAGS(orgId));
|
|
1410
|
+
}
|
|
1411
|
+
// ---- Billing & Credits (org-scoped) ----
|
|
1412
|
+
async getBilling(orgId) {
|
|
1413
|
+
return this.request("GET", ENDPOINTS.BILLING(orgId));
|
|
1414
|
+
}
|
|
1415
|
+
async listBillingTransactions(orgId, opts) {
|
|
1416
|
+
const params = new URLSearchParams();
|
|
1417
|
+
if (opts?.cursor)
|
|
1418
|
+
params.set("cursor", opts.cursor);
|
|
1419
|
+
if (opts?.limit)
|
|
1420
|
+
params.set("limit", String(opts.limit));
|
|
1421
|
+
if (opts?.types?.length)
|
|
1422
|
+
params.set("types", opts.types.join(","));
|
|
1423
|
+
if (opts?.user_id)
|
|
1424
|
+
params.set("user_id", opts.user_id);
|
|
1425
|
+
if (opts?.unresolved_actor)
|
|
1426
|
+
params.set("unresolved_actor", "true");
|
|
1427
|
+
if (opts?.machine_id)
|
|
1428
|
+
params.set("machine_id", opts.machine_id);
|
|
1429
|
+
if (opts?.unresolved_machine)
|
|
1430
|
+
params.set("unresolved_machine", "true");
|
|
1431
|
+
const qs = params.toString();
|
|
1432
|
+
return this.request("GET", `${ENDPOINTS.BILLING_TRANSACTIONS(orgId)}${qs ? `?${qs}` : ""}`);
|
|
1433
|
+
}
|
|
1434
|
+
async listBillingTransactionAgentGroups(orgId) {
|
|
1435
|
+
return this.request("GET", `${ENDPOINTS.BILLING_TRANSACTIONS(orgId)}?group_by=agent`);
|
|
1436
|
+
}
|
|
1437
|
+
async listBillingTransactionMachineGroups(orgId) {
|
|
1438
|
+
return this.request("GET", `${ENDPOINTS.BILLING_TRANSACTIONS(orgId)}?group_by=machine`);
|
|
1439
|
+
}
|
|
1440
|
+
async createCheckout(orgId, req) {
|
|
1441
|
+
return this.request("POST", ENDPOINTS.BILLING_CHECKOUT(orgId), req);
|
|
1442
|
+
}
|
|
1443
|
+
/**
|
|
1444
|
+
* Creates a Stripe-hosted card-collection session (Checkout in mode=setup)
|
|
1445
|
+
* and returns the redirect URL. The frontend navigates to `setup_url` and
|
|
1446
|
+
* Stripe handles the card form; the resulting payment_method is recorded
|
|
1447
|
+
* server-side via the `setup_intent.succeeded` webhook. Mirrors
|
|
1448
|
+
* `POST /billing/setup-intent`.
|
|
1449
|
+
*/
|
|
1450
|
+
async createSetupIntent(orgId) {
|
|
1451
|
+
return this.request("POST", ENDPOINTS.BILLING_SETUP_INTENT(orgId));
|
|
1452
|
+
}
|
|
1453
|
+
async getAutoReloadSettings(orgId) {
|
|
1454
|
+
return this.request("GET", ENDPOINTS.BILLING_AUTO_RELOAD(orgId));
|
|
1455
|
+
}
|
|
1456
|
+
async updateAutoReloadSettings(orgId, req) {
|
|
1457
|
+
return this.request("PUT", ENDPOINTS.BILLING_AUTO_RELOAD(orgId), req);
|
|
1458
|
+
}
|
|
1459
|
+
// The admin grant endpoints (POST/GET /internal/admin/orgs/
|
|
1460
|
+
// {id}/grants) intentionally do NOT live on this public client — they
|
|
1461
|
+
// are mounted on the unauthenticated `/internal/*` surface and reachable
|
|
1462
|
+
// only via internal network paths. The admin dashboard maintains its
|
|
1463
|
+
// own thin client (`ts/admin/lib/api.ts`) that hits these directly.
|
|
1464
|
+
async getComputePricing() {
|
|
1465
|
+
const resp = await this.request("GET", ENDPOINTS.COMPUTE_PRICING());
|
|
1466
|
+
return resp.data;
|
|
1467
|
+
}
|
|
1468
|
+
};
|
|
1469
|
+
function normalizeWikiChangeset(changeset) {
|
|
1470
|
+
return {
|
|
1471
|
+
...changeset,
|
|
1472
|
+
changed_paths: changeset.changed_paths ?? [],
|
|
1473
|
+
file_changes: changeset.file_changes ?? []
|
|
1474
|
+
};
|
|
1475
|
+
}
|
|
1476
|
+
var ApiError = class extends Error {
|
|
1477
|
+
status;
|
|
1478
|
+
code;
|
|
1479
|
+
extras;
|
|
1480
|
+
constructor(status, message, code) {
|
|
1481
|
+
super(message);
|
|
1482
|
+
this.status = status;
|
|
1483
|
+
this.code = code;
|
|
1484
|
+
this.name = "ApiError";
|
|
1485
|
+
}
|
|
1486
|
+
};
|
|
1487
|
+
|
|
1488
|
+
// ts/sdk/dist/ws.js
|
|
1489
|
+
var ParallWs = class {
|
|
1490
|
+
ws = null;
|
|
1491
|
+
options;
|
|
1492
|
+
listeners = /* @__PURE__ */ new Map();
|
|
1493
|
+
stateListeners = /* @__PURE__ */ new Set();
|
|
1494
|
+
heartbeatTimer = null;
|
|
1495
|
+
reconnectTimer = null;
|
|
1496
|
+
reconnectAttempts = 0;
|
|
1497
|
+
lastSeq = 0;
|
|
1498
|
+
_state = "disconnected";
|
|
1499
|
+
intentionalClose = false;
|
|
1500
|
+
lastReceivedAt = 0;
|
|
1501
|
+
heartbeatIntervalMs = 0;
|
|
1502
|
+
probeTimer = null;
|
|
1503
|
+
browserListenersActive = false;
|
|
1504
|
+
constructor(options) {
|
|
1505
|
+
this.options = {
|
|
1506
|
+
reconnect: true,
|
|
1507
|
+
reconnectInterval: 1e3,
|
|
1508
|
+
maxReconnectInterval: 3e4,
|
|
1509
|
+
...options
|
|
1510
|
+
};
|
|
1511
|
+
this.lastSeq = options.lastSeq ?? 0;
|
|
1512
|
+
}
|
|
1513
|
+
get state() {
|
|
1514
|
+
return this._state;
|
|
1515
|
+
}
|
|
1516
|
+
async connect() {
|
|
1517
|
+
this.intentionalClose = false;
|
|
1518
|
+
this.setupBrowserListeners();
|
|
1519
|
+
this.setState("connecting");
|
|
1520
|
+
let ticket;
|
|
1521
|
+
try {
|
|
1522
|
+
ticket = await this.options.getTicket();
|
|
1523
|
+
} catch (err) {
|
|
1524
|
+
console.error("Failed to get WS ticket:", err);
|
|
1525
|
+
if (this.options.reconnect) {
|
|
1526
|
+
this.scheduleReconnect();
|
|
1527
|
+
} else {
|
|
1528
|
+
this.setState("disconnected");
|
|
1529
|
+
}
|
|
1530
|
+
return;
|
|
1531
|
+
}
|
|
1532
|
+
const wsUrl = ticket.ws_url || this.options.wsUrl;
|
|
1533
|
+
if (!wsUrl) {
|
|
1534
|
+
console.error("No WS URL available");
|
|
1535
|
+
this.setState("disconnected");
|
|
1536
|
+
return;
|
|
1537
|
+
}
|
|
1538
|
+
const url = new URL(wsUrl);
|
|
1539
|
+
url.searchParams.set("ticket", ticket.ticket);
|
|
1540
|
+
if (this.lastSeq > 0) {
|
|
1541
|
+
url.searchParams.set("last_seq", String(this.lastSeq));
|
|
1542
|
+
}
|
|
1543
|
+
this.ws = new WebSocket(url.toString());
|
|
1544
|
+
const ws = this.ws;
|
|
1545
|
+
const connectTimeout = setTimeout(() => {
|
|
1546
|
+
if (this._state === "connected" || this.intentionalClose)
|
|
1547
|
+
return;
|
|
1548
|
+
ws.onclose = null;
|
|
1549
|
+
ws.onopen = null;
|
|
1550
|
+
ws.onerror = null;
|
|
1551
|
+
try {
|
|
1552
|
+
ws.close();
|
|
1553
|
+
} catch {
|
|
1554
|
+
}
|
|
1555
|
+
if (ws !== this.ws)
|
|
1556
|
+
return;
|
|
1557
|
+
if (this.options.reconnect) {
|
|
1558
|
+
this.scheduleReconnect();
|
|
1559
|
+
} else {
|
|
1560
|
+
this.setState("disconnected");
|
|
1561
|
+
}
|
|
1562
|
+
}, 15e3);
|
|
1563
|
+
this.ws.onopen = () => {
|
|
1564
|
+
clearTimeout(connectTimeout);
|
|
1565
|
+
this.reconnectAttempts = 0;
|
|
1566
|
+
this.lastReceivedAt = Date.now();
|
|
1567
|
+
this.setState("connected");
|
|
1568
|
+
};
|
|
1569
|
+
this.ws.onmessage = (event) => {
|
|
1570
|
+
try {
|
|
1571
|
+
const frame = JSON.parse(event.data);
|
|
1572
|
+
this.handleFrame(frame);
|
|
1573
|
+
} catch {
|
|
1574
|
+
}
|
|
1575
|
+
};
|
|
1576
|
+
this.ws.onclose = () => {
|
|
1577
|
+
clearTimeout(connectTimeout);
|
|
1578
|
+
this.stopHeartbeat();
|
|
1579
|
+
this.clearProbe();
|
|
1580
|
+
if (this.intentionalClose) {
|
|
1581
|
+
this.setState("disconnected");
|
|
1582
|
+
return;
|
|
1583
|
+
}
|
|
1584
|
+
if (this.options.reconnect) {
|
|
1585
|
+
this.scheduleReconnect();
|
|
1586
|
+
} else {
|
|
1587
|
+
this.setState("disconnected");
|
|
1588
|
+
}
|
|
1589
|
+
};
|
|
1590
|
+
this.ws.onerror = () => {
|
|
1591
|
+
};
|
|
1592
|
+
}
|
|
1593
|
+
disconnect() {
|
|
1594
|
+
this.intentionalClose = true;
|
|
1595
|
+
this.stopHeartbeat();
|
|
1596
|
+
this.clearReconnect();
|
|
1597
|
+
this.clearProbe();
|
|
1598
|
+
this.teardownBrowserListeners();
|
|
1599
|
+
if (this.ws) {
|
|
1600
|
+
this.ws.close();
|
|
1601
|
+
this.ws = null;
|
|
1602
|
+
}
|
|
1603
|
+
this.setState("disconnected");
|
|
1604
|
+
}
|
|
1605
|
+
// ---- Client -> Server messages ----
|
|
1606
|
+
/** Tell the server which chats the user is currently viewing (no auth implications). */
|
|
1607
|
+
watch(chatIds) {
|
|
1608
|
+
this.send({ type: WS_EVENTS.WATCH, data: { chat_ids: chatIds } });
|
|
1609
|
+
}
|
|
1610
|
+
sendTyping(chatId, action, threadRootId) {
|
|
1611
|
+
this.send({
|
|
1612
|
+
type: WS_EVENTS.TYPING,
|
|
1613
|
+
data: { chat_id: chatId, thread_root_id: threadRootId ?? null, action }
|
|
1614
|
+
});
|
|
1615
|
+
}
|
|
1616
|
+
/** Send an agent heartbeat with telemetry data. */
|
|
1617
|
+
sendAgentHeartbeat(sessionId, telemetry) {
|
|
1618
|
+
this.send({
|
|
1619
|
+
type: WS_EVENTS.AGENT_HEARTBEAT,
|
|
1620
|
+
data: { session_id: sessionId, telemetry }
|
|
1621
|
+
});
|
|
1622
|
+
}
|
|
1623
|
+
sendEvent(type, data) {
|
|
1624
|
+
this.send({ type, data });
|
|
1625
|
+
}
|
|
1626
|
+
// ---- Event listeners ----
|
|
1627
|
+
on(event, handler) {
|
|
1628
|
+
let set = this.listeners.get(event);
|
|
1629
|
+
if (!set) {
|
|
1630
|
+
set = /* @__PURE__ */ new Set();
|
|
1631
|
+
this.listeners.set(event, set);
|
|
1632
|
+
}
|
|
1633
|
+
set.add(handler);
|
|
1634
|
+
return () => {
|
|
1635
|
+
set.delete(handler);
|
|
1636
|
+
};
|
|
1637
|
+
}
|
|
1638
|
+
off(event, handler) {
|
|
1639
|
+
this.listeners.get(event)?.delete(handler);
|
|
1640
|
+
}
|
|
1641
|
+
onStateChange(handler) {
|
|
1642
|
+
this.stateListeners.add(handler);
|
|
1643
|
+
return () => {
|
|
1644
|
+
this.stateListeners.delete(handler);
|
|
1645
|
+
};
|
|
1646
|
+
}
|
|
1647
|
+
// ---- Internal ----
|
|
1648
|
+
send(frame) {
|
|
1649
|
+
if (this.ws?.readyState === WebSocket.OPEN) {
|
|
1650
|
+
this.ws.send(JSON.stringify(frame));
|
|
1651
|
+
}
|
|
1652
|
+
}
|
|
1653
|
+
handleFrame(frame) {
|
|
1654
|
+
this.lastReceivedAt = Date.now();
|
|
1655
|
+
this.clearProbe();
|
|
1656
|
+
if (frame.seq !== void 0) {
|
|
1657
|
+
this.lastSeq = frame.seq;
|
|
1658
|
+
}
|
|
1659
|
+
if (frame.type === WS_EVENTS.HELLO) {
|
|
1660
|
+
const interval = frame.data.heartbeat_interval;
|
|
1661
|
+
this.startHeartbeat(interval);
|
|
1662
|
+
}
|
|
1663
|
+
const handlers = this.listeners.get(frame.type);
|
|
1664
|
+
if (handlers) {
|
|
1665
|
+
for (const handler of handlers) {
|
|
1666
|
+
handler(frame.data, frame.seq);
|
|
1667
|
+
}
|
|
1668
|
+
}
|
|
1669
|
+
}
|
|
1670
|
+
startHeartbeat(intervalSec) {
|
|
1671
|
+
this.stopHeartbeat();
|
|
1672
|
+
if (!Number.isFinite(intervalSec) || intervalSec <= 0) {
|
|
1673
|
+
console.error("Invalid heartbeat interval from server:", intervalSec);
|
|
1674
|
+
return;
|
|
1675
|
+
}
|
|
1676
|
+
this.heartbeatIntervalMs = intervalSec * 1e3;
|
|
1677
|
+
this.heartbeatTimer = setInterval(() => {
|
|
1678
|
+
if (this.lastReceivedAt > 0 && Date.now() - this.lastReceivedAt > this.heartbeatIntervalMs * 1.5) {
|
|
1679
|
+
this.probeConnection();
|
|
1680
|
+
return;
|
|
1681
|
+
}
|
|
1682
|
+
this.send({ type: WS_EVENTS.PING, data: { ts: Date.now() } });
|
|
1683
|
+
}, intervalSec * 1e3);
|
|
1684
|
+
}
|
|
1685
|
+
stopHeartbeat() {
|
|
1686
|
+
if (this.heartbeatTimer) {
|
|
1687
|
+
clearInterval(this.heartbeatTimer);
|
|
1688
|
+
this.heartbeatTimer = null;
|
|
1689
|
+
}
|
|
1690
|
+
}
|
|
1691
|
+
scheduleReconnect() {
|
|
1692
|
+
this.setState("reconnecting");
|
|
1693
|
+
this.clearReconnect();
|
|
1694
|
+
const base = Math.min(this.options.reconnectInterval * Math.pow(2, this.reconnectAttempts), this.options.maxReconnectInterval);
|
|
1695
|
+
const delay = base * (0.5 + Math.random() * 0.5);
|
|
1696
|
+
this.reconnectAttempts++;
|
|
1697
|
+
this.reconnectTimer = setTimeout(() => {
|
|
1698
|
+
this.connect();
|
|
1699
|
+
}, delay);
|
|
1700
|
+
}
|
|
1701
|
+
clearReconnect() {
|
|
1702
|
+
if (this.reconnectTimer) {
|
|
1703
|
+
clearTimeout(this.reconnectTimer);
|
|
1704
|
+
this.reconnectTimer = null;
|
|
1705
|
+
}
|
|
1706
|
+
}
|
|
1707
|
+
/** Force-close a dead/stale connection and trigger reconnect. */
|
|
1708
|
+
forceReconnect() {
|
|
1709
|
+
this.stopHeartbeat();
|
|
1710
|
+
this.clearReconnect();
|
|
1711
|
+
this.clearProbe();
|
|
1712
|
+
if (this.ws) {
|
|
1713
|
+
this.ws.onclose = null;
|
|
1714
|
+
this.ws.onopen = null;
|
|
1715
|
+
this.ws.onerror = null;
|
|
1716
|
+
this.ws.onmessage = null;
|
|
1717
|
+
try {
|
|
1718
|
+
this.ws.close();
|
|
1719
|
+
} catch {
|
|
1720
|
+
}
|
|
1721
|
+
this.ws = null;
|
|
1722
|
+
}
|
|
1723
|
+
if (this.options.reconnect && !this.intentionalClose) {
|
|
1724
|
+
this.scheduleReconnect();
|
|
1725
|
+
} else {
|
|
1726
|
+
this.setState("disconnected");
|
|
1727
|
+
}
|
|
1728
|
+
}
|
|
1729
|
+
// ---- Browser event listeners for proactive reconnection ----
|
|
1730
|
+
setupBrowserListeners() {
|
|
1731
|
+
if (this.browserListenersActive)
|
|
1732
|
+
return;
|
|
1733
|
+
this.browserListenersActive = true;
|
|
1734
|
+
if (typeof document !== "undefined") {
|
|
1735
|
+
document.addEventListener("visibilitychange", this.handleVisibilityChange);
|
|
1736
|
+
}
|
|
1737
|
+
if (typeof window !== "undefined") {
|
|
1738
|
+
window.addEventListener("online", this.handleOnline);
|
|
1739
|
+
}
|
|
1740
|
+
}
|
|
1741
|
+
teardownBrowserListeners() {
|
|
1742
|
+
if (!this.browserListenersActive)
|
|
1743
|
+
return;
|
|
1744
|
+
this.browserListenersActive = false;
|
|
1745
|
+
if (typeof document !== "undefined") {
|
|
1746
|
+
document.removeEventListener("visibilitychange", this.handleVisibilityChange);
|
|
1747
|
+
}
|
|
1748
|
+
if (typeof window !== "undefined") {
|
|
1749
|
+
window.removeEventListener("online", this.handleOnline);
|
|
1750
|
+
}
|
|
1751
|
+
}
|
|
1752
|
+
/**
|
|
1753
|
+
* Send a ping and arm a short timeout. If no frame arrives within 5s the
|
|
1754
|
+
* connection is assumed dead and force-reconnected. Any received frame
|
|
1755
|
+
* (including the pong) cancels the timer via clearProbe() in handleFrame.
|
|
1756
|
+
*/
|
|
1757
|
+
probeConnection() {
|
|
1758
|
+
if (this.probeTimer)
|
|
1759
|
+
return;
|
|
1760
|
+
this.send({ type: WS_EVENTS.PING, data: { ts: Date.now() } });
|
|
1761
|
+
this.probeTimer = setTimeout(() => {
|
|
1762
|
+
this.forceReconnect();
|
|
1763
|
+
}, 5e3);
|
|
1764
|
+
}
|
|
1765
|
+
clearProbe() {
|
|
1766
|
+
if (this.probeTimer) {
|
|
1767
|
+
clearTimeout(this.probeTimer);
|
|
1768
|
+
this.probeTimer = null;
|
|
1769
|
+
}
|
|
1770
|
+
}
|
|
1771
|
+
/** Tab returned to foreground — verify connection or accelerate reconnect. */
|
|
1772
|
+
handleVisibilityChange = () => {
|
|
1773
|
+
if (typeof document !== "undefined" && document.hidden)
|
|
1774
|
+
return;
|
|
1775
|
+
if (this._state === "connected") {
|
|
1776
|
+
if (!this.ws || this.ws.readyState !== WebSocket.OPEN) {
|
|
1777
|
+
this.forceReconnect();
|
|
1778
|
+
} else {
|
|
1779
|
+
this.probeConnection();
|
|
1780
|
+
}
|
|
1781
|
+
} else if (this._state === "reconnecting") {
|
|
1782
|
+
this.clearReconnect();
|
|
1783
|
+
this.reconnectAttempts = 0;
|
|
1784
|
+
this.connect();
|
|
1785
|
+
}
|
|
1786
|
+
};
|
|
1787
|
+
/** Network restored — accelerate reconnection. */
|
|
1788
|
+
handleOnline = () => {
|
|
1789
|
+
if (this.intentionalClose)
|
|
1790
|
+
return;
|
|
1791
|
+
if (this._state === "connected") {
|
|
1792
|
+
if (!this.ws || this.ws.readyState !== WebSocket.OPEN) {
|
|
1793
|
+
this.forceReconnect();
|
|
1794
|
+
} else {
|
|
1795
|
+
this.probeConnection();
|
|
1796
|
+
}
|
|
1797
|
+
} else if (this._state === "reconnecting") {
|
|
1798
|
+
this.clearReconnect();
|
|
1799
|
+
this.reconnectAttempts = 0;
|
|
1800
|
+
this.connect();
|
|
1801
|
+
}
|
|
1802
|
+
};
|
|
1803
|
+
setState(state) {
|
|
1804
|
+
this._state = state;
|
|
1805
|
+
for (const listener of this.stateListeners) {
|
|
1806
|
+
listener(state);
|
|
1807
|
+
}
|
|
1808
|
+
}
|
|
1809
|
+
};
|
|
1810
|
+
|
|
1811
|
+
// ts/daemon/dist/config.js
|
|
1812
|
+
import * as fs from "node:fs";
|
|
1813
|
+
import * as os from "node:os";
|
|
1814
|
+
import * as path from "node:path";
|
|
1815
|
+
function resolvePath(value) {
|
|
1816
|
+
return path.isAbsolute(value) ? value : path.resolve(process.cwd(), value);
|
|
1817
|
+
}
|
|
1818
|
+
function parseMs(value, fallback) {
|
|
1819
|
+
if (!value)
|
|
1820
|
+
return fallback;
|
|
1821
|
+
const n = Number(value);
|
|
1822
|
+
return Number.isFinite(n) && n > 0 ? n : fallback;
|
|
1823
|
+
}
|
|
1824
|
+
function parseMsAllowZero(value, fallback) {
|
|
1825
|
+
if (value === void 0)
|
|
1826
|
+
return fallback;
|
|
1827
|
+
const n = Number(value);
|
|
1828
|
+
return Number.isFinite(n) && n >= 0 ? n : fallback;
|
|
1829
|
+
}
|
|
1830
|
+
function daemonConfigDir(env = process.env) {
|
|
1831
|
+
return path.join(env.HOME || os.homedir(), ".parall-daemon");
|
|
1832
|
+
}
|
|
1833
|
+
function daemonConfigPath(env = process.env) {
|
|
1834
|
+
return path.join(daemonConfigDir(env), "config.json");
|
|
1835
|
+
}
|
|
1836
|
+
function tryLoadConfigFile(env) {
|
|
1837
|
+
const cfgPath = daemonConfigPath(env);
|
|
1838
|
+
let content;
|
|
1839
|
+
try {
|
|
1840
|
+
content = fs.readFileSync(cfgPath, "utf-8");
|
|
1841
|
+
} catch (err) {
|
|
1842
|
+
if (err.code === "ENOENT")
|
|
1843
|
+
return null;
|
|
1844
|
+
console.error(`Failed to read daemon config at ${cfgPath}: ${String(err)}`);
|
|
1845
|
+
return null;
|
|
1846
|
+
}
|
|
1847
|
+
try {
|
|
1848
|
+
return JSON.parse(content);
|
|
1849
|
+
} catch (err) {
|
|
1850
|
+
console.error(`Failed to parse daemon config at ${cfgPath}: ${String(err)}`);
|
|
1851
|
+
return null;
|
|
1852
|
+
}
|
|
1853
|
+
}
|
|
1854
|
+
function resolveClaudeDaemonConfig(env = process.env) {
|
|
1855
|
+
let apiUrl = env.PRLL_API_URL?.trim() || "";
|
|
1856
|
+
let apiKey = env.PRLL_API_KEY?.trim() || "";
|
|
1857
|
+
if (!apiUrl || !apiKey) {
|
|
1858
|
+
const file = tryLoadConfigFile(env);
|
|
1859
|
+
if (file) {
|
|
1860
|
+
if (!apiUrl && file.api_url)
|
|
1861
|
+
apiUrl = file.api_url.trim();
|
|
1862
|
+
if (!apiKey && file.api_key)
|
|
1863
|
+
apiKey = file.api_key.trim();
|
|
1864
|
+
}
|
|
1865
|
+
}
|
|
1866
|
+
if (!apiUrl)
|
|
1867
|
+
throw new Error("Missing required env var: PRLL_API_URL");
|
|
1868
|
+
if (!apiKey)
|
|
1869
|
+
throw new Error("Missing required env var: PRLL_API_KEY");
|
|
1870
|
+
if (!apiKey.startsWith("mck_")) {
|
|
1871
|
+
throw new Error(`PRLL_API_KEY does not look like a Machine bearer (expected prefix "mck_"). Daemon mode requires a machine-scoped key issued via POST /machines/{id}/keys.`);
|
|
1872
|
+
}
|
|
1873
|
+
const rootClaudeHome = resolvePath(env.PRLL_CLAUDE_HOME?.trim() || env.HOME || os.homedir());
|
|
1874
|
+
const rootStateDir = resolvePath(env.PRLL_CLAUDE_STATE_DIR?.trim() || path.join(rootClaudeHome, ".parall-agent"));
|
|
1875
|
+
return {
|
|
1876
|
+
apiUrl,
|
|
1877
|
+
apiKey,
|
|
1878
|
+
agentBin: env.PRLL_CLAUDE_AGENT_BIN?.trim() || "parall-claude-agent",
|
|
1879
|
+
rootStateDir,
|
|
1880
|
+
rootClaudeHome,
|
|
1881
|
+
wsUrl: env.PRLL_WS_URL?.trim() || void 0,
|
|
1882
|
+
pollIntervalMs: parseMs(env.PRLL_DAEMON_POLL_INTERVAL_MS, 3e4),
|
|
1883
|
+
heartbeatIntervalMs: parseMs(env.PRLL_DAEMON_HEARTBEAT_INTERVAL_MS, 3e4),
|
|
1884
|
+
restartBackoffMs: parseMs(env.PRLL_DAEMON_RESTART_BACKOFF_MS, 5e3),
|
|
1885
|
+
restartBackoffMaxMs: parseMs(env.PRLL_DAEMON_RESTART_BACKOFF_MAX_MS, 5 * 6e4),
|
|
1886
|
+
bootstrapBackoffMs: parseMsAllowZero(env.PRLL_DAEMON_BOOTSTRAP_BACKOFF_MS, 2e3),
|
|
1887
|
+
bootstrapBackoffMaxMs: parseMs(env.PRLL_DAEMON_BOOTSTRAP_BACKOFF_MAX_MS, 6e4),
|
|
1888
|
+
supervisorRestartBackoffMs: parseMsAllowZero(env.PRLL_DAEMON_SUPERVISOR_RESTART_BACKOFF_MS, 5e3),
|
|
1889
|
+
supervisorRestartBackoffMaxMs: parseMs(env.PRLL_DAEMON_SUPERVISOR_RESTART_BACKOFF_MAX_MS, 5 * 6e4)
|
|
1890
|
+
};
|
|
1891
|
+
}
|
|
1892
|
+
function assertSafeAgentId(agentId) {
|
|
1893
|
+
if (!/^[A-Za-z0-9_-]+$/.test(agentId)) {
|
|
1894
|
+
throw new Error(`Invalid agentId for filesystem path: ${agentId}`);
|
|
1895
|
+
}
|
|
1896
|
+
return agentId;
|
|
1897
|
+
}
|
|
1898
|
+
function agentStateDirFor(rootStateDir, agentId) {
|
|
1899
|
+
return path.join(rootStateDir, "agents", assertSafeAgentId(agentId));
|
|
1900
|
+
}
|
|
1901
|
+
function agentClaudeHomeFor(rootClaudeHome, agentId) {
|
|
1902
|
+
return path.join(rootClaudeHome, "agents", assertSafeAgentId(agentId));
|
|
1903
|
+
}
|
|
1904
|
+
function sharedClaudeCredentialsFileFor(rootClaudeHome) {
|
|
1905
|
+
return path.join(rootClaudeHome, ".claude", ".credentials.json");
|
|
1906
|
+
}
|
|
1907
|
+
function agentClaudeCredentialsFileFor(agentClaudeHome) {
|
|
1908
|
+
return path.join(agentClaudeHome, ".claude", ".credentials.json");
|
|
1909
|
+
}
|
|
1910
|
+
function agentWorkspaceDirFor(rootStateDir, agentId) {
|
|
1911
|
+
return path.join(rootStateDir, "agents", assertSafeAgentId(agentId), "workspace");
|
|
1912
|
+
}
|
|
1913
|
+
function resolveWsUrl(apiUrl, explicitWsUrl) {
|
|
1914
|
+
return explicitWsUrl || `${apiUrl.replace(/\/$/, "").replace(/^http/, "ws")}/ws`;
|
|
1915
|
+
}
|
|
1916
|
+
|
|
1917
|
+
// ts/daemon/dist/supervisor.js
|
|
1918
|
+
import { spawn } from "node:child_process";
|
|
1919
|
+
import * as fs2 from "node:fs";
|
|
1920
|
+
import * as path2 from "node:path";
|
|
1921
|
+
|
|
1922
|
+
// ts/daemon/dist/runtimes.js
|
|
1923
|
+
var claudeCodeAdapter = {
|
|
1924
|
+
bin: "parall-claude-agent",
|
|
1925
|
+
buildEnv(baseEnv, agentId, orgId, apiKey, dirs) {
|
|
1926
|
+
const env = { ...baseEnv };
|
|
1927
|
+
env.PRLL_API_KEY = apiKey;
|
|
1928
|
+
env.PRLL_ORG_ID = orgId;
|
|
1929
|
+
env.AGENT_ID = agentId;
|
|
1930
|
+
env.PRLL_AGENT_ID = agentId;
|
|
1931
|
+
env.PRLL_CLAUDE_HOME = dirs.claudeHome;
|
|
1932
|
+
env.PRLL_CLAUDE_STATE_DIR = dirs.stateDir;
|
|
1933
|
+
env.PRLL_CLAUDE_WORKSPACE_DIR = dirs.workspaceDir;
|
|
1934
|
+
if ("ANTHROPIC_AUTH_TOKEN" in env) {
|
|
1935
|
+
env.ANTHROPIC_AUTH_TOKEN = apiKey;
|
|
1936
|
+
}
|
|
1937
|
+
delete env.PRLL_DAEMON_MODE;
|
|
1938
|
+
return env;
|
|
1939
|
+
}
|
|
1940
|
+
};
|
|
1941
|
+
var codexAdapter = {
|
|
1942
|
+
bin: "parall-codex-agent",
|
|
1943
|
+
buildEnv(baseEnv, agentId, orgId, apiKey, dirs) {
|
|
1944
|
+
const env = { ...baseEnv };
|
|
1945
|
+
env.PRLL_API_KEY = apiKey;
|
|
1946
|
+
env.PRLL_ORG_ID = orgId;
|
|
1947
|
+
env.AGENT_ID = agentId;
|
|
1948
|
+
env.PRLL_AGENT_ID = agentId;
|
|
1949
|
+
env.PRLL_CODEX_STATE_DIR = dirs.stateDir;
|
|
1950
|
+
env.PRLL_CODEX_WORKSPACE_DIR = dirs.workspaceDir;
|
|
1951
|
+
delete env.PRLL_DAEMON_MODE;
|
|
1952
|
+
return env;
|
|
1953
|
+
}
|
|
1954
|
+
};
|
|
1955
|
+
var defaultAdapter = {
|
|
1956
|
+
bin: "parall-agent",
|
|
1957
|
+
buildEnv(baseEnv, agentId, orgId, apiKey, dirs) {
|
|
1958
|
+
const env = { ...baseEnv };
|
|
1959
|
+
env.PRLL_API_KEY = apiKey;
|
|
1960
|
+
env.PRLL_ORG_ID = orgId;
|
|
1961
|
+
env.AGENT_ID = agentId;
|
|
1962
|
+
env.PRLL_AGENT_ID = agentId;
|
|
1963
|
+
env.PRLL_STATE_DIR = dirs.stateDir;
|
|
1964
|
+
env.PRLL_WORKSPACE_DIR = dirs.workspaceDir;
|
|
1965
|
+
delete env.PRLL_DAEMON_MODE;
|
|
1966
|
+
return env;
|
|
1967
|
+
}
|
|
1968
|
+
};
|
|
1969
|
+
var openclawAdapter = {
|
|
1970
|
+
bin: "parall-openclaw-agent",
|
|
1971
|
+
buildEnv(baseEnv, agentId, orgId, apiKey, dirs) {
|
|
1972
|
+
const env = { ...baseEnv };
|
|
1973
|
+
env.PRLL_API_KEY = apiKey;
|
|
1974
|
+
env.PRLL_ORG_ID = orgId;
|
|
1975
|
+
env.AGENT_ID = agentId;
|
|
1976
|
+
env.PRLL_AGENT_ID = agentId;
|
|
1977
|
+
env.PRLL_OPENCLAW_STATE_DIR = dirs.stateDir;
|
|
1978
|
+
env.PRLL_OPENCLAW_WORKSPACE_DIR = dirs.workspaceDir;
|
|
1979
|
+
env.OPENCLAW_GATEWAY_PORT = env.OPENCLAW_GATEWAY_PORT || "0";
|
|
1980
|
+
delete env.PRLL_DAEMON_MODE;
|
|
1981
|
+
return env;
|
|
1982
|
+
}
|
|
1983
|
+
};
|
|
1984
|
+
var RUNTIME_ADAPTERS = {
|
|
1985
|
+
"claude-code": claudeCodeAdapter,
|
|
1986
|
+
"codex": codexAdapter,
|
|
1987
|
+
"openclaw": openclawAdapter
|
|
1988
|
+
};
|
|
1989
|
+
function getRuntimeAdapter(runtimeType) {
|
|
1990
|
+
return RUNTIME_ADAPTERS[runtimeType] ?? defaultAdapter;
|
|
1991
|
+
}
|
|
1992
|
+
function assertAgentKey(apiKey) {
|
|
1993
|
+
if (apiKey.startsWith("mck_")) {
|
|
1994
|
+
throw new Error("BUG: machine key leaked to child process \u2014 expected agk_, got mck_");
|
|
1995
|
+
}
|
|
1996
|
+
}
|
|
1997
|
+
|
|
1998
|
+
// ts/daemon/dist/supervisor.js
|
|
1999
|
+
var RUNTIME_PACKAGES = {
|
|
2000
|
+
"claude-code": "@parall/claude-agent",
|
|
2001
|
+
"codex": "@parall/codex-agent",
|
|
2002
|
+
"openclaw": "@parall/openclaw-agent"
|
|
2003
|
+
};
|
|
2004
|
+
function sleepCancellable(ms, signal) {
|
|
2005
|
+
if (signal.aborted)
|
|
2006
|
+
return Promise.resolve(false);
|
|
2007
|
+
return new Promise((resolve3) => {
|
|
2008
|
+
const timer = setTimeout(() => {
|
|
2009
|
+
signal.removeEventListener("abort", onAbort);
|
|
2010
|
+
resolve3(true);
|
|
2011
|
+
}, ms);
|
|
2012
|
+
const onAbort = () => {
|
|
2013
|
+
clearTimeout(timer);
|
|
2014
|
+
resolve3(false);
|
|
2015
|
+
};
|
|
2016
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
2017
|
+
});
|
|
2018
|
+
}
|
|
2019
|
+
var DaemonSupervisor = class {
|
|
2020
|
+
config;
|
|
2021
|
+
client;
|
|
2022
|
+
log;
|
|
2023
|
+
children = /* @__PURE__ */ new Map();
|
|
2024
|
+
ws = null;
|
|
2025
|
+
running = false;
|
|
2026
|
+
machineOrgId = null;
|
|
2027
|
+
stopResolve = null;
|
|
2028
|
+
constructor(config, client, log) {
|
|
2029
|
+
this.config = config;
|
|
2030
|
+
this.client = client;
|
|
2031
|
+
this.log = log;
|
|
2032
|
+
}
|
|
2033
|
+
/** Start the supervisor. Returns a promise that resolves on `stop()`. */
|
|
2034
|
+
async run(signal) {
|
|
2035
|
+
if (this.running)
|
|
2036
|
+
throw new Error("supervisor already running");
|
|
2037
|
+
this.running = true;
|
|
2038
|
+
const onAbort = () => {
|
|
2039
|
+
this.stop().catch((err) => this.log.error(`stop() failed: ${String(err)}`));
|
|
2040
|
+
};
|
|
2041
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
2042
|
+
try {
|
|
2043
|
+
if (!await this.bootstrapWithRetry(signal)) {
|
|
2044
|
+
signal.removeEventListener("abort", onAbort);
|
|
2045
|
+
this.running = false;
|
|
2046
|
+
return;
|
|
2047
|
+
}
|
|
2048
|
+
} catch (err) {
|
|
2049
|
+
signal.removeEventListener("abort", onAbort);
|
|
2050
|
+
this.running = false;
|
|
2051
|
+
throw err;
|
|
2052
|
+
}
|
|
2053
|
+
this.migrateFlatLayout();
|
|
2054
|
+
await this.fullReconcile();
|
|
2055
|
+
this.ws = new ParallWs({
|
|
2056
|
+
getTicket: () => this.client.getMachineWsTicket(),
|
|
2057
|
+
wsUrl: this.config.wsUrl,
|
|
2058
|
+
reconnect: true
|
|
2059
|
+
});
|
|
2060
|
+
this.ws.on("machine.hello", (_data) => {
|
|
2061
|
+
this.log.info("machine WS connected (machine.hello)");
|
|
2062
|
+
void this.fullReconcile();
|
|
2063
|
+
});
|
|
2064
|
+
this.ws.on("machine.agent.attached", (data) => {
|
|
2065
|
+
this.log.info(`WS: agent ${data.agent_id} attached`);
|
|
2066
|
+
void this.handleAgentAttached(data.agent_id);
|
|
2067
|
+
});
|
|
2068
|
+
this.ws.on("machine.agent.detached", (data) => {
|
|
2069
|
+
this.log.info(`WS: agent ${data.agent_id} detached`);
|
|
2070
|
+
void this.handleAgentDetached(data.agent_id);
|
|
2071
|
+
});
|
|
2072
|
+
this.ws.on("machine.stop", (data) => {
|
|
2073
|
+
this.log.info(`WS: machine.stop received (reason=${data.reason ?? "none"})`);
|
|
2074
|
+
void this.stop();
|
|
2075
|
+
});
|
|
2076
|
+
this.ws.onStateChange((state) => {
|
|
2077
|
+
if (state === "disconnected" || state === "reconnecting") {
|
|
2078
|
+
this.log.warn(`machine WS state: ${state}`);
|
|
2079
|
+
}
|
|
2080
|
+
});
|
|
2081
|
+
await this.ws.connect();
|
|
2082
|
+
await new Promise((resolve3) => {
|
|
2083
|
+
this.stopResolve = resolve3;
|
|
2084
|
+
});
|
|
2085
|
+
signal.removeEventListener("abort", onAbort);
|
|
2086
|
+
}
|
|
2087
|
+
/** Disconnect WS, cancel timers, SIGTERM all children, await exit. */
|
|
2088
|
+
async stop() {
|
|
2089
|
+
if (!this.running)
|
|
2090
|
+
return;
|
|
2091
|
+
this.running = false;
|
|
2092
|
+
if (this.ws) {
|
|
2093
|
+
this.ws.disconnect();
|
|
2094
|
+
this.ws = null;
|
|
2095
|
+
}
|
|
2096
|
+
const exits = [];
|
|
2097
|
+
for (const state of this.children.values()) {
|
|
2098
|
+
state.shuttingDown = true;
|
|
2099
|
+
if (state.restartTimer) {
|
|
2100
|
+
clearTimeout(state.restartTimer);
|
|
2101
|
+
state.restartTimer = null;
|
|
2102
|
+
}
|
|
2103
|
+
exits.push(this.terminateChild(state));
|
|
2104
|
+
}
|
|
2105
|
+
await Promise.allSettled(exits);
|
|
2106
|
+
this.children.clear();
|
|
2107
|
+
this.log.info("daemon supervisor stopped");
|
|
2108
|
+
if (this.stopResolve) {
|
|
2109
|
+
this.stopResolve();
|
|
2110
|
+
this.stopResolve = null;
|
|
2111
|
+
}
|
|
2112
|
+
}
|
|
2113
|
+
// ---- Bootstrap (resilient identity probe) ----
|
|
2114
|
+
async bootstrapWithRetry(signal) {
|
|
2115
|
+
let attempt = 0;
|
|
2116
|
+
while (this.running && !signal.aborted) {
|
|
2117
|
+
try {
|
|
2118
|
+
const machine = await this.client.getMachineSelf();
|
|
2119
|
+
this.machineOrgId = machine.org_id;
|
|
2120
|
+
this.log.info(`daemon online \u2014 machine_id=${machine.id} org=${machine.org_id} label=${machine.label}`);
|
|
2121
|
+
return true;
|
|
2122
|
+
} catch (err) {
|
|
2123
|
+
if (this.config.bootstrapBackoffMs === 0) {
|
|
2124
|
+
this.log.error(`getMachineSelf failed: ${String(err)} (fail-fast mode)`);
|
|
2125
|
+
throw err;
|
|
2126
|
+
}
|
|
2127
|
+
const delay = Math.min(this.config.bootstrapBackoffMs * Math.pow(2, attempt), this.config.bootstrapBackoffMaxMs);
|
|
2128
|
+
attempt += 1;
|
|
2129
|
+
this.log.warn(`getMachineSelf failed (attempt ${attempt}): ${String(err)} \u2014 retrying in ${delay}ms`);
|
|
2130
|
+
const slept = await sleepCancellable(delay, signal);
|
|
2131
|
+
if (!slept)
|
|
2132
|
+
return false;
|
|
2133
|
+
}
|
|
2134
|
+
}
|
|
2135
|
+
return false;
|
|
2136
|
+
}
|
|
2137
|
+
// ---- Full reconcile (HTTP-based, used on boot + WS reconnect) ----
|
|
2138
|
+
// Safe to run concurrently with WS event handlers: JS single-threaded
|
|
2139
|
+
// event loop guarantees no mid-statement interleaving, and both
|
|
2140
|
+
// handleAgentAttached/Detached guard on children.has()/get() so a WS
|
|
2141
|
+
// event between the HTTP fetch and the spawn/kill loop is a no-op.
|
|
2142
|
+
async fullReconcile() {
|
|
2143
|
+
if (!this.running)
|
|
2144
|
+
return;
|
|
2145
|
+
let attached;
|
|
2146
|
+
try {
|
|
2147
|
+
attached = await this.client.listAttachedAgents();
|
|
2148
|
+
} catch (err) {
|
|
2149
|
+
this.log.warn(`fullReconcile: listAttachedAgents failed: ${String(err)}`);
|
|
2150
|
+
return;
|
|
2151
|
+
}
|
|
2152
|
+
const seen = /* @__PURE__ */ new Set();
|
|
2153
|
+
for (const a of attached) {
|
|
2154
|
+
const userId = a.user?.id ?? a.profile.user_id;
|
|
2155
|
+
if (!userId) {
|
|
2156
|
+
this.log.warn(`skipping attached entry with no user_id (profile=${JSON.stringify(a.profile)})`);
|
|
2157
|
+
continue;
|
|
2158
|
+
}
|
|
2159
|
+
if (a.user && a.user.status !== "active") {
|
|
2160
|
+
this.log.info(`agent ${userId} not active (status=${a.user.status}) \u2014 skipping`);
|
|
2161
|
+
continue;
|
|
2162
|
+
}
|
|
2163
|
+
seen.add(userId);
|
|
2164
|
+
const existing = this.children.get(userId);
|
|
2165
|
+
if (!existing) {
|
|
2166
|
+
const orgId = this.machineOrgId;
|
|
2167
|
+
if (!orgId) {
|
|
2168
|
+
this.log.warn(`agent ${userId}: no org_id available yet; skipping`);
|
|
2169
|
+
continue;
|
|
2170
|
+
}
|
|
2171
|
+
await this.spawnAgent(userId, orgId, a);
|
|
2172
|
+
} else if (!existing.child && !existing.restartTimer && !existing.shuttingDown) {
|
|
2173
|
+
await this.restartChildNow(existing, "reconcile found no live child");
|
|
2174
|
+
}
|
|
2175
|
+
}
|
|
2176
|
+
for (const [userId, state] of this.children) {
|
|
2177
|
+
if (!seen.has(userId)) {
|
|
2178
|
+
this.log.info(`agent ${userId} detached (reconcile) \u2014 terminating subprocess`);
|
|
2179
|
+
state.shuttingDown = true;
|
|
2180
|
+
if (state.restartTimer) {
|
|
2181
|
+
clearTimeout(state.restartTimer);
|
|
2182
|
+
state.restartTimer = null;
|
|
2183
|
+
}
|
|
2184
|
+
await this.terminateChild(state);
|
|
2185
|
+
this.children.delete(userId);
|
|
2186
|
+
}
|
|
2187
|
+
}
|
|
2188
|
+
}
|
|
2189
|
+
// ---- Flat layout migration (self-hosted → daemon) ----
|
|
2190
|
+
/**
|
|
2191
|
+
* Detects a legacy flat state layout (no agents/ subdir) and migrates it
|
|
2192
|
+
* into the per-agent directory for the owning agent. Ownership is determined
|
|
2193
|
+
* by parsing session state files which embed the agent ID in the runtimeKey.
|
|
2194
|
+
*/
|
|
2195
|
+
migrateFlatLayout() {
|
|
2196
|
+
const root = this.config.rootStateDir;
|
|
2197
|
+
const agentsDir = path2.join(root, "agents");
|
|
2198
|
+
const flatWorkspace = path2.join(root, "workspace");
|
|
2199
|
+
if (!fs2.existsSync(flatWorkspace) || fs2.existsSync(agentsDir))
|
|
2200
|
+
return;
|
|
2201
|
+
let ownerAgentId;
|
|
2202
|
+
const sessionsDir = path2.join(root, "sessions");
|
|
2203
|
+
if (fs2.existsSync(sessionsDir)) {
|
|
2204
|
+
try {
|
|
2205
|
+
for (const file of fs2.readdirSync(sessionsDir)) {
|
|
2206
|
+
if (!file.endsWith(".json"))
|
|
2207
|
+
continue;
|
|
2208
|
+
const decoded = Buffer.from(file.replace(".json", ""), "base64url").toString();
|
|
2209
|
+
const parts = decoded.split(":");
|
|
2210
|
+
if (parts.length >= 4 && parts[3].startsWith("usr_")) {
|
|
2211
|
+
ownerAgentId = parts[3];
|
|
2212
|
+
break;
|
|
2213
|
+
}
|
|
2214
|
+
}
|
|
2215
|
+
} catch {
|
|
2216
|
+
}
|
|
2217
|
+
}
|
|
2218
|
+
const targetId = ownerAgentId ?? "_orphan";
|
|
2219
|
+
const targetDir = path2.join(agentsDir, targetId);
|
|
2220
|
+
try {
|
|
2221
|
+
fs2.mkdirSync(targetDir, { recursive: true });
|
|
2222
|
+
for (const sub of ["workspace", "sessions", "dispatch-context"]) {
|
|
2223
|
+
const src = path2.join(root, sub);
|
|
2224
|
+
if (fs2.existsSync(src)) {
|
|
2225
|
+
fs2.renameSync(src, path2.join(targetDir, sub));
|
|
2226
|
+
}
|
|
2227
|
+
}
|
|
2228
|
+
this.log.info(`migrated legacy flat state \u2192 agents/${targetId}/`);
|
|
2229
|
+
} catch (err) {
|
|
2230
|
+
this.log.warn(`flat layout migration failed: ${String(err)}`);
|
|
2231
|
+
}
|
|
2232
|
+
}
|
|
2233
|
+
// ---- WS event handlers (incremental) ----
|
|
2234
|
+
async handleAgentAttached(agentId) {
|
|
2235
|
+
if (this.children.has(agentId))
|
|
2236
|
+
return;
|
|
2237
|
+
let attached;
|
|
2238
|
+
try {
|
|
2239
|
+
attached = await this.client.listAttachedAgents();
|
|
2240
|
+
} catch (err) {
|
|
2241
|
+
this.log.warn(`handleAgentAttached: listAttachedAgents failed: ${String(err)}`);
|
|
2242
|
+
return;
|
|
2243
|
+
}
|
|
2244
|
+
const entry = attached.find((a) => (a.user?.id ?? a.profile.user_id) === agentId);
|
|
2245
|
+
if (!entry) {
|
|
2246
|
+
this.log.warn(`handleAgentAttached: agent ${agentId} not found in attached list`);
|
|
2247
|
+
return;
|
|
2248
|
+
}
|
|
2249
|
+
if (entry.user && entry.user.status !== "active") {
|
|
2250
|
+
this.log.info(`agent ${agentId} not active (status=${entry.user.status}) \u2014 skipping`);
|
|
2251
|
+
return;
|
|
2252
|
+
}
|
|
2253
|
+
const orgId = this.machineOrgId;
|
|
2254
|
+
if (!orgId) {
|
|
2255
|
+
this.log.warn(`agent ${agentId}: no org_id available yet; skipping`);
|
|
2256
|
+
return;
|
|
2257
|
+
}
|
|
2258
|
+
await this.spawnAgent(agentId, orgId, entry);
|
|
2259
|
+
}
|
|
2260
|
+
async handleAgentDetached(agentId) {
|
|
2261
|
+
const state = this.children.get(agentId);
|
|
2262
|
+
if (!state)
|
|
2263
|
+
return;
|
|
2264
|
+
this.log.info(`agent ${agentId} detached \u2014 terminating subprocess`);
|
|
2265
|
+
state.shuttingDown = true;
|
|
2266
|
+
if (state.restartTimer) {
|
|
2267
|
+
clearTimeout(state.restartTimer);
|
|
2268
|
+
state.restartTimer = null;
|
|
2269
|
+
}
|
|
2270
|
+
await this.terminateChild(state);
|
|
2271
|
+
this.children.delete(agentId);
|
|
2272
|
+
}
|
|
2273
|
+
// ---- Spawn / restart ----
|
|
2274
|
+
async restartChildNow(state, reason) {
|
|
2275
|
+
if (!this.running || state.shuttingDown || state.child || state.restartTimer) {
|
|
2276
|
+
return;
|
|
2277
|
+
}
|
|
2278
|
+
try {
|
|
2279
|
+
state.credential = await this.client.mintLaunchCredential(state.agentId);
|
|
2280
|
+
state.restartAttempts = 0;
|
|
2281
|
+
this.log.info(`agent ${state.agentId}: restarting child (${reason})`);
|
|
2282
|
+
this.startChild(state);
|
|
2283
|
+
} catch (err) {
|
|
2284
|
+
this.log.warn(`agent ${state.agentId}: restart mint failed (${reason}): ${String(err)}`);
|
|
2285
|
+
}
|
|
2286
|
+
}
|
|
2287
|
+
async spawnAgent(agentId, orgId, attached) {
|
|
2288
|
+
let credential;
|
|
2289
|
+
try {
|
|
2290
|
+
credential = await this.client.mintLaunchCredential(agentId);
|
|
2291
|
+
} catch (err) {
|
|
2292
|
+
this.log.error(`mintLaunchCredential ${agentId} failed: ${String(err)}`);
|
|
2293
|
+
return;
|
|
2294
|
+
}
|
|
2295
|
+
const stateDir = agentStateDirFor(this.config.rootStateDir, agentId);
|
|
2296
|
+
const workspaceDir = attached.daemon_config?.workspace_path || agentWorkspaceDirFor(this.config.rootStateDir, agentId);
|
|
2297
|
+
const isK8s = !!process.env.KUBERNETES_SERVICE_HOST;
|
|
2298
|
+
const claudeHome = isK8s ? agentClaudeHomeFor(this.config.rootClaudeHome, agentId) : this.config.rootClaudeHome;
|
|
2299
|
+
try {
|
|
2300
|
+
fs2.mkdirSync(stateDir, { recursive: true });
|
|
2301
|
+
if (!attached.daemon_config?.workspace_path) {
|
|
2302
|
+
fs2.mkdirSync(workspaceDir, { recursive: true });
|
|
2303
|
+
}
|
|
2304
|
+
if (isK8s) {
|
|
2305
|
+
fs2.mkdirSync(claudeHome, { recursive: true });
|
|
2306
|
+
this.ensureSharedCredentialLink(claudeHome, agentId);
|
|
2307
|
+
}
|
|
2308
|
+
} catch (err) {
|
|
2309
|
+
this.log.warn(`mkdir agent dirs (${agentId}) failed: ${String(err)}`);
|
|
2310
|
+
}
|
|
2311
|
+
const runtimeType = attached.profile.runtime_type ?? "claude-code";
|
|
2312
|
+
const state = {
|
|
2313
|
+
agentId,
|
|
2314
|
+
orgId,
|
|
2315
|
+
runtimeType,
|
|
2316
|
+
workspacePath: workspaceDir,
|
|
2317
|
+
claudeHome,
|
|
2318
|
+
child: null,
|
|
2319
|
+
credential,
|
|
2320
|
+
restartAttempts: 0,
|
|
2321
|
+
restartTimer: null,
|
|
2322
|
+
shuttingDown: false
|
|
2323
|
+
};
|
|
2324
|
+
this.children.set(agentId, state);
|
|
2325
|
+
this.startChild(state);
|
|
2326
|
+
}
|
|
2327
|
+
startChild(state) {
|
|
2328
|
+
if (state.shuttingDown || !this.running)
|
|
2329
|
+
return;
|
|
2330
|
+
if (!state.credential) {
|
|
2331
|
+
this.log.error(`startChild ${state.agentId}: no credential \u2014 bug`);
|
|
2332
|
+
return;
|
|
2333
|
+
}
|
|
2334
|
+
assertAgentKey(state.credential.api_key);
|
|
2335
|
+
const adapter = getRuntimeAdapter(state.runtimeType);
|
|
2336
|
+
const dirs = {
|
|
2337
|
+
stateDir: agentStateDirFor(this.config.rootStateDir, state.agentId),
|
|
2338
|
+
workspaceDir: state.workspacePath,
|
|
2339
|
+
claudeHome: state.claudeHome
|
|
2340
|
+
};
|
|
2341
|
+
const env = adapter.buildEnv({ ...process.env, PRLL_API_URL: this.config.apiUrl }, state.agentId, state.orgId, state.credential.api_key, dirs);
|
|
2342
|
+
this.log.info(`spawning agent ${state.agentId} runtime=${state.runtimeType} bin=${adapter.bin} (attempt ${state.restartAttempts + 1})`);
|
|
2343
|
+
const child = spawn(adapter.bin, [], {
|
|
2344
|
+
env,
|
|
2345
|
+
stdio: ["ignore", "inherit", "inherit"],
|
|
2346
|
+
detached: false
|
|
2347
|
+
});
|
|
2348
|
+
state.child = child;
|
|
2349
|
+
let childSettled = false;
|
|
2350
|
+
const settleChild = (event, code, signal, err) => {
|
|
2351
|
+
if (childSettled)
|
|
2352
|
+
return;
|
|
2353
|
+
childSettled = true;
|
|
2354
|
+
if (state.child !== child)
|
|
2355
|
+
return;
|
|
2356
|
+
const wasShutting = state.shuttingDown;
|
|
2357
|
+
state.child = null;
|
|
2358
|
+
if (err) {
|
|
2359
|
+
this.log.error(`agent ${state.agentId} child ${event}: ${String(err)}${wasShutting ? " (shutting down)" : ""}`);
|
|
2360
|
+
} else {
|
|
2361
|
+
this.log.info(`agent ${state.agentId} exited code=${code ?? "null"} signal=${signal ?? "null"}${wasShutting ? " (shutting down)" : ""}`);
|
|
2362
|
+
}
|
|
2363
|
+
if (wasShutting || !this.running)
|
|
2364
|
+
return;
|
|
2365
|
+
const delay = Math.min(this.config.restartBackoffMs * Math.pow(2, state.restartAttempts), this.config.restartBackoffMaxMs);
|
|
2366
|
+
state.restartAttempts += 1;
|
|
2367
|
+
this.log.warn(`agent ${state.agentId} will restart in ${delay}ms`);
|
|
2368
|
+
state.restartTimer = setTimeout(() => {
|
|
2369
|
+
state.restartTimer = null;
|
|
2370
|
+
this.startChild(state);
|
|
2371
|
+
}, delay);
|
|
2372
|
+
};
|
|
2373
|
+
child.once("error", (err) => {
|
|
2374
|
+
if (err.code === "ENOENT") {
|
|
2375
|
+
const pkg = RUNTIME_PACKAGES[state.runtimeType] ?? `@parall/${state.runtimeType}-agent`;
|
|
2376
|
+
this.log.error(`Runtime binary "${adapter.bin}" not found in PATH. Install: npm install -g ${pkg}`);
|
|
2377
|
+
}
|
|
2378
|
+
settleChild("error", null, null, err);
|
|
2379
|
+
});
|
|
2380
|
+
child.once("close", (code, signal) => settleChild("close", code, signal));
|
|
2381
|
+
setTimeout(() => {
|
|
2382
|
+
if (state.child === child) {
|
|
2383
|
+
state.restartAttempts = 0;
|
|
2384
|
+
}
|
|
2385
|
+
}, Math.max(this.config.restartBackoffMs, 3e4));
|
|
2386
|
+
}
|
|
2387
|
+
async terminateChild(state) {
|
|
2388
|
+
const child = state.child;
|
|
2389
|
+
if (!child)
|
|
2390
|
+
return;
|
|
2391
|
+
return new Promise((resolve3) => {
|
|
2392
|
+
const onExit = () => resolve3();
|
|
2393
|
+
child.once("exit", onExit);
|
|
2394
|
+
try {
|
|
2395
|
+
child.kill("SIGTERM");
|
|
2396
|
+
} catch (err) {
|
|
2397
|
+
this.log.warn(`SIGTERM ${state.agentId} threw: ${String(err)}`);
|
|
2398
|
+
child.off("exit", onExit);
|
|
2399
|
+
resolve3();
|
|
2400
|
+
return;
|
|
2401
|
+
}
|
|
2402
|
+
const hardKill = setTimeout(() => {
|
|
2403
|
+
try {
|
|
2404
|
+
child.kill("SIGKILL");
|
|
2405
|
+
} catch {
|
|
2406
|
+
}
|
|
2407
|
+
}, 1e4);
|
|
2408
|
+
child.once("exit", () => clearTimeout(hardKill));
|
|
2409
|
+
});
|
|
2410
|
+
}
|
|
2411
|
+
ensureSharedCredentialLink(agentClaudeHome, agentId) {
|
|
2412
|
+
const sharedCredentials = path2.resolve(sharedClaudeCredentialsFileFor(this.config.rootClaudeHome));
|
|
2413
|
+
const agentCredentials = agentClaudeCredentialsFileFor(agentClaudeHome);
|
|
2414
|
+
const agentCredentialsDir = path2.dirname(agentCredentials);
|
|
2415
|
+
fs2.mkdirSync(path2.dirname(sharedCredentials), { recursive: true });
|
|
2416
|
+
fs2.mkdirSync(agentCredentialsDir, { recursive: true });
|
|
2417
|
+
try {
|
|
2418
|
+
const existing = fs2.lstatSync(agentCredentials);
|
|
2419
|
+
if (existing.isSymbolicLink()) {
|
|
2420
|
+
const currentTarget = fs2.readlinkSync(agentCredentials);
|
|
2421
|
+
if (path2.resolve(agentCredentialsDir, currentTarget) === sharedCredentials) {
|
|
2422
|
+
return;
|
|
2423
|
+
}
|
|
2424
|
+
fs2.unlinkSync(agentCredentials);
|
|
2425
|
+
} else if (existing.isDirectory()) {
|
|
2426
|
+
this.log.warn(`agent ${agentId}: credential path is a directory, cannot link ${agentCredentials}`);
|
|
2427
|
+
return;
|
|
2428
|
+
} else {
|
|
2429
|
+
fs2.unlinkSync(agentCredentials);
|
|
2430
|
+
}
|
|
2431
|
+
} catch (err) {
|
|
2432
|
+
if (err.code !== "ENOENT") {
|
|
2433
|
+
throw err;
|
|
2434
|
+
}
|
|
2435
|
+
}
|
|
2436
|
+
fs2.symlinkSync(sharedCredentials, agentCredentials);
|
|
2437
|
+
}
|
|
2438
|
+
};
|
|
2439
|
+
|
|
2440
|
+
// ts/daemon/dist/cli.js
|
|
2441
|
+
import * as fs3 from "node:fs";
|
|
2442
|
+
import * as path3 from "node:path";
|
|
2443
|
+
import * as os2 from "node:os";
|
|
2444
|
+
import * as readline from "node:readline";
|
|
2445
|
+
import { spawn as spawn2, execSync } from "node:child_process";
|
|
2446
|
+
var CONFIG_DIR = daemonConfigDir();
|
|
2447
|
+
var CONFIG_PATH = daemonConfigPath();
|
|
2448
|
+
function readConfig() {
|
|
2449
|
+
try {
|
|
2450
|
+
return JSON.parse(fs3.readFileSync(CONFIG_PATH, "utf-8"));
|
|
2451
|
+
} catch {
|
|
2452
|
+
return null;
|
|
2453
|
+
}
|
|
2454
|
+
}
|
|
2455
|
+
function writeConfig(config) {
|
|
2456
|
+
fs3.mkdirSync(CONFIG_DIR, { recursive: true, mode: 448 });
|
|
2457
|
+
fs3.writeFileSync(CONFIG_PATH, JSON.stringify(config, null, 2), { mode: 384 });
|
|
2458
|
+
fs3.chmodSync(CONFIG_PATH, 384);
|
|
2459
|
+
}
|
|
2460
|
+
function prompt(question) {
|
|
2461
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
2462
|
+
return new Promise((resolve3) => {
|
|
2463
|
+
rl.question(question, (answer) => {
|
|
2464
|
+
rl.close();
|
|
2465
|
+
resolve3(answer.trim());
|
|
2466
|
+
});
|
|
2467
|
+
});
|
|
2468
|
+
}
|
|
2469
|
+
function isMacOS() {
|
|
2470
|
+
return process.platform === "darwin";
|
|
2471
|
+
}
|
|
2472
|
+
function isLinux() {
|
|
2473
|
+
return process.platform === "linux";
|
|
2474
|
+
}
|
|
2475
|
+
var PLIST_LABEL = "com.parall.daemon";
|
|
2476
|
+
function plistPath() {
|
|
2477
|
+
return path3.join(os2.homedir(), "Library", "LaunchAgents", `${PLIST_LABEL}.plist`);
|
|
2478
|
+
}
|
|
2479
|
+
function systemdUnitPath() {
|
|
2480
|
+
return path3.join(os2.homedir(), ".config", "systemd", "user", "parall-daemon.service");
|
|
2481
|
+
}
|
|
2482
|
+
function getDaemonBin() {
|
|
2483
|
+
try {
|
|
2484
|
+
return execSync("which parall-daemon", { encoding: "utf-8", stdio: "pipe" }).trim();
|
|
2485
|
+
} catch {
|
|
2486
|
+
return process.argv[1] ?? "parall-daemon";
|
|
2487
|
+
}
|
|
2488
|
+
}
|
|
2489
|
+
function generatePlist(daemonBin) {
|
|
2490
|
+
const logPath = path3.join(os2.homedir(), "Library", "Logs", "parall-daemon.log");
|
|
2491
|
+
return `<?xml version="1.0" encoding="UTF-8"?>
|
|
2492
|
+
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
2493
|
+
<plist version="1.0">
|
|
2494
|
+
<dict>
|
|
2495
|
+
<key>Label</key>
|
|
2496
|
+
<string>${PLIST_LABEL}</string>
|
|
2497
|
+
<key>ProgramArguments</key>
|
|
2498
|
+
<array>
|
|
2499
|
+
<string>${daemonBin}</string>
|
|
2500
|
+
</array>
|
|
2501
|
+
<key>RunAtLoad</key>
|
|
2502
|
+
<true/>
|
|
2503
|
+
<key>KeepAlive</key>
|
|
2504
|
+
<true/>
|
|
2505
|
+
<key>ThrottleInterval</key>
|
|
2506
|
+
<integer>5</integer>
|
|
2507
|
+
<key>StandardOutPath</key>
|
|
2508
|
+
<string>${logPath}</string>
|
|
2509
|
+
<key>StandardErrorPath</key>
|
|
2510
|
+
<string>${logPath}</string>
|
|
2511
|
+
</dict>
|
|
2512
|
+
</plist>`;
|
|
2513
|
+
}
|
|
2514
|
+
function generateSystemdUnit(daemonBin) {
|
|
2515
|
+
return `[Unit]
|
|
2516
|
+
Description=Parall Daemon
|
|
2517
|
+
After=network-online.target
|
|
2518
|
+
Wants=network-online.target
|
|
2519
|
+
|
|
2520
|
+
[Service]
|
|
2521
|
+
Type=simple
|
|
2522
|
+
ExecStart=${daemonBin}
|
|
2523
|
+
Restart=always
|
|
2524
|
+
RestartSec=5
|
|
2525
|
+
|
|
2526
|
+
[Install]
|
|
2527
|
+
WantedBy=default.target`;
|
|
2528
|
+
}
|
|
2529
|
+
function installService() {
|
|
2530
|
+
const config = readConfig();
|
|
2531
|
+
if (!config) {
|
|
2532
|
+
console.error("No config found. Run `parall-daemon init` first.");
|
|
2533
|
+
process.exit(1);
|
|
2534
|
+
}
|
|
2535
|
+
const bin = getDaemonBin();
|
|
2536
|
+
if (isMacOS()) {
|
|
2537
|
+
const dir = path3.dirname(plistPath());
|
|
2538
|
+
fs3.mkdirSync(dir, { recursive: true });
|
|
2539
|
+
fs3.writeFileSync(plistPath(), generatePlist(bin));
|
|
2540
|
+
execSync(`launchctl bootout gui/$(id -u) ${plistPath()} 2>/dev/null || true`);
|
|
2541
|
+
execSync(`launchctl bootstrap gui/$(id -u) ${plistPath()}`);
|
|
2542
|
+
console.log(`launchd agent installed: ${plistPath()}`);
|
|
2543
|
+
} else if (isLinux()) {
|
|
2544
|
+
const dir = path3.dirname(systemdUnitPath());
|
|
2545
|
+
fs3.mkdirSync(dir, { recursive: true });
|
|
2546
|
+
fs3.writeFileSync(systemdUnitPath(), generateSystemdUnit(bin));
|
|
2547
|
+
execSync("systemctl --user daemon-reload");
|
|
2548
|
+
execSync("systemctl --user enable --now parall-daemon");
|
|
2549
|
+
console.log(`systemd service installed: ${systemdUnitPath()}`);
|
|
2550
|
+
} else {
|
|
2551
|
+
console.log("Unsupported platform. Run `parall-daemon` manually.");
|
|
2552
|
+
}
|
|
2553
|
+
}
|
|
2554
|
+
async function cmdInit() {
|
|
2555
|
+
const existing = readConfig();
|
|
2556
|
+
const defaultUrl = existing?.api_url || "https://api.parall.com";
|
|
2557
|
+
const apiUrl = await prompt(`API URL [${defaultUrl}]: `) || defaultUrl;
|
|
2558
|
+
const apiKey = await prompt("Machine key (mck_...): ");
|
|
2559
|
+
if (!apiKey.startsWith("mck_")) {
|
|
2560
|
+
console.error('Error: Machine key must start with "mck_"');
|
|
2561
|
+
process.exit(1);
|
|
2562
|
+
}
|
|
2563
|
+
writeConfig({ api_url: apiUrl, api_key: apiKey });
|
|
2564
|
+
console.log(`Config written to ${CONFIG_PATH}`);
|
|
2565
|
+
const install = await prompt("Install as background service? (Y/n): ");
|
|
2566
|
+
if (install.toLowerCase() !== "n") {
|
|
2567
|
+
installService();
|
|
2568
|
+
}
|
|
2569
|
+
}
|
|
2570
|
+
function cmdStatus() {
|
|
2571
|
+
const config = readConfig();
|
|
2572
|
+
console.log(`Config: ${config ? CONFIG_PATH : "not configured"}`);
|
|
2573
|
+
if (isMacOS()) {
|
|
2574
|
+
try {
|
|
2575
|
+
const output = execSync(`launchctl print gui/$(id -u)/${PLIST_LABEL} 2>&1`, {
|
|
2576
|
+
encoding: "utf-8"
|
|
2577
|
+
});
|
|
2578
|
+
const running = output.includes("state = running");
|
|
2579
|
+
console.log(`Service: ${running ? "running" : "stopped"}`);
|
|
2580
|
+
const pidMatch = output.match(/pid\s*=\s*(\d+)/);
|
|
2581
|
+
if (pidMatch)
|
|
2582
|
+
console.log(`PID: ${pidMatch[1]}`);
|
|
2583
|
+
} catch {
|
|
2584
|
+
console.log("Service: not installed");
|
|
2585
|
+
}
|
|
2586
|
+
} else if (isLinux()) {
|
|
2587
|
+
try {
|
|
2588
|
+
execSync("systemctl --user is-active parall-daemon", { encoding: "utf-8", stdio: "pipe" });
|
|
2589
|
+
console.log("Service: running");
|
|
2590
|
+
} catch {
|
|
2591
|
+
console.log("Service: stopped or not installed");
|
|
2592
|
+
}
|
|
2593
|
+
}
|
|
2594
|
+
}
|
|
2595
|
+
function cmdStop() {
|
|
2596
|
+
if (isMacOS()) {
|
|
2597
|
+
execSync(`launchctl bootout gui/$(id -u) ${plistPath()} 2>/dev/null || true`, {
|
|
2598
|
+
stdio: "inherit"
|
|
2599
|
+
});
|
|
2600
|
+
} else if (isLinux()) {
|
|
2601
|
+
execSync("systemctl --user stop parall-daemon", { stdio: "inherit" });
|
|
2602
|
+
}
|
|
2603
|
+
console.log("Daemon stopped.");
|
|
2604
|
+
}
|
|
2605
|
+
function cmdLogs(lines) {
|
|
2606
|
+
if (isLinux()) {
|
|
2607
|
+
const child2 = spawn2("journalctl", ["--user-unit", "parall-daemon", "-n", lines, "-f"], {
|
|
2608
|
+
stdio: "inherit"
|
|
2609
|
+
});
|
|
2610
|
+
child2.on("exit", (code) => process.exit(code ?? 0));
|
|
2611
|
+
return;
|
|
2612
|
+
}
|
|
2613
|
+
const logPath = path3.join(os2.homedir(), "Library", "Logs", "parall-daemon.log");
|
|
2614
|
+
if (!fs3.existsSync(logPath)) {
|
|
2615
|
+
console.log("No log file found at", logPath);
|
|
2616
|
+
return;
|
|
2617
|
+
}
|
|
2618
|
+
const child = spawn2("tail", ["-n", lines, "-f", logPath], { stdio: "inherit" });
|
|
2619
|
+
child.on("exit", (code) => process.exit(code ?? 0));
|
|
2620
|
+
}
|
|
2621
|
+
function cmdServiceUninstall() {
|
|
2622
|
+
if (isMacOS()) {
|
|
2623
|
+
execSync(`launchctl bootout gui/$(id -u) ${plistPath()} 2>/dev/null || true`);
|
|
2624
|
+
if (fs3.existsSync(plistPath()))
|
|
2625
|
+
fs3.unlinkSync(plistPath());
|
|
2626
|
+
console.log("launchd agent uninstalled.");
|
|
2627
|
+
} else if (isLinux()) {
|
|
2628
|
+
execSync("systemctl --user stop parall-daemon 2>/dev/null || true");
|
|
2629
|
+
execSync("systemctl --user disable parall-daemon 2>/dev/null || true");
|
|
2630
|
+
if (fs3.existsSync(systemdUnitPath()))
|
|
2631
|
+
fs3.unlinkSync(systemdUnitPath());
|
|
2632
|
+
execSync("systemctl --user daemon-reload");
|
|
2633
|
+
console.log("systemd service uninstalled.");
|
|
2634
|
+
} else {
|
|
2635
|
+
console.log("Unsupported platform.");
|
|
2636
|
+
}
|
|
2637
|
+
}
|
|
2638
|
+
function printUsage() {
|
|
2639
|
+
console.log(`
|
|
2640
|
+
parall-daemon \u2014 Parall local agent runtime
|
|
2641
|
+
|
|
2642
|
+
Usage:
|
|
2643
|
+
parall-daemon Run the daemon (default, foreground)
|
|
2644
|
+
parall-daemon init Configure the daemon (interactive)
|
|
2645
|
+
parall-daemon status Show daemon service status
|
|
2646
|
+
parall-daemon stop Stop the background service
|
|
2647
|
+
parall-daemon logs [-n LINES] Tail daemon logs
|
|
2648
|
+
parall-daemon service install Install as background service (launchd/systemd)
|
|
2649
|
+
parall-daemon service uninstall Uninstall background service
|
|
2650
|
+
parall-daemon help Show this help
|
|
2651
|
+
`.trim());
|
|
2652
|
+
}
|
|
2653
|
+
async function runCLI(args) {
|
|
2654
|
+
const cmd = args[0];
|
|
2655
|
+
switch (cmd) {
|
|
2656
|
+
case "init":
|
|
2657
|
+
await cmdInit();
|
|
2658
|
+
return "handled";
|
|
2659
|
+
case "status":
|
|
2660
|
+
cmdStatus();
|
|
2661
|
+
return "handled";
|
|
2662
|
+
case "stop":
|
|
2663
|
+
cmdStop();
|
|
2664
|
+
return "handled";
|
|
2665
|
+
case "logs": {
|
|
2666
|
+
let lines = "50";
|
|
2667
|
+
if (args[1] === "-n") {
|
|
2668
|
+
const n = Number(args[2]);
|
|
2669
|
+
if (!Number.isInteger(n) || n <= 0) {
|
|
2670
|
+
console.error("Error: -n requires a positive integer");
|
|
2671
|
+
process.exit(1);
|
|
2672
|
+
}
|
|
2673
|
+
lines = String(n);
|
|
2674
|
+
}
|
|
2675
|
+
cmdLogs(lines);
|
|
2676
|
+
return "handled";
|
|
2677
|
+
}
|
|
2678
|
+
case "service": {
|
|
2679
|
+
const sub = args[1];
|
|
2680
|
+
if (sub === "install") {
|
|
2681
|
+
installService();
|
|
2682
|
+
} else if (sub === "uninstall") {
|
|
2683
|
+
cmdServiceUninstall();
|
|
2684
|
+
} else {
|
|
2685
|
+
console.error(`Unknown service command: ${sub ?? "(none)"}`);
|
|
2686
|
+
console.log("Usage: parall-daemon service [install|uninstall]");
|
|
2687
|
+
process.exit(1);
|
|
2688
|
+
}
|
|
2689
|
+
return "handled";
|
|
2690
|
+
}
|
|
2691
|
+
case "help":
|
|
2692
|
+
case "--help":
|
|
2693
|
+
case "-h":
|
|
2694
|
+
printUsage();
|
|
2695
|
+
return "handled";
|
|
2696
|
+
default:
|
|
2697
|
+
if (cmd) {
|
|
2698
|
+
console.error(`Unknown command: ${cmd}`);
|
|
2699
|
+
printUsage();
|
|
2700
|
+
process.exit(1);
|
|
2701
|
+
}
|
|
2702
|
+
return "run-daemon";
|
|
2703
|
+
}
|
|
2704
|
+
}
|
|
2705
|
+
|
|
2706
|
+
// ts/daemon/dist/index.js
|
|
2707
|
+
function createLogger(prefix) {
|
|
2708
|
+
return {
|
|
2709
|
+
info: (msg) => console.log(`[${prefix}] ${msg}`),
|
|
2710
|
+
warn: (msg) => console.warn(`[${prefix}] ${msg}`),
|
|
2711
|
+
error: (msg) => console.error(`[${prefix}] ${msg}`)
|
|
2712
|
+
};
|
|
2713
|
+
}
|
|
2714
|
+
function formatError(reason) {
|
|
2715
|
+
if (reason instanceof Error) {
|
|
2716
|
+
return reason.stack ?? reason.message;
|
|
2717
|
+
}
|
|
2718
|
+
return String(reason);
|
|
2719
|
+
}
|
|
2720
|
+
async function runForever(config, client, log, signal) {
|
|
2721
|
+
let attempt = 0;
|
|
2722
|
+
while (!signal.aborted) {
|
|
2723
|
+
const supervisor = new DaemonSupervisor(config, client, log);
|
|
2724
|
+
try {
|
|
2725
|
+
await supervisor.run(signal);
|
|
2726
|
+
await supervisor.stop();
|
|
2727
|
+
return;
|
|
2728
|
+
} catch (err) {
|
|
2729
|
+
log.error(`supervisor crashed: ${String(err)}`);
|
|
2730
|
+
try {
|
|
2731
|
+
await supervisor.stop();
|
|
2732
|
+
} catch (stopErr) {
|
|
2733
|
+
log.warn(`supervisor.stop() after crash threw: ${String(stopErr)}`);
|
|
2734
|
+
}
|
|
2735
|
+
if (config.supervisorRestartBackoffMs === 0) {
|
|
2736
|
+
log.error("supervisor keepalive disabled (PRLL_DAEMON_SUPERVISOR_RESTART_BACKOFF_MS=0) \u2014 exiting");
|
|
2737
|
+
throw err;
|
|
2738
|
+
}
|
|
2739
|
+
const delay = Math.min(config.supervisorRestartBackoffMs * Math.pow(2, attempt), config.supervisorRestartBackoffMaxMs);
|
|
2740
|
+
attempt += 1;
|
|
2741
|
+
log.warn(`restarting supervisor in ${delay}ms (attempt ${attempt})`);
|
|
2742
|
+
const slept = await sleepCancellable(delay, signal);
|
|
2743
|
+
if (!slept)
|
|
2744
|
+
return;
|
|
2745
|
+
}
|
|
2746
|
+
}
|
|
2747
|
+
}
|
|
2748
|
+
async function main() {
|
|
2749
|
+
const config = resolveClaudeDaemonConfig(process.env);
|
|
2750
|
+
const log = createLogger("daemon");
|
|
2751
|
+
log.info(`boot: api=${config.apiUrl} pollMs=${config.pollIntervalMs} heartbeatMs=${config.heartbeatIntervalMs} agentBin=${config.agentBin}`);
|
|
2752
|
+
log.info(`keepalive: bootstrapBackoffMs=${config.bootstrapBackoffMs} supervisorRestartBackoffMs=${config.supervisorRestartBackoffMs}`);
|
|
2753
|
+
const client = new ParallClient({
|
|
2754
|
+
baseUrl: config.apiUrl,
|
|
2755
|
+
token: config.apiKey
|
|
2756
|
+
});
|
|
2757
|
+
const abortController = new AbortController();
|
|
2758
|
+
const onSignal = (sig) => {
|
|
2759
|
+
log.info(`received ${sig} \u2014 initiating shutdown`);
|
|
2760
|
+
abortController.abort();
|
|
2761
|
+
};
|
|
2762
|
+
process.on("SIGINT", () => onSignal("SIGINT"));
|
|
2763
|
+
process.on("SIGTERM", () => onSignal("SIGTERM"));
|
|
2764
|
+
process.on("unhandledRejection", (reason) => {
|
|
2765
|
+
log.error(`unhandledRejection: ${formatError(reason)}`);
|
|
2766
|
+
});
|
|
2767
|
+
process.on("uncaughtException", (err) => {
|
|
2768
|
+
log.error(`uncaughtException: ${formatError(err)}`);
|
|
2769
|
+
process.exitCode = 1;
|
|
2770
|
+
process.exit(1);
|
|
2771
|
+
});
|
|
2772
|
+
config.wsUrl = resolveWsUrl(config.apiUrl, config.wsUrl);
|
|
2773
|
+
await runForever(config, client, log, abortController.signal);
|
|
2774
|
+
}
|
|
2775
|
+
var cliArgs = process.argv.slice(2);
|
|
2776
|
+
runCLI(cliArgs).then((result) => {
|
|
2777
|
+
if (result === "handled")
|
|
2778
|
+
return;
|
|
2779
|
+
main().catch((err) => {
|
|
2780
|
+
console.error(`[daemon] fatal: ${formatError(err)}`);
|
|
2781
|
+
process.exitCode = 1;
|
|
2782
|
+
});
|
|
2783
|
+
}).catch((err) => {
|
|
2784
|
+
console.error(`[daemon] fatal: ${formatError(err)}`);
|
|
2785
|
+
process.exitCode = 1;
|
|
2786
|
+
});
|