@arcteninc/core 0.0.138 → 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.
- package/dist/core.css +1 -1
- package/dist/index.cjs +6 -15
- package/dist/index.mjs +2949 -2848
- package/dist/lib/useAgent.d.ts.map +1 -1
- package/dist/types/use-agent.d.ts +1 -0
- package/dist/types/use-agent.d.ts.map +1 -1
- package/dist/utils/extract-tool-metadata.d.ts +2 -3
- package/dist/utils/extract-tool-metadata.d.ts.map +1 -1
- package/package.json +14 -5
- package/scripts/arcten-cli.cjs +39 -29
- package/scripts/cli-create-project.ts +608 -0
- package/scripts/cli-create-wrapper.cjs +60 -0
- package/scripts/cli-extract-types-auto.ts +153 -1
- package/scripts/cli-init-wizard-wrapper.cjs +4 -3
- package/scripts/cli-init-wizard.ts +503 -823
- package/scripts/cli-sync-wrapper.cjs +69 -0
- package/scripts/cli-sync.ts +596 -0
- package/scripts/config-parser.ts +432 -0
- package/scripts/dashboard-sync.ts +454 -0
- package/scripts/tree-sitter-discover.ts +542 -0
- package/scripts/wasm/tree-sitter-tsx.wasm +0 -0
- package/scripts/wasm/tree-sitter-typescript.wasm +0 -0
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Wrapper for cli-sync.ts
|
|
4
|
+
* Handles TypeScript execution via tsx
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
const { spawn, execSync } = require('child_process');
|
|
8
|
+
const path = require('path');
|
|
9
|
+
const fs = require('fs');
|
|
10
|
+
|
|
11
|
+
const scriptPath = path.join(__dirname, 'cli-sync.ts');
|
|
12
|
+
|
|
13
|
+
// Check if script exists
|
|
14
|
+
if (!fs.existsSync(scriptPath)) {
|
|
15
|
+
console.error('❌ cli-sync.ts not found');
|
|
16
|
+
process.exit(1);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
// Try tsx first (most reliable for TypeScript)
|
|
20
|
+
function tryTsx() {
|
|
21
|
+
try {
|
|
22
|
+
execSync('npx tsx --version', { stdio: 'ignore' });
|
|
23
|
+
return true;
|
|
24
|
+
} catch {
|
|
25
|
+
return false;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// Try bun
|
|
30
|
+
function tryBun() {
|
|
31
|
+
try {
|
|
32
|
+
execSync('bun --version', { stdio: 'ignore' });
|
|
33
|
+
return true;
|
|
34
|
+
} catch {
|
|
35
|
+
return false;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
let runner, args;
|
|
40
|
+
|
|
41
|
+
if (tryTsx()) {
|
|
42
|
+
runner = 'npx';
|
|
43
|
+
args = ['tsx', scriptPath, ...process.argv.slice(2)];
|
|
44
|
+
} else if (tryBun()) {
|
|
45
|
+
runner = 'bun';
|
|
46
|
+
args = [scriptPath, ...process.argv.slice(2)];
|
|
47
|
+
} else {
|
|
48
|
+
console.error('❌ No TypeScript runner found.');
|
|
49
|
+
console.error('');
|
|
50
|
+
console.error('Install one of:');
|
|
51
|
+
console.error(' npm install -g tsx');
|
|
52
|
+
console.error(' OR install Bun: https://bun.sh');
|
|
53
|
+
process.exit(1);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const child = spawn(runner, args, {
|
|
57
|
+
stdio: 'inherit',
|
|
58
|
+
cwd: process.cwd(),
|
|
59
|
+
shell: true
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
child.on('error', (error) => {
|
|
63
|
+
console.error('❌ Failed to run sync:', error.message);
|
|
64
|
+
process.exit(1);
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
child.on('exit', (code) => {
|
|
68
|
+
process.exit(code || 0);
|
|
69
|
+
});
|
|
@@ -0,0 +1,596 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Arcten Sync - Sync tools from codebase to dashboard
|
|
4
|
+
*
|
|
5
|
+
* Runs:
|
|
6
|
+
* 1. arcten-extract-types (generates .arcten/tool-metadata.ts)
|
|
7
|
+
* 2. Generates .arcten/arcten.tools.ts with imports and arrays
|
|
8
|
+
* 3. Updates .arcten/sync-state.json for diff tracking
|
|
9
|
+
* 4. Syncs to dashboard (functions are READONLY)
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import * as fs from 'fs';
|
|
13
|
+
import * as path from 'path';
|
|
14
|
+
import * as readline from 'readline';
|
|
15
|
+
import { execSync } from 'child_process';
|
|
16
|
+
|
|
17
|
+
interface ToolMetadata {
|
|
18
|
+
generated: string;
|
|
19
|
+
discoveredFrom: string[];
|
|
20
|
+
functions: Record<string, {
|
|
21
|
+
name: string;
|
|
22
|
+
description?: string;
|
|
23
|
+
parameters: unknown;
|
|
24
|
+
returnType: string;
|
|
25
|
+
isAsync: boolean;
|
|
26
|
+
}>;
|
|
27
|
+
toolOrder: string[];
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
interface SyncState {
|
|
31
|
+
lastSync: string;
|
|
32
|
+
toolsFile: string;
|
|
33
|
+
functions: Record<string, {
|
|
34
|
+
classification: 'safe' | 'sensitive';
|
|
35
|
+
}>;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
interface ClassificationResult {
|
|
39
|
+
source: string;
|
|
40
|
+
classifications: Record<string, 'safe' | 'sensitive'>;
|
|
41
|
+
safe: string[];
|
|
42
|
+
sensitive: string[];
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// State
|
|
46
|
+
let rl: readline.Interface;
|
|
47
|
+
let apiKey: string;
|
|
48
|
+
let projectId: string;
|
|
49
|
+
let serverUrl: string;
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Read tool-metadata.ts to get current functions
|
|
53
|
+
*/
|
|
54
|
+
function readToolMetadata(projectRoot: string): ToolMetadata | null {
|
|
55
|
+
const metadataPath = path.join(projectRoot, '.arcten', 'tool-metadata.ts');
|
|
56
|
+
|
|
57
|
+
if (!fs.existsSync(metadataPath)) {
|
|
58
|
+
return null;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const content = fs.readFileSync(metadataPath, 'utf-8');
|
|
62
|
+
|
|
63
|
+
// Extract the object from "export const toolMetadata = { ... } as const;"
|
|
64
|
+
const match = content.match(/export const toolMetadata = ({[\s\S]*}) as const;/);
|
|
65
|
+
if (!match) {
|
|
66
|
+
return null;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
try {
|
|
70
|
+
// Use eval to parse the object (it's generated code, safe to eval)
|
|
71
|
+
const metadata = eval(`(${match[1]})`);
|
|
72
|
+
return metadata as ToolMetadata;
|
|
73
|
+
} catch {
|
|
74
|
+
return null;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Read sync-state.json to get previous state
|
|
80
|
+
*/
|
|
81
|
+
function readSyncState(projectRoot: string): SyncState | null {
|
|
82
|
+
const statePath = path.join(projectRoot, '.arcten', 'sync-state.json');
|
|
83
|
+
|
|
84
|
+
if (!fs.existsSync(statePath)) {
|
|
85
|
+
return null;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
try {
|
|
89
|
+
const content = fs.readFileSync(statePath, 'utf-8');
|
|
90
|
+
const parsed = JSON.parse(content) as SyncState;
|
|
91
|
+
// Validate the structure
|
|
92
|
+
if (!parsed.functions || typeof parsed.functions !== 'object') {
|
|
93
|
+
return null;
|
|
94
|
+
}
|
|
95
|
+
return parsed;
|
|
96
|
+
} catch {
|
|
97
|
+
return null;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Write sync-state.json
|
|
103
|
+
*/
|
|
104
|
+
function writeSyncState(projectRoot: string, state: SyncState): void {
|
|
105
|
+
const arctenDir = path.join(projectRoot, '.arcten');
|
|
106
|
+
if (!fs.existsSync(arctenDir)) {
|
|
107
|
+
fs.mkdirSync(arctenDir, { recursive: true });
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
const statePath = path.join(arctenDir, 'sync-state.json');
|
|
111
|
+
fs.writeFileSync(statePath, JSON.stringify(state, null, 2));
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Generate arcten.tools.ts with imports and arrays
|
|
116
|
+
*/
|
|
117
|
+
function generateToolsFile(
|
|
118
|
+
projectRoot: string,
|
|
119
|
+
toolsFilePath: string,
|
|
120
|
+
safeTools: string[],
|
|
121
|
+
sensitiveTools: string[]
|
|
122
|
+
): void {
|
|
123
|
+
const arctenDir = path.join(projectRoot, '.arcten');
|
|
124
|
+
if (!fs.existsSync(arctenDir)) {
|
|
125
|
+
fs.mkdirSync(arctenDir, { recursive: true });
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// Calculate relative import path from .arcten to tools file
|
|
129
|
+
const toolsFileRelative = path.relative(arctenDir, path.join(projectRoot, toolsFilePath));
|
|
130
|
+
const importPath = toolsFileRelative.replace(/\.ts$/, '').replace(/\\/g, '/');
|
|
131
|
+
|
|
132
|
+
const allTools = [...safeTools, ...sensitiveTools];
|
|
133
|
+
|
|
134
|
+
const content = `// .arcten/arcten.tools.ts - AUTO-GENERATED by arcten sync
|
|
135
|
+
// DO NOT EDIT - run \`arcten sync\` to regenerate
|
|
136
|
+
|
|
137
|
+
import {
|
|
138
|
+
${allTools.map(t => ` ${t},`).join('\n')}
|
|
139
|
+
} from '${importPath}';
|
|
140
|
+
|
|
141
|
+
// Safe tools - auto-execute without user approval
|
|
142
|
+
export const safeTools = [
|
|
143
|
+
${safeTools.map(t => ` ${t},`).join('\n')}
|
|
144
|
+
] as const;
|
|
145
|
+
|
|
146
|
+
// Sensitive tools - require user approval before execution
|
|
147
|
+
export const sensitiveTools = [
|
|
148
|
+
${sensitiveTools.map(t => ` ${t},`).join('\n')}
|
|
149
|
+
] as const;
|
|
150
|
+
|
|
151
|
+
// All tools combined
|
|
152
|
+
export const allTools = [...safeTools, ...sensitiveTools];
|
|
153
|
+
|
|
154
|
+
// Tool names for the safeToolNames prop
|
|
155
|
+
export const safeToolNames = safeTools.map(fn => fn.name);
|
|
156
|
+
|
|
157
|
+
// Type exports
|
|
158
|
+
export type SafeToolName = typeof safeTools[number]['name'];
|
|
159
|
+
export type SensitiveToolName = typeof sensitiveTools[number]['name'];
|
|
160
|
+
export type AllToolName = SafeToolName | SensitiveToolName;
|
|
161
|
+
`;
|
|
162
|
+
|
|
163
|
+
const outputPath = path.join(arctenDir, 'arcten.tools.ts');
|
|
164
|
+
fs.writeFileSync(outputPath, content);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* Classify tools using AI or pattern fallback
|
|
169
|
+
*/
|
|
170
|
+
async function classifyTools(
|
|
171
|
+
tools: Array<{ name: string; description?: string }>,
|
|
172
|
+
existingClassifications: Record<string, { classification: 'safe' | 'sensitive' }>
|
|
173
|
+
): Promise<ClassificationResult> {
|
|
174
|
+
// Filter to only new tools (not already classified)
|
|
175
|
+
const newTools = tools.filter(t => !existingClassifications[t.name]);
|
|
176
|
+
|
|
177
|
+
if (newTools.length === 0) {
|
|
178
|
+
// All tools already classified
|
|
179
|
+
const classifications: Record<string, 'safe' | 'sensitive'> = {};
|
|
180
|
+
for (const tool of tools) {
|
|
181
|
+
classifications[tool.name] = existingClassifications[tool.name]?.classification || 'sensitive';
|
|
182
|
+
}
|
|
183
|
+
return {
|
|
184
|
+
source: 'existing',
|
|
185
|
+
classifications,
|
|
186
|
+
safe: tools.filter(t => classifications[t.name] === 'safe').map(t => t.name),
|
|
187
|
+
sensitive: tools.filter(t => classifications[t.name] === 'sensitive').map(t => t.name),
|
|
188
|
+
};
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// Try AI classification for new tools
|
|
192
|
+
try {
|
|
193
|
+
const response = await fetch(`${serverUrl}/ai-init/classify`, {
|
|
194
|
+
method: 'POST',
|
|
195
|
+
headers: {
|
|
196
|
+
'Content-Type': 'application/json',
|
|
197
|
+
'x-arcten-api-key': apiKey,
|
|
198
|
+
},
|
|
199
|
+
body: JSON.stringify({
|
|
200
|
+
tools: newTools.map(t => ({ name: t.name, jsDoc: t.description })),
|
|
201
|
+
}),
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
if (response.ok) {
|
|
205
|
+
const aiResult = await response.json() as ClassificationResult;
|
|
206
|
+
|
|
207
|
+
// Merge with existing classifications
|
|
208
|
+
const mergedClassifications: Record<string, 'safe' | 'sensitive'> = {};
|
|
209
|
+
for (const tool of tools) {
|
|
210
|
+
if (existingClassifications[tool.name]) {
|
|
211
|
+
mergedClassifications[tool.name] = existingClassifications[tool.name].classification;
|
|
212
|
+
} else {
|
|
213
|
+
mergedClassifications[tool.name] = aiResult.classifications[tool.name] || 'sensitive';
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
return {
|
|
218
|
+
source: 'ai',
|
|
219
|
+
classifications: mergedClassifications,
|
|
220
|
+
safe: tools.filter(t => mergedClassifications[t.name] === 'safe').map(t => t.name),
|
|
221
|
+
sensitive: tools.filter(t => mergedClassifications[t.name] === 'sensitive').map(t => t.name),
|
|
222
|
+
};
|
|
223
|
+
}
|
|
224
|
+
} catch {
|
|
225
|
+
// Fall through to pattern fallback
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
// Pattern-based fallback
|
|
229
|
+
const safePatterns = /^(get|list|search|find|fetch|load|calculate|count)/i;
|
|
230
|
+
const sensitivePatterns = /^(create|update|delete|set|add|remove|regenerate)/i;
|
|
231
|
+
const sensitiveReads = /^(get|fetch).*(api.?key|credential|secret|password|token)/i;
|
|
232
|
+
|
|
233
|
+
const classifications: Record<string, 'safe' | 'sensitive'> = {};
|
|
234
|
+
|
|
235
|
+
for (const tool of tools) {
|
|
236
|
+
if (existingClassifications[tool.name]) {
|
|
237
|
+
classifications[tool.name] = existingClassifications[tool.name].classification;
|
|
238
|
+
} else if (sensitiveReads.test(tool.name)) {
|
|
239
|
+
classifications[tool.name] = 'sensitive';
|
|
240
|
+
} else if (sensitivePatterns.test(tool.name)) {
|
|
241
|
+
classifications[tool.name] = 'sensitive';
|
|
242
|
+
} else if (safePatterns.test(tool.name)) {
|
|
243
|
+
classifications[tool.name] = 'safe';
|
|
244
|
+
} else {
|
|
245
|
+
classifications[tool.name] = 'sensitive'; // Default to sensitive
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
return {
|
|
250
|
+
source: 'pattern-fallback',
|
|
251
|
+
classifications,
|
|
252
|
+
safe: tools.filter(t => classifications[t.name] === 'safe').map(t => t.name),
|
|
253
|
+
sensitive: tools.filter(t => classifications[t.name] === 'sensitive').map(t => t.name),
|
|
254
|
+
};
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
/**
|
|
258
|
+
* Sync to dashboard
|
|
259
|
+
*/
|
|
260
|
+
async function syncToDashboard(
|
|
261
|
+
tools: Array<{ name: string; description?: string; requiresApproval: boolean }>
|
|
262
|
+
): Promise<{ success: boolean; error?: string }> {
|
|
263
|
+
try {
|
|
264
|
+
// Get JWT token
|
|
265
|
+
const tokenResponse = await fetch(`${serverUrl}/token`, {
|
|
266
|
+
method: 'POST',
|
|
267
|
+
headers: { 'Content-Type': 'application/json' },
|
|
268
|
+
body: JSON.stringify({ apiKey }),
|
|
269
|
+
});
|
|
270
|
+
|
|
271
|
+
if (!tokenResponse.ok) {
|
|
272
|
+
return { success: false, error: 'Failed to authenticate' };
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
const { clientToken: token } = await tokenResponse.json();
|
|
276
|
+
|
|
277
|
+
// Format tools for sync
|
|
278
|
+
const toolsData = tools.map(tool => ({
|
|
279
|
+
name: tool.name,
|
|
280
|
+
description: tool.description || `Function: ${tool.name}`,
|
|
281
|
+
signature: JSON.stringify({}),
|
|
282
|
+
isEnabled: true,
|
|
283
|
+
isOverridable: true,
|
|
284
|
+
requiresApproval: tool.requiresApproval,
|
|
285
|
+
sensitiveParams: [],
|
|
286
|
+
readonly: true, // Functions are readonly in dashboard
|
|
287
|
+
syncedAt: Date.now(),
|
|
288
|
+
}));
|
|
289
|
+
|
|
290
|
+
// Sync to dashboard
|
|
291
|
+
const syncResponse = await fetch(`${serverUrl}/tools/sync`, {
|
|
292
|
+
method: 'POST',
|
|
293
|
+
headers: {
|
|
294
|
+
'Content-Type': 'application/json',
|
|
295
|
+
'Authorization': `Bearer ${token}`,
|
|
296
|
+
},
|
|
297
|
+
body: JSON.stringify({
|
|
298
|
+
projectId,
|
|
299
|
+
tools: toolsData,
|
|
300
|
+
}),
|
|
301
|
+
});
|
|
302
|
+
|
|
303
|
+
if (!syncResponse.ok) {
|
|
304
|
+
const error = await syncResponse.text();
|
|
305
|
+
return { success: false, error: `Sync failed: ${error}` };
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
return { success: true };
|
|
309
|
+
} catch (error: any) {
|
|
310
|
+
return { success: false, error: error.message };
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
/**
|
|
315
|
+
* Check for API key
|
|
316
|
+
*/
|
|
317
|
+
function checkApiKey(): { apiKey: string; projectId: string } | null {
|
|
318
|
+
const envFiles = ['.env.local', '.env'];
|
|
319
|
+
|
|
320
|
+
for (const envFile of envFiles) {
|
|
321
|
+
const envPath = path.join(process.cwd(), envFile);
|
|
322
|
+
if (fs.existsSync(envPath)) {
|
|
323
|
+
const envContent = fs.readFileSync(envPath, 'utf-8');
|
|
324
|
+
const match = envContent.match(/ARCTEN_API_KEY=([^\s\n]+)/);
|
|
325
|
+
if (match) {
|
|
326
|
+
const key = match[1];
|
|
327
|
+
const projectIdMatch = key.match(/sk_(proj_[a-zA-Z0-9]+)_/);
|
|
328
|
+
if (projectIdMatch) {
|
|
329
|
+
return { apiKey: key, projectId: projectIdMatch[1] };
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
return null;
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
/**
|
|
339
|
+
* Ask user a yes/no question
|
|
340
|
+
*/
|
|
341
|
+
function askYesNo(question: string): Promise<boolean> {
|
|
342
|
+
return new Promise((resolve) => {
|
|
343
|
+
rl.question(`${question} (y/n) `, (answer) => {
|
|
344
|
+
resolve(answer.trim().toLowerCase().startsWith('y'));
|
|
345
|
+
});
|
|
346
|
+
});
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
/**
|
|
350
|
+
* Main sync function
|
|
351
|
+
*/
|
|
352
|
+
async function main() {
|
|
353
|
+
const projectRoot = process.cwd();
|
|
354
|
+
const args = process.argv.slice(2);
|
|
355
|
+
const autoYes = args.includes('--yes') || args.includes('-y');
|
|
356
|
+
const quiet = autoYes; // Quiet mode when auto-yes
|
|
357
|
+
|
|
358
|
+
if (!quiet) {
|
|
359
|
+
console.log('');
|
|
360
|
+
console.log('Arcten Sync');
|
|
361
|
+
console.log('');
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
// Check for API key
|
|
365
|
+
const auth = checkApiKey();
|
|
366
|
+
if (!auth) {
|
|
367
|
+
if (!quiet) {
|
|
368
|
+
console.log('No API key found. Run `arcten init` first.');
|
|
369
|
+
}
|
|
370
|
+
process.exit(1);
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
apiKey = auth.apiKey;
|
|
374
|
+
projectId = auth.projectId;
|
|
375
|
+
serverUrl = process.env.ARCTEN_SERVER_URL || 'https://api.arcten.com';
|
|
376
|
+
|
|
377
|
+
// Create readline interface (only if interactive)
|
|
378
|
+
if (!autoYes) {
|
|
379
|
+
rl = readline.createInterface({
|
|
380
|
+
input: process.stdin,
|
|
381
|
+
output: process.stdout,
|
|
382
|
+
});
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
try {
|
|
386
|
+
// Step 1: Run arcten-extract-types to update tool-metadata.ts
|
|
387
|
+
if (!quiet) console.log('Extracting tool types...');
|
|
388
|
+
// Get script directory (ESM compatible)
|
|
389
|
+
const scriptDir = path.dirname(new URL(import.meta.url).pathname).replace(/^\/([A-Z]:)/, '$1');
|
|
390
|
+
|
|
391
|
+
try {
|
|
392
|
+
// Try running the wrapper script directly first
|
|
393
|
+
const extractScript = path.join(scriptDir, 'cli-extract-types-auto-wrapper.js');
|
|
394
|
+
if (fs.existsSync(extractScript)) {
|
|
395
|
+
execSync(`node "${extractScript}"`, {
|
|
396
|
+
cwd: projectRoot,
|
|
397
|
+
stdio: quiet ? 'pipe' : ['inherit', 'pipe', 'pipe'],
|
|
398
|
+
});
|
|
399
|
+
if (!quiet) console.log(' Done');
|
|
400
|
+
} else {
|
|
401
|
+
// Fall back to npx
|
|
402
|
+
execSync('npx arcten-extract-types', {
|
|
403
|
+
cwd: projectRoot,
|
|
404
|
+
stdio: quiet ? 'pipe' : ['inherit', 'pipe', 'pipe'],
|
|
405
|
+
});
|
|
406
|
+
if (!quiet) console.log(' Done');
|
|
407
|
+
}
|
|
408
|
+
} catch (error: any) {
|
|
409
|
+
if (!quiet) console.log(' Warning: Could not run extract-types, continuing with existing metadata...');
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
// Step 2: Read tool-metadata.ts
|
|
413
|
+
if (!quiet) console.log('Reading tool metadata...');
|
|
414
|
+
const metadata = readToolMetadata(projectRoot);
|
|
415
|
+
|
|
416
|
+
if (!metadata) {
|
|
417
|
+
if (!quiet) {
|
|
418
|
+
console.log('');
|
|
419
|
+
console.log('No tool-metadata.ts found.');
|
|
420
|
+
console.log('Make sure you have a tools file (e.g., app/tools.ts) with exported functions.');
|
|
421
|
+
console.log('');
|
|
422
|
+
}
|
|
423
|
+
if (rl) rl.close();
|
|
424
|
+
process.exit(1);
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
const currentFunctions = Object.keys(metadata.functions);
|
|
428
|
+
const toolsFile = metadata.discoveredFrom[0] || 'app/tools.ts';
|
|
429
|
+
|
|
430
|
+
if (!quiet) console.log(`Found ${currentFunctions.length} functions from ${toolsFile}`);
|
|
431
|
+
|
|
432
|
+
// Step 2: Read previous sync state
|
|
433
|
+
const previousState = readSyncState(projectRoot);
|
|
434
|
+
const previousFunctions = previousState
|
|
435
|
+
? Object.keys(previousState.functions)
|
|
436
|
+
: [];
|
|
437
|
+
|
|
438
|
+
// Step 3: Compute diff
|
|
439
|
+
const newFunctions = currentFunctions.filter(f => !previousFunctions.includes(f));
|
|
440
|
+
const removedFunctions = previousFunctions.filter(f => !currentFunctions.includes(f));
|
|
441
|
+
|
|
442
|
+
// If no changes and in auto mode, exit silently
|
|
443
|
+
if (newFunctions.length === 0 && removedFunctions.length === 0 && previousState) {
|
|
444
|
+
if (autoYes) {
|
|
445
|
+
// Silent exit - no changes needed
|
|
446
|
+
if (rl) rl.close();
|
|
447
|
+
process.exit(0);
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
console.log('');
|
|
451
|
+
console.log('No changes detected since last sync.');
|
|
452
|
+
|
|
453
|
+
const resync = await askYesNo('Re-sync anyway?');
|
|
454
|
+
if (!resync) {
|
|
455
|
+
console.log('Sync cancelled.');
|
|
456
|
+
if (rl) rl.close();
|
|
457
|
+
process.exit(0);
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
// Show changes if any
|
|
462
|
+
if (newFunctions.length > 0) {
|
|
463
|
+
console.log('');
|
|
464
|
+
console.log(`New functions since last sync:`);
|
|
465
|
+
for (const fn of newFunctions) {
|
|
466
|
+
console.log(` + ${fn}`);
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
if (removedFunctions.length > 0) {
|
|
471
|
+
console.log('');
|
|
472
|
+
console.log(`Removed functions:`);
|
|
473
|
+
for (const fn of removedFunctions) {
|
|
474
|
+
console.log(` - ${fn}`);
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
// Step 4: Classify tools (AI for new, preserve existing)
|
|
479
|
+
if (!quiet) {
|
|
480
|
+
console.log('');
|
|
481
|
+
console.log('Classifying tools...');
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
const existingClassifications = previousState?.functions || {};
|
|
485
|
+
const toolsToClassify = currentFunctions.map(name => ({
|
|
486
|
+
name,
|
|
487
|
+
description: metadata.functions[name]?.description,
|
|
488
|
+
}));
|
|
489
|
+
|
|
490
|
+
const classification = await classifyTools(toolsToClassify, existingClassifications);
|
|
491
|
+
|
|
492
|
+
if (!quiet) {
|
|
493
|
+
console.log('');
|
|
494
|
+
console.log(`Classification (${classification.source}):`);
|
|
495
|
+
console.log(` Safe (auto-execute): ${classification.safe.length} tools`);
|
|
496
|
+
if (classification.safe.length > 0 && classification.safe.length <= 10) {
|
|
497
|
+
console.log(` ${classification.safe.join(', ')}`);
|
|
498
|
+
}
|
|
499
|
+
console.log(` Sensitive (needs approval): ${classification.sensitive.length} tools`);
|
|
500
|
+
if (classification.sensitive.length > 0 && classification.sensitive.length <= 10) {
|
|
501
|
+
console.log(` ${classification.sensitive.join(', ')}`);
|
|
502
|
+
}
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
// Step 5: Confirm with user (skip if auto-yes)
|
|
506
|
+
if (!autoYes) {
|
|
507
|
+
console.log('');
|
|
508
|
+
const proceed = await askYesNo('Does this look right?');
|
|
509
|
+
|
|
510
|
+
if (!proceed) {
|
|
511
|
+
console.log('');
|
|
512
|
+
console.log('You can adjust classifications in .arcten/sync-state.json and re-run sync.');
|
|
513
|
+
if (rl) rl.close();
|
|
514
|
+
process.exit(0);
|
|
515
|
+
}
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
// Step 6: Generate arcten.tools.ts
|
|
519
|
+
if (!quiet) {
|
|
520
|
+
console.log('');
|
|
521
|
+
console.log('Generating .arcten/arcten.tools.ts...');
|
|
522
|
+
}
|
|
523
|
+
generateToolsFile(projectRoot, toolsFile.replace(/\\/g, '/'), classification.safe, classification.sensitive);
|
|
524
|
+
if (!quiet) console.log(' Created .arcten/arcten.tools.ts');
|
|
525
|
+
|
|
526
|
+
// Step 7: Update sync-state.json
|
|
527
|
+
const newState: SyncState = {
|
|
528
|
+
lastSync: new Date().toISOString(),
|
|
529
|
+
toolsFile,
|
|
530
|
+
functions: {},
|
|
531
|
+
};
|
|
532
|
+
for (const name of currentFunctions) {
|
|
533
|
+
newState.functions[name] = {
|
|
534
|
+
classification: classification.classifications[name],
|
|
535
|
+
};
|
|
536
|
+
}
|
|
537
|
+
writeSyncState(projectRoot, newState);
|
|
538
|
+
if (!quiet) console.log(' Updated .arcten/sync-state.json');
|
|
539
|
+
|
|
540
|
+
// Step 8: Sync to dashboard (skip logging in quiet mode unless there's an error)
|
|
541
|
+
if (!quiet) {
|
|
542
|
+
console.log('');
|
|
543
|
+
console.log('Syncing to dashboard...');
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
const toolsForDashboard = currentFunctions.map(name => ({
|
|
547
|
+
name,
|
|
548
|
+
description: metadata.functions[name]?.description,
|
|
549
|
+
requiresApproval: classification.classifications[name] === 'sensitive',
|
|
550
|
+
}));
|
|
551
|
+
|
|
552
|
+
const syncResult = await syncToDashboard(toolsForDashboard);
|
|
553
|
+
|
|
554
|
+
if (syncResult.success) {
|
|
555
|
+
if (!quiet) console.log(` Synced ${toolsForDashboard.length} tools to dashboard`);
|
|
556
|
+
} else {
|
|
557
|
+
// Always show dashboard errors
|
|
558
|
+
console.log(` Warning: Dashboard sync failed: ${syncResult.error}`);
|
|
559
|
+
if (!quiet) console.log(' Local files were still updated.');
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
if (!quiet) {
|
|
563
|
+
console.log('');
|
|
564
|
+
console.log('Sync complete!');
|
|
565
|
+
console.log('');
|
|
566
|
+
console.log('Usage in your components:');
|
|
567
|
+
console.log('');
|
|
568
|
+
console.log(' import { allTools, safeToolNames } from "./.arcten/arcten.tools";');
|
|
569
|
+
console.log('');
|
|
570
|
+
console.log(' <ArctenAgent');
|
|
571
|
+
console.log(' tools={allTools}');
|
|
572
|
+
console.log(' safeToolNames={safeToolNames}');
|
|
573
|
+
console.log(' />');
|
|
574
|
+
console.log('');
|
|
575
|
+
} else {
|
|
576
|
+
// Brief message when there were changes in quiet mode
|
|
577
|
+
if (newFunctions.length > 0 || removedFunctions.length > 0) {
|
|
578
|
+
console.log(`✓ Synced ${newFunctions.length} new, ${removedFunctions.length} removed tools`);
|
|
579
|
+
}
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
} catch (error: any) {
|
|
583
|
+
console.error('Error:', error.message);
|
|
584
|
+
process.exit(1);
|
|
585
|
+
} finally {
|
|
586
|
+
if (rl) rl.close();
|
|
587
|
+
}
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
// Run
|
|
591
|
+
main().catch((error) => {
|
|
592
|
+
console.error('Fatal error:', error);
|
|
593
|
+
process.exit(1);
|
|
594
|
+
});
|
|
595
|
+
|
|
596
|
+
export { main };
|