archer-wizard 0.1.0 → 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.
- package/README.md +200 -90
- 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 +14 -18
- 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/detector.d.ts +1 -1
- package/dist/wizard/detector.d.ts.map +1 -1
- package/dist/wizard/detector.js +14 -10
- package/dist/wizard/detector.js.map +1 -1
- package/dist/wizard/index.js +1 -1
- package/dist/wizard/index.js.map +1 -1
- package/dist/wizard/injector.js +1 -1
- package/dist/wizard/injector.js.map +1 -1
- package/dist/wizard/rules.d.ts +1 -1
- package/dist/wizard/rules.d.ts.map +1 -1
- package/dist/wizard/rules.js +24 -9
- package/dist/wizard/rules.js.map +1 -1
- package/dist/wizard/scanner.d.ts.map +1 -1
- package/dist/wizard/scanner.js +71 -0
- package/dist/wizard/scanner.js.map +1 -1
- package/package.json +3 -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 +16 -20
- package/src/tools/unwatch.ts +26 -0
- package/src/tools/watch.ts +46 -212
- package/src/tools/watches.ts +37 -0
- package/src/wizard/detector.ts +15 -10
- package/src/wizard/index.ts +1 -1
- package/src/wizard/injector.ts +1 -1
- package/src/wizard/rules.ts +24 -9
- package/src/wizard/scanner.ts +74 -0
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
|
@@ -1,33 +1,29 @@
|
|
|
1
1
|
import pc from 'picocolors';
|
|
2
2
|
|
|
3
|
+
// Force colors for better visibility
|
|
4
|
+
const colors = pc.createColors(true);
|
|
5
|
+
|
|
6
|
+
// Use picocolors green (more compatible than true color)
|
|
7
|
+
const emerald = colors.green;
|
|
8
|
+
const emeraldDim = colors.dim;
|
|
9
|
+
|
|
3
10
|
// ─── ASCII Art ──────────────────────────────────────────────
|
|
4
11
|
|
|
5
12
|
export function showAsciiArt(): void {
|
|
6
13
|
const lines = [
|
|
7
|
-
'
|
|
8
|
-
'
|
|
9
|
-
'
|
|
10
|
-
'
|
|
11
|
-
'
|
|
12
|
-
'
|
|
14
|
+
'',
|
|
15
|
+
' █████╗ ██████╗ ██████╗██╗ ██╗███████╗██████╗ ',
|
|
16
|
+
' ██╔══██╗██╔══██╗██╔════╝██║ ██║██╔════╝██╔══██╗',
|
|
17
|
+
' ███████║██████╔╝██║ ███████║█████╗ ██████╔╝',
|
|
18
|
+
' ██╔══██║██╔══██╗██║ ██╔══██║██╔══╝ ██╔══██╗',
|
|
19
|
+
' ██║ ██║██║ ██║╚██████╗██║ ██║███████╗██║ ██║',
|
|
20
|
+
' ╚═╝ ╚═╝╚═╝ ╚═╝ ╚═════╝╚═╝ ╚═╝╚══════╝╚═╝ ╚═╝',
|
|
21
|
+
'',
|
|
13
22
|
];
|
|
14
23
|
|
|
15
|
-
// Column boundaries for coloring:
|
|
16
|
-
// A: cols 0-5, R: 6-13, C: 14-21, H: 22-29, E: 30-37, R: 38-45
|
|
17
|
-
// ARCH = white, ER = green
|
|
18
|
-
const archEnd = 30; // columns 0-29 are A R C H
|
|
19
|
-
const erEnd = 46; // columns 30-45 are E R
|
|
20
|
-
|
|
21
24
|
for (const line of lines) {
|
|
22
|
-
|
|
23
|
-
const erPart = line.slice(archEnd, erEnd);
|
|
24
|
-
const rest = line.slice(erEnd);
|
|
25
|
-
process.stdout.write(pc.white(archPart) + pc.green(erPart) + rest + '\n');
|
|
25
|
+
console.log(emerald(line));
|
|
26
26
|
}
|
|
27
|
-
|
|
28
|
-
console.log();
|
|
29
|
-
console.log(pc.dim(' v0.1.0 · event intelligence for AI agents'));
|
|
30
|
-
console.log();
|
|
31
27
|
}
|
|
32
28
|
|
|
33
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
|
-
'Watch real-time events from Supabase. Monitors auth signups, table inserts, updates, and deletes. Fires a webhook when conditions are met.',
|
|
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
|
+
}
|
package/src/wizard/detector.ts
CHANGED
|
@@ -106,9 +106,10 @@ export function detectAgents(): AgentInfo[] {
|
|
|
106
106
|
const configExists = fileExists(configPath);
|
|
107
107
|
const dirExists = parentDirExists(configPath);
|
|
108
108
|
|
|
109
|
-
// Agent is "installed" if
|
|
110
|
-
//
|
|
111
|
-
|
|
109
|
+
// Agent is "installed" if:
|
|
110
|
+
// 1. The config file exists, OR
|
|
111
|
+
// 2. The parent directory exists (for first-time setup)
|
|
112
|
+
if (configExists || dirExists) {
|
|
112
113
|
detected.push({
|
|
113
114
|
name: agent.name,
|
|
114
115
|
installed: true,
|
|
@@ -133,19 +134,23 @@ export function getConfigKey(agentName: string): 'mcpServers' | 'mcp' {
|
|
|
133
134
|
|
|
134
135
|
// ─── Get Rules Path For Agent ───────────────────────────────
|
|
135
136
|
|
|
136
|
-
export function getRulesPath(agentName: string
|
|
137
|
+
export function getRulesPath(agentName: string): string {
|
|
138
|
+
const homeDir = os.homedir();
|
|
139
|
+
|
|
137
140
|
switch (agentName) {
|
|
138
141
|
case 'cursor':
|
|
139
|
-
return path.join(
|
|
142
|
+
return path.join(homeDir, '.cursor', 'rules', 'archer.mdc');
|
|
140
143
|
case 'claude-code':
|
|
141
|
-
|
|
144
|
+
// For Claude Code, we'll use the project's CLAUDE.md
|
|
145
|
+
// but we need to get the current working directory
|
|
146
|
+
return path.join(process.cwd(), 'CLAUDE.md');
|
|
142
147
|
case 'opencode':
|
|
143
|
-
return path.join(
|
|
148
|
+
return path.join(homeDir, '.config', 'opencode', 'rules.md');
|
|
144
149
|
case 'antigravity':
|
|
145
|
-
return path.join(
|
|
150
|
+
return path.join(homeDir, '.config', 'antigravity', 'rules.md');
|
|
146
151
|
case 'windsurf':
|
|
147
|
-
return path.join(
|
|
152
|
+
return path.join(homeDir, '.codeium', 'windsurf', 'rules.md');
|
|
148
153
|
default:
|
|
149
|
-
return path.join(
|
|
154
|
+
return path.join(homeDir, '.archer', 'rules.md');
|
|
150
155
|
}
|
|
151
156
|
}
|
package/src/wizard/index.ts
CHANGED
package/src/wizard/injector.ts
CHANGED
|
@@ -11,7 +11,7 @@ import type { AgentInfo, InjectionResult, McpServerEntry } from '../types/index.
|
|
|
11
11
|
function buildArcherEntry(supabaseUrl: string, serviceRoleKey: string): McpServerEntry {
|
|
12
12
|
return {
|
|
13
13
|
command: 'npx',
|
|
14
|
-
args: ['-y', 'archer', '--mcp'],
|
|
14
|
+
args: ['-y', 'archer-wizard@latest', '--mcp'],
|
|
15
15
|
env: {
|
|
16
16
|
SUPABASE_URL: supabaseUrl,
|
|
17
17
|
SUPABASE_SERVICE_ROLE_KEY: serviceRoleKey,
|