syntropic 0.9.7 → 0.9.8

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
@@ -133,11 +133,13 @@ syntropic remove
133
133
 
134
134
  ```bash
135
135
  syntropic audit # Analyse git history (no install needed)
136
- syntropic init [project-name] # Install methodology + docs
136
+ syntropic init [project-name] # Install methodology + docs + post-commit hook
137
137
  syntropic add cursor windsurf # Add tools to existing project
138
138
  syntropic remove # Clean uninstall (preserves docs)
139
139
  syntropic health # Run pre-flight checks
140
140
  syntropic report # Submit anonymous cycle report
141
+ syntropic autofire status # Check if post-commit hook is installed
142
+ syntropic autofire enable|disable # Manage the post-commit hook
141
143
  syntropic telemetry status # Check telemetry setting
142
144
  syntropic analyse # Deep-dive analysis (requires login)
143
145
  syntropic analyse --list # Browse past analyses
@@ -147,6 +149,14 @@ syntropic logout # Sign out
147
149
  syntropic whoami # Show current auth status
148
150
  ```
149
151
 
152
+ ### `syntropic init` flags
153
+
154
+ ```bash
155
+ syntropic init --no-hook # Skip installing the post-commit hook
156
+ syntropic init --no-email # Skip the optional email prompt
157
+ syntropic init --yes # Non-interactive (defaults to all tools, no email prompt)
158
+ ```
159
+
150
160
  ## How It Works
151
161
 
152
162
  Your instruction file contains a bootstrap rule (EG13) that fetches the full methodology from the PRISM network at cycle start. Rules, agents, and patterns are served fresh — always current, zero maintenance. Your LLM does all compute. Syntropic serves the methodology but runs zero inference.
package/bin/syntropic.js CHANGED
@@ -61,6 +61,7 @@ const COMMANDS = {
61
61
  health: () => require('../commands/health'),
62
62
  telemetry: () => require('../commands/telemetry'),
63
63
  report: () => require('../commands/report'),
64
+ autofire: () => require('../commands/autofire'),
64
65
  login: () => require('../commands/login'),
65
66
  logout: () => require('../commands/logout'),
66
67
  whoami: () => require('../commands/whoami'),
@@ -92,6 +93,7 @@ if (!command || args.includes('--help') || args.includes('-h')) {
92
93
  syntropic health Run a local health check
93
94
  syntropic telemetry [cmd] Manage pseudonymised PRISM telemetry (enable/disable/status)
94
95
  syntropic report [flags] Submit a PRISM cycle report
96
+ syntropic autofire [cmd] Manage post-commit hook that auto-fires report (enable/disable/status)
95
97
  syntropic --version Show version
96
98
  syntropic --help Show this help
97
99
 
@@ -107,6 +109,8 @@ if (!command || args.includes('--help') || args.includes('-h')) {
107
109
  --domain example.com Production domain
108
110
  --test-url /test Test page path
109
111
  --prod-url / Production page path
112
+ --no-hook Skip installing post-commit autofire hook
113
+ --no-email Skip the optional email prompt
110
114
  --yes Skip interactive prompts
111
115
 
112
116
  Flags (analyse):
@@ -504,13 +504,31 @@ async function runExecute(auth, profile, idea, focus, flags) {
504
504
  const bar = '─'.repeat(50);
505
505
  console.log(` ${bar} 100%\n`);
506
506
 
507
- // Display results
508
- displayReport(profile.project_name, status.sections);
507
+ // Separate standard sections from partner outputs
508
+ const standardSections = (status.sections || []).filter(s => !s.slug?.startsWith('partner_output'));
509
+ const partnerSections = (status.sections || []).filter(s => s.slug?.startsWith('partner_output'));
509
510
 
510
- // Save locally
511
- const savedPath = saveReport(profile.project_name, status.sections);
511
+ // Display results (standard sections only)
512
+ displayReport(profile.project_name, standardSections);
513
+
514
+ // Save standard report
515
+ const savedPath = saveReport(profile.project_name, standardSections);
512
516
  console.log(`\n Saved to: ${savedPath}`);
513
517
 
518
+ // Save partner outputs as separate files
519
+ for (const ps of partnerSections) {
520
+ try {
521
+ const jsonData = typeof ps.json_data === 'string' ? JSON.parse(ps.json_data) : (ps.json_data || {});
522
+ const filename = jsonData.output_filename || `${ps.type}.md`;
523
+ const downloadsDir = path.join(require('os').homedir(), 'Downloads');
524
+ const outputPath = path.join(downloadsDir, filename);
525
+ fs.writeFileSync(outputPath, ps.markdown, 'utf8');
526
+ console.log(` Partner output: ~/${path.relative(require('os').homedir(), outputPath)}`);
527
+ } catch (partnerErr) {
528
+ // Don't fail the whole flow for a partner output save error
529
+ }
530
+ }
531
+
514
532
  // Feedback prompt
515
533
  if (isInteractive) {
516
534
  await promptFeedback(auth, sessionId);
@@ -0,0 +1,125 @@
1
+ /**
2
+ * syntropic autofire [enable|disable|status]
3
+ *
4
+ * Manages the git post-commit hook that fires `syntropic report` after each commit.
5
+ * Opt-in activation signal — turns "installed" into "used".
6
+ */
7
+
8
+ const fs = require('fs');
9
+ const path = require('path');
10
+
11
+ const MARKER = '# syntropic-autofire';
12
+
13
+ function hookPath(targetDir) {
14
+ return path.join(targetDir, '.git', 'hooks', 'post-commit');
15
+ }
16
+
17
+ function isInstalled(targetDir) {
18
+ const p = hookPath(targetDir);
19
+ if (!fs.existsSync(p)) return false;
20
+ try {
21
+ return fs.readFileSync(p, 'utf8').includes(MARKER);
22
+ } catch {
23
+ return false;
24
+ }
25
+ }
26
+
27
+ function install(targetDir) {
28
+ const gitDir = path.join(targetDir, '.git');
29
+ if (!fs.existsSync(gitDir)) {
30
+ return { ok: false, error: 'Not a git repository. Run `git init` first.' };
31
+ }
32
+
33
+ const hooksDir = path.join(gitDir, 'hooks');
34
+ if (!fs.existsSync(hooksDir)) fs.mkdirSync(hooksDir, { recursive: true });
35
+
36
+ const p = hookPath(targetDir);
37
+ const body = `#!/bin/sh
38
+ ${MARKER}
39
+ # Auto-fires syntropic report after each commit. Non-blocking, fire-and-forget.
40
+ # Remove with: syntropic autofire disable
41
+ command -v syntropic >/dev/null 2>&1 || command -v npx >/dev/null 2>&1 || exit 0
42
+ (syntropic report >/dev/null 2>&1 || npx -y syntropic report >/dev/null 2>&1) &
43
+ exit 0
44
+ `;
45
+
46
+ if (fs.existsSync(p)) {
47
+ const existing = fs.readFileSync(p, 'utf8');
48
+ if (existing.includes(MARKER)) return { ok: true, already: true };
49
+ return { ok: false, error: 'A post-commit hook already exists. Remove it first or merge manually.' };
50
+ }
51
+
52
+ fs.writeFileSync(p, body, { encoding: 'utf8', mode: 0o755 });
53
+ return { ok: true, already: false };
54
+ }
55
+
56
+ function uninstall(targetDir) {
57
+ const p = hookPath(targetDir);
58
+ if (!fs.existsSync(p)) return { ok: true, notFound: true };
59
+ try {
60
+ const content = fs.readFileSync(p, 'utf8');
61
+ if (!content.includes(MARKER)) {
62
+ return { ok: false, error: 'Post-commit hook exists but was not installed by syntropic. Not removed.' };
63
+ }
64
+ fs.unlinkSync(p);
65
+ return { ok: true, notFound: false };
66
+ } catch (err) {
67
+ return { ok: false, error: err.message };
68
+ }
69
+ }
70
+
71
+ async function run(args) {
72
+ const sub = (args[0] || 'status').toLowerCase();
73
+ const targetDir = process.cwd();
74
+
75
+ if (sub === 'enable') {
76
+ const res = install(targetDir);
77
+ if (!res.ok) {
78
+ console.log(`\n ${res.error}\n`);
79
+ process.exit(1);
80
+ }
81
+ if (res.already) {
82
+ console.log('\n Autofire is already enabled.\n');
83
+ } else {
84
+ console.log('\n Autofire enabled. `syntropic report` will run after every commit (non-blocking).');
85
+ console.log(' Disable anytime: syntropic autofire disable\n');
86
+ }
87
+ return;
88
+ }
89
+
90
+ if (sub === 'disable') {
91
+ const res = uninstall(targetDir);
92
+ if (!res.ok) {
93
+ console.log(`\n ${res.error}\n`);
94
+ process.exit(1);
95
+ }
96
+ if (res.notFound) {
97
+ console.log('\n Autofire was not installed. Nothing to remove.\n');
98
+ } else {
99
+ console.log('\n Autofire disabled. Post-commit hook removed.');
100
+ console.log(' Re-enable anytime: syntropic autofire enable\n');
101
+ }
102
+ return;
103
+ }
104
+
105
+ // status
106
+ const installed = isInstalled(targetDir);
107
+ console.log(`
108
+ Syntropic Autofire
109
+ ──────────────────
110
+ Status: ${installed ? 'ENABLED' : 'DISABLED'}
111
+ Path: .git/hooks/post-commit
112
+
113
+ What it does:
114
+ Runs \`syntropic report\` after every git commit — a signal to the PRISM
115
+ network that a development cycle completed. Non-blocking, fire-and-forget.
116
+ Respects \`syntropic telemetry disable\`.
117
+
118
+ Commands:
119
+ syntropic autofire enable Install the post-commit hook in this repo
120
+ syntropic autofire disable Remove the hook
121
+ syntropic autofire status Show current state
122
+ `);
123
+ }
124
+
125
+ module.exports = run;
package/commands/init.js CHANGED
@@ -14,7 +14,138 @@
14
14
  const fs = require('fs');
15
15
  const path = require('path');
16
16
  const readline = require('readline');
17
- const { ensureConfig } = require('./config-utils');
17
+ const https = require('https');
18
+ const crypto = require('crypto');
19
+ const { ensureConfig, loadConfig, clientHash } = require('./config-utils');
20
+
21
+ const API_BASE = 'https://www.syntropicworks.com';
22
+
23
+ function postJson(urlPath, body, timeoutMs = 3000) {
24
+ return new Promise((resolve) => {
25
+ try {
26
+ const url = new URL(urlPath, API_BASE);
27
+ const data = JSON.stringify(body);
28
+ const req = https.request({
29
+ hostname: url.hostname,
30
+ port: 443,
31
+ path: url.pathname,
32
+ method: 'POST',
33
+ headers: {
34
+ 'Content-Type': 'application/json',
35
+ 'Content-Length': Buffer.byteLength(data),
36
+ },
37
+ timeout: timeoutMs,
38
+ }, (res) => {
39
+ res.resume();
40
+ res.on('end', () => resolve(res.statusCode >= 200 && res.statusCode < 300));
41
+ });
42
+ req.on('error', () => resolve(false));
43
+ req.on('timeout', () => { req.destroy(); resolve(false); });
44
+ req.write(data);
45
+ req.end();
46
+ } catch {
47
+ resolve(false);
48
+ }
49
+ });
50
+ }
51
+
52
+ function detectToolAndOs() {
53
+ let tool = 'unknown';
54
+ if (process.env.CURSOR_SESSION_ID || process.env.CURSOR_TRACE_ID) tool = 'cursor';
55
+ else if (process.env.WINDSURF_SESSION) tool = 'windsurf';
56
+ else if (process.env.GITHUB_COPILOT) tool = 'copilot';
57
+ else if (process.env.CODEX_SESSION) tool = 'codex';
58
+ else if (process.env.CLAUDECODE || process.env.CLAUDE_CODE_SESSION) tool = 'claude_code';
59
+ return { tool, os: require('os').platform() };
60
+ }
61
+
62
+ function detectFramework(dir) {
63
+ const has = (f) => fs.existsSync(path.join(dir, f));
64
+ if (has('next.config.js') || has('next.config.mjs') || has('next.config.ts')) return 'nextjs';
65
+ if (has('nuxt.config.js') || has('nuxt.config.ts')) return 'nuxt';
66
+ if (has('angular.json')) return 'angular';
67
+ if (has('svelte.config.js')) return 'svelte';
68
+ if (has('vite.config.js') || has('vite.config.ts')) return 'vite';
69
+ if (has('requirements.txt') || has('pyproject.toml')) return 'python';
70
+ if (has('Cargo.toml')) return 'rust';
71
+ if (has('go.mod')) return 'go';
72
+ return 'unknown';
73
+ }
74
+
75
+ function repoHashFor(dir) {
76
+ try {
77
+ const { execSync } = require('child_process');
78
+ let identity;
79
+ try {
80
+ identity = execSync('git remote get-url origin 2>/dev/null', { cwd: dir, encoding: 'utf8' }).trim();
81
+ } catch {
82
+ identity = path.basename(dir);
83
+ }
84
+ return crypto.createHash('sha256').update(identity || path.basename(dir)).digest('hex').slice(0, 16);
85
+ } catch {
86
+ return null;
87
+ }
88
+ }
89
+
90
+ async function sendActivationPing(targetDir, selectedTools, hasHook) {
91
+ const config = loadConfig();
92
+ if (!config.telemetry) return;
93
+ const env = detectToolAndOs();
94
+ await postJson('/api/v1/prism/report', {
95
+ type: 'activation',
96
+ anon_id: clientHash(config.device_id),
97
+ cli_version: require('../package.json').version,
98
+ tool: env.tool,
99
+ os: env.os,
100
+ framework: detectFramework(targetDir),
101
+ repo_hash: repoHashFor(targetDir),
102
+ tools: selectedTools,
103
+ has_hook: !!hasHook,
104
+ source: 'init',
105
+ });
106
+ }
107
+
108
+ async function sendWaitlistEmail(email, targetDir) {
109
+ const config = loadConfig();
110
+ const env = detectToolAndOs();
111
+ await postJson('/api/v1/cli/waitlist', {
112
+ email,
113
+ anon_id: config.telemetry ? clientHash(config.device_id) : null,
114
+ cli_version: require('../package.json').version,
115
+ tool: env.tool,
116
+ os: env.os,
117
+ framework: detectFramework(targetDir),
118
+ source: 'init',
119
+ }, 5000);
120
+ }
121
+
122
+ function installPostCommitHook(targetDir) {
123
+ const gitDir = path.join(targetDir, '.git');
124
+ if (!fs.existsSync(gitDir)) return { installed: false, reason: 'not a git repo' };
125
+
126
+ const hooksDir = path.join(gitDir, 'hooks');
127
+ if (!fs.existsSync(hooksDir)) fs.mkdirSync(hooksDir, { recursive: true });
128
+
129
+ const hookPath = path.join(hooksDir, 'post-commit');
130
+ const marker = '# syntropic-autofire';
131
+ const body = `#!/bin/sh
132
+ ${marker}
133
+ # Auto-fires syntropic report after each commit. Non-blocking, fire-and-forget.
134
+ # Remove with: syntropic autofire disable
135
+ command -v syntropic >/dev/null 2>&1 || command -v npx >/dev/null 2>&1 || exit 0
136
+ (syntropic report >/dev/null 2>&1 || npx -y syntropic report >/dev/null 2>&1) &
137
+ exit 0
138
+ `;
139
+
140
+ if (fs.existsSync(hookPath)) {
141
+ const existing = fs.readFileSync(hookPath, 'utf8');
142
+ if (existing.includes(marker)) return { installed: true, reason: 'already installed' };
143
+ return { installed: false, reason: 'post-commit hook exists (not overwritten)' };
144
+ }
145
+
146
+ fs.writeFileSync(hookPath, body, { encoding: 'utf8', mode: 0o755 });
147
+ return { installed: true, reason: 'new hook' };
148
+ }
18
149
 
19
150
  const TEMPLATES_DIR = path.join(__dirname, '..', 'templates');
20
151
 
@@ -316,6 +447,39 @@ async function run(args) {
316
447
  }
317
448
  }
318
449
 
450
+ // Install post-commit hook (auto-fires `syntropic report` after every commit)
451
+ // Skipped with --no-hook flag or when not a git repo
452
+ let hookResult = { installed: false, reason: 'skipped' };
453
+ if (!flags['no-hook']) {
454
+ hookResult = installPostCommitHook(targetDir);
455
+ if (hookResult.installed) {
456
+ console.log(' hook .git/hooks/post-commit (auto-fires `syntropic report` after each commit)');
457
+ } else if (hookResult.reason === 'not a git repo') {
458
+ console.log(' skip post-commit hook (not a git repo yet — run `git init` then `syntropic autofire enable`)');
459
+ } else if (hookResult.reason !== 'already installed') {
460
+ console.log(` skip post-commit hook (${hookResult.reason})`);
461
+ }
462
+ }
463
+
464
+ // Optional email capture — helps measure real users vs npm install noise
465
+ // Only prompts when interactive and user hasn't passed --no-email
466
+ if (isInteractive && !flags['no-email']) {
467
+ console.log('');
468
+ const email = await ask(' Optional: email for roadmap updates + early access to paid features (Enter to skip): ');
469
+ if (email && email.includes('@') && email.length < 254) {
470
+ try {
471
+ await sendWaitlistEmail(email.trim(), targetDir);
472
+ console.log(' Thanks. No spam — just occasional updates.');
473
+ } catch { /* fire-and-forget */ }
474
+ }
475
+ }
476
+
477
+ // Fire activation ping — measures real `init` completion, not just npm pulls
478
+ // Respects telemetry opt-out
479
+ try {
480
+ await sendActivationPing(targetDir, selectedTools, hookResult.installed);
481
+ } catch { /* fire-and-forget, never block init */ }
482
+
319
483
  console.log(`
320
484
  Done! Your project is set up with the Syntropic pipeline.
321
485
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "syntropic",
3
- "version": "0.9.7",
3
+ "version": "0.9.8",
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"