syntropic 0.9.4 → 0.9.6
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/README.md +3 -1
- package/commands/analyse.js +408 -74
- package/commands/audit.js +10 -4
- package/commands/init.js +2 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -102,7 +102,9 @@ syntropic remove # Clean uninstall (preserves docs)
|
|
|
102
102
|
syntropic health # Run pre-flight checks
|
|
103
103
|
syntropic report # Submit anonymous cycle report
|
|
104
104
|
syntropic telemetry status # Check telemetry setting
|
|
105
|
-
syntropic analyse # Deep-dive analysis (
|
|
105
|
+
syntropic analyse # Deep-dive analysis (requires login)
|
|
106
|
+
syntropic analyse --list # Browse your past analyses
|
|
107
|
+
syntropic login # Sign in (needed for analyse only)
|
|
106
108
|
```
|
|
107
109
|
|
|
108
110
|
## How It Works
|
package/commands/analyse.js
CHANGED
|
@@ -1,16 +1,16 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* syntropic analyse
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
* prompts from the server, and outputs everything for the user's AI
|
|
6
|
-
* coding tool to run.
|
|
4
|
+
* Run a full server-side venture analysis from the CLI.
|
|
7
5
|
*
|
|
8
6
|
* Usage:
|
|
9
|
-
* syntropic analyse #
|
|
7
|
+
* syntropic analyse # New analysis (interactive)
|
|
8
|
+
* syntropic analyse --idea "description" # New analysis (non-interactive)
|
|
10
9
|
* syntropic analyse --focus "question" # With specific focus
|
|
11
|
-
* syntropic analyse --
|
|
12
|
-
* syntropic analyse --
|
|
13
|
-
* syntropic analyse --
|
|
10
|
+
* syntropic analyse --list # Browse existing analyses
|
|
11
|
+
* syntropic analyse --prompts # Legacy: output prompts for AI tool
|
|
12
|
+
* syntropic analyse --dry-run # Output profile only, don't send
|
|
13
|
+
* syntropic analyse --yes # Skip confirmations
|
|
14
14
|
*/
|
|
15
15
|
|
|
16
16
|
const fs = require('fs');
|
|
@@ -100,7 +100,7 @@ function detectRoutes(targetDir) {
|
|
|
100
100
|
});
|
|
101
101
|
}
|
|
102
102
|
}
|
|
103
|
-
break;
|
|
103
|
+
break;
|
|
104
104
|
}
|
|
105
105
|
return routes;
|
|
106
106
|
}
|
|
@@ -108,7 +108,6 @@ function detectRoutes(targetDir) {
|
|
|
108
108
|
function detectDataModel(targetDir) {
|
|
109
109
|
const entities = [];
|
|
110
110
|
|
|
111
|
-
// Prisma schema
|
|
112
111
|
const prismaPath = path.join(targetDir, 'prisma', 'schema.prisma');
|
|
113
112
|
const prismaContent = readFileSafe(prismaPath, 50000);
|
|
114
113
|
if (prismaContent) {
|
|
@@ -127,7 +126,6 @@ function detectDataModel(targetDir) {
|
|
|
127
126
|
}
|
|
128
127
|
}
|
|
129
128
|
|
|
130
|
-
// SQL migrations — extract CREATE TABLE
|
|
131
129
|
const migrationDirs = ['supabase/migrations', 'migrations', 'db/migrations'];
|
|
132
130
|
for (const migDir of migrationDirs) {
|
|
133
131
|
const fullPath = path.join(targetDir, migDir);
|
|
@@ -135,7 +133,7 @@ function detectDataModel(targetDir) {
|
|
|
135
133
|
|
|
136
134
|
try {
|
|
137
135
|
const files = fs.readdirSync(fullPath).filter(f => f.endsWith('.sql')).sort();
|
|
138
|
-
for (const file of files.slice(-5)) {
|
|
136
|
+
for (const file of files.slice(-5)) {
|
|
139
137
|
const content = readFileSafe(path.join(fullPath, file), 50000);
|
|
140
138
|
if (!content) continue;
|
|
141
139
|
const tables = content.match(/CREATE TABLE[^(]+\(([^;]+)\)/gi);
|
|
@@ -171,7 +169,6 @@ function readProductProfile(targetDir) {
|
|
|
171
169
|
|
|
172
170
|
const found = [];
|
|
173
171
|
|
|
174
|
-
// package.json
|
|
175
172
|
const pkgPath = path.join(targetDir, 'package.json');
|
|
176
173
|
const pkg = readFileSafe(pkgPath);
|
|
177
174
|
if (pkg) {
|
|
@@ -186,7 +183,6 @@ function readProductProfile(targetDir) {
|
|
|
186
183
|
};
|
|
187
184
|
profile.dependencies_count = Object.keys(allDeps).length;
|
|
188
185
|
|
|
189
|
-
// Detect framework
|
|
190
186
|
if (allDeps.next) profile.tech_stack.framework = `Next.js ${allDeps.next}`;
|
|
191
187
|
else if (allDeps.nuxt) profile.tech_stack.framework = `Nuxt ${allDeps.nuxt}`;
|
|
192
188
|
else if (allDeps.react) profile.tech_stack.framework = `React ${allDeps.react}`;
|
|
@@ -194,11 +190,9 @@ function readProductProfile(targetDir) {
|
|
|
194
190
|
else if (allDeps.svelte || allDeps['@sveltejs/kit']) profile.tech_stack.framework = 'SvelteKit';
|
|
195
191
|
else if (allDeps.express) profile.tech_stack.framework = 'Express';
|
|
196
192
|
|
|
197
|
-
// Detect language
|
|
198
193
|
if (allDeps.typescript) profile.tech_stack.language = 'TypeScript';
|
|
199
194
|
else profile.tech_stack.language = 'JavaScript';
|
|
200
195
|
|
|
201
|
-
// Key deps (top 10 by name recognition)
|
|
202
196
|
const importantDeps = Object.keys(allDeps)
|
|
203
197
|
.filter(d => !d.startsWith('@types/') && !d.startsWith('eslint'))
|
|
204
198
|
.slice(0, 10);
|
|
@@ -210,7 +204,6 @@ function readProductProfile(targetDir) {
|
|
|
210
204
|
}
|
|
211
205
|
}
|
|
212
206
|
|
|
213
|
-
// Python: requirements.txt or pyproject.toml
|
|
214
207
|
const reqPath = path.join(targetDir, 'requirements.txt');
|
|
215
208
|
if (fs.existsSync(reqPath)) {
|
|
216
209
|
const content = readFileSafe(reqPath);
|
|
@@ -223,25 +216,21 @@ function readProductProfile(targetDir) {
|
|
|
223
216
|
}
|
|
224
217
|
}
|
|
225
218
|
|
|
226
|
-
// Go: go.mod
|
|
227
219
|
const goModPath = path.join(targetDir, 'go.mod');
|
|
228
220
|
if (fs.existsSync(goModPath)) {
|
|
229
221
|
profile.tech_stack.language = 'Go';
|
|
230
222
|
found.push('go.mod — Go');
|
|
231
223
|
}
|
|
232
224
|
|
|
233
|
-
// Rust: Cargo.toml
|
|
234
225
|
const cargoPath = path.join(targetDir, 'Cargo.toml');
|
|
235
226
|
if (fs.existsSync(cargoPath)) {
|
|
236
227
|
profile.tech_stack.language = 'Rust';
|
|
237
228
|
found.push('Cargo.toml — Rust');
|
|
238
229
|
}
|
|
239
230
|
|
|
240
|
-
// README.md
|
|
241
231
|
const readmePath = path.join(targetDir, 'README.md');
|
|
242
232
|
const readme = readFileSafe(readmePath);
|
|
243
233
|
if (readme) {
|
|
244
|
-
// Extract first paragraph as description if not set
|
|
245
234
|
if (!profile.description) {
|
|
246
235
|
const lines = readme.split('\n').filter(l => l.trim() && !l.startsWith('#'));
|
|
247
236
|
profile.description = lines.slice(0, 3).join(' ').trim().substring(0, 500);
|
|
@@ -249,25 +238,22 @@ function readProductProfile(targetDir) {
|
|
|
249
238
|
found.push('README.md — product description');
|
|
250
239
|
}
|
|
251
240
|
|
|
252
|
-
// Routes
|
|
253
241
|
const routes = detectRoutes(targetDir);
|
|
254
242
|
if (routes.length > 0) {
|
|
255
243
|
const pages = routes.filter(r => r.type === 'page').length;
|
|
256
244
|
const apis = routes.filter(r => r.type === 'api').length;
|
|
257
|
-
profile.features = routes.slice(0, 30);
|
|
245
|
+
profile.features = routes.slice(0, 30);
|
|
258
246
|
profile.file_structure_summary.pages = pages;
|
|
259
247
|
profile.file_structure_summary.api_routes = apis;
|
|
260
248
|
found.push(`routes — ${pages} pages, ${apis} API routes`);
|
|
261
249
|
}
|
|
262
250
|
|
|
263
|
-
// Data model
|
|
264
251
|
const dataModel = detectDataModel(targetDir);
|
|
265
252
|
if (dataModel.length > 0) {
|
|
266
253
|
profile.data_model = dataModel;
|
|
267
254
|
found.push(`schema — ${dataModel.length} entities`);
|
|
268
255
|
}
|
|
269
256
|
|
|
270
|
-
// Infrastructure
|
|
271
257
|
if (fs.existsSync(path.join(targetDir, 'vercel.json'))) {
|
|
272
258
|
profile.infrastructure.hosting = 'Vercel';
|
|
273
259
|
found.push('vercel.json — Vercel hosting');
|
|
@@ -283,7 +269,6 @@ function readProductProfile(targetDir) {
|
|
|
283
269
|
profile.infrastructure.ci = 'GitHub Actions';
|
|
284
270
|
}
|
|
285
271
|
|
|
286
|
-
// Database detection
|
|
287
272
|
if (fs.existsSync(path.join(targetDir, 'prisma'))) {
|
|
288
273
|
profile.tech_stack.database = 'Prisma';
|
|
289
274
|
}
|
|
@@ -291,7 +276,6 @@ function readProductProfile(targetDir) {
|
|
|
291
276
|
profile.infrastructure.database = 'Supabase';
|
|
292
277
|
}
|
|
293
278
|
|
|
294
|
-
// Component count
|
|
295
279
|
const componentDirs = ['components', 'src/components', 'app/components'];
|
|
296
280
|
for (const dir of componentDirs) {
|
|
297
281
|
const fullPath = path.join(targetDir, dir);
|
|
@@ -302,7 +286,6 @@ function readProductProfile(targetDir) {
|
|
|
302
286
|
}
|
|
303
287
|
}
|
|
304
288
|
|
|
305
|
-
// Lib/utils count
|
|
306
289
|
const libDirs = ['lib', 'src/lib', 'src/utils', 'utils'];
|
|
307
290
|
for (const dir of libDirs) {
|
|
308
291
|
const fullPath = path.join(targetDir, dir);
|
|
@@ -318,6 +301,45 @@ function readProductProfile(targetDir) {
|
|
|
318
301
|
|
|
319
302
|
// ── API Communication ──────────────────────────────────────
|
|
320
303
|
|
|
304
|
+
function apiRequest(method, endpoint, accessToken, body) {
|
|
305
|
+
return new Promise((resolve, reject) => {
|
|
306
|
+
const url = new URL(endpoint, API_BASE);
|
|
307
|
+
const postData = body ? JSON.stringify(body) : null;
|
|
308
|
+
const options = {
|
|
309
|
+
hostname: url.hostname,
|
|
310
|
+
port: 443,
|
|
311
|
+
path: url.pathname,
|
|
312
|
+
method,
|
|
313
|
+
headers: {
|
|
314
|
+
'Authorization': `Bearer ${accessToken}`,
|
|
315
|
+
'Content-Type': 'application/json',
|
|
316
|
+
},
|
|
317
|
+
};
|
|
318
|
+
if (postData) {
|
|
319
|
+
options.headers['Content-Length'] = Buffer.byteLength(postData);
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
const req = https.request(options, (res) => {
|
|
323
|
+
let data = '';
|
|
324
|
+
res.on('data', (chunk) => { data += chunk; });
|
|
325
|
+
res.on('end', () => {
|
|
326
|
+
try {
|
|
327
|
+
const parsed = JSON.parse(data);
|
|
328
|
+
parsed._statusCode = res.statusCode;
|
|
329
|
+
resolve(parsed);
|
|
330
|
+
} catch (e) {
|
|
331
|
+
reject(new Error(`Invalid response from server (${res.statusCode})`));
|
|
332
|
+
}
|
|
333
|
+
});
|
|
334
|
+
});
|
|
335
|
+
|
|
336
|
+
req.on('error', reject);
|
|
337
|
+
if (postData) req.write(postData);
|
|
338
|
+
req.end();
|
|
339
|
+
});
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
// Legacy prompt fetch for --prompts mode
|
|
321
343
|
function fetchPrompts(accessToken) {
|
|
322
344
|
return new Promise((resolve, reject) => {
|
|
323
345
|
const url = new URL('/api/v1/analyse/prompts', API_BASE);
|
|
@@ -379,62 +401,295 @@ function parseFlags(args) {
|
|
|
379
401
|
return flags;
|
|
380
402
|
}
|
|
381
403
|
|
|
382
|
-
// ──
|
|
404
|
+
// ── Progress Display ──────────────────────────────────────
|
|
383
405
|
|
|
384
|
-
|
|
385
|
-
|
|
406
|
+
function clearLine() {
|
|
407
|
+
if (process.stdout.isTTY) {
|
|
408
|
+
process.stdout.write('\r\x1b[K');
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
function formatDuration(ms) {
|
|
413
|
+
const s = Math.round(ms / 1000);
|
|
414
|
+
return s < 60 ? `${s}s` : `${Math.floor(s / 60)}m${s % 60}s`;
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
// ── Execute Flow (New Analysis) ──────────────────────────────
|
|
418
|
+
|
|
419
|
+
async function runExecute(auth, profile, idea, focus, flags) {
|
|
386
420
|
const isInteractive = process.stdin.isTTY !== false && !flags.yes;
|
|
387
421
|
|
|
388
|
-
|
|
422
|
+
// Start analysis
|
|
423
|
+
console.log('\n Screening...');
|
|
424
|
+
const startResult = await apiRequest('POST', '/api/v1/analyse/start', auth.access_token, {
|
|
425
|
+
idea,
|
|
426
|
+
productProfile: profile,
|
|
427
|
+
focus: focus || null,
|
|
428
|
+
});
|
|
389
429
|
|
|
390
|
-
//
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
430
|
+
// Handle screening rejection
|
|
431
|
+
if (startResult.screened) {
|
|
432
|
+
console.log('\n This idea was not analysed.');
|
|
433
|
+
console.log(' Syntropic does not analyse ideas intended to cause harm.');
|
|
434
|
+
console.log(' We cannot see what you submitted.\n');
|
|
435
|
+
|
|
436
|
+
if (isInteractive) {
|
|
437
|
+
const wasMistake = await ask(' Was this a mistake? [y/N] ');
|
|
438
|
+
if (wasMistake.toLowerCase() === 'y') {
|
|
439
|
+
const feedback = await ask(' Tell us why (your idea is NOT included): ');
|
|
440
|
+
if (feedback) {
|
|
441
|
+
await apiRequest('POST', '/api/v1/feedback', auth.access_token, {
|
|
442
|
+
type: 'rejection',
|
|
443
|
+
comment: feedback,
|
|
444
|
+
source: 'cli',
|
|
445
|
+
});
|
|
446
|
+
console.log(' Feedback sent. We\'ll review and improve our screening.\n');
|
|
447
|
+
}
|
|
448
|
+
}
|
|
396
449
|
}
|
|
450
|
+
return;
|
|
451
|
+
}
|
|
397
452
|
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
453
|
+
if (!startResult.success) {
|
|
454
|
+
if (startResult._statusCode === 429) {
|
|
455
|
+
console.log(`\n No credits remaining. Visit syntropicworks.com for more.\n`);
|
|
456
|
+
} else {
|
|
457
|
+
console.log(`\n Error: ${startResult.error || 'Failed to start analysis'}\n`);
|
|
401
458
|
}
|
|
459
|
+
return;
|
|
402
460
|
}
|
|
403
461
|
|
|
404
|
-
|
|
405
|
-
console.log('
|
|
406
|
-
const targetDir = process.cwd();
|
|
407
|
-
const { profile, found } = readProductProfile(targetDir);
|
|
462
|
+
const sessionId = startResult.sessionId;
|
|
463
|
+
console.log(' Screening passed.\n');
|
|
408
464
|
|
|
409
|
-
for (
|
|
410
|
-
|
|
465
|
+
// Poll for progress (10 minute timeout)
|
|
466
|
+
const startTime = Date.now();
|
|
467
|
+
const POLL_TIMEOUT_MS = 10 * 60 * 1000;
|
|
468
|
+
let lastMessage = '';
|
|
469
|
+
let lastCompleted = 0;
|
|
470
|
+
|
|
471
|
+
while (Date.now() - startTime < POLL_TIMEOUT_MS) {
|
|
472
|
+
const status = await apiRequest('GET', `/api/v1/analyse/status/${sessionId}`, auth.access_token);
|
|
473
|
+
|
|
474
|
+
if (!status.success) {
|
|
475
|
+
console.log(`\n Error polling status: ${status.error}\n`);
|
|
476
|
+
return;
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
// Show progress updates
|
|
480
|
+
const p = status.progress;
|
|
481
|
+
const total = p.agentsTotal || 10;
|
|
482
|
+
const completed = p.agentsCompleted || 0;
|
|
483
|
+
const elapsed = formatDuration(Date.now() - startTime);
|
|
484
|
+
|
|
485
|
+
if (completed > lastCompleted && p.currentAgent) {
|
|
486
|
+
// A new agent completed — print its line
|
|
487
|
+
console.log(` [${completed}/${total}] ${lastMessage || 'Processing...'} done`);
|
|
488
|
+
lastCompleted = completed;
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
if (p.message !== lastMessage && status.status !== 'complete') {
|
|
492
|
+
lastMessage = p.message;
|
|
493
|
+
if (process.stdout.isTTY) {
|
|
494
|
+
clearLine();
|
|
495
|
+
const step = completed + 1;
|
|
496
|
+
process.stdout.write(` [${step}/${total}] ${p.message} ${elapsed}`);
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
if (status.status === 'complete') {
|
|
501
|
+
clearLine();
|
|
502
|
+
console.log(` [${total}/${total}] Complete! done`);
|
|
503
|
+
|
|
504
|
+
const bar = '─'.repeat(50);
|
|
505
|
+
console.log(` ${bar} 100%\n`);
|
|
506
|
+
|
|
507
|
+
// Display results
|
|
508
|
+
displayReport(profile.project_name, status.sections);
|
|
509
|
+
|
|
510
|
+
// Save locally
|
|
511
|
+
const savedPath = saveReport(profile.project_name, status.sections);
|
|
512
|
+
console.log(`\n Saved to: ${savedPath}`);
|
|
513
|
+
|
|
514
|
+
// Feedback prompt
|
|
515
|
+
if (isInteractive) {
|
|
516
|
+
await promptFeedback(auth, sessionId);
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
return;
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
if (status.status === 'failed') {
|
|
523
|
+
clearLine();
|
|
524
|
+
console.log(`\n Analysis failed: ${status.error || 'Unknown error'}\n`);
|
|
525
|
+
return;
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
// Poll interval
|
|
529
|
+
await new Promise(r => setTimeout(r, 3000));
|
|
411
530
|
}
|
|
412
531
|
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
532
|
+
// Timeout reached
|
|
533
|
+
clearLine();
|
|
534
|
+
console.log('\n Analysis timed out after 10 minutes.');
|
|
535
|
+
console.log(` It may still be running. Try: syntropic analyse --list\n`);
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
// ── Display Report ──────────────────────────────────────
|
|
539
|
+
|
|
540
|
+
function displayReport(projectName, sections) {
|
|
541
|
+
const sep = '═'.repeat(58);
|
|
542
|
+
console.log(` ${sep}`);
|
|
543
|
+
console.log(` SYNTROPIC ANALYSIS: ${projectName}`);
|
|
544
|
+
console.log(` ${sep}\n`);
|
|
545
|
+
|
|
546
|
+
if (!sections || sections.length === 0) {
|
|
547
|
+
console.log(' No report sections available.\n');
|
|
548
|
+
return;
|
|
417
549
|
}
|
|
418
550
|
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
551
|
+
for (const section of sections) {
|
|
552
|
+
if (section.markdown) {
|
|
553
|
+
// Indent each line for consistent display
|
|
554
|
+
const lines = section.markdown.split('\n');
|
|
555
|
+
for (const line of lines) {
|
|
556
|
+
console.log(` ${line}`);
|
|
557
|
+
}
|
|
558
|
+
console.log('');
|
|
559
|
+
}
|
|
426
560
|
}
|
|
427
|
-
if (profile.data_model.length) console.log(` Data Model: ${profile.data_model.length} entities`);
|
|
428
561
|
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
562
|
+
console.log(` ${sep}`);
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
// ── Save Report ──────────────────────────────────────
|
|
566
|
+
|
|
567
|
+
function saveReport(projectName, sections) {
|
|
568
|
+
const reportsDir = path.join(process.cwd(), '.syntropic', 'reports');
|
|
569
|
+
if (!fs.existsSync(reportsDir)) {
|
|
570
|
+
fs.mkdirSync(reportsDir, { recursive: true });
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
const date = new Date().toISOString().slice(0, 10);
|
|
574
|
+
const safeName = projectName.replace(/[^a-zA-Z0-9-_]/g, '-').toLowerCase();
|
|
575
|
+
const fileName = `${date}-${safeName}.md`;
|
|
576
|
+
const filePath = path.join(reportsDir, fileName);
|
|
577
|
+
|
|
578
|
+
const content = [];
|
|
579
|
+
content.push(`# Syntropic Analysis: ${projectName}`);
|
|
580
|
+
content.push(`*Generated: ${new Date().toISOString()}*\n`);
|
|
581
|
+
|
|
582
|
+
for (const section of (sections || [])) {
|
|
583
|
+
if (section.markdown) {
|
|
584
|
+
content.push(section.markdown);
|
|
585
|
+
content.push('');
|
|
586
|
+
}
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
fs.writeFileSync(filePath, content.join('\n'), 'utf8');
|
|
590
|
+
return `.syntropic/reports/${fileName}`;
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
// ── Feedback Prompt ──────────────────────────────────────
|
|
594
|
+
|
|
595
|
+
async function promptFeedback(auth, sessionId) {
|
|
596
|
+
console.log('');
|
|
597
|
+
const rating = await ask(' Was this useful? [Y]es [N]o [S]kip: ');
|
|
598
|
+
|
|
599
|
+
if (rating.toLowerCase() === 's' || !rating) return;
|
|
600
|
+
|
|
601
|
+
const ratingValue = rating.toLowerCase() === 'y' ? 'positive' : 'negative';
|
|
602
|
+
const comment = await ask(' Comments (optional, Enter to skip): ');
|
|
603
|
+
|
|
604
|
+
await apiRequest('POST', '/api/v1/feedback', auth.access_token, {
|
|
605
|
+
type: 'report',
|
|
606
|
+
sessionId,
|
|
607
|
+
rating: ratingValue,
|
|
608
|
+
comment: comment || null,
|
|
609
|
+
source: 'cli',
|
|
610
|
+
});
|
|
611
|
+
|
|
612
|
+
console.log(' Thanks.\n');
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
// ── List Flow ──────────────────────────────────────
|
|
616
|
+
|
|
617
|
+
async function runList(auth) {
|
|
618
|
+
console.log('\n Fetching your analyses...\n');
|
|
619
|
+
|
|
620
|
+
const result = await apiRequest('GET', '/api/v1/analyse/sessions', auth.access_token);
|
|
621
|
+
|
|
622
|
+
if (!result.success || !result.sessions || result.sessions.length === 0) {
|
|
623
|
+
console.log(' No analyses found. Run `syntropic analyse` to start one.\n');
|
|
624
|
+
return;
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
const sessions = result.sessions.filter(s => s.status === 'complete');
|
|
628
|
+
|
|
629
|
+
if (sessions.length === 0) {
|
|
630
|
+
console.log(' No completed analyses found.\n');
|
|
434
631
|
return;
|
|
435
632
|
}
|
|
436
633
|
|
|
437
|
-
//
|
|
634
|
+
// Display list
|
|
635
|
+
console.log(' # Name Date');
|
|
636
|
+
console.log(' ── ────────────────────────────────────── ──────────');
|
|
637
|
+
|
|
638
|
+
for (let i = 0; i < sessions.length; i++) {
|
|
639
|
+
const s = sessions[i];
|
|
640
|
+
const num = String(i + 1).padStart(2);
|
|
641
|
+
const name = (s.name || 'Untitled').substring(0, 40).padEnd(40);
|
|
642
|
+
const date = new Date(s.date).toLocaleDateString('en-GB', {
|
|
643
|
+
day: '2-digit', month: 'short', year: 'numeric',
|
|
644
|
+
});
|
|
645
|
+
console.log(` ${num} ${name} ${date}`);
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
const choice = await ask(`\n Select [1-${sessions.length}] or Enter to cancel: `);
|
|
649
|
+
const idx = parseInt(choice, 10) - 1;
|
|
650
|
+
|
|
651
|
+
if (isNaN(idx) || idx < 0 || idx >= sessions.length) {
|
|
652
|
+
console.log(' Cancelled.\n');
|
|
653
|
+
return;
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
const selected = sessions[idx];
|
|
657
|
+
const action = await ask(' [V]iew report [R]e-analyse [Enter] cancel: ');
|
|
658
|
+
|
|
659
|
+
if (action.toLowerCase() === 'v') {
|
|
660
|
+
// Fetch full report
|
|
661
|
+
const status = await apiRequest('GET', `/api/v1/analyse/status/${selected.sessionId}`, auth.access_token);
|
|
662
|
+
if (status.success && status.sections) {
|
|
663
|
+
displayReport(selected.name, status.sections);
|
|
664
|
+
} else {
|
|
665
|
+
console.log(` Error fetching report: ${status.error || 'Unknown error'}\n`);
|
|
666
|
+
}
|
|
667
|
+
} else if (action.toLowerCase() === 'r') {
|
|
668
|
+
// Re-analyse with updated context
|
|
669
|
+
const updatedContext = await ask(' Updated context (optional, Enter to keep same): ');
|
|
670
|
+
|
|
671
|
+
// Fetch original idea from the session
|
|
672
|
+
const status = await apiRequest('GET', `/api/v1/analyse/status/${selected.sessionId}`, auth.access_token);
|
|
673
|
+
|
|
674
|
+
// Build updated idea
|
|
675
|
+
const idea = updatedContext
|
|
676
|
+
? `Re-analysis of: ${selected.name}\n\nUpdated context: ${updatedContext}`
|
|
677
|
+
: `Re-analysis of: ${selected.name}`;
|
|
678
|
+
|
|
679
|
+
const targetDir = process.cwd();
|
|
680
|
+
const { profile } = readProductProfile(targetDir);
|
|
681
|
+
|
|
682
|
+
await runExecute(auth, profile, idea, null, { yes: true });
|
|
683
|
+
} else {
|
|
684
|
+
console.log(' Cancelled.\n');
|
|
685
|
+
}
|
|
686
|
+
}
|
|
687
|
+
|
|
688
|
+
// ── Legacy Prompts Mode ──────────────────────────────────────
|
|
689
|
+
|
|
690
|
+
async function runPrompts(auth, profile, found, flags) {
|
|
691
|
+
const isInteractive = process.stdin.isTTY !== false && !flags.yes;
|
|
692
|
+
|
|
438
693
|
if (isInteractive) {
|
|
439
694
|
const confirm = await ask('\n Send profile to Syntropic for analysis prompts? [Y/n] ');
|
|
440
695
|
if (confirm.toLowerCase() === 'n') {
|
|
@@ -443,7 +698,6 @@ async function run(args) {
|
|
|
443
698
|
}
|
|
444
699
|
}
|
|
445
700
|
|
|
446
|
-
// Optional focus question
|
|
447
701
|
let focus = flags.focus || '';
|
|
448
702
|
if (!focus && isInteractive) {
|
|
449
703
|
focus = await ask(' Focus question (optional, press Enter to skip): ');
|
|
@@ -451,13 +705,12 @@ async function run(args) {
|
|
|
451
705
|
|
|
452
706
|
const context = flags.context || '';
|
|
453
707
|
|
|
454
|
-
// Fetch analysis prompts
|
|
455
708
|
console.log('\n Fetching analysis prompts...');
|
|
456
709
|
|
|
457
710
|
let prompts;
|
|
458
711
|
try {
|
|
459
712
|
prompts = await fetchPrompts(auth.access_token);
|
|
460
|
-
console.log('
|
|
713
|
+
console.log(' Analysis prompts received\n');
|
|
461
714
|
} catch (err) {
|
|
462
715
|
console.error(` Error: ${err.message}`);
|
|
463
716
|
if (err.message.includes('401') || err.message.includes('Unauthorized')) {
|
|
@@ -466,12 +719,10 @@ async function run(args) {
|
|
|
466
719
|
process.exit(1);
|
|
467
720
|
}
|
|
468
721
|
|
|
469
|
-
// Output the full analysis context for the AI tool
|
|
470
722
|
console.log(' ═══════════════════════════════════════════════════════');
|
|
471
723
|
console.log(' SYNTROPIC ANALYSIS — Your AI tool will now run this');
|
|
472
724
|
console.log(' ═══════════════════════════════════════════════════════\n');
|
|
473
725
|
|
|
474
|
-
// Build the combined prompt
|
|
475
726
|
const analysisOutput = buildAnalysisOutput(profile, prompts, focus, context);
|
|
476
727
|
console.log(analysisOutput);
|
|
477
728
|
|
|
@@ -488,7 +739,6 @@ function buildAnalysisOutput(profile, prompts, focus, context) {
|
|
|
488
739
|
sections.push(`# Syntropic Analysis: ${profile.project_name}`);
|
|
489
740
|
sections.push(`*Run this analysis using the instructions below.*\n`);
|
|
490
741
|
|
|
491
|
-
// Product profile context
|
|
492
742
|
sections.push(`## Product Profile\n`);
|
|
493
743
|
sections.push('```json');
|
|
494
744
|
sections.push(JSON.stringify(profile, null, 2));
|
|
@@ -501,19 +751,16 @@ function buildAnalysisOutput(profile, prompts, focus, context) {
|
|
|
501
751
|
sections.push(`## Additional Context\n${context}\n`);
|
|
502
752
|
}
|
|
503
753
|
|
|
504
|
-
// Instructions
|
|
505
754
|
sections.push(`## Instructions\n`);
|
|
506
755
|
sections.push(prompts.instructions);
|
|
507
756
|
sections.push('');
|
|
508
757
|
|
|
509
|
-
// Report prompt
|
|
510
758
|
sections.push(`## Step 1: Generate Analysis Report\n`);
|
|
511
759
|
sections.push(`Use this system prompt to analyse the product profile above:\n`);
|
|
512
760
|
sections.push('---');
|
|
513
761
|
sections.push(prompts.report_prompt);
|
|
514
762
|
sections.push('---\n');
|
|
515
763
|
|
|
516
|
-
// Lens prompts
|
|
517
764
|
sections.push(`## Step 2: Philosophy Lens Deep Dives\n`);
|
|
518
765
|
sections.push(`Run each of the following 8 lenses against the report from Step 1:\n`);
|
|
519
766
|
|
|
@@ -525,7 +772,6 @@ function buildAnalysisOutput(profile, prompts, focus, context) {
|
|
|
525
772
|
sections.push('');
|
|
526
773
|
}
|
|
527
774
|
|
|
528
|
-
// Validation
|
|
529
775
|
sections.push(`## Step 3: Validation Plan\n`);
|
|
530
776
|
sections.push(prompts.validation_prompt);
|
|
531
777
|
sections.push('');
|
|
@@ -533,4 +779,92 @@ function buildAnalysisOutput(profile, prompts, focus, context) {
|
|
|
533
779
|
return sections.join('\n');
|
|
534
780
|
}
|
|
535
781
|
|
|
782
|
+
// ── Main ──────────────────────────────────────
|
|
783
|
+
|
|
784
|
+
async function run(args) {
|
|
785
|
+
const flags = parseFlags(args || []);
|
|
786
|
+
const isInteractive = process.stdin.isTTY !== false && !flags.yes;
|
|
787
|
+
|
|
788
|
+
console.log('\n Syntropic Analyse\n');
|
|
789
|
+
|
|
790
|
+
// Check auth
|
|
791
|
+
const auth = getAuth();
|
|
792
|
+
if (!flags['dry-run']) {
|
|
793
|
+
if (!auth || !auth.access_token) {
|
|
794
|
+
console.log(' Not signed in. Run `syntropic login` first.\n');
|
|
795
|
+
process.exit(1);
|
|
796
|
+
}
|
|
797
|
+
|
|
798
|
+
if (isAuthExpired(auth)) {
|
|
799
|
+
console.log(' Session expired. Run `syntropic login` to refresh.\n');
|
|
800
|
+
process.exit(1);
|
|
801
|
+
}
|
|
802
|
+
}
|
|
803
|
+
|
|
804
|
+
// List mode
|
|
805
|
+
if (flags.list) {
|
|
806
|
+
await runList(auth);
|
|
807
|
+
return;
|
|
808
|
+
}
|
|
809
|
+
|
|
810
|
+
// Read codebase
|
|
811
|
+
console.log(' Reading codebase...');
|
|
812
|
+
const targetDir = process.cwd();
|
|
813
|
+
const { profile, found } = readProductProfile(targetDir);
|
|
814
|
+
|
|
815
|
+
for (const item of found) {
|
|
816
|
+
console.log(` ${item}`);
|
|
817
|
+
}
|
|
818
|
+
|
|
819
|
+
if (found.length === 0) {
|
|
820
|
+
console.log(' No recognisable project files found.');
|
|
821
|
+
console.log(' Run this from your project root directory.\n');
|
|
822
|
+
process.exit(1);
|
|
823
|
+
}
|
|
824
|
+
|
|
825
|
+
// Show profile summary
|
|
826
|
+
console.log(`\n Product Profile:`);
|
|
827
|
+
console.log(` Name: ${profile.project_name}`);
|
|
828
|
+
if (profile.description) console.log(` Description: ${profile.description.substring(0, 100)}...`);
|
|
829
|
+
if (profile.tech_stack.framework) console.log(` Stack: ${profile.tech_stack.framework} (${profile.tech_stack.language || 'JS'})`);
|
|
830
|
+
if (profile.file_structure_summary.pages) {
|
|
831
|
+
console.log(` Features: ${profile.file_structure_summary.pages} pages, ${profile.file_structure_summary.api_routes || 0} API routes`);
|
|
832
|
+
}
|
|
833
|
+
if (profile.data_model.length) console.log(` Data Model: ${profile.data_model.length} entities`);
|
|
834
|
+
|
|
835
|
+
// Dry run
|
|
836
|
+
if (flags['dry-run']) {
|
|
837
|
+
console.log('\n Product Profile (JSON):\n');
|
|
838
|
+
console.log(JSON.stringify(profile, null, 2));
|
|
839
|
+
console.log('\n Dry run complete. No data sent.\n');
|
|
840
|
+
return;
|
|
841
|
+
}
|
|
842
|
+
|
|
843
|
+
// Legacy prompts mode
|
|
844
|
+
if (flags.prompts) {
|
|
845
|
+
await runPrompts(auth, profile, found, flags);
|
|
846
|
+
return;
|
|
847
|
+
}
|
|
848
|
+
|
|
849
|
+
// Execute mode (default) — get the idea
|
|
850
|
+
let idea = flags.idea || '';
|
|
851
|
+
|
|
852
|
+
if (!idea && isInteractive) {
|
|
853
|
+
console.log('');
|
|
854
|
+
idea = await ask(' Describe your idea or venture:\n > ');
|
|
855
|
+
}
|
|
856
|
+
|
|
857
|
+
if (!idea) {
|
|
858
|
+
console.log('\n No idea provided. Use --idea "description" or run interactively.\n');
|
|
859
|
+
process.exit(1);
|
|
860
|
+
}
|
|
861
|
+
|
|
862
|
+
let focus = flags.focus || '';
|
|
863
|
+
if (!focus && isInteractive) {
|
|
864
|
+
focus = await ask('\n Focus question (optional, Enter to skip): ');
|
|
865
|
+
}
|
|
866
|
+
|
|
867
|
+
await runExecute(auth, profile, idea, focus, flags);
|
|
868
|
+
}
|
|
869
|
+
|
|
536
870
|
module.exports = run;
|
package/commands/audit.js
CHANGED
|
@@ -286,8 +286,15 @@ function run(args) {
|
|
|
286
286
|
console.log(` ${red('Significant process gaps.')} A development methodology would catch these early.`);
|
|
287
287
|
}
|
|
288
288
|
|
|
289
|
-
|
|
290
|
-
|
|
289
|
+
// Next steps — clear CTAs based on score
|
|
290
|
+
if (score < 8) {
|
|
291
|
+
console.log(` Fix these automatically:`);
|
|
292
|
+
console.log(` ${bold('npx syntropic init')}`);
|
|
293
|
+
console.log('');
|
|
294
|
+
}
|
|
295
|
+
console.log(` Already using syntropic? Submit a cycle report:`);
|
|
296
|
+
console.log(` ${bold('syntropic report')}`);
|
|
297
|
+
console.log('');
|
|
291
298
|
|
|
292
299
|
// Anonymous audit ping — fire-and-forget, respects telemetry opt-out
|
|
293
300
|
sendAuditPing(score);
|
|
@@ -304,8 +311,7 @@ function sendAuditPing(score) {
|
|
|
304
311
|
if (fs.existsSync(configPath)) {
|
|
305
312
|
const config = JSON.parse(fs.readFileSync(configPath, 'utf8'));
|
|
306
313
|
if (config.telemetry === 'disabled' || config.telemetry === false) return;
|
|
307
|
-
|
|
308
|
-
anonId = crypto.createHash('sha256').update(config.device_id + date).digest('hex');
|
|
314
|
+
anonId = crypto.createHash('sha256').update(config.device_id).digest('hex');
|
|
309
315
|
} else {
|
|
310
316
|
// No config yet (haven't run init) — hash hostname+user for counting
|
|
311
317
|
anonId = crypto.createHash('sha256').update(os.hostname() + os.userInfo().username).digest('hex');
|
package/commands/init.js
CHANGED
|
@@ -346,6 +346,8 @@ ${toolFiles}
|
|
|
346
346
|
1. Fill in your North Star — vision, mission, and goals
|
|
347
347
|
2. Review your instruction file and add project-specific context
|
|
348
348
|
3. Start building! Your AI follows the EG7 pipeline automatically
|
|
349
|
+
4. After each cycle, run: syntropic report
|
|
350
|
+
(anonymous — helps improve the methodology for everyone)
|
|
349
351
|
|
|
350
352
|
Trust & privacy:
|
|
351
353
|
Everything in .claude/ is local and yours. PRISM telemetry is
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "syntropic",
|
|
3
|
-
"version": "0.9.
|
|
3
|
+
"version": "0.9.6",
|
|
4
4
|
"description": "Ship better software with a proven development methodology. Audit your git history, install disciplined rules, and track iterations — for Claude Code, Cursor, Windsurf, GitHub Copilot, and OpenAI Codex.",
|
|
5
5
|
"bin": {
|
|
6
6
|
"syntropic": "./bin/syntropic.js"
|