archer-wizard 0.1.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 (59) hide show
  1. package/.env.example +6 -0
  2. package/README.md +140 -0
  3. package/dist/index.d.ts +3 -0
  4. package/dist/index.d.ts.map +1 -0
  5. package/dist/index.js +77 -0
  6. package/dist/index.js.map +1 -0
  7. package/dist/lib/ascii.d.ts +14 -0
  8. package/dist/lib/ascii.d.ts.map +1 -0
  9. package/dist/lib/ascii.js +88 -0
  10. package/dist/lib/ascii.js.map +1 -0
  11. package/dist/lib/supabase.d.ts +7 -0
  12. package/dist/lib/supabase.d.ts.map +1 -0
  13. package/dist/lib/supabase.js +54 -0
  14. package/dist/lib/supabase.js.map +1 -0
  15. package/dist/lib/webhook.d.ts +4 -0
  16. package/dist/lib/webhook.d.ts.map +1 -0
  17. package/dist/lib/webhook.js +53 -0
  18. package/dist/lib/webhook.js.map +1 -0
  19. package/dist/tools/watch.d.ts +35 -0
  20. package/dist/tools/watch.d.ts.map +1 -0
  21. package/dist/tools/watch.js +178 -0
  22. package/dist/tools/watch.js.map +1 -0
  23. package/dist/types/index.d.ts +84 -0
  24. package/dist/types/index.d.ts.map +1 -0
  25. package/dist/types/index.js +24 -0
  26. package/dist/types/index.js.map +1 -0
  27. package/dist/wizard/detector.d.ts +5 -0
  28. package/dist/wizard/detector.d.ts.map +1 -0
  29. package/dist/wizard/detector.js +125 -0
  30. package/dist/wizard/detector.js.map +1 -0
  31. package/dist/wizard/index.d.ts +2 -0
  32. package/dist/wizard/index.d.ts.map +1 -0
  33. package/dist/wizard/index.js +50 -0
  34. package/dist/wizard/index.js.map +1 -0
  35. package/dist/wizard/injector.d.ts +3 -0
  36. package/dist/wizard/injector.d.ts.map +1 -0
  37. package/dist/wizard/injector.js +91 -0
  38. package/dist/wizard/injector.js.map +1 -0
  39. package/dist/wizard/rules.d.ts +3 -0
  40. package/dist/wizard/rules.d.ts.map +1 -0
  41. package/dist/wizard/rules.js +94 -0
  42. package/dist/wizard/rules.js.map +1 -0
  43. package/dist/wizard/scanner.d.ts +7 -0
  44. package/dist/wizard/scanner.d.ts.map +1 -0
  45. package/dist/wizard/scanner.js +201 -0
  46. package/dist/wizard/scanner.js.map +1 -0
  47. package/package.json +28 -0
  48. package/src/index.ts +90 -0
  49. package/src/lib/ascii.ts +109 -0
  50. package/src/lib/supabase.ts +78 -0
  51. package/src/lib/webhook.ts +69 -0
  52. package/src/tools/watch.ts +223 -0
  53. package/src/types/index.ts +120 -0
  54. package/src/wizard/detector.ts +151 -0
  55. package/src/wizard/index.ts +69 -0
  56. package/src/wizard/injector.ts +115 -0
  57. package/src/wizard/rules.ts +112 -0
  58. package/src/wizard/scanner.ts +250 -0
  59. package/tsconfig.json +20 -0
@@ -0,0 +1,69 @@
1
+ import { stderrAction, stderrSuccess, stderrError } from './ascii.js';
2
+ import type { WebhookOptions, WebhookPayload } from '../types/index.js';
3
+
4
+ // ─── Retry Config ───────────────────────────────────────────
5
+
6
+ const MAX_RETRIES = 3;
7
+ const RETRY_DELAY_MS = 2000;
8
+
9
+ function sleep(ms: number): Promise<void> {
10
+ return new Promise((resolve) => setTimeout(resolve, ms));
11
+ }
12
+
13
+ // ─── Fire Webhook ───────────────────────────────────────────
14
+
15
+ export async function fireWebhook(options: WebhookOptions): Promise<boolean> {
16
+ const { url, payload, event } = options;
17
+
18
+ for (let attempt = 1; attempt <= MAX_RETRIES; attempt++) {
19
+ try {
20
+ stderrAction(`webhook → ${url} (attempt ${attempt}/${MAX_RETRIES})`);
21
+
22
+ const response = await fetch(url, {
23
+ method: 'POST',
24
+ headers: {
25
+ 'Content-Type': 'application/json',
26
+ 'User-Agent': 'Archer/0.1.0',
27
+ 'X-Archer-Event': event,
28
+ },
29
+ body: JSON.stringify(payload),
30
+ });
31
+
32
+ if (response.ok) {
33
+ stderrSuccess(`webhook delivered → ${response.status}`);
34
+ return true;
35
+ }
36
+
37
+ stderrError(`webhook failed → ${response.status} ${response.statusText}`);
38
+ } catch (err) {
39
+ const message = err instanceof Error ? err.message : String(err);
40
+ stderrError(`webhook error → ${message}`);
41
+ }
42
+
43
+ if (attempt < MAX_RETRIES) {
44
+ stderrAction(`retrying in ${RETRY_DELAY_MS / 1000}s...`);
45
+ await sleep(RETRY_DELAY_MS);
46
+ }
47
+ }
48
+
49
+ stderrError(`webhook exhausted all ${MAX_RETRIES} retries`);
50
+ return false;
51
+ }
52
+
53
+ // ─── Build Payload ──────────────────────────────────────────
54
+
55
+ export function buildWebhookPayload(
56
+ watchId: string,
57
+ event: string,
58
+ data: Record<string, unknown>,
59
+ ): WebhookPayload {
60
+ return {
61
+ archer: {
62
+ watchId,
63
+ event,
64
+ source: 'supabase',
65
+ firedAt: new Date().toISOString(),
66
+ },
67
+ data,
68
+ };
69
+ }
@@ -0,0 +1,223 @@
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
+ });
97
+ };
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
+
120
+ 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);
138
+
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);
149
+ }
150
+
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);
162
+
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}`;
166
+
167
+ stderrSuccess(message);
168
+
169
+ return {
170
+ success: true,
171
+ watchId,
172
+ message,
173
+ condition: input.condition ?? null,
174
+ };
175
+ } 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
+ };
185
+ }
186
+ }
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,120 @@
1
+ import { z } from 'zod';
2
+
3
+ // ─── Framework Detection ────────────────────────────────────
4
+
5
+ export type Framework = 'nextjs' | 'vite' | 'unknown';
6
+
7
+ // ─── Scanner Types ──────────────────────────────────────────
8
+
9
+ export interface ScanResult {
10
+ supabaseUrl: string | null;
11
+ serviceRoleKey: string | null;
12
+ anonKey: string | null;
13
+ framework: Framework;
14
+ hasSupabaseInstalled: boolean;
15
+ foundInFile: string | null;
16
+ }
17
+
18
+ export interface ValidatedCredentials {
19
+ supabaseUrl: string;
20
+ serviceRoleKey: string;
21
+ anonKey: string | null;
22
+ }
23
+
24
+ // ─── Agent Detection ────────────────────────────────────────
25
+
26
+ export interface AgentInfo {
27
+ name: string;
28
+ installed: boolean;
29
+ configPath: string;
30
+ configExists: boolean;
31
+ }
32
+
33
+ export type AgentName = 'cursor' | 'claude-code' | 'opencode' | 'antigravity' | 'windsurf';
34
+
35
+ export interface AgentConfigFormat {
36
+ name: AgentName;
37
+ displayName: string;
38
+ configKey: 'mcpServers' | 'mcp';
39
+ rulesPath: (cwd: string) => string;
40
+ }
41
+
42
+ // ─── Watch Tool Types ───────────────────────────────────────
43
+
44
+ export const WatchEventSchema = z.enum([
45
+ 'auth.signup',
46
+ 'table.insert',
47
+ 'table.update',
48
+ 'table.delete',
49
+ ]);
50
+
51
+ export type WatchEvent = z.infer<typeof WatchEventSchema>;
52
+
53
+ export const WatchInputSchema = z.object({
54
+ source: z.literal('supabase'),
55
+ event: WatchEventSchema,
56
+ table: z.string().optional(),
57
+ condition: z.string().optional(),
58
+ webhookUrl: z.string().url('webhookUrl must be a valid URL'),
59
+ }).refine(
60
+ (data) => {
61
+ if (data.event !== 'auth.signup' && !data.table) {
62
+ return false;
63
+ }
64
+ return true;
65
+ },
66
+ {
67
+ message: 'table is required when event is not auth.signup',
68
+ path: ['table'],
69
+ }
70
+ );
71
+
72
+ export type WatchInput = z.infer<typeof WatchInputSchema>;
73
+
74
+ export interface WatchResult {
75
+ success: boolean;
76
+ watchId: string;
77
+ message: string;
78
+ condition: string | null;
79
+ }
80
+
81
+ // ─── Webhook Types ──────────────────────────────────────────
82
+
83
+ export interface WebhookPayload {
84
+ archer: {
85
+ watchId: string;
86
+ event: string;
87
+ source: 'supabase';
88
+ firedAt: string;
89
+ };
90
+ data: Record<string, unknown>;
91
+ }
92
+
93
+ export interface WebhookOptions {
94
+ url: string;
95
+ payload: WebhookPayload;
96
+ event: string;
97
+ }
98
+
99
+ // ─── Supabase Realtime Types ────────────────────────────────
100
+
101
+ export type PostgresEvent = 'INSERT' | 'UPDATE' | 'DELETE';
102
+
103
+ export interface RealtimeChannelConfig {
104
+ table: string;
105
+ event: PostgresEvent;
106
+ }
107
+
108
+ // ─── Injection Types ────────────────────────────────────────
109
+
110
+ export interface InjectionResult {
111
+ agent: string;
112
+ success: boolean;
113
+ error?: string;
114
+ }
115
+
116
+ export interface McpServerEntry {
117
+ command: string;
118
+ args: string[];
119
+ env: Record<string, string>;
120
+ }
@@ -0,0 +1,151 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import os from 'node:os';
4
+ import type { AgentInfo } from '../types/index.js';
5
+
6
+ // ─── Agent Config Paths ─────────────────────────────────────
7
+
8
+ interface AgentPathConfig {
9
+ name: string;
10
+ paths: {
11
+ darwin: string;
12
+ linux: string;
13
+ win32: string;
14
+ };
15
+ }
16
+
17
+ const AGENTS: AgentPathConfig[] = [
18
+ {
19
+ name: 'cursor',
20
+ paths: {
21
+ darwin: path.join(os.homedir(), '.cursor', 'mcp.json'),
22
+ linux: path.join(os.homedir(), '.cursor', 'mcp.json'),
23
+ win32: path.join(process.env['APPDATA'] ?? os.homedir(), 'Cursor', 'mcp.json'),
24
+ },
25
+ },
26
+ {
27
+ name: 'claude-code',
28
+ paths: {
29
+ darwin: path.join(os.homedir(), 'Library', 'Application Support', 'Claude', 'claude_desktop_config.json'),
30
+ linux: path.join(os.homedir(), '.config', 'claude', 'claude_desktop_config.json'),
31
+ win32: path.join(process.env['APPDATA'] ?? os.homedir(), 'Claude', 'claude_desktop_config.json'),
32
+ },
33
+ },
34
+ {
35
+ name: 'opencode',
36
+ paths: {
37
+ darwin: path.join(os.homedir(), '.config', 'opencode', 'config.json'),
38
+ linux: path.join(os.homedir(), '.config', 'opencode', 'config.json'),
39
+ win32: path.join(process.env['APPDATA'] ?? os.homedir(), 'opencode', 'config.json'),
40
+ },
41
+ },
42
+ {
43
+ name: 'antigravity',
44
+ paths: {
45
+ darwin: path.join(os.homedir(), '.config', 'antigravity', 'config.json'),
46
+ linux: path.join(os.homedir(), '.config', 'antigravity', 'config.json'),
47
+ win32: path.join(process.env['APPDATA'] ?? os.homedir(), 'Antigravity', 'config.json'),
48
+ },
49
+ },
50
+ {
51
+ name: 'windsurf',
52
+ paths: {
53
+ darwin: path.join(os.homedir(), '.codeium', 'windsurf', 'mcp_config.json'),
54
+ linux: path.join(os.homedir(), '.codeium', 'windsurf', 'mcp_config.json'),
55
+ win32: path.join(process.env['APPDATA'] ?? os.homedir(), 'Windsurf', 'mcp_config.json'),
56
+ },
57
+ },
58
+ ];
59
+
60
+ // ─── Platform Detection ─────────────────────────────────────
61
+
62
+ function getPlatform(): 'darwin' | 'linux' | 'win32' {
63
+ const platform = os.platform();
64
+ if (platform === 'darwin' || platform === 'linux' || platform === 'win32') {
65
+ return platform;
66
+ }
67
+ // Default to linux for other Unix-like systems
68
+ return 'linux';
69
+ }
70
+
71
+ // ─── Get Config Path For Agent ──────────────────────────────
72
+
73
+ function getConfigPath(agent: AgentPathConfig): string {
74
+ const platform = getPlatform();
75
+ return agent.paths[platform];
76
+ }
77
+
78
+ // ─── Check If Path Exists ───────────────────────────────────
79
+
80
+ function fileExists(filePath: string): boolean {
81
+ try {
82
+ // Check if the file itself OR its parent directory exists
83
+ // The config file might not exist yet, but the parent app directory should
84
+ return fs.existsSync(filePath);
85
+ } catch {
86
+ return false;
87
+ }
88
+ }
89
+
90
+ function parentDirExists(filePath: string): boolean {
91
+ try {
92
+ const dir = path.dirname(filePath);
93
+ return fs.existsSync(dir);
94
+ } catch {
95
+ return false;
96
+ }
97
+ }
98
+
99
+ // ─── Detect Installed Agents ────────────────────────────────
100
+
101
+ export function detectAgents(): AgentInfo[] {
102
+ const detected: AgentInfo[] = [];
103
+
104
+ for (const agent of AGENTS) {
105
+ const configPath = getConfigPath(agent);
106
+ const configExists = fileExists(configPath);
107
+ const dirExists = parentDirExists(configPath);
108
+
109
+ // Agent is "installed" if its parent directory exists
110
+ // (the config file might not exist yet)
111
+ if (dirExists) {
112
+ detected.push({
113
+ name: agent.name,
114
+ installed: true,
115
+ configPath,
116
+ configExists,
117
+ });
118
+ }
119
+ }
120
+
121
+ return detected;
122
+ }
123
+
124
+ // ─── Get Config Key For Agent ───────────────────────────────
125
+
126
+ export function getConfigKey(agentName: string): 'mcpServers' | 'mcp' {
127
+ if (agentName === 'opencode') {
128
+ return 'mcp';
129
+ }
130
+ // cursor, claude-code, antigravity, windsurf all use mcpServers
131
+ return 'mcpServers';
132
+ }
133
+
134
+ // ─── Get Rules Path For Agent ───────────────────────────────
135
+
136
+ export function getRulesPath(agentName: string, cwd: string): string {
137
+ switch (agentName) {
138
+ case 'cursor':
139
+ return path.join(cwd, '.cursor', 'rules', 'archer.mdc');
140
+ case 'claude-code':
141
+ return path.join(cwd, 'CLAUDE.md');
142
+ case 'opencode':
143
+ return path.join(cwd, '.opencode', 'rules.md');
144
+ case 'antigravity':
145
+ return path.join(cwd, '.antigravity', 'rules.md');
146
+ case 'windsurf':
147
+ return path.join(cwd, '.windsurf', 'rules.md');
148
+ default:
149
+ return path.join(cwd, '.archer', 'rules.md');
150
+ }
151
+ }
@@ -0,0 +1,69 @@
1
+ import * as clack from '@clack/prompts';
2
+ import pc from 'picocolors';
3
+ import { showAsciiArt, logAction, logSuccess, showSuccessBox } from '../lib/ascii.js';
4
+ import { scanProject, promptForMissing } from './scanner.js';
5
+ import { detectAgents } from './detector.js';
6
+ import { injectIntoAgents } from './injector.js';
7
+ import { injectRules } from './rules.js';
8
+
9
+ // ─── Wizard Sequence ────────────────────────────────────────
10
+
11
+ export async function runWizard(): Promise<void> {
12
+ const cwd = process.cwd();
13
+
14
+ // ─ Step 1: ASCII Art ─
15
+ showAsciiArt();
16
+
17
+ // ─ Step 2: Start clack ─
18
+ clack.intro(pc.bgGreen(pc.black(' archer setup ')));
19
+
20
+ // ─ Step 3: Scanning ─
21
+ logAction('scanning project for Supabase credentials...');
22
+ console.log();
23
+
24
+ // ─ Step 4-6: Scan env files ─
25
+ const scan = await scanProject(cwd);
26
+ console.log();
27
+
28
+ // ─ Step 7: Framework detection ─
29
+ if (scan.framework !== 'unknown') {
30
+ logSuccess(`detected ${pc.bold(scan.framework)} project`);
31
+ }
32
+ if (scan.hasSupabaseInstalled) {
33
+ logSuccess('found @supabase/supabase-js');
34
+ }
35
+
36
+ // ─ Step 8: Prompt for missing credentials ─
37
+ const credentials = await promptForMissing(scan);
38
+ console.log();
39
+
40
+ // ─ Step 9: Detect agents ─
41
+ logAction('detecting AI agents...');
42
+ const agents = detectAgents();
43
+ console.log();
44
+
45
+ // ─ Step 10-11: Inject into agents ─
46
+ const injectionResults = await injectIntoAgents(
47
+ agents,
48
+ credentials.supabaseUrl,
49
+ credentials.serviceRoleKey,
50
+ );
51
+ console.log();
52
+
53
+ // ─ Step 12: Filter successful injections ─
54
+ const successfulAgents = agents.filter((a) =>
55
+ injectionResults.some((r) => r.agent === a.name && r.success),
56
+ );
57
+
58
+ // ─ Step 13: Inject rules ─
59
+ if (successfulAgents.length > 0) {
60
+ logAction('writing agent rules...');
61
+ injectRules(successfulAgents, cwd);
62
+ console.log();
63
+ }
64
+
65
+ // ─ Step 14: Success ─
66
+ showSuccessBox(successfulAgents.length);
67
+
68
+ clack.outro(pc.dim('docs → github.com/archer-mcp'));
69
+ }