@zgeoff/atc 0.1.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.
@@ -0,0 +1,32 @@
1
+ import { readdirSync, statSync } from 'node:fs';
2
+ import { join } from 'node:path';
3
+ import pkg from '../package.json';
4
+
5
+ /**
6
+ * The build identity used in the protocol handshake. The version alone
7
+ * never changes between pulls, so the newest mtime across the source tree
8
+ * (or the compiled binary's) is folded in — a client and daemon from
9
+ * different checkouts of the same version still read as different builds,
10
+ * which is what triggers the stale-daemon restart. Any source file counts:
11
+ * a stamp taken from a single file misses every change that lands
12
+ * elsewhere and leaves stale daemons in service.
13
+ */
14
+ export function getBuild(): string {
15
+ let stamp = 0;
16
+
17
+ try {
18
+ for (const entry of readdirSync(import.meta.dir)) {
19
+ if (entry.endsWith('.ts')) {
20
+ stamp = Math.max(stamp, statSync(join(import.meta.dir, entry)).mtimeMs);
21
+ }
22
+ }
23
+ } catch {}
24
+
25
+ if (stamp === 0) {
26
+ try {
27
+ stamp = statSync(process.execPath).mtimeMs;
28
+ } catch {}
29
+ }
30
+
31
+ return `atc/${pkg.version}+${Math.round(stamp).toString(36)}`;
32
+ }
@@ -0,0 +1,31 @@
1
+ import { isRecord, sendReport } from './report';
2
+
3
+ /**
4
+ * Runs as a Claude Code hook inside wrangled sessions. Reads the hook event
5
+ * from stdin and forwards it to the atc unix socket. Always exits 0 so it
6
+ * never blocks the session it reports on.
7
+ */
8
+ export async function runHookReport(): Promise<void> {
9
+ const sock = process.env['ATC_SOCKET'];
10
+ const atcId = process.env['ATC_SESSION_ID'];
11
+
12
+ if (sock !== undefined && sock !== '' && atcId !== undefined && atcId !== '') {
13
+ const raw = await new Response(Bun.stdin.stream()).text();
14
+
15
+ let payload: Record<string, unknown> = {};
16
+
17
+ try {
18
+ const parsed: unknown = JSON.parse(raw);
19
+
20
+ if (isRecord(parsed)) {
21
+ payload = parsed;
22
+ }
23
+ } catch {}
24
+
25
+ const line = `${JSON.stringify({ atcId, event: payload['hook_event_name'], payload })}\n`;
26
+
27
+ await sendReport(sock, line, 2000);
28
+ }
29
+
30
+ process.exit(0);
31
+ }
package/src/hooks.ts ADDED
@@ -0,0 +1,54 @@
1
+ import { unlinkSync } from 'node:fs';
2
+ import { socketPath } from './config';
3
+ import { isRecord } from './report';
4
+
5
+ export interface HookEvent {
6
+ atcId: string;
7
+ event: string;
8
+ payload: Record<string, unknown>;
9
+ }
10
+
11
+ export function startHookServer(onEvent: (e: HookEvent) => void, path: string = socketPath) {
12
+ try {
13
+ unlinkSync(path);
14
+ } catch {}
15
+
16
+ return Bun.listen<string>({
17
+ unix: path,
18
+ socket: {
19
+ data(socket, buf) {
20
+ const buffered = (socket.data ?? '') + buf.toString();
21
+ const lines = buffered.split('\n');
22
+
23
+ socket.data = lines.pop() ?? '';
24
+
25
+ for (const line of lines) {
26
+ if (line.trim() === '') {
27
+ continue;
28
+ }
29
+
30
+ try {
31
+ const parsed: unknown = JSON.parse(line);
32
+
33
+ if (
34
+ isRecord(parsed) &&
35
+ typeof parsed['atcId'] === 'string' &&
36
+ typeof parsed['event'] === 'string' &&
37
+ isRecord(parsed['payload'])
38
+ ) {
39
+ onEvent({
40
+ atcId: parsed['atcId'],
41
+ event: parsed['event'],
42
+ payload: parsed['payload'],
43
+ });
44
+ }
45
+ } catch {}
46
+ }
47
+ },
48
+ open(socket) {
49
+ socket.data = '';
50
+ },
51
+ error() {},
52
+ },
53
+ });
54
+ }