agent-companion 0.1.4 → 0.1.5
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/server.mjs +22 -6
- package/package.json +2 -1
- package/relay/server.mjs +302 -0
package/bridge/server.mjs
CHANGED
|
@@ -1244,12 +1244,8 @@ function createWorkspaceFolder(input) {
|
|
|
1244
1244
|
return { statusCode: 500, error: String(error?.message || error) };
|
|
1245
1245
|
}
|
|
1246
1246
|
|
|
1247
|
-
const normalizedPath = normalizeExistingDirectoryPath(targetPath);
|
|
1248
|
-
|
|
1249
|
-
return { statusCode: 500, error: "workspace path could not be resolved" };
|
|
1250
|
-
}
|
|
1251
|
-
|
|
1252
|
-
const workspace = describeWorkspaceCandidate(normalizedPath);
|
|
1247
|
+
const normalizedPath = normalizeExistingDirectoryPath(targetPath) || path.resolve(targetPath);
|
|
1248
|
+
const workspace = describeWorkspaceCandidate(normalizedPath) || buildWorkspaceFallback(normalizedPath);
|
|
1253
1249
|
if (!workspace) {
|
|
1254
1250
|
return { statusCode: 500, error: "workspace could not be indexed" };
|
|
1255
1251
|
}
|
|
@@ -1262,6 +1258,26 @@ function createWorkspaceFolder(input) {
|
|
|
1262
1258
|
};
|
|
1263
1259
|
}
|
|
1264
1260
|
|
|
1261
|
+
function buildWorkspaceFallback(workspacePath) {
|
|
1262
|
+
const normalized = safeTrimmedText(workspacePath, 2000);
|
|
1263
|
+
if (!normalized) return null;
|
|
1264
|
+
|
|
1265
|
+
try {
|
|
1266
|
+
const stat = fs.statSync(normalized);
|
|
1267
|
+
if (!stat.isDirectory()) return null;
|
|
1268
|
+
|
|
1269
|
+
return {
|
|
1270
|
+
path: normalized,
|
|
1271
|
+
name: path.basename(normalized),
|
|
1272
|
+
hasGit: fs.existsSync(path.join(normalized, ".git")),
|
|
1273
|
+
score: 0,
|
|
1274
|
+
lastModified: stat.mtimeMs || Date.now()
|
|
1275
|
+
};
|
|
1276
|
+
} catch {
|
|
1277
|
+
return null;
|
|
1278
|
+
}
|
|
1279
|
+
}
|
|
1280
|
+
|
|
1265
1281
|
function detectWorkspaceMeta(workspacePath) {
|
|
1266
1282
|
const repo = path.basename(workspacePath);
|
|
1267
1283
|
const branch = detectGitBranch(workspacePath);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "agent-companion",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.5",
|
|
4
4
|
"private": false,
|
|
5
5
|
"type": "module",
|
|
6
6
|
"description": "Phone-to-computer companion for Codex and Claude Code.",
|
|
@@ -54,6 +54,7 @@
|
|
|
54
54
|
},
|
|
55
55
|
"dependencies": {
|
|
56
56
|
"@radix-ui/react-switch": "^1.2.6",
|
|
57
|
+
"@vercel/analytics": "^1.6.1",
|
|
57
58
|
"class-variance-authority": "^0.7.1",
|
|
58
59
|
"clsx": "^2.1.1",
|
|
59
60
|
"cors": "^2.8.6",
|
package/relay/server.mjs
CHANGED
|
@@ -17,6 +17,7 @@ const RELAY_PUBLIC_URL = trimTrailingSlash(
|
|
|
17
17
|
process.env.RELAY_PUBLIC_URL || process.env.RENDER_EXTERNAL_URL || `http://localhost:${RELAY_PORT}`
|
|
18
18
|
);
|
|
19
19
|
const RELAY_TOKEN_SECRET = resolveRelayTokenSecret();
|
|
20
|
+
const RELAY_ADMIN_TOKEN = safeText(process.env.RELAY_ADMIN_TOKEN || process.env.ADMIN_TOKEN || "", 500);
|
|
20
21
|
const RELAY_WAKE_PROXY_URL = trimTrailingSlash(process.env.RELAY_WAKE_PROXY_URL || process.env.WAKE_PROXY_URL || "");
|
|
21
22
|
const RELAY_WAKE_PROXY_TOKEN = safeText(process.env.RELAY_WAKE_PROXY_TOKEN || process.env.WAKE_PROXY_TOKEN || "", 500);
|
|
22
23
|
const RELAY_WAKE_TIMEOUT_MS = clamp(toInt(process.env.RELAY_WAKE_TIMEOUT_MS, 90_000), 5_000, 5 * 60 * 1000);
|
|
@@ -82,6 +83,12 @@ app.get("/health", (_req, res) => {
|
|
|
82
83
|
});
|
|
83
84
|
});
|
|
84
85
|
|
|
86
|
+
app.get("/api/admin/analytics", requireAdminToken, (_req, res) => {
|
|
87
|
+
cleanupExpiredPairings();
|
|
88
|
+
cleanupExpiredPreviews();
|
|
89
|
+
res.json(buildAdminAnalytics());
|
|
90
|
+
});
|
|
91
|
+
|
|
85
92
|
app.get("/pair", (req, res) => {
|
|
86
93
|
const code = normalizePairCode(req.query?.code);
|
|
87
94
|
const pairing = code ? findPairingByCode(code) : null;
|
|
@@ -1870,6 +1877,301 @@ function loadState() {
|
|
|
1870
1877
|
}
|
|
1871
1878
|
}
|
|
1872
1879
|
|
|
1880
|
+
function requireAdminToken(req, res, next) {
|
|
1881
|
+
if (!RELAY_ADMIN_TOKEN) {
|
|
1882
|
+
return res.status(503).json({
|
|
1883
|
+
ok: false,
|
|
1884
|
+
error: "admin analytics not configured"
|
|
1885
|
+
});
|
|
1886
|
+
}
|
|
1887
|
+
|
|
1888
|
+
const incoming = extractAdminToken(req);
|
|
1889
|
+
if (!incoming || !secureTokenMatch(incoming, RELAY_ADMIN_TOKEN)) {
|
|
1890
|
+
return res.status(401).json({
|
|
1891
|
+
ok: false,
|
|
1892
|
+
error: "unauthorized"
|
|
1893
|
+
});
|
|
1894
|
+
}
|
|
1895
|
+
|
|
1896
|
+
next();
|
|
1897
|
+
}
|
|
1898
|
+
|
|
1899
|
+
function extractAdminToken(req) {
|
|
1900
|
+
const authHeader = safeText(req.headers?.authorization || "", 4000);
|
|
1901
|
+
if (authHeader.toLowerCase().startsWith("bearer ")) {
|
|
1902
|
+
return safeText(authHeader.slice(7), 4000);
|
|
1903
|
+
}
|
|
1904
|
+
|
|
1905
|
+
const headerToken = safeText(req.headers?.["x-admin-token"] || "", 4000);
|
|
1906
|
+
if (headerToken) return headerToken;
|
|
1907
|
+
|
|
1908
|
+
return safeText(req.query?.token || "", 4000);
|
|
1909
|
+
}
|
|
1910
|
+
|
|
1911
|
+
function secureTokenMatch(left, right) {
|
|
1912
|
+
const actual = Buffer.from(String(left || ""), "utf8");
|
|
1913
|
+
const expected = Buffer.from(String(right || ""), "utf8");
|
|
1914
|
+
if (actual.length !== expected.length || actual.length === 0) return false;
|
|
1915
|
+
return timingSafeEqual(actual, expected);
|
|
1916
|
+
}
|
|
1917
|
+
|
|
1918
|
+
function buildAdminAnalytics() {
|
|
1919
|
+
const now = Date.now();
|
|
1920
|
+
const onlineLaptopIds = new Set(
|
|
1921
|
+
[...laptopSockets.entries()]
|
|
1922
|
+
.filter(([, ws]) => ws && ws.readyState === WebSocket.OPEN)
|
|
1923
|
+
.map(([laptopId]) => laptopId)
|
|
1924
|
+
);
|
|
1925
|
+
const claimedPairings = state.pairings.filter((item) => item.claimedAt);
|
|
1926
|
+
const activePreviews = state.previews.filter((item) => item.expiresAt > now);
|
|
1927
|
+
const snapshotRollup = collectSnapshotAnalytics(state.laptops, onlineLaptopIds, now);
|
|
1928
|
+
const uniquePairedDeviceIds = new Set(
|
|
1929
|
+
state.laptops.map((item) => safeText(item.deviceId, 200)).filter(Boolean)
|
|
1930
|
+
);
|
|
1931
|
+
|
|
1932
|
+
return {
|
|
1933
|
+
ok: true,
|
|
1934
|
+
generatedAt: now,
|
|
1935
|
+
summary: {
|
|
1936
|
+
totalUsers: uniquePairedDeviceIds.size,
|
|
1937
|
+
totalPairs: claimedPairings.length,
|
|
1938
|
+
newUsers24h: countUniqueRecentPairings(claimedPairings, now, 24 * 60 * 60 * 1000),
|
|
1939
|
+
newUsers7d: countUniqueRecentPairings(claimedPairings, now, 7 * 24 * 60 * 60 * 1000),
|
|
1940
|
+
onlineDevices: onlineLaptopIds.size,
|
|
1941
|
+
activeSessions: snapshotRollup.summary.activeSessions,
|
|
1942
|
+
appSessionsTotal: snapshotRollup.summary.appSessionsTotal,
|
|
1943
|
+
appRunsTotal: snapshotRollup.summary.appRunsTotal,
|
|
1944
|
+
appRuns24h: snapshotRollup.summary.appRuns24h,
|
|
1945
|
+
appRuns7d: snapshotRollup.summary.appRuns7d
|
|
1946
|
+
},
|
|
1947
|
+
agentUsage: snapshotRollup.agentUsage,
|
|
1948
|
+
sessionStates: snapshotRollup.sessionStates,
|
|
1949
|
+
runStates: snapshotRollup.runStates,
|
|
1950
|
+
daily: buildDailyActivitySeries({
|
|
1951
|
+
pairings: claimedPairings.map((item) => item.claimedAt),
|
|
1952
|
+
runs: snapshotRollup.dailyRuns
|
|
1953
|
+
}),
|
|
1954
|
+
devices: snapshotRollup.devices
|
|
1955
|
+
};
|
|
1956
|
+
}
|
|
1957
|
+
|
|
1958
|
+
function collectSnapshotAnalytics(laptops, onlineLaptopIds, now) {
|
|
1959
|
+
const sessionStates = {
|
|
1960
|
+
RUNNING: 0,
|
|
1961
|
+
WAITING_INPUT: 0,
|
|
1962
|
+
COMPLETED: 0,
|
|
1963
|
+
FAILED: 0,
|
|
1964
|
+
CANCELLED: 0
|
|
1965
|
+
};
|
|
1966
|
+
const runStates = {
|
|
1967
|
+
STARTING: 0,
|
|
1968
|
+
RUNNING: 0,
|
|
1969
|
+
COMPLETED: 0,
|
|
1970
|
+
FAILED: 0,
|
|
1971
|
+
STOPPED: 0
|
|
1972
|
+
};
|
|
1973
|
+
const agentUsage = {
|
|
1974
|
+
codexRuns: 0,
|
|
1975
|
+
claudeRuns: 0,
|
|
1976
|
+
codexSessionsFromApp: 0,
|
|
1977
|
+
claudeSessionsFromApp: 0
|
|
1978
|
+
};
|
|
1979
|
+
const dailyRuns = [];
|
|
1980
|
+
|
|
1981
|
+
const devices = laptops
|
|
1982
|
+
.map((laptop) => {
|
|
1983
|
+
const snapshot = isObject(laptop.latestSnapshot) ? laptop.latestSnapshot : {};
|
|
1984
|
+
const runs = Array.isArray(snapshot.runs) ? snapshot.runs.filter((item) => isObject(item)) : [];
|
|
1985
|
+
const sessionSummaries = Array.isArray(snapshot.sessionSummaries)
|
|
1986
|
+
? snapshot.sessionSummaries.filter((item) => isObject(item))
|
|
1987
|
+
: [];
|
|
1988
|
+
const sessions = Array.isArray(snapshot.sessions) ? snapshot.sessions.filter((item) => isObject(item)) : [];
|
|
1989
|
+
const chatTurns = Array.isArray(snapshot.chatTurns) ? snapshot.chatTurns.filter((item) => isObject(item)) : [];
|
|
1990
|
+
const sessionList = sessionSummaries.length > 0 ? sessionSummaries : sessions;
|
|
1991
|
+
const sessionLookup = new Map();
|
|
1992
|
+
for (const session of sessionList) {
|
|
1993
|
+
const sessionId = safeText(session.id || session?.session?.id, 200);
|
|
1994
|
+
if (sessionId) {
|
|
1995
|
+
sessionLookup.set(sessionId, session);
|
|
1996
|
+
}
|
|
1997
|
+
}
|
|
1998
|
+
const appSessionIds = new Set();
|
|
1999
|
+
|
|
2000
|
+
for (const run of runs) {
|
|
2001
|
+
const status = safeText(run.status, 40).toUpperCase();
|
|
2002
|
+
if (Object.prototype.hasOwnProperty.call(runStates, status)) {
|
|
2003
|
+
runStates[status] += 1;
|
|
2004
|
+
}
|
|
2005
|
+
|
|
2006
|
+
const agentType = safeText(run.agentType, 20).toUpperCase();
|
|
2007
|
+
if (agentType === "CODEX") agentUsage.codexRuns += 1;
|
|
2008
|
+
if (agentType === "CLAUDE") agentUsage.claudeRuns += 1;
|
|
2009
|
+
|
|
2010
|
+
const sessionId = safeText(run.sessionId, 200);
|
|
2011
|
+
if (sessionId) appSessionIds.add(sessionId);
|
|
2012
|
+
|
|
2013
|
+
const createdAt = toInt(run.createdAt, 0);
|
|
2014
|
+
if (createdAt) {
|
|
2015
|
+
dailyRuns.push(createdAt);
|
|
2016
|
+
}
|
|
2017
|
+
}
|
|
2018
|
+
|
|
2019
|
+
for (const turn of chatTurns) {
|
|
2020
|
+
const sessionId = safeText(turn.sessionId, 200);
|
|
2021
|
+
const role = safeText(turn.role, 20).toUpperCase();
|
|
2022
|
+
const source = safeText(turn.source, 40).toUpperCase();
|
|
2023
|
+
if (!sessionId || role !== "USER") continue;
|
|
2024
|
+
if (source === "MESSAGE_API" || source === "LAUNCH") {
|
|
2025
|
+
appSessionIds.add(sessionId);
|
|
2026
|
+
}
|
|
2027
|
+
}
|
|
2028
|
+
|
|
2029
|
+
for (const sessionId of appSessionIds) {
|
|
2030
|
+
const session = sessionLookup.get(sessionId);
|
|
2031
|
+
if (!session) continue;
|
|
2032
|
+
|
|
2033
|
+
const sessionState =
|
|
2034
|
+
safeText(session?.session?.state, 40).toUpperCase() ||
|
|
2035
|
+
safeText(session.state, 40).toUpperCase();
|
|
2036
|
+
if (Object.prototype.hasOwnProperty.call(sessionStates, sessionState)) {
|
|
2037
|
+
sessionStates[sessionState] += 1;
|
|
2038
|
+
}
|
|
2039
|
+
|
|
2040
|
+
const agentType =
|
|
2041
|
+
safeText(session?.thread?.agentType, 20).toUpperCase() ||
|
|
2042
|
+
safeText(session?.session?.agentType, 20).toUpperCase() ||
|
|
2043
|
+
safeText(session.agentType, 20).toUpperCase();
|
|
2044
|
+
if (agentType === "CODEX") agentUsage.codexSessionsFromApp += 1;
|
|
2045
|
+
if (agentType === "CLAUDE") agentUsage.claudeSessionsFromApp += 1;
|
|
2046
|
+
}
|
|
2047
|
+
|
|
2048
|
+
const lastSeenAt =
|
|
2049
|
+
laptop.lastSnapshotAt ||
|
|
2050
|
+
laptop.lastConnectedAt ||
|
|
2051
|
+
laptop.lastDisconnectedAt ||
|
|
2052
|
+
laptop.pairedAt ||
|
|
2053
|
+
laptop.createdAt ||
|
|
2054
|
+
now;
|
|
2055
|
+
|
|
2056
|
+
return {
|
|
2057
|
+
laptopId: laptop.laptopId,
|
|
2058
|
+
deviceId: laptop.deviceId,
|
|
2059
|
+
name: laptop.name || "Unnamed computer",
|
|
2060
|
+
online: onlineLaptopIds.has(laptop.laptopId),
|
|
2061
|
+
createdAt: laptop.createdAt,
|
|
2062
|
+
pairedAt: laptop.pairedAt || null,
|
|
2063
|
+
lastSeenAt,
|
|
2064
|
+
sessionCount: appSessionIds.size,
|
|
2065
|
+
activeSessionCount: [...appSessionIds]
|
|
2066
|
+
.map((sessionId) => sessionLookup.get(sessionId))
|
|
2067
|
+
.filter(Boolean)
|
|
2068
|
+
.filter((session) => {
|
|
2069
|
+
const stateValue =
|
|
2070
|
+
safeText(session?.session?.state, 40).toUpperCase() ||
|
|
2071
|
+
safeText(session.state, 40).toUpperCase();
|
|
2072
|
+
return stateValue === "RUNNING" || stateValue === "WAITING_INPUT";
|
|
2073
|
+
}).length,
|
|
2074
|
+
runCount: runs.length,
|
|
2075
|
+
codexRuns: runs.filter((item) => safeText(item.agentType, 20).toUpperCase() === "CODEX").length,
|
|
2076
|
+
claudeRuns: runs.filter((item) => safeText(item.agentType, 20).toUpperCase() === "CLAUDE").length
|
|
2077
|
+
};
|
|
2078
|
+
})
|
|
2079
|
+
.sort((a, b) => {
|
|
2080
|
+
if (a.online !== b.online) return a.online ? -1 : 1;
|
|
2081
|
+
return (b.lastSeenAt || 0) - (a.lastSeenAt || 0);
|
|
2082
|
+
});
|
|
2083
|
+
|
|
2084
|
+
return {
|
|
2085
|
+
summary: {
|
|
2086
|
+
activeSessions:
|
|
2087
|
+
sessionStates.RUNNING + sessionStates.WAITING_INPUT,
|
|
2088
|
+
appSessionsTotal: devices.reduce((sum, item) => sum + item.sessionCount, 0),
|
|
2089
|
+
appRunsTotal: devices.reduce((sum, item) => sum + item.runCount, 0),
|
|
2090
|
+
appRuns24h: countRecentTimestamps(dailyRuns, now, 24 * 60 * 60 * 1000),
|
|
2091
|
+
appRuns7d: countRecentTimestamps(dailyRuns, now, 7 * 24 * 60 * 60 * 1000)
|
|
2092
|
+
},
|
|
2093
|
+
sessionStates,
|
|
2094
|
+
runStates,
|
|
2095
|
+
agentUsage,
|
|
2096
|
+
devices,
|
|
2097
|
+
dailyRuns
|
|
2098
|
+
};
|
|
2099
|
+
}
|
|
2100
|
+
|
|
2101
|
+
function countRecentDevices(laptops, now, windowMs) {
|
|
2102
|
+
return laptops.filter((item) => {
|
|
2103
|
+
const lastSeenAt =
|
|
2104
|
+
item.lastSnapshotAt ||
|
|
2105
|
+
item.lastConnectedAt ||
|
|
2106
|
+
item.lastDisconnectedAt ||
|
|
2107
|
+
item.pairedAt ||
|
|
2108
|
+
item.createdAt ||
|
|
2109
|
+
0;
|
|
2110
|
+
return lastSeenAt > 0 && now - lastSeenAt <= windowMs;
|
|
2111
|
+
}).length;
|
|
2112
|
+
}
|
|
2113
|
+
|
|
2114
|
+
function countUniqueRecentPairings(pairings, now, windowMs) {
|
|
2115
|
+
const deviceIds = new Set();
|
|
2116
|
+
for (const pairing of pairings) {
|
|
2117
|
+
const claimedAt = toInt(pairing?.claimedAt, 0);
|
|
2118
|
+
const deviceId = safeText(pairing?.deviceId, 200);
|
|
2119
|
+
if (!claimedAt || !deviceId) continue;
|
|
2120
|
+
if (now - claimedAt > windowMs) continue;
|
|
2121
|
+
deviceIds.add(deviceId);
|
|
2122
|
+
}
|
|
2123
|
+
return deviceIds.size;
|
|
2124
|
+
}
|
|
2125
|
+
|
|
2126
|
+
function countRecentTimestamps(values, now, windowMs) {
|
|
2127
|
+
return values.filter((value) => {
|
|
2128
|
+
const timestamp = toInt(value, 0);
|
|
2129
|
+
return timestamp > 0 && now - timestamp <= windowMs;
|
|
2130
|
+
}).length;
|
|
2131
|
+
}
|
|
2132
|
+
|
|
2133
|
+
function buildDailyActivitySeries({ pairings, runs }, days = 14) {
|
|
2134
|
+
const dayMs = 24 * 60 * 60 * 1000;
|
|
2135
|
+
const startOfToday = new Date();
|
|
2136
|
+
startOfToday.setHours(0, 0, 0, 0);
|
|
2137
|
+
const startTime = startOfToday.getTime() - (days - 1) * dayMs;
|
|
2138
|
+
const buckets = new Map();
|
|
2139
|
+
|
|
2140
|
+
for (let offset = 0; offset < days; offset += 1) {
|
|
2141
|
+
const ts = startTime + offset * dayMs;
|
|
2142
|
+
const key = formatDayKey(ts);
|
|
2143
|
+
buckets.set(key, {
|
|
2144
|
+
date: key,
|
|
2145
|
+
pairings: 0,
|
|
2146
|
+
runs: 0
|
|
2147
|
+
});
|
|
2148
|
+
}
|
|
2149
|
+
|
|
2150
|
+
for (const ts of pairings) {
|
|
2151
|
+
const value = toInt(ts, 0);
|
|
2152
|
+
if (!value || value < startTime) continue;
|
|
2153
|
+
const bucket = buckets.get(formatDayKey(value));
|
|
2154
|
+
if (bucket) bucket.pairings += 1;
|
|
2155
|
+
}
|
|
2156
|
+
|
|
2157
|
+
for (const ts of runs) {
|
|
2158
|
+
const value = toInt(ts, 0);
|
|
2159
|
+
if (!value || value < startTime) continue;
|
|
2160
|
+
const bucket = buckets.get(formatDayKey(value));
|
|
2161
|
+
if (bucket) bucket.runs += 1;
|
|
2162
|
+
}
|
|
2163
|
+
|
|
2164
|
+
return [...buckets.values()];
|
|
2165
|
+
}
|
|
2166
|
+
|
|
2167
|
+
function formatDayKey(timestamp) {
|
|
2168
|
+
const date = new Date(timestamp);
|
|
2169
|
+
const year = date.getFullYear();
|
|
2170
|
+
const month = String(date.getMonth() + 1).padStart(2, "0");
|
|
2171
|
+
const day = String(date.getDate()).padStart(2, "0");
|
|
2172
|
+
return `${year}-${month}-${day}`;
|
|
2173
|
+
}
|
|
2174
|
+
|
|
1873
2175
|
function sanitizeState(raw) {
|
|
1874
2176
|
const fallback = {
|
|
1875
2177
|
laptops: [],
|