agent-companion 0.1.1 → 0.1.3
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/bridge/directIngest.mjs +4 -0
- package/bridge/server.mjs +243 -21
- package/package.json +1 -6
- package/relay/server.mjs +11 -2
- package/scripts/laptop-companion.mjs +68 -1
package/bridge/directIngest.mjs
CHANGED
|
@@ -290,6 +290,7 @@ function parseCodexFile(file, nowMs) {
|
|
|
290
290
|
agentType: "CODEX",
|
|
291
291
|
title,
|
|
292
292
|
repo,
|
|
293
|
+
workspacePath: cwd || "",
|
|
293
294
|
branch: "main",
|
|
294
295
|
state,
|
|
295
296
|
lastUpdated: effectiveLastUpdated,
|
|
@@ -461,6 +462,7 @@ function parseClaudeFile(file, nowMs) {
|
|
|
461
462
|
agentType: "CLAUDE",
|
|
462
463
|
title,
|
|
463
464
|
repo,
|
|
465
|
+
workspacePath: cwd || "",
|
|
464
466
|
branch,
|
|
465
467
|
state,
|
|
466
468
|
lastUpdated: effectiveLastUpdated,
|
|
@@ -608,6 +610,8 @@ function isNoisePrompt(text) {
|
|
|
608
610
|
if (raw.includes("Filesystem sandboxing defines")) return true;
|
|
609
611
|
if (raw.includes("AGENT_COMPANION_PERSIST_SERVER_HINT_V1")) return true;
|
|
610
612
|
if (raw.includes("[Request interrupted by user for tool use]")) return true;
|
|
613
|
+
if (/^\[background-service\]/i.test(raw.trim())) return true;
|
|
614
|
+
if (/"service"\s*:\s*\{/.test(raw) && /"localhostUrl"\s*:/.test(raw)) return true;
|
|
611
615
|
if (raw === "Answer questions?") return true;
|
|
612
616
|
return false;
|
|
613
617
|
}
|
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";
|
|
@@ -479,6 +484,7 @@ function upsertSessionThread(input) {
|
|
|
479
484
|
function syncSessionThreadFromSession(session, input = {}) {
|
|
480
485
|
if (!session?.id) return null;
|
|
481
486
|
const existingThread = findSessionThread(session.id);
|
|
487
|
+
const inferredIdentity = inferThreadIdentityFromSessionId(session.id);
|
|
482
488
|
const fallbackLookupKey = buildSessionThreadLookupKey({
|
|
483
489
|
agentType: session.agentType,
|
|
484
490
|
workspacePath: input.workspacePath || existingThread?.workspacePath || "",
|
|
@@ -504,10 +510,15 @@ function syncSessionThreadFromSession(session, input = {}) {
|
|
|
504
510
|
input.lastMessageAt,
|
|
505
511
|
safeNumber(existingThread?.lastMessageAt, safeNumber(session.lastUpdated, 0))
|
|
506
512
|
),
|
|
507
|
-
codexThreadId:
|
|
513
|
+
codexThreadId:
|
|
514
|
+
safeTrimmedText(input.codexThreadId, 120) ||
|
|
515
|
+
existingThread?.codexThreadId ||
|
|
516
|
+
inferredIdentity.codexThreadId ||
|
|
517
|
+
null,
|
|
508
518
|
claudeSessionId:
|
|
509
519
|
safeTrimmedText(input.claudeSessionId, 120).toLowerCase() ||
|
|
510
520
|
safeTrimmedText(existingThread?.claudeSessionId, 120).toLowerCase() ||
|
|
521
|
+
inferredIdentity.claudeSessionId ||
|
|
511
522
|
null
|
|
512
523
|
});
|
|
513
524
|
}
|
|
@@ -627,17 +638,214 @@ function extractClaudeSessionIdFromRun(run) {
|
|
|
627
638
|
return "";
|
|
628
639
|
}
|
|
629
640
|
|
|
641
|
+
function normalizeClaudeSessionId(value) {
|
|
642
|
+
const candidate = safeTrimmedText(value, 120).toLowerCase();
|
|
643
|
+
if (!candidate) return "";
|
|
644
|
+
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)) {
|
|
645
|
+
return candidate;
|
|
646
|
+
}
|
|
647
|
+
return "";
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
function inferThreadIdentityFromSessionId(sessionId) {
|
|
651
|
+
const id = safeTrimmedText(sessionId, 160);
|
|
652
|
+
if (!id) {
|
|
653
|
+
return { codexThreadId: null, claudeSessionId: null };
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
if (id.startsWith("codex:")) {
|
|
657
|
+
const codexThreadId = safeTrimmedText(id.slice("codex:".length), 120).toLowerCase();
|
|
658
|
+
return {
|
|
659
|
+
codexThreadId: codexThreadId || null,
|
|
660
|
+
claudeSessionId: null
|
|
661
|
+
};
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
if (id.startsWith("claude:")) {
|
|
665
|
+
const claudeSessionId = normalizeClaudeSessionId(id.slice("claude:".length));
|
|
666
|
+
return {
|
|
667
|
+
codexThreadId: null,
|
|
668
|
+
claudeSessionId: claudeSessionId || null
|
|
669
|
+
};
|
|
670
|
+
}
|
|
671
|
+
|
|
672
|
+
return { codexThreadId: null, claudeSessionId: null };
|
|
673
|
+
}
|
|
674
|
+
|
|
675
|
+
function getLauncherRunUpdatedAt(run) {
|
|
676
|
+
return Math.max(
|
|
677
|
+
safeNumber(run?.endedAt, 0),
|
|
678
|
+
safeNumber(run?.startedAt, 0),
|
|
679
|
+
safeNumber(run?.createdAt, 0)
|
|
680
|
+
);
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
function resolveCanonicalSessionIdForDirectSession(sessionId) {
|
|
684
|
+
const directSessionId = safeTrimmedText(sessionId, 160);
|
|
685
|
+
if (!isDirectSessionId(directSessionId)) return directSessionId;
|
|
686
|
+
|
|
687
|
+
if (directSessionId.startsWith("codex:")) {
|
|
688
|
+
const threadId = directSessionId.slice("codex:".length).trim().toLowerCase();
|
|
689
|
+
if (!threadId) return directSessionId;
|
|
690
|
+
|
|
691
|
+
const mappedRun = [...launcherRuns.values()]
|
|
692
|
+
.slice()
|
|
693
|
+
.sort((a, b) => getLauncherRunUpdatedAt(b) - getLauncherRunUpdatedAt(a))
|
|
694
|
+
.find((run) => !isDirectSessionId(run?.sessionId) && extractCodexThreadIdFromRun(run) === threadId);
|
|
695
|
+
if (mappedRun?.sessionId) return mappedRun.sessionId;
|
|
696
|
+
|
|
697
|
+
const mappedThread = (Array.isArray(state.sessionThreads) ? state.sessionThreads : [])
|
|
698
|
+
.slice()
|
|
699
|
+
.sort((a, b) => safeNumber(b.updatedAt, 0) - safeNumber(a.updatedAt, 0))
|
|
700
|
+
.find(
|
|
701
|
+
(thread) =>
|
|
702
|
+
!isDirectSessionId(thread?.id) &&
|
|
703
|
+
safeTrimmedText(thread?.codexThreadId, 120).toLowerCase() === threadId
|
|
704
|
+
);
|
|
705
|
+
if (mappedThread?.id) return mappedThread.id;
|
|
706
|
+
|
|
707
|
+
return directSessionId;
|
|
708
|
+
}
|
|
709
|
+
|
|
710
|
+
const claudeSessionId = normalizeClaudeSessionId(directSessionId.slice("claude:".length));
|
|
711
|
+
if (!claudeSessionId) return directSessionId;
|
|
712
|
+
|
|
713
|
+
const mappedRun = [...launcherRuns.values()]
|
|
714
|
+
.slice()
|
|
715
|
+
.sort((a, b) => getLauncherRunUpdatedAt(b) - getLauncherRunUpdatedAt(a))
|
|
716
|
+
.find((run) => !isDirectSessionId(run?.sessionId) && extractClaudeSessionIdFromRun(run) === claudeSessionId);
|
|
717
|
+
if (mappedRun?.sessionId) return mappedRun.sessionId;
|
|
718
|
+
|
|
719
|
+
const mappedThread = (Array.isArray(state.sessionThreads) ? state.sessionThreads : [])
|
|
720
|
+
.slice()
|
|
721
|
+
.sort((a, b) => safeNumber(b.updatedAt, 0) - safeNumber(a.updatedAt, 0))
|
|
722
|
+
.find(
|
|
723
|
+
(thread) =>
|
|
724
|
+
!isDirectSessionId(thread?.id) &&
|
|
725
|
+
normalizeClaudeSessionId(thread?.claudeSessionId) === claudeSessionId
|
|
726
|
+
);
|
|
727
|
+
if (mappedThread?.id) return mappedThread.id;
|
|
728
|
+
|
|
729
|
+
return directSessionId;
|
|
730
|
+
}
|
|
731
|
+
|
|
732
|
+
function rewriteDirectScopedId(id, originalSessionId, canonicalSessionId) {
|
|
733
|
+
const text = safeTrimmedText(id, 240);
|
|
734
|
+
if (!text || !originalSessionId || !canonicalSessionId || originalSessionId === canonicalSessionId) {
|
|
735
|
+
return text || id;
|
|
736
|
+
}
|
|
737
|
+
|
|
738
|
+
if (text === `pending:${originalSessionId}`) {
|
|
739
|
+
return `pending:${canonicalSessionId}`;
|
|
740
|
+
}
|
|
741
|
+
if (text === `event:${originalSessionId}`) {
|
|
742
|
+
return `event:${canonicalSessionId}`;
|
|
743
|
+
}
|
|
744
|
+
|
|
745
|
+
const directPrefix = `direct:${originalSessionId}:`;
|
|
746
|
+
if (text.startsWith(directPrefix)) {
|
|
747
|
+
return `direct:${canonicalSessionId}:${text.slice(directPrefix.length)}`;
|
|
748
|
+
}
|
|
749
|
+
|
|
750
|
+
return text;
|
|
751
|
+
}
|
|
752
|
+
|
|
753
|
+
function canonicalizeDirectSnapshot(snapshot) {
|
|
754
|
+
if (!snapshot || typeof snapshot !== "object") return snapshot;
|
|
755
|
+
|
|
756
|
+
const sessionIdMap = new Map();
|
|
757
|
+
const sourceDirectSessionIds = new Set();
|
|
758
|
+
|
|
759
|
+
const remapSessionId = (value) => {
|
|
760
|
+
const sessionId = safeTrimmedText(value, 160);
|
|
761
|
+
if (!isDirectSessionId(sessionId)) return sessionId;
|
|
762
|
+
sourceDirectSessionIds.add(sessionId);
|
|
763
|
+
|
|
764
|
+
const existing = sessionIdMap.get(sessionId);
|
|
765
|
+
if (existing) return existing;
|
|
766
|
+
|
|
767
|
+
const canonical = resolveCanonicalSessionIdForDirectSession(sessionId) || sessionId;
|
|
768
|
+
sessionIdMap.set(sessionId, canonical);
|
|
769
|
+
return canonical;
|
|
770
|
+
};
|
|
771
|
+
|
|
772
|
+
const sessions = (Array.isArray(snapshot.sessions) ? snapshot.sessions : []).map((item) => {
|
|
773
|
+
const originalSessionId = safeTrimmedText(item?.id, 160);
|
|
774
|
+
const canonicalSessionId = remapSessionId(originalSessionId);
|
|
775
|
+
if (!originalSessionId || !canonicalSessionId || canonicalSessionId === originalSessionId) {
|
|
776
|
+
return item;
|
|
777
|
+
}
|
|
778
|
+
return { ...item, id: canonicalSessionId };
|
|
779
|
+
});
|
|
780
|
+
|
|
781
|
+
const pendingInputs = (Array.isArray(snapshot.pendingInputs) ? snapshot.pendingInputs : []).map((item) => {
|
|
782
|
+
const originalSessionId = safeTrimmedText(item?.sessionId, 160);
|
|
783
|
+
const canonicalSessionId = remapSessionId(originalSessionId);
|
|
784
|
+
if (!originalSessionId || !canonicalSessionId || canonicalSessionId === originalSessionId) {
|
|
785
|
+
return item;
|
|
786
|
+
}
|
|
787
|
+
return {
|
|
788
|
+
...item,
|
|
789
|
+
id: rewriteDirectScopedId(item?.id, originalSessionId, canonicalSessionId),
|
|
790
|
+
sessionId: canonicalSessionId
|
|
791
|
+
};
|
|
792
|
+
});
|
|
793
|
+
|
|
794
|
+
const events = (Array.isArray(snapshot.events) ? snapshot.events : []).map((item) => {
|
|
795
|
+
const originalSessionId = safeTrimmedText(item?.sessionId, 160);
|
|
796
|
+
const canonicalSessionId = remapSessionId(originalSessionId);
|
|
797
|
+
if (!originalSessionId || !canonicalSessionId || canonicalSessionId === originalSessionId) {
|
|
798
|
+
return item;
|
|
799
|
+
}
|
|
800
|
+
return {
|
|
801
|
+
...item,
|
|
802
|
+
id: rewriteDirectScopedId(item?.id, originalSessionId, canonicalSessionId),
|
|
803
|
+
sessionId: canonicalSessionId
|
|
804
|
+
};
|
|
805
|
+
});
|
|
806
|
+
|
|
807
|
+
const chatTurns = (Array.isArray(snapshot.chatTurns) ? snapshot.chatTurns : []).map((item) => {
|
|
808
|
+
const originalSessionId = safeTrimmedText(item?.sessionId, 160);
|
|
809
|
+
const canonicalSessionId = remapSessionId(originalSessionId);
|
|
810
|
+
if (!originalSessionId || !canonicalSessionId || canonicalSessionId === originalSessionId) {
|
|
811
|
+
return item;
|
|
812
|
+
}
|
|
813
|
+
return {
|
|
814
|
+
...item,
|
|
815
|
+
id: rewriteDirectScopedId(item?.id, originalSessionId, canonicalSessionId),
|
|
816
|
+
sessionId: canonicalSessionId
|
|
817
|
+
};
|
|
818
|
+
});
|
|
819
|
+
|
|
820
|
+
return {
|
|
821
|
+
...snapshot,
|
|
822
|
+
sessions,
|
|
823
|
+
pendingInputs,
|
|
824
|
+
events,
|
|
825
|
+
chatTurns,
|
|
826
|
+
directSourceSessionIds: [...sourceDirectSessionIds]
|
|
827
|
+
};
|
|
828
|
+
}
|
|
829
|
+
|
|
630
830
|
function buildContinueCommandFromThread(thread, prompt) {
|
|
631
831
|
const safePrompt = safeTrimmedText(prompt, 1500);
|
|
632
832
|
if (!safePrompt || !thread) return null;
|
|
633
833
|
|
|
634
834
|
if (thread.agentType === "CODEX") {
|
|
635
|
-
const threadId =
|
|
835
|
+
const threadId =
|
|
836
|
+
safeTrimmedText(thread.codexThreadId, 120).toLowerCase() ||
|
|
837
|
+
(safeTrimmedText(thread.id, 160).startsWith("codex:")
|
|
838
|
+
? safeTrimmedText(thread.id.slice("codex:".length), 120).toLowerCase()
|
|
839
|
+
: "");
|
|
636
840
|
if (!threadId) return null;
|
|
637
841
|
return ["codex", "exec", "resume", threadId, safePrompt];
|
|
638
842
|
}
|
|
639
843
|
|
|
640
|
-
const claudeSessionId =
|
|
844
|
+
const claudeSessionId =
|
|
845
|
+
safeTrimmedText(thread.claudeSessionId, 120).toLowerCase() ||
|
|
846
|
+
(safeTrimmedText(thread.id, 160).startsWith("claude:")
|
|
847
|
+
? normalizeClaudeSessionId(thread.id.slice("claude:".length))
|
|
848
|
+
: "");
|
|
641
849
|
if (!claudeSessionId) return null;
|
|
642
850
|
return ["claude", "--resume", claudeSessionId, "-p", safePrompt, "--output-format", "stream-json"];
|
|
643
851
|
}
|
|
@@ -655,14 +863,12 @@ function resolveContinueCommandForSession(sessionId, prompt) {
|
|
|
655
863
|
}
|
|
656
864
|
|
|
657
865
|
function validateSessionReuse(input) {
|
|
658
|
-
const
|
|
659
|
-
if (!
|
|
866
|
+
const requestedSessionId = safeTrimmedText(input?.sessionId, 160);
|
|
867
|
+
if (!requestedSessionId) {
|
|
660
868
|
return { ok: true, sessionId: "", continueCommand: null };
|
|
661
869
|
}
|
|
662
870
|
|
|
663
|
-
|
|
664
|
-
return { ok: false, error: "direct local sessions cannot be reused from launcher" };
|
|
665
|
-
}
|
|
871
|
+
const sessionId = resolveCanonicalSessionIdForDirectSession(requestedSessionId) || requestedSessionId;
|
|
666
872
|
|
|
667
873
|
const session = findSession(sessionId);
|
|
668
874
|
const thread = findSessionThread(sessionId);
|
|
@@ -729,9 +935,11 @@ function writeToRunStdin(run, message) {
|
|
|
729
935
|
|
|
730
936
|
function buildEphemeralSessionThread(session) {
|
|
731
937
|
if (!session?.id) return null;
|
|
938
|
+
const workspacePath = safeTrimmedText(session?.workspacePath, 500) || "";
|
|
939
|
+
const inferredIdentity = inferThreadIdentityFromSessionId(session.id);
|
|
732
940
|
const lookupKey = buildSessionThreadLookupKey({
|
|
733
941
|
agentType: session.agentType,
|
|
734
|
-
workspacePath
|
|
942
|
+
workspacePath,
|
|
735
943
|
repo: session.repo,
|
|
736
944
|
title: session.title
|
|
737
945
|
});
|
|
@@ -740,7 +948,7 @@ function buildEphemeralSessionThread(session) {
|
|
|
740
948
|
key: lookupKey,
|
|
741
949
|
lookupKey,
|
|
742
950
|
agentType: session.agentType,
|
|
743
|
-
workspacePath
|
|
951
|
+
workspacePath,
|
|
744
952
|
repo: session.repo,
|
|
745
953
|
branch: session.branch,
|
|
746
954
|
title: session.title,
|
|
@@ -749,7 +957,9 @@ function buildEphemeralSessionThread(session) {
|
|
|
749
957
|
updatedAt: safeNumber(session.lastUpdated, Date.now()),
|
|
750
958
|
lastRunId: null,
|
|
751
959
|
runCount: 0,
|
|
752
|
-
lastMessageAt: safeNumber(session.lastUpdated, 0)
|
|
960
|
+
lastMessageAt: safeNumber(session.lastUpdated, 0),
|
|
961
|
+
codexThreadId: inferredIdentity.codexThreadId,
|
|
962
|
+
claudeSessionId: inferredIdentity.claudeSessionId
|
|
753
963
|
};
|
|
754
964
|
}
|
|
755
965
|
|
|
@@ -1937,21 +2147,28 @@ function stopManagedService(service, signalInput) {
|
|
|
1937
2147
|
function mergeDirectSnapshot(snapshot) {
|
|
1938
2148
|
if (!snapshot || typeof snapshot !== "object") return;
|
|
1939
2149
|
|
|
2150
|
+
snapshot = canonicalizeDirectSnapshot(snapshot);
|
|
2151
|
+
|
|
1940
2152
|
const incomingDirectSessionIds = new Set(
|
|
1941
2153
|
(Array.isArray(snapshot.sessions) ? snapshot.sessions : [])
|
|
1942
2154
|
.map((item) => item?.id)
|
|
1943
|
-
.filter((id) => typeof id === "string")
|
|
2155
|
+
.filter((id) => typeof id === "string" && isDirectSessionId(id))
|
|
2156
|
+
);
|
|
2157
|
+
const incomingDirectSourceSessionIds = new Set(
|
|
2158
|
+
(Array.isArray(snapshot.directSourceSessionIds) ? snapshot.directSourceSessionIds : [])
|
|
2159
|
+
.map((item) => safeTrimmedText(item, 160))
|
|
2160
|
+
.filter(Boolean)
|
|
1944
2161
|
);
|
|
1945
2162
|
|
|
1946
|
-
if (incomingDirectSessionIds.size > 0) {
|
|
2163
|
+
if (incomingDirectSessionIds.size > 0 || incomingDirectSourceSessionIds.size > 0) {
|
|
1947
2164
|
state.sessions = state.sessions.filter((item) => {
|
|
1948
2165
|
const id = String(item?.id || "");
|
|
1949
|
-
if (!
|
|
2166
|
+
if (!isDirectSessionId(id)) return true;
|
|
1950
2167
|
return incomingDirectSessionIds.has(id);
|
|
1951
2168
|
});
|
|
1952
2169
|
state.sessionThreads = (Array.isArray(state.sessionThreads) ? state.sessionThreads : []).filter((item) => {
|
|
1953
2170
|
const id = String(item?.id || "");
|
|
1954
|
-
if (!
|
|
2171
|
+
if (!isDirectSessionId(id)) return true;
|
|
1955
2172
|
return incomingDirectSessionIds.has(id);
|
|
1956
2173
|
});
|
|
1957
2174
|
}
|
|
@@ -1963,11 +2180,13 @@ function mergeDirectSnapshot(snapshot) {
|
|
|
1963
2180
|
if (previous) {
|
|
1964
2181
|
Object.assign(previous, incoming);
|
|
1965
2182
|
syncSessionThreadFromSession(previous, {
|
|
2183
|
+
workspacePath: safeTrimmedText(previous?.workspacePath, 500),
|
|
1966
2184
|
updatedAt: safeNumber(previous.lastUpdated, Date.now())
|
|
1967
2185
|
});
|
|
1968
2186
|
} else {
|
|
1969
2187
|
state.sessions.push(incoming);
|
|
1970
2188
|
syncSessionThreadFromSession(incoming, {
|
|
2189
|
+
workspacePath: safeTrimmedText(incoming?.workspacePath, 500),
|
|
1971
2190
|
updatedAt: safeNumber(incoming.lastUpdated, Date.now())
|
|
1972
2191
|
});
|
|
1973
2192
|
}
|
|
@@ -2802,11 +3021,14 @@ app.post("/api/import/snapshot", (req, res) => {
|
|
|
2802
3021
|
return res.json({ ok: true });
|
|
2803
3022
|
});
|
|
2804
3023
|
|
|
3024
|
+
function refreshDirectSnapshot() {
|
|
3025
|
+
withPersist(() => {
|
|
3026
|
+
const snapshot = collectDirectSnapshot();
|
|
3027
|
+
mergeDirectSnapshot(snapshot);
|
|
3028
|
+
});
|
|
3029
|
+
}
|
|
3030
|
+
|
|
2805
3031
|
app.listen(PORT, () => {
|
|
2806
|
-
|
|
2807
|
-
|
|
2808
|
-
const snapshot = collectDirectSnapshot();
|
|
2809
|
-
mergeDirectSnapshot(snapshot);
|
|
2810
|
-
});
|
|
2811
|
-
}, DIRECT_SNAPSHOT_POLL_INTERVAL_MS);
|
|
3032
|
+
refreshDirectSnapshot();
|
|
3033
|
+
setInterval(refreshDirectSnapshot, DIRECT_SNAPSHOT_POLL_INTERVAL_MS);
|
|
2812
3034
|
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "agent-companion",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.3",
|
|
4
4
|
"private": false,
|
|
5
5
|
"type": "module",
|
|
6
6
|
"description": "Phone-to-computer companion for Codex and Claude Code.",
|
|
@@ -47,8 +47,6 @@
|
|
|
47
47
|
"build": "tsc -b && vite build",
|
|
48
48
|
"preview": "vite preview",
|
|
49
49
|
"wake-proxy": "node wake-proxy/server.mjs",
|
|
50
|
-
"video:studio": "remotion studio video/index.ts",
|
|
51
|
-
"video:render": "remotion render video/index.ts AgentCompanionLaunch video-renders/agent-companion-launch.mp4",
|
|
52
50
|
"prepublishOnly": "npm run build"
|
|
53
51
|
},
|
|
54
52
|
"bin": {
|
|
@@ -67,7 +65,6 @@
|
|
|
67
65
|
"ws": "^8.18.3"
|
|
68
66
|
},
|
|
69
67
|
"devDependencies": {
|
|
70
|
-
"@remotion/cli": "^4.0.431",
|
|
71
68
|
"@types/node": "^25.2.3",
|
|
72
69
|
"@types/react": "^19.2.14",
|
|
73
70
|
"@types/react-dom": "^19.2.3",
|
|
@@ -75,10 +72,8 @@
|
|
|
75
72
|
"@vitejs/plugin-react": "^5.1.0",
|
|
76
73
|
"autoprefixer": "^10.4.24",
|
|
77
74
|
"postcss": "^8.5.6",
|
|
78
|
-
"remotion": "^4.0.431",
|
|
79
75
|
"tailwindcss": "^3.4.17",
|
|
80
76
|
"tailwindcss-animate": "^1.0.7",
|
|
81
|
-
"tslib": "^2.8.1",
|
|
82
77
|
"typescript": "^5.9.3",
|
|
83
78
|
"vite": "^7.3.1",
|
|
84
79
|
"vite-plugin-pwa": "^1.2.0"
|
package/relay/server.mjs
CHANGED
|
@@ -23,6 +23,7 @@ const RELAY_WAKE_TIMEOUT_MS = clamp(toInt(process.env.RELAY_WAKE_TIMEOUT_MS, 90_
|
|
|
23
23
|
const RELAY_WAKE_POLL_INTERVAL_MS = clamp(toInt(process.env.RELAY_WAKE_POLL_INTERVAL_MS, 1200), 250, 10_000);
|
|
24
24
|
const RELAY_WAKE_REQUEST_TIMEOUT_MS = clamp(toInt(process.env.RELAY_WAKE_REQUEST_TIMEOUT_MS, 6000), 1000, 60_000);
|
|
25
25
|
const PAIRING_TTL_MS = clamp(toInt(process.env.PAIRING_TTL_MS, 10 * 60 * 1000), 30_000, 24 * 60 * 60 * 1000);
|
|
26
|
+
const PAIR_CODE_LENGTH = clamp(toInt(process.env.PAIR_CODE_LENGTH, 10), 8, 16);
|
|
26
27
|
const RPC_TIMEOUT_MS = clamp(toInt(process.env.RELAY_RPC_TIMEOUT_MS, 15_000), 500, 60_000);
|
|
27
28
|
const PREVIEW_DEFAULT_TTL_MS = clamp(toInt(process.env.RELAY_PREVIEW_TTL_MS, 2 * 60 * 60 * 1000), 60_000, 7 * 24 * 60 * 60 * 1000);
|
|
28
29
|
const PREVIEW_MAX_TTL_MS = clamp(
|
|
@@ -396,6 +397,14 @@ app.get("/api/devices/:id/launcher/runs", async (req, res) => {
|
|
|
396
397
|
});
|
|
397
398
|
});
|
|
398
399
|
|
|
400
|
+
app.get("/api/devices/:id/launcher/runs/:runId", async (req, res) => {
|
|
401
|
+
const runId = encodeURIComponent(String(req.params.runId || ""));
|
|
402
|
+
return proxyToLaptopBridge(req, res, {
|
|
403
|
+
method: "GET",
|
|
404
|
+
path: `/api/launcher/runs/${runId}`
|
|
405
|
+
});
|
|
406
|
+
});
|
|
407
|
+
|
|
399
408
|
app.get("/api/devices/:id/launcher/services", async (req, res) => {
|
|
400
409
|
const pathWithQuery = withQuery("/api/launcher/services", req.query);
|
|
401
410
|
return proxyToLaptopBridge(req, res, {
|
|
@@ -1368,7 +1377,7 @@ function createPairingForLaptop(laptop) {
|
|
|
1368
1377
|
}
|
|
1369
1378
|
|
|
1370
1379
|
if (!code) {
|
|
1371
|
-
code =
|
|
1380
|
+
code = generatePairCode();
|
|
1372
1381
|
}
|
|
1373
1382
|
|
|
1374
1383
|
const now = Date.now();
|
|
@@ -1789,7 +1798,7 @@ function parseSignedToken(token, expectedPrefix) {
|
|
|
1789
1798
|
function generatePairCode() {
|
|
1790
1799
|
const alphabet = "23456789ABCDEFGHJKMNPQRSTVWXYZ";
|
|
1791
1800
|
let out = "";
|
|
1792
|
-
for (let i = 0; i <
|
|
1801
|
+
for (let i = 0; i < PAIR_CODE_LENGTH; i += 1) {
|
|
1793
1802
|
out += alphabet[Math.floor(Math.random() * alphabet.length)];
|
|
1794
1803
|
}
|
|
1795
1804
|
return out;
|
|
@@ -43,6 +43,8 @@ let reconnectDelayMs = 1200;
|
|
|
43
43
|
|
|
44
44
|
let bridgeChild = null;
|
|
45
45
|
let bridgeStartedByCompanion = false;
|
|
46
|
+
let pairingWatchTimer = null;
|
|
47
|
+
let pairingNoticeShown = false;
|
|
46
48
|
|
|
47
49
|
process.on("SIGINT", () => shutdown("SIGINT"));
|
|
48
50
|
process.on("SIGTERM", () => shutdown("SIGTERM"));
|
|
@@ -63,6 +65,7 @@ async function main() {
|
|
|
63
65
|
|
|
64
66
|
let registration = await getOrCreateLaptopRegistration();
|
|
65
67
|
printPairingInfo(registration);
|
|
68
|
+
startPairingWatch(registration.laptopToken);
|
|
66
69
|
|
|
67
70
|
while (!shuttingDown) {
|
|
68
71
|
const cached = loadCompanionState();
|
|
@@ -84,6 +87,7 @@ async function main() {
|
|
|
84
87
|
} else if (connectionResult?.authRejected) {
|
|
85
88
|
registration = await getOrCreateLaptopRegistration();
|
|
86
89
|
printPairingInfo(registration);
|
|
90
|
+
startPairingWatch(registration.laptopToken);
|
|
87
91
|
}
|
|
88
92
|
|
|
89
93
|
const waitMs = reconnectDelayMs;
|
|
@@ -275,10 +279,12 @@ async function waitForRelayAvailability() {
|
|
|
275
279
|
}
|
|
276
280
|
|
|
277
281
|
function printPairingInfo(registration) {
|
|
282
|
+
pairingNoticeShown = false;
|
|
278
283
|
console.log("");
|
|
279
284
|
console.log("Agent Companion computer is ready.");
|
|
280
285
|
console.log(`Pairing code: ${registration.pairCode}`);
|
|
281
|
-
console.log("
|
|
286
|
+
console.log(formatCliWarning("Open the app and enter this code. Don't share it."));
|
|
287
|
+
console.log("Keep Agent Companion running after pairing.");
|
|
282
288
|
if (!quietLogs) {
|
|
283
289
|
if (wakeMacAddress) {
|
|
284
290
|
console.log(`Auto-wake MAC: ${wakeMacAddress}`);
|
|
@@ -292,6 +298,58 @@ function printPairingInfo(registration) {
|
|
|
292
298
|
console.log("");
|
|
293
299
|
}
|
|
294
300
|
|
|
301
|
+
function startPairingWatch(laptopToken) {
|
|
302
|
+
stopPairingWatch();
|
|
303
|
+
|
|
304
|
+
const token = safeText(laptopToken, 1000);
|
|
305
|
+
if (!token || pairingNoticeShown) return;
|
|
306
|
+
|
|
307
|
+
const poll = async () => {
|
|
308
|
+
if (shuttingDown || pairingNoticeShown) return;
|
|
309
|
+
|
|
310
|
+
try {
|
|
311
|
+
const response = await fetchWithTimeout(
|
|
312
|
+
`${relayBaseUrl}/api/laptops/me`,
|
|
313
|
+
{
|
|
314
|
+
method: "GET",
|
|
315
|
+
headers: {
|
|
316
|
+
Accept: "application/json",
|
|
317
|
+
Authorization: `Bearer ${token}`
|
|
318
|
+
}
|
|
319
|
+
},
|
|
320
|
+
5000
|
|
321
|
+
);
|
|
322
|
+
|
|
323
|
+
if (response.ok) {
|
|
324
|
+
const body = await safeParseJson(response);
|
|
325
|
+
if (body?.pairedAt) {
|
|
326
|
+
pairingNoticeShown = true;
|
|
327
|
+
console.log("");
|
|
328
|
+
console.log("Device paired. Keep Agent Companion running.");
|
|
329
|
+
console.log("");
|
|
330
|
+
stopPairingWatch();
|
|
331
|
+
return;
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
} catch {
|
|
335
|
+
// ignore transient relay issues while waiting for pairing
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
pairingWatchTimer = setTimeout(() => {
|
|
339
|
+
void poll();
|
|
340
|
+
}, 3000);
|
|
341
|
+
};
|
|
342
|
+
|
|
343
|
+
void poll();
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
function stopPairingWatch() {
|
|
347
|
+
if (pairingWatchTimer) {
|
|
348
|
+
clearTimeout(pairingWatchTimer);
|
|
349
|
+
pairingWatchTimer = null;
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
|
|
295
353
|
function connectWebSocketOnce(laptopToken) {
|
|
296
354
|
const wsUrl = buildLaptopWsUrl(relayBaseUrl, laptopToken);
|
|
297
355
|
|
|
@@ -330,6 +388,9 @@ function connectWebSocketOnce(laptopToken) {
|
|
|
330
388
|
pairingExpiresAt: cached?.pairingExpiresAt || null,
|
|
331
389
|
pairingUrl: cached?.pairingUrl || null
|
|
332
390
|
});
|
|
391
|
+
if (!pairingNoticeShown) {
|
|
392
|
+
startPairingWatch(safeText(message.laptopToken, 1000));
|
|
393
|
+
}
|
|
333
394
|
}
|
|
334
395
|
if (message.type === "rpc_request" && typeof message.id === "string") {
|
|
335
396
|
void handleRpcRequest(socket, message);
|
|
@@ -1079,6 +1140,7 @@ async function shutdown(signal) {
|
|
|
1079
1140
|
console.log(`[companion] received ${signal}, shutting down`);
|
|
1080
1141
|
}
|
|
1081
1142
|
stopSnapshotLoop();
|
|
1143
|
+
stopPairingWatch();
|
|
1082
1144
|
|
|
1083
1145
|
if (websocket && websocket.readyState === WebSocket.OPEN) {
|
|
1084
1146
|
websocket.close(1000, "shutdown");
|
|
@@ -1101,6 +1163,11 @@ function sleep(ms) {
|
|
|
1101
1163
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
1102
1164
|
}
|
|
1103
1165
|
|
|
1166
|
+
function formatCliWarning(text) {
|
|
1167
|
+
if (!process.stdout.isTTY) return text;
|
|
1168
|
+
return `\u001b[31m${text}\u001b[0m`;
|
|
1169
|
+
}
|
|
1170
|
+
|
|
1104
1171
|
function isObject(value) {
|
|
1105
1172
|
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
1106
1173
|
}
|