humanbehavior-js 0.7.0 → 0.7.1
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/package.json +1 -1
- package/packages/wizard/dist/agent/agent-installation-runner.d.ts +45 -0
- package/packages/wizard/dist/agent/agent-installation-runner.d.ts.map +1 -0
- package/packages/wizard/dist/agent/local-tool-executor.d.ts +35 -0
- package/packages/wizard/dist/agent/local-tool-executor.d.ts.map +1 -0
- package/packages/wizard/dist/agent/managed-wizard-api-client.d.ts +81 -0
- package/packages/wizard/dist/agent/managed-wizard-api-client.d.ts.map +1 -0
- package/packages/wizard/dist/cli/ai-auto-install.d.ts +7 -7
- package/packages/wizard/dist/cli/ai-auto-install.d.ts.map +1 -1
- package/packages/wizard/dist/cli/ai-auto-install.js +569 -1862
- package/packages/wizard/dist/cli/ai-auto-install.js.map +1 -1
- package/packages/wizard/dist/index.d.ts +6 -0
- package/packages/wizard/dist/index.d.ts.map +1 -1
- package/packages/wizard/dist/index.js +1 -1
- package/packages/wizard/dist/index.js.map +1 -1
- package/packages/wizard/dist/index.mjs +1 -1
- package/packages/wizard/dist/index.mjs.map +1 -1
|
@@ -1,1890 +1,621 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
+
import * as clack from '@clack/prompts';
|
|
2
3
|
import * as fs from 'fs';
|
|
3
4
|
import * as path from 'path';
|
|
4
|
-
import {
|
|
5
|
-
import
|
|
5
|
+
import { exec } from 'child_process';
|
|
6
|
+
import { promisify } from 'util';
|
|
6
7
|
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
class
|
|
14
|
-
constructor(
|
|
15
|
-
this.
|
|
16
|
-
this.
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
}
|
|
20
|
-
/**
|
|
21
|
-
* Simple version comparison utility
|
|
22
|
-
*/
|
|
23
|
-
compareVersions(version1, version2) {
|
|
24
|
-
const v1Parts = version1.split('.').map(Number);
|
|
25
|
-
const v2Parts = version2.split('.').map(Number);
|
|
26
|
-
for (let i = 0; i < Math.max(v1Parts.length, v2Parts.length); i++) {
|
|
27
|
-
const v1 = v1Parts[i] || 0;
|
|
28
|
-
const v2 = v2Parts[i] || 0;
|
|
29
|
-
if (v1 > v2)
|
|
30
|
-
return 1;
|
|
31
|
-
if (v1 < v2)
|
|
32
|
-
return -1;
|
|
33
|
-
}
|
|
34
|
-
return 0;
|
|
35
|
-
}
|
|
36
|
-
isVersionGte(version, target) {
|
|
37
|
-
return this.compareVersions(version, target) >= 0;
|
|
38
|
-
}
|
|
39
|
-
getMajorVersion(version) {
|
|
40
|
-
return parseInt(version.split('.')[0]) || 0;
|
|
41
|
-
}
|
|
42
|
-
/**
|
|
43
|
-
* Main installation method - detects framework and auto-installs
|
|
44
|
-
*/
|
|
45
|
-
async install() {
|
|
8
|
+
const execAsync = promisify(exec);
|
|
9
|
+
const MAX_READ_BYTES = 24000;
|
|
10
|
+
const MAX_SEARCH_RESULTS = 40;
|
|
11
|
+
const MAX_BASH_OUTPUT = 24000;
|
|
12
|
+
const VERIFY_EVENT = '$humanbehavior_wizard_verified';
|
|
13
|
+
const DEFAULT_INGESTION_URL = 'https://ingest.humanbehavior.co';
|
|
14
|
+
class LocalToolExecutor {
|
|
15
|
+
constructor(projectRoot, verification) {
|
|
16
|
+
this.projectRoot = path.resolve(projectRoot);
|
|
17
|
+
this.verification = verification;
|
|
18
|
+
}
|
|
19
|
+
async execute(request) {
|
|
46
20
|
try {
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
21
|
+
let content;
|
|
22
|
+
switch (request.name) {
|
|
23
|
+
case 'hb_list_files':
|
|
24
|
+
content = this.listFiles(request.input);
|
|
25
|
+
break;
|
|
26
|
+
case 'hb_read_file':
|
|
27
|
+
content = this.readFile(request.input);
|
|
28
|
+
break;
|
|
29
|
+
case 'hb_write_file':
|
|
30
|
+
content = this.writeFile(request.input);
|
|
31
|
+
break;
|
|
32
|
+
case 'hb_grep':
|
|
33
|
+
content = this.grep(request.input);
|
|
34
|
+
break;
|
|
35
|
+
case 'hb_bash':
|
|
36
|
+
content = await this.bash(request.input);
|
|
37
|
+
break;
|
|
38
|
+
case 'hb_verify_installation':
|
|
39
|
+
content = await this.verifyInstallation();
|
|
40
|
+
break;
|
|
41
|
+
default:
|
|
42
|
+
throw new Error(`Unknown tool: ${request.name}`);
|
|
43
|
+
}
|
|
56
44
|
return {
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
errors: [],
|
|
61
|
-
nextSteps
|
|
45
|
+
type: 'tool_result',
|
|
46
|
+
tool_use_id: request.id,
|
|
47
|
+
content
|
|
62
48
|
};
|
|
63
49
|
}
|
|
64
50
|
catch (error) {
|
|
65
51
|
return {
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
nextSteps: []
|
|
52
|
+
type: 'tool_result',
|
|
53
|
+
tool_use_id: request.id,
|
|
54
|
+
content: error instanceof Error ? error.message : 'Unknown tool error',
|
|
55
|
+
is_error: true
|
|
71
56
|
};
|
|
72
57
|
}
|
|
73
58
|
}
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
const
|
|
87
|
-
const
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
59
|
+
resolveProjectPath(inputPath) {
|
|
60
|
+
const relativePath = typeof inputPath === 'string' && inputPath.trim() ? inputPath : '.';
|
|
61
|
+
const resolved = path.resolve(this.projectRoot, relativePath);
|
|
62
|
+
if (resolved !== this.projectRoot && !resolved.startsWith(`${this.projectRoot}${path.sep}`)) {
|
|
63
|
+
throw new Error(`Path escapes project root: ${relativePath}`);
|
|
64
|
+
}
|
|
65
|
+
return resolved;
|
|
66
|
+
}
|
|
67
|
+
listFiles(input) {
|
|
68
|
+
const start = this.resolveProjectPath(input.path);
|
|
69
|
+
const maxDepth = typeof input.maxDepth === 'number' ? Math.max(0, Math.min(input.maxDepth, 6)) : 3;
|
|
70
|
+
const ignored = new Set(['.git', '.next', 'dist', 'build', 'coverage', 'node_modules']);
|
|
71
|
+
const results = [];
|
|
72
|
+
const visit = (dir, depth) => {
|
|
73
|
+
if (depth > maxDepth || results.length >= 300)
|
|
74
|
+
return;
|
|
75
|
+
const entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
76
|
+
for (const entry of entries) {
|
|
77
|
+
if (ignored.has(entry.name) || entry.name.endsWith('-lock.json') || entry.name === 'pnpm-lock.yaml' || entry.name === 'yarn.lock')
|
|
78
|
+
continue;
|
|
79
|
+
const fullPath = path.join(dir, entry.name);
|
|
80
|
+
const relative = path.relative(this.projectRoot, fullPath) || '.';
|
|
81
|
+
results.push(entry.isDirectory() ? `${relative}/` : relative);
|
|
82
|
+
if (entry.isDirectory())
|
|
83
|
+
visit(fullPath, depth + 1);
|
|
84
|
+
if (results.length >= 120)
|
|
85
|
+
break;
|
|
86
|
+
}
|
|
97
87
|
};
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
const
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
projectRoot: this.projectRoot,
|
|
140
|
-
features: {}
|
|
141
|
-
};
|
|
142
|
-
}
|
|
143
|
-
else if (dependencies.react) {
|
|
144
|
-
const reactVersion = dependencies.react;
|
|
145
|
-
const isReact18 = this.isVersionGte(reactVersion, '18.0.0');
|
|
146
|
-
framework = {
|
|
147
|
-
name: 'react',
|
|
148
|
-
type: 'react',
|
|
149
|
-
version: reactVersion,
|
|
150
|
-
majorVersion: this.getMajorVersion(reactVersion),
|
|
151
|
-
hasTypeScript: !!dependencies.typescript || !!dependencies['@types/react'],
|
|
152
|
-
hasRouter: !!dependencies['react-router-dom'] || !!dependencies['react-router'],
|
|
153
|
-
projectRoot: this.projectRoot,
|
|
154
|
-
features: {
|
|
155
|
-
hasReact18: isReact18
|
|
156
|
-
}
|
|
157
|
-
};
|
|
158
|
-
}
|
|
159
|
-
else if (dependencies.vue) {
|
|
160
|
-
const vueVersion = dependencies.vue;
|
|
161
|
-
const isVue3 = this.isVersionGte(vueVersion, '3.0.0');
|
|
162
|
-
framework = {
|
|
163
|
-
name: 'vue',
|
|
164
|
-
type: 'vue',
|
|
165
|
-
version: vueVersion,
|
|
166
|
-
majorVersion: this.getMajorVersion(vueVersion),
|
|
167
|
-
hasTypeScript: !!dependencies.typescript || !!dependencies['@vue/cli-service'],
|
|
168
|
-
hasRouter: !!dependencies['vue-router'],
|
|
169
|
-
projectRoot: this.projectRoot,
|
|
170
|
-
features: {
|
|
171
|
-
hasVue3: isVue3
|
|
172
|
-
}
|
|
173
|
-
};
|
|
174
|
-
}
|
|
175
|
-
else if (dependencies['@angular/core']) {
|
|
176
|
-
const angularVersion = dependencies['@angular/core'];
|
|
177
|
-
const isAngular17 = this.isVersionGte(angularVersion, '17.0.0');
|
|
178
|
-
framework = {
|
|
179
|
-
name: 'angular',
|
|
180
|
-
type: 'angular',
|
|
181
|
-
version: angularVersion,
|
|
182
|
-
majorVersion: this.getMajorVersion(angularVersion),
|
|
183
|
-
hasTypeScript: true,
|
|
184
|
-
hasRouter: true,
|
|
185
|
-
projectRoot: this.projectRoot,
|
|
186
|
-
features: {
|
|
187
|
-
hasAngularStandalone: isAngular17
|
|
88
|
+
visit(start, 0);
|
|
89
|
+
return results.slice(0, 120).join('\n') || '(no files)';
|
|
90
|
+
}
|
|
91
|
+
readFile(input) {
|
|
92
|
+
const filePath = this.resolveProjectPath(input.path);
|
|
93
|
+
const maxBytes = typeof input.maxBytes === 'number'
|
|
94
|
+
? Math.max(1, Math.min(input.maxBytes, MAX_READ_BYTES))
|
|
95
|
+
: MAX_READ_BYTES;
|
|
96
|
+
const buffer = fs.readFileSync(filePath);
|
|
97
|
+
const truncated = buffer.length > maxBytes;
|
|
98
|
+
const content = buffer.subarray(0, maxBytes).toString('utf8');
|
|
99
|
+
return truncated ? `${content}\n\n[truncated at ${maxBytes} bytes]` : content;
|
|
100
|
+
}
|
|
101
|
+
writeFile(input) {
|
|
102
|
+
if (typeof input.content !== 'string') {
|
|
103
|
+
throw new Error('content must be a string');
|
|
104
|
+
}
|
|
105
|
+
const filePath = this.resolveProjectPath(input.path);
|
|
106
|
+
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
107
|
+
fs.writeFileSync(filePath, input.content, 'utf8');
|
|
108
|
+
return `Wrote ${Buffer.byteLength(input.content, 'utf8')} bytes to ${path.relative(this.projectRoot, filePath)}`;
|
|
109
|
+
}
|
|
110
|
+
grep(input) {
|
|
111
|
+
if (typeof input.pattern !== 'string' || !input.pattern) {
|
|
112
|
+
throw new Error('pattern is required');
|
|
113
|
+
}
|
|
114
|
+
const root = this.resolveProjectPath(input.path);
|
|
115
|
+
const regex = new RegExp(input.pattern, 'i');
|
|
116
|
+
const ignored = new Set(['.git', '.next', 'dist', 'build', 'coverage', 'node_modules']);
|
|
117
|
+
const results = [];
|
|
118
|
+
const visit = (target) => {
|
|
119
|
+
if (results.length >= MAX_SEARCH_RESULTS)
|
|
120
|
+
return;
|
|
121
|
+
const stat = fs.statSync(target);
|
|
122
|
+
if (stat.isDirectory()) {
|
|
123
|
+
for (const entry of fs.readdirSync(target, { withFileTypes: true })) {
|
|
124
|
+
if (ignored.has(entry.name) || entry.name.endsWith('-lock.json') || entry.name === 'pnpm-lock.yaml' || entry.name === 'yarn.lock')
|
|
125
|
+
continue;
|
|
126
|
+
visit(path.join(target, entry.name));
|
|
127
|
+
if (results.length >= MAX_SEARCH_RESULTS)
|
|
128
|
+
break;
|
|
188
129
|
}
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
const
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
projectRoot: this.projectRoot,
|
|
202
|
-
features: {
|
|
203
|
-
hasSvelteKit: isSvelteKit
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
132
|
+
if (!stat.isFile() || stat.size > 512000)
|
|
133
|
+
return;
|
|
134
|
+
const text = fs.readFileSync(target, 'utf8');
|
|
135
|
+
const lines = text.split(/\r?\n/);
|
|
136
|
+
for (let index = 0; index < lines.length; index++) {
|
|
137
|
+
if (regex.test(lines[index])) {
|
|
138
|
+
const line = lines[index].length > 240 ? `${lines[index].slice(0, 240)}...` : lines[index];
|
|
139
|
+
results.push(`${path.relative(this.projectRoot, target)}:${index + 1}: ${line}`);
|
|
140
|
+
if (results.length >= MAX_SEARCH_RESULTS)
|
|
141
|
+
break;
|
|
204
142
|
}
|
|
205
|
-
};
|
|
206
|
-
}
|
|
207
|
-
else if (dependencies.astro) {
|
|
208
|
-
const astroVersion = dependencies.astro;
|
|
209
|
-
framework = {
|
|
210
|
-
name: 'astro',
|
|
211
|
-
type: 'astro',
|
|
212
|
-
version: astroVersion,
|
|
213
|
-
majorVersion: this.getMajorVersion(astroVersion),
|
|
214
|
-
hasTypeScript: !!dependencies.typescript || !!dependencies['@astrojs/ts-plugin'],
|
|
215
|
-
hasRouter: true,
|
|
216
|
-
projectRoot: this.projectRoot,
|
|
217
|
-
features: {}
|
|
218
|
-
};
|
|
219
|
-
}
|
|
220
|
-
else if (dependencies.gatsby) {
|
|
221
|
-
const gatsbyVersion = dependencies.gatsby;
|
|
222
|
-
framework = {
|
|
223
|
-
name: 'gatsby',
|
|
224
|
-
type: 'gatsby',
|
|
225
|
-
version: gatsbyVersion,
|
|
226
|
-
majorVersion: this.getMajorVersion(gatsbyVersion),
|
|
227
|
-
hasTypeScript: !!dependencies.typescript || !!dependencies['@types/react'],
|
|
228
|
-
hasRouter: true,
|
|
229
|
-
projectRoot: this.projectRoot,
|
|
230
|
-
features: {}
|
|
231
|
-
};
|
|
232
|
-
}
|
|
233
|
-
// Detect bundler
|
|
234
|
-
if (dependencies.vite) {
|
|
235
|
-
framework.bundler = 'vite';
|
|
236
|
-
}
|
|
237
|
-
else if (dependencies.webpack) {
|
|
238
|
-
framework.bundler = 'webpack';
|
|
239
|
-
}
|
|
240
|
-
else if (dependencies.esbuild) {
|
|
241
|
-
framework.bundler = 'esbuild';
|
|
242
|
-
}
|
|
243
|
-
else if (dependencies.rollup) {
|
|
244
|
-
framework.bundler = 'rollup';
|
|
245
|
-
}
|
|
246
|
-
// Detect package manager
|
|
247
|
-
if (fs.existsSync(path.join(this.projectRoot, 'yarn.lock'))) {
|
|
248
|
-
framework.packageManager = 'yarn';
|
|
249
|
-
}
|
|
250
|
-
else if (fs.existsSync(path.join(this.projectRoot, 'pnpm-lock.yaml'))) {
|
|
251
|
-
framework.packageManager = 'pnpm';
|
|
252
|
-
}
|
|
253
|
-
else {
|
|
254
|
-
framework.packageManager = 'npm';
|
|
255
|
-
}
|
|
256
|
-
return framework;
|
|
257
|
-
}
|
|
258
|
-
/**
|
|
259
|
-
* Install the SDK package with latest version range
|
|
260
|
-
*/
|
|
261
|
-
async installPackage() {
|
|
262
|
-
// Build base command with latest version range
|
|
263
|
-
let command = this.framework?.packageManager === 'yarn'
|
|
264
|
-
? 'yarn add humanbehavior-js@latest'
|
|
265
|
-
: this.framework?.packageManager === 'pnpm'
|
|
266
|
-
? 'pnpm add humanbehavior-js@latest'
|
|
267
|
-
: 'npm install humanbehavior-js@latest';
|
|
268
|
-
// Add legacy peer deps flag for npm to handle dependency conflicts
|
|
269
|
-
if (this.framework?.packageManager !== 'yarn' && this.framework?.packageManager !== 'pnpm') {
|
|
270
|
-
command += ' --legacy-peer-deps';
|
|
271
|
-
}
|
|
272
|
-
try {
|
|
273
|
-
execSync(command, { cwd: this.projectRoot, stdio: 'inherit' });
|
|
274
|
-
}
|
|
275
|
-
catch (error) {
|
|
276
|
-
throw new Error(`Failed to install humanbehavior-js: ${error}`);
|
|
277
|
-
}
|
|
278
|
-
}
|
|
279
|
-
/**
|
|
280
|
-
* Generate code modifications based on framework
|
|
281
|
-
*/
|
|
282
|
-
async generateModifications() {
|
|
283
|
-
const modifications = [];
|
|
284
|
-
switch (this.framework?.type) {
|
|
285
|
-
case 'react':
|
|
286
|
-
modifications.push(...await this.generateReactModifications());
|
|
287
|
-
break;
|
|
288
|
-
case 'nextjs':
|
|
289
|
-
modifications.push(...await this.generateNextJSModifications());
|
|
290
|
-
break;
|
|
291
|
-
case 'nuxt':
|
|
292
|
-
modifications.push(...await this.generateNuxtModifications());
|
|
293
|
-
break;
|
|
294
|
-
case 'astro':
|
|
295
|
-
modifications.push(...await this.generateAstroModifications());
|
|
296
|
-
break;
|
|
297
|
-
case 'gatsby':
|
|
298
|
-
modifications.push(...await this.generateGatsbyModifications());
|
|
299
|
-
break;
|
|
300
|
-
case 'remix':
|
|
301
|
-
modifications.push(...await this.generateRemixModifications());
|
|
302
|
-
break;
|
|
303
|
-
case 'vue':
|
|
304
|
-
modifications.push(...await this.generateVueModifications());
|
|
305
|
-
break;
|
|
306
|
-
case 'angular':
|
|
307
|
-
modifications.push(...await this.generateAngularModifications());
|
|
308
|
-
break;
|
|
309
|
-
case 'svelte':
|
|
310
|
-
modifications.push(...await this.generateSvelteModifications());
|
|
311
|
-
break;
|
|
312
|
-
default:
|
|
313
|
-
modifications.push(...await this.generateVanillaModifications());
|
|
314
|
-
}
|
|
315
|
-
return modifications;
|
|
316
|
-
}
|
|
317
|
-
/**
|
|
318
|
-
* Generate React-specific modifications
|
|
319
|
-
*/
|
|
320
|
-
async generateReactModifications() {
|
|
321
|
-
const modifications = [];
|
|
322
|
-
// Find main App component or index file
|
|
323
|
-
const appFile = this.findReactAppFile();
|
|
324
|
-
if (appFile) {
|
|
325
|
-
const content = fs.readFileSync(appFile, 'utf8');
|
|
326
|
-
const modifiedContent = this.injectReactProvider(content, appFile);
|
|
327
|
-
modifications.push({
|
|
328
|
-
filePath: appFile,
|
|
329
|
-
action: 'modify',
|
|
330
|
-
content: modifiedContent,
|
|
331
|
-
description: 'Added HumanBehaviorProvider to React app'
|
|
332
|
-
});
|
|
333
|
-
}
|
|
334
|
-
// Create or append to environment file
|
|
335
|
-
modifications.push(this.createEnvironmentModification(this.framework));
|
|
336
|
-
return modifications;
|
|
337
|
-
}
|
|
338
|
-
/**
|
|
339
|
-
* Helper: Merge HBProvider into existing providers.tsx file
|
|
340
|
-
*/
|
|
341
|
-
mergeProvidersFile(filePath) {
|
|
342
|
-
const hbProviderContent = `export function HBProvider({ children }: { children: React.ReactNode }) {
|
|
343
|
-
return (
|
|
344
|
-
<HumanBehaviorProvider apiKey={process.env.NEXT_PUBLIC_HUMANBEHAVIOR_API_KEY}>
|
|
345
|
-
{children}
|
|
346
|
-
</HumanBehaviorProvider>
|
|
347
|
-
);
|
|
348
|
-
}`;
|
|
349
|
-
if (!fs.existsSync(filePath)) {
|
|
350
|
-
// File doesn't exist, create new file
|
|
351
|
-
return `'use client';
|
|
352
|
-
|
|
353
|
-
import { HumanBehaviorProvider } from 'humanbehavior-js/react';
|
|
354
|
-
|
|
355
|
-
${hbProviderContent}`;
|
|
356
|
-
}
|
|
357
|
-
// File exists, read and merge
|
|
358
|
-
const existingContent = fs.readFileSync(filePath, 'utf8');
|
|
359
|
-
// Check if HBProvider already exists
|
|
360
|
-
if (existingContent.includes('export function HBProvider') || existingContent.includes('export const HBProvider')) {
|
|
361
|
-
// Already exists, return unchanged
|
|
362
|
-
return existingContent;
|
|
363
|
-
}
|
|
364
|
-
// Check if HumanBehaviorProvider import exists
|
|
365
|
-
let modifiedContent = existingContent;
|
|
366
|
-
if (!existingContent.includes("from 'humanbehavior-js/react'")) {
|
|
367
|
-
// Add the import - try to add after 'use client' or at the top
|
|
368
|
-
if (existingContent.includes("'use client'")) {
|
|
369
|
-
modifiedContent = existingContent.replace(/('use client';?)\s*\n/, `$1\n\nimport { HumanBehaviorProvider } from 'humanbehavior-js/react';\n`);
|
|
370
143
|
}
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
144
|
+
};
|
|
145
|
+
visit(root);
|
|
146
|
+
return results.join('\n') || '(no matches)';
|
|
147
|
+
}
|
|
148
|
+
async bash(input) {
|
|
149
|
+
if (typeof input.command !== 'string' || !input.command.trim()) {
|
|
150
|
+
throw new Error('command is required');
|
|
151
|
+
}
|
|
152
|
+
this.assertSafeCommand(input.command);
|
|
153
|
+
const { stdout, stderr } = await execAsync(input.command, {
|
|
154
|
+
cwd: this.projectRoot,
|
|
155
|
+
timeout: 60000,
|
|
156
|
+
maxBuffer: MAX_BASH_OUTPUT,
|
|
157
|
+
env: process.env
|
|
158
|
+
});
|
|
159
|
+
const output = [stdout, stderr].filter(Boolean).join('\n');
|
|
160
|
+
if (!output)
|
|
161
|
+
return '(command completed with no output)';
|
|
162
|
+
return output.length > MAX_BASH_OUTPUT
|
|
163
|
+
? `${output.slice(0, MAX_BASH_OUTPUT)}\n\n[truncated at ${MAX_BASH_OUTPUT} bytes]`
|
|
164
|
+
: output;
|
|
165
|
+
}
|
|
166
|
+
async verifyInstallation() {
|
|
167
|
+
if (!this.verification) {
|
|
168
|
+
throw new Error('Verification is not configured for this run.');
|
|
169
|
+
}
|
|
170
|
+
const sessionId = `hb-wizard-${this.verification.runId}`;
|
|
171
|
+
const endUserId = `hb-wizard-${this.verification.runId}`;
|
|
172
|
+
const ingestionUrl = (this.verification.ingestionUrl || process.env.HUMANBEHAVIOR_INGESTION_URL || DEFAULT_INGESTION_URL).replace(/\/$/, '');
|
|
173
|
+
const response = await fetch(`${ingestionUrl}/api/ingestion/customEvent`, {
|
|
174
|
+
method: 'POST',
|
|
175
|
+
headers: {
|
|
176
|
+
'content-type': 'application/json',
|
|
177
|
+
authorization: `Bearer ${this.verification.apiKey}`
|
|
178
|
+
},
|
|
179
|
+
body: JSON.stringify({
|
|
180
|
+
sessionId,
|
|
181
|
+
endUserId,
|
|
182
|
+
eventName: VERIFY_EVENT,
|
|
183
|
+
eventProperties: {
|
|
184
|
+
wizardRunId: this.verification.runId,
|
|
185
|
+
source: 'humanbehavior-wizard',
|
|
186
|
+
projectRoot: this.projectRoot
|
|
187
|
+
},
|
|
188
|
+
automaticProperties: {
|
|
189
|
+
url: 'humanbehavior://wizard',
|
|
190
|
+
pathname: '/wizard'
|
|
191
|
+
}
|
|
192
|
+
})
|
|
193
|
+
});
|
|
194
|
+
if (!response.ok) {
|
|
195
|
+
const body = await response.text().catch(() => response.statusText);
|
|
196
|
+
throw new Error(`Verification event failed (${response.status}): ${body || response.statusText}`);
|
|
197
|
+
}
|
|
198
|
+
for (let attempt = 1; attempt <= 12; attempt++) {
|
|
199
|
+
const verification = await this.verification.verifyRun();
|
|
200
|
+
if (verification.verified) {
|
|
201
|
+
return JSON.stringify({
|
|
202
|
+
verified: true,
|
|
203
|
+
event: VERIFY_EVENT,
|
|
204
|
+
attempt,
|
|
205
|
+
verification: verification.verification ?? {}
|
|
206
|
+
});
|
|
374
207
|
}
|
|
208
|
+
await new Promise((resolve) => setTimeout(resolve, 2500));
|
|
375
209
|
}
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
// File is empty (after trimming trailing whitespace)
|
|
381
|
-
modifiedContent = hbProviderContent;
|
|
382
|
-
}
|
|
383
|
-
else {
|
|
384
|
-
// Add double newline for separation
|
|
385
|
-
modifiedContent = `${trimmed}\n\n${hbProviderContent}`;
|
|
386
|
-
}
|
|
387
|
-
return modifiedContent;
|
|
388
|
-
}
|
|
389
|
-
/**
|
|
390
|
-
* Generate Next.js-specific modifications
|
|
391
|
-
*/
|
|
392
|
-
async generateNextJSModifications() {
|
|
393
|
-
const modifications = [];
|
|
394
|
-
// Check for App Router - try both with and without src directory
|
|
395
|
-
const appLayoutFileWithSrc = path.join(this.projectRoot, 'src', 'app', 'layout.tsx');
|
|
396
|
-
const appLayoutFile = path.join(this.projectRoot, 'app', 'layout.tsx');
|
|
397
|
-
const pagesLayoutFileWithSrc = path.join(this.projectRoot, 'src', 'pages', '_app.tsx');
|
|
398
|
-
const pagesLayoutFile = path.join(this.projectRoot, 'pages', '_app.tsx');
|
|
399
|
-
// Determine which layout file exists and set paths accordingly
|
|
400
|
-
let actualAppLayoutFile = null;
|
|
401
|
-
let providersFilePath = null;
|
|
402
|
-
let providersImportPath = null;
|
|
403
|
-
if (fs.existsSync(appLayoutFileWithSrc)) {
|
|
404
|
-
actualAppLayoutFile = appLayoutFileWithSrc;
|
|
405
|
-
providersFilePath = path.join(this.projectRoot, 'src', 'app', 'providers.tsx');
|
|
406
|
-
providersImportPath = '@/app/providers';
|
|
407
|
-
}
|
|
408
|
-
else if (fs.existsSync(appLayoutFile)) {
|
|
409
|
-
actualAppLayoutFile = appLayoutFile;
|
|
410
|
-
providersFilePath = path.join(this.projectRoot, 'app', 'providers.tsx');
|
|
411
|
-
providersImportPath = '@/app/providers';
|
|
412
|
-
}
|
|
413
|
-
if (actualAppLayoutFile) {
|
|
414
|
-
// Merge or create providers.tsx file
|
|
415
|
-
const providersContent = this.mergeProvidersFile(providersFilePath);
|
|
416
|
-
const fileExists = fs.existsSync(providersFilePath);
|
|
417
|
-
modifications.push({
|
|
418
|
-
filePath: providersFilePath,
|
|
419
|
-
action: fileExists ? 'modify' : 'create',
|
|
420
|
-
content: providersContent,
|
|
421
|
-
description: fileExists
|
|
422
|
-
? 'Merged HBProvider into existing providers.tsx file'
|
|
423
|
-
: 'Created providers.tsx file with HBProvider for Next.js App Router'
|
|
424
|
-
});
|
|
425
|
-
// Modify layout.tsx to use the provider
|
|
426
|
-
const content = fs.readFileSync(actualAppLayoutFile, 'utf8');
|
|
427
|
-
const modifiedContent = this.injectNextJSAppRouter(content, providersImportPath);
|
|
428
|
-
modifications.push({
|
|
429
|
-
filePath: actualAppLayoutFile,
|
|
430
|
-
action: 'modify',
|
|
431
|
-
content: modifiedContent,
|
|
432
|
-
description: 'Added HumanBehavior provider wrapper to Next.js App Router layout'
|
|
433
|
-
});
|
|
434
|
-
}
|
|
435
|
-
else if (fs.existsSync(pagesLayoutFileWithSrc) || fs.existsSync(pagesLayoutFile)) {
|
|
436
|
-
const actualPagesLayoutFile = fs.existsSync(pagesLayoutFileWithSrc) ? pagesLayoutFileWithSrc : pagesLayoutFile;
|
|
437
|
-
const providersPath = fs.existsSync(pagesLayoutFileWithSrc)
|
|
438
|
-
? path.join(this.projectRoot, 'src', 'components', 'HumanBehaviorProvider.tsx')
|
|
439
|
-
: path.join(this.projectRoot, 'components', 'HumanBehaviorProvider.tsx');
|
|
440
|
-
const importPath = fs.existsSync(pagesLayoutFileWithSrc)
|
|
441
|
-
? '../components/HumanBehaviorProvider'
|
|
442
|
-
: './components/HumanBehaviorProvider';
|
|
443
|
-
// Create dedicated HumanBehavior provider file for Pages Router
|
|
444
|
-
modifications.push({
|
|
445
|
-
filePath: providersPath,
|
|
446
|
-
action: 'create',
|
|
447
|
-
content: `'use client';
|
|
448
|
-
|
|
449
|
-
import { HumanBehaviorProvider } from 'humanbehavior-js/react';
|
|
450
|
-
|
|
451
|
-
export function HBProvider({ children }: { children: React.ReactNode }) {
|
|
452
|
-
return (
|
|
453
|
-
<HumanBehaviorProvider apiKey={process.env.NEXT_PUBLIC_HUMANBEHAVIOR_API_KEY}>
|
|
454
|
-
{children}
|
|
455
|
-
</HumanBehaviorProvider>
|
|
456
|
-
);
|
|
457
|
-
}`,
|
|
458
|
-
description: 'Created HumanBehavior provider file for Pages Router'
|
|
459
|
-
});
|
|
460
|
-
// Modify _app.tsx to use the provider
|
|
461
|
-
const content = fs.readFileSync(actualPagesLayoutFile, 'utf8');
|
|
462
|
-
const modifiedContent = this.injectNextJSPagesRouter(content, importPath);
|
|
463
|
-
modifications.push({
|
|
464
|
-
filePath: actualPagesLayoutFile,
|
|
465
|
-
action: 'modify',
|
|
466
|
-
content: modifiedContent,
|
|
467
|
-
description: 'Added HumanBehavior provider wrapper to Next.js Pages Router'
|
|
468
|
-
});
|
|
469
|
-
}
|
|
470
|
-
// Create or append to environment file
|
|
471
|
-
modifications.push(this.createEnvironmentModification(this.framework));
|
|
472
|
-
return modifications;
|
|
473
|
-
}
|
|
474
|
-
/**
|
|
475
|
-
* Generate Astro-specific modifications
|
|
476
|
-
*/
|
|
477
|
-
async generateAstroModifications() {
|
|
478
|
-
const modifications = [];
|
|
479
|
-
// Create Astro component for HumanBehavior
|
|
480
|
-
const astroComponentPath = path.join(this.projectRoot, 'src', 'components', 'HumanBehavior.astro');
|
|
481
|
-
const astroComponentContent = `---
|
|
482
|
-
// This component will only run on the client side
|
|
483
|
-
---
|
|
484
|
-
|
|
485
|
-
<script>
|
|
486
|
-
import { HumanBehaviorTracker } from 'humanbehavior-js';
|
|
487
|
-
const apiKey = import.meta.env.PUBLIC_HUMANBEHAVIOR_API_KEY;
|
|
488
|
-
if (apiKey) {
|
|
489
|
-
HumanBehaviorTracker.init(apiKey);
|
|
490
|
-
}
|
|
491
|
-
</script>`;
|
|
492
|
-
modifications.push({
|
|
493
|
-
filePath: astroComponentPath,
|
|
494
|
-
action: 'create',
|
|
495
|
-
content: astroComponentContent,
|
|
496
|
-
description: 'Created Astro component for HumanBehavior SDK'
|
|
210
|
+
return JSON.stringify({
|
|
211
|
+
verified: false,
|
|
212
|
+
event: VERIFY_EVENT,
|
|
213
|
+
message: 'Verification event was sent but has not appeared in ClickHouse yet.'
|
|
497
214
|
});
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
215
|
+
}
|
|
216
|
+
assertSafeCommand(command) {
|
|
217
|
+
const deniedPatterns = [
|
|
218
|
+
/\brm\s+-rf\b/,
|
|
219
|
+
/\bgit\s+reset\b/,
|
|
220
|
+
/\bgit\s+clean\b/,
|
|
221
|
+
/\bsudo\b/,
|
|
222
|
+
/\bchmod\s+-R\b/,
|
|
223
|
+
/>\s*\/dev\/(?:disk|rdisk)/,
|
|
503
224
|
];
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
layoutFile = file;
|
|
508
|
-
break;
|
|
225
|
+
for (const pattern of deniedPatterns) {
|
|
226
|
+
if (pattern.test(command)) {
|
|
227
|
+
throw new Error(`Refusing unsafe command: ${command}`);
|
|
509
228
|
}
|
|
510
229
|
}
|
|
511
|
-
if (layoutFile) {
|
|
512
|
-
const content = fs.readFileSync(layoutFile, 'utf8');
|
|
513
|
-
const modifiedContent = this.injectAstroLayout(content);
|
|
514
|
-
modifications.push({
|
|
515
|
-
filePath: layoutFile,
|
|
516
|
-
action: 'modify',
|
|
517
|
-
content: modifiedContent,
|
|
518
|
-
description: 'Added HumanBehavior component to Astro layout'
|
|
519
|
-
});
|
|
520
|
-
}
|
|
521
|
-
// Add environment variable
|
|
522
|
-
modifications.push(this.createEnvironmentModification(this.framework));
|
|
523
|
-
return modifications;
|
|
524
230
|
}
|
|
525
|
-
|
|
526
|
-
* Generate Nuxt-specific modifications
|
|
527
|
-
*/
|
|
528
|
-
async generateNuxtModifications() {
|
|
529
|
-
const modifications = [];
|
|
530
|
-
// Create plugin file for Nuxt (in app directory)
|
|
531
|
-
const pluginFile = path.join(this.projectRoot, 'app', 'plugins', 'humanbehavior.client.ts');
|
|
532
|
-
modifications.push({
|
|
533
|
-
filePath: pluginFile,
|
|
534
|
-
action: 'create',
|
|
535
|
-
content: `import { HumanBehaviorTracker } from 'humanbehavior-js';
|
|
231
|
+
}
|
|
536
232
|
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
HumanBehaviorTracker.init(apiKey);
|
|
233
|
+
const DEFAULT_APP_URL = 'https://www.humanbehavior.co';
|
|
234
|
+
class ManagedWizardApiClient {
|
|
235
|
+
constructor(options) {
|
|
236
|
+
this.apiKey = options.apiKey;
|
|
237
|
+
this.baseUrl = (options.baseUrl || process.env.HUMANBEHAVIOR_APP_URL || DEFAULT_APP_URL).replace(/\/$/, '');
|
|
543
238
|
}
|
|
544
|
-
|
|
545
|
-
}
|
|
546
|
-
|
|
239
|
+
getRunConsoleUrl(projectSlug, runId) {
|
|
240
|
+
return `${this.baseUrl}/projects/${projectSlug}/get-started/sdk?wizardRun=${runId}`;
|
|
241
|
+
}
|
|
242
|
+
async createRun(input) {
|
|
243
|
+
const response = await this.request('/api/wizard/runs', {
|
|
244
|
+
method: 'POST',
|
|
245
|
+
body: JSON.stringify(input)
|
|
547
246
|
});
|
|
548
|
-
|
|
549
|
-
const nuxtConfigFile = path.join(this.projectRoot, 'nuxt.config.ts');
|
|
550
|
-
{
|
|
551
|
-
const mod = this.applyOrNotify(nuxtConfigFile, (c) => this.injectNuxtConfig(c), 'Added HumanBehavior runtime config to Nuxt config', 'Nuxt: Add inside defineNuxtConfig({ … }):\nruntimeConfig: { public: { humanBehaviorApiKey: process.env.NUXT_PUBLIC_HUMANBEHAVIOR_API_KEY } },');
|
|
552
|
-
if (mod)
|
|
553
|
-
modifications.push(mod);
|
|
554
|
-
}
|
|
555
|
-
// Create or append to environment file
|
|
556
|
-
modifications.push(this.createEnvironmentModification(this.framework));
|
|
557
|
-
return modifications;
|
|
247
|
+
return response.json();
|
|
558
248
|
}
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
// Find root.tsx file
|
|
565
|
-
const rootFile = path.join(this.projectRoot, 'app', 'root.tsx');
|
|
566
|
-
if (fs.existsSync(rootFile)) {
|
|
567
|
-
const content = fs.readFileSync(rootFile, 'utf8');
|
|
568
|
-
const modifiedContent = this.injectRemixProvider(content);
|
|
569
|
-
modifications.push({
|
|
570
|
-
filePath: rootFile,
|
|
571
|
-
action: 'modify',
|
|
572
|
-
content: modifiedContent,
|
|
573
|
-
description: 'Added HumanBehaviorProvider to Remix root component'
|
|
574
|
-
});
|
|
575
|
-
}
|
|
576
|
-
// Create or append to environment file
|
|
577
|
-
modifications.push(this.createEnvironmentModification(this.framework));
|
|
578
|
-
return modifications;
|
|
249
|
+
async updateRun(runId, input) {
|
|
250
|
+
await this.request(`/api/wizard/runs/${runId}`, {
|
|
251
|
+
method: 'PATCH',
|
|
252
|
+
body: JSON.stringify(input)
|
|
253
|
+
});
|
|
579
254
|
}
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
const
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
}
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
if (!fs.existsSync(composablePath)) {
|
|
609
|
-
modifications.push({
|
|
610
|
-
filePath: composablePath,
|
|
611
|
-
action: 'create',
|
|
612
|
-
content: composableContent,
|
|
613
|
-
description: 'Created Vue composable useHumanBehavior'
|
|
614
|
-
});
|
|
255
|
+
async createAgentSession(input) {
|
|
256
|
+
const response = await this.request('/api/wizard/agent-sessions', {
|
|
257
|
+
method: 'POST',
|
|
258
|
+
body: JSON.stringify(input)
|
|
259
|
+
});
|
|
260
|
+
return response.json();
|
|
261
|
+
}
|
|
262
|
+
async runAgentTurn(sessionId, sessionToken, messages) {
|
|
263
|
+
const response = await this.request(`/api/wizard/agent-sessions/${sessionId}/turn`, {
|
|
264
|
+
method: 'POST',
|
|
265
|
+
body: JSON.stringify({ messages })
|
|
266
|
+
}, sessionToken);
|
|
267
|
+
return response.json();
|
|
268
|
+
}
|
|
269
|
+
async verifyRun(runId) {
|
|
270
|
+
const response = await this.request(`/api/wizard/runs/${runId}/verify`, {
|
|
271
|
+
method: 'POST',
|
|
272
|
+
body: JSON.stringify({})
|
|
273
|
+
});
|
|
274
|
+
return response.json();
|
|
275
|
+
}
|
|
276
|
+
async request(path, init, token = this.apiKey) {
|
|
277
|
+
const response = await fetch(`${this.baseUrl}${path}`, {
|
|
278
|
+
...init,
|
|
279
|
+
headers: {
|
|
280
|
+
'content-type': 'application/json',
|
|
281
|
+
authorization: `Bearer ${token}`,
|
|
282
|
+
...init.headers
|
|
615
283
|
}
|
|
284
|
+
});
|
|
285
|
+
if (!response.ok) {
|
|
286
|
+
const message = await response.text().catch(() => response.statusText);
|
|
287
|
+
throw new Error(`Wizard API request failed (${response.status}): ${message || response.statusText}`);
|
|
616
288
|
}
|
|
617
|
-
|
|
618
|
-
if (mainFile) {
|
|
619
|
-
const content = fs.readFileSync(mainFile, 'utf8');
|
|
620
|
-
const modifiedContent = this.injectVuePlugin(content);
|
|
621
|
-
modifications.push({
|
|
622
|
-
filePath: mainFile,
|
|
623
|
-
action: 'modify',
|
|
624
|
-
content: modifiedContent,
|
|
625
|
-
description: 'Added HumanBehaviorPlugin to Vue app'
|
|
626
|
-
});
|
|
627
|
-
}
|
|
628
|
-
// Create or append to environment file
|
|
629
|
-
modifications.push(this.createEnvironmentModification(this.framework));
|
|
630
|
-
return modifications;
|
|
289
|
+
return response;
|
|
631
290
|
}
|
|
632
|
-
|
|
633
|
-
* Generate Angular-specific modifications
|
|
634
|
-
*/
|
|
635
|
-
async generateAngularModifications() {
|
|
636
|
-
const modifications = [];
|
|
637
|
-
// Create Angular service (docs pattern)
|
|
638
|
-
const serviceDir = path.join(this.projectRoot, 'src', 'app', 'services');
|
|
639
|
-
const servicePath = path.join(serviceDir, 'hb.service.ts');
|
|
640
|
-
const serviceContent = `import { Injectable, NgZone, Inject, PLATFORM_ID } from '@angular/core';
|
|
641
|
-
import { isPlatformBrowser } from '@angular/common';
|
|
642
|
-
import { HumanBehaviorTracker } from 'humanbehavior-js';
|
|
643
|
-
import { environment } from '../../environments/environment';
|
|
644
|
-
|
|
645
|
-
@Injectable({ providedIn: 'root' })
|
|
646
|
-
export class HumanBehavior {
|
|
647
|
-
private tracker: ReturnType<typeof HumanBehaviorTracker.init> | null = null;
|
|
291
|
+
}
|
|
648
292
|
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
293
|
+
const MANUAL_DOCS_URL = 'https://documentation.humanbehavior.co';
|
|
294
|
+
const MAX_AGENT_TURNS = 18;
|
|
295
|
+
class AgentInstallationRunner {
|
|
296
|
+
constructor(options) {
|
|
297
|
+
this.apiKey = options.apiKey;
|
|
298
|
+
this.projectPath = path.resolve(options.projectPath || process.cwd());
|
|
299
|
+
this.executionMode = options.executionMode || 'interactive';
|
|
300
|
+
this.ingestionUrl = options.ingestionUrl;
|
|
301
|
+
this.apiClient = new ManagedWizardApiClient({
|
|
302
|
+
apiKey: options.apiKey,
|
|
303
|
+
baseUrl: options.appUrl
|
|
304
|
+
});
|
|
654
305
|
}
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
}
|
|
664
|
-
|
|
665
|
-
trackPageView(path?: string) {
|
|
666
|
-
this.tracker?.trackPageView(path);
|
|
667
|
-
}
|
|
668
|
-
}
|
|
669
|
-
`;
|
|
670
|
-
if (!fs.existsSync(serviceDir)) {
|
|
671
|
-
fs.mkdirSync(serviceDir, { recursive: true });
|
|
306
|
+
async run() {
|
|
307
|
+
const repo = this.scanRepo();
|
|
308
|
+
let runId = null;
|
|
309
|
+
let dashboardUrl;
|
|
310
|
+
let syncWarning = null;
|
|
311
|
+
let agentSession = null;
|
|
312
|
+
if (!this.apiKey.startsWith('hb_')) {
|
|
313
|
+
return this.failure(repo, 'HumanBehavior API key should start with "hb_".');
|
|
672
314
|
}
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
content: serviceContent,
|
|
678
|
-
description: 'Created Angular HumanBehavior service (singleton)'
|
|
315
|
+
try {
|
|
316
|
+
const created = await this.apiClient.createRun({
|
|
317
|
+
executionMode: this.executionMode,
|
|
318
|
+
repo
|
|
679
319
|
});
|
|
320
|
+
runId = created.run.id;
|
|
321
|
+
dashboardUrl = this.apiClient.getRunConsoleUrl(created.projectSlug, created.run.id);
|
|
680
322
|
}
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
// Create environments directory if it doesn't exist
|
|
685
|
-
const envDir = path.dirname(envFile);
|
|
686
|
-
if (!fs.existsSync(envDir)) {
|
|
687
|
-
fs.mkdirSync(envDir, { recursive: true });
|
|
688
|
-
}
|
|
689
|
-
// Create or update development environment
|
|
690
|
-
if (fs.existsSync(envFile)) {
|
|
691
|
-
const content = fs.readFileSync(envFile, 'utf8');
|
|
692
|
-
if (!content.includes('humanBehaviorApiKey')) {
|
|
693
|
-
const modifiedContent = content.replace(/export const environment = {([\s\S]*?)};/, `export const environment = {
|
|
694
|
-
$1,
|
|
695
|
-
humanBehaviorApiKey: '${this.apiKey}'
|
|
696
|
-
};`);
|
|
697
|
-
modifications.push({
|
|
698
|
-
filePath: envFile,
|
|
699
|
-
action: 'modify',
|
|
700
|
-
content: modifiedContent,
|
|
701
|
-
description: 'Added API key to Angular development environment'
|
|
702
|
-
});
|
|
323
|
+
catch (error) {
|
|
324
|
+
if (this.executionMode === 'dry-run') {
|
|
325
|
+
syncWarning = `Dashboard sync unavailable: ${error instanceof Error ? error.message : 'Unknown error'}`;
|
|
703
326
|
}
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
// Create new development environment file
|
|
707
|
-
modifications.push({
|
|
708
|
-
filePath: envFile,
|
|
709
|
-
action: 'create',
|
|
710
|
-
content: `export const environment = {
|
|
711
|
-
production: false,
|
|
712
|
-
humanBehaviorApiKey: '${this.apiKey}'
|
|
713
|
-
};`,
|
|
714
|
-
description: 'Created Angular development environment file'
|
|
715
|
-
});
|
|
716
|
-
}
|
|
717
|
-
// Create or update production environment
|
|
718
|
-
if (fs.existsSync(envProdFile)) {
|
|
719
|
-
const content = fs.readFileSync(envProdFile, 'utf8');
|
|
720
|
-
if (!content.includes('humanBehaviorApiKey')) {
|
|
721
|
-
const modifiedContent = content.replace(/export const environment = {([\s\S]*?)};/, `export const environment = {
|
|
722
|
-
$1,
|
|
723
|
-
humanBehaviorApiKey: '${this.apiKey}'
|
|
724
|
-
};`);
|
|
725
|
-
modifications.push({
|
|
726
|
-
filePath: envProdFile,
|
|
727
|
-
action: 'modify',
|
|
728
|
-
content: modifiedContent,
|
|
729
|
-
description: 'Added API key to Angular production environment'
|
|
730
|
-
});
|
|
327
|
+
else {
|
|
328
|
+
return this.failure(repo, `Could not start a managed wizard run: ${error instanceof Error ? error.message : 'Unknown error'}`);
|
|
731
329
|
}
|
|
732
330
|
}
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
331
|
+
if (this.executionMode === 'dry-run') {
|
|
332
|
+
const result = {
|
|
333
|
+
success: true,
|
|
334
|
+
phase: 'analyze',
|
|
335
|
+
repo,
|
|
336
|
+
errors: [],
|
|
337
|
+
nextSteps: [
|
|
338
|
+
'Dry run complete. No files were changed.',
|
|
339
|
+
'The managed agent session is required before edit planning can run.',
|
|
340
|
+
...(dashboardUrl ? [`Dashboard run: ${dashboardUrl}`] : []),
|
|
341
|
+
...(syncWarning ? [syncWarning] : []),
|
|
342
|
+
`Manual setup docs: ${MANUAL_DOCS_URL}`
|
|
343
|
+
],
|
|
344
|
+
dashboardUrl
|
|
345
|
+
};
|
|
346
|
+
await this.updateRunSafely(runId, {
|
|
347
|
+
status: 'completed',
|
|
348
|
+
phase: 'finish',
|
|
349
|
+
result
|
|
743
350
|
});
|
|
351
|
+
return result;
|
|
744
352
|
}
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
const appComponentPath = path.join(this.projectRoot, 'src', 'app', 'app.ts');
|
|
749
|
-
if (fs.existsSync(appComponentPath)) {
|
|
750
|
-
const appContent = fs.readFileSync(appComponentPath, 'utf8');
|
|
751
|
-
// Check if already has HumanBehavior service
|
|
752
|
-
if (!appContent.includes('HumanBehavior')) {
|
|
753
|
-
let modifiedAppContent = appContent
|
|
754
|
-
.replace(/import { Component } from '@angular\/core';/, `import { Component } from '@angular/core';
|
|
755
|
-
import { HumanBehavior } from './services/hb.service';`)
|
|
756
|
-
.replace(/export class App {/, `export class App {
|
|
757
|
-
constructor(private readonly humanBehavior: HumanBehavior) {}`);
|
|
758
|
-
// Do not modify standalone setting; leave component decorator unchanged
|
|
759
|
-
modifications.push({
|
|
760
|
-
action: 'modify',
|
|
761
|
-
filePath: appComponentPath,
|
|
762
|
-
content: modifiedAppContent,
|
|
763
|
-
description: 'Injected HumanBehavior service into Angular app component'
|
|
764
|
-
});
|
|
765
|
-
}
|
|
766
|
-
}
|
|
767
|
-
return modifications;
|
|
768
|
-
}
|
|
769
|
-
/**
|
|
770
|
-
* Generate Svelte-specific modifications
|
|
771
|
-
*/
|
|
772
|
-
async generateSvelteModifications() {
|
|
773
|
-
const modifications = [];
|
|
774
|
-
// Check for SvelteKit
|
|
775
|
-
const svelteConfigFile = path.join(this.projectRoot, 'svelte.config.js');
|
|
776
|
-
const isSvelteKit = fs.existsSync(svelteConfigFile);
|
|
777
|
-
if (isSvelteKit) {
|
|
778
|
-
// SvelteKit - create layout file
|
|
779
|
-
const layoutFile = path.join(this.projectRoot, 'src', 'routes', '+layout.svelte');
|
|
780
|
-
if (fs.existsSync(layoutFile)) {
|
|
781
|
-
const content = fs.readFileSync(layoutFile, 'utf8');
|
|
782
|
-
const modifiedContent = this.injectSvelteKitLayout(content);
|
|
783
|
-
modifications.push({
|
|
784
|
-
filePath: layoutFile,
|
|
785
|
-
action: 'modify',
|
|
786
|
-
content: modifiedContent,
|
|
787
|
-
description: 'Added HumanBehavior tracker init to SvelteKit layout'
|
|
788
|
-
});
|
|
789
|
-
}
|
|
790
|
-
}
|
|
791
|
-
else {
|
|
792
|
-
// Regular Svelte - modify main file
|
|
793
|
-
const mainFile = this.findSvelteMainFile();
|
|
794
|
-
if (mainFile) {
|
|
795
|
-
const content = fs.readFileSync(mainFile, 'utf8');
|
|
796
|
-
const modifiedContent = this.injectSvelteStore(content);
|
|
797
|
-
modifications.push({
|
|
798
|
-
filePath: mainFile,
|
|
799
|
-
action: 'modify',
|
|
800
|
-
content: modifiedContent,
|
|
801
|
-
description: 'Added HumanBehavior tracker init to Svelte app'
|
|
802
|
-
});
|
|
353
|
+
try {
|
|
354
|
+
if (!runId) {
|
|
355
|
+
throw new Error('Wizard run was not created.');
|
|
803
356
|
}
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
return modifications;
|
|
808
|
-
}
|
|
809
|
-
/**
|
|
810
|
-
* Generate vanilla JS/TS modifications
|
|
811
|
-
*/
|
|
812
|
-
async generateVanillaModifications() {
|
|
813
|
-
const modifications = [];
|
|
814
|
-
// Find HTML file to inject script
|
|
815
|
-
const htmlFile = this.findHTMLFile();
|
|
816
|
-
if (htmlFile) {
|
|
817
|
-
const content = fs.readFileSync(htmlFile, 'utf8');
|
|
818
|
-
const modifiedContent = this.injectVanillaScript(content);
|
|
819
|
-
modifications.push({
|
|
820
|
-
filePath: htmlFile,
|
|
821
|
-
action: 'modify',
|
|
822
|
-
content: modifiedContent,
|
|
823
|
-
description: 'Added HumanBehavior CDN script to HTML file'
|
|
824
|
-
});
|
|
825
|
-
}
|
|
826
|
-
// Create or append to environment file
|
|
827
|
-
modifications.push(this.createEnvironmentModification(this.framework));
|
|
828
|
-
return modifications;
|
|
829
|
-
}
|
|
830
|
-
/**
|
|
831
|
-
* Generate Gatsby-specific modifications
|
|
832
|
-
*/
|
|
833
|
-
async generateGatsbyModifications() {
|
|
834
|
-
const modifications = [];
|
|
835
|
-
// Modify or create gatsby-browser.js for Gatsby
|
|
836
|
-
const gatsbyBrowserFile = path.join(this.projectRoot, 'gatsby-browser.js');
|
|
837
|
-
if (fs.existsSync(gatsbyBrowserFile)) {
|
|
838
|
-
const content = fs.readFileSync(gatsbyBrowserFile, 'utf8');
|
|
839
|
-
const modifiedContent = this.injectGatsbyBrowser(content);
|
|
840
|
-
modifications.push({
|
|
841
|
-
filePath: gatsbyBrowserFile,
|
|
842
|
-
action: 'modify',
|
|
843
|
-
content: modifiedContent,
|
|
844
|
-
description: 'Added HumanBehavior initialization to Gatsby browser'
|
|
357
|
+
const session = await this.apiClient.createAgentSession({
|
|
358
|
+
runId,
|
|
359
|
+
permissionMode: this.executionMode === 'yes' ? 'acceptEdits' : 'default'
|
|
845
360
|
});
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
modifications.push({
|
|
850
|
-
filePath: gatsbyBrowserFile,
|
|
851
|
-
action: 'create',
|
|
852
|
-
content: `import { HumanBehaviorTracker } from 'humanbehavior-js';
|
|
853
|
-
|
|
854
|
-
export const onClientEntry = () => {
|
|
855
|
-
const apiKey = process.env.GATSBY_HUMANBEHAVIOR_API_KEY;
|
|
856
|
-
if (apiKey) {
|
|
857
|
-
HumanBehaviorTracker.init(apiKey);
|
|
858
|
-
}
|
|
859
|
-
};`,
|
|
860
|
-
description: 'Created gatsby-browser.js with HumanBehavior initialization'
|
|
861
|
-
});
|
|
862
|
-
}
|
|
863
|
-
// Create or append to environment file
|
|
864
|
-
modifications.push(this.createEnvironmentModification(this.framework));
|
|
865
|
-
return modifications;
|
|
866
|
-
}
|
|
867
|
-
/**
|
|
868
|
-
* Apply modifications to the codebase
|
|
869
|
-
*/
|
|
870
|
-
async applyModifications(modifications) {
|
|
871
|
-
for (const modification of modifications) {
|
|
872
|
-
try {
|
|
873
|
-
const dir = path.dirname(modification.filePath);
|
|
874
|
-
if (!fs.existsSync(dir)) {
|
|
875
|
-
fs.mkdirSync(dir, { recursive: true });
|
|
876
|
-
}
|
|
877
|
-
switch (modification.action) {
|
|
878
|
-
case 'create':
|
|
879
|
-
fs.writeFileSync(modification.filePath, modification.content);
|
|
880
|
-
break;
|
|
881
|
-
case 'modify':
|
|
882
|
-
fs.writeFileSync(modification.filePath, modification.content);
|
|
883
|
-
break;
|
|
884
|
-
case 'append':
|
|
885
|
-
fs.appendFileSync(modification.filePath, '\n' + modification.content);
|
|
886
|
-
break;
|
|
887
|
-
}
|
|
888
|
-
}
|
|
889
|
-
catch (error) {
|
|
890
|
-
throw new Error(`Failed to apply modification to ${modification.filePath}: ${error}`);
|
|
891
|
-
}
|
|
892
|
-
}
|
|
893
|
-
}
|
|
894
|
-
/**
|
|
895
|
-
* Generate next steps for the user
|
|
896
|
-
*/
|
|
897
|
-
generateNextSteps() {
|
|
898
|
-
const steps = [
|
|
899
|
-
'✅ SDK installed and configured automatically!',
|
|
900
|
-
'🚀 Your app is now tracking user behavior',
|
|
901
|
-
'📊 View sessions in your HumanBehavior dashboard',
|
|
902
|
-
'🔧 Customize tracking in your code as needed'
|
|
903
|
-
];
|
|
904
|
-
if (this.framework?.type === 'react' || this.framework?.type === 'nextjs') {
|
|
905
|
-
steps.push('💡 Use the useHumanBehavior() hook to track custom events');
|
|
906
|
-
}
|
|
907
|
-
// Append any manual notes gathered during transformation
|
|
908
|
-
if (this.manualNotes.length) {
|
|
909
|
-
steps.push(...this.manualNotes.map((n) => `⚠️ ${n}`));
|
|
910
|
-
}
|
|
911
|
-
return steps;
|
|
912
|
-
}
|
|
913
|
-
/**
|
|
914
|
-
* Helper: apply a file transform or record a manual instruction if unchanged/missing
|
|
915
|
-
*/
|
|
916
|
-
applyOrNotify(filePath, transform, description, manualNote) {
|
|
917
|
-
if (!fs.existsSync(filePath)) {
|
|
918
|
-
this.manualNotes.push(`${manualNote} (file missing: ${path.relative(this.projectRoot, filePath)})`);
|
|
919
|
-
return null;
|
|
920
|
-
}
|
|
921
|
-
const original = fs.readFileSync(filePath, 'utf8');
|
|
922
|
-
const updated = transform(original);
|
|
923
|
-
if (updated !== original) {
|
|
924
|
-
return {
|
|
925
|
-
filePath,
|
|
926
|
-
action: 'modify',
|
|
927
|
-
content: updated,
|
|
928
|
-
description
|
|
361
|
+
agentSession = {
|
|
362
|
+
id: session.session.id,
|
|
363
|
+
token: session.sessionToken
|
|
929
364
|
};
|
|
930
365
|
}
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
366
|
+
catch (error) {
|
|
367
|
+
const result = this.failure(repo, `Could not start managed Claude agent session: ${error instanceof Error ? error.message : 'Unknown error'}`, dashboardUrl);
|
|
368
|
+
await this.updateRunSafely(runId, {
|
|
369
|
+
status: 'failed',
|
|
370
|
+
phase: 'analyze',
|
|
371
|
+
result
|
|
372
|
+
});
|
|
373
|
+
return result;
|
|
374
|
+
}
|
|
375
|
+
return this.runRemoteToolLoop(runId, agentSession, repo, dashboardUrl);
|
|
376
|
+
}
|
|
377
|
+
async runRemoteToolLoop(runId, session, repo, dashboardUrl) {
|
|
378
|
+
let installationVerified = false;
|
|
379
|
+
const executor = new LocalToolExecutor(this.projectPath, {
|
|
380
|
+
apiKey: this.apiKey,
|
|
381
|
+
runId,
|
|
382
|
+
ingestionUrl: this.ingestionUrl,
|
|
383
|
+
verifyRun: async () => {
|
|
384
|
+
const verification = await this.apiClient.verifyRun(runId);
|
|
385
|
+
if (verification.verified)
|
|
386
|
+
installationVerified = true;
|
|
387
|
+
return verification;
|
|
944
388
|
}
|
|
945
|
-
}
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
'src/main.js', 'src/main.ts', 'src/main.jsx', 'src/main.tsx'
|
|
951
|
-
];
|
|
952
|
-
for (const file of possibleFiles) {
|
|
953
|
-
const fullPath = path.join(this.projectRoot, file);
|
|
954
|
-
if (fs.existsSync(fullPath)) {
|
|
955
|
-
return fullPath;
|
|
389
|
+
});
|
|
390
|
+
const messages = [
|
|
391
|
+
{
|
|
392
|
+
role: 'user',
|
|
393
|
+
content: this.createInitialPrompt(repo)
|
|
956
394
|
}
|
|
957
|
-
}
|
|
958
|
-
return null;
|
|
959
|
-
}
|
|
960
|
-
findSvelteMainFile() {
|
|
961
|
-
const possibleFiles = [
|
|
962
|
-
'src/main.js', 'src/main.ts', 'src/main.svelte'
|
|
963
395
|
];
|
|
964
|
-
for (
|
|
965
|
-
const
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
}
|
|
980
|
-
return null;
|
|
981
|
-
}
|
|
982
|
-
injectReactProvider(content, filePath) {
|
|
983
|
-
filePath.endsWith('.tsx') || filePath.endsWith('.ts');
|
|
984
|
-
// Check if already has HumanBehaviorProvider
|
|
985
|
-
if (content.includes('HumanBehaviorProvider')) {
|
|
986
|
-
return content;
|
|
987
|
-
}
|
|
988
|
-
// Determine the correct environment variable syntax based on bundler
|
|
989
|
-
const isVite = this.framework?.bundler === 'vite';
|
|
990
|
-
const envVar = isVite
|
|
991
|
-
? 'import.meta.env.VITE_HUMANBEHAVIOR_API_KEY!'
|
|
992
|
-
: 'process.env.REACT_APP_HUMANBEHAVIOR_API_KEY!';
|
|
993
|
-
const importStatement = `import { HumanBehaviorProvider } from 'humanbehavior-js/react';`;
|
|
994
|
-
// Enhanced parsing for React 18+ features
|
|
995
|
-
const hasReact18 = this.framework?.features?.hasReact18;
|
|
996
|
-
// Handle different React patterns
|
|
997
|
-
if (content.includes('function App()') || content.includes('const App =')) {
|
|
998
|
-
// Add import statement
|
|
999
|
-
let modifiedContent = content.replace(/(import.*?from.*?['"]react['"];?)/, `$1\n${importStatement}`);
|
|
1000
|
-
// If no React import found, add it at the top
|
|
1001
|
-
if (!modifiedContent.includes(importStatement)) {
|
|
1002
|
-
modifiedContent = `${importStatement}\n\n${modifiedContent}`;
|
|
1003
|
-
}
|
|
1004
|
-
// Wrap the App component return with HumanBehaviorProvider
|
|
1005
|
-
modifiedContent = modifiedContent.replace(/return\s*\(([\s\S]*?)\)\s*;/, `return (
|
|
1006
|
-
<HumanBehaviorProvider apiKey={${envVar}}>
|
|
1007
|
-
$1
|
|
1008
|
-
</HumanBehaviorProvider>
|
|
1009
|
-
);`);
|
|
1010
|
-
return modifiedContent;
|
|
1011
|
-
}
|
|
1012
|
-
// Handle React 18+ createRoot pattern
|
|
1013
|
-
if (hasReact18 && content.includes('createRoot')) {
|
|
1014
|
-
let modifiedContent = content.replace(/(import.*?from.*?['"]react['"];?)/, `$1\n${importStatement}`);
|
|
1015
|
-
if (!modifiedContent.includes(importStatement)) {
|
|
1016
|
-
modifiedContent = `${importStatement}\n\n${modifiedContent}`;
|
|
1017
|
-
}
|
|
1018
|
-
// Wrap the root render with HumanBehaviorProvider
|
|
1019
|
-
modifiedContent = modifiedContent.replace(/(root\.render\s*\([\s\S]*?\)\s*;)/, `root.render(
|
|
1020
|
-
<HumanBehaviorProvider apiKey={${envVar}}>
|
|
1021
|
-
$1
|
|
1022
|
-
</HumanBehaviorProvider>
|
|
1023
|
-
);`);
|
|
1024
|
-
return modifiedContent;
|
|
1025
|
-
}
|
|
1026
|
-
// Fallback: simple injection
|
|
1027
|
-
return `${importStatement}\n\n${content}`;
|
|
1028
|
-
}
|
|
1029
|
-
injectNextJSAppRouter(content, importPath = '@/app/providers') {
|
|
1030
|
-
if (content.includes('HBProvider')) {
|
|
1031
|
-
return content;
|
|
1032
|
-
}
|
|
1033
|
-
const importStatement = `import { HBProvider } from '${importPath}';`;
|
|
1034
|
-
// First, add the import statement
|
|
1035
|
-
let modifiedContent = content.replace(/export default function RootLayout/, `${importStatement}\n\nexport default function RootLayout`);
|
|
1036
|
-
// Then wrap the body content with Providers
|
|
1037
|
-
// Use a more specific approach to handle the body content
|
|
1038
|
-
modifiedContent = modifiedContent.replace(/<body([^>]*)>([\s\S]*?)<\/body>/, (match, bodyAttrs, bodyContent) => {
|
|
1039
|
-
// Trim whitespace and newlines from bodyContent
|
|
1040
|
-
const trimmedContent = bodyContent.trim();
|
|
1041
|
-
return `<body${bodyAttrs}>
|
|
1042
|
-
<HBProvider>
|
|
1043
|
-
${trimmedContent}
|
|
1044
|
-
</HBProvider>
|
|
1045
|
-
</body>`;
|
|
1046
|
-
});
|
|
1047
|
-
return modifiedContent;
|
|
1048
|
-
}
|
|
1049
|
-
injectNextJSPagesRouter(content, importPath = '../components/HumanBehaviorProvider') {
|
|
1050
|
-
if (content.includes('HBProvider')) {
|
|
1051
|
-
return content;
|
|
1052
|
-
}
|
|
1053
|
-
const importStatement = `import { HBProvider } from '${importPath}';`;
|
|
1054
|
-
return content.replace(/function MyApp/, `${importStatement}\n\nfunction MyApp`).replace(/return \(([\s\S]*?)\);/, `return (
|
|
1055
|
-
<HBProvider>
|
|
1056
|
-
$1
|
|
1057
|
-
</HBProvider>
|
|
1058
|
-
);`);
|
|
1059
|
-
}
|
|
1060
|
-
injectRemixProvider(content) {
|
|
1061
|
-
if (content.includes('HumanBehaviorProvider')) {
|
|
1062
|
-
return content;
|
|
1063
|
-
}
|
|
1064
|
-
let modifiedContent = content;
|
|
1065
|
-
// Step 1: Add useLoaderData import
|
|
1066
|
-
if (!content.includes('useLoaderData')) {
|
|
1067
|
-
modifiedContent = modifiedContent.replace(/(} from ['"]@remix-run\/react['"];?\s*)/, `$1import { useLoaderData } from '@remix-run/react';
|
|
1068
|
-
`);
|
|
1069
|
-
}
|
|
1070
|
-
// Step 2: Add HumanBehaviorProvider import
|
|
1071
|
-
if (!content.includes('HumanBehaviorProvider')) {
|
|
1072
|
-
modifiedContent = modifiedContent.replace(/(} from ['"]@remix-run\/react['"];?\s*)/, `$1import { HumanBehaviorProvider } from 'humanbehavior-js/react';
|
|
1073
|
-
`);
|
|
1074
|
-
}
|
|
1075
|
-
// Step 3: Add LoaderFunctionArgs import
|
|
1076
|
-
if (!content.includes('LoaderFunctionArgs')) {
|
|
1077
|
-
modifiedContent = modifiedContent.replace(/(} from ['"]@remix-run\/node['"];?\s*)/, `$1import type { LoaderFunctionArgs } from '@remix-run/node';
|
|
1078
|
-
`);
|
|
1079
|
-
}
|
|
1080
|
-
// Step 4: Add loader function before Layout function
|
|
1081
|
-
if (!content.includes('export const loader')) {
|
|
1082
|
-
modifiedContent = modifiedContent.replace(/(export function Layout)/, `export const loader = async ({ request }: LoaderFunctionArgs) => {
|
|
1083
|
-
return {
|
|
1084
|
-
ENV: {
|
|
1085
|
-
HUMANBEHAVIOR_API_KEY: process.env.HUMANBEHAVIOR_API_KEY,
|
|
1086
|
-
},
|
|
1087
|
-
};
|
|
1088
|
-
};
|
|
1089
|
-
|
|
1090
|
-
$1`);
|
|
1091
|
-
}
|
|
1092
|
-
// Step 5: Add useLoaderData call and wrap App function's return content with HumanBehaviorProvider
|
|
1093
|
-
if (!content.includes('const data = useLoaderData')) {
|
|
1094
|
-
modifiedContent = modifiedContent.replace(/(export default function App\(\) \{\s*)(return \(\s*<div[^>]*>[\s\S]*?<\/div>\s*\);\s*\})/, `$1const data = useLoaderData<typeof loader>();
|
|
1095
|
-
|
|
1096
|
-
return (
|
|
1097
|
-
<HumanBehaviorProvider apiKey={data.ENV.HUMANBEHAVIOR_API_KEY}>
|
|
1098
|
-
<div className="min-h-screen bg-gray-50">
|
|
1099
|
-
<Navigation />
|
|
1100
|
-
<Outlet />
|
|
1101
|
-
</div>
|
|
1102
|
-
</HumanBehaviorProvider>
|
|
1103
|
-
);
|
|
1104
|
-
}`);
|
|
1105
|
-
}
|
|
1106
|
-
return modifiedContent;
|
|
1107
|
-
}
|
|
1108
|
-
injectVuePlugin(content) {
|
|
1109
|
-
// New: use composable/tracker pattern per docs; idempotent and migrates from plugin
|
|
1110
|
-
if (content.includes('useHumanBehavior')) {
|
|
1111
|
-
return content;
|
|
1112
|
-
}
|
|
1113
|
-
const hasVue3 = this.framework?.features?.hasVue3;
|
|
1114
|
-
const isVue3ByContent = content.includes('createApp') || content.includes('import { createApp }');
|
|
1115
|
-
let modifiedContent = content
|
|
1116
|
-
.replace(/import\s*\{\s*HumanBehaviorPlugin\s*\}\s*from\s*['\"]humanbehavior-js\/vue['\"];?/g, '')
|
|
1117
|
-
.replace(/app\.use\(\s*HumanBehaviorPlugin[\s\S]*?\);?/g, '');
|
|
1118
|
-
if (hasVue3 || isVue3ByContent) {
|
|
1119
|
-
const importComposable = `import { useHumanBehavior } from './composables/useHumanBehavior';`;
|
|
1120
|
-
if (!modifiedContent.includes(importComposable)) {
|
|
1121
|
-
const lastImportIndex = modifiedContent.lastIndexOf('import');
|
|
1122
|
-
if (lastImportIndex !== -1) {
|
|
1123
|
-
const nextLineIndex = modifiedContent.indexOf('\n', lastImportIndex);
|
|
1124
|
-
if (nextLineIndex !== -1) {
|
|
1125
|
-
modifiedContent = modifiedContent.slice(0, nextLineIndex + 1) + importComposable + '\n' + modifiedContent.slice(nextLineIndex + 1);
|
|
1126
|
-
}
|
|
1127
|
-
else {
|
|
1128
|
-
modifiedContent = modifiedContent + '\n' + importComposable;
|
|
1129
|
-
}
|
|
1130
|
-
}
|
|
1131
|
-
else {
|
|
1132
|
-
modifiedContent = importComposable + '\n' + modifiedContent;
|
|
396
|
+
for (let turn = 0; turn < MAX_AGENT_TURNS; turn++) {
|
|
397
|
+
const response = await this.apiClient.runAgentTurn(session.id, session.token, messages);
|
|
398
|
+
messages.push({
|
|
399
|
+
role: 'assistant',
|
|
400
|
+
content: response.message.content
|
|
401
|
+
});
|
|
402
|
+
if (response.toolRequests.length === 0) {
|
|
403
|
+
if (!installationVerified) {
|
|
404
|
+
const result = this.failure(repo, 'Managed agent stopped before verification completed.', dashboardUrl);
|
|
405
|
+
await this.updateRunSafely(runId, {
|
|
406
|
+
status: 'failed',
|
|
407
|
+
phase: 'check-errors',
|
|
408
|
+
result
|
|
409
|
+
});
|
|
410
|
+
return result;
|
|
1133
411
|
}
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
if (!content.includes('environment')) {
|
|
1160
|
-
modifiedContent = content.replace(/import.*from.*['"]@angular/, `${environmentImport}\n$&`);
|
|
1161
|
-
}
|
|
1162
|
-
return modifiedContent.replace(/imports:\s*\[([\s\S]*?)\]/, `imports: [
|
|
1163
|
-
$1,
|
|
1164
|
-
HumanBehaviorModule.forRoot({
|
|
1165
|
-
apiKey: environment.humanBehaviorApiKey
|
|
1166
|
-
})
|
|
1167
|
-
]`).replace(/import.*from.*['"]@angular/, `$&\n${importStatement}`);
|
|
1168
|
-
}
|
|
1169
|
-
injectAngularStandaloneInit(content) {
|
|
1170
|
-
if (content.includes('initializeHumanBehavior')) {
|
|
1171
|
-
return content;
|
|
1172
|
-
}
|
|
1173
|
-
const importStatement = `import { initializeHumanBehavior } from 'humanbehavior-js/angular';`;
|
|
1174
|
-
const environmentImport = `import { environment } from './environments/environment';`;
|
|
1175
|
-
// Add imports at the top
|
|
1176
|
-
let modifiedContent = content.replace(/import.*from.*['"]@angular/, `${importStatement}\n${environmentImport}\n$&`);
|
|
1177
|
-
// Add initialization after bootstrapApplication
|
|
1178
|
-
modifiedContent = modifiedContent.replace(/(bootstrapApplication\([^}]+\}?\)(?:\s*\.catch[^;]+;)?)/, `$1
|
|
1179
|
-
|
|
1180
|
-
// Initialize HumanBehavior SDK (client-side only)
|
|
1181
|
-
if (typeof window !== 'undefined') {
|
|
1182
|
-
const tracker = initializeHumanBehavior(environment.humanBehaviorApiKey);
|
|
1183
|
-
}`);
|
|
1184
|
-
return modifiedContent;
|
|
1185
|
-
}
|
|
1186
|
-
injectSvelteStore(content) {
|
|
1187
|
-
// Direct tracker init for non-SSR Svelte
|
|
1188
|
-
if (content.includes('HumanBehaviorTracker.init')) {
|
|
1189
|
-
return content;
|
|
1190
|
-
}
|
|
1191
|
-
const importStatement = `import { HumanBehaviorTracker } from 'humanbehavior-js';`;
|
|
1192
|
-
const initCode = `// Initialize HumanBehavior SDK\nHumanBehaviorTracker.init(import.meta.env?.VITE_HUMANBEHAVIOR_API_KEY || process.env.PUBLIC_HUMANBEHAVIOR_API_KEY || '');`;
|
|
1193
|
-
return `${importStatement}\n${initCode}\n\n${content}`;
|
|
1194
|
-
}
|
|
1195
|
-
injectSvelteKitLayout(content) {
|
|
1196
|
-
// Direct tracker init with browser guard for SvelteKit
|
|
1197
|
-
if (content.includes('HumanBehaviorTracker.init')) {
|
|
1198
|
-
return content;
|
|
1199
|
-
}
|
|
1200
|
-
const envImport = `import { PUBLIC_HUMANBEHAVIOR_API_KEY } from '$env/static/public';`;
|
|
1201
|
-
const hbImport = `import { HumanBehaviorTracker } from 'humanbehavior-js';`;
|
|
1202
|
-
const browserImport = `import { browser } from '$app/environment';`;
|
|
1203
|
-
const initCode = `if (browser) {\n const apiKey = PUBLIC_HUMANBEHAVIOR_API_KEY || import.meta.env.VITE_HUMANBEHAVIOR_API_KEY;\n if (apiKey) {\n HumanBehaviorTracker.init(apiKey);\n }\n}`;
|
|
1204
|
-
if (content.includes('<script lang="ts">')) {
|
|
1205
|
-
return content.replace(/<script lang="ts">/, `<script lang="ts">\n\t${browserImport}\n\t${envImport}\n\t${hbImport}\n\t${initCode}`);
|
|
1206
|
-
}
|
|
1207
|
-
else if (content.includes('<script>')) {
|
|
1208
|
-
return content.replace(/<script>/, `<script>\n\t${browserImport}\n\t${envImport}\n\t${hbImport}\n\t${initCode}`);
|
|
1209
|
-
}
|
|
1210
|
-
else {
|
|
1211
|
-
return `<script lang="ts">\n${browserImport}\n${envImport}\n${hbImport}\n${initCode}\n</script>\n\n${content}`;
|
|
1212
|
-
}
|
|
1213
|
-
}
|
|
1214
|
-
injectVanillaScript(content) {
|
|
1215
|
-
if (content.includes('humanbehavior-js')) {
|
|
1216
|
-
return content;
|
|
1217
|
-
}
|
|
1218
|
-
const cdnScript = `<script src="https://unpkg.com/humanbehavior-js@latest/dist/index.min.js"></script>`;
|
|
1219
|
-
const initScript = `<script>
|
|
1220
|
-
// Initialize HumanBehavior SDK
|
|
1221
|
-
// Note: For vanilla HTML, the API key must be hardcoded since env vars aren't available
|
|
1222
|
-
const tracker = HumanBehaviorTracker.init('${this.apiKey}');
|
|
1223
|
-
</script>`;
|
|
1224
|
-
return content.replace(/<\/head>/, ` ${cdnScript}\n ${initScript}\n</head>`);
|
|
1225
|
-
}
|
|
1226
|
-
/**
|
|
1227
|
-
* Inject Astro layout with HumanBehavior component
|
|
1228
|
-
*/
|
|
1229
|
-
injectAstroLayout(content) {
|
|
1230
|
-
// Check if HumanBehavior component is already imported
|
|
1231
|
-
if (content.includes('HumanBehavior') || content.includes('humanbehavior-js')) {
|
|
1232
|
-
return content; // Already has HumanBehavior
|
|
1233
|
-
}
|
|
1234
|
-
// Add import inside frontmatter if not present
|
|
1235
|
-
let modifiedContent = content;
|
|
1236
|
-
if (!content.includes('import HumanBehavior')) {
|
|
1237
|
-
const importStatement = 'import HumanBehavior from \'../components/HumanBehavior.astro\';';
|
|
1238
|
-
const frontmatterEndIndex = content.indexOf('---', 3);
|
|
1239
|
-
if (frontmatterEndIndex !== -1) {
|
|
1240
|
-
// Insert import inside frontmatter, before the closing ---
|
|
1241
|
-
modifiedContent = content.slice(0, frontmatterEndIndex) + '\n' + importStatement + '\n' + content.slice(frontmatterEndIndex);
|
|
1242
|
-
}
|
|
1243
|
-
else {
|
|
1244
|
-
// No frontmatter, add at the very beginning
|
|
1245
|
-
modifiedContent = '---\n' + importStatement + '\n---\n\n' + content;
|
|
1246
|
-
}
|
|
1247
|
-
}
|
|
1248
|
-
// Find the closing </body> tag and add HumanBehavior component before it
|
|
1249
|
-
const bodyCloseIndex = modifiedContent.lastIndexOf('</body>');
|
|
1250
|
-
if (bodyCloseIndex === -1) {
|
|
1251
|
-
// No body tag found, append to end
|
|
1252
|
-
return modifiedContent + '\n\n<HumanBehavior />';
|
|
1253
|
-
}
|
|
1254
|
-
// Add component before closing body tag
|
|
1255
|
-
return modifiedContent.slice(0, bodyCloseIndex) + ' <HumanBehavior />\n' + modifiedContent.slice(bodyCloseIndex);
|
|
1256
|
-
}
|
|
1257
|
-
injectNuxtConfig(content) {
|
|
1258
|
-
if (content.includes('humanBehaviorApiKey')) {
|
|
1259
|
-
return content;
|
|
1260
|
-
}
|
|
1261
|
-
// Enhanced Nuxt 3 support with version detection
|
|
1262
|
-
const hasNuxt3 = this.framework?.features?.hasNuxt3;
|
|
1263
|
-
if (hasNuxt3) {
|
|
1264
|
-
// Nuxt 3 with runtime config (robust match for opening object)
|
|
1265
|
-
const pattern = /export\s+default\s+defineNuxtConfig\s*\(\s*\{/;
|
|
1266
|
-
if (pattern.test(content)) {
|
|
1267
|
-
const replaced = content.replace(pattern, `export default defineNuxtConfig({\n runtimeConfig: {\n public: {\n humanBehaviorApiKey: process.env.NUXT_PUBLIC_HUMANBEHAVIOR_API_KEY\n }\n },`);
|
|
1268
|
-
if (replaced !== content)
|
|
1269
|
-
return replaced;
|
|
1270
|
-
}
|
|
1271
|
-
// Fallback: insert runtimeConfig after opening brace of defineNuxtConfig
|
|
1272
|
-
const startIdx = content.indexOf('defineNuxtConfig(');
|
|
1273
|
-
if (startIdx !== -1) {
|
|
1274
|
-
const braceIdx = content.indexOf('{', startIdx);
|
|
1275
|
-
if (braceIdx !== -1) {
|
|
1276
|
-
const insertion = `\n runtimeConfig: {\n public: {\n humanBehaviorApiKey: process.env.NUXT_PUBLIC_HUMANBEHAVIOR_API_KEY\n }\n },`;
|
|
1277
|
-
const before = content.slice(0, braceIdx + 1);
|
|
1278
|
-
const after = content.slice(braceIdx + 1);
|
|
1279
|
-
return `${before}${insertion}${after}`;
|
|
412
|
+
const result = {
|
|
413
|
+
success: true,
|
|
414
|
+
phase: 'finish',
|
|
415
|
+
repo,
|
|
416
|
+
errors: [],
|
|
417
|
+
nextSteps: [
|
|
418
|
+
'Managed agent loop completed.',
|
|
419
|
+
'Review the local git diff before committing.',
|
|
420
|
+
...(dashboardUrl ? [`Dashboard run: ${dashboardUrl}`] : [])
|
|
421
|
+
],
|
|
422
|
+
dashboardUrl
|
|
423
|
+
};
|
|
424
|
+
await this.updateRunSafely(runId, {
|
|
425
|
+
status: 'completed',
|
|
426
|
+
phase: 'finish',
|
|
427
|
+
result
|
|
428
|
+
});
|
|
429
|
+
return result;
|
|
430
|
+
}
|
|
431
|
+
await this.updateRunSafely(runId, {
|
|
432
|
+
status: 'running',
|
|
433
|
+
phase: this.phaseForTools(response.toolRequests),
|
|
434
|
+
result: {
|
|
435
|
+
success: false,
|
|
436
|
+
nextSteps: [`Executing ${response.toolRequests.length} agent tool request(s).`]
|
|
1280
437
|
}
|
|
438
|
+
});
|
|
439
|
+
const toolResults = [];
|
|
440
|
+
for (const toolRequest of response.toolRequests) {
|
|
441
|
+
toolResults.push(await executor.execute(toolRequest));
|
|
1281
442
|
}
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
return content.replace(/export default \{/, `export default {
|
|
1287
|
-
env: {
|
|
1288
|
-
humanBehaviorApiKey: process.env.HUMANBEHAVIOR_API_KEY
|
|
1289
|
-
},`);
|
|
443
|
+
messages.push({
|
|
444
|
+
role: 'user',
|
|
445
|
+
content: toolResults
|
|
446
|
+
});
|
|
1290
447
|
}
|
|
448
|
+
const result = this.failure(repo, `Managed agent exceeded ${MAX_AGENT_TURNS} turns before finishing.`, dashboardUrl);
|
|
449
|
+
await this.updateRunSafely(runId, {
|
|
450
|
+
status: 'failed',
|
|
451
|
+
phase: 'check-errors',
|
|
452
|
+
result
|
|
453
|
+
});
|
|
454
|
+
return result;
|
|
1291
455
|
}
|
|
1292
|
-
|
|
1293
|
-
|
|
1294
|
-
|
|
456
|
+
createInitialPrompt(repo) {
|
|
457
|
+
return [
|
|
458
|
+
'Run the Human Behavior SDK installation now.',
|
|
459
|
+
'',
|
|
460
|
+
`Project root: ${this.projectPath}`,
|
|
461
|
+
`Execution mode: ${this.executionMode}`,
|
|
462
|
+
`Human Behavior API key: ${this.apiKey}`,
|
|
463
|
+
`Package manager: ${repo.packageManager}`,
|
|
464
|
+
`Workspace shape: ${repo.workspaceShape}`,
|
|
465
|
+
`package.json files: ${repo.packageJsonPaths.join(', ') || 'none'}`,
|
|
466
|
+
'',
|
|
467
|
+
'Use the available tools to inspect and edit the project.',
|
|
468
|
+
'Do not inspect node_modules, package-lock.json, pnpm-lock.yaml, yarn.lock, build output, or dist output unless absolutely necessary.',
|
|
469
|
+
'For browser SDK imports, humanbehavior-js has no default export. Use named exports, for example: import { init } from "humanbehavior-js"; then call init(apiKey).',
|
|
470
|
+
'When using hb_write_file, write exactly one complete file body. Do not duplicate previous file contents.',
|
|
471
|
+
'When finished, stop requesting tools and summarize what changed.'
|
|
472
|
+
].join('\n');
|
|
473
|
+
}
|
|
474
|
+
phaseForTools(toolRequests) {
|
|
475
|
+
if (toolRequests.some((tool) => tool.name === 'hb_verify_installation')) {
|
|
476
|
+
return 'check-errors';
|
|
477
|
+
}
|
|
478
|
+
if (toolRequests.some((tool) => tool.name === 'hb_write_file')) {
|
|
479
|
+
return 'edit';
|
|
480
|
+
}
|
|
481
|
+
if (toolRequests.some((tool) => tool.name === 'hb_bash')) {
|
|
482
|
+
return 'check-errors';
|
|
483
|
+
}
|
|
484
|
+
return 'analyze';
|
|
485
|
+
}
|
|
486
|
+
async updateRunSafely(runId, input) {
|
|
487
|
+
if (!runId)
|
|
488
|
+
return;
|
|
489
|
+
try {
|
|
490
|
+
await this.apiClient.updateRun(runId, input);
|
|
1295
491
|
}
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
let modifiedContent = content.replace(/import.*from.*['"]\./, `${importStatement}\n$&`);
|
|
1300
|
-
// Add component before closing body tag
|
|
1301
|
-
modifiedContent = modifiedContent.replace(/(\s*<\/body>)/, `\n ${componentUsage}\n$1`);
|
|
1302
|
-
return modifiedContent;
|
|
1303
|
-
}
|
|
1304
|
-
injectGatsbyBrowser(content) {
|
|
1305
|
-
const importStatement = `import { HumanBehaviorTracker } from 'humanbehavior-js';`;
|
|
1306
|
-
// If an onClientEntry already exists, merge init into it idempotently
|
|
1307
|
-
if (/export\s+const\s+onClientEntry\s*=\s*\(/.test(content)) {
|
|
1308
|
-
let modified = content;
|
|
1309
|
-
// Ensure import exists
|
|
1310
|
-
if (!modified.includes("from 'humanbehavior-js'")) {
|
|
1311
|
-
modified = `${importStatement}\n${modified}`;
|
|
1312
|
-
}
|
|
1313
|
-
// If init already present, return as-is
|
|
1314
|
-
if (modified.includes('HumanBehaviorTracker.init(')) {
|
|
1315
|
-
return modified;
|
|
1316
|
-
}
|
|
1317
|
-
// Inject minimal init at start of onClientEntry body
|
|
1318
|
-
modified = modified.replace(/(export\s+const\s+onClientEntry\s*=\s*\([^)]*\)\s*=>\s*\{)/, `$1\n const apiKey = process.env.GATSBY_HUMANBEHAVIOR_API_KEY;\n if (apiKey) {\n HumanBehaviorTracker.init(apiKey);\n }\n`);
|
|
1319
|
-
return modified;
|
|
492
|
+
catch {
|
|
493
|
+
// The CLI result is more important than run-state sync. Keep failures visible
|
|
494
|
+
// through the main result path when run creation fails.
|
|
1320
495
|
}
|
|
1321
|
-
// No existing onClientEntry: create minimal file content or prepend to existing
|
|
1322
|
-
const block = `export const onClientEntry = () => {\n const apiKey = process.env.GATSBY_HUMANBEHAVIOR_API_KEY;\n if (apiKey) {\n HumanBehaviorTracker.init(apiKey);\n }\n};`;
|
|
1323
|
-
const header = content.trim() ? `${importStatement}\n` : `${importStatement}\n`;
|
|
1324
|
-
return `${header}${block}${content.trim() ? `\n\n${content}` : ''}`;
|
|
1325
496
|
}
|
|
1326
|
-
|
|
1327
|
-
* Helper method to find the best environment file for a framework
|
|
1328
|
-
*/
|
|
1329
|
-
findBestEnvFile(framework) {
|
|
1330
|
-
const possibleEnvFiles = [
|
|
1331
|
-
'.env.local',
|
|
1332
|
-
'.env.development.local',
|
|
1333
|
-
'.env.development',
|
|
1334
|
-
'.env.local.development',
|
|
1335
|
-
'.env',
|
|
1336
|
-
'.env.production',
|
|
1337
|
-
'.env.staging'
|
|
1338
|
-
];
|
|
1339
|
-
// Framework-specific environment variable names
|
|
1340
|
-
const getEnvVarName = (framework) => {
|
|
1341
|
-
// Handle React+Vite specifically
|
|
1342
|
-
if (framework.type === 'react' && framework.bundler === 'vite') {
|
|
1343
|
-
return 'VITE_HUMANBEHAVIOR_API_KEY';
|
|
1344
|
-
}
|
|
1345
|
-
// Framework-specific mappings
|
|
1346
|
-
const envVarNames = {
|
|
1347
|
-
react: 'REACT_APP_HUMANBEHAVIOR_API_KEY',
|
|
1348
|
-
nextjs: 'NEXT_PUBLIC_HUMANBEHAVIOR_API_KEY',
|
|
1349
|
-
vue: 'VITE_HUMANBEHAVIOR_API_KEY',
|
|
1350
|
-
svelte: 'PUBLIC_HUMANBEHAVIOR_API_KEY',
|
|
1351
|
-
angular: 'HUMANBEHAVIOR_API_KEY',
|
|
1352
|
-
nuxt: 'NUXT_PUBLIC_HUMANBEHAVIOR_API_KEY',
|
|
1353
|
-
remix: 'HUMANBEHAVIOR_API_KEY',
|
|
1354
|
-
vanilla: 'HUMANBEHAVIOR_API_KEY',
|
|
1355
|
-
astro: 'PUBLIC_HUMANBEHAVIOR_API_KEY',
|
|
1356
|
-
gatsby: 'GATSBY_HUMANBEHAVIOR_API_KEY',
|
|
1357
|
-
node: 'HUMANBEHAVIOR_API_KEY',
|
|
1358
|
-
auto: 'HUMANBEHAVIOR_API_KEY'
|
|
1359
|
-
};
|
|
1360
|
-
return envVarNames[framework.type] || 'HUMANBEHAVIOR_API_KEY';
|
|
1361
|
-
};
|
|
1362
|
-
const envVarName = getEnvVarName(framework);
|
|
1363
|
-
// Check for existing files
|
|
1364
|
-
for (const envFile of possibleEnvFiles) {
|
|
1365
|
-
const fullPath = path.join(this.projectRoot, envFile);
|
|
1366
|
-
if (fs.existsSync(fullPath)) {
|
|
1367
|
-
return { filePath: fullPath, envVarName };
|
|
1368
|
-
}
|
|
1369
|
-
}
|
|
1370
|
-
// Framework-specific default file creation
|
|
1371
|
-
const defaultFiles = {
|
|
1372
|
-
react: '.env.local',
|
|
1373
|
-
nextjs: '.env.local',
|
|
1374
|
-
vue: '.env.local',
|
|
1375
|
-
svelte: '.env',
|
|
1376
|
-
angular: '.env',
|
|
1377
|
-
nuxt: '.env',
|
|
1378
|
-
remix: '.env.local',
|
|
1379
|
-
vanilla: '.env',
|
|
1380
|
-
astro: '.env',
|
|
1381
|
-
gatsby: '.env.development',
|
|
1382
|
-
node: '.env',
|
|
1383
|
-
auto: '.env'
|
|
1384
|
-
};
|
|
1385
|
-
const defaultFile = defaultFiles[framework.type] || '.env';
|
|
497
|
+
failure(repo, error, dashboardUrl) {
|
|
1386
498
|
return {
|
|
1387
|
-
|
|
1388
|
-
|
|
499
|
+
success: false,
|
|
500
|
+
phase: 'analyze',
|
|
501
|
+
repo,
|
|
502
|
+
errors: [error],
|
|
503
|
+
nextSteps: [
|
|
504
|
+
'No files were changed.',
|
|
505
|
+
'Please follow the manual installation docs while the agent installer is unavailable.',
|
|
506
|
+
...(dashboardUrl ? [`Dashboard run: ${dashboardUrl}`] : []),
|
|
507
|
+
`Manual setup docs: ${MANUAL_DOCS_URL}`
|
|
508
|
+
],
|
|
509
|
+
dashboardUrl
|
|
1389
510
|
};
|
|
1390
511
|
}
|
|
1391
|
-
|
|
1392
|
-
|
|
1393
|
-
|
|
1394
|
-
|
|
1395
|
-
|
|
1396
|
-
|
|
1397
|
-
|
|
1398
|
-
if (fs.existsSync(filePath)) {
|
|
1399
|
-
// Check if the variable already exists
|
|
1400
|
-
const content = fs.readFileSync(filePath, 'utf8');
|
|
1401
|
-
if (content.includes(envVarName)) {
|
|
1402
|
-
// Variable exists, don't modify
|
|
1403
|
-
return {
|
|
1404
|
-
filePath,
|
|
1405
|
-
action: 'modify',
|
|
1406
|
-
content: content, // No change
|
|
1407
|
-
description: `API key already exists in ${path.basename(filePath)}`
|
|
1408
|
-
};
|
|
1409
|
-
}
|
|
1410
|
-
else {
|
|
1411
|
-
// Append to existing file
|
|
1412
|
-
return {
|
|
1413
|
-
filePath,
|
|
1414
|
-
action: 'append',
|
|
1415
|
-
content: `\n${envVarName}=${cleanApiKey}`,
|
|
1416
|
-
description: `Added API key to existing ${path.basename(filePath)}`
|
|
1417
|
-
};
|
|
1418
|
-
}
|
|
1419
|
-
}
|
|
1420
|
-
else {
|
|
1421
|
-
// Create new file
|
|
1422
|
-
return {
|
|
1423
|
-
filePath,
|
|
1424
|
-
action: 'create',
|
|
1425
|
-
content: `${envVarName}=${cleanApiKey}`,
|
|
1426
|
-
description: `Created ${path.basename(filePath)} with API key`
|
|
1427
|
-
};
|
|
1428
|
-
}
|
|
1429
|
-
}
|
|
1430
|
-
}
|
|
1431
|
-
|
|
1432
|
-
/**
|
|
1433
|
-
* Remote AI Service Implementation
|
|
1434
|
-
*
|
|
1435
|
-
* This connects to your deployed Lambda function via API Gateway
|
|
1436
|
-
*/
|
|
1437
|
-
class RemoteAIService {
|
|
1438
|
-
constructor(config) {
|
|
1439
|
-
this.config = {
|
|
1440
|
-
timeout: 10000, // 10 seconds
|
|
1441
|
-
...config
|
|
512
|
+
scanRepo() {
|
|
513
|
+
const packageJsonPaths = this.findPackageJsonPaths(this.projectPath);
|
|
514
|
+
return {
|
|
515
|
+
projectPath: this.projectPath,
|
|
516
|
+
packageManager: this.detectPackageManager(this.projectPath),
|
|
517
|
+
workspaceShape: this.detectWorkspaceShape(packageJsonPaths),
|
|
518
|
+
packageJsonPaths
|
|
1442
519
|
};
|
|
1443
520
|
}
|
|
1444
|
-
|
|
1445
|
-
|
|
1446
|
-
|
|
1447
|
-
|
|
1448
|
-
|
|
1449
|
-
|
|
1450
|
-
|
|
1451
|
-
|
|
1452
|
-
|
|
1453
|
-
},
|
|
1454
|
-
body: JSON.stringify({ codeSamples }),
|
|
1455
|
-
signal: AbortSignal.timeout(this.config.timeout || 10000)
|
|
1456
|
-
});
|
|
1457
|
-
if (!response.ok) {
|
|
1458
|
-
throw new Error(`AI service returned ${response.status}: ${response.statusText}`);
|
|
521
|
+
findPackageJsonPaths(root) {
|
|
522
|
+
const results = [];
|
|
523
|
+
const ignoredDirs = new Set(['.git', '.next', 'dist', 'build', 'coverage', 'node_modules']);
|
|
524
|
+
const visit = (dir, depth) => {
|
|
525
|
+
if (depth > 4)
|
|
526
|
+
return;
|
|
527
|
+
let entries;
|
|
528
|
+
try {
|
|
529
|
+
entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
1459
530
|
}
|
|
1460
|
-
|
|
1461
|
-
|
|
1462
|
-
}
|
|
1463
|
-
catch (error) {
|
|
1464
|
-
console.warn('Remote AI service failed, falling back to heuristic analysis:', error);
|
|
1465
|
-
return this.performHeuristicAnalysis(codeSamples);
|
|
1466
|
-
}
|
|
1467
|
-
}
|
|
1468
|
-
/**
|
|
1469
|
-
* Resolve conflicts using your deployed AI service
|
|
1470
|
-
*/
|
|
1471
|
-
async resolveConflicts(conflicts, framework) {
|
|
1472
|
-
try {
|
|
1473
|
-
const response = await fetch(`${this.config.apiEndpoint}/resolve-conflicts`, {
|
|
1474
|
-
method: 'POST',
|
|
1475
|
-
headers: {
|
|
1476
|
-
'Content-Type': 'application/json',
|
|
1477
|
-
},
|
|
1478
|
-
body: JSON.stringify({ conflicts, framework }),
|
|
1479
|
-
signal: AbortSignal.timeout(this.config.timeout || 10000)
|
|
1480
|
-
});
|
|
1481
|
-
if (!response.ok) {
|
|
1482
|
-
throw new Error(`AI service returned ${response.status}: ${response.statusText}`);
|
|
531
|
+
catch {
|
|
532
|
+
return;
|
|
1483
533
|
}
|
|
1484
|
-
const
|
|
1485
|
-
|
|
1486
|
-
|
|
1487
|
-
|
|
1488
|
-
|
|
1489
|
-
|
|
1490
|
-
|
|
1491
|
-
|
|
1492
|
-
|
|
1493
|
-
* Generate optimizations using your deployed AI service
|
|
1494
|
-
*/
|
|
1495
|
-
async generateOptimizations(framework, patterns) {
|
|
1496
|
-
try {
|
|
1497
|
-
const response = await fetch(`${this.config.apiEndpoint}/optimize`, {
|
|
1498
|
-
method: 'POST',
|
|
1499
|
-
headers: {
|
|
1500
|
-
'Content-Type': 'application/json',
|
|
1501
|
-
},
|
|
1502
|
-
body: JSON.stringify({ framework, patterns }),
|
|
1503
|
-
signal: AbortSignal.timeout(this.config.timeout || 10000)
|
|
1504
|
-
});
|
|
1505
|
-
if (!response.ok) {
|
|
1506
|
-
throw new Error(`AI service returned ${response.status}: ${response.statusText}`);
|
|
534
|
+
for (const entry of entries) {
|
|
535
|
+
const fullPath = path.join(dir, entry.name);
|
|
536
|
+
if (entry.isFile() && entry.name === 'package.json') {
|
|
537
|
+
results.push(path.relative(root, fullPath) || 'package.json');
|
|
538
|
+
continue;
|
|
539
|
+
}
|
|
540
|
+
if (entry.isDirectory() && !ignoredDirs.has(entry.name) && !entry.name.startsWith('.')) {
|
|
541
|
+
visit(fullPath, depth + 1);
|
|
542
|
+
}
|
|
1507
543
|
}
|
|
1508
|
-
const result = await response.json();
|
|
1509
|
-
return result.optimizations || [];
|
|
1510
|
-
}
|
|
1511
|
-
catch (error) {
|
|
1512
|
-
console.warn('Remote AI optimization generation failed, using heuristic approach:', error);
|
|
1513
|
-
return this.generateOptimizationsHeuristic(framework, patterns);
|
|
1514
|
-
}
|
|
1515
|
-
}
|
|
1516
|
-
/**
|
|
1517
|
-
* Heuristic analysis fallback
|
|
1518
|
-
*/
|
|
1519
|
-
performHeuristicAnalysis(codeSamples) {
|
|
1520
|
-
const patterns = codeSamples.join(' ').toLowerCase();
|
|
1521
|
-
// Framework detection
|
|
1522
|
-
let framework = { name: 'vanilla', type: 'vanilla' };
|
|
1523
|
-
let confidence = 0.5;
|
|
1524
|
-
if (patterns.includes('nuxt') || patterns.includes('nuxtjs') || patterns.includes('defineNuxtConfig') || patterns.includes('nuxt.config') || patterns.includes('@nuxt/') || patterns.includes('useNuxtApp') || patterns.includes('useRuntimeConfig') || patterns.includes('useSeoMeta') || patterns.includes('useHead') || patterns.includes('useLazyFetch') || patterns.includes('useFetch') || patterns.includes('useAsyncData') || patterns.includes('#app')) {
|
|
1525
|
-
framework = { name: 'nuxt', type: 'nuxt' };
|
|
1526
|
-
confidence = 0.95;
|
|
1527
|
-
}
|
|
1528
|
-
else if (patterns.includes('next') || patterns.includes('nextjs') || patterns.includes('next/link') || patterns.includes('next/image') || patterns.includes('next/navigation') || patterns.includes('next/router') || patterns.includes('getserverSideProps') || patterns.includes('getstaticProps') || patterns.includes('getstaticPaths') || patterns.includes('app/layout') || patterns.includes('app/page') || patterns.includes('pages/')) {
|
|
1529
|
-
framework = { name: 'nextjs', type: 'nextjs' };
|
|
1530
|
-
confidence = 0.95;
|
|
1531
|
-
}
|
|
1532
|
-
else if (patterns.includes('gatsby') || patterns.includes('gatsby-browser') || patterns.includes('gatsby-ssr') || patterns.includes('gatsby-node') || patterns.includes('gatsby-config') || patterns.includes('useStaticQuery') || patterns.includes('graphql')) {
|
|
1533
|
-
framework = { name: 'gatsby', type: 'gatsby' };
|
|
1534
|
-
confidence = 0.95;
|
|
1535
|
-
}
|
|
1536
|
-
else if (patterns.includes('react')) {
|
|
1537
|
-
framework = { name: 'react', type: 'react' };
|
|
1538
|
-
confidence = 0.9;
|
|
1539
|
-
}
|
|
1540
|
-
else if (patterns.includes('vue')) {
|
|
1541
|
-
framework = { name: 'vue', type: 'vue' };
|
|
1542
|
-
confidence = 0.9;
|
|
1543
|
-
}
|
|
1544
|
-
else if (patterns.includes('angular')) {
|
|
1545
|
-
framework = { name: 'angular', type: 'angular' };
|
|
1546
|
-
confidence = 0.9;
|
|
1547
|
-
}
|
|
1548
|
-
else if (patterns.includes('svelte')) {
|
|
1549
|
-
framework = { name: 'svelte', type: 'svelte' };
|
|
1550
|
-
confidence = 0.9;
|
|
1551
|
-
}
|
|
1552
|
-
else if (patterns.includes('astro')) {
|
|
1553
|
-
framework = { name: 'astro', type: 'astro' };
|
|
1554
|
-
confidence = 0.9;
|
|
1555
|
-
}
|
|
1556
|
-
// Integration strategy
|
|
1557
|
-
let integrationStrategy = 'script';
|
|
1558
|
-
if (framework.type === 'react' || framework.type === 'nextjs' || framework.type === 'gatsby') {
|
|
1559
|
-
integrationStrategy = 'provider';
|
|
1560
|
-
}
|
|
1561
|
-
else if (framework.type === 'vue') {
|
|
1562
|
-
integrationStrategy = 'plugin';
|
|
1563
|
-
}
|
|
1564
|
-
else if (framework.type === 'angular') {
|
|
1565
|
-
integrationStrategy = 'module';
|
|
1566
|
-
}
|
|
1567
|
-
// Compatibility mode
|
|
1568
|
-
let compatibilityMode = 'modern';
|
|
1569
|
-
if (patterns.includes('require(') || patterns.includes('var ')) {
|
|
1570
|
-
compatibilityMode = 'legacy';
|
|
1571
|
-
}
|
|
1572
|
-
return {
|
|
1573
|
-
framework,
|
|
1574
|
-
confidence,
|
|
1575
|
-
patterns: codeSamples,
|
|
1576
|
-
conflicts: [],
|
|
1577
|
-
recommendations: [],
|
|
1578
|
-
integrationStrategy,
|
|
1579
|
-
compatibilityMode
|
|
1580
544
|
};
|
|
545
|
+
visit(root, 0);
|
|
546
|
+
return results.sort();
|
|
1581
547
|
}
|
|
1582
|
-
|
|
1583
|
-
|
|
1584
|
-
|
|
1585
|
-
resolveConflictsHeuristic(conflicts, framework) {
|
|
1586
|
-
const resolutions = [];
|
|
1587
|
-
for (const conflict of conflicts) {
|
|
1588
|
-
switch (conflict) {
|
|
1589
|
-
case 'existing_humanbehavior_code':
|
|
1590
|
-
resolutions.push('update_existing_integration');
|
|
1591
|
-
break;
|
|
1592
|
-
case 'existing_provider':
|
|
1593
|
-
resolutions.push('merge_providers');
|
|
1594
|
-
break;
|
|
1595
|
-
case 'module_system_conflict':
|
|
1596
|
-
resolutions.push('hybrid_module_support');
|
|
1597
|
-
break;
|
|
1598
|
-
default:
|
|
1599
|
-
resolutions.push('skip_conflict');
|
|
1600
|
-
}
|
|
548
|
+
detectPackageManager(root) {
|
|
549
|
+
if (fs.existsSync(path.join(root, 'bun.lockb')) || fs.existsSync(path.join(root, 'bun.lock'))) {
|
|
550
|
+
return 'bun';
|
|
1601
551
|
}
|
|
1602
|
-
|
|
1603
|
-
|
|
1604
|
-
/**
|
|
1605
|
-
* Heuristic optimization generation
|
|
1606
|
-
*/
|
|
1607
|
-
generateOptimizationsHeuristic(framework, patterns) {
|
|
1608
|
-
const optimizations = [];
|
|
1609
|
-
switch (framework.type) {
|
|
1610
|
-
case 'react':
|
|
1611
|
-
optimizations.push('Use React.memo for performance optimization');
|
|
1612
|
-
optimizations.push('Implement error boundaries for better error tracking');
|
|
1613
|
-
optimizations.push('Consider using React.lazy for code splitting');
|
|
1614
|
-
break;
|
|
1615
|
-
case 'vue':
|
|
1616
|
-
optimizations.push('Use Vue 3 Composition API for better performance');
|
|
1617
|
-
optimizations.push('Implement proper error handling in components');
|
|
1618
|
-
optimizations.push('Consider using Vue Router for navigation tracking');
|
|
1619
|
-
break;
|
|
1620
|
-
case 'angular':
|
|
1621
|
-
optimizations.push('Use Angular standalone components for better tree-shaking');
|
|
1622
|
-
optimizations.push('Implement proper error handling with ErrorHandler');
|
|
1623
|
-
optimizations.push('Consider using Angular signals for state management');
|
|
1624
|
-
break;
|
|
1625
|
-
default:
|
|
1626
|
-
optimizations.push('Enable performance tracking');
|
|
1627
|
-
optimizations.push('Implement error tracking');
|
|
1628
|
-
optimizations.push('Consider progressive enhancement');
|
|
552
|
+
if (fs.existsSync(path.join(root, 'pnpm-lock.yaml'))) {
|
|
553
|
+
return 'pnpm';
|
|
1629
554
|
}
|
|
1630
|
-
|
|
1631
|
-
|
|
1632
|
-
|
|
1633
|
-
|
|
1634
|
-
/**
|
|
1635
|
-
* Manual Framework Installation Wizard
|
|
1636
|
-
*
|
|
1637
|
-
* This wizard allows users to manually specify their framework instead of auto-detection.
|
|
1638
|
-
* Useful when auto-detection fails or users want more control.
|
|
1639
|
-
*/
|
|
1640
|
-
class ManualFrameworkInstallationWizard extends AutoInstallationWizard {
|
|
1641
|
-
constructor(apiKey, projectRoot = process.cwd(), framework) {
|
|
1642
|
-
super(apiKey, projectRoot);
|
|
1643
|
-
this.selectedFramework = framework.toLowerCase();
|
|
1644
|
-
this.framework = this.createFrameworkInfo(this.selectedFramework);
|
|
1645
|
-
}
|
|
1646
|
-
/**
|
|
1647
|
-
* Manual installation with user-specified framework
|
|
1648
|
-
*/
|
|
1649
|
-
async install() {
|
|
1650
|
-
try {
|
|
1651
|
-
// Step 1: Handle framework selection
|
|
1652
|
-
if (this.selectedFramework === 'auto') {
|
|
1653
|
-
// Use full AI detection for "Other" option
|
|
1654
|
-
this.framework = await this.runFullDetection();
|
|
1655
|
-
}
|
|
1656
|
-
else {
|
|
1657
|
-
// Set framework based on user selection
|
|
1658
|
-
this.framework = this.createFrameworkInfo(this.selectedFramework);
|
|
1659
|
-
if (!this.framework) {
|
|
1660
|
-
this.framework = { name: 'unknown', type: 'vanilla' };
|
|
1661
|
-
}
|
|
1662
|
-
// Step 2: Run full detection logic to find entry points, file names, etc.
|
|
1663
|
-
const detectedFramework = await this.runFullDetection();
|
|
1664
|
-
// Step 3: Merge manual framework with detected details
|
|
1665
|
-
this.framework = {
|
|
1666
|
-
...detectedFramework,
|
|
1667
|
-
name: this.framework.name,
|
|
1668
|
-
type: this.framework.type
|
|
1669
|
-
};
|
|
555
|
+
if (fs.existsSync(path.join(root, 'yarn.lock'))) {
|
|
556
|
+
if (fs.existsSync(path.join(root, '.yarnrc.yml')) || fs.existsSync(path.join(root, '.pnp.cjs'))) {
|
|
557
|
+
return 'yarn-berry';
|
|
1670
558
|
}
|
|
1671
|
-
|
|
1672
|
-
await this.installPackage();
|
|
1673
|
-
// Step 5: Generate and apply code modifications
|
|
1674
|
-
const modifications = await this.generateModifications();
|
|
1675
|
-
await this.applyModifications(modifications);
|
|
1676
|
-
// Step 6: Generate next steps
|
|
1677
|
-
const nextSteps = this.generateManualNextSteps();
|
|
1678
|
-
return {
|
|
1679
|
-
success: true,
|
|
1680
|
-
framework: this.framework,
|
|
1681
|
-
modifications,
|
|
1682
|
-
errors: [],
|
|
1683
|
-
nextSteps,
|
|
1684
|
-
selectedFramework: this.selectedFramework,
|
|
1685
|
-
manualMode: true
|
|
1686
|
-
};
|
|
559
|
+
return 'yarn-classic';
|
|
1687
560
|
}
|
|
1688
|
-
|
|
1689
|
-
return
|
|
1690
|
-
success: false,
|
|
1691
|
-
framework: this.framework || { name: 'unknown', type: 'vanilla' },
|
|
1692
|
-
modifications: [],
|
|
1693
|
-
errors: [error instanceof Error ? error.message : 'Unknown error'],
|
|
1694
|
-
nextSteps: [],
|
|
1695
|
-
selectedFramework: this.selectedFramework,
|
|
1696
|
-
manualMode: true
|
|
1697
|
-
};
|
|
561
|
+
if (fs.existsSync(path.join(root, 'package-lock.json'))) {
|
|
562
|
+
return 'npm';
|
|
1698
563
|
}
|
|
564
|
+
return 'unknown';
|
|
1699
565
|
}
|
|
1700
|
-
|
|
1701
|
-
|
|
1702
|
-
|
|
1703
|
-
async runFullDetection() {
|
|
1704
|
-
if (this.selectedFramework === 'auto') {
|
|
1705
|
-
// Use AI service for auto-detection
|
|
1706
|
-
const aiService = new RemoteAIService({
|
|
1707
|
-
apiEndpoint: 'https://ik3zxh4790.execute-api.us-east-1.amazonaws.com/prod'
|
|
1708
|
-
});
|
|
1709
|
-
// Use AI service directly for detection
|
|
1710
|
-
const projectFiles = await this.scanProjectFiles();
|
|
1711
|
-
const codeSamples = await this.extractCodeSamples(projectFiles);
|
|
1712
|
-
const aiAnalysis = await aiService.analyzeCodePatterns(codeSamples);
|
|
1713
|
-
return aiAnalysis.framework;
|
|
566
|
+
detectWorkspaceShape(packageJsonPaths) {
|
|
567
|
+
if (packageJsonPaths.length === 0) {
|
|
568
|
+
return 'unknown';
|
|
1714
569
|
}
|
|
1715
|
-
|
|
1716
|
-
|
|
1717
|
-
const tempWizard = new AutoInstallationWizard(this.apiKey, this.projectRoot);
|
|
1718
|
-
const detected = await tempWizard.detectFramework();
|
|
1719
|
-
return detected;
|
|
570
|
+
if (packageJsonPaths.length === 1) {
|
|
571
|
+
return 'single-app';
|
|
1720
572
|
}
|
|
1721
|
-
|
|
1722
|
-
|
|
1723
|
-
* Scan project files for analysis
|
|
1724
|
-
*/
|
|
1725
|
-
async scanProjectFiles() {
|
|
1726
|
-
const files = [];
|
|
1727
|
-
const scanDir = (dir, depth = 0) => {
|
|
1728
|
-
if (depth > 3)
|
|
1729
|
-
return; // Limit depth
|
|
1730
|
-
try {
|
|
1731
|
-
const items = fs.readdirSync(dir);
|
|
1732
|
-
for (const item of items) {
|
|
1733
|
-
const fullPath = path.join(dir, item);
|
|
1734
|
-
const stat = fs.statSync(fullPath);
|
|
1735
|
-
if (stat.isDirectory() && !item.startsWith('.') && item !== 'node_modules') {
|
|
1736
|
-
scanDir(fullPath, depth + 1);
|
|
1737
|
-
}
|
|
1738
|
-
else if (stat.isFile() && this.isRelevantFile(item)) {
|
|
1739
|
-
files.push(fullPath);
|
|
1740
|
-
}
|
|
1741
|
-
}
|
|
1742
|
-
}
|
|
1743
|
-
catch (error) {
|
|
1744
|
-
// Skip inaccessible directories
|
|
1745
|
-
}
|
|
1746
|
-
};
|
|
1747
|
-
scanDir(this.projectRoot);
|
|
1748
|
-
return files;
|
|
1749
|
-
}
|
|
1750
|
-
/**
|
|
1751
|
-
* Check if file is relevant for analysis
|
|
1752
|
-
*/
|
|
1753
|
-
isRelevantFile(filename) {
|
|
1754
|
-
const relevantExtensions = [
|
|
1755
|
-
'.js', '.jsx', '.ts', '.tsx', '.vue', '.svelte', '.html',
|
|
1756
|
-
'.json', '.config.js', '.config.ts', '.babelrc', '.eslintrc'
|
|
1757
|
-
];
|
|
1758
|
-
const relevantNames = [
|
|
1759
|
-
'package.json', 'tsconfig.json', 'vite.config', 'webpack.config',
|
|
1760
|
-
'next.config', 'nuxt.config', 'angular.json', 'svelte.config'
|
|
1761
|
-
];
|
|
1762
|
-
return relevantExtensions.some(ext => filename.endsWith(ext)) ||
|
|
1763
|
-
relevantNames.some(name => filename.includes(name));
|
|
1764
|
-
}
|
|
1765
|
-
/**
|
|
1766
|
-
* Extract code samples for AI analysis
|
|
1767
|
-
*/
|
|
1768
|
-
async extractCodeSamples(files) {
|
|
1769
|
-
const samples = [];
|
|
1770
|
-
for (const file of files.slice(0, 20)) { // Limit to 20 files
|
|
1771
|
-
try {
|
|
1772
|
-
const content = fs.readFileSync(file, 'utf8');
|
|
1773
|
-
const relativePath = path.relative(this.projectRoot, file);
|
|
1774
|
-
samples.push(`File: ${relativePath}\n${content.substring(0, 1000)}`);
|
|
1775
|
-
}
|
|
1776
|
-
catch (error) {
|
|
1777
|
-
// Skip unreadable files
|
|
1778
|
-
}
|
|
1779
|
-
}
|
|
1780
|
-
return samples;
|
|
1781
|
-
}
|
|
1782
|
-
/**
|
|
1783
|
-
* Create framework info based on user selection
|
|
1784
|
-
*/
|
|
1785
|
-
createFrameworkInfo(framework) {
|
|
1786
|
-
const frameworkMap = {
|
|
1787
|
-
'react': { name: 'react', type: 'react' },
|
|
1788
|
-
'nextjs': { name: 'nextjs', type: 'nextjs' },
|
|
1789
|
-
'next': { name: 'nextjs', type: 'nextjs' },
|
|
1790
|
-
'vue': { name: 'vue', type: 'vue' },
|
|
1791
|
-
'nuxt': { name: 'nuxt', type: 'nuxt' },
|
|
1792
|
-
'nuxtjs': { name: 'nuxt', type: 'nuxt' },
|
|
1793
|
-
'angular': { name: 'angular', type: 'angular' },
|
|
1794
|
-
'svelte': { name: 'svelte', type: 'svelte' },
|
|
1795
|
-
'sveltekit': { name: 'svelte', type: 'svelte' },
|
|
1796
|
-
'remix': { name: 'remix', type: 'remix' },
|
|
1797
|
-
'astro': { name: 'astro', type: 'astro' },
|
|
1798
|
-
'gatsby': { name: 'gatsby', type: 'gatsby' },
|
|
1799
|
-
'vanilla': { name: 'vanilla', type: 'vanilla' },
|
|
1800
|
-
'node': { name: 'node', type: 'node' },
|
|
1801
|
-
'auto': { name: 'auto-detected', type: 'auto' }
|
|
1802
|
-
};
|
|
1803
|
-
return frameworkMap[framework] || { name: framework, type: 'vanilla' };
|
|
1804
|
-
}
|
|
1805
|
-
/**
|
|
1806
|
-
* Override framework detection to use manual selection
|
|
1807
|
-
*/
|
|
1808
|
-
async detectFramework() {
|
|
1809
|
-
return this.framework || { name: 'unknown', type: 'vanilla' };
|
|
1810
|
-
}
|
|
1811
|
-
/**
|
|
1812
|
-
* Generate next steps with manual mode info
|
|
1813
|
-
*/
|
|
1814
|
-
generateManualNextSteps() {
|
|
1815
|
-
return [
|
|
1816
|
-
'✅ Manual framework installation completed!',
|
|
1817
|
-
`🎯 Selected framework: ${this.framework?.name || 'unknown'}`,
|
|
1818
|
-
`🔧 Integration strategy: ${this.getIntegrationStrategy()}`,
|
|
1819
|
-
'🚀 Your app is now ready to track user behavior',
|
|
1820
|
-
'📊 View sessions in your HumanBehavior dashboard'
|
|
1821
|
-
];
|
|
1822
|
-
}
|
|
1823
|
-
/**
|
|
1824
|
-
* Get integration strategy based on framework
|
|
1825
|
-
*/
|
|
1826
|
-
getIntegrationStrategy() {
|
|
1827
|
-
if (!this.framework?.type)
|
|
1828
|
-
return 'script';
|
|
1829
|
-
switch (this.framework.type) {
|
|
1830
|
-
case 'react':
|
|
1831
|
-
case 'nextjs':
|
|
1832
|
-
return 'provider';
|
|
1833
|
-
case 'vue':
|
|
1834
|
-
return 'plugin';
|
|
1835
|
-
case 'angular':
|
|
1836
|
-
return 'module';
|
|
1837
|
-
default:
|
|
1838
|
-
return 'script';
|
|
573
|
+
if (packageJsonPaths.length <= 4) {
|
|
574
|
+
return 'simple-monorepo';
|
|
1839
575
|
}
|
|
576
|
+
return 'complex-monorepo';
|
|
1840
577
|
}
|
|
1841
578
|
}
|
|
1842
579
|
|
|
1843
580
|
/**
|
|
1844
|
-
*
|
|
581
|
+
* HumanBehavior managed agent installation CLI.
|
|
1845
582
|
*
|
|
1846
|
-
*
|
|
1847
|
-
*
|
|
1848
|
-
*
|
|
1849
|
-
* and generate optimal integration code that's both future-proof and backward-compatible.
|
|
583
|
+
* This entrypoint intentionally does not fall back to the old heuristic
|
|
584
|
+
* installer. If the managed agent is unavailable, it fails closed and points
|
|
585
|
+
* users to manual docs instead of making lower-confidence edits.
|
|
1850
586
|
*/
|
|
1851
587
|
class AIAutoInstallCLI {
|
|
1852
588
|
constructor(options) {
|
|
1853
589
|
this.options = options;
|
|
1854
590
|
}
|
|
1855
591
|
async run() {
|
|
1856
|
-
clack.intro('
|
|
592
|
+
clack.intro('HumanBehavior Agent Installer');
|
|
1857
593
|
try {
|
|
1858
|
-
// Get API key
|
|
1859
594
|
const apiKey = await this.getApiKey();
|
|
1860
595
|
if (!apiKey) {
|
|
1861
596
|
clack.cancel('API key is required');
|
|
1862
597
|
process.exit(1);
|
|
1863
598
|
}
|
|
1864
|
-
// Get project path
|
|
1865
599
|
const projectPath = this.options.projectPath || process.cwd();
|
|
1866
|
-
// Choose framework
|
|
1867
|
-
const framework = await this.chooseFramework();
|
|
1868
|
-
if (!framework) {
|
|
1869
|
-
clack.cancel('Installation cancelled.');
|
|
1870
|
-
process.exit(0);
|
|
1871
|
-
}
|
|
1872
|
-
// Confirm installation
|
|
1873
600
|
if (!this.options.yes) {
|
|
1874
|
-
const confirmed = await this.confirmInstallation(projectPath
|
|
601
|
+
const confirmed = await this.confirmInstallation(projectPath);
|
|
1875
602
|
if (!confirmed) {
|
|
1876
603
|
clack.cancel('Installation cancelled.');
|
|
1877
604
|
process.exit(0);
|
|
1878
605
|
}
|
|
1879
606
|
}
|
|
1880
|
-
// Run installation
|
|
1881
607
|
const spinner = clack.spinner();
|
|
1882
|
-
spinner.start(
|
|
1883
|
-
const
|
|
1884
|
-
|
|
1885
|
-
|
|
1886
|
-
|
|
1887
|
-
|
|
608
|
+
spinner.start(this.options.dryRun ? 'Scanning project...' : 'Starting managed agent installer...');
|
|
609
|
+
const runner = new AgentInstallationRunner({
|
|
610
|
+
apiKey,
|
|
611
|
+
projectPath,
|
|
612
|
+
executionMode: this.getExecutionMode(),
|
|
613
|
+
appUrl: this.options.appUrl,
|
|
614
|
+
ingestionUrl: this.options.ingestionUrl
|
|
615
|
+
});
|
|
616
|
+
const result = await runner.run();
|
|
617
|
+
spinner.stop(this.options.dryRun ? 'Scan complete.' : 'Agent installer finished.');
|
|
618
|
+
this.displayResults(result);
|
|
1888
619
|
}
|
|
1889
620
|
catch (error) {
|
|
1890
621
|
clack.cancel(`Error: ${error instanceof Error ? error.message : 'Unknown error'}`);
|
|
@@ -1908,58 +639,41 @@ class AIAutoInstallCLI {
|
|
|
1908
639
|
});
|
|
1909
640
|
return apiKey;
|
|
1910
641
|
}
|
|
1911
|
-
|
|
642
|
+
getExecutionMode() {
|
|
643
|
+
if (this.options.dryRun)
|
|
644
|
+
return 'dry-run';
|
|
645
|
+
if (this.options.yes)
|
|
646
|
+
return 'yes';
|
|
647
|
+
return 'interactive';
|
|
648
|
+
}
|
|
649
|
+
async confirmInstallation(projectPath) {
|
|
1912
650
|
const confirmed = await clack.confirm({
|
|
1913
|
-
message: `Ready to
|
|
651
|
+
message: `Ready to run the HumanBehavior agent installer in ${projectPath}?`
|
|
1914
652
|
});
|
|
1915
653
|
return confirmed;
|
|
1916
654
|
}
|
|
1917
|
-
|
|
1918
|
-
|
|
1919
|
-
|
|
1920
|
-
|
|
1921
|
-
|
|
1922
|
-
|
|
1923
|
-
|
|
1924
|
-
{ label: 'Angular', value: 'angular' },
|
|
1925
|
-
{ label: 'Svelte', value: 'svelte' },
|
|
1926
|
-
{ label: 'Nuxt.js', value: 'nuxt' },
|
|
1927
|
-
{ label: 'Remix', value: 'remix' },
|
|
1928
|
-
{ label: 'Astro', value: 'astro' },
|
|
1929
|
-
{ label: 'Gatsby', value: 'gatsby' },
|
|
1930
|
-
{ label: 'Vanilla JS/TS', value: 'vanilla' }
|
|
1931
|
-
]
|
|
1932
|
-
});
|
|
1933
|
-
return framework;
|
|
1934
|
-
}
|
|
1935
|
-
displayResults(result, framework) {
|
|
655
|
+
displayResults(result) {
|
|
656
|
+
clack.note([
|
|
657
|
+
`Project path: ${result.repo.projectPath}`,
|
|
658
|
+
`Package manager: ${result.repo.packageManager}`,
|
|
659
|
+
`Workspace shape: ${result.repo.workspaceShape}`,
|
|
660
|
+
`package.json files: ${result.repo.packageJsonPaths.length || 'none'}`
|
|
661
|
+
].join('\n'), 'Project Scan');
|
|
1936
662
|
if (result.success) {
|
|
1937
|
-
|
|
1938
|
-
// Display framework info
|
|
1939
|
-
clack.note(`Framework detected: ${result.framework.name} (${result.framework.type})`, 'Framework Info');
|
|
1940
|
-
// Display modifications
|
|
1941
|
-
if (result.modifications && result.modifications.length > 0) {
|
|
1942
|
-
const modifications = result.modifications.map((mod) => `${mod.action}: ${mod.filePath} - ${mod.description}`);
|
|
1943
|
-
clack.note(modifications.join('\n'), 'Files Modified');
|
|
1944
|
-
}
|
|
1945
|
-
// Display next steps
|
|
1946
|
-
if (result.nextSteps && result.nextSteps.length > 0) {
|
|
663
|
+
if (result.nextSteps.length > 0) {
|
|
1947
664
|
clack.note(result.nextSteps.join('\n'), 'Next Steps');
|
|
1948
665
|
}
|
|
1949
|
-
|
|
1950
|
-
|
|
1951
|
-
clack.note(`Confidence: ${Math.round(result.aiAnalysis.confidence * 100)}%`, 'AI Analysis');
|
|
1952
|
-
if (result.aiAnalysis.recommendations && result.aiAnalysis.recommendations.length > 0) {
|
|
1953
|
-
clack.note(result.aiAnalysis.recommendations.join('\n'), 'AI Recommendations');
|
|
1954
|
-
}
|
|
1955
|
-
}
|
|
666
|
+
clack.outro(this.options.dryRun ? 'Dry run complete.' : 'Installation completed successfully.');
|
|
667
|
+
return;
|
|
1956
668
|
}
|
|
1957
|
-
|
|
1958
|
-
clack.
|
|
1959
|
-
if (result.errors && result.errors.length > 0) {
|
|
1960
|
-
clack.note(result.errors.join('\n'), 'Errors');
|
|
1961
|
-
}
|
|
669
|
+
if (result.errors.length > 0) {
|
|
670
|
+
clack.note(result.errors.join('\n'), 'Errors');
|
|
1962
671
|
}
|
|
672
|
+
if (result.nextSteps.length > 0) {
|
|
673
|
+
clack.note(result.nextSteps.join('\n'), 'Next Steps');
|
|
674
|
+
}
|
|
675
|
+
clack.cancel('Agent installer unavailable.');
|
|
676
|
+
process.exit(1);
|
|
1963
677
|
}
|
|
1964
678
|
}
|
|
1965
679
|
function parseArgs() {
|
|
@@ -1984,16 +698,19 @@ function parseArgs() {
|
|
|
1984
698
|
case '-p':
|
|
1985
699
|
options.projectPath = args[++i];
|
|
1986
700
|
break;
|
|
701
|
+
case '--app-url':
|
|
702
|
+
options.appUrl = args[++i];
|
|
703
|
+
break;
|
|
704
|
+
case '--ingestion-url':
|
|
705
|
+
options.ingestionUrl = args[++i];
|
|
706
|
+
break;
|
|
1987
707
|
case '--framework':
|
|
1988
708
|
case '-f':
|
|
1989
|
-
|
|
1990
|
-
break;
|
|
709
|
+
throw new Error('--framework is no longer supported. The agent installer scans the project automatically.');
|
|
1991
710
|
case 'init':
|
|
1992
|
-
if (
|
|
1993
|
-
|
|
1994
|
-
process.exit(1);
|
|
711
|
+
if (args[i + 1] && !args[i + 1].startsWith('-')) {
|
|
712
|
+
options.apiKey = args[++i];
|
|
1995
713
|
}
|
|
1996
|
-
options.apiKey = args[++i];
|
|
1997
714
|
break;
|
|
1998
715
|
default:
|
|
1999
716
|
if (!options.apiKey && !arg.startsWith('-')) {
|
|
@@ -2006,43 +723,33 @@ function parseArgs() {
|
|
|
2006
723
|
}
|
|
2007
724
|
function showHelp() {
|
|
2008
725
|
console.log(`
|
|
2009
|
-
|
|
726
|
+
HumanBehavior Agent Installer
|
|
2010
727
|
|
|
2011
|
-
Usage: npx humanbehavior-js
|
|
728
|
+
Usage: npx humanbehavior-js init [api-key] [options]
|
|
2012
729
|
|
|
2013
730
|
Options:
|
|
2014
731
|
-h, --help Show this help message
|
|
2015
|
-
-y, --yes Skip
|
|
2016
|
-
--dry-run
|
|
2017
|
-
|
|
732
|
+
-y, --yes Skip confirmation prompts
|
|
733
|
+
--dry-run Scan and report without changing files
|
|
2018
734
|
-p, --project <path> Specify project directory
|
|
2019
|
-
-
|
|
2020
|
-
|
|
2021
|
-
Examples:
|
|
2022
|
-
npx humanbehavior-js ai-auto-install
|
|
2023
|
-
npx humanbehavior-js ai-auto-install hb_your_api_key_here
|
|
2024
|
-
npx humanbehavior-js ai-auto-install --project ./my-app --ai
|
|
2025
|
-
npx humanbehavior-js ai-auto-install --framework react --yes
|
|
2026
|
-
`);
|
|
2027
|
-
}
|
|
2028
|
-
function showInitHelp() {
|
|
2029
|
-
console.log(`
|
|
2030
|
-
Usage: npx humanbehavior-js init <api-key>
|
|
2031
|
-
|
|
2032
|
-
Arguments:
|
|
2033
|
-
api-key Your HumanBehavior API key (required, starts with "hb_")
|
|
735
|
+
--app-url <url> HumanBehavior app URL for local/dev testing
|
|
736
|
+
--ingestion-url <url> HumanBehavior ingestion URL for local/dev testing
|
|
2034
737
|
|
|
2035
738
|
Examples:
|
|
739
|
+
npx humanbehavior-js init
|
|
2036
740
|
npx humanbehavior-js init hb_your_api_key_here
|
|
741
|
+
npx humanbehavior-js init --project ./my-app --yes
|
|
2037
742
|
`);
|
|
2038
743
|
}
|
|
2039
|
-
|
|
2040
|
-
|
|
2041
|
-
const
|
|
2042
|
-
cli
|
|
2043
|
-
|
|
2044
|
-
|
|
2045
|
-
|
|
744
|
+
const isMain = import.meta.url === `file://${process.argv[1]}`;
|
|
745
|
+
if (isMain) {
|
|
746
|
+
const options = parseArgs();
|
|
747
|
+
const cli = new AIAutoInstallCLI(options);
|
|
748
|
+
cli.run().catch((error) => {
|
|
749
|
+
clack.cancel(`Unexpected error: ${error.message}`);
|
|
750
|
+
process.exit(1);
|
|
751
|
+
});
|
|
752
|
+
}
|
|
2046
753
|
|
|
2047
754
|
export { AIAutoInstallCLI };
|
|
2048
755
|
//# sourceMappingURL=ai-auto-install.js.map
|