agent-companion 0.1.0 → 0.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +21 -379
- package/bridge/server.mjs +189 -10
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -1,12 +1,18 @@
|
|
|
1
|
-
# Agent Companion
|
|
1
|
+
# Agent Companion
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Agent Companion lets you start and monitor Codex and Claude Code from your phone while the work runs on your computer.
|
|
4
4
|
|
|
5
|
-
##
|
|
6
|
-
|
|
5
|
+
## What it does
|
|
6
|
+
- Pair your phone to your computer with a short code
|
|
7
|
+
- Start new Codex or Claude Code tasks remotely
|
|
8
|
+
- Check live run status and session updates
|
|
9
|
+
- Approve actions and send follow-up messages from your phone
|
|
10
|
+
|
|
11
|
+
## Run it
|
|
12
|
+
One-off:
|
|
7
13
|
|
|
8
14
|
```bash
|
|
9
|
-
npx agent-companion
|
|
15
|
+
npx -y agent-companion
|
|
10
16
|
```
|
|
11
17
|
|
|
12
18
|
Global install:
|
|
@@ -16,386 +22,22 @@ npm i -g agent-companion
|
|
|
16
22
|
agent-companion
|
|
17
23
|
```
|
|
18
24
|
|
|
19
|
-
|
|
25
|
+
## Options
|
|
26
|
+
Use your hosted relay:
|
|
20
27
|
|
|
21
28
|
```bash
|
|
22
|
-
agent-companion
|
|
23
29
|
agent-companion --relay https://agent-companion-relay.onrender.com
|
|
24
|
-
agent-companion --with-local-relay
|
|
25
|
-
```
|
|
26
|
-
|
|
27
|
-
## Included
|
|
28
|
-
- Live status for Codex + Claude Code sessions.
|
|
29
|
-
- Session states: `RUNNING`, `WAITING_INPUT`, `COMPLETED`, `FAILED`, `CANCELLED`.
|
|
30
|
-
- Pending input actions: `Approve`, `Reject`, `Text Reply`.
|
|
31
|
-
- Timeline and token stats (total + per-agent).
|
|
32
|
-
- Bridge mode for real laptop sessions.
|
|
33
|
-
- Direct monitoring of local Codex and Claude Code history files (no wrapper required).
|
|
34
|
-
- Phone-triggered local task launcher (no GitHub required).
|
|
35
|
-
- Relay pairing flow for phone-to-laptop remote control.
|
|
36
|
-
- Auto-wake support for sleeping laptops (Wake-on-LAN via wake proxy).
|
|
37
|
-
|
|
38
|
-
## UI workflow (Claude resume thread)
|
|
39
|
-
For UI-only iteration, use your dedicated Claude thread:
|
|
40
|
-
|
|
41
|
-
```bash
|
|
42
|
-
cd /Users/nakshjain/Desktop/agent
|
|
43
|
-
npm run ui:claude
|
|
44
|
-
```
|
|
45
|
-
|
|
46
|
-
Background delegation (non-interactive, recommended):
|
|
47
|
-
|
|
48
|
-
```bash
|
|
49
|
-
cd /Users/nakshjain/Desktop/agent
|
|
50
|
-
npm run ui:delegate -- "Fix chat response UX and composer jump"
|
|
51
|
-
```
|
|
52
|
-
|
|
53
|
-
This writes job logs to `.agent/ui-jobs/*.log` and metadata to `.agent/ui-jobs/*.json`.
|
|
54
|
-
|
|
55
|
-
Direct command:
|
|
56
|
-
|
|
57
|
-
```bash
|
|
58
|
-
claude --resume 9f359518-cae4-4da8-9e55-0ac7e261e85a
|
|
59
|
-
```
|
|
60
|
-
|
|
61
|
-
Suggested prompt to paste in that thread:
|
|
62
|
-
|
|
63
|
-
```text
|
|
64
|
-
Need focused UI polish on Agent Companion chat view:
|
|
65
|
-
1) While model is responding, show stable "Thinking..." (no blinking cursor).
|
|
66
|
-
2) Do not show intermediate thinking/token/tool fragments that later disappear.
|
|
67
|
-
3) Render final assistant response cleanly with no flicker or swap-jump.
|
|
68
|
-
4) Fix composer/input/cursor jump (input should stay fixed; no up/down movement during updates).
|
|
69
|
-
|
|
70
|
-
Constraints:
|
|
71
|
-
- Keep dark premium style intact.
|
|
72
|
-
- Do not change backend contracts.
|
|
73
|
-
- Keep changes minimal and production-safe.
|
|
74
|
-
- Return exact files changed and why.
|
|
75
|
-
```
|
|
76
|
-
|
|
77
|
-
## 0) Full remote pairing flow (phone + laptop)
|
|
78
|
-
This is the end-to-end path when users are not on the same local network.
|
|
79
|
-
|
|
80
|
-
Terminal A (relay):
|
|
81
|
-
```bash
|
|
82
|
-
cd /Users/nakshjain/Desktop/agent
|
|
83
|
-
npm install
|
|
84
|
-
npm run relay
|
|
85
|
-
```
|
|
86
|
-
|
|
87
|
-
Terminal B (laptop companion):
|
|
88
|
-
```bash
|
|
89
|
-
cd /Users/nakshjain/Desktop/agent
|
|
90
|
-
npm run laptop:companion -- --relay http://localhost:9797 --bridge http://localhost:8787
|
|
91
|
-
```
|
|
92
|
-
|
|
93
|
-
What happens:
|
|
94
|
-
- Companion registers laptop with relay.
|
|
95
|
-
- Companion prints a short pair code.
|
|
96
|
-
- Phone app claims pair code via relay.
|
|
97
|
-
- Relay stores phone token and proxies requests to connected laptop bridge.
|
|
98
|
-
|
|
99
|
-
Useful relay endpoints:
|
|
100
|
-
```bash
|
|
101
|
-
curl http://localhost:9797/health
|
|
102
|
-
curl http://localhost:9797/pair?code=<PAIR_CODE>
|
|
103
|
-
```
|
|
104
|
-
|
|
105
|
-
Phone-side API contract (with `Authorization: Bearer <phoneToken>`):
|
|
106
|
-
- `GET /api/devices/:id/status`
|
|
107
|
-
- `GET /api/devices/:id/bootstrap`
|
|
108
|
-
- `POST /api/devices/:id/actions`
|
|
109
|
-
- `POST /api/devices/:id/sessions/:sessionId/messages`
|
|
110
|
-
- `GET /api/devices/:id/launcher/workspaces`
|
|
111
|
-
- `GET /api/devices/:id/launcher/runs`
|
|
112
|
-
- `POST /api/devices/:id/launcher/start`
|
|
113
|
-
- `POST /api/devices/:id/launcher/runs/:runId/stop`
|
|
114
|
-
- `GET /api/devices/:id/launcher/services`
|
|
115
|
-
- `POST /api/devices/:id/launcher/services/start`
|
|
116
|
-
- `POST /api/devices/:id/launcher/services/:serviceId/stop`
|
|
117
|
-
- `GET /api/devices/:id/previews`
|
|
118
|
-
- `POST /api/devices/:id/previews`
|
|
119
|
-
- `DELETE /api/devices/:id/previews/:previewId`
|
|
120
|
-
- `POST /api/devices/:id/wake`
|
|
121
|
-
|
|
122
|
-
Public preview URL format:
|
|
123
|
-
- `GET /p/:previewToken/*` (also available as `/preview/:previewToken/*`)
|
|
124
|
-
|
|
125
|
-
## 0.2) Live preview tunnel (laptop app -> phone browser)
|
|
126
|
-
Expose a locally running app (for example `http://localhost:5173`) to phone through relay:
|
|
127
|
-
|
|
128
|
-
```bash
|
|
129
|
-
curl -X POST "https://<relay>/api/devices/<deviceId>/previews" \
|
|
130
|
-
-H "Authorization: Bearer <phoneToken>" \
|
|
131
|
-
-H "Content-Type: application/json" \
|
|
132
|
-
-d '{
|
|
133
|
-
"port": 5173,
|
|
134
|
-
"label": "Vite Preview"
|
|
135
|
-
}'
|
|
136
|
-
```
|
|
137
|
-
|
|
138
|
-
Response includes `preview.publicUrl` (for example `https://<relay>/p/<token>`).
|
|
139
|
-
Open that URL on phone to view the app running on laptop.
|
|
140
|
-
|
|
141
|
-
List previews:
|
|
142
|
-
```bash
|
|
143
|
-
curl "https://<relay>/api/devices/<deviceId>/previews" \
|
|
144
|
-
-H "Authorization: Bearer <phoneToken>"
|
|
145
|
-
```
|
|
146
|
-
|
|
147
|
-
Stop a preview:
|
|
148
|
-
```bash
|
|
149
|
-
curl -X DELETE "https://<relay>/api/devices/<deviceId>/previews/<previewId>" \
|
|
150
|
-
-H "Authorization: Bearer <phoneToken>"
|
|
151
|
-
```
|
|
152
|
-
|
|
153
|
-
Notes:
|
|
154
|
-
- Target must be local-only (`localhost`, `127.0.0.1`, `::1`, `0.0.0.0`) for safety.
|
|
155
|
-
- Preview links are temporary (TTL).
|
|
156
|
-
- Laptop still needs internet + relay connection (sleep/offline handled via wake flow if configured).
|
|
157
|
-
|
|
158
|
-
### Keep localhost servers alive after agent run exits
|
|
159
|
-
Some agent shells clean detached processes when a one-shot run ends.
|
|
160
|
-
Use the managed background service command so preview URLs keep working:
|
|
161
|
-
|
|
162
|
-
```bash
|
|
163
|
-
cd /Users/nakshjain/Desktop/agent
|
|
164
|
-
npm run background:start -- \
|
|
165
|
-
--workspace "/absolute/workspace/path" \
|
|
166
|
-
--session "<optional-session-id>" \
|
|
167
|
-
--label "dev-server" \
|
|
168
|
-
--port 5173 \
|
|
169
|
-
-- npm run dev -- --host 127.0.0.1 --port 5173
|
|
170
|
-
```
|
|
171
|
-
|
|
172
|
-
List/stop managed services:
|
|
173
|
-
|
|
174
|
-
```bash
|
|
175
|
-
curl http://localhost:8787/api/launcher/services
|
|
176
|
-
curl -X POST http://localhost:8787/api/launcher/services/<serviceId>/stop \
|
|
177
|
-
-H "Content-Type: application/json" \
|
|
178
|
-
-d '{"signal":"SIGTERM"}'
|
|
179
|
-
```
|
|
180
|
-
|
|
181
|
-
Agent runs now receive a runtime hint to use this helper whenever they need a persistent localhost server.
|
|
182
|
-
|
|
183
|
-
## 0.25) Auto-wake for sleeping laptops
|
|
184
|
-
To wake a sleeping laptop automatically before a run/message, deploy the wake proxy on an always-on device in the same LAN as the laptop (small VM, mini-PC, NAS, router host, etc).
|
|
185
|
-
|
|
186
|
-
Wake proxy:
|
|
187
|
-
```bash
|
|
188
|
-
cd /Users/nakshjain/Desktop/agent
|
|
189
|
-
npm run wake-proxy
|
|
190
|
-
```
|
|
191
|
-
|
|
192
|
-
Relay environment:
|
|
193
|
-
- `RELAY_WAKE_PROXY_URL=https://<wake-proxy-host>`
|
|
194
|
-
- `RELAY_WAKE_PROXY_TOKEN=<shared-secret>` (optional but recommended)
|
|
195
|
-
|
|
196
|
-
Wake proxy environment:
|
|
197
|
-
- `WAKE_PROXY_TOKEN=<shared-secret>` (must match relay token if set)
|
|
198
|
-
- `WAKE_BROADCAST_ADDRESS=255.255.255.255` (or subnet broadcast)
|
|
199
|
-
- `WAKE_UDP_PORT=9`
|
|
200
|
-
|
|
201
|
-
Laptop companion auto-detects MAC address and registers it with relay. You can override:
|
|
202
|
-
```bash
|
|
203
|
-
npm run laptop:service -- --wake-mac AA:BB:CC:DD:EE:FF
|
|
204
|
-
```
|
|
205
|
-
|
|
206
|
-
## 0.5) One-command laptop service (recommended packaging)
|
|
207
|
-
Run bridge + companion together from one command:
|
|
208
|
-
|
|
209
|
-
```bash
|
|
210
|
-
cd /Users/nakshjain/Desktop/agent
|
|
211
|
-
npm run laptop:service
|
|
212
|
-
```
|
|
213
|
-
|
|
214
|
-
Override relay URL when needed:
|
|
215
|
-
|
|
216
|
-
```bash
|
|
217
|
-
cd /Users/nakshjain/Desktop/agent
|
|
218
|
-
npm run laptop:service -- --relay https://<your-public-relay-url>
|
|
219
|
-
```
|
|
220
|
-
|
|
221
|
-
All-in-one local test mode (starts relay too):
|
|
222
|
-
|
|
223
|
-
```bash
|
|
224
|
-
cd /Users/nakshjain/Desktop/agent
|
|
225
|
-
npm run laptop:service -- --with-local-relay
|
|
226
|
-
```
|
|
227
|
-
|
|
228
|
-
Notes:
|
|
229
|
-
- `--with-local-relay` is for local/dev only unless you expose relay publicly.
|
|
230
|
-
- Default public relay is `https://agent-companion-relay.onrender.com`.
|
|
231
|
-
- Override relay via `--relay` only when needed.
|
|
232
|
-
- Optional `--wake-mac` sets/overrides Wake-on-LAN MAC.
|
|
233
|
-
- Companion prints pair code only (no QR).
|
|
234
|
-
|
|
235
|
-
## 0.6) Deploy relay on Render (free)
|
|
236
|
-
This repo now includes a Render blueprint at `render.yaml`.
|
|
237
|
-
|
|
238
|
-
Quick path:
|
|
239
|
-
1. Push this repo to GitHub.
|
|
240
|
-
2. In Render, create a new **Blueprint** service from that repo.
|
|
241
|
-
3. Set env var `RELAY_PUBLIC_URL` to your Render HTTPS URL (for example `https://agent-companion-relay.onrender.com`).
|
|
242
|
-
4. (Optional auto-wake) Set `RELAY_WAKE_PROXY_URL` and `RELAY_WAKE_PROXY_TOKEN`.
|
|
243
|
-
5. Deploy.
|
|
244
|
-
|
|
245
|
-
After deploy:
|
|
246
|
-
```bash
|
|
247
|
-
cd /Users/nakshjain/Desktop/agent
|
|
248
|
-
npm run laptop:service -- --relay https://<your-render-relay>.onrender.com
|
|
249
|
-
```
|
|
250
|
-
|
|
251
|
-
In the phone app pairing screen, enter only the pair code.
|
|
252
|
-
|
|
253
|
-
Note: Render free tier can sleep when idle; first request/pair may take a short warm-up.
|
|
254
|
-
|
|
255
|
-
## 1) Start the app + local bridge
|
|
256
|
-
Open two terminals:
|
|
257
|
-
|
|
258
|
-
Terminal A (bridge API)
|
|
259
|
-
```bash
|
|
260
|
-
cd /Users/nakshjain/Desktop/agent
|
|
261
|
-
npm install
|
|
262
|
-
npm run bridge
|
|
263
|
-
```
|
|
264
|
-
|
|
265
|
-
Terminal B (web app)
|
|
266
|
-
```bash
|
|
267
|
-
cd /Users/nakshjain/Desktop/agent
|
|
268
|
-
npm run dev -- --host 0.0.0.0 --port 5173
|
|
269
|
-
```
|
|
270
|
-
|
|
271
|
-
On your laptop open: [http://localhost:5173](http://localhost:5173)
|
|
272
|
-
|
|
273
|
-
On your phone (same Wi-Fi) open: `http://<your-laptop-ip>:5173`
|
|
274
|
-
|
|
275
|
-
One-command alternative:
|
|
276
|
-
```bash
|
|
277
|
-
cd /Users/nakshjain/Desktop/agent
|
|
278
|
-
npm install
|
|
279
|
-
npm run stack:start
|
|
280
|
-
```
|
|
281
|
-
|
|
282
|
-
## 2) Use Codex/Claude directly (recommended)
|
|
283
|
-
With bridge running, direct CLI sessions are auto-ingested from local history:
|
|
284
|
-
|
|
285
|
-
```bash
|
|
286
|
-
# direct Codex
|
|
287
|
-
codex exec "Implement feature X"
|
|
288
|
-
|
|
289
|
-
# direct Claude Code
|
|
290
|
-
claude -p "Implement feature Y"
|
|
291
30
|
```
|
|
292
31
|
|
|
293
|
-
|
|
32
|
+
Run with a local relay for development:
|
|
294
33
|
|
|
295
|
-
## 3) Launch local workspace tasks from phone/API (no GitHub)
|
|
296
|
-
The bridge can start agent runs directly on your laptop workspace via REST.
|
|
297
|
-
|
|
298
|
-
Discover candidate workspaces:
|
|
299
34
|
```bash
|
|
300
|
-
|
|
301
|
-
```
|
|
302
|
-
|
|
303
|
-
Start a Codex task in a local folder:
|
|
304
|
-
```bash
|
|
305
|
-
curl -X POST http://localhost:8787/api/launcher/start \\
|
|
306
|
-
-H 'Content-Type: application/json' \\
|
|
307
|
-
-d '{
|
|
308
|
-
"agentType":"CODEX",
|
|
309
|
-
"workspacePath":"/absolute/path/to/local/project",
|
|
310
|
-
"title":"Phone task",
|
|
311
|
-
"prompt":"Implement auth middleware and tests"
|
|
312
|
-
}'
|
|
313
|
-
```
|
|
314
|
-
|
|
315
|
-
Optional CLI helper:
|
|
316
|
-
```bash
|
|
317
|
-
cd /Users/nakshjain/Desktop/agent
|
|
318
|
-
npm run task:start -- --agent CODEX --workspace /absolute/path/to/local/project --prompt "Implement auth middleware and tests"
|
|
319
|
-
```
|
|
320
|
-
|
|
321
|
-
Track runs:
|
|
322
|
-
```bash
|
|
323
|
-
curl http://localhost:8787/api/launcher/runs
|
|
324
|
-
```
|
|
325
|
-
|
|
326
|
-
For Codex runs, the bridge timeline now records a resume hint like:
|
|
327
|
-
```bash
|
|
328
|
-
codex exec resume <thread-id>
|
|
329
|
-
```
|
|
330
|
-
Use that on your laptop to continue the same Codex thread later.
|
|
331
|
-
|
|
332
|
-
Launcher-created Codex runs are also promoted to CLI resume metadata so they appear in the Codex `/resume` picker.
|
|
333
|
-
This promotion is applied after run completion with atomic file writes for safety.
|
|
334
|
-
|
|
335
|
-
Optional controls:
|
|
336
|
-
- `AGENT_ENABLE_CODEX_RESUME_PROMOTION=false` to disable rollout metadata promotion.
|
|
337
|
-
- `AGENT_ENABLE_CODEX_THREAD_INDEX=false` to disable thread title indexing in `~/.codex/.codex-global-state.json`.
|
|
338
|
-
|
|
339
|
-
If your task was launched in another workspace, use:
|
|
340
|
-
```bash
|
|
341
|
-
codex resume --all
|
|
342
|
-
```
|
|
343
|
-
The default `codex resume` picker is workspace-scoped.
|
|
344
|
-
|
|
345
|
-
Backfill repair for older launcher sessions:
|
|
346
|
-
```bash
|
|
347
|
-
cd /Users/nakshjain/Desktop/agent
|
|
348
|
-
npm run resume:repair
|
|
349
|
-
```
|
|
350
|
-
|
|
351
|
-
Stop a run:
|
|
352
|
-
```bash
|
|
353
|
-
curl -X POST http://localhost:8787/api/launcher/runs/<run-id>/stop
|
|
354
|
-
```
|
|
355
|
-
|
|
356
|
-
If you set `AGENT_BRIDGE_TOKEN`, include header `x-bridge-token` on `/api/launcher/*` calls.
|
|
357
|
-
You can also lock workspaces with:
|
|
358
|
-
- `AGENT_BRIDGE_ALLOW_ANY_WORKSPACE=false`
|
|
359
|
-
- `AGENT_BRIDGE_WORKSPACE_ROOTS=/path/one,/path/two`
|
|
360
|
-
|
|
361
|
-
## 4) Optional wrapper mode
|
|
362
|
-
You can still run via wrapper for explicit session IDs and custom metadata.
|
|
363
|
-
|
|
364
|
-
Codex example:
|
|
365
|
-
```bash
|
|
366
|
-
cd /Users/nakshjain/Desktop/agent
|
|
367
|
-
npm run run:agent -- --agent CODEX --title "Fix auth middleware" -- codex exec "Fix auth middleware"
|
|
368
|
-
```
|
|
369
|
-
|
|
370
|
-
Claude Code example:
|
|
371
|
-
```bash
|
|
372
|
-
cd /Users/nakshjain/Desktop/agent
|
|
373
|
-
npm run run:agent -- --agent CLAUDE --title "Improve retry logic" -- claude -p "Improve retry logic"
|
|
374
|
-
```
|
|
375
|
-
|
|
376
|
-
When the command runs, the session appears in the PWA. When it exits, state updates to completed/failed.
|
|
377
|
-
If you type `codex run ...`, the wrapper auto-converts it to `codex exec ...`.
|
|
378
|
-
For Codex commands, the wrapper also injects `--skip-git-repo-check` automatically.
|
|
379
|
-
|
|
380
|
-
## 5) Test pending input flow
|
|
381
|
-
```bash
|
|
382
|
-
cd /Users/nakshjain/Desktop/agent
|
|
383
|
-
npm run pending:add -- --session s_codex_live_01 --priority HIGH --prompt "Approve migration plan?"
|
|
384
|
-
```
|
|
385
|
-
|
|
386
|
-
Then use `Approve`, `Reject`, or `Text Reply` in the phone UI.
|
|
387
|
-
|
|
388
|
-
## Useful commands
|
|
389
|
-
```bash
|
|
390
|
-
npm run bridge:reset # reset bridge to default sample sessions
|
|
391
|
-
npm run build # production build
|
|
392
|
-
npm run preview # preview production build
|
|
35
|
+
agent-companion --with-local-relay
|
|
393
36
|
```
|
|
394
37
|
|
|
395
|
-
##
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
- Protect bridge launcher routes with `AGENT_BRIDGE_TOKEN` and send `x-bridge-token` from trusted callers.
|
|
38
|
+
## Pairing flow
|
|
39
|
+
1. Run `agent-companion` on your computer
|
|
40
|
+
2. Copy the pairing code shown in the terminal
|
|
41
|
+
3. Open the Agent Companion web app on your phone
|
|
42
|
+
4. Enter the pairing code
|
|
43
|
+
5. Start and monitor tasks from your phone
|
package/bridge/server.mjs
CHANGED
|
@@ -151,6 +151,11 @@ function normalizeState(value) {
|
|
|
151
151
|
return allowed.includes(value) ? value : "RUNNING";
|
|
152
152
|
}
|
|
153
153
|
|
|
154
|
+
function isDirectSessionId(value) {
|
|
155
|
+
const id = safeTrimmedText(value, 160);
|
|
156
|
+
return id.startsWith("codex:") || id.startsWith("claude:");
|
|
157
|
+
}
|
|
158
|
+
|
|
154
159
|
function normalizeCategory(value) {
|
|
155
160
|
const allowed = ["INFO", "ACTION", "INPUT", "ERROR"];
|
|
156
161
|
return allowed.includes(value) ? value : "INFO";
|
|
@@ -627,6 +632,170 @@ function extractClaudeSessionIdFromRun(run) {
|
|
|
627
632
|
return "";
|
|
628
633
|
}
|
|
629
634
|
|
|
635
|
+
function normalizeClaudeSessionId(value) {
|
|
636
|
+
const candidate = safeTrimmedText(value, 120).toLowerCase();
|
|
637
|
+
if (!candidate) return "";
|
|
638
|
+
if (/^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/.test(candidate)) {
|
|
639
|
+
return candidate;
|
|
640
|
+
}
|
|
641
|
+
return "";
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
function getLauncherRunUpdatedAt(run) {
|
|
645
|
+
return Math.max(
|
|
646
|
+
safeNumber(run?.endedAt, 0),
|
|
647
|
+
safeNumber(run?.startedAt, 0),
|
|
648
|
+
safeNumber(run?.createdAt, 0)
|
|
649
|
+
);
|
|
650
|
+
}
|
|
651
|
+
|
|
652
|
+
function resolveCanonicalSessionIdForDirectSession(sessionId) {
|
|
653
|
+
const directSessionId = safeTrimmedText(sessionId, 160);
|
|
654
|
+
if (!isDirectSessionId(directSessionId)) return directSessionId;
|
|
655
|
+
|
|
656
|
+
if (directSessionId.startsWith("codex:")) {
|
|
657
|
+
const threadId = directSessionId.slice("codex:".length).trim().toLowerCase();
|
|
658
|
+
if (!threadId) return directSessionId;
|
|
659
|
+
|
|
660
|
+
const mappedRun = [...launcherRuns.values()]
|
|
661
|
+
.slice()
|
|
662
|
+
.sort((a, b) => getLauncherRunUpdatedAt(b) - getLauncherRunUpdatedAt(a))
|
|
663
|
+
.find((run) => !isDirectSessionId(run?.sessionId) && extractCodexThreadIdFromRun(run) === threadId);
|
|
664
|
+
if (mappedRun?.sessionId) return mappedRun.sessionId;
|
|
665
|
+
|
|
666
|
+
const mappedThread = (Array.isArray(state.sessionThreads) ? state.sessionThreads : [])
|
|
667
|
+
.slice()
|
|
668
|
+
.sort((a, b) => safeNumber(b.updatedAt, 0) - safeNumber(a.updatedAt, 0))
|
|
669
|
+
.find(
|
|
670
|
+
(thread) =>
|
|
671
|
+
!isDirectSessionId(thread?.id) &&
|
|
672
|
+
safeTrimmedText(thread?.codexThreadId, 120).toLowerCase() === threadId
|
|
673
|
+
);
|
|
674
|
+
if (mappedThread?.id) return mappedThread.id;
|
|
675
|
+
|
|
676
|
+
return directSessionId;
|
|
677
|
+
}
|
|
678
|
+
|
|
679
|
+
const claudeSessionId = normalizeClaudeSessionId(directSessionId.slice("claude:".length));
|
|
680
|
+
if (!claudeSessionId) return directSessionId;
|
|
681
|
+
|
|
682
|
+
const mappedRun = [...launcherRuns.values()]
|
|
683
|
+
.slice()
|
|
684
|
+
.sort((a, b) => getLauncherRunUpdatedAt(b) - getLauncherRunUpdatedAt(a))
|
|
685
|
+
.find((run) => !isDirectSessionId(run?.sessionId) && extractClaudeSessionIdFromRun(run) === claudeSessionId);
|
|
686
|
+
if (mappedRun?.sessionId) return mappedRun.sessionId;
|
|
687
|
+
|
|
688
|
+
const mappedThread = (Array.isArray(state.sessionThreads) ? state.sessionThreads : [])
|
|
689
|
+
.slice()
|
|
690
|
+
.sort((a, b) => safeNumber(b.updatedAt, 0) - safeNumber(a.updatedAt, 0))
|
|
691
|
+
.find(
|
|
692
|
+
(thread) =>
|
|
693
|
+
!isDirectSessionId(thread?.id) &&
|
|
694
|
+
normalizeClaudeSessionId(thread?.claudeSessionId) === claudeSessionId
|
|
695
|
+
);
|
|
696
|
+
if (mappedThread?.id) return mappedThread.id;
|
|
697
|
+
|
|
698
|
+
return directSessionId;
|
|
699
|
+
}
|
|
700
|
+
|
|
701
|
+
function rewriteDirectScopedId(id, originalSessionId, canonicalSessionId) {
|
|
702
|
+
const text = safeTrimmedText(id, 240);
|
|
703
|
+
if (!text || !originalSessionId || !canonicalSessionId || originalSessionId === canonicalSessionId) {
|
|
704
|
+
return text || id;
|
|
705
|
+
}
|
|
706
|
+
|
|
707
|
+
if (text === `pending:${originalSessionId}`) {
|
|
708
|
+
return `pending:${canonicalSessionId}`;
|
|
709
|
+
}
|
|
710
|
+
if (text === `event:${originalSessionId}`) {
|
|
711
|
+
return `event:${canonicalSessionId}`;
|
|
712
|
+
}
|
|
713
|
+
|
|
714
|
+
const directPrefix = `direct:${originalSessionId}:`;
|
|
715
|
+
if (text.startsWith(directPrefix)) {
|
|
716
|
+
return `direct:${canonicalSessionId}:${text.slice(directPrefix.length)}`;
|
|
717
|
+
}
|
|
718
|
+
|
|
719
|
+
return text;
|
|
720
|
+
}
|
|
721
|
+
|
|
722
|
+
function canonicalizeDirectSnapshot(snapshot) {
|
|
723
|
+
if (!snapshot || typeof snapshot !== "object") return snapshot;
|
|
724
|
+
|
|
725
|
+
const sessionIdMap = new Map();
|
|
726
|
+
const sourceDirectSessionIds = new Set();
|
|
727
|
+
|
|
728
|
+
const remapSessionId = (value) => {
|
|
729
|
+
const sessionId = safeTrimmedText(value, 160);
|
|
730
|
+
if (!isDirectSessionId(sessionId)) return sessionId;
|
|
731
|
+
sourceDirectSessionIds.add(sessionId);
|
|
732
|
+
|
|
733
|
+
const existing = sessionIdMap.get(sessionId);
|
|
734
|
+
if (existing) return existing;
|
|
735
|
+
|
|
736
|
+
const canonical = resolveCanonicalSessionIdForDirectSession(sessionId) || sessionId;
|
|
737
|
+
sessionIdMap.set(sessionId, canonical);
|
|
738
|
+
return canonical;
|
|
739
|
+
};
|
|
740
|
+
|
|
741
|
+
const sessions = (Array.isArray(snapshot.sessions) ? snapshot.sessions : []).map((item) => {
|
|
742
|
+
const originalSessionId = safeTrimmedText(item?.id, 160);
|
|
743
|
+
const canonicalSessionId = remapSessionId(originalSessionId);
|
|
744
|
+
if (!originalSessionId || !canonicalSessionId || canonicalSessionId === originalSessionId) {
|
|
745
|
+
return item;
|
|
746
|
+
}
|
|
747
|
+
return { ...item, id: canonicalSessionId };
|
|
748
|
+
});
|
|
749
|
+
|
|
750
|
+
const pendingInputs = (Array.isArray(snapshot.pendingInputs) ? snapshot.pendingInputs : []).map((item) => {
|
|
751
|
+
const originalSessionId = safeTrimmedText(item?.sessionId, 160);
|
|
752
|
+
const canonicalSessionId = remapSessionId(originalSessionId);
|
|
753
|
+
if (!originalSessionId || !canonicalSessionId || canonicalSessionId === originalSessionId) {
|
|
754
|
+
return item;
|
|
755
|
+
}
|
|
756
|
+
return {
|
|
757
|
+
...item,
|
|
758
|
+
id: rewriteDirectScopedId(item?.id, originalSessionId, canonicalSessionId),
|
|
759
|
+
sessionId: canonicalSessionId
|
|
760
|
+
};
|
|
761
|
+
});
|
|
762
|
+
|
|
763
|
+
const events = (Array.isArray(snapshot.events) ? snapshot.events : []).map((item) => {
|
|
764
|
+
const originalSessionId = safeTrimmedText(item?.sessionId, 160);
|
|
765
|
+
const canonicalSessionId = remapSessionId(originalSessionId);
|
|
766
|
+
if (!originalSessionId || !canonicalSessionId || canonicalSessionId === originalSessionId) {
|
|
767
|
+
return item;
|
|
768
|
+
}
|
|
769
|
+
return {
|
|
770
|
+
...item,
|
|
771
|
+
id: rewriteDirectScopedId(item?.id, originalSessionId, canonicalSessionId),
|
|
772
|
+
sessionId: canonicalSessionId
|
|
773
|
+
};
|
|
774
|
+
});
|
|
775
|
+
|
|
776
|
+
const chatTurns = (Array.isArray(snapshot.chatTurns) ? snapshot.chatTurns : []).map((item) => {
|
|
777
|
+
const originalSessionId = safeTrimmedText(item?.sessionId, 160);
|
|
778
|
+
const canonicalSessionId = remapSessionId(originalSessionId);
|
|
779
|
+
if (!originalSessionId || !canonicalSessionId || canonicalSessionId === originalSessionId) {
|
|
780
|
+
return item;
|
|
781
|
+
}
|
|
782
|
+
return {
|
|
783
|
+
...item,
|
|
784
|
+
id: rewriteDirectScopedId(item?.id, originalSessionId, canonicalSessionId),
|
|
785
|
+
sessionId: canonicalSessionId
|
|
786
|
+
};
|
|
787
|
+
});
|
|
788
|
+
|
|
789
|
+
return {
|
|
790
|
+
...snapshot,
|
|
791
|
+
sessions,
|
|
792
|
+
pendingInputs,
|
|
793
|
+
events,
|
|
794
|
+
chatTurns,
|
|
795
|
+
directSourceSessionIds: [...sourceDirectSessionIds]
|
|
796
|
+
};
|
|
797
|
+
}
|
|
798
|
+
|
|
630
799
|
function buildContinueCommandFromThread(thread, prompt) {
|
|
631
800
|
const safePrompt = safeTrimmedText(prompt, 1500);
|
|
632
801
|
if (!safePrompt || !thread) return null;
|
|
@@ -1937,21 +2106,28 @@ function stopManagedService(service, signalInput) {
|
|
|
1937
2106
|
function mergeDirectSnapshot(snapshot) {
|
|
1938
2107
|
if (!snapshot || typeof snapshot !== "object") return;
|
|
1939
2108
|
|
|
2109
|
+
snapshot = canonicalizeDirectSnapshot(snapshot);
|
|
2110
|
+
|
|
1940
2111
|
const incomingDirectSessionIds = new Set(
|
|
1941
2112
|
(Array.isArray(snapshot.sessions) ? snapshot.sessions : [])
|
|
1942
2113
|
.map((item) => item?.id)
|
|
1943
|
-
.filter((id) => typeof id === "string")
|
|
2114
|
+
.filter((id) => typeof id === "string" && isDirectSessionId(id))
|
|
2115
|
+
);
|
|
2116
|
+
const incomingDirectSourceSessionIds = new Set(
|
|
2117
|
+
(Array.isArray(snapshot.directSourceSessionIds) ? snapshot.directSourceSessionIds : [])
|
|
2118
|
+
.map((item) => safeTrimmedText(item, 160))
|
|
2119
|
+
.filter(Boolean)
|
|
1944
2120
|
);
|
|
1945
2121
|
|
|
1946
|
-
if (incomingDirectSessionIds.size > 0) {
|
|
2122
|
+
if (incomingDirectSessionIds.size > 0 || incomingDirectSourceSessionIds.size > 0) {
|
|
1947
2123
|
state.sessions = state.sessions.filter((item) => {
|
|
1948
2124
|
const id = String(item?.id || "");
|
|
1949
|
-
if (!
|
|
2125
|
+
if (!isDirectSessionId(id)) return true;
|
|
1950
2126
|
return incomingDirectSessionIds.has(id);
|
|
1951
2127
|
});
|
|
1952
2128
|
state.sessionThreads = (Array.isArray(state.sessionThreads) ? state.sessionThreads : []).filter((item) => {
|
|
1953
2129
|
const id = String(item?.id || "");
|
|
1954
|
-
if (!
|
|
2130
|
+
if (!isDirectSessionId(id)) return true;
|
|
1955
2131
|
return incomingDirectSessionIds.has(id);
|
|
1956
2132
|
});
|
|
1957
2133
|
}
|
|
@@ -2802,11 +2978,14 @@ app.post("/api/import/snapshot", (req, res) => {
|
|
|
2802
2978
|
return res.json({ ok: true });
|
|
2803
2979
|
});
|
|
2804
2980
|
|
|
2981
|
+
function refreshDirectSnapshot() {
|
|
2982
|
+
withPersist(() => {
|
|
2983
|
+
const snapshot = collectDirectSnapshot();
|
|
2984
|
+
mergeDirectSnapshot(snapshot);
|
|
2985
|
+
});
|
|
2986
|
+
}
|
|
2987
|
+
|
|
2805
2988
|
app.listen(PORT, () => {
|
|
2806
|
-
|
|
2807
|
-
|
|
2808
|
-
const snapshot = collectDirectSnapshot();
|
|
2809
|
-
mergeDirectSnapshot(snapshot);
|
|
2810
|
-
});
|
|
2811
|
-
}, DIRECT_SNAPSHOT_POLL_INTERVAL_MS);
|
|
2989
|
+
refreshDirectSnapshot();
|
|
2990
|
+
setInterval(refreshDirectSnapshot, DIRECT_SNAPSHOT_POLL_INTERVAL_MS);
|
|
2812
2991
|
});
|
package/package.json
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "agent-companion",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.2",
|
|
4
4
|
"private": false,
|
|
5
5
|
"type": "module",
|
|
6
|
-
"description": "
|
|
6
|
+
"description": "Phone-to-computer companion for Codex and Claude Code.",
|
|
7
7
|
"repository": {
|
|
8
8
|
"type": "git",
|
|
9
9
|
"url": "git+https://github.com/Nakshjainsonigara/agent_companion.git"
|