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.
Files changed (74) hide show
  1. package/README.md +200 -90
  2. package/dist/daemon/client.d.ts +4 -0
  3. package/dist/daemon/client.d.ts.map +1 -0
  4. package/dist/daemon/client.js +44 -0
  5. package/dist/daemon/client.js.map +1 -0
  6. package/dist/daemon/lifecycle.d.ts +9 -0
  7. package/dist/daemon/lifecycle.d.ts.map +1 -0
  8. package/dist/daemon/lifecycle.js +118 -0
  9. package/dist/daemon/lifecycle.js.map +1 -0
  10. package/dist/daemon/process.d.ts +2 -0
  11. package/dist/daemon/process.d.ts.map +1 -0
  12. package/dist/daemon/process.js +171 -0
  13. package/dist/daemon/process.js.map +1 -0
  14. package/dist/daemon/run.d.ts +2 -0
  15. package/dist/daemon/run.d.ts.map +1 -0
  16. package/dist/daemon/run.js +5 -0
  17. package/dist/daemon/run.js.map +1 -0
  18. package/dist/daemon/store.d.ts +9 -0
  19. package/dist/daemon/store.d.ts.map +1 -0
  20. package/dist/daemon/store.js +53 -0
  21. package/dist/daemon/store.js.map +1 -0
  22. package/dist/daemon/types.d.ts +47 -0
  23. package/dist/daemon/types.d.ts.map +1 -0
  24. package/dist/daemon/types.js +10 -0
  25. package/dist/daemon/types.js.map +1 -0
  26. package/dist/index.js +105 -21
  27. package/dist/index.js.map +1 -1
  28. package/dist/lib/ascii.d.ts.map +1 -1
  29. package/dist/lib/ascii.js +14 -18
  30. package/dist/lib/ascii.js.map +1 -1
  31. package/dist/tools/unwatch.d.ts +7 -0
  32. package/dist/tools/unwatch.d.ts.map +1 -0
  33. package/dist/tools/unwatch.js +20 -0
  34. package/dist/tools/unwatch.js.map +1 -0
  35. package/dist/tools/watch.d.ts +14 -34
  36. package/dist/tools/watch.d.ts.map +1 -1
  37. package/dist/tools/watch.js +36 -170
  38. package/dist/tools/watch.js.map +1 -1
  39. package/dist/tools/watches.d.ts +2 -0
  40. package/dist/tools/watches.d.ts.map +1 -0
  41. package/dist/tools/watches.js +33 -0
  42. package/dist/tools/watches.js.map +1 -0
  43. package/dist/wizard/detector.d.ts +1 -1
  44. package/dist/wizard/detector.d.ts.map +1 -1
  45. package/dist/wizard/detector.js +14 -10
  46. package/dist/wizard/detector.js.map +1 -1
  47. package/dist/wizard/index.js +1 -1
  48. package/dist/wizard/index.js.map +1 -1
  49. package/dist/wizard/injector.js +1 -1
  50. package/dist/wizard/injector.js.map +1 -1
  51. package/dist/wizard/rules.d.ts +1 -1
  52. package/dist/wizard/rules.d.ts.map +1 -1
  53. package/dist/wizard/rules.js +24 -9
  54. package/dist/wizard/rules.js.map +1 -1
  55. package/dist/wizard/scanner.d.ts.map +1 -1
  56. package/dist/wizard/scanner.js +71 -0
  57. package/dist/wizard/scanner.js.map +1 -1
  58. package/package.json +3 -1
  59. package/src/daemon/client.ts +50 -0
  60. package/src/daemon/lifecycle.ts +126 -0
  61. package/src/daemon/process.ts +207 -0
  62. package/src/daemon/run.ts +6 -0
  63. package/src/daemon/store.ts +60 -0
  64. package/src/daemon/types.ts +41 -0
  65. package/src/index.ts +111 -22
  66. package/src/lib/ascii.ts +16 -20
  67. package/src/tools/unwatch.ts +26 -0
  68. package/src/tools/watch.ts +46 -212
  69. package/src/tools/watches.ts +37 -0
  70. package/src/wizard/detector.ts +15 -10
  71. package/src/wizard/index.ts +1 -1
  72. package/src/wizard/injector.ts +1 -1
  73. package/src/wizard/rules.ts +24 -9
  74. 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 (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
@@ -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
- const archPart = line.slice(0, archEnd);
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
+ }
@@ -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
- '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
+ }
@@ -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 its parent directory exists
110
- // (the config file might not exist yet)
111
- if (dirExists) {
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, cwd: string): 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(cwd, '.cursor', 'rules', 'archer.mdc');
142
+ return path.join(homeDir, '.cursor', 'rules', 'archer.mdc');
140
143
  case 'claude-code':
141
- return path.join(cwd, 'CLAUDE.md');
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(cwd, '.opencode', 'rules.md');
148
+ return path.join(homeDir, '.config', 'opencode', 'rules.md');
144
149
  case 'antigravity':
145
- return path.join(cwd, '.antigravity', 'rules.md');
150
+ return path.join(homeDir, '.config', 'antigravity', 'rules.md');
146
151
  case 'windsurf':
147
- return path.join(cwd, '.windsurf', 'rules.md');
152
+ return path.join(homeDir, '.codeium', 'windsurf', 'rules.md');
148
153
  default:
149
- return path.join(cwd, '.archer', 'rules.md');
154
+ return path.join(homeDir, '.archer', 'rules.md');
150
155
  }
151
156
  }
@@ -58,7 +58,7 @@ export async function runWizard(): Promise<void> {
58
58
  // ─ Step 13: Inject rules ─
59
59
  if (successfulAgents.length > 0) {
60
60
  logAction('writing agent rules...');
61
- injectRules(successfulAgents, cwd);
61
+ injectRules(successfulAgents);
62
62
  console.log();
63
63
  }
64
64
 
@@ -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,