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.
- package/dist/daemon/client.d.ts +4 -0
- package/dist/daemon/client.d.ts.map +1 -0
- package/dist/daemon/client.js +44 -0
- package/dist/daemon/client.js.map +1 -0
- package/dist/daemon/lifecycle.d.ts +9 -0
- package/dist/daemon/lifecycle.d.ts.map +1 -0
- package/dist/daemon/lifecycle.js +118 -0
- package/dist/daemon/lifecycle.js.map +1 -0
- package/dist/daemon/process.d.ts +2 -0
- package/dist/daemon/process.d.ts.map +1 -0
- package/dist/daemon/process.js +171 -0
- package/dist/daemon/process.js.map +1 -0
- package/dist/daemon/run.d.ts +2 -0
- package/dist/daemon/run.d.ts.map +1 -0
- package/dist/daemon/run.js +5 -0
- package/dist/daemon/run.js.map +1 -0
- package/dist/daemon/store.d.ts +9 -0
- package/dist/daemon/store.d.ts.map +1 -0
- package/dist/daemon/store.js +53 -0
- package/dist/daemon/store.js.map +1 -0
- package/dist/daemon/types.d.ts +47 -0
- package/dist/daemon/types.d.ts.map +1 -0
- package/dist/daemon/types.js +10 -0
- package/dist/daemon/types.js.map +1 -0
- package/dist/index.js +105 -21
- package/dist/index.js.map +1 -1
- package/dist/lib/ascii.d.ts.map +1 -1
- package/dist/lib/ascii.js +9 -41
- package/dist/lib/ascii.js.map +1 -1
- package/dist/tools/unwatch.d.ts +7 -0
- package/dist/tools/unwatch.d.ts.map +1 -0
- package/dist/tools/unwatch.js +20 -0
- package/dist/tools/unwatch.js.map +1 -0
- package/dist/tools/watch.d.ts +14 -34
- package/dist/tools/watch.d.ts.map +1 -1
- package/dist/tools/watch.js +36 -170
- package/dist/tools/watch.js.map +1 -1
- package/dist/tools/watches.d.ts +2 -0
- package/dist/tools/watches.d.ts.map +1 -0
- package/dist/tools/watches.js +33 -0
- package/dist/tools/watches.js.map +1 -0
- package/dist/wizard/index.d.ts.map +1 -1
- package/dist/wizard/index.js +57 -23
- package/dist/wizard/index.js.map +1 -1
- package/dist/wizard/scanner.d.ts.map +1 -1
- package/dist/wizard/scanner.js +0 -23
- package/dist/wizard/scanner.js.map +1 -1
- package/package.json +2 -1
- package/src/daemon/client.ts +50 -0
- package/src/daemon/lifecycle.ts +126 -0
- package/src/daemon/process.ts +207 -0
- package/src/daemon/run.ts +6 -0
- package/src/daemon/store.ts +60 -0
- package/src/daemon/types.ts +41 -0
- package/src/index.ts +111 -22
- package/src/lib/ascii.ts +9 -44
- package/src/tools/unwatch.ts +26 -0
- package/src/tools/watch.ts +46 -212
- package/src/tools/watches.ts +37 -0
- package/src/wizard/index.ts +54 -24
- 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 (
|
|
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,
|
|
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.
|
|
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: [
|
|
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
|
-
|
|
44
|
-
|
|
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
|
-
|
|
49
|
-
|
|
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
|
-
|
|
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
|
+
}
|
package/src/tools/watch.ts
CHANGED
|
@@ -1,223 +1,57 @@
|
|
|
1
|
-
import
|
|
2
|
-
import
|
|
3
|
-
import
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
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
|
-
|
|
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
|
-
|
|
140
|
-
|
|
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
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
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
|
-
|
|
164
|
-
|
|
165
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
+
}
|