archer-wizard 0.1.1 → 0.2.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.
Files changed (53) hide show
  1. package/dist/daemon/client.d.ts +4 -0
  2. package/dist/daemon/client.d.ts.map +1 -0
  3. package/dist/daemon/client.js +44 -0
  4. package/dist/daemon/client.js.map +1 -0
  5. package/dist/daemon/lifecycle.d.ts +9 -0
  6. package/dist/daemon/lifecycle.d.ts.map +1 -0
  7. package/dist/daemon/lifecycle.js +118 -0
  8. package/dist/daemon/lifecycle.js.map +1 -0
  9. package/dist/daemon/process.d.ts +2 -0
  10. package/dist/daemon/process.d.ts.map +1 -0
  11. package/dist/daemon/process.js +171 -0
  12. package/dist/daemon/process.js.map +1 -0
  13. package/dist/daemon/run.d.ts +2 -0
  14. package/dist/daemon/run.d.ts.map +1 -0
  15. package/dist/daemon/run.js +5 -0
  16. package/dist/daemon/run.js.map +1 -0
  17. package/dist/daemon/store.d.ts +9 -0
  18. package/dist/daemon/store.d.ts.map +1 -0
  19. package/dist/daemon/store.js +53 -0
  20. package/dist/daemon/store.js.map +1 -0
  21. package/dist/daemon/types.d.ts +47 -0
  22. package/dist/daemon/types.d.ts.map +1 -0
  23. package/dist/daemon/types.js +10 -0
  24. package/dist/daemon/types.js.map +1 -0
  25. package/dist/index.js +105 -21
  26. package/dist/index.js.map +1 -1
  27. package/dist/lib/ascii.d.ts.map +1 -1
  28. package/dist/lib/ascii.js +9 -41
  29. package/dist/lib/ascii.js.map +1 -1
  30. package/dist/tools/unwatch.d.ts +7 -0
  31. package/dist/tools/unwatch.d.ts.map +1 -0
  32. package/dist/tools/unwatch.js +20 -0
  33. package/dist/tools/unwatch.js.map +1 -0
  34. package/dist/tools/watch.d.ts +14 -34
  35. package/dist/tools/watch.d.ts.map +1 -1
  36. package/dist/tools/watch.js +36 -170
  37. package/dist/tools/watch.js.map +1 -1
  38. package/dist/tools/watches.d.ts +2 -0
  39. package/dist/tools/watches.d.ts.map +1 -0
  40. package/dist/tools/watches.js +33 -0
  41. package/dist/tools/watches.js.map +1 -0
  42. package/package.json +2 -1
  43. package/src/daemon/client.ts +50 -0
  44. package/src/daemon/lifecycle.ts +126 -0
  45. package/src/daemon/process.ts +207 -0
  46. package/src/daemon/run.ts +6 -0
  47. package/src/daemon/store.ts +60 -0
  48. package/src/daemon/types.ts +41 -0
  49. package/src/index.ts +111 -22
  50. package/src/lib/ascii.ts +9 -44
  51. package/src/tools/unwatch.ts +26 -0
  52. package/src/tools/watch.ts +46 -212
  53. package/src/tools/watches.ts +37 -0
@@ -0,0 +1,33 @@
1
+ import { sendCommand } from '../daemon/client.js';
2
+ // ─── Execute List Watches (via daemon IPC) ──────────────────
3
+ export async function executeListWatches() {
4
+ try {
5
+ const res = await sendCommand({ type: 'list_watches' });
6
+ if (!res.ok) {
7
+ return `❌ Failed to list watches: ${res.error}`;
8
+ }
9
+ if (res.type !== 'watch_list') {
10
+ return `❌ Unexpected response type: ${res.type}`;
11
+ }
12
+ if (res.watches.length === 0) {
13
+ return '📭 No active watches.';
14
+ }
15
+ const lines = [`📋 Active watches (${res.watches.length}):\n`];
16
+ for (const w of res.watches) {
17
+ lines.push(` 🔹 ${w.id}`);
18
+ lines.push(` table: ${w.table}`);
19
+ lines.push(` event: ${w.event}`);
20
+ if (w.filter)
21
+ lines.push(` filter: ${w.filter}`);
22
+ if (w.webhookUrl)
23
+ lines.push(` webhook: ${w.webhookUrl}`);
24
+ lines.push(` created: ${w.createdAt}`);
25
+ lines.push('');
26
+ }
27
+ return lines.join('\n');
28
+ }
29
+ catch (err) {
30
+ return `❌ Daemon error: ${err instanceof Error ? err.message : String(err)}\n\nIs the archer daemon running?`;
31
+ }
32
+ }
33
+ //# sourceMappingURL=watches.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"watches.js","sourceRoot":"","sources":["../../src/tools/watches.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,MAAM,qBAAqB,CAAC;AAElD,+DAA+D;AAE/D,MAAM,CAAC,KAAK,UAAU,kBAAkB;IACtC,IAAI,CAAC;QACH,MAAM,GAAG,GAAG,MAAM,WAAW,CAAC,EAAE,IAAI,EAAE,cAAc,EAAE,CAAC,CAAC;QAExD,IAAI,CAAC,GAAG,CAAC,EAAE,EAAE,CAAC;YACZ,OAAO,6BAA6B,GAAG,CAAC,KAAK,EAAE,CAAC;QAClD,CAAC;QAED,IAAI,GAAG,CAAC,IAAI,KAAK,YAAY,EAAE,CAAC;YAC9B,OAAO,+BAA+B,GAAG,CAAC,IAAI,EAAE,CAAC;QACnD,CAAC;QAED,IAAI,GAAG,CAAC,OAAO,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;YAC7B,OAAO,uBAAuB,CAAC;QACjC,CAAC;QAED,MAAM,KAAK,GAAG,CAAC,sBAAsB,GAAG,CAAC,OAAO,CAAC,MAAM,MAAM,CAAC,CAAC;QAE/D,KAAK,MAAM,CAAC,IAAI,GAAG,CAAC,OAAO,EAAE,CAAC;YAC5B,KAAK,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC;YAC3B,KAAK,CAAC,IAAI,CAAC,eAAe,CAAC,CAAC,KAAK,EAAE,CAAC,CAAC;YACrC,KAAK,CAAC,IAAI,CAAC,eAAe,CAAC,CAAC,KAAK,EAAE,CAAC,CAAC;YACrC,IAAI,CAAC,CAAC,MAAM;gBAAE,KAAK,CAAC,IAAI,CAAC,gBAAgB,CAAC,CAAC,MAAM,EAAE,CAAC,CAAC;YACrD,IAAI,CAAC,CAAC,UAAU;gBAAE,KAAK,CAAC,IAAI,CAAC,iBAAiB,CAAC,CAAC,UAAU,EAAE,CAAC,CAAC;YAC9D,KAAK,CAAC,IAAI,CAAC,iBAAiB,CAAC,CAAC,SAAS,EAAE,CAAC,CAAC;YAC3C,KAAK,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;QACjB,CAAC;QAED,OAAO,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;IAC1B,CAAC;IAAC,OAAO,GAAG,EAAE,CAAC;QACb,OAAO,mBAAmB,GAAG,YAAY,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC,mCAAmC,CAAC;IAChH,CAAC;AACH,CAAC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "archer-wizard",
3
- "version": "0.1.1",
3
+ "version": "0.2.0",
4
4
  "description": "event intelligence layer for AI agents",
5
5
  "type": "module",
6
6
  "bin": {
@@ -11,6 +11,7 @@
11
11
  "build": "tsc",
12
12
  "dev": "tsx src/index.ts",
13
13
  "dev:mcp": "tsx src/index.ts --mcp",
14
+ "dev:daemon": "tsx src/index.ts --daemon",
14
15
  "prepublishOnly": "npm run build"
15
16
  },
16
17
  "dependencies": {
@@ -0,0 +1,50 @@
1
+ import net from 'node:net';
2
+ import { DAEMON_PORT, DAEMON_HOST } from './types.js';
3
+ import type { IpcRequest, IpcResponse } from './types.js';
4
+
5
+ // ─── Send Command to Daemon ─────────────────────────────────
6
+
7
+ export function sendCommand(req: IpcRequest): Promise<IpcResponse> {
8
+ return new Promise((resolve, reject) => {
9
+ const socket = net.createConnection({ host: DAEMON_HOST, port: DAEMON_PORT }, () => {
10
+ socket.write(JSON.stringify(req) + '\n');
11
+ });
12
+
13
+ let buffer = '';
14
+
15
+ socket.on('data', (data) => {
16
+ buffer += data.toString();
17
+ const newlineIdx = buffer.indexOf('\n');
18
+ if (newlineIdx !== -1) {
19
+ const line = buffer.slice(0, newlineIdx).trim();
20
+ socket.end();
21
+ try {
22
+ resolve(JSON.parse(line) as IpcResponse);
23
+ } catch (err) {
24
+ reject(new Error(`invalid daemon response: ${line}`));
25
+ }
26
+ }
27
+ });
28
+
29
+ socket.on('error', (err) => {
30
+ reject(new Error(`daemon connection failed: ${err.message}`));
31
+ });
32
+
33
+ // Timeout after 5 seconds
34
+ socket.setTimeout(5_000, () => {
35
+ socket.destroy();
36
+ reject(new Error('daemon response timeout'));
37
+ });
38
+ });
39
+ }
40
+
41
+ // ─── Check if Daemon is Running ─────────────────────────────
42
+
43
+ export async function isDaemonRunning(): Promise<boolean> {
44
+ try {
45
+ const res = await sendCommand({ type: 'ping' });
46
+ return res.ok && res.type === 'pong';
47
+ } catch {
48
+ return false;
49
+ }
50
+ }
@@ -0,0 +1,126 @@
1
+ import { spawn } from 'node:child_process';
2
+ import fs from 'node:fs';
3
+ import path from 'node:path';
4
+ import { fileURLToPath } from 'node:url';
5
+ import { PID_FILE, ARCHER_DIR, LOG_FILE } from './types.js';
6
+ import { isDaemonRunning } from './client.js';
7
+
8
+ // ─── Start Daemon ───────────────────────────────────────────
9
+
10
+ export async function startDaemon(): Promise<{ pid: number; alreadyRunning: boolean }> {
11
+ // Check if already running
12
+ if (await isDaemonRunning()) {
13
+ const pid = readPid();
14
+ return { pid: pid ?? 0, alreadyRunning: true };
15
+ }
16
+
17
+ // Ensure directory exists
18
+ if (!fs.existsSync(ARCHER_DIR)) {
19
+ fs.mkdirSync(ARCHER_DIR, { recursive: true });
20
+ }
21
+
22
+ // Resolve the daemon entry script
23
+ // In compiled mode this is dist/daemon/process.js
24
+ // In dev mode with tsx this is src/daemon/process.ts
25
+ const thisFile = fileURLToPath(import.meta.url);
26
+ const thisDir = path.dirname(thisFile);
27
+
28
+ // Look for the daemon runner script
29
+ const daemonRunner = path.join(thisDir, 'run.js');
30
+ const daemonRunnerTs = path.join(thisDir, 'run.ts');
31
+
32
+ let cmd: string;
33
+ let args: string[];
34
+
35
+ if (fs.existsSync(daemonRunner)) {
36
+ // Compiled mode
37
+ cmd = process.execPath; // node
38
+ args = [daemonRunner];
39
+ } else if (fs.existsSync(daemonRunnerTs)) {
40
+ // Dev mode — use tsx
41
+ cmd = 'npx';
42
+ args = ['tsx', daemonRunnerTs];
43
+ } else {
44
+ // Fallback: assume we're in dist/ and run process.js directly
45
+ const processJs = path.join(thisDir, 'process.js');
46
+ cmd = process.execPath;
47
+ args = [processJs, '--run-daemon'];
48
+ }
49
+
50
+ // Open log file for daemon output
51
+ const logFd = fs.openSync(LOG_FILE, 'a');
52
+
53
+ const child = spawn(cmd, args, {
54
+ detached: true,
55
+ stdio: ['ignore', logFd, logFd],
56
+ env: { ...process.env },
57
+ });
58
+
59
+ child.unref();
60
+ fs.closeSync(logFd);
61
+
62
+ const pid = child.pid ?? 0;
63
+
64
+ // Wait briefly for daemon to start, then verify
65
+ await sleep(500);
66
+
67
+ const running = await isDaemonRunning();
68
+ if (!running) {
69
+ // Give it another moment
70
+ await sleep(1000);
71
+ }
72
+
73
+ return { pid, alreadyRunning: false };
74
+ }
75
+
76
+ // ─── Stop Daemon ────────────────────────────────────────────
77
+
78
+ export async function stopDaemon(): Promise<boolean> {
79
+ const pid = readPid();
80
+ if (pid === null) return false;
81
+
82
+ try {
83
+ process.kill(pid, 'SIGTERM');
84
+ // Wait for it to die
85
+ await sleep(500);
86
+ return true;
87
+ } catch {
88
+ // Process already dead — clean up PID file
89
+ try { fs.unlinkSync(PID_FILE); } catch {}
90
+ return false;
91
+ }
92
+ }
93
+
94
+ // ─── Ensure Daemon is Running ───────────────────────────────
95
+
96
+ export async function ensureDaemon(): Promise<{ pid: number }> {
97
+ const result = await startDaemon();
98
+ return { pid: result.pid };
99
+ }
100
+
101
+ // ─── Helpers ────────────────────────────────────────────────
102
+
103
+ function readPid(): number | null {
104
+ try {
105
+ if (!fs.existsSync(PID_FILE)) return null;
106
+ const raw = fs.readFileSync(PID_FILE, 'utf-8').trim();
107
+ const pid = parseInt(raw, 10);
108
+ if (isNaN(pid)) return null;
109
+
110
+ // Check if process is alive
111
+ try {
112
+ process.kill(pid, 0); // Signal 0 = existence check
113
+ return pid;
114
+ } catch {
115
+ // Process is dead — clean up stale PID
116
+ try { fs.unlinkSync(PID_FILE); } catch {}
117
+ return null;
118
+ }
119
+ } catch {
120
+ return null;
121
+ }
122
+ }
123
+
124
+ function sleep(ms: number): Promise<void> {
125
+ return new Promise((resolve) => setTimeout(resolve, ms));
126
+ }
@@ -0,0 +1,207 @@
1
+ import net from 'node:net';
2
+ import { createClient, type RealtimeChannel, type SupabaseClient } from '@supabase/supabase-js';
3
+ import { DAEMON_PORT, DAEMON_HOST, PID_FILE, ARCHER_DIR, LOG_FILE } from './types.js';
4
+ import type { IpcRequest, IpcResponse, WatchConfig } from './types.js';
5
+ import { loadWatches, addWatch, removeWatch } from './store.js';
6
+ import fs from 'node:fs';
7
+
8
+ // ─── Simple webhook delivery for daemon context ─────────────
9
+
10
+ async function deliverWebhook(url: string, payload: unknown): Promise<void> {
11
+ const res = await fetch(url, {
12
+ method: 'POST',
13
+ headers: {
14
+ 'Content-Type': 'application/json',
15
+ 'User-Agent': 'Archer/0.2.0',
16
+ },
17
+ body: JSON.stringify(payload),
18
+ });
19
+ if (!res.ok) {
20
+ throw new Error(`webhook ${res.status} ${res.statusText}`);
21
+ }
22
+ }
23
+
24
+ // ─── Active Channel Registry ────────────────────────────────
25
+
26
+ const channels = new Map<string, { client: SupabaseClient; channel: RealtimeChannel }>();
27
+
28
+ // ─── Subscribe to a Watch ───────────────────────────────────
29
+
30
+ function subscribe(watch: WatchConfig): void {
31
+ // Skip if already subscribed
32
+ if (channels.has(watch.id)) return;
33
+
34
+ const client = createClient(watch.supabaseUrl, watch.supabaseKey);
35
+
36
+ const channelName = `daemon-${watch.table}-${watch.id}`;
37
+ const channel = client
38
+ .channel(channelName)
39
+ .on(
40
+ 'postgres_changes',
41
+ {
42
+ event: watch.event,
43
+ schema: 'public',
44
+ table: watch.table,
45
+ ...(watch.filter ? { filter: watch.filter } : {}),
46
+ },
47
+ async (payload) => {
48
+ log(`[event] ${watch.table} ${payload.eventType} → watch ${watch.id}`);
49
+
50
+ if (watch.webhookUrl) {
51
+ try {
52
+ await deliverWebhook(watch.webhookUrl, payload);
53
+ log(`[webhook] delivered to ${watch.webhookUrl}`);
54
+ } catch (err) {
55
+ log(`[webhook] failed: ${err instanceof Error ? err.message : String(err)}`);
56
+ }
57
+ }
58
+ },
59
+ )
60
+ .subscribe((status) => {
61
+ log(`[channel] ${channelName} → ${status}`);
62
+ });
63
+
64
+ channels.set(watch.id, { client, channel });
65
+ }
66
+
67
+ // ─── Unsubscribe from a Watch ───────────────────────────────
68
+
69
+ async function unsubscribe(watchId: string): Promise<void> {
70
+ const entry = channels.get(watchId);
71
+ if (!entry) return;
72
+
73
+ await entry.client.removeChannel(entry.channel);
74
+ channels.delete(watchId);
75
+ }
76
+
77
+ // ─── Handle IPC Command ─────────────────────────────────────
78
+
79
+ async function handleCommand(req: IpcRequest): Promise<IpcResponse> {
80
+ switch (req.type) {
81
+ case 'add_watch': {
82
+ const watches = addWatch(req.watch);
83
+ subscribe(req.watch);
84
+ return { ok: true, type: 'watch_added', watchId: req.watch.id };
85
+ }
86
+
87
+ case 'remove_watch': {
88
+ const { watches, removed } = removeWatch(req.watchId);
89
+ if (!removed) {
90
+ return { ok: false, error: `watch ${req.watchId} not found` };
91
+ }
92
+ await unsubscribe(req.watchId);
93
+ return { ok: true, type: 'watch_removed', watchId: req.watchId };
94
+ }
95
+
96
+ case 'list_watches': {
97
+ const watches = loadWatches();
98
+ return { ok: true, type: 'watch_list', watches };
99
+ }
100
+
101
+ case 'ping': {
102
+ return { ok: true, type: 'pong' };
103
+ }
104
+
105
+ default:
106
+ return { ok: false, error: 'unknown command' };
107
+ }
108
+ }
109
+
110
+ // ─── Logging ────────────────────────────────────────────────
111
+
112
+ function log(message: string): void {
113
+ const ts = new Date().toISOString();
114
+ const line = `[${ts}] ${message}\n`;
115
+ try {
116
+ fs.appendFileSync(LOG_FILE, line);
117
+ } catch {
118
+ // If we can't write to log, just continue
119
+ }
120
+ }
121
+
122
+ // ─── Main: Start TCP Server ─────────────────────────────────
123
+
124
+ export function startDaemonProcess(): void {
125
+ // Ensure directory exists
126
+ if (!fs.existsSync(ARCHER_DIR)) {
127
+ fs.mkdirSync(ARCHER_DIR, { recursive: true });
128
+ }
129
+
130
+ // Write PID file
131
+ fs.writeFileSync(PID_FILE, String(process.pid), 'utf-8');
132
+
133
+ log('daemon starting');
134
+
135
+ // Re-subscribe all persisted watches
136
+ const watches = loadWatches();
137
+ for (const watch of watches) {
138
+ subscribe(watch);
139
+ log(`[restore] re-subscribed watch ${watch.id} on ${watch.table}`);
140
+ }
141
+ log(`restored ${watches.length} watch(es)`);
142
+
143
+ // Create TCP server
144
+ const server = net.createServer((socket) => {
145
+ let buffer = '';
146
+
147
+ socket.on('data', async (data) => {
148
+ buffer += data.toString();
149
+
150
+ // Process complete JSON lines
151
+ let newlineIdx: number;
152
+ while ((newlineIdx = buffer.indexOf('\n')) !== -1) {
153
+ const line = buffer.slice(0, newlineIdx).trim();
154
+ buffer = buffer.slice(newlineIdx + 1);
155
+
156
+ if (!line) continue;
157
+
158
+ try {
159
+ const req = JSON.parse(line) as IpcRequest;
160
+ const res = await handleCommand(req);
161
+ socket.write(JSON.stringify(res) + '\n');
162
+ } catch (err) {
163
+ const errRes: IpcResponse = {
164
+ ok: false,
165
+ error: `parse error: ${err instanceof Error ? err.message : String(err)}`,
166
+ };
167
+ socket.write(JSON.stringify(errRes) + '\n');
168
+ }
169
+ }
170
+ });
171
+
172
+ socket.on('error', (err) => {
173
+ log(`[socket] error: ${err.message}`);
174
+ });
175
+ });
176
+
177
+ server.listen(DAEMON_PORT, DAEMON_HOST, () => {
178
+ log(`daemon listening on ${DAEMON_HOST}:${DAEMON_PORT} (pid ${process.pid})`);
179
+ });
180
+
181
+ server.on('error', (err) => {
182
+ log(`[server] error: ${err.message}`);
183
+ process.exit(1);
184
+ });
185
+
186
+ // Clean shutdown
187
+ const shutdown = () => {
188
+ log('daemon shutting down');
189
+ server.close();
190
+
191
+ // Unsubscribe all channels
192
+ for (const [id, entry] of channels) {
193
+ entry.client.removeChannel(entry.channel).catch(() => {});
194
+ }
195
+ channels.clear();
196
+
197
+ // Remove PID file
198
+ try {
199
+ fs.unlinkSync(PID_FILE);
200
+ } catch {}
201
+
202
+ process.exit(0);
203
+ };
204
+
205
+ process.on('SIGTERM', shutdown);
206
+ process.on('SIGINT', shutdown);
207
+ }
@@ -0,0 +1,6 @@
1
+ // Daemon runner — this file is the entry point for the detached daemon process.
2
+ // It gets spawned by lifecycle.ts and runs startDaemonProcess().
3
+
4
+ import { startDaemonProcess } from './process.js';
5
+
6
+ startDaemonProcess();
@@ -0,0 +1,60 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import { ARCHER_DIR, STATE_FILE } from './types.js';
4
+ import type { WatchConfig } from './types.js';
5
+
6
+ // ─── Ensure Directory ───────────────────────────────────────
7
+
8
+ function ensureDir(): void {
9
+ if (!fs.existsSync(ARCHER_DIR)) {
10
+ fs.mkdirSync(ARCHER_DIR, { recursive: true });
11
+ }
12
+ }
13
+
14
+ // ─── Load Watches ───────────────────────────────────────────
15
+
16
+ export function loadWatches(): WatchConfig[] {
17
+ try {
18
+ if (!fs.existsSync(STATE_FILE)) return [];
19
+ const raw = fs.readFileSync(STATE_FILE, 'utf-8');
20
+ const parsed = JSON.parse(raw);
21
+ if (!Array.isArray(parsed)) return [];
22
+ return parsed as WatchConfig[];
23
+ } catch {
24
+ return [];
25
+ }
26
+ }
27
+
28
+ // ─── Save Watches (atomic write) ────────────────────────────
29
+
30
+ export function saveWatches(watches: WatchConfig[]): void {
31
+ ensureDir();
32
+ const tmp = STATE_FILE + '.tmp';
33
+ fs.writeFileSync(tmp, JSON.stringify(watches, null, 2) + '\n', 'utf-8');
34
+ fs.renameSync(tmp, STATE_FILE);
35
+ }
36
+
37
+ // ─── Add Watch ──────────────────────────────────────────────
38
+
39
+ export function addWatch(watch: WatchConfig): WatchConfig[] {
40
+ const watches = loadWatches();
41
+ // Replace if same ID exists
42
+ const idx = watches.findIndex((w) => w.id === watch.id);
43
+ if (idx >= 0) {
44
+ watches[idx] = watch;
45
+ } else {
46
+ watches.push(watch);
47
+ }
48
+ saveWatches(watches);
49
+ return watches;
50
+ }
51
+
52
+ // ─── Remove Watch ───────────────────────────────────────────
53
+
54
+ export function removeWatch(watchId: string): { watches: WatchConfig[]; removed: boolean } {
55
+ const watches = loadWatches();
56
+ const before = watches.length;
57
+ const filtered = watches.filter((w) => w.id !== watchId);
58
+ saveWatches(filtered);
59
+ return { watches: filtered, removed: filtered.length < before };
60
+ }
@@ -0,0 +1,41 @@
1
+ // ─── Daemon Configuration ───────────────────────────────────
2
+
3
+ import path from 'node:path';
4
+ import os from 'node:os';
5
+
6
+ export const DAEMON_PORT = 7481;
7
+ export const DAEMON_HOST = '127.0.0.1';
8
+ export const ARCHER_DIR = path.join(os.homedir(), '.archer');
9
+ export const STATE_FILE = path.join(ARCHER_DIR, 'state.json');
10
+ export const PID_FILE = path.join(ARCHER_DIR, 'daemon.pid');
11
+ export const LOG_FILE = path.join(ARCHER_DIR, 'daemon.log');
12
+
13
+ // ─── Watch State ────────────────────────────────────────────
14
+
15
+ export interface WatchConfig {
16
+ id: string;
17
+ table: string;
18
+ event: 'INSERT' | 'UPDATE' | 'DELETE' | '*';
19
+ filter?: string;
20
+ webhookUrl?: string;
21
+ createdAt: string; // ISO timestamp
22
+ supabaseUrl: string; // needed to re-subscribe
23
+ supabaseKey: string; // service role key
24
+ }
25
+
26
+ // ─── IPC Protocol ───────────────────────────────────────────
27
+ // JSON lines over TCP. Each message is JSON + '\n'.
28
+ // Client sends a Request, daemon responds with a Response.
29
+
30
+ export type IpcRequest =
31
+ | { type: 'add_watch'; watch: WatchConfig }
32
+ | { type: 'remove_watch'; watchId: string }
33
+ | { type: 'list_watches' }
34
+ | { type: 'ping' };
35
+
36
+ export type IpcResponse =
37
+ | { ok: true; type: 'watch_added'; watchId: string }
38
+ | { ok: true; type: 'watch_removed'; watchId: string }
39
+ | { ok: true; type: 'watch_list'; watches: WatchConfig[] }
40
+ | { ok: true; type: 'pong' }
41
+ | { ok: false; error: string };