lwazi-2.0 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/bin/lwazi.js ADDED
@@ -0,0 +1,336 @@
1
+ #!/usr/bin/env node
2
+
3
+ import fs from 'fs';
4
+ import path from 'path';
5
+ import { fileURLToPath } from 'url';
6
+ import { createRequire } from 'module';
7
+
8
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
9
+ const require = createRequire(import.meta.url);
10
+ const cliRoot = path.resolve(__dirname, '..');
11
+ const projectRoot = process.cwd();
12
+
13
+ async function main() {
14
+ const args = process.argv.slice(2);
15
+ const command = args[0] || 'help';
16
+
17
+ switch (command) {
18
+ case 'init':
19
+ await initCommand();
20
+ break;
21
+ case 'sync':
22
+ await syncCommand();
23
+ break;
24
+ case 'inspect':
25
+ await inspectCommand(args);
26
+ break;
27
+ case 'status':
28
+ await statusCommand();
29
+ break;
30
+ case 'start':
31
+ await startCommand();
32
+ break;
33
+ case 'link':
34
+ await linkCommand();
35
+ break;
36
+ case 'ask':
37
+ await askCommand(args.slice(1));
38
+ break;
39
+ case 'help':
40
+ case '--help':
41
+ case '-h':
42
+ showHelp();
43
+ break;
44
+ case 'version':
45
+ case '--version':
46
+ case '-v':
47
+ showVersion();
48
+ break;
49
+ default:
50
+ console.error(`Unknown command: ${command}`);
51
+ showHelp();
52
+ process.exit(1);
53
+ }
54
+ }
55
+
56
+ async function initCommand() {
57
+ const { Init } = await import('../lib/init.js');
58
+ const init = new Init(projectRoot);
59
+ await init.run();
60
+ }
61
+
62
+ async function syncCommand() {
63
+ const configPath = path.join(projectRoot, 'lwazi.config.json');
64
+ if (!fs.existsSync(configPath)) {
65
+ console.error('No lwazi.config.json found. Run "lwazi-2.0 init" first.');
66
+ process.exit(1);
67
+ }
68
+
69
+ const { Sync } = await import('../lib/sync.js');
70
+ const sync = new Sync(projectRoot);
71
+ await sync.sync();
72
+ }
73
+
74
+ async function inspectCommand(args) {
75
+ const configPath = path.join(projectRoot, 'lwazi.config.json');
76
+ if (!fs.existsSync(configPath)) {
77
+ console.error('No lwazi.config.json found. Run "lwazi-2.0 init" first.');
78
+ process.exit(1);
79
+ }
80
+
81
+ const config = JSON.parse(fs.readFileSync(configPath, 'utf-8'));
82
+ const serverUrl = config.lwazi_server || 'http://localhost:11500';
83
+
84
+ const addUrlIndex = args.indexOf('--add-url');
85
+ if (addUrlIndex !== -1 && args[addUrlIndex + 1]) {
86
+ console.log('Manual URL addition not yet implemented via CLI.');
87
+ console.log('Edit the lwazi.config.json or re-index with updated routes.');
88
+ return;
89
+ }
90
+
91
+ try {
92
+ const axios = (await import('axios')).default;
93
+ const response = await axios.get(`${serverUrl}/health`);
94
+ const health = response.data;
95
+
96
+ console.log('\n' + '='.repeat(60));
97
+ console.log(' Lwazi Project Inspection');
98
+ console.log('='.repeat(60));
99
+ console.log(`\n Project: ${config.project_name}`);
100
+ console.log(` Project ID: ${config.project_id}`);
101
+ console.log(` Base URL: ${config.base_url}`);
102
+ console.log(` Server: ${config.lwazi_server}`);
103
+ console.log(` Indexed: ${config.last_synced || 'Not yet synced'}`);
104
+ console.log(` LLM Model: ${health.llm?.model || 'unknown'}`);
105
+ console.log(` Embedding: ${health.llm?.embedding_model || 'unknown'}`);
106
+ console.log(`\n Skip paths: ${config.skip_paths?.join(', ') || 'none'}`);
107
+ console.log(` Max crawl: ${config.max_crawl_pages} pages`);
108
+ console.log('='.repeat(60) + '\n');
109
+ } catch (err) {
110
+ console.log('\n Project config (server offline):');
111
+ console.log(` Project: ${config.project_name}`);
112
+ console.log(` Project ID: ${config.project_id}`);
113
+ console.log(` Base URL: ${config.base_url}`);
114
+ console.log(` Server: ${config.lwazi_server}\n`);
115
+ }
116
+ }
117
+
118
+ async function statusCommand() {
119
+ const configPath = path.join(projectRoot, 'lwazi.config.json');
120
+ const config = fs.existsSync(configPath) ? JSON.parse(fs.readFileSync(configPath, 'utf-8')) : null;
121
+ const serverUrl = config?.lwazi_server || 'http://localhost:11500';
122
+
123
+ console.log('\n' + '='.repeat(60));
124
+ console.log(' Lwazi Status');
125
+ console.log('='.repeat(60));
126
+
127
+ try {
128
+ const axios = (await import('axios')).default;
129
+ const health = await axios.get(`${serverUrl}/health`, { timeout: 5000 });
130
+ const h = health.data;
131
+
132
+ console.log(`\n Server: ${h.status === 'ok' ? '✓ Running' : '✗ Degraded'}`);
133
+ console.log(` LLM: ${h.llm?.model || 'Not available'}`);
134
+ console.log(` Embedding: ${h.llm?.embedding_model || 'Not available'}`);
135
+ console.log(` Response time: ${h.response_time_ms}ms`);
136
+
137
+ if (config) {
138
+ console.log(`\n Project: ${config.project_name}`);
139
+ console.log(` Last indexed: ${config.last_synced || 'Never'}`);
140
+ console.log(` Routes URL: ${config.base_url}`);
141
+ } else {
142
+ console.log(`\n Project: Not initialized (no lwazi.config.json)`);
143
+ }
144
+ } catch (err) {
145
+ console.log(`\n Server: ✗ Offline (${err.code || err.message})`);
146
+ console.log(` Expected at: ${serverUrl}`);
147
+
148
+ if (config) {
149
+ console.log(`\n Project config found: ${config.project_name}`);
150
+ console.log(` Run "lwazi-2.0 start" to start the server.`);
151
+ }
152
+ }
153
+
154
+ console.log('='.repeat(60) + '\n');
155
+ }
156
+
157
+ async function startCommand() {
158
+ console.log('\n Starting Lwazi Server...\n');
159
+
160
+ try {
161
+ const axios = (await import('axios')).default;
162
+ const ollamaCheck = await axios.get('http://localhost:11434/api/tags', { timeout: 5000 });
163
+ const models = ollamaCheck.data?.models || [];
164
+ const modelNames = models.map(m => m.name);
165
+
166
+ console.log(` Ollama: ✓ Running`);
167
+ console.log(` Models: ${modelNames.join(', ') || 'none'}`);
168
+
169
+ if (!modelNames.some(m => m.includes('llama3.2'))) {
170
+ console.log(`\n ⚠ Recommended model "llama3.2:1b" not found.`);
171
+ console.log(' Pull it with: ollama pull llama3.2:1b');
172
+ }
173
+ } catch (err) {
174
+ console.log(' Ollama: ✗ Not running');
175
+ console.log(' Start Ollama: ollama serve');
176
+ process.exit(1);
177
+ }
178
+
179
+ const serverDir = path.resolve(cliRoot, '..', 'server');
180
+ if (!fs.existsSync(path.join(serverDir, 'main.py'))) {
181
+ console.error(' Server files not found at', serverDir);
182
+ process.exit(1);
183
+ }
184
+
185
+ const venvPython = path.join(serverDir, 'venv', 'bin', 'python3');
186
+ const venvPythonWin = path.join(serverDir, 'venv', 'Scripts', 'python.exe');
187
+ let pythonCmd = 'python3';
188
+ if (fs.existsSync(venvPython)) {
189
+ pythonCmd = venvPython;
190
+ console.log(` Using venv: server/venv/`);
191
+ } else if (fs.existsSync(venvPythonWin)) {
192
+ pythonCmd = venvPythonWin;
193
+ console.log(` Using venv: server/venv/`);
194
+ } else {
195
+ console.log(' ⚠ No venv found. Create one with:');
196
+ console.log(' cd server && python3 -m venv venv && source venv/bin/activate && pip install -r requirements.txt\n');
197
+ }
198
+
199
+ const port = process.env.LWAZI_PORT || 11500;
200
+ console.log(` Starting server on port ${port}...\n`);
201
+
202
+ const { spawn } = await import('child_process');
203
+ const server = spawn(pythonCmd, ['-m', 'uvicorn', 'server.main:app', '--host', '0.0.0.0', '--port', String(port), '--reload'], {
204
+ cwd: serverDir,
205
+ stdio: 'inherit',
206
+ env: { ...process.env, LWAZI_PORT: String(port) }
207
+ });
208
+
209
+ process.on('SIGINT', () => {
210
+ server.kill('SIGINT');
211
+ process.exit(0);
212
+ });
213
+
214
+ process.on('SIGTERM', () => {
215
+ server.kill('SIGTERM');
216
+ process.exit(0);
217
+ });
218
+ }
219
+
220
+ async function linkCommand() {
221
+ const { execSync } = await import('child_process');
222
+ try {
223
+ console.log('\n Linking Lwazi CLI globally...\n');
224
+ execSync('npm link', { cwd: cliRoot, stdio: 'inherit' });
225
+ console.log('\n Done! You can now run "lwazi-2.0" from any project.\n');
226
+ } catch (err) {
227
+ console.error(' Link failed. Try: cd cli && npm link');
228
+ process.exit(1);
229
+ }
230
+ }
231
+
232
+ async function askCommand(args) {
233
+ const question = args.join(' ');
234
+ if (!question) {
235
+ console.error('Usage: lwazi-2.0 ask "your question here"');
236
+ process.exit(1);
237
+ }
238
+
239
+ const configPath = path.join(projectRoot, 'lwazi.config.json');
240
+ if (!fs.existsSync(configPath)) {
241
+ console.error('No lwazi.config.json found. Run "lwazi-2.0 init" first.');
242
+ process.exit(1);
243
+ }
244
+
245
+ const config = JSON.parse(fs.readFileSync(configPath, 'utf-8'));
246
+ const serverUrl = config.lwazi_server || 'http://localhost:11500';
247
+
248
+ console.log(`\n Asking Lwazi: "${question}"\n`);
249
+
250
+ try {
251
+ const axios = (await import('axios')).default;
252
+ const response = await axios.post(`${serverUrl}/chat`, {
253
+ project_id: config.project_id,
254
+ message: question,
255
+ session_id: null
256
+ }, { timeout: 120000 });
257
+
258
+ const data = response.data;
259
+
260
+ if (data.type === 'answer') {
261
+ console.log(` ${data.message}\n`);
262
+ if (data.sources && data.sources.length > 0) {
263
+ console.log(' Sources:');
264
+ for (const src of data.sources) {
265
+ console.log(` • ${src.title}: ${src.url}`);
266
+ }
267
+ }
268
+ if (data.answer_confidence === 'low') {
269
+ console.log('\n ⚠ I\'m not fully certain — please verify.');
270
+ }
271
+ } else if (data.type === 'auth_required') {
272
+ console.log(` 🔒 ${data.message}`);
273
+ } else if (data.type === 'not_found') {
274
+ console.log(` ${data.message}`);
275
+ } else if (data.type === 'error') {
276
+ console.log(` ✗ ${data.message}`);
277
+ }
278
+
279
+ console.log('');
280
+ } catch (err) {
281
+ if (err.code === 'ECONNREFUSED') {
282
+ console.error(' Error: Lwazi server is not running.');
283
+ console.error(` Start it with: lwazi-2.0 start`);
284
+ } else {
285
+ console.error(' Error:', err.message);
286
+ }
287
+ }
288
+ }
289
+
290
+ function showHelp() {
291
+ console.log(`
292
+ Lwazi 2.0 — Intelligent project-aware AI chatbot
293
+
294
+ USAGE:
295
+ lwazi-2.0 <command>
296
+
297
+ COMMANDS:
298
+ init Install Lwazi into the current project
299
+ sync Re-index the project (after changes)
300
+ inspect Show project configuration and indexing details
301
+ status Check server health and project status
302
+ start Start the Lwazi server
303
+ ask Ask a question from the terminal
304
+ help Show this help message
305
+ version Show version information
306
+
307
+ EXAMPLES:
308
+ lwazi-2.0 init
309
+ lwazi-2.0 ask "how do I register?"
310
+ lwazi-2.0 inspect --add-url "/pricing" "Pricing Page"
311
+ `);
312
+ }
313
+
314
+ function showVersion() {
315
+ const pkgPath = path.join(cliRoot, 'package.json');
316
+ if (fs.existsSync(pkgPath)) {
317
+ const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf-8'));
318
+ console.log(`lwazi-2.0 v${pkg.version}`);
319
+ } else {
320
+ console.log('lwazi-2.0 v0.1.0');
321
+ }
322
+ }
323
+
324
+ main().catch(err => {
325
+ if (err.code === 'ERR_MODULE_NOT_FOUND' || err.code === 'MODULE_NOT_FOUND') {
326
+ console.error('\n Lwazi CLI is not installed.\n');
327
+ console.error(' Run these commands to set it up:');
328
+ console.error(` cd ${path.resolve(__dirname, '..')}`);
329
+ console.error(' npm install');
330
+ console.error(' npm link\n');
331
+ console.error(' Then run your command again.\n');
332
+ } else {
333
+ console.error('Fatal error:', err.message);
334
+ }
335
+ process.exit(1);
336
+ });
package/lib/init.js ADDED
@@ -0,0 +1,216 @@
1
+ import fs from 'fs';
2
+ import path from 'path';
3
+ import { createRequire } from 'module';
4
+ import inquirer from 'inquirer';
5
+ import chalk from 'chalk';
6
+
7
+ const require = createRequire(import.meta.url);
8
+ const { nanoid } = await import('nanoid');
9
+
10
+ import { ProjectProbe } from './project_probe.js';
11
+ import { InjectWidget } from './inject_widget.js';
12
+
13
+ export class Init {
14
+ constructor(projectRoot) {
15
+ this.projectRoot = path.resolve(projectRoot);
16
+ this.configPath = path.join(this.projectRoot, 'lwazi.config.json');
17
+ }
18
+
19
+ async run() {
20
+ console.log(chalk.bold('\n Lwazi 2.0 — Install into project\n'));
21
+
22
+ const probe = new ProjectProbe(this.projectRoot);
23
+ const probeResult = probe.probe();
24
+ probe.display(probeResult);
25
+
26
+ if (fs.existsSync(this.configPath)) {
27
+ const existing = JSON.parse(fs.readFileSync(this.configPath, 'utf-8'));
28
+ console.log(chalk.yellow(` Existing config found for project: ${existing.project_name}`));
29
+ const { overwrite } = await inquirer.prompt([{
30
+ type: 'confirm',
31
+ name: 'overwrite',
32
+ message: 'Re-initialize this project?',
33
+ default: false
34
+ }]);
35
+ if (!overwrite) {
36
+ console.log(' Init cancelled.');
37
+ return;
38
+ }
39
+ }
40
+
41
+ const defaultName = path.basename(this.projectRoot);
42
+
43
+ const answers = await inquirer.prompt([
44
+ {
45
+ type: 'input',
46
+ name: 'projectName',
47
+ message: 'Project name?',
48
+ default: defaultName
49
+ },
50
+ {
51
+ type: 'input',
52
+ name: 'baseUrl',
53
+ message: 'What URL does this site run on locally?',
54
+ default: 'http://localhost:3000'
55
+ },
56
+ {
57
+ type: 'input',
58
+ name: 'lwaziServer',
59
+ message: 'Lwazi server URL?',
60
+ default: 'http://localhost:11500'
61
+ },
62
+ {
63
+ type: 'input',
64
+ name: 'skipDirs',
65
+ message: 'Any directories to skip during indexing? (comma-separated, or Enter to skip)',
66
+ default: ''
67
+ },
68
+ {
69
+ type: 'confirm',
70
+ name: 'setUserPassword',
71
+ message: 'Set a user-level password? (for protected content)',
72
+ default: false
73
+ },
74
+ {
75
+ type: 'password',
76
+ name: 'userPassword',
77
+ message: 'User password:',
78
+ when: (a) => a.setUserPassword,
79
+ mask: '*'
80
+ },
81
+ {
82
+ type: 'confirm',
83
+ name: 'setAdminPassword',
84
+ message: 'Set an admin password?',
85
+ default: false
86
+ },
87
+ {
88
+ type: 'password',
89
+ name: 'adminPassword',
90
+ message: 'Admin password:',
91
+ when: (a) => a.setAdminPassword,
92
+ mask: '*'
93
+ }
94
+ ]);
95
+
96
+ const projectId = nanoid(12);
97
+ const skipPaths = answers.skipDirs ? answers.skipDirs.split(',').map(s => s.trim()).filter(Boolean) : [];
98
+
99
+ const config = {
100
+ project_id: projectId,
101
+ project_name: answers.projectName,
102
+ project_root: this.projectRoot,
103
+ base_url: answers.baseUrl.replace(/\/+$/, ''),
104
+ lwazi_server: answers.lwaziServer.replace(/\/+$/, ''),
105
+ skip_paths: skipPaths,
106
+ max_crawl_pages: 500,
107
+ created_at: new Date().toISOString(),
108
+ last_synced: null,
109
+ };
110
+
111
+ if (answers.setUserPassword && answers.userPassword) {
112
+ const bcryptjs = await import('bcryptjs');
113
+ config.user_password_hash = bcryptjs.hashSync(answers.userPassword, 10);
114
+ }
115
+
116
+ if (answers.setAdminPassword && answers.adminPassword) {
117
+ const bcryptjs = await import('bcryptjs');
118
+ config.admin_password_hash = bcryptjs.hashSync(answers.adminPassword, 10);
119
+ }
120
+
121
+ fs.writeFileSync(this.configPath, JSON.stringify(config, null, 2));
122
+ console.log(chalk.green(`\n ✓ Config written to lwazi.config.json`));
123
+
124
+ const serverUrl = answers.lwaziServer.replace(/\/+$/, '');
125
+
126
+ console.log(chalk.bold(`\n Indexing project...\n`));
127
+
128
+ const axios = (await import('axios')).default;
129
+
130
+ try {
131
+ const response = await axios.post(`${serverUrl}/index`, {
132
+ project_id: projectId,
133
+ project_root: this.projectRoot,
134
+ base_url: config.base_url,
135
+ project_name: answers.projectName,
136
+ max_crawl_pages: 500,
137
+ skip_paths: skipPaths
138
+ }, {
139
+ responseType: 'stream',
140
+ timeout: 600000
141
+ });
142
+
143
+ const stream = response.data;
144
+ let resultData = '';
145
+
146
+ for await (const chunk of stream) {
147
+ const text = chunk.toString();
148
+ const lines = text.split('\n').filter(l => l.trim());
149
+
150
+ for (const line of lines) {
151
+ if (line.startsWith('data:')) {
152
+ const data = line.slice(5).trim();
153
+ if (data === '[DONE]') break;
154
+ try {
155
+ const parsed = JSON.parse(data);
156
+ if (parsed.event === 'progress') {
157
+ process.stdout.write(`\r ${chalk.cyan(parsed.data)}`);
158
+ } else if (parsed.event === 'complete') {
159
+ resultData = parsed.data;
160
+ } else if (parsed.event === 'error') {
161
+ console.log(`\n ${chalk.red('Error:')} ${parsed.data}`);
162
+ }
163
+ } catch (e) {
164
+ if (data && !data.startsWith('[')) {
165
+ process.stdout.write(`\r ${chalk.cyan(data)}`);
166
+ }
167
+ }
168
+ }
169
+ }
170
+ }
171
+
172
+ console.log('\n');
173
+
174
+ if (resultData) {
175
+ try {
176
+ const result = JSON.parse(resultData);
177
+ console.log(chalk.green(' ✓ Indexing complete!'));
178
+ console.log(` Files indexed: ${result.files_indexed}`);
179
+ console.log(` Routes found: ${result.routes_found}`);
180
+ console.log(` Pages crawled: ${result.pages_indexed}`);
181
+ console.log(` Framework: ${result.framework_detected}`);
182
+ console.log(` Duration: ${result.duration_seconds}s`);
183
+ } catch (e) {
184
+ console.log(' Indexing completed.');
185
+ }
186
+ }
187
+ } catch (err) {
188
+ if (err.code === 'ECONNREFUSED') {
189
+ console.log(chalk.yellow(' ⚠ Could not reach Lwazi server. Config saved, but indexing was skipped.'));
190
+ console.log(chalk.yellow(' Start the server with: lwazi-2.0 start'));
191
+ console.log(chalk.yellow(' Then run: lwazi-2.0 sync'));
192
+ } else {
193
+ console.log(chalk.yellow(` ⚠ Indexing encountered an issue: ${err.message}`));
194
+ console.log(chalk.yellow(' Config was saved. Run "lwazi-2.0 sync" to try again.'));
195
+ }
196
+ }
197
+
198
+ console.log(chalk.bold('\n Injecting chat widget...\n'));
199
+ const injector = new InjectWidget(this.projectRoot, serverUrl, projectId);
200
+ const injectResult = await injector.inject();
201
+ injector.report();
202
+
203
+ if (injectResult.injected.length > 0) {
204
+ console.log(chalk.green(' ✓ Lwazi is ready!'));
205
+ } else {
206
+ console.log(chalk.yellow(' ⚠ Manual widget injection needed.'));
207
+ }
208
+
209
+ console.log(chalk.bold('\n Next steps:'));
210
+ console.log(' - Browse your site and look for the chat bubble');
211
+ console.log(' - Run: lwazi-2.0 status (check server health)');
212
+ console.log(' - Run: lwazi-2.0 inspect (verify routes)');
213
+ console.log(' - Run: lwazi-2.0 ask "question" (test from terminal)');
214
+ console.log(' - Run: lwazi-2.0 sync (re-index after changes)\n');
215
+ }
216
+ }
@@ -0,0 +1,179 @@
1
+ import fs from 'fs';
2
+ import path from 'path';
3
+ import { glob } from 'glob';
4
+
5
+ export class InjectWidget {
6
+ constructor(projectRoot, lwaziServer, projectId) {
7
+ this.projectRoot = path.resolve(projectRoot);
8
+ this.lwaziServer = lwaziServer.replace(/\/+$/, '');
9
+ this.projectId = projectId;
10
+ this.injected = [];
11
+ this.failed = [];
12
+ }
13
+
14
+ async inject() {
15
+ const injectionPoints = await this.findInjectionPoints();
16
+
17
+ if (injectionPoints.length === 0) {
18
+ this.createLoaderFile();
19
+ return { injected: [], failed: [], loaderCreated: true };
20
+ }
21
+
22
+ const scriptTag = `<script src="${this.lwaziServer}/widget/lwazi-widget.js" data-project-id="${this.projectId}" data-server="${this.lwaziServer}" defer></script>`;
23
+
24
+ for (const filePath of injectionPoints) {
25
+ try {
26
+ let content = fs.readFileSync(filePath, 'utf-8');
27
+ const bodyCloseIndex = content.lastIndexOf('</body>');
28
+ if (bodyCloseIndex !== -1) {
29
+ content = content.slice(0, bodyCloseIndex) + ' ' + scriptTag + '\n' + content.slice(bodyCloseIndex);
30
+ } else {
31
+ content += '\n' + scriptTag + '\n';
32
+ }
33
+ fs.writeFileSync(filePath, content, 'utf-8');
34
+ this.injected.push(filePath);
35
+ } catch (err) {
36
+ this.failed.push({ file: filePath, error: err.message });
37
+ }
38
+ }
39
+
40
+ return { injected: this.injected, failed: this.failed, loaderCreated: false };
41
+ }
42
+
43
+ async findInjectionPoints() {
44
+ const points = new Set();
45
+
46
+ const knownLayouts = [
47
+ 'app/layout.tsx', 'app/layout.jsx', 'app/layout.js',
48
+ 'pages/_app.js', 'pages/_app.tsx', 'pages/_app.jsx', 'pages/_app.ts',
49
+ 'resources/views/layouts/app.blade.php',
50
+ 'resources/views/layouts/master.blade.php',
51
+ 'templates/base.html', 'templates/layout.html',
52
+ 'views/layouts/application.html.erb',
53
+ 'src/App.vue', 'src/layouts/default.vue',
54
+ 'src/app.component.html',
55
+ 'layouts/default.vue', 'layouts/default.html',
56
+ 'base.html', 'layout.html', 'master.html',
57
+ ];
58
+
59
+ for (const layoutPath of knownLayouts) {
60
+ const fullPath = path.join(this.projectRoot, layoutPath);
61
+ if (fs.existsSync(fullPath) && fs.statSync(fullPath).isFile()) {
62
+ points.add(fullPath);
63
+ }
64
+ }
65
+
66
+ const layoutPatterns = [
67
+ '**/*layout*', '**/*base*', '**/*master*',
68
+ '**/*_app.*', '**/*app.component.*',
69
+ ];
70
+ for (const pattern of layoutPatterns) {
71
+ try {
72
+ const files = await glob(pattern, { cwd: this.projectRoot, nodir: true, ignore: '**/node_modules/**' });
73
+ for (const f of files) {
74
+ const fullPath = path.join(this.projectRoot, f);
75
+ if (fs.existsSync(fullPath)) {
76
+ const content = fs.readFileSync(fullPath, 'utf-8');
77
+ if (content.includes('</body>') || content.includes('</html>') || content.includes('<!DOCTYPE')) {
78
+ points.add(fullPath);
79
+ }
80
+ }
81
+ }
82
+ } catch (e) {}
83
+ }
84
+
85
+ const publicDirs = ['public', 'www', 'web', 'htdocs', 'static'];
86
+ for (const dir of publicDirs) {
87
+ const dirPath = path.join(this.projectRoot, dir);
88
+ if (fs.existsSync(dirPath)) {
89
+ try {
90
+ const files = await glob('**/*.html', { cwd: dirPath, nodir: true });
91
+ for (const f of files) {
92
+ const fullPath = path.join(dirPath, f);
93
+ const content = fs.readFileSync(fullPath, 'utf-8');
94
+ if (content.includes('<!DOCTYPE') || content.includes('<html')) {
95
+ points.add(fullPath);
96
+ }
97
+ }
98
+ } catch (e) {}
99
+ }
100
+ }
101
+
102
+ if (points.size === 0) {
103
+ try {
104
+ const rootHtmlFiles = await glob('*.html', { cwd: this.projectRoot, nodir: true });
105
+ for (const f of rootHtmlFiles) {
106
+ const fullPath = path.join(this.projectRoot, f);
107
+ const content = fs.readFileSync(fullPath, 'utf-8');
108
+ if (content.includes('<!DOCTYPE') || content.includes('<html')) {
109
+ points.add(fullPath);
110
+ }
111
+ }
112
+ } catch (e) {}
113
+
114
+ try {
115
+ const phpFiles = await glob('**/*.php', { cwd: this.projectRoot, nodir: true, ignore: '**/node_modules/**' });
116
+ for (const f of phpFiles.slice(0, 30)) {
117
+ const fullPath = path.join(this.projectRoot, f);
118
+ const content = fs.readFileSync(fullPath, 'utf-8');
119
+ if (content.includes('<!DOCTYPE') || content.includes('<html')) {
120
+ points.add(fullPath);
121
+ }
122
+ }
123
+ } catch (e) {}
124
+ }
125
+
126
+ if (points.size === 0) {
127
+ try {
128
+ const allHtml = await glob('**/*.html', { cwd: this.projectRoot, nodir: true, ignore: '**/node_modules/**' });
129
+ for (const f of allHtml) {
130
+ const fullPath = path.join(this.projectRoot, f);
131
+ points.add(fullPath);
132
+ }
133
+ } catch (e) {}
134
+ }
135
+
136
+ return [...points];
137
+ }
138
+
139
+ createLoaderFile() {
140
+ const loaderPath = path.join(this.projectRoot, '_lwazi_loader.js');
141
+ const content = `// Lwazi 2.0 Widget Loader
142
+ // Add this script to your HTML layout file to enable the Lwazi chat widget.
143
+ //
144
+ // Insert before </body> in your layout/template:
145
+ //
146
+ // <script src="${this.lwaziServer}/widget/lwazi-widget.js" data-project-id="${this.projectId}" data-server="${this.lwaziServer}" defer></script>
147
+ //
148
+ // Lwazi couldn't find an HTML/template file to inject automatically.
149
+ // See https://opencode.ai/lwazi for manual integration help.
150
+ console.log('Lwazi 2.0: Widget not auto-injected. See instructions in this file.');
151
+ `;
152
+ fs.writeFileSync(loaderPath, content, 'utf-8');
153
+ }
154
+
155
+ report() {
156
+ console.log('\n' + '='.repeat(60));
157
+ console.log(' Lwazi Widget Injection Report');
158
+ console.log('='.repeat(60));
159
+
160
+ for (const file of this.injected) {
161
+ const rel = path.relative(this.projectRoot, file);
162
+ console.log(` ✓ Widget injected into: ${rel}`);
163
+ }
164
+
165
+ for (const { file, error } of this.failed) {
166
+ const rel = path.relative(this.projectRoot, file);
167
+ console.log(` ✗ Failed to inject into: ${rel} (${error})`);
168
+ }
169
+
170
+ if (this.injected.length === 0 && !this.failed.length) {
171
+ console.log(`\n ⚠ Could not find HTML/template files to inject into.`);
172
+ console.log(` Created _lwazi_loader.js with manual instructions.`);
173
+ console.log(` Add this to your layout manually:`);
174
+ console.log(` <script src="${this.lwaziServer}/widget/lwazi-widget.js" data-project-id="${this.projectId}" data-server="${this.lwaziServer}" defer></script>`);
175
+ }
176
+
177
+ console.log('='.repeat(60) + '\n');
178
+ }
179
+ }
@@ -0,0 +1,234 @@
1
+ import fs from 'fs';
2
+ import path from 'path';
3
+ import { fileURLToPath } from 'url';
4
+
5
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
6
+
7
+ export class ProjectProbe {
8
+ constructor(projectRoot) {
9
+ this.projectRoot = path.resolve(projectRoot);
10
+ }
11
+
12
+ probe() {
13
+ const result = {
14
+ has_package_json: false,
15
+ has_composer_json: false,
16
+ has_requirements_txt: false,
17
+ has_gemfile: false,
18
+ has_manage_py: false,
19
+ has_artisan: false,
20
+ has_makefile: false,
21
+ has_dockerfile: false,
22
+ html_files_count: 0,
23
+ php_files_count: 0,
24
+ js_files_count: 0,
25
+ py_files_count: 0,
26
+ rb_files_count: 0,
27
+ go_files_count: 0,
28
+ rs_files_count: 0,
29
+ java_files_count: 0,
30
+ cs_files_count: 0,
31
+ other_extensions: new Set(),
32
+ root_files: [],
33
+ subdirectories: [],
34
+ likely_entry_files: [],
35
+ framework_guess: 'Unknown',
36
+ confidence: 'unknown',
37
+ template_engines: [],
38
+ public_dirs: [],
39
+ };
40
+
41
+ try {
42
+ const entries = fs.readdirSync(this.projectRoot, { withFileTypes: true });
43
+ for (const entry of entries) {
44
+ if (entry.isFile()) {
45
+ result.root_files.push(entry.name);
46
+ const ext = path.extname(entry.name).toLowerCase();
47
+ const basename = entry.name.toLowerCase();
48
+
49
+ if (basename === 'package.json') result.has_package_json = true;
50
+ if (basename === 'composer.json') result.has_composer_json = true;
51
+ if (basename === 'requirements.txt') result.has_requirements_txt = true;
52
+ if (basename === 'gemfile' || basename === 'gemfile.lock') result.has_gemfile = true;
53
+ if (basename === 'manage.py') result.has_manage_py = true;
54
+ if (basename === 'artisan') result.has_artisan = true;
55
+ if (basename === 'makefile' || basename === 'makefile') result.has_makefile = true;
56
+ if (basename === 'dockerfile') result.has_dockerfile = true;
57
+
58
+ if (ext === '.html' || ext === '.htm') result.html_files_count++;
59
+ else if (ext === '.php') result.php_files_count++;
60
+ else if (ext === '.js' || ext === '.jsx') result.js_files_count++;
61
+ else if (ext === '.py') result.py_files_count++;
62
+ else if (ext === '.rb') result.rb_files_count++;
63
+ else if (ext === '.go') result.go_files_count++;
64
+ else if (ext === '.rs') result.rs_files_count++;
65
+ else if (ext === '.java') result.java_files_count++;
66
+ else if (ext === '.cs') result.cs_files_count++;
67
+ else if (ext) result.other_extensions.add(ext);
68
+
69
+ if (/^index\./.test(basename) || /^main\./.test(basename) || /^app\./.test(basename) || /^server\./.test(basename)) {
70
+ result.likely_entry_files.push(entry.name);
71
+ }
72
+ } else if (entry.isDirectory()) {
73
+ const dirname = entry.name.toLowerCase();
74
+ result.subdirectories.push(entry.name);
75
+ if (['public', 'www', 'web', 'htdocs', 'static', 'dist'].includes(dirname)) {
76
+ result.public_dirs.push(entry.name);
77
+ }
78
+ if (['views', 'templates', 'pages', 'components', 'layouts'].includes(dirname)) {
79
+ if (dirname === 'views' || dirname === 'templates') result.template_engines.push(dirname);
80
+ }
81
+ }
82
+ }
83
+ } catch (err) {
84
+ return result;
85
+ }
86
+
87
+ result.other_extensions = [...result.other_extensions].sort();
88
+
89
+ result.framework_guess = this.guessFramework(result);
90
+ result.confidence = result.framework_guess === 'Unknown' ? 'unknown' : 'medium';
91
+
92
+ return result;
93
+ }
94
+
95
+ guessFramework(probe) {
96
+ if (probe.has_package_json) {
97
+ try {
98
+ const pkgPath = path.join(this.projectRoot, 'package.json');
99
+ const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf-8'));
100
+ const deps = { ...pkg.dependencies, ...pkg.devDependencies };
101
+ const depNames = Object.keys(deps).join(' ').toLowerCase();
102
+
103
+ if (depNames.includes('next')) return 'Next.js';
104
+ if (depNames.includes('nuxt')) return 'Nuxt.js';
105
+ if (depNames.includes('sveltekit') || (depNames.includes('svelte') && depNames.includes('@sveltejs/kit'))) return 'SvelteKit';
106
+ if (depNames.includes('@angular') || depNames.includes('angular')) return 'Angular';
107
+ if (depNames.includes('vue') && depNames.includes('vue-router')) return 'Vue.js';
108
+ if (depNames.includes('react') && depNames.includes('react-router')) return 'React';
109
+ if (depNames.includes('express')) return 'Express';
110
+ if (depNames.includes('gatsby')) return 'Gatsby';
111
+ if (depNames.includes('astro')) return 'Astro';
112
+ if (depNames.includes('remix')) return 'Remix';
113
+ if (depNames.includes('docusaurus')) return 'Docusaurus';
114
+ if (depNames.includes('eleventy') || depNames.includes('11ty')) return 'Eleventy';
115
+ if (depNames.includes('hugo') || depNames.includes('gohugo')) return 'Hugo';
116
+ if (depNames.includes('jekyll')) return 'Jekyll';
117
+
118
+ if (depNames.includes('webpack') || depNames.includes('vite') || depNames.includes('parcel')) {
119
+ return 'JavaScript (build tool)';
120
+ }
121
+ } catch (e) {}
122
+ }
123
+
124
+ if (probe.has_composer_json) {
125
+ try {
126
+ const composerPath = path.join(this.projectRoot, 'composer.json');
127
+ const composer = JSON.parse(fs.readFileSync(composerPath, 'utf-8'));
128
+ const reqs = { ...composer.require, ...composer['require-dev'] };
129
+ const reqNames = Object.keys(reqs).join(' ').toLowerCase();
130
+ if (reqNames.includes('laravel')) return 'Laravel';
131
+ if (reqNames.includes('symfony')) return 'Symfony';
132
+ if (reqNames.includes('cakephp')) return 'CakePHP';
133
+ if (reqNames.includes('codeigniter')) return 'CodeIgniter';
134
+ if (reqNames.includes('yii')) return 'Yii';
135
+ if (reqNames.includes('wordpress') || reqNames.includes('wp-')) return 'WordPress';
136
+ if (reqNames.includes('drupal')) return 'Drupal';
137
+ if (reqNames.includes('joomla')) return 'Joomla';
138
+ if (reqNames.includes('magento')) return 'Magento';
139
+ if (reqNames.includes('shopify')) return 'Shopify';
140
+ return 'PHP';
141
+ } catch (e) {}
142
+ }
143
+
144
+ if (probe.has_requirements_txt || probe.has_manage_py) {
145
+ if (probe.has_manage_py) return 'Django';
146
+ try {
147
+ const reqPath = path.join(this.projectRoot, 'requirements.txt');
148
+ const reqs = fs.readFileSync(reqPath, 'utf-8').toLowerCase();
149
+ if (reqs.includes('flask')) return 'Flask';
150
+ if (reqs.includes('fastapi')) return 'FastAPI';
151
+ if (reqs.includes('django')) return 'Django';
152
+ if (reqs.includes('pyramid')) return 'Pyramid';
153
+ if (reqs.includes('tornado')) return 'Tornado';
154
+ if (reqs.includes('bottle')) return 'Bottle';
155
+ return 'Python';
156
+ } catch (e) {
157
+ return 'Python';
158
+ }
159
+ }
160
+
161
+ if (probe.has_gemfile) {
162
+ try {
163
+ const gemfile = fs.readFileSync(path.join(this.projectRoot, 'Gemfile'), 'utf-8').toLowerCase();
164
+ if (gemfile.includes('rails')) return 'Ruby on Rails';
165
+ if (gemfile.includes('sinatra')) return 'Sinatra';
166
+ if (gemfile.includes('jekyll')) return 'Jekyll';
167
+ if (gemfile.includes('middleman')) return 'Middleman';
168
+ return 'Ruby';
169
+ } catch (e) {}
170
+ }
171
+
172
+ if (probe.has_artisan) return 'Laravel';
173
+
174
+ if (probe.php_files_count > 3) return 'PHP (custom)';
175
+
176
+ if (probe.html_files_count > 3 && probe.js_files_count === 0) return 'Static HTML';
177
+
178
+ if (probe.js_files_count > 3) return 'JavaScript';
179
+
180
+ if (probe.py_files_count > 3) return 'Python';
181
+
182
+ if (probe.go_files_count > 3) return 'Go';
183
+
184
+ if (probe.rs_files_count > 3) return 'Rust';
185
+
186
+ if (probe.java_files_count > 3) return 'Java';
187
+
188
+ if (probe.cs_files_count > 3) return 'C#';
189
+
190
+ return 'Unknown';
191
+ }
192
+
193
+ display(probe) {
194
+ console.log('\n' + '='.repeat(60));
195
+ console.log(' LWazi Project Probe Results');
196
+ console.log('='.repeat(60));
197
+
198
+ const guessStr = probe.framework_guess !== 'Unknown' ? probe.framework_guess : 'custom project structure';
199
+ console.log(`\n I found what looks like a ${guessStr} project`);
200
+
201
+ console.log(`\n Files found in project root:`);
202
+ console.log(` HTML files: ${probe.html_files_count}`);
203
+ console.log(` PHP files: ${probe.php_files_count}`);
204
+ console.log(` JS files: ${probe.js_files_count}`);
205
+ console.log(` Python: ${probe.py_files_count}`);
206
+ console.log(` Ruby: ${probe.rb_files_count}`);
207
+ console.log(` Other ext.: ${probe.other_extensions.join(', ')}`);
208
+
209
+ if (probe.subdirectories.length > 0) {
210
+ console.log(`\n Directories: ${probe.subdirectories.join(', ')}`);
211
+ }
212
+
213
+ if (probe.likely_entry_files.length > 0) {
214
+ console.log(`\n Likely entry files: ${probe.likely_entry_files.join(', ')}`);
215
+ }
216
+
217
+ if (probe.template_engines.length > 0) {
218
+ console.log(` Template engines detected: ${probe.template_engines.join(', ')}`);
219
+ }
220
+
221
+ if (probe.public_dirs.length > 0) {
222
+ console.log(` Public directories: ${probe.public_dirs.join(', ')}`);
223
+ }
224
+
225
+ if (probe.confidence === 'unknown' || probe.framework_guess === 'Unknown') {
226
+ console.log(`\n I found a custom project structure — Lwazi will analyse it intelligently.`);
227
+ console.log(` The AI-powered indexer will understand your project without assumptions.`);
228
+ } else {
229
+ console.log(`\n Lwazi will use its AI-powered indexer to deeply understand your project.`);
230
+ }
231
+
232
+ console.log('='.repeat(60) + '\n');
233
+ }
234
+ }
package/lib/sync.js ADDED
@@ -0,0 +1,92 @@
1
+ import fs from 'fs';
2
+ import path from 'path';
3
+ import axios from 'axios';
4
+
5
+ export class Sync {
6
+ constructor(projectRoot) {
7
+ this.projectRoot = path.resolve(projectRoot);
8
+ this.configPath = path.join(this.projectRoot, 'lwazi.config.json');
9
+ }
10
+
11
+ async sync() {
12
+ if (!fs.existsSync(this.configPath)) {
13
+ console.error('No lwazi.config.json found. Run "lwazi-2.0 init" first.');
14
+ process.exit(1);
15
+ }
16
+
17
+ const config = JSON.parse(fs.readFileSync(this.configPath, 'utf-8'));
18
+ const serverUrl = config.lwazi_server || 'http://localhost:11500';
19
+
20
+ console.log('\nRe-indexing project...\n');
21
+
22
+ try {
23
+ const response = await axios.post(`${serverUrl}/index`, {
24
+ project_id: config.project_id,
25
+ project_root: this.projectRoot,
26
+ base_url: config.base_url,
27
+ project_name: config.project_name,
28
+ max_crawl_pages: config.max_crawl_pages || 500,
29
+ skip_paths: config.skip_paths || []
30
+ }, {
31
+ responseType: 'stream',
32
+ timeout: 600000
33
+ });
34
+
35
+ const stream = response.data;
36
+ let resultData = '';
37
+
38
+ for await (const chunk of stream) {
39
+ const text = chunk.toString();
40
+ const lines = text.split('\n').filter(l => l.trim());
41
+
42
+ for (const line of lines) {
43
+ if (line.startsWith('data:')) {
44
+ const data = line.slice(5).trim();
45
+ if (data === '[DONE]') break;
46
+ try {
47
+ const parsed = JSON.parse(data);
48
+ if (parsed.event === 'progress') {
49
+ process.stdout.write(`\r ${parsed.data}`);
50
+ } else if (parsed.event === 'complete') {
51
+ resultData = parsed.data;
52
+ } else if (parsed.event === 'error') {
53
+ console.error(`\n Error: ${parsed.data}`);
54
+ }
55
+ } catch (e) {
56
+ if (data && !data.startsWith('[')) {
57
+ process.stdout.write(`\r ${data}`);
58
+ }
59
+ }
60
+ }
61
+ }
62
+ }
63
+
64
+ console.log('\n');
65
+
66
+ if (resultData) {
67
+ try {
68
+ const result = JSON.parse(resultData);
69
+ console.log(' Sync complete!');
70
+ console.log(` Files indexed: ${result.files_indexed}`);
71
+ console.log(` Routes found: ${result.routes_found}`);
72
+ console.log(` Pages crawled: ${result.pages_indexed}`);
73
+ console.log(` Duration: ${result.duration_seconds}s`);
74
+ } catch (e) {
75
+ console.log(' Sync completed (could not parse result).');
76
+ }
77
+ }
78
+
79
+ config.last_synced = new Date().toISOString();
80
+ fs.writeFileSync(this.configPath, JSON.stringify(config, null, 2));
81
+
82
+ } catch (err) {
83
+ if (err.code === 'ECONNREFUSED') {
84
+ console.error(' Error: Could not reach Lwazi server at', serverUrl);
85
+ console.error(' Make sure the Lwazi server is running.');
86
+ } else {
87
+ console.error(' Error during sync:', err.message);
88
+ }
89
+ process.exit(1);
90
+ }
91
+ }
92
+ }
package/package.json ADDED
@@ -0,0 +1,23 @@
1
+ {
2
+ "name": "lwazi-2.0",
3
+ "version": "0.1.0",
4
+ "description": "Intelligent project-aware AI chatbot. Works with any website.",
5
+ "author": "Nigel Nkomo <nigel.nkomo@gmail.com>",
6
+ "type": "module",
7
+ "bin": {
8
+ "lwazi-2.0": "./bin/lwazi.js"
9
+ },
10
+ "scripts": {
11
+ "link": "npm link",
12
+ "install:global": "npm install -g ."
13
+ },
14
+ "dependencies": {
15
+ "axios": "^1.6.0",
16
+ "bcryptjs": "^2.4.3",
17
+ "chalk": "^5.3.0",
18
+ "inquirer": "^9.2.0",
19
+ "nanoid": "^5.0.0",
20
+ "glob": "^10.3.0",
21
+ "cheerio": "^1.0.0"
22
+ }
23
+ }