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.
Files changed (3) hide show
  1. package/README.md +21 -379
  2. package/bridge/server.mjs +189 -10
  3. package/package.json +2 -2
package/README.md CHANGED
@@ -1,12 +1,18 @@
1
- # Agent Companion (PWA)
1
+ # Agent Companion
2
2
 
3
- Premium dark **web/PWA** for monitoring Codex + Claude Code sessions from phone.
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
- ## Install CLI
6
- One-off run:
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
- Default start commands:
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
- These will appear in the app automatically.
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
- curl http://localhost:8787/api/launcher/workspaces
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
- ## Notes
396
- - Bridge mode is local-first (`http://localhost:8787` by default).
397
- - The app can still run in mock mode (toggle in Settings).
398
- - Token stats are exact only if your CLI output includes token values; otherwise they remain estimated/last-known.
399
- - Relay defaults to `http://localhost:9797`; set `RELAY_PUBLIC_URL` when hosting remotely.
400
- - Companion persists laptop identity at `~/.agent-companion/companion.json` (override with `AGENT_COMPANION_STATE_FILE`).
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 (!id.startsWith("codex:") && !id.startsWith("claude:")) return true;
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 (!id.startsWith("codex:") && !id.startsWith("claude:")) return true;
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
- setInterval(() => {
2807
- withPersist(() => {
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.0",
3
+ "version": "0.1.2",
4
4
  "private": false,
5
5
  "type": "module",
6
- "description": "Premium dark PWA and CLI companion for monitoring Codex and Claude Code sessions.",
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"