agentvigil 1.0.0
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/LICENSE +21 -0
- package/README.md +76 -0
- package/dist/commands/autostart.d.ts +3 -0
- package/dist/commands/autostart.d.ts.map +1 -0
- package/dist/commands/autostart.js +57 -0
- package/dist/commands/autostart.js.map +1 -0
- package/dist/commands/daemon.d.ts +31 -0
- package/dist/commands/daemon.d.ts.map +1 -0
- package/dist/commands/daemon.js +131 -0
- package/dist/commands/daemon.js.map +1 -0
- package/dist/commands/setup.d.ts +5 -0
- package/dist/commands/setup.d.ts.map +1 -0
- package/dist/commands/setup.js +158 -0
- package/dist/commands/setup.js.map +1 -0
- package/dist/commands/start.d.ts +2 -0
- package/dist/commands/start.d.ts.map +1 -0
- package/dist/commands/start.js +89 -0
- package/dist/commands/start.js.map +1 -0
- package/dist/commands/uninstall.d.ts +7 -0
- package/dist/commands/uninstall.d.ts.map +1 -0
- package/dist/commands/uninstall.js +13 -0
- package/dist/commands/uninstall.js.map +1 -0
- package/dist/crypto/encryption.d.ts +11 -0
- package/dist/crypto/encryption.d.ts.map +1 -0
- package/dist/crypto/encryption.js +57 -0
- package/dist/crypto/encryption.js.map +1 -0
- package/dist/hooks/hook-handler.d.ts +18 -0
- package/dist/hooks/hook-handler.d.ts.map +1 -0
- package/dist/hooks/hook-handler.js +256 -0
- package/dist/hooks/hook-handler.js.map +1 -0
- package/dist/hooks/hook-manager.d.ts +22 -0
- package/dist/hooks/hook-manager.d.ts.map +1 -0
- package/dist/hooks/hook-manager.js +133 -0
- package/dist/hooks/hook-manager.js.map +1 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +88 -0
- package/dist/index.js.map +1 -0
- package/dist/notifications/fcm-client.d.ts +15 -0
- package/dist/notifications/fcm-client.d.ts.map +1 -0
- package/dist/notifications/fcm-client.js +72 -0
- package/dist/notifications/fcm-client.js.map +1 -0
- package/dist/notifications/ntfy-client.d.ts +6 -0
- package/dist/notifications/ntfy-client.d.ts.map +1 -0
- package/dist/notifications/ntfy-client.js +51 -0
- package/dist/notifications/ntfy-client.js.map +1 -0
- package/dist/relay/relay-handler.d.ts +51 -0
- package/dist/relay/relay-handler.d.ts.map +1 -0
- package/dist/relay/relay-handler.js +325 -0
- package/dist/relay/relay-handler.js.map +1 -0
- package/dist/sessions/keystroke-injector.d.ts +4 -0
- package/dist/sessions/keystroke-injector.d.ts.map +1 -0
- package/dist/sessions/keystroke-injector.js +216 -0
- package/dist/sessions/keystroke-injector.js.map +1 -0
- package/dist/sessions/process-detector.d.ts +27 -0
- package/dist/sessions/process-detector.d.ts.map +1 -0
- package/dist/sessions/process-detector.js +180 -0
- package/dist/sessions/process-detector.js.map +1 -0
- package/dist/sessions/session-manager.d.ts +37 -0
- package/dist/sessions/session-manager.d.ts.map +1 -0
- package/dist/sessions/session-manager.js +90 -0
- package/dist/sessions/session-manager.js.map +1 -0
- package/dist/sessions/session-poller.d.ts +18 -0
- package/dist/sessions/session-poller.d.ts.map +1 -0
- package/dist/sessions/session-poller.js +73 -0
- package/dist/sessions/session-poller.js.map +1 -0
- package/dist/sessions/session-watcher.d.ts +20 -0
- package/dist/sessions/session-watcher.d.ts.map +1 -0
- package/dist/sessions/session-watcher.js +71 -0
- package/dist/sessions/session-watcher.js.map +1 -0
- package/dist/sessions/tmux-bridge.d.ts +11 -0
- package/dist/sessions/tmux-bridge.d.ts.map +1 -0
- package/dist/sessions/tmux-bridge.js +67 -0
- package/dist/sessions/tmux-bridge.js.map +1 -0
- package/dist/tunnel/tunnel-manager.d.ts +8 -0
- package/dist/tunnel/tunnel-manager.d.ts.map +1 -0
- package/dist/tunnel/tunnel-manager.js +57 -0
- package/dist/tunnel/tunnel-manager.js.map +1 -0
- package/dist/tunnel/websocket-server.d.ts +48 -0
- package/dist/tunnel/websocket-server.d.ts.map +1 -0
- package/dist/tunnel/websocket-server.js +173 -0
- package/dist/tunnel/websocket-server.js.map +1 -0
- package/dist/types.d.ts +37 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +2 -0
- package/dist/types.js.map +1 -0
- package/dist/utils/config.d.ts +23 -0
- package/dist/utils/config.d.ts.map +1 -0
- package/dist/utils/config.js +54 -0
- package/dist/utils/config.js.map +1 -0
- package/dist/utils/logger.d.ts +9 -0
- package/dist/utils/logger.d.ts.map +1 -0
- package/dist/utils/logger.js +11 -0
- package/dist/utils/logger.js.map +1 -0
- package/dist/utils/qr.d.ts +19 -0
- package/dist/utils/qr.d.ts.map +1 -0
- package/dist/utils/qr.js +17 -0
- package/dist/utils/qr.js.map +1 -0
- package/package.json +67 -0
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { logger } from '../utils/logger.js';
|
|
2
|
+
import { deleteSession, getAllSessions, getSession, updateSession, } from './session-manager.js';
|
|
3
|
+
import { detectAgentProcesses, findSessionFileForCwd, isPidAlive } from './process-detector.js';
|
|
4
|
+
export const PROCESS_POLL_INTERVAL_MS = 10_000;
|
|
5
|
+
/**
|
|
6
|
+
* Reconciles the OS process table with the in-memory session store every
|
|
7
|
+
* PROCESS_POLL_INTERVAL_MS milliseconds.
|
|
8
|
+
*
|
|
9
|
+
* - Sessions whose process has died are removed immediately.
|
|
10
|
+
* - Running agent processes not yet in the store are added.
|
|
11
|
+
* - done/error sessions are left for cleanup() to expire.
|
|
12
|
+
*
|
|
13
|
+
* Runs the first poll synchronously (awaited) before returning so the store
|
|
14
|
+
* is fully populated when the caller proceeds.
|
|
15
|
+
*/
|
|
16
|
+
export async function startSessionPolling(onSessionAdded, onSessionRemoved, intervalMs = PROCESS_POLL_INTERVAL_MS) {
|
|
17
|
+
async function poll() {
|
|
18
|
+
try {
|
|
19
|
+
const liveProcs = await detectAgentProcesses();
|
|
20
|
+
// ── Remove dead sessions ──────────────────────────────────────────
|
|
21
|
+
for (const session of getAllSessions()) {
|
|
22
|
+
// done/error sessions are managed by cleanup() — leave them alone
|
|
23
|
+
if (session.state === 'done' || session.state === 'error')
|
|
24
|
+
continue;
|
|
25
|
+
if (session.pid && !isPidAlive(session.pid)) {
|
|
26
|
+
logger.info(`Session terminated (pid ${session.pid} dead): ${session.project_name}`);
|
|
27
|
+
deleteSession(session.session_id);
|
|
28
|
+
onSessionRemoved(session.session_id);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
// ── Add new sessions ──────────────────────────────────────────────
|
|
32
|
+
for (const proc of liveProcs) {
|
|
33
|
+
// Already tracked by pid?
|
|
34
|
+
const byPid = getAllSessions().find(s => s.pid === proc.pid);
|
|
35
|
+
if (byPid)
|
|
36
|
+
continue;
|
|
37
|
+
const fileInfo = await findSessionFileForCwd(proc.cwd);
|
|
38
|
+
if (!fileInfo) {
|
|
39
|
+
// JSONL not written yet — session is just starting; next poll will catch it.
|
|
40
|
+
continue;
|
|
41
|
+
}
|
|
42
|
+
// Already tracked by session_id (hook may have created it first)?
|
|
43
|
+
if (getSession(fileInfo.sessionId)) {
|
|
44
|
+
const existing = getSession(fileInfo.sessionId);
|
|
45
|
+
if (!existing.pid) {
|
|
46
|
+
// Attach pid so future dead-process checks work.
|
|
47
|
+
updateSession(fileInfo.sessionId, { pid: proc.pid });
|
|
48
|
+
}
|
|
49
|
+
continue;
|
|
50
|
+
}
|
|
51
|
+
const session = updateSession(fileInfo.sessionId, {
|
|
52
|
+
cwd: proc.cwd,
|
|
53
|
+
agent: proc.agentType,
|
|
54
|
+
state: 'working',
|
|
55
|
+
last_message: fileInfo.lastMessage,
|
|
56
|
+
last_activity: new Date(),
|
|
57
|
+
pid: proc.pid,
|
|
58
|
+
});
|
|
59
|
+
logger.info(`New session detected: ${session.project_name} (pid: ${proc.pid})`);
|
|
60
|
+
onSessionAdded(session);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
catch (err) {
|
|
64
|
+
logger.warn('Session poll error', err);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
await poll(); // first run awaited — store is populated when we return
|
|
68
|
+
const activeSessions = getAllSessions().filter(s => s.state === 'working' || s.state === 'blocked' || s.state === 'idle');
|
|
69
|
+
logger.info(`Session poll ready: ${activeSessions.length} active session(s)`);
|
|
70
|
+
const timer = setInterval(() => void poll(), intervalMs);
|
|
71
|
+
return { stop: () => clearInterval(timer) };
|
|
72
|
+
}
|
|
73
|
+
//# sourceMappingURL=session-poller.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"session-poller.js","sourceRoot":"","sources":["../../src/sessions/session-poller.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { type FSWatcher } from 'chokidar';
|
|
2
|
+
export declare const SESSION_BLOCKLIST: string[];
|
|
3
|
+
/** Returns true when the basename of `cwd` exactly matches a blocklisted tool/meta directory. */
|
|
4
|
+
export declare function isBlocklisted(cwd: string): boolean;
|
|
5
|
+
export interface SessionUpdate {
|
|
6
|
+
session_id: string;
|
|
7
|
+
cwd: string;
|
|
8
|
+
last_message?: string;
|
|
9
|
+
last_activity: Date;
|
|
10
|
+
}
|
|
11
|
+
export declare function readLastLine(filePath: string): Promise<string | undefined>;
|
|
12
|
+
export declare function extractCwdFromPath(filePath: string): string;
|
|
13
|
+
export declare function processTranscriptFile(filePath: string, onUpdate: (update: SessionUpdate) => void): Promise<void>;
|
|
14
|
+
/**
|
|
15
|
+
* Watches JSONL transcripts for live last_message updates on existing sessions.
|
|
16
|
+
* Session creation and deletion is owned by the process poller — this watcher
|
|
17
|
+
* only fires on 'change' events (ignoreInitial) to keep last_message current.
|
|
18
|
+
*/
|
|
19
|
+
export declare function watchSessions(onUpdate: (update: SessionUpdate) => void): FSWatcher;
|
|
20
|
+
//# sourceMappingURL=session-watcher.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"session-watcher.d.ts","sourceRoot":"","sources":["../../src/sessions/session-watcher.ts"],"names":[],"mappings":"AAGA,OAAiB,EAAE,KAAK,SAAS,EAAE,MAAM,UAAU,CAAC;AAEpD,eAAO,MAAM,iBAAiB,UAK7B,CAAC;AAEF,iGAAiG;AACjG,wBAAgB,aAAa,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAGlD;AAED,MAAM,WAAW,aAAa;IAC5B,UAAU,EAAE,MAAM,CAAC;IACnB,GAAG,EAAE,MAAM,CAAC;IACZ,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,aAAa,EAAE,IAAI,CAAC;CACrB;AAED,wBAAsB,YAAY,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,GAAG,SAAS,CAAC,CAIhF;AAKD,wBAAgB,kBAAkB,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,CAG3D;AAED,wBAAsB,qBAAqB,CACzC,QAAQ,EAAE,MAAM,EAChB,QAAQ,EAAE,CAAC,MAAM,EAAE,aAAa,KAAK,IAAI,GACxC,OAAO,CAAC,IAAI,CAAC,CAyBf;AAED;;;;GAIG;AACH,wBAAgB,aAAa,CAC3B,QAAQ,EAAE,CAAC,MAAM,EAAE,aAAa,KAAK,IAAI,GACxC,SAAS,CAaX"}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import fs from 'node:fs/promises';
|
|
2
|
+
import os from 'node:os';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import chokidar from 'chokidar';
|
|
5
|
+
export const SESSION_BLOCKLIST = [
|
|
6
|
+
'agentvigil-mac', // Mac companion — never a monitored session
|
|
7
|
+
'node_modules',
|
|
8
|
+
'.claude',
|
|
9
|
+
'.agentvigil',
|
|
10
|
+
];
|
|
11
|
+
/** Returns true when the basename of `cwd` exactly matches a blocklisted tool/meta directory. */
|
|
12
|
+
export function isBlocklisted(cwd) {
|
|
13
|
+
const basename = path.basename(cwd).toLowerCase();
|
|
14
|
+
return SESSION_BLOCKLIST.includes(basename);
|
|
15
|
+
}
|
|
16
|
+
export async function readLastLine(filePath) {
|
|
17
|
+
const content = await fs.readFile(filePath, 'utf8');
|
|
18
|
+
const lines = content.split('\n').map((line) => line.trim()).filter(Boolean);
|
|
19
|
+
return lines.at(-1);
|
|
20
|
+
}
|
|
21
|
+
// Claude Code project directory names are the cwd with '/' replaced by '-'.
|
|
22
|
+
// That's lossy for paths containing dashes, so this is only a fallback for
|
|
23
|
+
// when a transcript entry doesn't carry its own `cwd` field.
|
|
24
|
+
export function extractCwdFromPath(filePath) {
|
|
25
|
+
const projectDir = path.basename(path.dirname(filePath));
|
|
26
|
+
return projectDir.replace(/-/g, '/');
|
|
27
|
+
}
|
|
28
|
+
export async function processTranscriptFile(filePath, onUpdate) {
|
|
29
|
+
try {
|
|
30
|
+
const lastLine = await readLastLine(filePath);
|
|
31
|
+
if (!lastLine)
|
|
32
|
+
return;
|
|
33
|
+
const entry = JSON.parse(lastLine);
|
|
34
|
+
// Skip sessions that have already ended — they show up in the initial
|
|
35
|
+
// chokidar scan but should not appear as active sessions on startup.
|
|
36
|
+
const hookEvent = typeof entry.hook_event_name === 'string' ? entry.hook_event_name : '';
|
|
37
|
+
if (hookEvent === 'Stop' || hookEvent === 'SubagentStop')
|
|
38
|
+
return;
|
|
39
|
+
if (typeof entry.state === 'string' && entry.state === 'done')
|
|
40
|
+
return;
|
|
41
|
+
const cwd = typeof entry.cwd === 'string' ? entry.cwd : extractCwdFromPath(filePath);
|
|
42
|
+
if (isBlocklisted(cwd))
|
|
43
|
+
return;
|
|
44
|
+
onUpdate({
|
|
45
|
+
session_id: path.basename(filePath, '.jsonl'),
|
|
46
|
+
cwd,
|
|
47
|
+
last_message: typeof entry.message === 'string' ? entry.message : undefined,
|
|
48
|
+
last_activity: new Date(),
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
catch {
|
|
52
|
+
// Malformed line or unreadable file — skip it, the watcher keeps running.
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Watches JSONL transcripts for live last_message updates on existing sessions.
|
|
57
|
+
* Session creation and deletion is owned by the process poller — this watcher
|
|
58
|
+
* only fires on 'change' events (ignoreInitial) to keep last_message current.
|
|
59
|
+
*/
|
|
60
|
+
export function watchSessions(onUpdate) {
|
|
61
|
+
const projectsDir = path.join(os.homedir(), '.claude', 'projects');
|
|
62
|
+
const watcher = chokidar.watch(`${projectsDir}/**/*.jsonl`, {
|
|
63
|
+
ignoreInitial: true, // poller owns detection; watcher is supplementary only
|
|
64
|
+
awaitWriteFinish: { stabilityThreshold: 100, pollInterval: 50 },
|
|
65
|
+
});
|
|
66
|
+
watcher.on('change', (filePath) => {
|
|
67
|
+
void processTranscriptFile(filePath, onUpdate);
|
|
68
|
+
});
|
|
69
|
+
return watcher;
|
|
70
|
+
}
|
|
71
|
+
//# sourceMappingURL=session-watcher.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"session-watcher.js","sourceRoot":"","sources":["../../src/sessions/session-watcher.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export interface TmuxPane {
|
|
2
|
+
pane_id: string;
|
|
3
|
+
pid: string;
|
|
4
|
+
command: string;
|
|
5
|
+
cwd: string;
|
|
6
|
+
}
|
|
7
|
+
export declare function isPidAlive(pid: string): boolean;
|
|
8
|
+
export declare function enumerateTmuxSessions(): Promise<TmuxPane[]>;
|
|
9
|
+
export declare function findTmuxPaneForSession(cwd: string, panes: TmuxPane[]): TmuxPane | undefined;
|
|
10
|
+
export declare function sendMacNotification(title: string, message: string): Promise<void>;
|
|
11
|
+
//# sourceMappingURL=tmux-bridge.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"tmux-bridge.d.ts","sourceRoot":"","sources":["../../src/sessions/tmux-bridge.ts"],"names":[],"mappings":"AAMA,MAAM,WAAW,QAAQ;IACvB,OAAO,EAAE,MAAM,CAAC;IAChB,GAAG,EAAE,MAAM,CAAC;IACZ,OAAO,EAAE,MAAM,CAAC;IAChB,GAAG,EAAE,MAAM,CAAC;CACb;AAED,wBAAgB,UAAU,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAO/C;AAWD,wBAAsB,qBAAqB,IAAI,OAAO,CAAC,QAAQ,EAAE,CAAC,CAwBjE;AAED,wBAAgB,sBAAsB,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,QAAQ,EAAE,GAAG,QAAQ,GAAG,SAAS,CAK3F;AAMD,wBAAsB,mBAAmB,CAAC,KAAK,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAOvF"}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { execFile } from 'node:child_process';
|
|
2
|
+
import { logger } from '../utils/logger.js';
|
|
3
|
+
import { isBlocklisted } from './session-watcher.js';
|
|
4
|
+
const AGENT_COMMANDS = ['claude', 'codex', 'amp'];
|
|
5
|
+
export function isPidAlive(pid) {
|
|
6
|
+
try {
|
|
7
|
+
process.kill(parseInt(pid, 10), 0);
|
|
8
|
+
return true;
|
|
9
|
+
}
|
|
10
|
+
catch {
|
|
11
|
+
return false;
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
function runCommand(command, args) {
|
|
15
|
+
return new Promise((resolve, reject) => {
|
|
16
|
+
execFile(command, args, (error, stdout) => {
|
|
17
|
+
if (error)
|
|
18
|
+
reject(error);
|
|
19
|
+
else
|
|
20
|
+
resolve(stdout.toString());
|
|
21
|
+
});
|
|
22
|
+
});
|
|
23
|
+
}
|
|
24
|
+
export async function enumerateTmuxSessions() {
|
|
25
|
+
try {
|
|
26
|
+
const stdout = await runCommand('tmux', [
|
|
27
|
+
'list-panes',
|
|
28
|
+
'-a',
|
|
29
|
+
'-F',
|
|
30
|
+
'#{pane_id}|#{pane_pid}|#{pane_current_command}|#{pane_current_path}',
|
|
31
|
+
]);
|
|
32
|
+
return stdout
|
|
33
|
+
.trim()
|
|
34
|
+
.split('\n')
|
|
35
|
+
.filter(Boolean)
|
|
36
|
+
.map((line) => {
|
|
37
|
+
const [pane_id, pid, command, cwd] = line.split('|');
|
|
38
|
+
return { pane_id, pid, command, cwd };
|
|
39
|
+
})
|
|
40
|
+
.filter((pane) => AGENT_COMMANDS.some((agent) => pane.command.includes(agent)))
|
|
41
|
+
.filter((pane) => isPidAlive(pane.pid))
|
|
42
|
+
.filter((pane) => !isBlocklisted(pane.cwd));
|
|
43
|
+
}
|
|
44
|
+
catch {
|
|
45
|
+
// tmux not running or not installed
|
|
46
|
+
return [];
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
export function findTmuxPaneForSession(cwd, panes) {
|
|
50
|
+
// Prefer the most specific match so a nested-directory session binds to its
|
|
51
|
+
// own pane rather than a parent directory's (e.g. monorepo sub-packages).
|
|
52
|
+
const candidates = panes.filter((pane) => pane.cwd === cwd || cwd.startsWith(`${pane.cwd}/`));
|
|
53
|
+
return candidates.sort((a, b) => b.cwd.length - a.cwd.length)[0];
|
|
54
|
+
}
|
|
55
|
+
function appleScriptString(value) {
|
|
56
|
+
return `"${value.replace(/\\/g, '\\\\').replace(/"/g, '\\"')}"`;
|
|
57
|
+
}
|
|
58
|
+
export async function sendMacNotification(title, message) {
|
|
59
|
+
const script = `display notification ${appleScriptString(message)} with title ${appleScriptString(title)} sound name "Funk"`;
|
|
60
|
+
try {
|
|
61
|
+
await runCommand('osascript', ['-e', script]);
|
|
62
|
+
}
|
|
63
|
+
catch (err) {
|
|
64
|
+
logger.warn('Failed to display Mac notification', err);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
//# sourceMappingURL=tmux-bridge.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"tmux-bridge.js","sourceRoot":"","sources":["../../src/sessions/tmux-bridge.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"tunnel-manager.d.ts","sourceRoot":"","sources":["../../src/tunnel/tunnel-manager.ts"],"names":[],"mappings":"AAMA,qBAAa,aAAa;IACxB,OAAO,CAAC,OAAO,CAA6B;IAC5C,OAAO,CAAC,SAAS,CAAuB;IAElC,KAAK,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC;IA8C1C,YAAY,IAAI,MAAM,GAAG,IAAI;IAI7B,IAAI,IAAI,IAAI;CAKb"}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { spawn } from 'node:child_process';
|
|
2
|
+
import { logger } from '../utils/logger.js';
|
|
3
|
+
const TUNNEL_URL_PATTERN = /https:\/\/[a-z0-9-]+\.trycloudflare\.com/;
|
|
4
|
+
const TUNNEL_URL_TIMEOUT_MS = 30_000;
|
|
5
|
+
export class TunnelManager {
|
|
6
|
+
process = null;
|
|
7
|
+
tunnelUrl = null;
|
|
8
|
+
async start(port) {
|
|
9
|
+
return new Promise((resolve, reject) => {
|
|
10
|
+
let settled = false;
|
|
11
|
+
let timeoutHandle;
|
|
12
|
+
const settle = (action) => {
|
|
13
|
+
if (settled)
|
|
14
|
+
return;
|
|
15
|
+
settled = true;
|
|
16
|
+
clearTimeout(timeoutHandle);
|
|
17
|
+
action();
|
|
18
|
+
};
|
|
19
|
+
this.process = spawn('cloudflared', [
|
|
20
|
+
'tunnel',
|
|
21
|
+
'--url',
|
|
22
|
+
`http://localhost:${port}`,
|
|
23
|
+
'--no-autoupdate',
|
|
24
|
+
'--logfile',
|
|
25
|
+
'/dev/null',
|
|
26
|
+
]);
|
|
27
|
+
this.process.stderr?.on('data', (data) => {
|
|
28
|
+
const match = data.toString().match(TUNNEL_URL_PATTERN);
|
|
29
|
+
if (match && !this.tunnelUrl) {
|
|
30
|
+
this.tunnelUrl = match[0].replace('https://', 'wss://');
|
|
31
|
+
logger.success(`Tunnel established: ${this.tunnelUrl}`);
|
|
32
|
+
settle(() => resolve(this.tunnelUrl));
|
|
33
|
+
}
|
|
34
|
+
});
|
|
35
|
+
this.process.on('exit', (code) => {
|
|
36
|
+
if (!this.tunnelUrl) {
|
|
37
|
+
settle(() => reject(new Error(`cloudflared exited with code ${code}`)));
|
|
38
|
+
}
|
|
39
|
+
});
|
|
40
|
+
this.process.on('error', (err) => {
|
|
41
|
+
settle(() => reject(err));
|
|
42
|
+
});
|
|
43
|
+
timeoutHandle = setTimeout(() => {
|
|
44
|
+
settle(() => reject(new Error('Tunnel URL not received within 30s')));
|
|
45
|
+
}, TUNNEL_URL_TIMEOUT_MS);
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
getTunnelUrl() {
|
|
49
|
+
return this.tunnelUrl;
|
|
50
|
+
}
|
|
51
|
+
stop() {
|
|
52
|
+
this.process?.kill();
|
|
53
|
+
this.process = null;
|
|
54
|
+
this.tunnelUrl = null;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
//# sourceMappingURL=tunnel-manager.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"tunnel-manager.js","sourceRoot":"","sources":["../../src/tunnel/tunnel-manager.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,EAAqB,MAAM,oBAAoB,CAAC;AAC9D,OAAO,EAAE,MAAM,EAAE,MAAM,oBAAoB,CAAC;AAE5C,MAAM,kBAAkB,GAAG,0CAA0C,CAAC;AACtE,MAAM,qBAAqB,GAAG,MAAM,CAAC;AAErC,MAAM,OAAO,aAAa;IAChB,OAAO,GAAwB,IAAI,CAAC;IACpC,SAAS,GAAkB,IAAI,CAAC;IAExC,KAAK,CAAC,KAAK,CAAC,IAAY;QACtB,OAAO,IAAI,OAAO,CAAC,CAAC,OAAO,EAAE,MAAM,EAAE,EAAE;YACrC,IAAI,OAAO,GAAG,KAAK,CAAC;YACpB,IAAI,aAA4C,CAAC;YAEjD,MAAM,MAAM,GAAG,CAAC,MAAkB,EAAE,EAAE;gBACpC,IAAI,OAAO;oBAAE,OAAO;gBACpB,OAAO,GAAG,IAAI,CAAC;gBACf,YAAY,CAAC,aAAa,CAAC,CAAC;gBAC5B,MAAM,EAAE,CAAC;YACX,CAAC,CAAC;YAEF,IAAI,CAAC,OAAO,GAAG,KAAK,CAAC,aAAa,EAAE;gBAClC,QAAQ;gBACR,OAAO;gBACP,oBAAoB,IAAI,EAAE;gBAC1B,iBAAiB;gBACjB,WAAW;gBACX,WAAW;aACZ,CAAC,CAAC;YAEH,IAAI,CAAC,OAAO,CAAC,MAAM,EAAE,EAAE,CAAC,MAAM,EAAE,CAAC,IAAY,EAAE,EAAE;gBAC/C,MAAM,KAAK,GAAG,IAAI,CAAC,QAAQ,EAAE,CAAC,KAAK,CAAC,kBAAkB,CAAC,CAAC;gBACxD,IAAI,KAAK,IAAI,CAAC,IAAI,CAAC,SAAS,EAAE,CAAC;oBAC7B,IAAI,CAAC,SAAS,GAAG,KAAK,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,UAAU,EAAE,QAAQ,CAAC,CAAC;oBACxD,MAAM,CAAC,OAAO,CAAC,uBAAuB,IAAI,CAAC,SAAS,EAAE,CAAC,CAAC;oBACxD,MAAM,CAAC,GAAG,EAAE,CAAC,OAAO,CAAC,IAAI,CAAC,SAAU,CAAC,CAAC,CAAC;gBACzC,CAAC;YACH,CAAC,CAAC,CAAC;YAEH,IAAI,CAAC,OAAO,CAAC,EAAE,CAAC,MAAM,EAAE,CAAC,IAAI,EAAE,EAAE;gBAC/B,IAAI,CAAC,IAAI,CAAC,SAAS,EAAE,CAAC;oBACpB,MAAM,CAAC,GAAG,EAAE,CAAC,MAAM,CAAC,IAAI,KAAK,CAAC,gCAAgC,IAAI,EAAE,CAAC,CAAC,CAAC,CAAC;gBAC1E,CAAC;YACH,CAAC,CAAC,CAAC;YAEH,IAAI,CAAC,OAAO,CAAC,EAAE,CAAC,OAAO,EAAE,CAAC,GAAG,EAAE,EAAE;gBAC/B,MAAM,CAAC,GAAG,EAAE,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,CAAC;YAC5B,CAAC,CAAC,CAAC;YAEH,aAAa,GAAG,UAAU,CAAC,GAAG,EAAE;gBAC9B,MAAM,CAAC,GAAG,EAAE,CAAC,MAAM,CAAC,IAAI,KAAK,CAAC,oCAAoC,CAAC,CAAC,CAAC,CAAC;YACxE,CAAC,EAAE,qBAAqB,CAAC,CAAC;QAC5B,CAAC,CAAC,CAAC;IACL,CAAC;IAED,YAAY;QACV,OAAO,IAAI,CAAC,SAAS,CAAC;IACxB,CAAC;IAED,IAAI;QACF,IAAI,CAAC,OAAO,EAAE,IAAI,EAAE,CAAC;QACrB,IAAI,CAAC,OAAO,GAAG,IAAI,CAAC;QACpB,IAAI,CAAC,SAAS,GAAG,IAAI,CAAC;IACxB,CAAC;CACF"}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { WebSocket } from 'ws';
|
|
2
|
+
import type { AgentEvent, PhoneCommand } from '../types.js';
|
|
3
|
+
export interface PairRequest {
|
|
4
|
+
type: 'pair';
|
|
5
|
+
pub_key: string;
|
|
6
|
+
device_name: string;
|
|
7
|
+
}
|
|
8
|
+
export interface AgentVigilWsServerOptions {
|
|
9
|
+
/** The phone's first message after scanning the QR arrives unencrypted as this. */
|
|
10
|
+
onPairRequest?: (request: PairRequest, socket: WebSocket) => void;
|
|
11
|
+
/** Decrypted phone commands (approve/deny/send_prompt), once a shared secret is known. */
|
|
12
|
+
onCommand?: (command: PhoneCommand) => void;
|
|
13
|
+
/** Supplies the current session list for the full_sync sent on connect. */
|
|
14
|
+
getSessions?: () => AgentEvent[];
|
|
15
|
+
/**
|
|
16
|
+
* AgentEvents forwarded in-process by short-lived `agentvigil hook` CLI
|
|
17
|
+
* invocations (see hook-handler.ts) — the only way they can reach the live
|
|
18
|
+
* phone connection the daemon is holding. Requires a matching `localToken`.
|
|
19
|
+
*/
|
|
20
|
+
onHookEvent?: (event: AgentEvent) => void;
|
|
21
|
+
/** Shared secret only the Mac's own processes know (its X25519 secret key) — authenticates `hook_event` messages so the tunnel-exposed phone can't forge them. */
|
|
22
|
+
localToken?: string;
|
|
23
|
+
}
|
|
24
|
+
export declare class AgentVigilWsServer {
|
|
25
|
+
private readonly port;
|
|
26
|
+
private readonly wss;
|
|
27
|
+
private readonly getSessions;
|
|
28
|
+
private readonly onPairRequest?;
|
|
29
|
+
private readonly onCommand?;
|
|
30
|
+
private readonly onHookEvent?;
|
|
31
|
+
private readonly localToken?;
|
|
32
|
+
private phoneSocket;
|
|
33
|
+
private sharedSecret;
|
|
34
|
+
private heartbeatTimer;
|
|
35
|
+
constructor(port?: number, options?: AgentVigilWsServerOptions);
|
|
36
|
+
start(): void;
|
|
37
|
+
/** Records the shared secret derived for the currently-paired device (or clears it). */
|
|
38
|
+
setSharedSecret(secret: string | undefined): void;
|
|
39
|
+
/** The NaCl shared secret for the paired phone, or undefined if not yet paired. */
|
|
40
|
+
getSharedSecret(): string | undefined;
|
|
41
|
+
onMessage(raw: string): void;
|
|
42
|
+
sendEvent(event: AgentEvent): void;
|
|
43
|
+
sendFullSync(): void;
|
|
44
|
+
get isPhoneConnected(): boolean;
|
|
45
|
+
stop(): void;
|
|
46
|
+
private sendHeartbeat;
|
|
47
|
+
}
|
|
48
|
+
//# sourceMappingURL=websocket-server.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"websocket-server.d.ts","sourceRoot":"","sources":["../../src/tunnel/websocket-server.ts"],"names":[],"mappings":"AACA,OAAO,EAAmB,SAAS,EAAgB,MAAM,IAAI,CAAC;AAG9D,OAAO,KAAK,EAAE,UAAU,EAAiB,YAAY,EAAE,MAAM,aAAa,CAAC;AAI3E,MAAM,WAAW,WAAW;IAC1B,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,MAAM,CAAC;IAChB,WAAW,EAAE,MAAM,CAAC;CACrB;AAED,MAAM,WAAW,yBAAyB;IACxC,mFAAmF;IACnF,aAAa,CAAC,EAAE,CAAC,OAAO,EAAE,WAAW,EAAE,MAAM,EAAE,SAAS,KAAK,IAAI,CAAC;IAClE,0FAA0F;IAC1F,SAAS,CAAC,EAAE,CAAC,OAAO,EAAE,YAAY,KAAK,IAAI,CAAC;IAC5C,2EAA2E;IAC3E,WAAW,CAAC,EAAE,MAAM,UAAU,EAAE,CAAC;IACjC;;;;OAIG;IACH,WAAW,CAAC,EAAE,CAAC,KAAK,EAAE,UAAU,KAAK,IAAI,CAAC;IAC1C,kKAAkK;IAClK,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB;AAED,qBAAa,kBAAkB;IAYjB,OAAO,CAAC,QAAQ,CAAC,IAAI;IAXjC,OAAO,CAAC,QAAQ,CAAC,GAAG,CAAkB;IACtC,OAAO,CAAC,QAAQ,CAAC,WAAW,CAAqB;IACjD,OAAO,CAAC,QAAQ,CAAC,aAAa,CAAC,CAAoD;IACnF,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAC,CAAkC;IAC7D,OAAO,CAAC,QAAQ,CAAC,WAAW,CAAC,CAA8B;IAC3D,OAAO,CAAC,QAAQ,CAAC,UAAU,CAAC,CAAS;IAErC,OAAO,CAAC,WAAW,CAA0B;IAC7C,OAAO,CAAC,YAAY,CAAqB;IACzC,OAAO,CAAC,cAAc,CAA+C;gBAExC,IAAI,GAAE,MAAa,EAAE,OAAO,GAAE,yBAA8B;IASzF,KAAK,IAAI,IAAI;IAyCb,wFAAwF;IACxF,eAAe,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,GAAG,IAAI;IAIjD,mFAAmF;IACnF,eAAe,IAAI,MAAM,GAAG,SAAS;IAIrC,SAAS,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI;IA2D5B,SAAS,CAAC,KAAK,EAAE,UAAU,GAAG,IAAI;IAMlC,YAAY,IAAI,IAAI;IAepB,IAAI,gBAAgB,IAAI,OAAO,CAE9B;IAED,IAAI,IAAI,IAAI;IAUZ,OAAO,CAAC,aAAa;CAYtB"}
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
import { WebSocketServer, WebSocket } from 'ws';
|
|
2
|
+
import { logger } from '../utils/logger.js';
|
|
3
|
+
import { decrypt, encrypt } from '../crypto/encryption.js';
|
|
4
|
+
const HEARTBEAT_INTERVAL_MS = 30_000;
|
|
5
|
+
export class AgentVigilWsServer {
|
|
6
|
+
port;
|
|
7
|
+
wss;
|
|
8
|
+
getSessions;
|
|
9
|
+
onPairRequest;
|
|
10
|
+
onCommand;
|
|
11
|
+
onHookEvent;
|
|
12
|
+
localToken;
|
|
13
|
+
phoneSocket = null;
|
|
14
|
+
sharedSecret;
|
|
15
|
+
heartbeatTimer = null;
|
|
16
|
+
constructor(port = 3847, options = {}) {
|
|
17
|
+
this.port = port;
|
|
18
|
+
this.wss = new WebSocketServer({ port });
|
|
19
|
+
this.getSessions = options.getSessions ?? (() => []);
|
|
20
|
+
this.onPairRequest = options.onPairRequest;
|
|
21
|
+
this.onCommand = options.onCommand;
|
|
22
|
+
this.onHookEvent = options.onHookEvent;
|
|
23
|
+
this.localToken = options.localToken;
|
|
24
|
+
}
|
|
25
|
+
start() {
|
|
26
|
+
this.wss.on('connection', (ws, request) => {
|
|
27
|
+
// Short-lived connections forwarded by `agentvigil hook` CLI invocations
|
|
28
|
+
// (see hook-handler.ts) hit a dedicated path so they're never mistaken
|
|
29
|
+
// for the phone — otherwise they'd overwrite `phoneSocket` and the real
|
|
30
|
+
// phone connection would be silently orphaned when the hook's socket closes.
|
|
31
|
+
if (request?.url?.startsWith('/hook')) {
|
|
32
|
+
ws.on('message', (data) => {
|
|
33
|
+
try {
|
|
34
|
+
this.onMessage(data.toString());
|
|
35
|
+
}
|
|
36
|
+
catch (err) {
|
|
37
|
+
logger.error('Error handling hook_event message', err);
|
|
38
|
+
}
|
|
39
|
+
});
|
|
40
|
+
ws.on('error', (err) => logger.error('Hook connection WS error', err));
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
logger.info('Phone connected via tunnel');
|
|
44
|
+
this.phoneSocket = ws;
|
|
45
|
+
ws.on('message', (data) => {
|
|
46
|
+
try {
|
|
47
|
+
this.onMessage(data.toString());
|
|
48
|
+
}
|
|
49
|
+
catch (err) {
|
|
50
|
+
logger.error('Error handling phone message — keeping socket open', err);
|
|
51
|
+
}
|
|
52
|
+
});
|
|
53
|
+
ws.on('close', () => {
|
|
54
|
+
logger.warn('Phone disconnected');
|
|
55
|
+
if (this.phoneSocket === ws)
|
|
56
|
+
this.phoneSocket = null;
|
|
57
|
+
});
|
|
58
|
+
ws.on('error', (err) => logger.error('WS error', err));
|
|
59
|
+
this.sendFullSync();
|
|
60
|
+
});
|
|
61
|
+
this.heartbeatTimer = setInterval(() => this.sendHeartbeat(), HEARTBEAT_INTERVAL_MS);
|
|
62
|
+
logger.success(`WS server listening on port ${this.port}`);
|
|
63
|
+
}
|
|
64
|
+
/** Records the shared secret derived for the currently-paired device (or clears it). */
|
|
65
|
+
setSharedSecret(secret) {
|
|
66
|
+
this.sharedSecret = secret;
|
|
67
|
+
}
|
|
68
|
+
/** The NaCl shared secret for the paired phone, or undefined if not yet paired. */
|
|
69
|
+
getSharedSecret() {
|
|
70
|
+
return this.sharedSecret;
|
|
71
|
+
}
|
|
72
|
+
onMessage(raw) {
|
|
73
|
+
let parsed;
|
|
74
|
+
try {
|
|
75
|
+
parsed = JSON.parse(raw);
|
|
76
|
+
}
|
|
77
|
+
catch {
|
|
78
|
+
logger.warn('Received a malformed WS message — ignoring');
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
// Forwarded by a short-lived `agentvigil hook` CLI process — authenticated
|
|
82
|
+
// with a local-only token (the Mac's own secret key) rather than the
|
|
83
|
+
// phone's shared secret, since the hook process isn't "paired".
|
|
84
|
+
if (parsed?.type === 'hook_event') {
|
|
85
|
+
if (this.localToken && parsed.token === this.localToken) {
|
|
86
|
+
this.onHookEvent?.(parsed.event);
|
|
87
|
+
}
|
|
88
|
+
else {
|
|
89
|
+
logger.warn('Rejected hook_event message with an invalid local token');
|
|
90
|
+
}
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
// The pairing handshake is the one message that travels unencrypted.
|
|
94
|
+
if (parsed?.type === 'pair') {
|
|
95
|
+
if (this.phoneSocket) {
|
|
96
|
+
try {
|
|
97
|
+
this.onPairRequest?.({ type: 'pair', pub_key: parsed.pub_key, device_name: parsed.device_name }, this.phoneSocket);
|
|
98
|
+
}
|
|
99
|
+
catch (err) {
|
|
100
|
+
logger.error('Pairing handshake failed', err);
|
|
101
|
+
this.phoneSocket.send(JSON.stringify({ type: 'pairing_error', message: String(err) }));
|
|
102
|
+
// DO NOT close the socket — let the phone retry
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
if (!this.sharedSecret) {
|
|
108
|
+
logger.warn('Received a message before pairing was established — ignoring');
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
try {
|
|
112
|
+
const decrypted = decrypt(parsed.payload, this.sharedSecret);
|
|
113
|
+
const command = JSON.parse(decrypted);
|
|
114
|
+
// full_sync_request is a WS-layer concern — respond immediately with
|
|
115
|
+
// the current session snapshot rather than forwarding to the relay.
|
|
116
|
+
if (command.type === 'full_sync_request') {
|
|
117
|
+
logger.dim('full_sync_request from phone — sending snapshot');
|
|
118
|
+
this.sendFullSync();
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
121
|
+
this.onCommand?.(command);
|
|
122
|
+
}
|
|
123
|
+
catch (err) {
|
|
124
|
+
logger.error('Failed to decrypt phone message', err);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
sendEvent(event) {
|
|
128
|
+
if (!this.isPhoneConnected || !this.sharedSecret)
|
|
129
|
+
return;
|
|
130
|
+
const encrypted = encrypt(JSON.stringify(event), this.sharedSecret);
|
|
131
|
+
this.phoneSocket.send(JSON.stringify({ payload: encrypted }));
|
|
132
|
+
}
|
|
133
|
+
sendFullSync() {
|
|
134
|
+
const sessions = this.getSessions();
|
|
135
|
+
const event = {
|
|
136
|
+
type: 'full_sync',
|
|
137
|
+
session_id: 'full_sync',
|
|
138
|
+
project_name: 'AgentVigil',
|
|
139
|
+
cwd: '',
|
|
140
|
+
agent: 'claude-code',
|
|
141
|
+
message: `${sessions.length} active session(s)`,
|
|
142
|
+
timestamp: new Date().toISOString(),
|
|
143
|
+
sessions,
|
|
144
|
+
};
|
|
145
|
+
this.sendEvent(event);
|
|
146
|
+
}
|
|
147
|
+
get isPhoneConnected() {
|
|
148
|
+
return this.phoneSocket?.readyState === WebSocket.OPEN;
|
|
149
|
+
}
|
|
150
|
+
stop() {
|
|
151
|
+
if (this.heartbeatTimer) {
|
|
152
|
+
clearInterval(this.heartbeatTimer);
|
|
153
|
+
this.heartbeatTimer = null;
|
|
154
|
+
}
|
|
155
|
+
this.phoneSocket?.close();
|
|
156
|
+
this.phoneSocket = null;
|
|
157
|
+
this.wss.close();
|
|
158
|
+
}
|
|
159
|
+
sendHeartbeat() {
|
|
160
|
+
if (!this.isPhoneConnected)
|
|
161
|
+
return;
|
|
162
|
+
this.sendEvent({
|
|
163
|
+
type: 'heartbeat',
|
|
164
|
+
session_id: 'heartbeat',
|
|
165
|
+
project_name: 'AgentVigil',
|
|
166
|
+
cwd: '',
|
|
167
|
+
agent: 'claude-code',
|
|
168
|
+
message: 'heartbeat',
|
|
169
|
+
timestamp: new Date().toISOString(),
|
|
170
|
+
});
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
//# sourceMappingURL=websocket-server.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"websocket-server.js","sourceRoot":"","sources":["../../src/tunnel/websocket-server.ts"],"names":[],"mappings":"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"}
|
package/dist/types.d.ts
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
export interface HookPayload {
|
|
2
|
+
session_id: string;
|
|
3
|
+
transcript_path: string;
|
|
4
|
+
cwd: string;
|
|
5
|
+
hook_event_name: string;
|
|
6
|
+
notification_type?: string;
|
|
7
|
+
message?: string;
|
|
8
|
+
}
|
|
9
|
+
export type AgentEventType = 'permission_prompt' | 'task_complete' | 'session_error' | 'idle_waiting' | 'session_started' | 'session_ended' | 'session_updated' | 'heartbeat' | 'full_sync';
|
|
10
|
+
export interface AgentEvent {
|
|
11
|
+
type: AgentEventType;
|
|
12
|
+
session_id: string;
|
|
13
|
+
project_name: string;
|
|
14
|
+
cwd: string;
|
|
15
|
+
agent: 'claude-code' | 'codex' | 'amp';
|
|
16
|
+
message: string;
|
|
17
|
+
permission_command?: string;
|
|
18
|
+
tool_name?: string;
|
|
19
|
+
tool_input?: string;
|
|
20
|
+
timestamp: string;
|
|
21
|
+
pid?: string;
|
|
22
|
+
}
|
|
23
|
+
export interface FullSyncEvent extends AgentEvent {
|
|
24
|
+
type: 'full_sync';
|
|
25
|
+
sessions: AgentEvent[];
|
|
26
|
+
}
|
|
27
|
+
export type PhoneCommandType = 'approve' | 'deny' | 'send_prompt' | 'heartbeat' | 'register_fcm_token';
|
|
28
|
+
export interface PhoneCommand {
|
|
29
|
+
type: PhoneCommandType;
|
|
30
|
+
session_id: string;
|
|
31
|
+
payload?: string;
|
|
32
|
+
/** FCM registration token — present on 'register_fcm_token' commands. */
|
|
33
|
+
token?: string;
|
|
34
|
+
/** Human-readable phone name — present on 'register_fcm_token' commands. */
|
|
35
|
+
device_name?: string;
|
|
36
|
+
}
|
|
37
|
+
//# sourceMappingURL=types.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA,MAAM,WAAW,WAAW;IAC1B,UAAU,EAAE,MAAM,CAAC;IACnB,eAAe,EAAE,MAAM,CAAC;IACxB,GAAG,EAAE,MAAM,CAAC;IACZ,eAAe,EAAE,MAAM,CAAC;IACxB,iBAAiB,CAAC,EAAE,MAAM,CAAC;IAC3B,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AAED,MAAM,MAAM,cAAc,GACtB,mBAAmB,GACnB,eAAe,GACf,eAAe,GACf,cAAc,GACd,iBAAiB,GACjB,eAAe,GACf,iBAAiB,GACjB,WAAW,GACX,WAAW,CAAC;AAEhB,MAAM,WAAW,UAAU;IACzB,IAAI,EAAE,cAAc,CAAC;IACrB,UAAU,EAAE,MAAM,CAAC;IACnB,YAAY,EAAE,MAAM,CAAC;IACrB,GAAG,EAAE,MAAM,CAAC;IACZ,KAAK,EAAE,aAAa,GAAG,OAAO,GAAG,KAAK,CAAC;IACvC,OAAO,EAAE,MAAM,CAAC;IAChB,kBAAkB,CAAC,EAAE,MAAM,CAAC;IAC5B,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,SAAS,EAAE,MAAM,CAAC;IAClB,GAAG,CAAC,EAAE,MAAM,CAAC;CACd;AAED,MAAM,WAAW,aAAc,SAAQ,UAAU;IAC/C,IAAI,EAAE,WAAW,CAAC;IAClB,QAAQ,EAAE,UAAU,EAAE,CAAC;CACxB;AAED,MAAM,MAAM,gBAAgB,GAAG,SAAS,GAAG,MAAM,GAAG,aAAa,GAAG,WAAW,GAAG,oBAAoB,CAAC;AAEvG,MAAM,WAAW,YAAY;IAC3B,IAAI,EAAE,gBAAgB,CAAC;IACvB,UAAU,EAAE,MAAM,CAAC;IACnB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,yEAAyE;IACzE,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,4EAA4E;IAC5E,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB"}
|
package/dist/types.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.js","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":""}
|