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 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
- if (!normalizedPath) {
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.4",
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: [],