archer-wizard 0.1.1 → 0.2.1

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 (61) 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/dist/wizard/index.d.ts.map +1 -1
  43. package/dist/wizard/index.js +57 -23
  44. package/dist/wizard/index.js.map +1 -1
  45. package/dist/wizard/scanner.d.ts.map +1 -1
  46. package/dist/wizard/scanner.js +0 -23
  47. package/dist/wizard/scanner.js.map +1 -1
  48. package/package.json +2 -1
  49. package/src/daemon/client.ts +50 -0
  50. package/src/daemon/lifecycle.ts +126 -0
  51. package/src/daemon/process.ts +207 -0
  52. package/src/daemon/run.ts +6 -0
  53. package/src/daemon/store.ts +60 -0
  54. package/src/daemon/types.ts +41 -0
  55. package/src/index.ts +111 -22
  56. package/src/lib/ascii.ts +9 -44
  57. package/src/tools/unwatch.ts +26 -0
  58. package/src/tools/watch.ts +46 -212
  59. package/src/tools/watches.ts +37 -0
  60. package/src/wizard/index.ts +54 -24
  61. package/src/wizard/scanner.ts +0 -23
@@ -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 };
package/src/index.ts CHANGED
@@ -5,8 +5,13 @@ import 'dotenv/config';
5
5
  // ─── Mode Detection ─────────────────────────────────────────
6
6
 
7
7
  const isMcpMode = process.argv.includes('--mcp');
8
+ const isDaemonMode = process.argv.includes('--daemon');
8
9
 
9
- if (isMcpMode) {
10
+ if (isDaemonMode) {
11
+ // ─── Daemon Mode ──────────────────────────────────────────
12
+ const { startDaemonProcess } = await import('./daemon/process.js');
13
+ startDaemonProcess();
14
+ } else if (isMcpMode) {
10
15
  // ─── MCP Server Mode ─────────────────────────────────────
11
16
  const { Server } = await import('@modelcontextprotocol/sdk/server/index.js');
12
17
  const { StdioServerTransport } = await import('@modelcontextprotocol/sdk/server/stdio.js');
@@ -14,13 +19,78 @@ if (isMcpMode) {
14
19
  CallToolRequestSchema,
15
20
  ListToolsRequestSchema,
16
21
  } = await import('@modelcontextprotocol/sdk/types.js');
17
- const { executeWatch, WATCH_TOOL_SCHEMA } = await import('./tools/watch.js');
22
+ const { executeWatch, WatchInputSchema } = await import('./tools/watch.js');
23
+ const { executeUnwatch, UnwatchInputSchema } = await import('./tools/unwatch.js');
24
+ const { executeListWatches } = await import('./tools/watches.js');
25
+ const { ensureDaemon } = await import('./daemon/lifecycle.js');
18
26
  const { stderrReady, stderrError } = await import('./lib/ascii.js');
19
27
 
28
+ // Auto-start daemon if not running
29
+ try {
30
+ const { pid } = await ensureDaemon();
31
+ stderrReady(`daemon running (pid ${pid})`);
32
+ } catch {
33
+ stderrError('warning: could not start daemon — watches will fail');
34
+ }
35
+
36
+ // Read credentials from environment
37
+ const supabaseUrl = process.env.SUPABASE_URL ?? '';
38
+ const serviceRoleKey = process.env.SUPABASE_SERVICE_ROLE_KEY ?? '';
39
+
40
+ // ─── Tool Definitions ──────────────────────────────────────
41
+
42
+ const WATCH_TOOL = {
43
+ name: 'archer_watch',
44
+ description:
45
+ 'Create a persistent real-time watch on a Supabase table. The watch survives agent session restarts. Delivers changes to a webhook URL.',
46
+ inputSchema: {
47
+ type: 'object' as const,
48
+ properties: {
49
+ table: { type: 'string', description: 'Supabase table name to watch' },
50
+ event: {
51
+ type: 'string',
52
+ enum: ['INSERT', 'UPDATE', 'DELETE', '*'],
53
+ description: 'Database event to listen for (default: *)',
54
+ default: '*',
55
+ },
56
+ filter: {
57
+ type: 'string',
58
+ description: 'Optional Postgres filter e.g. "status=eq.active"',
59
+ },
60
+ webhookUrl: {
61
+ type: 'string',
62
+ description: 'URL to receive webhook POST when event fires',
63
+ },
64
+ },
65
+ required: ['table'],
66
+ },
67
+ };
68
+
69
+ const UNWATCH_TOOL = {
70
+ name: 'archer_unwatch',
71
+ description: 'Remove an active watch by its ID. The watch will stop listening and be deleted from persistent storage.',
72
+ inputSchema: {
73
+ type: 'object' as const,
74
+ properties: {
75
+ watchId: { type: 'string', description: 'The watch ID returned by archer_watch' },
76
+ },
77
+ required: ['watchId'],
78
+ },
79
+ };
80
+
81
+ const WATCHES_TOOL = {
82
+ name: 'archer_watches',
83
+ description: 'List all active watches managed by the archer daemon, including their IDs, tables, events, and webhook URLs.',
84
+ inputSchema: {
85
+ type: 'object' as const,
86
+ properties: {},
87
+ },
88
+ };
89
+
20
90
  const server = new Server(
21
91
  {
22
92
  name: 'archer',
23
- version: '0.1.0',
93
+ version: '0.2.0',
24
94
  },
25
95
  {
26
96
  capabilities: {
@@ -32,7 +102,7 @@ if (isMcpMode) {
32
102
  // Register tool listing
33
103
  server.setRequestHandler(ListToolsRequestSchema, async () => {
34
104
  return {
35
- tools: [WATCH_TOOL_SCHEMA],
105
+ tools: [WATCH_TOOL, UNWATCH_TOOL, WATCHES_TOOL],
36
106
  };
37
107
  });
38
108
 
@@ -40,27 +110,46 @@ if (isMcpMode) {
40
110
  server.setRequestHandler(CallToolRequestSchema, async (request) => {
41
111
  const { name, arguments: args } = request.params;
42
112
 
43
- if (name === 'archer_watch') {
44
- const result = executeWatch(args);
113
+ try {
114
+ let resultText: string;
115
+
116
+ switch (name) {
117
+ case 'archer_watch': {
118
+ const input = WatchInputSchema.parse(args);
119
+ resultText = await executeWatch(input, supabaseUrl, serviceRoleKey);
120
+ break;
121
+ }
122
+ case 'archer_unwatch': {
123
+ const input = UnwatchInputSchema.parse(args);
124
+ resultText = await executeUnwatch(input);
125
+ break;
126
+ }
127
+ case 'archer_watches': {
128
+ resultText = await executeListWatches();
129
+ break;
130
+ }
131
+ default:
132
+ return {
133
+ content: [
134
+ {
135
+ type: 'text' as const,
136
+ text: JSON.stringify({ error: `unknown tool: ${name}` }),
137
+ },
138
+ ],
139
+ isError: true,
140
+ };
141
+ }
142
+
45
143
  return {
46
- content: [
47
- {
48
- type: 'text' as const,
49
- text: JSON.stringify(result, null, 2),
50
- },
51
- ],
144
+ content: [{ type: 'text' as const, text: resultText }],
145
+ };
146
+ } catch (err) {
147
+ const message = err instanceof Error ? err.message : String(err);
148
+ return {
149
+ content: [{ type: 'text' as const, text: `❌ Error: ${message}` }],
150
+ isError: true,
52
151
  };
53
152
  }
54
-
55
- return {
56
- content: [
57
- {
58
- type: 'text' as const,
59
- text: JSON.stringify({ error: `unknown tool: ${name}` }),
60
- },
61
- ],
62
- isError: true,
63
- };
64
153
  });
65
154
 
66
155
  // Start server
package/src/lib/ascii.ts CHANGED
@@ -11,54 +11,19 @@ const emeraldDim = colors.dim;
11
11
 
12
12
  export function showAsciiArt(): void {
13
13
  const lines = [
14
- '█████╗ ██████╗ ██████╗██╗ ██╗███████╗██████╗ ',
15
- '██╔══██╗██╔══██╗██╔════╝██║ ██║██╔════╝██╔══██╗',
16
- '███████║██████╔╝██║ ███████║█████╗ ██████╔╝',
17
- '██╔══██║██╔══██╗██║ ██╔══██║██╔══╝ ██╔══██╗',
18
- '██║ ██║██║ ██║╚██████╗██║ ██║███████╗██║ ██║',
19
- '╚═╝ ╚═╝╚═╝ ╚═╝ ╚═════╝╚═╝ ╚═╝╚══════╝╚═╝ ╚═╝',
14
+ '',
15
+ ' █████╗ ██████╗ ██████╗██╗ ██╗███████╗██████╗ ',
16
+ ' ██╔══██╗██╔══██╗██╔════╝██║ ██║██╔════╝██╔══██╗',
17
+ ' ███████║██████╔╝██║ ███████║█████╗ ██████╔╝',
18
+ ' ██╔══██║██╔══██╗██║ ██╔══██║██╔══╝ ██╔══██╗',
19
+ ' ██║ ██║██║ ██║╚██████╗██║ ██║███████╗██║ ██║',
20
+ ' ╚═╝ ╚═╝╚═╝ ╚═╝ ╚═════╝╚═╝ ╚═╝╚══════╝╚═╝ ╚═╝',
21
+ '',
20
22
  ];
21
23
 
22
- // Column boundaries for coloring:
23
- // A: cols 0-5, R: 6-13, C: 14-21, H: 22-29, E: 30-37, R: 38-45
24
- // ARCH = white, ER = emerald green
25
- const archEnd = 30; // columns 0-29 are A R C H
26
- const erEnd = 46; // columns 30-45 are E R
27
-
28
24
  for (const line of lines) {
29
- const archPart = line.slice(0, archEnd);
30
- const erPart = line.slice(archEnd, erEnd);
31
- const rest = line.slice(erEnd);
32
- process.stdout.write(colors.white(archPart) + emerald(erPart) + rest + '\n');
25
+ console.log(emerald(line));
33
26
  }
34
-
35
- // Pixel art bow and arrow (emerald green)
36
- const bowArrow = [
37
- ` ${emerald('╭───╮')}`,
38
- ` ${emerald('╱')} ${emerald('╲')}`,
39
- ` ${emerald('╱')} ${emerald('╲')}`,
40
- ` ${emerald('╱')} ${emerald('╲')}`,
41
- ` ${emerald('╱')} ${emerald('╲')}`,
42
- ` ${emerald('╱')} ${emerald('╲')}`,
43
- ` ${emerald('╱')} ${emerald('╲')}`,
44
- ` ${emerald('╱')} ${emerald('╲')}`,
45
- `${emerald('╱')} ${emerald('╲')}`,
46
- ` ${emerald('╲')} ${emerald('╱')}`,
47
- ` ${emerald('╲')} ${emerald('╱')}`,
48
- ` ${emerald('╲')} ${emerald('╱')}`,
49
- ` ${emerald('╲')} ${emerald('╱')}`,
50
- ` ${emerald('╲')} ${emerald('╱')}`,
51
- ` ${emerald('╲')} ${emerald('╱')}`,
52
- ` ${emerald('╲')} ${emerald('╱')}`,
53
- ` ${emerald('╰───╯')}`,
54
- ` ${emerald('───►')}`,
55
- ];
56
-
57
- console.log();
58
- bowArrow.forEach(line => console.log(line));
59
- console.log();
60
- console.log(emeraldDim(' v0.1.0 · event intelligence for AI agents'));
61
- console.log();
62
27
  }
63
28
 
64
29
  // ─── Status Logger ──────────────────────────────────────────
@@ -0,0 +1,26 @@
1
+ import { z } from 'zod';
2
+ import { sendCommand } from '../daemon/client.js';
3
+
4
+ // ─── Input Schema ───────────────────────────────────────────
5
+
6
+ export const UnwatchInputSchema = z.object({
7
+ watchId: z.string().min(1, 'watchId is required'),
8
+ });
9
+
10
+ export type UnwatchInput = z.infer<typeof UnwatchInputSchema>;
11
+
12
+ // ─── Execute Unwatch (via daemon IPC) ───────────────────────
13
+
14
+ export async function executeUnwatch(input: UnwatchInput): Promise<string> {
15
+ try {
16
+ const res = await sendCommand({ type: 'remove_watch', watchId: input.watchId });
17
+
18
+ if (!res.ok) {
19
+ return `❌ Failed to remove watch: ${res.error}`;
20
+ }
21
+
22
+ return `✅ Watch removed: ${input.watchId}`;
23
+ } catch (err) {
24
+ return `❌ Daemon error: ${err instanceof Error ? err.message : String(err)}\n\nIs the archer daemon running?`;
25
+ }
26
+ }
@@ -1,223 +1,57 @@
1
- import crypto from 'node:crypto';
2
- import type { RealtimeChannel } from '@supabase/supabase-js';
3
- import { WatchInputSchema, type WatchInput, type WatchResult, type PostgresEvent } from '../types/index.js';
4
- import { createAuthChannel, createTableChannel } from '../lib/supabase.js';
5
- import { fireWebhook, buildWebhookPayload } from '../lib/webhook.js';
6
- import { stderrAction, stderrSuccess, stderrError } from '../lib/ascii.js';
7
-
8
- // ─── Active Subscriptions ───────────────────────────────────
9
-
10
- interface ActiveWatch {
11
- watchId: string;
12
- channel: RealtimeChannel;
13
- event: string;
14
- table?: string;
15
- condition?: string;
16
- webhookUrl: string;
17
- }
18
-
19
- const activeWatches = new Map<string, ActiveWatch>();
20
-
21
- // ─── Condition Evaluator ────────────────────────────────────
22
-
23
- function evaluateCondition(
24
- data: Record<string, unknown>,
25
- condition: string,
26
- ): boolean {
27
- // Parse: "field operator value"
28
- // Supported: "ends with", "starts with", "contains", "equals"
29
- const operators = ['ends with', 'starts with', 'contains', 'equals'] as const;
30
-
31
- const conditionLower = condition.toLowerCase();
32
- let matchedOperator: (typeof operators)[number] | null = null;
33
- let splitIndex = -1;
34
-
35
- for (const op of operators) {
36
- const idx = conditionLower.indexOf(op);
37
- if (idx !== -1) {
38
- matchedOperator = op;
39
- splitIndex = idx;
40
- break;
41
- }
42
- }
43
-
44
- if (!matchedOperator || splitIndex === -1) {
45
- stderrError(`unknown condition format: "${condition}"`);
46
- return true; // Pass through if condition can't be parsed
47
- }
48
-
49
- const field = condition.slice(0, splitIndex).trim();
50
- const value = condition.slice(splitIndex + matchedOperator.length).trim();
51
- const fieldValue = String(data[field] ?? '');
52
-
53
- switch (matchedOperator) {
54
- case 'ends with':
55
- return fieldValue.endsWith(value);
56
- case 'starts with':
57
- return fieldValue.startsWith(value);
58
- case 'contains':
59
- return fieldValue.includes(value);
60
- case 'equals':
61
- return fieldValue === value;
62
- default:
63
- return true;
64
- }
65
- }
66
-
67
- // ─── Map Event To Postgres Event ────────────────────────────
68
-
69
- function toPostgresEvent(event: string): PostgresEvent {
70
- switch (event) {
71
- case 'table.insert': return 'INSERT';
72
- case 'table.update': return 'UPDATE';
73
- case 'table.delete': return 'DELETE';
74
- default: return 'INSERT';
75
- }
76
- }
77
-
78
- // ─── Event Handler ──────────────────────────────────────────
79
-
80
- function createEventHandler(watch: ActiveWatch) {
81
- return async (data: Record<string, unknown>) => {
82
- stderrAction(`event received → ${watch.event}${watch.table ? ` on ${watch.table}` : ''}`);
83
-
84
- // Apply condition filter
85
- if (watch.condition && !evaluateCondition(data, watch.condition)) {
86
- stderrAction(`condition not met → "${watch.condition}", skipping`);
87
- return;
88
- }
89
-
90
- // Build and fire webhook
91
- const payload = buildWebhookPayload(watch.watchId, watch.event, data);
92
- await fireWebhook({
93
- url: watch.webhookUrl,
94
- payload,
95
- event: watch.event,
96
- });
1
+ import { z } from 'zod';
2
+ import { sendCommand } from '../daemon/client.js';
3
+ import type { WatchConfig } from '../daemon/types.js';
4
+
5
+ // ─── Input Schema ───────────────────────────────────────────
6
+
7
+ export const WatchInputSchema = z.object({
8
+ table: z.string().min(1, 'table name is required'),
9
+ event: z.enum(['INSERT', 'UPDATE', 'DELETE', '*']).default('*'),
10
+ filter: z.string().optional(),
11
+ webhookUrl: z.string().url().optional(),
12
+ });
13
+
14
+ export type WatchInput = z.infer<typeof WatchInputSchema>;
15
+
16
+ // ─── Execute Watch (via daemon IPC) ─────────────────────────
17
+
18
+ export async function executeWatch(
19
+ input: WatchInput,
20
+ supabaseUrl: string,
21
+ serviceRoleKey: string,
22
+ ): Promise<string> {
23
+ const watchId = `watch-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
24
+
25
+ const watch: WatchConfig = {
26
+ id: watchId,
27
+ table: input.table,
28
+ event: input.event,
29
+ filter: input.filter,
30
+ webhookUrl: input.webhookUrl,
31
+ createdAt: new Date().toISOString(),
32
+ supabaseUrl,
33
+ supabaseKey: serviceRoleKey,
97
34
  };
98
- }
99
-
100
- // ─── Main Watch Implementation ──────────────────────────────
101
-
102
- export function executeWatch(rawInput: unknown): WatchResult {
103
- // Validate input
104
- const parseResult = WatchInputSchema.safeParse(rawInput);
105
- if (!parseResult.success) {
106
- const errors = parseResult.error.issues.map((e) => e.message).join(', ');
107
- return {
108
- success: false,
109
- watchId: '',
110
- message: `validation failed: ${errors}`,
111
- condition: null,
112
- };
113
- }
114
-
115
- const input: WatchInput = parseResult.data;
116
- const watchId = `watch_${crypto.randomUUID().slice(0, 8)}`;
117
-
118
- stderrAction(`creating watch ${watchId} for ${input.event}`);
119
35
 
120
36
  try {
121
- let channel: RealtimeChannel;
122
-
123
- if (input.event === 'auth.signup') {
124
- // Auth signup → watch auth.users table
125
- const handler = createEventHandler({
126
- watchId,
127
- channel: null as unknown as RealtimeChannel,
128
- event: input.event,
129
- condition: input.condition,
130
- webhookUrl: input.webhookUrl,
131
- });
132
-
133
- channel = createAuthChannel(watchId, handler);
134
- } else {
135
- // Table events → watch specific table
136
- const table = input.table!;
137
- const pgEvent = toPostgresEvent(input.event);
37
+ const res = await sendCommand({ type: 'add_watch', watch });
138
38
 
139
- const handler = createEventHandler({
140
- watchId,
141
- channel: null as unknown as RealtimeChannel,
142
- event: input.event,
143
- table,
144
- condition: input.condition,
145
- webhookUrl: input.webhookUrl,
146
- });
147
-
148
- channel = createTableChannel(watchId, table, pgEvent, handler);
39
+ if (!res.ok) {
40
+ return `❌ Failed to add watch: ${res.error}`;
149
41
  }
150
42
 
151
- // Store active watch
152
- const watch: ActiveWatch = {
153
- watchId,
154
- channel,
155
- event: input.event,
156
- table: input.table,
157
- condition: input.condition,
158
- webhookUrl: input.webhookUrl,
159
- };
160
-
161
- activeWatches.set(watchId, watch);
43
+ const parts = [
44
+ `✅ Watch created: ${watchId}`,
45
+ ` table: ${input.table}`,
46
+ ` event: ${input.event}`,
47
+ ];
162
48
 
163
- const tableInfo = input.table ? ` on table "${input.table}"` : '';
164
- const conditionInfo = input.condition ? ` where ${input.condition}` : '';
165
- const message = `watching ${input.event}${tableInfo}${conditionInfo} → ${input.webhookUrl}`;
49
+ if (input.filter) parts.push(` filter: ${input.filter}`);
50
+ if (input.webhookUrl) parts.push(` webhook: ${input.webhookUrl}`);
51
+ parts.push(` status: subscribed via daemon (persistent)`);
166
52
 
167
- stderrSuccess(message);
168
-
169
- return {
170
- success: true,
171
- watchId,
172
- message,
173
- condition: input.condition ?? null,
174
- };
53
+ return parts.join('\n');
175
54
  } catch (err) {
176
- const message = err instanceof Error ? err.message : String(err);
177
- stderrError(`watch failed: ${message}`);
178
-
179
- return {
180
- success: false,
181
- watchId,
182
- message: `watch failed: ${message}`,
183
- condition: input.condition ?? null,
184
- };
55
+ return `❌ Daemon error: ${err instanceof Error ? err.message : String(err)}\n\nIs the archer daemon running? Try restarting with: npx archer-wizard@latest --daemon`;
185
56
  }
186
57
  }
187
-
188
- // ─── Tool Schema (for MCP registration) ─────────────────────
189
-
190
- export const WATCH_TOOL_SCHEMA = {
191
- name: 'archer_watch',
192
- description:
193
- 'Archer is the event intelligence layer for AI agents. Watch real-time events from data sources. Monitors auth signups, table inserts, updates, and deletes. Fires a webhook when conditions are met. Agents stop waiting for you to talk to them and start acting on their own when the world moves.',
194
- inputSchema: {
195
- type: 'object' as const,
196
- properties: {
197
- source: {
198
- type: 'string' as const,
199
- enum: ['supabase'],
200
- description: 'Event source (currently only "supabase")',
201
- },
202
- event: {
203
- type: 'string' as const,
204
- enum: ['auth.signup', 'table.insert', 'table.update', 'table.delete'],
205
- description: 'Event type to watch for',
206
- },
207
- table: {
208
- type: 'string' as const,
209
- description: 'Table name (required for table.* events)',
210
- },
211
- condition: {
212
- type: 'string' as const,
213
- description:
214
- 'Optional filter like "email ends with @gmail.com". Supports: ends with, starts with, contains, equals',
215
- },
216
- webhookUrl: {
217
- type: 'string' as const,
218
- description: 'URL to receive POST notifications when events match',
219
- },
220
- },
221
- required: ['source', 'event', 'webhookUrl'],
222
- },
223
- };
@@ -0,0 +1,37 @@
1
+ import { sendCommand } from '../daemon/client.js';
2
+
3
+ // ─── Execute List Watches (via daemon IPC) ──────────────────
4
+
5
+ export async function executeListWatches(): Promise<string> {
6
+ try {
7
+ const res = await sendCommand({ type: 'list_watches' });
8
+
9
+ if (!res.ok) {
10
+ return `❌ Failed to list watches: ${res.error}`;
11
+ }
12
+
13
+ if (res.type !== 'watch_list') {
14
+ return `❌ Unexpected response type: ${res.type}`;
15
+ }
16
+
17
+ if (res.watches.length === 0) {
18
+ return '📭 No active watches.';
19
+ }
20
+
21
+ const lines = [`📋 Active watches (${res.watches.length}):\n`];
22
+
23
+ for (const w of res.watches) {
24
+ lines.push(` 🔹 ${w.id}`);
25
+ lines.push(` table: ${w.table}`);
26
+ lines.push(` event: ${w.event}`);
27
+ if (w.filter) lines.push(` filter: ${w.filter}`);
28
+ if (w.webhookUrl) lines.push(` webhook: ${w.webhookUrl}`);
29
+ lines.push(` created: ${w.createdAt}`);
30
+ lines.push('');
31
+ }
32
+
33
+ return lines.join('\n');
34
+ } catch (err) {
35
+ return `❌ Daemon error: ${err instanceof Error ? err.message : String(err)}\n\nIs the archer daemon running?`;
36
+ }
37
+ }