syntropic 0.7.3 → 0.8.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/syntropic.js +19 -6
- package/commands/analyse.js +536 -0
- package/commands/config-utils.js +32 -0
- package/commands/init.js +34 -1
- package/commands/login.js +160 -0
- package/commands/logout.js +22 -0
- package/commands/whoami.js +28 -0
- package/package.json +4 -2
package/bin/syntropic.js
CHANGED
|
@@ -19,6 +19,11 @@ const COMMANDS = {
|
|
|
19
19
|
health: () => require('../commands/health'),
|
|
20
20
|
telemetry: () => require('../commands/telemetry'),
|
|
21
21
|
report: () => require('../commands/report'),
|
|
22
|
+
login: () => require('../commands/login'),
|
|
23
|
+
logout: () => require('../commands/logout'),
|
|
24
|
+
whoami: () => require('../commands/whoami'),
|
|
25
|
+
analyse: () => require('../commands/analyse'),
|
|
26
|
+
analyze: () => require('../commands/analyse'), // US spelling alias
|
|
22
27
|
};
|
|
23
28
|
|
|
24
29
|
// Version flag
|
|
@@ -36,19 +41,21 @@ if (!command || args.includes('--help') || args.includes('-h')) {
|
|
|
36
41
|
Usage:
|
|
37
42
|
syntropic init [project-name] Scaffold a new project with the Syntropic pipeline
|
|
38
43
|
syntropic add <tool> [tool...] Add support for another AI tool
|
|
44
|
+
syntropic analyse Analyse your product using 8 philosophy lenses
|
|
45
|
+
syntropic login Sign in to Syntropic (required for analyse)
|
|
46
|
+
syntropic logout Sign out
|
|
47
|
+
syntropic whoami Show current auth status
|
|
39
48
|
syntropic health Run a local health check
|
|
40
|
-
syntropic telemetry [cmd] Manage
|
|
41
|
-
syntropic report [flags] Submit
|
|
49
|
+
syntropic telemetry [cmd] Manage pseudonymised PRISM telemetry (enable/disable/status)
|
|
50
|
+
syntropic report [flags] Submit a PRISM cycle report
|
|
42
51
|
syntropic --version Show version
|
|
43
52
|
syntropic --help Show this help
|
|
44
53
|
|
|
45
54
|
What you get:
|
|
46
55
|
- Instruction files for Claude Code, Cursor, Windsurf, GitHub Copilot, and/or OpenAI Codex
|
|
47
|
-
-
|
|
48
|
-
- Evergreen Rules (EG1-EG9) — a disciplined dev pipeline
|
|
49
|
-
- Generic agents: dev, qa, research, plan, devops, security (Claude Code)
|
|
50
|
-
- Daily health check workflow with auto-remediation
|
|
56
|
+
- Evergreen Rules (EG1-EG13) — a disciplined dev pipeline
|
|
51
57
|
- PRISM efficiency tracking methodology
|
|
58
|
+
- Market analysis via 8 philosophy lenses (syntropic analyse)
|
|
52
59
|
|
|
53
60
|
Flags (init):
|
|
54
61
|
--tools claude,cursor,windsurf,copilot,codex Select AI tools (default: all)
|
|
@@ -58,6 +65,12 @@ if (!command || args.includes('--help') || args.includes('-h')) {
|
|
|
58
65
|
--prod-url / Production page path
|
|
59
66
|
--yes Skip interactive prompts
|
|
60
67
|
|
|
68
|
+
Flags (analyse):
|
|
69
|
+
--focus "question" Focus the analysis on a specific question
|
|
70
|
+
--context "extra info" Provide additional business context
|
|
71
|
+
--dry-run Output product profile without sending
|
|
72
|
+
--yes Skip confirmation prompts
|
|
73
|
+
|
|
61
74
|
Learn more: https://www.syntropicworks.com
|
|
62
75
|
`);
|
|
63
76
|
process.exit(0);
|
|
@@ -0,0 +1,536 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* syntropic analyse
|
|
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.
|
|
7
|
+
*
|
|
8
|
+
* Usage:
|
|
9
|
+
* syntropic analyse # Analyse current directory
|
|
10
|
+
* 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
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
const fs = require('fs');
|
|
17
|
+
const path = require('path');
|
|
18
|
+
const https = require('https');
|
|
19
|
+
const readline = require('readline');
|
|
20
|
+
const { getAuth, isAuthExpired } = require('./config-utils');
|
|
21
|
+
|
|
22
|
+
const API_BASE = 'https://www.syntropicworks.com';
|
|
23
|
+
|
|
24
|
+
// ── Codebase Reading ──────────────────────────────────────
|
|
25
|
+
|
|
26
|
+
const IGNORE_DIRS = new Set([
|
|
27
|
+
'node_modules', '.git', '.next', 'dist', 'build', '.vercel',
|
|
28
|
+
'vendor', '__pycache__', '.cache', 'coverage', '.turbo',
|
|
29
|
+
'.svelte-kit', '.nuxt', '.output', 'target', 'out',
|
|
30
|
+
]);
|
|
31
|
+
|
|
32
|
+
const IGNORE_FILES = new Set([
|
|
33
|
+
'.env', '.env.local', '.env.production', '.env.development',
|
|
34
|
+
'.DS_Store', 'Thumbs.db',
|
|
35
|
+
]);
|
|
36
|
+
|
|
37
|
+
function readFileSafe(filePath, maxBytes = 10000) {
|
|
38
|
+
try {
|
|
39
|
+
const stat = fs.statSync(filePath);
|
|
40
|
+
if (stat.size > maxBytes) return null;
|
|
41
|
+
return fs.readFileSync(filePath, 'utf8');
|
|
42
|
+
} catch (_) {
|
|
43
|
+
return null;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function countFiles(dir, ext, depth = 0) {
|
|
48
|
+
if (depth > 4) return 0;
|
|
49
|
+
let count = 0;
|
|
50
|
+
try {
|
|
51
|
+
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
52
|
+
if (IGNORE_DIRS.has(entry.name)) continue;
|
|
53
|
+
if (entry.isDirectory()) {
|
|
54
|
+
count += countFiles(path.join(dir, entry.name), ext, depth + 1);
|
|
55
|
+
} else if (!ext || entry.name.endsWith(ext)) {
|
|
56
|
+
count++;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
} catch (_) {}
|
|
60
|
+
return count;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function listDir(dir, depth = 0, maxDepth = 2) {
|
|
64
|
+
if (depth > maxDepth) return [];
|
|
65
|
+
const entries = [];
|
|
66
|
+
try {
|
|
67
|
+
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
68
|
+
if (IGNORE_DIRS.has(entry.name) || entry.name.startsWith('.')) continue;
|
|
69
|
+
if (entry.isDirectory()) {
|
|
70
|
+
entries.push(entry.name + '/');
|
|
71
|
+
if (depth < maxDepth) {
|
|
72
|
+
const children = listDir(path.join(dir, entry.name), depth + 1, maxDepth);
|
|
73
|
+
entries.push(...children.map(c => ' ' + c));
|
|
74
|
+
}
|
|
75
|
+
} else {
|
|
76
|
+
entries.push(entry.name);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
} catch (_) {}
|
|
80
|
+
return entries;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function detectRoutes(targetDir) {
|
|
84
|
+
const routes = [];
|
|
85
|
+
const routeDirs = ['pages', 'app', 'src/routes', 'src/pages', 'routes'];
|
|
86
|
+
|
|
87
|
+
for (const routeDir of routeDirs) {
|
|
88
|
+
const fullPath = path.join(targetDir, routeDir);
|
|
89
|
+
if (!fs.existsSync(fullPath)) continue;
|
|
90
|
+
|
|
91
|
+
const files = listDir(fullPath, 0, 3);
|
|
92
|
+
for (const f of files) {
|
|
93
|
+
const clean = f.trim();
|
|
94
|
+
if (clean.endsWith('.js') || clean.endsWith('.tsx') || clean.endsWith('.ts') ||
|
|
95
|
+
clean.endsWith('.vue') || clean.endsWith('.svelte') || clean.endsWith('/')) {
|
|
96
|
+
const isApi = clean.includes('api/') || clean.includes('api\\');
|
|
97
|
+
routes.push({
|
|
98
|
+
path: clean.replace(/\.(js|tsx|ts|vue|svelte)$/, ''),
|
|
99
|
+
type: isApi ? 'api' : 'page',
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
break; // Use first found route directory
|
|
104
|
+
}
|
|
105
|
+
return routes;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function detectDataModel(targetDir) {
|
|
109
|
+
const entities = [];
|
|
110
|
+
|
|
111
|
+
// Prisma schema
|
|
112
|
+
const prismaPath = path.join(targetDir, 'prisma', 'schema.prisma');
|
|
113
|
+
const prismaContent = readFileSafe(prismaPath, 50000);
|
|
114
|
+
if (prismaContent) {
|
|
115
|
+
const models = prismaContent.match(/model\s+(\w+)\s*\{([^}]+)\}/g);
|
|
116
|
+
if (models) {
|
|
117
|
+
for (const model of models) {
|
|
118
|
+
const nameMatch = model.match(/model\s+(\w+)/);
|
|
119
|
+
const fields = model.match(/^\s+(\w+)\s/gm);
|
|
120
|
+
if (nameMatch) {
|
|
121
|
+
entities.push({
|
|
122
|
+
entity: nameMatch[1],
|
|
123
|
+
fields: (fields || []).map(f => f.trim()).filter(f => f && f !== 'model'),
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// SQL migrations — extract CREATE TABLE
|
|
131
|
+
const migrationDirs = ['supabase/migrations', 'migrations', 'db/migrations'];
|
|
132
|
+
for (const migDir of migrationDirs) {
|
|
133
|
+
const fullPath = path.join(targetDir, migDir);
|
|
134
|
+
if (!fs.existsSync(fullPath)) continue;
|
|
135
|
+
|
|
136
|
+
try {
|
|
137
|
+
const files = fs.readdirSync(fullPath).filter(f => f.endsWith('.sql')).sort();
|
|
138
|
+
for (const file of files.slice(-5)) { // Last 5 migrations
|
|
139
|
+
const content = readFileSafe(path.join(fullPath, file), 50000);
|
|
140
|
+
if (!content) continue;
|
|
141
|
+
const tables = content.match(/CREATE TABLE[^(]+\(([^;]+)\)/gi);
|
|
142
|
+
if (tables) {
|
|
143
|
+
for (const table of tables) {
|
|
144
|
+
const nameMatch = table.match(/CREATE TABLE\s+(?:IF NOT EXISTS\s+)?(\w+)/i);
|
|
145
|
+
if (nameMatch && !entities.find(e => e.entity === nameMatch[1])) {
|
|
146
|
+
entities.push({ entity: nameMatch[1], fields: ['(from SQL migration)'] });
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
} catch (_) {}
|
|
152
|
+
break;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
return entities;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function readProductProfile(targetDir) {
|
|
159
|
+
const profile = {
|
|
160
|
+
profile_version: '1.0',
|
|
161
|
+
project_name: path.basename(targetDir),
|
|
162
|
+
description: '',
|
|
163
|
+
tech_stack: {},
|
|
164
|
+
features: [],
|
|
165
|
+
data_model: [],
|
|
166
|
+
dependencies_count: 0,
|
|
167
|
+
key_dependencies: [],
|
|
168
|
+
infrastructure: {},
|
|
169
|
+
file_structure_summary: {},
|
|
170
|
+
};
|
|
171
|
+
|
|
172
|
+
const found = [];
|
|
173
|
+
|
|
174
|
+
// package.json
|
|
175
|
+
const pkgPath = path.join(targetDir, 'package.json');
|
|
176
|
+
const pkg = readFileSafe(pkgPath);
|
|
177
|
+
if (pkg) {
|
|
178
|
+
try {
|
|
179
|
+
const parsed = JSON.parse(pkg);
|
|
180
|
+
profile.project_name = parsed.name || profile.project_name;
|
|
181
|
+
profile.description = parsed.description || '';
|
|
182
|
+
|
|
183
|
+
const allDeps = {
|
|
184
|
+
...(parsed.dependencies || {}),
|
|
185
|
+
...(parsed.devDependencies || {}),
|
|
186
|
+
};
|
|
187
|
+
profile.dependencies_count = Object.keys(allDeps).length;
|
|
188
|
+
|
|
189
|
+
// Detect framework
|
|
190
|
+
if (allDeps.next) profile.tech_stack.framework = `Next.js ${allDeps.next}`;
|
|
191
|
+
else if (allDeps.nuxt) profile.tech_stack.framework = `Nuxt ${allDeps.nuxt}`;
|
|
192
|
+
else if (allDeps.react) profile.tech_stack.framework = `React ${allDeps.react}`;
|
|
193
|
+
else if (allDeps.vue) profile.tech_stack.framework = `Vue ${allDeps.vue}`;
|
|
194
|
+
else if (allDeps.svelte || allDeps['@sveltejs/kit']) profile.tech_stack.framework = 'SvelteKit';
|
|
195
|
+
else if (allDeps.express) profile.tech_stack.framework = 'Express';
|
|
196
|
+
|
|
197
|
+
// Detect language
|
|
198
|
+
if (allDeps.typescript) profile.tech_stack.language = 'TypeScript';
|
|
199
|
+
else profile.tech_stack.language = 'JavaScript';
|
|
200
|
+
|
|
201
|
+
// Key deps (top 10 by name recognition)
|
|
202
|
+
const importantDeps = Object.keys(allDeps)
|
|
203
|
+
.filter(d => !d.startsWith('@types/') && !d.startsWith('eslint'))
|
|
204
|
+
.slice(0, 10);
|
|
205
|
+
profile.key_dependencies = importantDeps;
|
|
206
|
+
|
|
207
|
+
found.push(`package.json — ${profile.tech_stack.framework || 'Node.js'}, ${profile.dependencies_count} dependencies`);
|
|
208
|
+
} catch (_) {
|
|
209
|
+
found.push('package.json — found (parse error)');
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
// Python: requirements.txt or pyproject.toml
|
|
214
|
+
const reqPath = path.join(targetDir, 'requirements.txt');
|
|
215
|
+
if (fs.existsSync(reqPath)) {
|
|
216
|
+
const content = readFileSafe(reqPath);
|
|
217
|
+
if (content) {
|
|
218
|
+
const deps = content.split('\n').filter(l => l.trim() && !l.startsWith('#'));
|
|
219
|
+
profile.dependencies_count = deps.length;
|
|
220
|
+
profile.tech_stack.language = 'Python';
|
|
221
|
+
profile.key_dependencies = deps.slice(0, 10).map(d => d.split('==')[0].trim());
|
|
222
|
+
found.push(`requirements.txt — Python, ${deps.length} dependencies`);
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
// Go: go.mod
|
|
227
|
+
const goModPath = path.join(targetDir, 'go.mod');
|
|
228
|
+
if (fs.existsSync(goModPath)) {
|
|
229
|
+
profile.tech_stack.language = 'Go';
|
|
230
|
+
found.push('go.mod — Go');
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
// Rust: Cargo.toml
|
|
234
|
+
const cargoPath = path.join(targetDir, 'Cargo.toml');
|
|
235
|
+
if (fs.existsSync(cargoPath)) {
|
|
236
|
+
profile.tech_stack.language = 'Rust';
|
|
237
|
+
found.push('Cargo.toml — Rust');
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
// README.md
|
|
241
|
+
const readmePath = path.join(targetDir, 'README.md');
|
|
242
|
+
const readme = readFileSafe(readmePath);
|
|
243
|
+
if (readme) {
|
|
244
|
+
// Extract first paragraph as description if not set
|
|
245
|
+
if (!profile.description) {
|
|
246
|
+
const lines = readme.split('\n').filter(l => l.trim() && !l.startsWith('#'));
|
|
247
|
+
profile.description = lines.slice(0, 3).join(' ').trim().substring(0, 500);
|
|
248
|
+
}
|
|
249
|
+
found.push('README.md — product description');
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
// Routes
|
|
253
|
+
const routes = detectRoutes(targetDir);
|
|
254
|
+
if (routes.length > 0) {
|
|
255
|
+
const pages = routes.filter(r => r.type === 'page').length;
|
|
256
|
+
const apis = routes.filter(r => r.type === 'api').length;
|
|
257
|
+
profile.features = routes.slice(0, 30); // Cap at 30
|
|
258
|
+
profile.file_structure_summary.pages = pages;
|
|
259
|
+
profile.file_structure_summary.api_routes = apis;
|
|
260
|
+
found.push(`routes — ${pages} pages, ${apis} API routes`);
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
// Data model
|
|
264
|
+
const dataModel = detectDataModel(targetDir);
|
|
265
|
+
if (dataModel.length > 0) {
|
|
266
|
+
profile.data_model = dataModel;
|
|
267
|
+
found.push(`schema — ${dataModel.length} entities`);
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
// Infrastructure
|
|
271
|
+
if (fs.existsSync(path.join(targetDir, 'vercel.json'))) {
|
|
272
|
+
profile.infrastructure.hosting = 'Vercel';
|
|
273
|
+
found.push('vercel.json — Vercel hosting');
|
|
274
|
+
}
|
|
275
|
+
if (fs.existsSync(path.join(targetDir, 'netlify.toml'))) {
|
|
276
|
+
profile.infrastructure.hosting = 'Netlify';
|
|
277
|
+
}
|
|
278
|
+
if (fs.existsSync(path.join(targetDir, 'docker-compose.yml')) || fs.existsSync(path.join(targetDir, 'Dockerfile'))) {
|
|
279
|
+
profile.infrastructure.containers = 'Docker';
|
|
280
|
+
found.push('Docker — containerised');
|
|
281
|
+
}
|
|
282
|
+
if (fs.existsSync(path.join(targetDir, '.github', 'workflows'))) {
|
|
283
|
+
profile.infrastructure.ci = 'GitHub Actions';
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
// Database detection
|
|
287
|
+
if (fs.existsSync(path.join(targetDir, 'prisma'))) {
|
|
288
|
+
profile.tech_stack.database = 'Prisma';
|
|
289
|
+
}
|
|
290
|
+
if (fs.existsSync(path.join(targetDir, 'supabase'))) {
|
|
291
|
+
profile.infrastructure.database = 'Supabase';
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
// Component count
|
|
295
|
+
const componentDirs = ['components', 'src/components', 'app/components'];
|
|
296
|
+
for (const dir of componentDirs) {
|
|
297
|
+
const fullPath = path.join(targetDir, dir);
|
|
298
|
+
if (fs.existsSync(fullPath)) {
|
|
299
|
+
const count = countFiles(fullPath, null);
|
|
300
|
+
profile.file_structure_summary.components = count;
|
|
301
|
+
break;
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
// Lib/utils count
|
|
306
|
+
const libDirs = ['lib', 'src/lib', 'src/utils', 'utils'];
|
|
307
|
+
for (const dir of libDirs) {
|
|
308
|
+
const fullPath = path.join(targetDir, dir);
|
|
309
|
+
if (fs.existsSync(fullPath)) {
|
|
310
|
+
const count = countFiles(fullPath, null);
|
|
311
|
+
profile.file_structure_summary.lib_utils = count;
|
|
312
|
+
break;
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
return { profile, found };
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
// ── API Communication ──────────────────────────────────────
|
|
320
|
+
|
|
321
|
+
function fetchPrompts(accessToken) {
|
|
322
|
+
return new Promise((resolve, reject) => {
|
|
323
|
+
const url = new URL('/api/v1/analyse/prompts', API_BASE);
|
|
324
|
+
const options = {
|
|
325
|
+
hostname: url.hostname,
|
|
326
|
+
port: 443,
|
|
327
|
+
path: url.pathname,
|
|
328
|
+
method: 'GET',
|
|
329
|
+
headers: {
|
|
330
|
+
'Authorization': `Bearer ${accessToken}`,
|
|
331
|
+
'Content-Type': 'application/json',
|
|
332
|
+
},
|
|
333
|
+
};
|
|
334
|
+
|
|
335
|
+
const req = https.request(options, (res) => {
|
|
336
|
+
let data = '';
|
|
337
|
+
res.on('data', (chunk) => { data += chunk; });
|
|
338
|
+
res.on('end', () => {
|
|
339
|
+
try {
|
|
340
|
+
if (res.statusCode === 200) {
|
|
341
|
+
resolve(JSON.parse(data));
|
|
342
|
+
} else {
|
|
343
|
+
const parsed = JSON.parse(data);
|
|
344
|
+
reject(new Error(parsed.error || `Server returned ${res.statusCode}`));
|
|
345
|
+
}
|
|
346
|
+
} catch (e) {
|
|
347
|
+
reject(new Error(`Invalid response from server (${res.statusCode})`));
|
|
348
|
+
}
|
|
349
|
+
});
|
|
350
|
+
});
|
|
351
|
+
|
|
352
|
+
req.on('error', reject);
|
|
353
|
+
req.end();
|
|
354
|
+
});
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
// ── Interactive Prompts ──────────────────────────────────────
|
|
358
|
+
|
|
359
|
+
function ask(question) {
|
|
360
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
361
|
+
return new Promise((resolve) => {
|
|
362
|
+
rl.question(question, (answer) => {
|
|
363
|
+
rl.close();
|
|
364
|
+
resolve(answer.trim());
|
|
365
|
+
});
|
|
366
|
+
});
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
// ── Flag Parsing ──────────────────────────────────────
|
|
370
|
+
|
|
371
|
+
function parseFlags(args) {
|
|
372
|
+
const flags = {};
|
|
373
|
+
for (let i = 0; i < args.length; i++) {
|
|
374
|
+
if (args[i].startsWith('--')) {
|
|
375
|
+
const key = args[i].replace('--', '');
|
|
376
|
+
flags[key] = args[i + 1] && !args[i + 1].startsWith('--') ? args[++i] : true;
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
return flags;
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
// ── Main ──────────────────────────────────────
|
|
383
|
+
|
|
384
|
+
async function run(args) {
|
|
385
|
+
const flags = parseFlags(args || []);
|
|
386
|
+
const isInteractive = process.stdin.isTTY !== false && !flags.yes;
|
|
387
|
+
|
|
388
|
+
console.log('\n Syntropic Analyse — powered by your AI coding tool\n');
|
|
389
|
+
|
|
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);
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
if (isAuthExpired(auth)) {
|
|
399
|
+
console.log(' Session expired. Run `syntropic login` to refresh.\n');
|
|
400
|
+
process.exit(1);
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
// Read codebase
|
|
405
|
+
console.log(' Reading codebase...');
|
|
406
|
+
const targetDir = process.cwd();
|
|
407
|
+
const { profile, found } = readProductProfile(targetDir);
|
|
408
|
+
|
|
409
|
+
for (const item of found) {
|
|
410
|
+
console.log(` ✓ ${item}`);
|
|
411
|
+
}
|
|
412
|
+
|
|
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);
|
|
417
|
+
}
|
|
418
|
+
|
|
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`);
|
|
426
|
+
}
|
|
427
|
+
if (profile.data_model.length) console.log(` Data Model: ${profile.data_model.length} entities`);
|
|
428
|
+
|
|
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');
|
|
434
|
+
return;
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
// Confirmation
|
|
438
|
+
if (isInteractive) {
|
|
439
|
+
const confirm = await ask('\n Send profile to Syntropic for analysis prompts? [Y/n] ');
|
|
440
|
+
if (confirm.toLowerCase() === 'n') {
|
|
441
|
+
console.log(' Cancelled.\n');
|
|
442
|
+
return;
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
// Optional focus question
|
|
447
|
+
let focus = flags.focus || '';
|
|
448
|
+
if (!focus && isInteractive) {
|
|
449
|
+
focus = await ask(' Focus question (optional, press Enter to skip): ');
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
const context = flags.context || '';
|
|
453
|
+
|
|
454
|
+
// Fetch analysis prompts
|
|
455
|
+
console.log('\n Fetching analysis prompts...');
|
|
456
|
+
|
|
457
|
+
let prompts;
|
|
458
|
+
try {
|
|
459
|
+
prompts = await fetchPrompts(auth.access_token);
|
|
460
|
+
console.log(' ✓ Analysis prompts received\n');
|
|
461
|
+
} catch (err) {
|
|
462
|
+
console.error(` Error: ${err.message}`);
|
|
463
|
+
if (err.message.includes('401') || err.message.includes('Unauthorized')) {
|
|
464
|
+
console.log(' Session may have expired. Run `syntropic login` to refresh.\n');
|
|
465
|
+
}
|
|
466
|
+
process.exit(1);
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
// Output the full analysis context for the AI tool
|
|
470
|
+
console.log(' ═══════════════════════════════════════════════════════');
|
|
471
|
+
console.log(' SYNTROPIC ANALYSIS — Your AI tool will now run this');
|
|
472
|
+
console.log(' ═══════════════════════════════════════════════════════\n');
|
|
473
|
+
|
|
474
|
+
// Build the combined prompt
|
|
475
|
+
const analysisOutput = buildAnalysisOutput(profile, prompts, focus, context);
|
|
476
|
+
console.log(analysisOutput);
|
|
477
|
+
|
|
478
|
+
console.log('\n ═══════════════════════════════════════════════════════');
|
|
479
|
+
console.log(' Your AI coding tool should now process the above.');
|
|
480
|
+
console.log(' The analysis uses 8 philosophy lenses for deep insight.');
|
|
481
|
+
console.log(' ═══════════════════════════════════════════════════════');
|
|
482
|
+
console.log(`\n Feedback: hello@syntropicworks.com\n`);
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
function buildAnalysisOutput(profile, prompts, focus, context) {
|
|
486
|
+
const sections = [];
|
|
487
|
+
|
|
488
|
+
sections.push(`# Syntropic Analysis: ${profile.project_name}`);
|
|
489
|
+
sections.push(`*Run this analysis using the instructions below.*\n`);
|
|
490
|
+
|
|
491
|
+
// Product profile context
|
|
492
|
+
sections.push(`## Product Profile\n`);
|
|
493
|
+
sections.push('```json');
|
|
494
|
+
sections.push(JSON.stringify(profile, null, 2));
|
|
495
|
+
sections.push('```\n');
|
|
496
|
+
|
|
497
|
+
if (focus) {
|
|
498
|
+
sections.push(`## Focus Question\n${focus}\n`);
|
|
499
|
+
}
|
|
500
|
+
if (context) {
|
|
501
|
+
sections.push(`## Additional Context\n${context}\n`);
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
// Instructions
|
|
505
|
+
sections.push(`## Instructions\n`);
|
|
506
|
+
sections.push(prompts.instructions);
|
|
507
|
+
sections.push('');
|
|
508
|
+
|
|
509
|
+
// Report prompt
|
|
510
|
+
sections.push(`## Step 1: Generate Analysis Report\n`);
|
|
511
|
+
sections.push(`Use this system prompt to analyse the product profile above:\n`);
|
|
512
|
+
sections.push('---');
|
|
513
|
+
sections.push(prompts.report_prompt);
|
|
514
|
+
sections.push('---\n');
|
|
515
|
+
|
|
516
|
+
// Lens prompts
|
|
517
|
+
sections.push(`## Step 2: Philosophy Lens Deep Dives\n`);
|
|
518
|
+
sections.push(`Run each of the following 8 lenses against the report from Step 1:\n`);
|
|
519
|
+
|
|
520
|
+
for (const key of prompts.lens_order) {
|
|
521
|
+
const lens = prompts.lens_prompts[key];
|
|
522
|
+
if (!lens) continue;
|
|
523
|
+
sections.push(`### ${lens.label}\n`);
|
|
524
|
+
sections.push(lens.prompt);
|
|
525
|
+
sections.push('');
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
// Validation
|
|
529
|
+
sections.push(`## Step 3: Validation Plan\n`);
|
|
530
|
+
sections.push(prompts.validation_prompt);
|
|
531
|
+
sections.push('');
|
|
532
|
+
|
|
533
|
+
return sections.join('\n');
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
module.exports = run;
|
package/commands/config-utils.js
CHANGED
|
@@ -56,6 +56,34 @@ function hmacSign(payload, hmacKey) {
|
|
|
56
56
|
return crypto.createHmac('sha256', hmacKey).update(payload).digest('hex');
|
|
57
57
|
}
|
|
58
58
|
|
|
59
|
+
/**
|
|
60
|
+
* Auth token helpers for CLI authentication
|
|
61
|
+
*/
|
|
62
|
+
function getAuth() {
|
|
63
|
+
const config = loadConfig();
|
|
64
|
+
return config.auth || null;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function saveAuth(auth) {
|
|
68
|
+
const config = loadConfig();
|
|
69
|
+
config.auth = auth;
|
|
70
|
+
saveConfig(config);
|
|
71
|
+
// Set restrictive permissions (owner read/write only)
|
|
72
|
+
try { fs.chmodSync(CONFIG_FILE, 0o600); } catch (_) {}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function clearAuth() {
|
|
76
|
+
const config = loadConfig();
|
|
77
|
+
delete config.auth;
|
|
78
|
+
saveConfig(config);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function isAuthExpired(auth) {
|
|
82
|
+
if (!auth || !auth.expires_at) return true;
|
|
83
|
+
// Expired if within 5 minutes of expiry
|
|
84
|
+
return new Date(auth.expires_at) < new Date(Date.now() + 5 * 60 * 1000);
|
|
85
|
+
}
|
|
86
|
+
|
|
59
87
|
module.exports = {
|
|
60
88
|
CONFIG_DIR,
|
|
61
89
|
CONFIG_FILE,
|
|
@@ -64,4 +92,8 @@ module.exports = {
|
|
|
64
92
|
saveConfig,
|
|
65
93
|
clientHash,
|
|
66
94
|
hmacSign,
|
|
95
|
+
getAuth,
|
|
96
|
+
saveAuth,
|
|
97
|
+
clearAuth,
|
|
98
|
+
isAuthExpired,
|
|
67
99
|
};
|
package/commands/init.js
CHANGED
|
@@ -86,6 +86,33 @@ function replaceInFile(filePath, replacements) {
|
|
|
86
86
|
fs.writeFileSync(filePath, content, 'utf8');
|
|
87
87
|
}
|
|
88
88
|
|
|
89
|
+
function installPostCommitHook(targetDir) {
|
|
90
|
+
const hooksDir = path.join(targetDir, '.git', 'hooks');
|
|
91
|
+
if (!fs.existsSync(hooksDir)) return false; // Not a git repo
|
|
92
|
+
|
|
93
|
+
const hookPath = path.join(hooksDir, 'post-commit');
|
|
94
|
+
if (fs.existsSync(hookPath)) {
|
|
95
|
+
// Don't overwrite existing hook — check if it already has syntropic
|
|
96
|
+
const existing = fs.readFileSync(hookPath, 'utf8');
|
|
97
|
+
if (existing.includes('syntropic report')) return 'exists';
|
|
98
|
+
// Append to existing hook
|
|
99
|
+
fs.appendFileSync(hookPath, '\n# Syntropic PRISM — anonymous cycle reporting\ncommand -v syntropic >/dev/null 2>&1 && syntropic report >/dev/null 2>&1 &\n');
|
|
100
|
+
console.log(` append .git/hooks/post-commit (added PRISM reporting)`);
|
|
101
|
+
return 'appended';
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// Create new hook
|
|
105
|
+
const hookContent = `#!/bin/sh
|
|
106
|
+
# Syntropic PRISM — anonymous cycle reporting
|
|
107
|
+
# Fire-and-forget: never blocks commits, respects telemetry opt-out
|
|
108
|
+
# Disable: syntropic telemetry disable (or delete this hook)
|
|
109
|
+
command -v syntropic >/dev/null 2>&1 && syntropic report >/dev/null 2>&1 &
|
|
110
|
+
`;
|
|
111
|
+
fs.writeFileSync(hookPath, hookContent, { mode: 0o755 });
|
|
112
|
+
console.log(` create .git/hooks/post-commit (PRISM cycle reporting)`);
|
|
113
|
+
return 'created';
|
|
114
|
+
}
|
|
115
|
+
|
|
89
116
|
function hasSyntropicMarker(filePath) {
|
|
90
117
|
if (!fs.existsSync(filePath)) return false;
|
|
91
118
|
return fs.readFileSync(filePath, 'utf8').includes('<!-- syntropic -->');
|
|
@@ -250,13 +277,19 @@ async function run(args) {
|
|
|
250
277
|
// Ensure ~/.syntropic/config.json exists (device_id, hmac_key, telemetry)
|
|
251
278
|
ensureConfig();
|
|
252
279
|
|
|
280
|
+
// Install post-commit hook for PRISM cycle reporting
|
|
281
|
+
const hookResult = installPostCommitHook(targetDir);
|
|
282
|
+
const hookLine = hookResult === 'created' || hookResult === 'appended'
|
|
283
|
+
? '\n .git/hooks/post-commit PRISM cycle reporting (anonymous)'
|
|
284
|
+
: '';
|
|
285
|
+
|
|
253
286
|
console.log(`
|
|
254
287
|
Done! Your project is set up with the Syntropic pipeline.
|
|
255
288
|
|
|
256
289
|
What was created:
|
|
257
290
|
${toolFiles}
|
|
258
291
|
.github/workflows/ Daily health check with auto-remediation
|
|
259
|
-
scripts/health-check.js Local health check script
|
|
292
|
+
scripts/health-check.js Local health check script${hookLine}
|
|
260
293
|
|
|
261
294
|
Tools configured: ${selectedTools.map(t => TOOLS[t].label).join(', ')}
|
|
262
295
|
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* syntropic login
|
|
3
|
+
*
|
|
4
|
+
* Device Code Flow authentication:
|
|
5
|
+
* 1. Request device code from server
|
|
6
|
+
* 2. Open browser to verification URL
|
|
7
|
+
* 3. Poll until user completes auth
|
|
8
|
+
* 4. Store tokens locally
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
const https = require('https');
|
|
12
|
+
const { execFileSync } = require('child_process');
|
|
13
|
+
const { getAuth, saveAuth } = require('./config-utils');
|
|
14
|
+
|
|
15
|
+
const API_BASE = 'https://www.syntropicworks.com';
|
|
16
|
+
|
|
17
|
+
function request(method, path, body) {
|
|
18
|
+
return new Promise((resolve, reject) => {
|
|
19
|
+
const url = new URL(path, API_BASE);
|
|
20
|
+
const options = {
|
|
21
|
+
hostname: url.hostname,
|
|
22
|
+
port: 443,
|
|
23
|
+
path: url.pathname + url.search,
|
|
24
|
+
method,
|
|
25
|
+
headers: { 'Content-Type': 'application/json' },
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
const req = https.request(options, (res) => {
|
|
29
|
+
let data = '';
|
|
30
|
+
res.on('data', (chunk) => { data += chunk; });
|
|
31
|
+
res.on('end', () => {
|
|
32
|
+
try {
|
|
33
|
+
resolve({ status: res.statusCode, data: JSON.parse(data) });
|
|
34
|
+
} catch (e) {
|
|
35
|
+
resolve({ status: res.statusCode, data: {} });
|
|
36
|
+
}
|
|
37
|
+
});
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
req.on('error', reject);
|
|
41
|
+
if (body) req.write(JSON.stringify(body));
|
|
42
|
+
req.end();
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function openBrowser(url) {
|
|
47
|
+
// H5 fix: Use execFileSync to avoid shell injection via URL
|
|
48
|
+
try {
|
|
49
|
+
// macOS
|
|
50
|
+
execFileSync('open', [url], { stdio: 'ignore' });
|
|
51
|
+
return true;
|
|
52
|
+
} catch (_) {
|
|
53
|
+
try {
|
|
54
|
+
// Linux
|
|
55
|
+
execFileSync('xdg-open', [url], { stdio: 'ignore' });
|
|
56
|
+
return true;
|
|
57
|
+
} catch (_) {
|
|
58
|
+
try {
|
|
59
|
+
// Windows — 'start' needs cmd.exe, use 'explorer' instead
|
|
60
|
+
execFileSync('explorer', [url], { stdio: 'ignore' });
|
|
61
|
+
return true;
|
|
62
|
+
} catch (_) {
|
|
63
|
+
return false;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function sleep(ms) {
|
|
70
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
async function run() {
|
|
74
|
+
console.log('\n syntropic login\n');
|
|
75
|
+
|
|
76
|
+
// Check if already authenticated
|
|
77
|
+
const existing = getAuth();
|
|
78
|
+
if (existing && existing.email) {
|
|
79
|
+
console.log(` Already signed in as ${existing.email}`);
|
|
80
|
+
console.log(' Run `syntropic logout` to sign out.\n');
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// Step 1: Request device code
|
|
85
|
+
console.log(' Requesting authentication...');
|
|
86
|
+
const { status, data } = await request('POST', '/api/v1/auth/device', {});
|
|
87
|
+
|
|
88
|
+
if (status !== 200) {
|
|
89
|
+
console.error(` Error: ${data.error || 'Failed to start authentication'}`);
|
|
90
|
+
process.exit(1);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const { device_code, user_code, verification_url, expires_in, interval } = data;
|
|
94
|
+
|
|
95
|
+
// Step 2: Open browser
|
|
96
|
+
console.log(`\n Your code: ${user_code}\n`);
|
|
97
|
+
|
|
98
|
+
const opened = openBrowser(verification_url);
|
|
99
|
+
if (opened) {
|
|
100
|
+
console.log(' Browser opened. Sign in and accept the terms to continue.');
|
|
101
|
+
} else {
|
|
102
|
+
console.log(' Open this URL in your browser to sign in:');
|
|
103
|
+
console.log(` ${verification_url}`);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// Step 3: Poll for completion
|
|
107
|
+
console.log('\n Waiting for authentication...');
|
|
108
|
+
const pollInterval = (interval || 5) * 1000;
|
|
109
|
+
const deadline = Date.now() + (expires_in || 900) * 1000;
|
|
110
|
+
|
|
111
|
+
while (Date.now() < deadline) {
|
|
112
|
+
await sleep(pollInterval);
|
|
113
|
+
|
|
114
|
+
try {
|
|
115
|
+
const poll = await request('POST', '/api/v1/auth/poll', { device_code });
|
|
116
|
+
|
|
117
|
+
if (poll.status === 200) {
|
|
118
|
+
// Success!
|
|
119
|
+
const { access_token, refresh_token, email, tier } = poll.data;
|
|
120
|
+
|
|
121
|
+
// H3 fix: Extract expiry from JWT payload instead of hardcoding
|
|
122
|
+
let expiresAt;
|
|
123
|
+
try {
|
|
124
|
+
const payload = JSON.parse(Buffer.from(access_token.split('.')[1], 'base64').toString());
|
|
125
|
+
expiresAt = new Date(payload.exp * 1000).toISOString();
|
|
126
|
+
} catch (_) {
|
|
127
|
+
expiresAt = new Date(Date.now() + 60 * 60 * 1000).toISOString(); // fallback 1 hour
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
saveAuth({
|
|
131
|
+
access_token,
|
|
132
|
+
refresh_token,
|
|
133
|
+
email,
|
|
134
|
+
tier: tier || 'cli_trial',
|
|
135
|
+
expires_at: expiresAt,
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
console.log(` ✓ Authenticated as ${email} (${tier || 'CLI Trial'})\n`);
|
|
139
|
+
console.log(' Run `syntropic analyse` to get started.\n');
|
|
140
|
+
return;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
if (poll.status === 410) {
|
|
144
|
+
console.error('\n Authentication expired. Run `syntropic login` to try again.\n');
|
|
145
|
+
process.exit(1);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// 202 = still pending, continue polling
|
|
149
|
+
process.stdout.write('.');
|
|
150
|
+
} catch (err) {
|
|
151
|
+
// Network error — retry
|
|
152
|
+
process.stdout.write('x');
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
console.error('\n Authentication timed out. Run `syntropic login` to try again.\n');
|
|
157
|
+
process.exit(1);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
module.exports = run;
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* syntropic logout
|
|
3
|
+
*
|
|
4
|
+
* Clears stored authentication tokens.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
const { getAuth, clearAuth } = require('./config-utils');
|
|
8
|
+
|
|
9
|
+
async function run() {
|
|
10
|
+
const auth = getAuth();
|
|
11
|
+
|
|
12
|
+
if (!auth || !auth.email) {
|
|
13
|
+
console.log('\n Not signed in.\n');
|
|
14
|
+
return;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const email = auth.email;
|
|
18
|
+
clearAuth();
|
|
19
|
+
console.log(`\n Signed out (${email}).\n`);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
module.exports = run;
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* syntropic whoami
|
|
3
|
+
*
|
|
4
|
+
* Shows current authentication status.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
const { getAuth, isAuthExpired } = require('./config-utils');
|
|
8
|
+
|
|
9
|
+
async function run() {
|
|
10
|
+
const auth = getAuth();
|
|
11
|
+
|
|
12
|
+
if (!auth || !auth.email) {
|
|
13
|
+
console.log('\n Not signed in. Run `syntropic login` to authenticate.\n');
|
|
14
|
+
return;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const expired = isAuthExpired(auth);
|
|
18
|
+
const tierLabel = {
|
|
19
|
+
cli_trial: 'CLI Trial',
|
|
20
|
+
beta: 'Beta',
|
|
21
|
+
pro: 'Pro',
|
|
22
|
+
free: 'Free',
|
|
23
|
+
}[auth.tier] || auth.tier;
|
|
24
|
+
|
|
25
|
+
console.log(`\n ${auth.email} (${tierLabel})${expired ? ' — token expired, run `syntropic login` to refresh' : ''}\n`);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
module.exports = run;
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "syntropic",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "Philosophy-as-code development pipeline for Claude Code, Cursor, Windsurf, GitHub Copilot, and OpenAI Codex.",
|
|
3
|
+
"version": "0.8.1",
|
|
4
|
+
"description": "Philosophy-as-code development pipeline with market analysis for Claude Code, Cursor, Windsurf, GitHub Copilot, and OpenAI Codex.",
|
|
5
5
|
"bin": {
|
|
6
6
|
"syntropic": "./bin/syntropic.js"
|
|
7
7
|
},
|
|
@@ -19,6 +19,8 @@
|
|
|
19
19
|
"skill-md",
|
|
20
20
|
"ai-development",
|
|
21
21
|
"dev-pipeline",
|
|
22
|
+
"market-analysis",
|
|
23
|
+
"startup-validation",
|
|
22
24
|
"syntropic"
|
|
23
25
|
],
|
|
24
26
|
"author": "Syntropic Works <hello@syntropicworks.com>",
|