@arcteninc/core 0.0.139 → 0.0.140

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.
@@ -1,968 +1,648 @@
1
1
  #!/usr/bin/env node
2
2
  /**
3
- * Arcten Init Wizard - Interactive setup for AI agent integration
3
+ * Arcten Init - Complete End-to-End Setup
4
4
  *
5
- * Features:
6
- * - Scans codebase for functions
7
- * - Estimates analysis cost
8
- * - Optional AI analysis for tool recommendations
9
- * - Syncs configuration to dashboard
10
- * - Generates local setup files
5
+ * Creates a FULLY WORKING setup with zero manual steps:
6
+ * 1. Deterministic discovery (tree-sitter scan)
7
+ * 2. Deterministic classification (AI API + pattern fallback)
8
+ * 3. Placement menu (user choice)
9
+ * 4. Deterministic file creation (templates)
10
+ * 5. AI review (checks generated files)
11
+ * 6. Run sync (generates arcten.tools.ts)
11
12
  */
12
13
 
13
14
  import * as fs from 'fs';
14
15
  import * as path from 'path';
15
- import { glob } from 'glob';
16
16
  import * as readline from 'readline';
17
- import { autoDiscoverAndExtract } from './cli-extract-types-auto.ts';
17
+ import { execSync } from 'child_process';
18
+ import { discoverFunctionsInDirectory, filterLikelyTools, type DiscoveredFunction } from './tree-sitter-discover.js';
18
19
 
19
20
  // Types
20
- interface FunctionMetadata {
21
- name: string;
22
- file: string;
23
- params: any;
24
- returnType?: string;
25
- jsDoc?: string;
26
- sourceCode?: string;
21
+ interface InitContext {
22
+ projectRoot: string;
23
+ framework: 'nextjs-app' | 'nextjs-pages' | 'vite' | 'unknown';
24
+ toolsFile: string;
25
+ toolsFileRelative: string;
26
+ functions: DiscoveredFunction[];
27
+ apiKey: string;
28
+ projectId: string;
29
+ serverUrl: string;
27
30
  }
28
31
 
29
- interface CodebaseContext {
30
- fileTree: string;
31
- framework: {
32
- name: string;
33
- version?: string;
34
- type: 'nextjs-app' | 'nextjs-pages' | 'vite' | 'cra' | 'unknown';
35
- };
36
- dependencies: Record<string, string>;
37
- functions: FunctionMetadata[];
38
- stats: {
39
- totalFunctions: number;
40
- skippedPaths: string[];
41
- };
42
- }
43
-
44
- interface AnalysisEstimate {
45
- success: boolean;
46
- estimatedTokens: number;
47
- estimatedCost: number;
48
- functionCount: number;
49
- chunkCount: number;
50
- willBeChunked: boolean;
51
- currency: string;
52
- message: string;
53
- }
54
-
55
- interface AnalysisResult {
56
- success: boolean;
57
- tokensUsed: number;
58
- inputTokens: number;
59
- outputTokens: number;
60
- recommendations: ToolRecommendation[];
61
- suggestedAgents: AgentConfig[];
62
- suggestedPlacements: PlacementSuggestion[];
63
- stats: any;
32
+ interface Classification {
33
+ safe: string[];
34
+ sensitive: string[];
35
+ source: 'ai' | 'pattern-fallback';
64
36
  }
65
37
 
66
- interface ToolRecommendation {
67
- name: string;
68
- category: 'data-read' | 'data-write' | 'action' | 'query';
69
- description: string;
70
- shouldEnable: boolean;
71
- requiresApproval: boolean;
72
- sensitiveParams?: string[];
73
- suggestedAgents?: string[];
74
- }
38
+ type Placement = 'everywhere' | 'specific' | 'developer';
75
39
 
76
- interface AgentConfig {
77
- name: string;
78
- systemPrompt: string;
79
- enabledTools: string[];
80
- description?: string;
40
+ interface InitResult {
41
+ context: InitContext;
42
+ classification: Classification;
43
+ placement: Placement;
44
+ specificPages?: string[];
81
45
  }
82
46
 
83
- interface PlacementSuggestion {
84
- file: string;
85
- location: string;
86
- agentName: string;
87
- }
47
+ // State
48
+ let rl: readline.Interface;
88
49
 
89
- // Configuration
90
- const EXCLUDE_PATHS = [
91
- 'node_modules',
92
- '.git',
93
- 'dist',
94
- 'build',
95
- '.next',
96
- 'out',
97
- '__tests__',
98
- '*.test.*',
99
- '*.spec.*',
100
- ];
101
-
102
- // Helper: Create readline interface
103
- function createPrompt() {
104
- return readline.createInterface({
105
- input: process.stdin,
106
- output: process.stdout,
107
- });
108
- }
109
-
110
- // Helper: Ask a question
111
- function ask(rl: readline.Interface, question: string): Promise<string> {
112
- return new Promise((resolve) => {
113
- rl.question(question, (answer) => {
114
- resolve(answer.trim());
115
- });
116
- });
117
- }
50
+ // ============================================================================
51
+ // PHASE 1: Deterministic Discovery
52
+ // ============================================================================
118
53
 
119
- // Helper: Confirm action
120
- async function confirm(rl: readline.Interface, question: string): Promise<boolean> {
121
- const answer = await ask(rl, `${question} (y/n): `);
122
- return answer.toLowerCase() === 'y' || answer.toLowerCase() === 'yes';
123
- }
124
-
125
- // Step 1: Check for API key
126
- async function checkApiKey(rl: readline.Interface): Promise<{ apiKey: string; projectId: string }> {
127
- console.log('\n📋 Step 1: Authentication\n');
128
-
129
- let apiKey: string | undefined;
130
- let foundInFile: string | undefined;
54
+ async function discoverContext(): Promise<InitContext> {
55
+ const projectRoot = process.cwd();
131
56
 
132
- // Check multiple env files in order of preference
133
- const envFiles = ['.env.local', '.env'];
57
+ console.log('Scanning your project...\n');
134
58
 
135
- for (const envFile of envFiles) {
136
- const envPath = path.join(process.cwd(), envFile);
137
- if (fs.existsSync(envPath)) {
138
- const envContent = fs.readFileSync(envPath, 'utf-8');
139
- const match = envContent.match(/ARCTEN_API_KEY=([^\s\n]+)/);
140
- if (match) {
141
- apiKey = match[1];
142
- foundInFile = envFile;
143
- console.log(`✓ Found API key in ${envFile}`);
144
- break;
145
- }
59
+ // Detect framework
60
+ const packageJsonPath = path.join(projectRoot, 'package.json');
61
+ let framework: InitContext['framework'] = 'unknown';
62
+
63
+ if (fs.existsSync(packageJsonPath)) {
64
+ const pkg = JSON.parse(fs.readFileSync(packageJsonPath, 'utf-8'));
65
+ const deps = { ...pkg.dependencies, ...pkg.devDependencies };
66
+ if (deps.next) {
67
+ const hasAppDir = fs.existsSync(path.join(projectRoot, 'app'));
68
+ framework = hasAppDir ? 'nextjs-app' : 'nextjs-pages';
69
+ } else if (deps.vite) {
70
+ framework = 'vite';
146
71
  }
147
72
  }
148
73
 
149
- // If not found, prompt
150
- if (!apiKey) {
151
- console.log('No API key found in .env or .env.local files.');
152
- console.log('\n📍 Get your API key from: https://arcten.com/dashboard');
153
- apiKey = await ask(rl, '\nEnter your API key: ');
74
+ // Discover functions
75
+ let functions = await discoverFunctionsInDirectory(projectRoot, {
76
+ functionsOnly: true,
77
+ exportedOnly: true,
78
+ });
154
79
 
155
- if (!apiKey) {
156
- console.error('❌ API key is required');
157
- process.exit(1);
158
- }
80
+ functions = filterLikelyTools(functions);
159
81
 
160
- // Save to .env.local (preferred for local development)
161
- const envPath = path.join(process.cwd(), '.env.local');
162
- const envEntry = `\nARCTEN_API_KEY=${apiKey}\n`;
163
- fs.appendFileSync(envPath, envEntry);
164
- console.log('✓ Saved API key to .env.local');
165
- }
166
-
167
- // Extract project ID from API key (format: sk_proj_xxxxx_yyyyy)
168
- const projectIdMatch = apiKey.match(/sk_(proj_[a-zA-Z0-9]+)_/);
169
- if (!projectIdMatch) {
170
- console.error('❌ Invalid API key format');
82
+ if (functions.length === 0) {
83
+ console.log('No exported functions found in your project.');
84
+ console.log('');
85
+ console.log('Create a tools file (e.g., app/tools.ts) with exported functions:');
86
+ console.log('');
87
+ console.log(' export function getOrders(status: string) { ... }');
88
+ console.log(' export function createOrder(data: OrderData) { ... }');
89
+ console.log('');
171
90
  process.exit(1);
172
91
  }
173
92
 
174
- const projectId = projectIdMatch[1];
175
- console.log(`✓ Project ID: ${projectId}`);
93
+ // Find the primary tools file (most functions)
94
+ const fileGroups = new Map<string, DiscoveredFunction[]>();
95
+ for (const fn of functions) {
96
+ const file = fn.file;
97
+ if (!fileGroups.has(file)) {
98
+ fileGroups.set(file, []);
99
+ }
100
+ fileGroups.get(file)!.push(fn);
101
+ }
176
102
 
177
- return { apiKey, projectId };
178
- }
103
+ // Sort by function count and pick the best
104
+ const sortedFiles = [...fileGroups.entries()].sort((a, b) => b[1].length - a[1].length);
105
+ const [toolsFile, toolFunctions] = sortedFiles[0];
179
106
 
180
- // Step 2: Scan codebase
181
- async function scanCodebase(): Promise<CodebaseContext> {
182
- console.log('\n📂 Step 2: Scanning codebase...\n');
107
+ const toolsFileRelative = path.relative(projectRoot, toolsFile);
183
108
 
184
- const projectRoot = process.cwd();
109
+ // Check for API key
110
+ const auth = checkApiKey(projectRoot);
111
+ if (!auth) {
112
+ console.log('No API key found.');
113
+ console.log('');
114
+ console.log('Get your API key from: https://arcten.com/dashboard');
115
+ console.log('');
185
116
 
186
- // Detect framework
187
- const framework = detectFramework(projectRoot);
188
- console.log(`✓ Detected framework: ${framework.name} (${framework.type})`);
117
+ const apiKeyInput = await askQuestion('Paste your API key: ');
118
+ const projectIdMatch = apiKeyInput.match(/sk_(proj_[a-zA-Z0-9]+)_/);
189
119
 
190
- // Get dependencies
191
- const dependencies = getDependencies(projectRoot);
192
- console.log(`✓ Found ${Object.keys(dependencies).length} dependencies`);
120
+ if (!projectIdMatch) {
121
+ console.log('Invalid API key format. Expected: sk_proj_xxxxx_yyyyy');
122
+ process.exit(1);
123
+ }
193
124
 
194
- // Scan for functions
195
- const functions = await scanFunctions(projectRoot);
196
- console.log(`✓ Found ${functions.length} functions`);
125
+ // Save to .env.local
126
+ const envPath = path.join(projectRoot, '.env.local');
127
+ let envContent = '';
128
+ if (fs.existsSync(envPath)) {
129
+ envContent = fs.readFileSync(envPath, 'utf-8');
130
+ }
131
+ if (!envContent.includes('ARCTEN_API_KEY')) {
132
+ envContent += `\nARCTEN_API_KEY=${apiKeyInput}\n`;
133
+ envContent += `NEXT_PUBLIC_ARCTEN_API_URL=https://api.arcten.com\n`;
134
+ fs.writeFileSync(envPath, envContent.trim() + '\n');
135
+ console.log(' Saved API key to .env.local');
136
+ }
197
137
 
198
- // Build file tree
199
- const fileTree = buildFileTree(projectRoot);
138
+ return {
139
+ projectRoot,
140
+ framework,
141
+ toolsFile,
142
+ toolsFileRelative,
143
+ functions: toolFunctions,
144
+ apiKey: apiKeyInput,
145
+ projectId: projectIdMatch[1],
146
+ serverUrl: process.env.ARCTEN_SERVER_URL || 'https://api.arcten.com',
147
+ };
148
+ }
200
149
 
201
- // Debug: log context size
202
- const contextStr = JSON.stringify({
203
- fileTree,
204
- framework,
205
- dependencies,
206
- functions,
207
- stats: {
208
- totalFunctions: functions.length,
209
- skippedPaths: EXCLUDE_PATHS,
210
- },
211
- });
212
- console.log(`📊 Context size: ${(contextStr.length / 1024).toFixed(2)} KB (~${Math.round(contextStr.length / 4)} tokens)`);
150
+ console.log(`Found ${framework === 'nextjs-app' ? 'Next.js App Router' : framework} project`);
151
+ console.log(`Found ${toolFunctions.length} functions in ${toolsFileRelative}`);
152
+ console.log('');
213
153
 
214
154
  return {
215
- fileTree,
155
+ projectRoot,
216
156
  framework,
217
- dependencies,
218
- functions,
219
- stats: {
220
- totalFunctions: functions.length,
221
- skippedPaths: EXCLUDE_PATHS,
222
- },
157
+ toolsFile,
158
+ toolsFileRelative,
159
+ functions: toolFunctions,
160
+ apiKey: auth.apiKey,
161
+ projectId: auth.projectId,
162
+ serverUrl: process.env.ARCTEN_SERVER_URL || 'https://api.arcten.com',
223
163
  };
224
164
  }
225
165
 
226
- // Helper: Detect framework
227
- function detectFramework(projectRoot: string): CodebaseContext['framework'] {
228
- const packageJsonPath = path.join(projectRoot, 'package.json');
166
+ // ============================================================================
167
+ // PHASE 2: Deterministic Classification
168
+ // ============================================================================
229
169
 
230
- if (!fs.existsSync(packageJsonPath)) {
231
- return { name: 'Unknown', type: 'unknown' };
232
- }
233
-
234
- const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf-8'));
235
- const deps = { ...packageJson.dependencies, ...packageJson.devDependencies };
236
-
237
- if (deps.next) {
238
- const hasAppDir = fs.existsSync(path.join(projectRoot, 'app'));
239
- return {
240
- name: hasAppDir ? 'Next.js App Router' : 'Next.js Pages Router',
241
- version: deps.next,
242
- type: hasAppDir ? 'nextjs-app' : 'nextjs-pages',
243
- };
244
- }
170
+ async function classifyTools(context: InitContext): Promise<Classification> {
171
+ console.log('Classifying tools...');
245
172
 
246
- if (deps.vite) {
247
- return { name: 'Vite', version: deps.vite, type: 'vite' };
248
- }
173
+ const toolsToClassify = context.functions.map(f => ({
174
+ name: f.name,
175
+ jsDoc: f.jsDocComment,
176
+ }));
249
177
 
250
- if (deps['react-scripts']) {
251
- return { name: 'Create React App', version: deps['react-scripts'], type: 'cra' };
252
- }
178
+ // Try AI classification first
179
+ try {
180
+ const response = await fetch(`${context.serverUrl}/ai-init/classify`, {
181
+ method: 'POST',
182
+ headers: {
183
+ 'Content-Type': 'application/json',
184
+ 'x-arcten-api-key': context.apiKey,
185
+ },
186
+ body: JSON.stringify({ tools: toolsToClassify }),
187
+ });
253
188
 
254
- return { name: 'Unknown', type: 'unknown' };
255
- }
189
+ if (response.ok) {
190
+ const result = await response.json() as {
191
+ classifications: Record<string, 'safe' | 'sensitive'>;
192
+ safe: string[];
193
+ sensitive: string[];
194
+ };
256
195
 
257
- // Helper: Get dependencies
258
- function getDependencies(projectRoot: string): Record<string, string> {
259
- const packageJsonPath = path.join(projectRoot, 'package.json');
196
+ console.log(` ${result.safe.length} safe (auto-execute)`);
197
+ console.log(` ${result.sensitive.length} sensitive (needs approval)`);
198
+ console.log('');
260
199
 
261
- if (!fs.existsSync(packageJsonPath)) {
262
- return {};
200
+ return {
201
+ safe: result.safe,
202
+ sensitive: result.sensitive,
203
+ source: 'ai',
204
+ };
205
+ }
206
+ } catch {
207
+ // Fall through to pattern-based
263
208
  }
264
209
 
265
- const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf-8'));
266
- return { ...packageJson.dependencies };
267
- }
268
-
269
- // Helper: Scan for functions (reuses existing autoDiscoverAndExtract)
270
- async function scanFunctions(projectRoot: string): Promise<FunctionMetadata[]> {
271
- console.log('🔍 Using TypeScript AST to scan for exported functions...');
272
-
273
- // Generate metadata file using existing extraction logic
274
- const outputPath = path.join(projectRoot, '.arcten', 'tool-metadata.ts');
275
-
276
- // Ensure .arcten directory exists
277
- const arctenDir = path.dirname(outputPath);
278
- if (!fs.existsSync(arctenDir)) {
279
- fs.mkdirSync(arctenDir, { recursive: true });
210
+ // Pattern-based fallback
211
+ const safePatterns = /^(get|list|search|find|fetch|load|calculate|count)/i;
212
+ const sensitivePatterns = /^(create|update|delete|set|add|remove|regenerate)/i;
213
+ const sensitiveReads = /^(get|fetch).*(api.?key|credential|secret|password|token)/i;
214
+
215
+ const safe: string[] = [];
216
+ const sensitive: string[] = [];
217
+
218
+ for (const tool of toolsToClassify) {
219
+ if (sensitiveReads.test(tool.name)) {
220
+ sensitive.push(tool.name);
221
+ } else if (sensitivePatterns.test(tool.name)) {
222
+ sensitive.push(tool.name);
223
+ } else if (safePatterns.test(tool.name)) {
224
+ safe.push(tool.name);
225
+ } else {
226
+ sensitive.push(tool.name); // Default to sensitive
227
+ }
280
228
  }
281
229
 
282
- // Run the auto-discovery
283
- await autoDiscoverAndExtract(projectRoot, outputPath);
230
+ console.log(` ${safe.length} safe (auto-execute)`);
231
+ console.log(` ${sensitive.length} sensitive (needs approval)`);
232
+ console.log('');
284
233
 
285
- // Read the generated metadata
286
- const content = fs.readFileSync(outputPath, 'utf-8');
287
- const metadataMatch = content.match(/export const toolMetadata = ([\s\S]+) as const;/);
234
+ return { safe, sensitive, source: 'pattern-fallback' };
235
+ }
288
236
 
289
- if (!metadataMatch) {
290
- return [];
291
- }
237
+ // ============================================================================
238
+ // PHASE 3: Placement Menu
239
+ // ============================================================================
292
240
 
293
- const metadata = JSON.parse(metadataMatch[1]);
294
- const functions: FunctionMetadata[] = [];
241
+ async function showPlacementMenu(): Promise<Placement> {
242
+ console.log('Where should your agent appear?\n');
243
+ console.log(' [1] 🌍 Everywhere - sidebar on every page');
244
+ console.log(' [2] 📄 Specific pages - only where you choose');
245
+ console.log(' [3] 🔧 Developer mode - just the hook, build your own UI');
246
+ console.log('');
295
247
 
296
- // Convert to wizard format
297
- for (const [name, fn] of Object.entries(metadata.functions as any)) {
298
- // Find which file this function was discovered from
299
- const fileIndex = metadata.discoveredFrom.findIndex((f: string) =>
300
- content.includes(name)
301
- );
248
+ const answer = await askQuestion('Choose (1/2/3): ');
249
+ const choice = answer.trim();
302
250
 
303
- functions.push({
304
- name,
305
- file: metadata.discoveredFrom[fileIndex] || metadata.discoveredFrom[0] || 'unknown',
306
- params: Object.entries(fn.parameters?.properties || {}).map(([paramName, prop]: [string, any]) => ({
307
- name: paramName,
308
- type: prop.type || 'any',
309
- })),
310
- returnType: fn.returnType,
311
- jsDoc: fn.description,
312
- sourceCode: `${name}(${Object.keys(fn.parameters?.properties || {}).join(', ')})`,
313
- });
251
+ if (choice === '2' || choice.toLowerCase().includes('specific')) {
252
+ return 'specific';
253
+ } else if (choice === '3' || choice.toLowerCase().includes('dev')) {
254
+ return 'developer';
314
255
  }
315
256
 
316
- return functions;
257
+ return 'everywhere';
317
258
  }
318
259
 
319
- // Helper: Build file tree (limited depth to reduce tokens)
320
- function buildFileTree(projectRoot: string, depth: number = 0, maxDepth: number = 2): string {
321
- if (depth >= maxDepth) return '';
260
+ // ============================================================================
261
+ // PHASE 4: Deterministic File Creation
262
+ // ============================================================================
322
263
 
323
- const entries = fs.readdirSync(projectRoot, { withFileTypes: true });
324
- let tree = '';
264
+ async function createAllFiles(result: InitResult): Promise<string[]> {
265
+ const { context, classification, placement } = result;
266
+ const { projectRoot, toolsFileRelative, projectId, framework } = context;
267
+ const createdFiles: string[] = [];
325
268
 
326
- for (const entry of entries) {
327
- if (EXCLUDE_PATHS.some(p => entry.name.includes(p.replace('*', '')))) {
328
- continue;
329
- }
269
+ console.log('Creating files...\n');
330
270
 
331
- const indent = ' '.repeat(depth);
332
- tree += `${indent}${entry.isDirectory() ? '📁' : '📄'} ${entry.name}\n`;
271
+ // Determine app directory based on framework
272
+ const appDir = framework === 'nextjs-app' ? 'app' : 'src';
273
+ const appDirPath = path.join(projectRoot, appDir);
333
274
 
334
- if (entry.isDirectory() && depth < maxDepth - 1) {
335
- tree += buildFileTree(path.join(projectRoot, entry.name), depth + 1, maxDepth);
336
- }
275
+ // 1. Create .arcten/config.ts
276
+ const arctenDir = path.join(projectRoot, '.arcten');
277
+ if (!fs.existsSync(arctenDir)) {
278
+ fs.mkdirSync(arctenDir, { recursive: true });
337
279
  }
338
280
 
339
- return tree;
340
- }
341
-
342
- // Step 2.5: Handle missing descriptions
343
- async function handleMissingDescriptions(
344
- rl: readline.Interface,
345
- functions: FunctionMetadata[]
346
- ): Promise<FunctionMetadata[]> {
347
- // Find functions with generic "Execute X" descriptions
348
- const missingDescriptions = functions.filter(fn =>
349
- !fn.jsDoc || fn.jsDoc.startsWith('Execute ')
350
- );
351
-
352
- if (missingDescriptions.length === 0) {
353
- return functions;
354
- }
281
+ const configContent = `// Arcten configuration
282
+ export const arctenConfig = {
283
+ projectId: "${projectId}",
284
+ toolFile: "./${toolsFileRelative.replace(/\\/g, '/')}",
285
+ routes: [
286
+ { pattern: "/*", agentName: "default" },
287
+ ],
288
+ } as const;
289
+ `;
290
+ fs.writeFileSync(path.join(arctenDir, 'config.ts'), configContent);
291
+ createdFiles.push('.arcten/config.ts');
292
+ console.log(' Created .arcten/config.ts');
293
+
294
+ // 2. Create app/api/arcten/token/route.ts (Next.js App Router only)
295
+ if (framework === 'nextjs-app') {
296
+ const tokenDir = path.join(appDirPath, 'api', 'arcten', 'token');
297
+ if (!fs.existsSync(tokenDir)) {
298
+ fs.mkdirSync(tokenDir, { recursive: true });
299
+ }
355
300
 
356
- console.log(`\n⚠️ ${missingDescriptions.length} function${missingDescriptions.length > 1 ? 's are' : ' is'} missing JSDoc descriptions\n`);
357
- console.log('How would you like to handle these?');
358
- console.log(' 1. Edit descriptions now (one by one)');
359
- console.log(' 2. Skip - I\'ll add JSDoc comments manually');
360
- console.log(' 3. Skip - I\'ll edit in dashboard later');
301
+ const tokenRouteContent = `import { verifyToken } from "@arcteninc/core/server";
302
+ import { NextRequest, NextResponse } from "next/server";
361
303
 
362
- const choice = await ask(rl, '\n? Choose option (1-3): ');
304
+ export async function POST(req: NextRequest) {
305
+ try {
306
+ const { user } = await req.json();
307
+ const apiKey = process.env.ARCTEN_API_KEY;
363
308
 
364
- if (choice === '1') {
365
- // Edit descriptions one by one
366
- console.log('');
367
- for (const fn of missingDescriptions) {
368
- console.log(`📝 ${fn.name} (${fn.file})`);
369
- console.log(` Current: ${fn.jsDoc || `Execute ${fn.name}`}\n`);
309
+ if (!apiKey) {
310
+ return NextResponse.json(
311
+ { error: "ARCTEN_API_KEY not configured" },
312
+ { status: 500 }
313
+ );
314
+ }
370
315
 
371
- const newDesc = await ask(rl, ' Enter description (or press Enter to skip): ');
316
+ const tokenResponse = await verifyToken({
317
+ apiKey,
318
+ user: user || { id: "anonymous", name: "User" },
319
+ apiUrl: process.env.NEXT_PUBLIC_ARCTEN_API_URL || "https://api.arcten.com",
320
+ });
372
321
 
373
- if (newDesc.trim()) {
374
- fn.jsDoc = newDesc.trim();
375
- console.log(` ✓ Updated description for ${fn.name}\n`);
376
- } else {
377
- console.log(` ⏭️ Skipped ${fn.name}\n`);
378
- }
379
- }
380
- console.log('✅ Description editing complete\n');
381
- } else {
382
- console.log('\n⏭️ Skipping description editing\n');
383
- console.log('💡 You can add JSDoc comments to your code or edit descriptions in the dashboard.');
322
+ return NextResponse.json(tokenResponse);
323
+ } catch (error: any) {
324
+ console.error("Token generation error:", error);
325
+ return NextResponse.json(
326
+ { error: error.message || "Failed to generate token" },
327
+ { status: 500 }
328
+ );
384
329
  }
385
-
386
- return functions;
387
330
  }
331
+ `;
332
+ fs.writeFileSync(path.join(tokenDir, 'route.ts'), tokenRouteContent);
333
+ createdFiles.push(`${appDir}/api/arcten/token/route.ts`);
334
+ console.log(` Created ${appDir}/api/arcten/token/route.ts`);
335
+ }
388
336
 
389
- // Step 3: Get user intent
390
- async function getUserIntent(rl: readline.Interface): Promise<string> {
391
- console.log('\n💭 Step 3: Define your agent\'s purpose\n');
392
- console.log('Examples:');
393
- console.log(' - "Help customers place orders and track shipments"');
394
- console.log(' - "Answer questions about our product documentation"');
395
- console.log(' - "Assist with user account management"');
337
+ // 3. Create wrapper component based on placement
338
+ const toolsImportPath = path.relative(appDirPath, context.toolsFile).replace(/\.ts$/, '').replace(/\\/g, '/');
396
339
 
397
- const intent = await ask(rl, '\n? What should your AI agent do?\n> ');
340
+ if (placement === 'everywhere') {
341
+ const wrapperContent = `"use client";
398
342
 
399
- if (!intent) {
400
- console.error('❌ Intent is required');
401
- process.exit(1);
402
- }
343
+ import { ArctenAgent } from "@arcteninc/core";
344
+ import { toolMetadata } from "../.arcten/tool-metadata";
345
+ import { allTools, safeToolNames } from "../.arcten/arcten.tools";
403
346
 
404
- return intent;
347
+ interface ArctenWrapperProps {
348
+ children: React.ReactNode;
405
349
  }
406
350
 
407
- // Helper: Get JWT token from API key
408
- async function getAuthToken(apiKey: string, serverUrl: string): Promise<string> {
409
- console.log(`🔑 Getting auth token from ${serverUrl}/token...`);
410
-
411
- try {
412
- const response = await fetch(`${serverUrl}/token`, {
413
- method: 'POST',
414
- headers: {
415
- 'Content-Type': 'application/json',
416
- },
417
- body: JSON.stringify({ apiKey }),
418
- });
419
-
420
- console.log(`🔑 Token response status: ${response.status}`);
351
+ export function ArctenWrapper({ children }: ArctenWrapperProps) {
352
+ return (
353
+ <div className="flex h-screen">
354
+ <div className="flex-1 overflow-auto">
355
+ {children}
356
+ </div>
357
+ <ArctenAgent
358
+ apiBaseUrl={process.env.NEXT_PUBLIC_ARCTEN_API_URL || "https://api.arcten.com"}
359
+ user={{ id: "user-1", name: "User" }}
360
+ tools={allTools}
361
+ safeToolNames={safeToolNames}
362
+ toolMetadata={toolMetadata.functions}
363
+ />
364
+ </div>
365
+ );
366
+ }
421
367
 
422
- if (!response.ok) {
423
- const error = await response.json();
424
- console.error(`🔑 Token error:`, error);
425
- throw new Error(error.error || 'Failed to authenticate');
426
- }
368
+ export default ArctenWrapper;
369
+ `;
370
+ fs.writeFileSync(path.join(appDirPath, 'ArctenWrapper.tsx'), wrapperContent);
371
+ createdFiles.push(`${appDir}/ArctenWrapper.tsx`);
372
+ console.log(` Created ${appDir}/ArctenWrapper.tsx`);
373
+
374
+ // 4. Modify layout.tsx to wrap with ArctenWrapper
375
+ const layoutPath = path.join(appDirPath, 'layout.tsx');
376
+ if (fs.existsSync(layoutPath)) {
377
+ let layoutContent = fs.readFileSync(layoutPath, 'utf-8');
378
+
379
+ // Add import if not present
380
+ if (!layoutContent.includes('ArctenWrapper')) {
381
+ // Add import at the top (after existing imports)
382
+ const importStatement = `import { ArctenWrapper } from "./ArctenWrapper";\n`;
383
+ const lastImportIndex = layoutContent.lastIndexOf('import ');
384
+ if (lastImportIndex !== -1) {
385
+ const endOfImport = layoutContent.indexOf('\n', lastImportIndex);
386
+ layoutContent = layoutContent.slice(0, endOfImport + 1) + importStatement + layoutContent.slice(endOfImport + 1);
387
+ } else {
388
+ layoutContent = importStatement + layoutContent;
389
+ }
427
390
 
428
- const data = await response.json();
429
- const token = data.clientToken || data.token; // Server returns "clientToken"
430
- console.log(`🔑 Got token: ${token ? token.substring(0, 20) + '...' : 'NO TOKEN'}`);
391
+ // Wrap {children} with <ArctenWrapper>
392
+ layoutContent = layoutContent.replace(
393
+ /(\{children\})/,
394
+ '<ArctenWrapper>{children}</ArctenWrapper>'
395
+ );
431
396
 
432
- if (!token) {
433
- throw new Error('No token received from server');
397
+ fs.writeFileSync(layoutPath, layoutContent);
398
+ createdFiles.push(`${appDir}/layout.tsx (modified)`);
399
+ console.log(` Modified ${appDir}/layout.tsx`);
400
+ }
434
401
  }
402
+ } else if (placement === 'specific') {
403
+ // Create standalone component for specific pages
404
+ const componentContent = `"use client";
405
+
406
+ import { ArctenAgent } from "@arcteninc/core";
407
+ import { toolMetadata } from "../.arcten/tool-metadata";
408
+ import { allTools, safeToolNames } from "../.arcten/arcten.tools";
409
+
410
+ export function ArctenChat() {
411
+ return (
412
+ <ArctenAgent
413
+ apiBaseUrl={process.env.NEXT_PUBLIC_ARCTEN_API_URL || "https://api.arcten.com"}
414
+ user={{ id: "user-1", name: "User" }}
415
+ tools={allTools}
416
+ safeToolNames={safeToolNames}
417
+ toolMetadata={toolMetadata.functions}
418
+ />
419
+ );
420
+ }
435
421
 
436
- return token;
437
- } catch (error: any) {
438
- if (error.cause?.code === 'ENOTFOUND' || error.cause?.code === 'ECONNREFUSED' || error.message.includes('fetch')) {
439
- console.error(`\n❌ Unable to connect to: ${serverUrl}`);
440
- console.error('\nPossible issues:');
441
- console.error(' • No internet connection');
442
- console.error(' • API server is down');
443
- console.error(' • Firewall blocking the request');
444
- throw new Error('Connection failed');
422
+ export default ArctenChat;
423
+ `;
424
+ const componentsDir = path.join(projectRoot, 'components');
425
+ if (!fs.existsSync(componentsDir)) {
426
+ fs.mkdirSync(componentsDir, { recursive: true });
445
427
  }
446
- throw error;
428
+ fs.writeFileSync(path.join(componentsDir, 'ArctenChat.tsx'), componentContent);
429
+ createdFiles.push('components/ArctenChat.tsx');
430
+ console.log(' Created components/ArctenChat.tsx');
431
+ console.log('');
432
+ console.log(' Import it in your pages:');
433
+ console.log(' import { ArctenChat } from "@/components/ArctenChat";');
434
+ console.log('');
435
+ } else {
436
+ // Developer mode - just show hook usage
437
+ console.log('');
438
+ console.log(' Developer mode - use the hook directly:');
439
+ console.log('');
440
+ console.log(' import { useAgent } from "@arcteninc/core";');
441
+ console.log(' import { allTools, safeToolNames } from "./.arcten/arcten.tools";');
442
+ console.log(' import { toolMetadata } from "./.arcten/tool-metadata";');
443
+ console.log('');
444
+ console.log(' const { messages, sendMessage } = useAgent({');
445
+ console.log(' tools: allTools,');
446
+ console.log(' safeToolNames,');
447
+ console.log(' toolMetadata: toolMetadata.functions,');
448
+ console.log(' });');
449
+ console.log('');
447
450
  }
448
- }
449
451
 
450
- // Step 4: Estimate cost
451
- async function estimateCost(apiKey: string, serverUrl: string, context: CodebaseContext): Promise<AnalysisEstimate> {
452
- console.log('\n💰 Estimating analysis cost...\n');
452
+ // 5. Update package.json scripts
453
+ const packageJsonPath = path.join(projectRoot, 'package.json');
454
+ if (fs.existsSync(packageJsonPath)) {
455
+ const pkg = JSON.parse(fs.readFileSync(packageJsonPath, 'utf-8'));
453
456
 
454
- try {
455
- const url = `${serverUrl}/tools/analyze/estimate`;
456
- const response = await fetch(url, {
457
- method: 'POST',
458
- headers: {
459
- 'Content-Type': 'application/json',
460
- },
461
- body: JSON.stringify({ context }),
462
- });
457
+ let modified = false;
463
458
 
464
- if (!response.ok) {
465
- const error = await response.json();
466
- throw new Error(error.error || 'Failed to estimate cost');
459
+ // Add arcten sync to dev and build (runs silently if no changes)
460
+ if (pkg.scripts?.dev && !pkg.scripts.dev.includes('arcten sync')) {
461
+ pkg.scripts.dev = `arcten sync --yes && ${pkg.scripts.dev}`;
462
+ modified = true;
467
463
  }
468
-
469
- return await response.json();
470
- } catch (error: any) {
471
- if (error.cause?.code === 'ENOTFOUND' || error.cause?.code === 'ECONNREFUSED' || error.message.includes('fetch')) {
472
- console.error(`\n❌ Unable to connect to: ${serverUrl}`);
473
- console.error('\nPossible issues:');
474
- console.error(' • No internet connection');
475
- console.error(' • API server is down');
476
- console.error(' • Firewall blocking the request');
477
- console.error('\n💡 Tip: Use --allow-all flag to skip analysis and enable all tools');
478
- throw new Error('Connection failed');
464
+ if (pkg.scripts?.build && !pkg.scripts.build.includes('arcten sync')) {
465
+ pkg.scripts.build = `arcten sync --yes && ${pkg.scripts.build}`;
466
+ modified = true;
479
467
  }
480
- throw error;
481
- }
482
- }
483
468
 
484
- // Step 5: Confirm analysis
485
- async function confirmAnalysis(rl: readline.Interface, estimate: AnalysisEstimate): Promise<boolean> {
486
- console.log('┌─────────────────────────────────────────────────┐');
487
- console.log(' 💰 AI Analysis (Billable) │');
488
- console.log('│ │');
489
- console.log(`│ Estimated cost: $${estimate.estimatedCost.toFixed(2)}`.padEnd(50) + '│');
490
- console.log(`│ ${estimate.functionCount} functions across ${estimate.chunkCount} chunk${estimate.chunkCount > 1 ? 's' : ''}`.padEnd(50) + '│');
491
- console.log('│ │');
492
-
493
- if (estimate.willBeChunked) {
494
- console.log('│ ⚠️ Large codebase - will process in chunks │');
495
- console.log('│ │');
469
+ if (modified) {
470
+ fs.writeFileSync(packageJsonPath, JSON.stringify(pkg, null, 2) + '\n');
471
+ createdFiles.push('package.json (modified)');
472
+ console.log(' Updated package.json scripts');
473
+ }
496
474
  }
497
475
 
498
- console.log('│ This will be deducted from your subscription. │');
499
- console.log('│ │');
500
- console.log('│ The AI will: │');
501
- console.log(`│ • Categorize your ${estimate.functionCount} functions`.padEnd(50) + '│');
502
- console.log('│ • Suggest which to enable for your use case │');
503
- console.log('│ • Recommend agent configurations │');
504
- console.log('│ • Suggest where to place the agent │');
505
- console.log('└─────────────────────────────────────────────────┘');
506
476
  console.log('');
507
-
508
- return await confirm(rl, `? Continue with analysis ($${estimate.estimatedCost.toFixed(2)})?`);
477
+ return createdFiles;
509
478
  }
510
479
 
511
- // Step 6: Run analysis
512
- async function runAnalysis(
513
- apiKey: string,
514
- serverUrl: string,
515
- context: CodebaseContext,
516
- userIntent: string
517
- ): Promise<AnalysisResult> {
518
- console.log('\n🤖 Analyzing with AI...\n');
519
-
520
- // Get JWT token from API key
521
- const token = await getAuthToken(apiKey, serverUrl);
522
- console.log(`📡 Using token for analysis: ${token ? token.substring(0, 20) + '...' : 'NO TOKEN'}`);
523
- console.log(`📡 Calling: ${serverUrl}/tools/analyze`);
524
-
525
- const response = await fetch(`${serverUrl}/tools/analyze`, {
526
- method: 'POST',
527
- headers: {
528
- 'Content-Type': 'application/json',
529
- 'Authorization': `Bearer ${token}`,
530
- },
531
- body: JSON.stringify({ context, userIntent }),
532
- });
533
-
534
- console.log(`📡 Analysis response status: ${response.status}`);
535
-
536
- if (!response.ok) {
537
- const error = await response.json();
538
- console.error(`📡 Analysis error response:`, error);
539
- throw new Error(error.error || 'Analysis failed');
540
- }
541
-
542
- const result = await response.json();
543
- console.log(`📡 Analysis result:`, JSON.stringify(result).substring(0, 200));
544
-
545
- if (!result || !result.success) {
546
- throw new Error(result?.error || 'Analysis failed - invalid response');
547
- }
548
-
549
- const tokensUsed = result.tokensUsed || 0;
550
- const actualCost = result.stats?.actualCost || 0;
551
-
552
- console.log(`✓ Analysis complete - ${tokensUsed.toLocaleString()} tokens used`);
553
- console.log(` Actual cost: $${actualCost.toFixed(2)}`);
480
+ // ============================================================================
481
+ // PHASE 5: AI Review (disabled - endpoint not implemented)
482
+ // ============================================================================
554
483
 
555
- return result;
484
+ async function aiReviewFiles(_result: InitResult, _createdFiles: string[]): Promise<void> {
485
+ // AI review is optional and requires server endpoint /ai-init/review
486
+ // Skipping for now since the generated files are deterministic
556
487
  }
557
488
 
558
- // Step 7: Sync to dashboard
559
- async function syncToDashboard(
560
- apiKey: string,
561
- serverUrl: string,
562
- projectId: string,
563
- tools: ToolRecommendation[],
564
- agents?: AgentConfig[]
565
- ): Promise<void> {
566
- console.log('\n☁️ Step 7: Syncing to dashboard...\n');
567
-
568
- // Convert tools to the format expected by bulkUpsert
569
- const toolsData = tools.map(tool => ({
570
- name: tool.name,
571
- description: tool.description,
572
- signature: JSON.stringify({ /* simplified */ }),
573
- sourceFile: undefined, // Not tracked in wizard yet
574
- category: tool.category,
575
- isEnabled: tool.shouldEnable,
576
- isOverridable: true,
577
- requiresApproval: tool.requiresApproval,
578
- sensitiveParams: tool.sensitiveParams || [],
579
- }));
489
+ // ============================================================================
490
+ // PHASE 6: Run Sync
491
+ // ============================================================================
580
492
 
581
- // Convert agents to the format expected by bulkUpsert
582
- const agentsData = agents?.map(agent => ({
583
- name: agent.name,
584
- systemPrompt: agent.systemPrompt,
585
- enabledTools: agent.enabledTools,
586
- description: agent.description || `Agent: ${agent.name}`,
587
- })) || [];
493
+ async function runSync(): Promise<void> {
494
+ console.log('Running sync...\n');
588
495
 
589
496
  try {
590
- // Get JWT token
591
- const token = await getAuthToken(apiKey, serverUrl);
592
-
593
- // Call server endpoint which will handle Convex mutation
594
- const response = await fetch(`${serverUrl}/tools/sync`, {
595
- method: 'POST',
596
- headers: {
597
- 'Content-Type': 'application/json',
598
- 'Authorization': `Bearer ${token}`,
599
- },
600
- body: JSON.stringify({
601
- projectId,
602
- tools: toolsData,
603
- agents: agentsData,
604
- }),
605
- });
606
-
607
- if (!response.ok) {
608
- const errorText = await response.text().catch(() => 'Unknown error');
609
- throw new Error(`Failed to sync: ${response.status} ${errorText}`);
610
- }
611
-
612
- const result = await response.json();
613
-
614
- if (!result.success) {
615
- throw new Error(result.error || 'Failed to sync to dashboard');
616
- }
617
-
618
- console.log(`✅ Synced ${tools.length} tools${agents && agents.length > 0 ? ` and ${agents.length} agents` : ''} to dashboard`);
619
-
620
- if (result.upserted) {
621
- const created = result.upserted.filter((t: any) => t.created).length;
622
- const updated = result.upserted.filter((t: any) => !t.created).length;
623
- console.log(` 📝 Tools - Created: ${created} | Updated: ${updated}`);
624
- }
625
-
626
- if (result.agents && agents && agents.length > 0) {
627
- const agentCreated = result.agents.filter((a: any) => a.created).length;
628
- const agentUpdated = result.agents.filter((a: any) => !a.created).length;
629
- console.log(` 📝 Agents - Created: ${agentCreated} | Updated: ${agentUpdated}`);
497
+ // Get script directory (ESM compatible)
498
+ const scriptDir = path.dirname(new URL(import.meta.url).pathname).replace(/^\/([A-Z]:)/, '$1');
499
+
500
+ // Run arcten sync which handles extract-types + tool array generation
501
+ const syncScript = path.join(scriptDir, 'cli-sync-wrapper.cjs');
502
+ if (fs.existsSync(syncScript)) {
503
+ execSync(`node "${syncScript}" --yes`, {
504
+ cwd: process.cwd(),
505
+ stdio: 'inherit',
506
+ });
507
+ } else {
508
+ // Fallback: run extract-types directly
509
+ console.log(' Running arcten-extract-types...');
510
+ const extractScript = path.join(scriptDir, 'cli-extract-types-auto-wrapper.js');
511
+ if (fs.existsSync(extractScript)) {
512
+ execSync(`node "${extractScript}"`, {
513
+ cwd: process.cwd(),
514
+ stdio: ['inherit', 'pipe', 'pipe'],
515
+ });
516
+ } else {
517
+ execSync('npx arcten-extract-types', {
518
+ cwd: process.cwd(),
519
+ stdio: ['inherit', 'pipe', 'pipe'],
520
+ });
521
+ }
522
+ console.log(' Done');
630
523
  }
631
524
  } catch (error: any) {
632
- console.error(`❌ Failed to sync: ${error.message}`);
633
- throw error;
525
+ console.log(' Warning: Sync encountered an error');
526
+ console.log(' You can run `arcten sync` manually after setup');
634
527
  }
635
528
  }
636
529
 
637
- // Step 7.5: Display placement suggestions
638
- async function displayPlacementSuggestions(
639
- placements: PlacementSuggestion[]
640
- ): Promise<void> {
641
- if (!placements || placements.length === 0) {
642
- return;
643
- }
530
+ // ============================================================================
531
+ // Helper Functions
532
+ // ============================================================================
644
533
 
645
- console.log('\n📍 Suggested Placements:\n');
534
+ function checkApiKey(projectRoot: string): { apiKey: string; projectId: string } | null {
535
+ const envFiles = ['.env.local', '.env'];
646
536
 
647
- for (const placement of placements) {
648
- console.log(` ${placement.file}`);
649
- console.log(` └─ Location: ${placement.location}`);
650
- console.log(` └─ Agent: ${placement.agentName}`);
651
- console.log('');
537
+ for (const envFile of envFiles) {
538
+ const envPath = path.join(projectRoot, envFile);
539
+ if (fs.existsSync(envPath)) {
540
+ const envContent = fs.readFileSync(envPath, 'utf-8');
541
+ const match = envContent.match(/ARCTEN_API_KEY=([^\s\n]+)/);
542
+ if (match) {
543
+ const key = match[1];
544
+ const projectIdMatch = key.match(/sk_(proj_[a-zA-Z0-9]+)_/);
545
+ if (projectIdMatch) {
546
+ return { apiKey: key, projectId: projectIdMatch[1] };
547
+ }
548
+ }
549
+ }
652
550
  }
653
551
 
654
- console.log('💡 Integration Steps:\n');
655
- console.log(' 1. Install @arcteninc/core: bun add @arcteninc/core');
656
- console.log(' 2. Import and use the agent in your components:');
657
- console.log('');
658
- console.log(' import { useAgent } from "@arcteninc/core";');
659
- console.log(' import { arctenConfig } from "./.arcten/config";');
660
- console.log('');
661
- console.log(' function MyComponent() {');
662
- console.log(' const agent = useAgent({');
663
- console.log(' projectId: arctenConfig.projectId,');
664
- console.log(' agentName: "default",');
665
- console.log(' tools: myTools,');
666
- console.log(' });');
667
- console.log('');
668
- console.log(' return (');
669
- console.log(' <div>');
670
- console.log(' {agent.messages.map(m => <div key={m.id}>{m.content}</div>)}');
671
- console.log(' </div>');
672
- console.log(' );');
673
- console.log(' }');
674
- console.log('');
675
- console.log(' 3. For pre-built UI, use <ArctenAgent /> component');
676
- console.log('');
677
- console.log(' For more info: https://arcten.com/docs/integration');
678
- console.log('');
552
+ return null;
679
553
  }
680
554
 
681
- // Generate agent profiles from user description
682
- async function generateAgents(
683
- apiKey: string,
684
- serverUrl: string,
685
- description: string,
686
- functions: FunctionMetadata[]
687
- ): Promise<AgentConfig[]> {
688
- const token = await getAuthToken(apiKey, serverUrl);
689
-
690
- const tools = functions.map(fn => ({
691
- name: fn.name,
692
- description: fn.jsDoc || `Function: ${fn.name}`,
693
- }));
694
-
695
- const response = await fetch(`${serverUrl}/tools/generate-agents`, {
696
- method: 'POST',
697
- headers: {
698
- 'Content-Type': 'application/json',
699
- 'Authorization': `Bearer ${token}`,
700
- },
701
- body: JSON.stringify({
702
- description,
703
- tools,
704
- }),
555
+ function askQuestion(question: string): Promise<string> {
556
+ return new Promise((resolve) => {
557
+ rl.question(question, (answer) => {
558
+ resolve(answer.trim());
559
+ });
705
560
  });
706
-
707
- if (!response.ok) {
708
- const errorText = await response.text().catch(() => 'Unknown error');
709
- throw new Error(`Failed to generate agents: ${response.status} ${errorText}`);
710
- }
711
-
712
- const result = await response.json();
713
-
714
- if (!result.success || !result.agents) {
715
- throw new Error(result.error || 'Failed to generate agent profiles');
716
- }
717
-
718
- return result.agents;
719
561
  }
720
562
 
721
- // Step 8: Generate local config files
722
- async function generateLocalConfig(
723
- projectId: string,
724
- agentName: string = 'default'
725
- ): Promise<void> {
726
- console.log('\n📁 Step 8: Generating local configuration...\n');
727
-
728
- try {
729
- // Create .arcten directory if it doesn't exist
730
- const arctenDir = path.join(process.cwd(), '.arcten');
731
- if (!fs.existsSync(arctenDir)) {
732
- fs.mkdirSync(arctenDir, { recursive: true });
733
- }
734
-
735
- // Generate .arcten/config.ts
736
- const configContent = `/**
737
- * Arcten Configuration
738
- * Generated by: arcten init
739
- *
740
- * This file contains your project ID and route configurations.
741
- * Modify this file to change which agents are used on different routes.
742
- */
743
-
744
- export const arctenConfig = {
745
- projectId: "${projectId}",
746
- routes: [
747
- // Default route - matches all pages
748
- { pattern: "/*", agentName: "${agentName}" },
749
-
750
- // Add more route-specific agents here
751
- // Example:
752
- // { pattern: "/products/*", agentName: "sales-assistant" },
753
- // { pattern: "/support/*", agentName: "support-bot" },
754
- ],
755
- } as const;
756
- `;
757
-
758
- const configPath = path.join(arctenDir, 'config.ts');
759
- fs.writeFileSync(configPath, configContent, 'utf-8');
760
- console.log(`✅ Created ${path.relative(process.cwd(), configPath)}`);
563
+ // ============================================================================
564
+ // Main
565
+ // ============================================================================
761
566
 
762
- // Generate .arcten/README.md
763
- const readmeContent = `# Arcten Configuration
764
-
765
- This directory contains configuration files generated by \`arcten init\`.
766
-
767
- ## Files
768
-
769
- - \`config.ts\` - Project ID and route configuration
770
- - \`tool-metadata.ts\` - (Optional) Build-time tool metadata
771
-
772
- ## Usage
773
-
774
- Import the config in your application:
775
-
776
- \`\`\`typescript
777
- import { arctenConfig } from './.arcten/config';
778
- import { useAgent } from '@arcteninc/core';
567
+ async function main() {
568
+ console.log('');
569
+ console.log(' ___ ____________ _____ _____ _ _ ');
570
+ console.log(' / _ \\ | ___ \\ ___ \\_ _| ___| \\ | |');
571
+ console.log('/ /_\\ \\| |_/ / | / | | | |__ | \\| |');
572
+ console.log('| _ || /| | | | | | __|| . ` |');
573
+ console.log('| | | || |\\ \\| |__/\\ | | | |___| |\\ |');
574
+ console.log('\\_| |_/\\_| \\_\\____/ \\_/ \\____/\\_| \\_/');
575
+ console.log('');
576
+ console.log(' Setup Wizard');
577
+ console.log('');
779
578
 
780
- function MyComponent() {
781
- const agent = useAgent({
782
- projectId: arctenConfig.projectId,
783
- agentName: "default",
784
- tools: myTools,
579
+ // Create readline interface
580
+ rl = readline.createInterface({
581
+ input: process.stdin,
582
+ output: process.stdout,
785
583
  });
786
584
 
787
- // Use agent.messages, agent.sendMessage, etc.
788
- }
789
- \`\`\`
790
-
791
- For more information, visit https://arcten.com/docs
792
- `;
793
-
794
- const readmePath = path.join(arctenDir, 'README.md');
795
- fs.writeFileSync(readmePath, readmeContent, 'utf-8');
796
- console.log(`✅ Created ${path.relative(process.cwd(), readmePath)}`);
797
-
798
- } catch (error: any) {
799
- console.error(`❌ Failed to generate config: ${error.message}`);
800
- throw error;
801
- }
802
- }
585
+ try {
586
+ // PHASE 1: Deterministic Discovery
587
+ const context = await discoverContext();
803
588
 
804
- // Main wizard
805
- async function main() {
806
- // Check for flags
807
- const args = process.argv.slice(2);
808
- const skipAnalysis = args.includes('--skip-analysis');
809
- const allowAll = args.includes('--allow-all');
589
+ // PHASE 2: Deterministic Classification
590
+ const classification = await classifyTools(context);
810
591
 
811
- if (allowAll) {
812
- console.log('');
813
- console.log('╔═══════════════════════════════════════════════╗');
814
- console.log('║ ║');
815
- console.log('║ 🔓 Arcten Setup (Allow All) 🔓 ║');
816
- console.log('║ ║');
817
- console.log('╚═══════════════════════════════════════════════╝');
818
- console.log('');
819
- } else if (skipAnalysis) {
820
- console.log('');
821
- console.log('╔═══════════════════════════════════════════════╗');
822
- console.log('║ ║');
823
- console.log('║ 🔄 Arcten Sync Tools 🔄 ║');
824
- console.log('║ ║');
825
- console.log('╚═══════════════════════════════════════════════╝');
826
- console.log('');
827
- } else {
828
- console.log('');
829
- console.log('╔═══════════════════════════════════════════════╗');
830
- console.log('║ ║');
831
- console.log('║ 🚀 Arcten Setup Wizard 🚀 ║');
832
- console.log('║ ║');
833
- console.log('╚═══════════════════════════════════════════════╝');
592
+ // Show classification summary
593
+ console.log('Classification:');
594
+ if (classification.safe.length > 0) {
595
+ console.log(` Safe: ${classification.safe.slice(0, 5).join(', ')}${classification.safe.length > 5 ? '...' : ''}`);
596
+ }
597
+ if (classification.sensitive.length > 0) {
598
+ console.log(` Sensitive: ${classification.sensitive.slice(0, 5).join(', ')}${classification.sensitive.length > 5 ? '...' : ''}`);
599
+ }
834
600
  console.log('');
835
- }
836
-
837
- const rl = createPrompt();
838
- const serverUrl = process.env.ARCTEN_SERVER_URL || 'https://api.arcten.com';
839
601
 
840
- try {
841
- // Check for existing setup
842
- const arctenDir = path.join(process.cwd(), '.arcten');
843
- const hasExistingSetup = fs.existsSync(arctenDir);
844
-
845
- if (hasExistingSetup && !skipAnalysis && !allowAll) {
846
- console.log('⚠️ Arcten is already configured in this project!\n');
847
- console.log('📁 Found existing .arcten/ directory\n');
848
- console.log('Running init again will:');
849
- console.log(' • Overwrite your .arcten/config.ts file');
850
- console.log(' • Run AI analysis again (costs money)');
851
- console.log(' • Resync all tools to dashboard\n');
852
-
853
- const shouldContinue = await confirm(rl, 'Do you want to continue and reconfigure?');
854
-
855
- if (!shouldContinue) {
856
- console.log('\n💡 Tip: To re-sync tools without analysis, use:');
857
- console.log(' arcten sync\n');
858
- rl.close();
859
- process.exit(0);
860
- }
602
+ const classificationOk = await askQuestion('Does this look right? (y/n): ');
603
+ if (classificationOk.toLowerCase() === 'n') {
604
+ console.log('');
605
+ console.log('You can adjust classifications later by editing .arcten/sync-state.json');
606
+ console.log('and running `arcten sync`.');
861
607
  console.log('');
862
608
  }
863
609
 
864
- // Step 1: Authentication
865
- const { apiKey, projectId } = await checkApiKey(rl);
866
-
867
- // Step 2: Scan codebase
868
- const context = await scanCodebase();
869
-
870
- // Step 2.5: Handle missing descriptions (only in full wizard mode, not sync/allow-all)
871
- if (!skipAnalysis && !allowAll) {
872
- context.functions = await handleMissingDescriptions(rl, context.functions);
873
- }
874
-
875
- // Step 3: Sync all tools to dashboard
876
- console.log('\n☁️ Syncing tools to dashboard...\n');
877
- const tools: ToolRecommendation[] = context.functions.map(fn => ({
878
- name: fn.name,
879
- category: 'query' as const,
880
- description: fn.jsDoc || `Function: ${fn.name}`,
881
- shouldEnable: allowAll, // Enable all in allow-all mode
882
- requiresApproval: false,
883
- sensitiveParams: [],
884
- }));
885
-
886
- await syncToDashboard(apiKey, serverUrl, projectId, tools);
887
- console.log(`✅ Synced ${tools.length} tools to dashboard\n`);
888
-
889
- // Step 4: Create agents
890
- let agents: AgentConfig[] = [];
891
-
892
- if (skipAnalysis) {
893
- // Just create default agent
894
- agents = [{
895
- name: 'default',
896
- systemPrompt: 'You are a helpful assistant.',
897
- enabledTools: context.functions.map(f => f.name),
898
- description: 'General assistant',
899
- }];
900
- } else {
901
- // Ask user to describe agents
902
- console.log('📋 Tools synced. Now let\'s create your agent(s).\n');
903
- const agentDescription = await ask(rl,
904
- 'Describe the agent(s) you want (or press Enter for a default agent):\n> '
905
- );
610
+ // PHASE 3: Placement Menu
611
+ const placement = await showPlacementMenu();
612
+ console.log('');
906
613
 
907
- if (!agentDescription.trim()) {
908
- // Default agent
909
- agents = [{
910
- name: 'default',
911
- systemPrompt: 'You are a helpful assistant.',
912
- enabledTools: context.functions.map(f => f.name),
913
- description: 'General assistant',
914
- }];
915
- } else {
916
- // Generate agents via LLM
917
- console.log('\n🤖 Generating agent profiles...\n');
918
- agents = await generateAgents(apiKey, serverUrl, agentDescription, context.functions);
919
-
920
- // Show what will be created
921
- console.log('🤖 Agents to create:\n');
922
- for (const agent of agents) {
923
- console.log(` 📌 ${agent.name}: ${agent.description}`);
924
- console.log(` Tools: ${agent.enabledTools.length} of ${context.functions.length}\n`);
925
- }
614
+ const result: InitResult = {
615
+ context,
616
+ classification,
617
+ placement,
618
+ };
926
619
 
927
- const confirm = await ask(rl, 'Create these agents? (Y/n): ');
928
- if (confirm.toLowerCase() === 'n') {
929
- // Fallback to default
930
- agents = [{
931
- name: 'default',
932
- systemPrompt: 'You are a helpful assistant.',
933
- enabledTools: context.functions.map(f => f.name),
934
- description: 'General assistant',
935
- }];
936
- }
937
- }
938
- }
620
+ // PHASE 4: Deterministic File Creation
621
+ const createdFiles = await createAllFiles(result);
939
622
 
940
- // Step 5: Sync agents
941
- console.log('\n☁️ Syncing agents to dashboard...\n');
942
- await syncToDashboard(apiKey, serverUrl, projectId, tools, agents);
943
- console.log(`✅ Synced ${agents.length} agent(s) to dashboard\n`);
623
+ // PHASE 5: AI Review
624
+ await aiReviewFiles(result, createdFiles);
944
625
 
945
- // Step 6: Generate local config
946
- await generateLocalConfig(projectId, agents[0].name);
626
+ // PHASE 6: Run Sync
627
+ await runSync();
947
628
 
948
- console.log('\n✅ Setup complete!\n');
949
- console.log(`📍 Dashboard: https://arcten.com/dashboard/projects/${projectId}`);
950
629
  console.log('');
951
-
630
+ console.log('Setup complete!');
631
+ console.log('');
632
+ console.log('Run `bun dev` (or `npm run dev`) to start your app.');
633
+ console.log('');
952
634
  } catch (error: any) {
953
- console.error('\n❌ Error:', error.message);
635
+ console.error('Error:', error.message);
954
636
  process.exit(1);
955
637
  } finally {
956
638
  rl.close();
957
639
  }
958
640
  }
959
641
 
960
- // Run if executed directly
961
- if (require.main === module) {
962
- main().catch((error) => {
963
- console.error('Fatal error:', error);
964
- process.exit(1);
965
- });
966
- }
642
+ // Run
643
+ main().catch((error) => {
644
+ console.error('Fatal error:', error);
645
+ process.exit(1);
646
+ });
967
647
 
968
648
  export { main };