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 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 (8 philosophy lenses)
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
@@ -1,16 +1,16 @@
1
1
  /**
2
2
  * syntropic analyse
3
3
  *
4
- * Reads local codebase, generates a product profile, fetches analysis
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 # Analyse current directory
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 --context "extra info" # Additional context
12
- * syntropic analyse --dry-run # Output profile only, don't fetch
13
- * syntropic analyse --yes # Skip confirmation
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; // Use first found route directory
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)) { // Last 5 migrations
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); // Cap at 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
- // ── Main ──────────────────────────────────────
404
+ // ── Progress Display ──────────────────────────────────────
383
405
 
384
- async function run(args) {
385
- const flags = parseFlags(args || []);
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
- console.log('\n Syntropic Analyse — powered by your AI coding tool\n');
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
- // Check auth (dry-run doesn't need auth)
391
- const auth = getAuth();
392
- if (!flags['dry-run']) {
393
- if (!auth || !auth.access_token) {
394
- console.log(' Not signed in. Run `syntropic login` first.\n');
395
- process.exit(1);
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
- if (isAuthExpired(auth)) {
399
- console.log(' Session expired. Run `syntropic login` to refresh.\n');
400
- process.exit(1);
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
- // Read codebase
405
- console.log(' Reading codebase...');
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 (const item of found) {
410
- console.log(` ✓ ${item}`);
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
- if (found.length === 0) {
414
- console.log(' No recognisable project files found.');
415
- console.log(' Run this from your project root directory.\n');
416
- process.exit(1);
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
- // Show profile summary
420
- console.log(`\n Product Profile:`);
421
- console.log(` Name: ${profile.project_name}`);
422
- if (profile.description) console.log(` Description: ${profile.description.substring(0, 100)}...`);
423
- if (profile.tech_stack.framework) console.log(` Stack: ${profile.tech_stack.framework} (${profile.tech_stack.language || 'JS'})`);
424
- if (profile.file_structure_summary.pages) {
425
- console.log(` Features: ${profile.file_structure_summary.pages} pages, ${profile.file_structure_summary.api_routes || 0} API routes`);
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
- // Dry run — output profile and stop
430
- if (flags['dry-run']) {
431
- console.log('\n Product Profile (JSON):\n');
432
- console.log(JSON.stringify(profile, null, 2));
433
- console.log('\n Dry run complete. No data sent.\n');
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
- // Confirmation
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(' Analysis prompts received\n');
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
- console.log(`\n ${dim('Run `syntropic init` to install the methodology and prevent these automatically.')}`);
290
- console.log(` ${dim('Learn more: https://www.syntropicworks.com')}\n`);
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
- const date = new Date().toISOString().slice(0, 10);
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.4",
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"