chati-dev 4.0.4 → 4.0.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/bin/chati.js CHANGED
@@ -338,6 +338,11 @@ Intelligence:
338
338
  npx chati-dev registry [stats|check] Entity registry
339
339
  npx chati-dev health System health check
340
340
 
341
+ License:
342
+ npx chati-dev activate [--key=CHATI-XXXX] Activate license on this machine
343
+ npx chati-dev deactivate Remove license key locally
344
+ npx chati-dev license Show license status
345
+
341
346
  Telemetry:
342
347
  npx chati-dev telemetry Show telemetry status
343
348
  npx chati-dev telemetry enable Enable anonymous telemetry
@@ -347,6 +352,32 @@ Telemetry:
347
352
  break;
348
353
  }
349
354
 
355
+ case 'activate': {
356
+ const keyFlag = args.find(a => a.startsWith('--key='));
357
+ const keyArg = keyFlag ? keyFlag.replace('--key=', '') : null;
358
+ const { runActivate } = await import('../src/license/commands.js');
359
+ await runActivate(targetDir, keyArg);
360
+ break;
361
+ }
362
+
363
+ case 'deactivate': {
364
+ const { runDeactivate } = await import('../src/license/commands.js');
365
+ runDeactivate(targetDir);
366
+ break;
367
+ }
368
+
369
+ case 'license': {
370
+ const licSubCmd = args[1] || 'status';
371
+ if (licSubCmd === 'deactivate') {
372
+ const { runDeactivate } = await import('../src/license/commands.js');
373
+ runDeactivate(targetDir);
374
+ } else {
375
+ const { runLicenseStatus } = await import('../src/license/commands.js');
376
+ await runLicenseStatus(targetDir);
377
+ }
378
+ break;
379
+ }
380
+
350
381
  default: {
351
382
  console.error(`Unknown command: ${command}`);
352
383
  console.error("Run 'npx chati-dev --help' for usage.");
@@ -0,0 +1,140 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * License Guard Hook — UserPromptSubmit
4
+ *
5
+ * Validates license status before each user prompt turn.
6
+ * Uses a 24h cache stored in ~/.chati-dev/license.yaml (global, all projects).
7
+ *
8
+ * Behavior:
9
+ * - No key configured → allow (orchestrator handles inline activation)
10
+ * - Cache VALID < 24h → allow silently
11
+ * - Cache EXPIRED/INVALID < 24h → block with renewal message
12
+ * - Cache stale (> 24h) → call API, refresh cache, then allow or block
13
+ * - API unreachable → fail open (allow), never block work
14
+ */
15
+
16
+ import { existsSync, readFileSync, writeFileSync, mkdirSync } from 'fs';
17
+ import { join } from 'path';
18
+ import { homedir } from 'os';
19
+
20
+ const API_BASE = 'https://chati.dev/api';
21
+ const CACHE_TTL_MS = 24 * 60 * 60 * 1000; // 24 hours
22
+
23
+ const GLOBAL_DIR = join(homedir(), '.chati-dev');
24
+ const LICENSE_PATH = join(GLOBAL_DIR, 'license.yaml');
25
+
26
+ async function main() {
27
+ let input = '';
28
+ for await (const chunk of process.stdin) input += chunk;
29
+
30
+ try {
31
+ // Read global license file
32
+ if (!existsSync(LICENSE_PATH)) {
33
+ allow(); // No license configured — orchestrator handles inline activation
34
+ return;
35
+ }
36
+
37
+ const licenseRaw = readFileSync(LICENSE_PATH, 'utf-8');
38
+ const licenseKey = readYamlField(licenseRaw, 'key');
39
+
40
+ if (!licenseKey || licenseKey === 'null') {
41
+ allow();
42
+ return;
43
+ }
44
+
45
+ // Check cache validity
46
+ const status = readYamlField(licenseRaw, 'status');
47
+ const checkedAt = readYamlField(licenseRaw, 'checked_at');
48
+ const age = checkedAt ? Date.now() - new Date(checkedAt).getTime() : Infinity;
49
+
50
+ if (age < CACHE_TTL_MS) {
51
+ if (status === 'VALID') { allow(); return; }
52
+ if (status === 'EXPIRED' || status === 'INVALID') {
53
+ block(buildMessage(status, readYamlField(licenseRaw, 'reason')));
54
+ return;
55
+ }
56
+ }
57
+
58
+ // Cache stale — call API
59
+ try {
60
+ const machineId = await computeMachineId();
61
+ const res = await fetch(
62
+ `${API_BASE}/license/validate?key=${encodeURIComponent(licenseKey)}&machine_id=${encodeURIComponent(machineId)}`,
63
+ { signal: AbortSignal.timeout(5000) }
64
+ );
65
+ const data = await res.json();
66
+
67
+ // Persist updated status (keep existing key)
68
+ const existing = parseYaml(licenseRaw);
69
+ const updated = {
70
+ ...existing,
71
+ status: data.status,
72
+ plan: data.plan ?? '',
73
+ days_remaining: data.days_remaining ?? '',
74
+ expires_at: data.expires_at ?? '',
75
+ reason: data.reason ?? '',
76
+ checked_at: new Date().toISOString(),
77
+ };
78
+ mkdirSync(GLOBAL_DIR, { recursive: true });
79
+ writeFileSync(LICENSE_PATH, dumpYaml(updated));
80
+
81
+ if (data.status === 'VALID') { allow(); return; }
82
+ block(buildMessage(data.status, data.reason));
83
+ } catch {
84
+ allow(); // Fail open — API unreachable
85
+ }
86
+ } catch {
87
+ allow(); // Parse error — fail open
88
+ }
89
+ }
90
+
91
+ function allow() {
92
+ process.stdout.write(JSON.stringify({ decision: 'allow' }));
93
+ }
94
+
95
+ function block(reason) {
96
+ process.stdout.write(JSON.stringify({ decision: 'block', reason }));
97
+ }
98
+
99
+ function buildMessage(status, reason) {
100
+ if (status === 'EXPIRED') {
101
+ return `chati.dev license expired. Renew at https://chati.dev/pricing\nAfter renewing: npx chati-dev activate --key=YOUR-KEY`;
102
+ }
103
+ return `chati.dev license invalid${reason ? ` (${reason})` : ''}.\nRun: npx chati-dev activate --key=YOUR-KEY\nOr visit https://chati.dev/pricing`;
104
+ }
105
+
106
+ function readYamlField(raw, field) {
107
+ const match = raw.match(new RegExp(`^${field}:\\s*(.+)$`, 'm'));
108
+ return match ? match[1].trim().replace(/^["']|["']$/g, '') : null;
109
+ }
110
+
111
+ /** Minimal YAML object parser for simple key:value files */
112
+ function parseYaml(raw) {
113
+ const obj = {};
114
+ for (const line of raw.split('\n')) {
115
+ const m = line.match(/^(\w+):\s*(.*)$/);
116
+ if (m) obj[m[1]] = m[2].trim().replace(/^["']|["']$/g, '');
117
+ }
118
+ return obj;
119
+ }
120
+
121
+ /** Minimal YAML serializer for simple flat objects */
122
+ function dumpYaml(obj) {
123
+ return Object.entries(obj)
124
+ .map(([k, v]) => `${k}: ${v == null ? '' : String(v).includes(':') ? `'${v}'` : v}`)
125
+ .join('\n') + '\n';
126
+ }
127
+
128
+ async function computeMachineId() {
129
+ const { createHash } = await import('crypto');
130
+ const os = await import('os');
131
+ const ifaces = Object.values(os.networkInterfaces()).flat();
132
+ const mac = ifaces.find(i => i && !i.internal && i.mac !== '00:00:00:00:00:00')?.mac ?? 'no-mac';
133
+ const raw = [os.hostname(), os.platform(), os.arch(), os.cpus()[0]?.model ?? '', os.userInfo().username, mac].join('|');
134
+ return createHash('sha256').update(raw).digest('hex').substring(0, 16);
135
+ }
136
+
137
+ import { fileURLToPath } from 'url';
138
+ if (process.argv[1] === fileURLToPath(import.meta.url)) {
139
+ main();
140
+ }
@@ -5,6 +5,10 @@
5
5
  {
6
6
  "matcher": ".*",
7
7
  "hooks": [
8
+ {
9
+ "type": "command",
10
+ "command": "node chati.dev/hooks/license-guard.js"
11
+ },
8
12
  {
9
13
  "type": "command",
10
14
  "command": "node chati.dev/hooks/prism-engine.js"
@@ -18,6 +18,39 @@ You are the **Chati.dev Orchestrator**, the single entry point for the Chati.dev
18
18
 
19
19
  When the user invokes `/chati`, execute this sequence:
20
20
 
21
+ ### Pre-Flight: License Check
22
+
23
+ FIRST — before anything else:
24
+
25
+ ```
26
+ 1. Read ~/.chati-dev/license.yaml
27
+
28
+ If file is MISSING or status = "MISSING":
29
+ Say:
30
+ "Welcome to chati.dev! Before we start, I need to activate your license.
31
+
32
+ Option 1 — Paste your license key below.
33
+ Option 2 — Get a free 14-day trial: https://chati.dev/pricing
34
+
35
+ Enter your key (or press Enter if you just received one via email):"
36
+
37
+ → User provides key
38
+ → Run: `npx chati-dev activate --key={key}`
39
+ → Re-read ~/.chati-dev/license.yaml
40
+ → If status = VALID: "✓ Activated! Let's build." → proceed to Step 1
41
+ → If error: show error message, ask to try again (max 3 attempts, then stop)
42
+
43
+ If status = "EXPIRED":
44
+ Say:
45
+ "Your chati.dev license has expired.
46
+ Renew at https://chati.dev/pricing
47
+ After renewing, run: npx chati-dev activate --key=YOUR-KEY"
48
+ → STOP. Do not proceed.
49
+
50
+ If status = "VALID":
51
+ → Proceed silently. Zero mention of license in normal flow.
52
+ ```
53
+
21
54
  ### Step 1: Load Context
22
55
  ```
23
56
  1. Read .chati/session.yaml
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "chati-dev",
3
- "version": "4.0.4",
3
+ "version": "4.0.6",
4
4
  "description": "AI-Powered Multi-Agent Orchestration System — Structured vibe coding for Full Stack Development",
5
5
  "type": "module",
6
6
  "bin": {
@@ -0,0 +1,138 @@
1
+ import { homedir } from 'os';
2
+ import { existsSync, readFileSync, writeFileSync, mkdirSync } from 'fs';
3
+ import { join } from 'path';
4
+ import yaml from 'js-yaml';
5
+ import { getMachineId, getMachineName } from './machine-id.js';
6
+
7
+ const API_BASE = 'https://chati.dev/api';
8
+ export const CACHE_TTL_MS = 24 * 60 * 60 * 1000; // 24h
9
+
10
+ // ---------------------------------------------------------------------------
11
+ // Global storage — ~/.chati-dev/license.yaml
12
+ // One activation covers ALL projects on this machine.
13
+ // ---------------------------------------------------------------------------
14
+
15
+ export function getGlobalDir() {
16
+ return join(homedir(), '.chati-dev');
17
+ }
18
+
19
+ export function getGlobalLicensePath() {
20
+ return join(getGlobalDir(), 'license.yaml');
21
+ }
22
+
23
+ function readGlobal() {
24
+ const p = getGlobalLicensePath();
25
+ if (!existsSync(p)) return {};
26
+ try { return yaml.load(readFileSync(p, 'utf-8')) || {}; } catch { return {}; }
27
+ }
28
+
29
+ function writeGlobal(data) {
30
+ mkdirSync(getGlobalDir(), { recursive: true });
31
+ writeFileSync(getGlobalLicensePath(), yaml.dump(data, { lineWidth: -1 }));
32
+ }
33
+
34
+ // ---------------------------------------------------------------------------
35
+ // Key helpers
36
+ // ---------------------------------------------------------------------------
37
+
38
+ /** Returns license key or null. projectDir param accepted but unused (global storage). */
39
+ export function getLicenseKey(_projectDir) {
40
+ const key = readGlobal().key;
41
+ return key && key !== 'null' ? key : null;
42
+ }
43
+
44
+ export function setLicenseKey(_projectDir, key) {
45
+ const data = readGlobal();
46
+ data.key = key ?? null;
47
+ writeGlobal(data);
48
+ }
49
+
50
+ // ---------------------------------------------------------------------------
51
+ // Status cache (stored in same global file)
52
+ // ---------------------------------------------------------------------------
53
+
54
+ export function getLicenseStatus(_projectDir) {
55
+ const data = readGlobal();
56
+ return Object.keys(data).length ? data : null;
57
+ }
58
+
59
+ export function saveLicenseStatus(_projectDir, status) {
60
+ const existing = readGlobal();
61
+ writeGlobal({ ...existing, ...status });
62
+ }
63
+
64
+ // ---------------------------------------------------------------------------
65
+ // API operations
66
+ // ---------------------------------------------------------------------------
67
+
68
+ /**
69
+ * Activate a license key on this machine.
70
+ * Saves key + status to ~/.chati-dev/license.yaml.
71
+ * projectDir param accepted but unused.
72
+ */
73
+ export async function activateLicense(_projectDir, key) {
74
+ const machineId = getMachineId();
75
+ const machineName = getMachineName();
76
+
77
+ const res = await fetch(`${API_BASE}/license/activate`, {
78
+ method: 'POST',
79
+ headers: { 'Content-Type': 'application/json' },
80
+ body: JSON.stringify({ key, machine_id: machineId, machine_name: machineName }),
81
+ signal: AbortSignal.timeout(10000),
82
+ });
83
+
84
+ const data = await res.json();
85
+ if (!res.ok) throw new Error(data.error || 'Activation failed');
86
+
87
+ writeGlobal({
88
+ key,
89
+ status: 'VALID',
90
+ plan: data.plan ?? 'trial',
91
+ days_remaining: data.days_remaining ?? null,
92
+ expires_at: data.expires_at ?? null,
93
+ checked_at: new Date().toISOString(),
94
+ });
95
+
96
+ return data;
97
+ }
98
+
99
+ /**
100
+ * Validate current license against API and refresh cache.
101
+ */
102
+ export async function validateLicense(_projectDir) {
103
+ const key = getLicenseKey();
104
+ if (!key) return { status: 'MISSING' };
105
+
106
+ const machineId = getMachineId();
107
+ const res = await fetch(
108
+ `${API_BASE}/license/validate?key=${encodeURIComponent(key)}&machine_id=${encodeURIComponent(machineId)}`,
109
+ { signal: AbortSignal.timeout(5000) }
110
+ );
111
+ const data = await res.json();
112
+
113
+ const result = {
114
+ key,
115
+ status: data.status,
116
+ plan: data.plan ?? null,
117
+ days_remaining: data.days_remaining ?? null,
118
+ expires_at: data.expires_at ?? null,
119
+ reason: data.reason ?? null,
120
+ checked_at: new Date().toISOString(),
121
+ };
122
+
123
+ writeGlobal(result);
124
+ return result;
125
+ }
126
+
127
+ /**
128
+ * Remove license locally (does not deactivate machine slot on server).
129
+ */
130
+ export function deactivateLicense(_projectDir) {
131
+ const data = readGlobal();
132
+ writeGlobal({
133
+ ...data,
134
+ key: null,
135
+ status: 'MISSING',
136
+ checked_at: new Date().toISOString(),
137
+ });
138
+ }
@@ -0,0 +1,96 @@
1
+ /**
2
+ * License CLI command handlers.
3
+ * Used by bin/chati.js for: activate, deactivate, license
4
+ */
5
+
6
+ import { text, isCancel } from '@clack/prompts';
7
+ import { activateLicense, deactivateLicense, getLicenseKey, getLicenseStatus, validateLicense } from './client.js';
8
+
9
+ // ---------------------------------------------------------------------------
10
+ // activate
11
+ // ---------------------------------------------------------------------------
12
+
13
+ export async function runActivate(projectDir, keyArg) {
14
+ let key = keyArg;
15
+
16
+ if (!key) {
17
+ const input = await text({
18
+ message: 'Enter your license key (format: CHATI-XXXX-XXXX-XXXX):',
19
+ placeholder: 'CHATI-XXXX-XXXX-XXXX',
20
+ validate(v) {
21
+ if (!v || !v.trim()) return 'Key is required';
22
+ if (!v.trim().startsWith('CHATI-')) return 'Key must start with CHATI-';
23
+ return undefined;
24
+ },
25
+ });
26
+ if (isCancel(input)) {
27
+ console.log('Activation cancelled.');
28
+ return;
29
+ }
30
+ key = input.trim();
31
+ }
32
+
33
+ console.log(`Activating ${key}...`);
34
+
35
+ try {
36
+ const result = await activateLicense(projectDir, key);
37
+ const plan = result.plan ?? 'trial';
38
+ const days = result.days_remaining ?? '?';
39
+ console.log(`\n✓ Activated! Plan: ${plan} — ${days} day(s) remaining.`);
40
+ console.log(` Key saved to chati.dev/config.yaml`);
41
+ console.log(` Run /chati to start.\n`);
42
+ } catch (err) {
43
+ console.error(`\n✗ Activation failed: ${err.message}`);
44
+ console.error(` Visit https://chati.dev/pricing to get a key.\n`);
45
+ process.exit(1);
46
+ }
47
+ }
48
+
49
+ // ---------------------------------------------------------------------------
50
+ // deactivate
51
+ // ---------------------------------------------------------------------------
52
+
53
+ export function runDeactivate(projectDir) {
54
+ const key = getLicenseKey(projectDir);
55
+ if (!key) {
56
+ console.log('No license key found locally. Nothing to remove.');
57
+ return;
58
+ }
59
+ deactivateLicense(projectDir);
60
+ console.log(`\n✓ License key removed locally.`);
61
+ console.log(` To free up a machine slot, visit https://chati.dev/dashboard\n`);
62
+ }
63
+
64
+ // ---------------------------------------------------------------------------
65
+ // license status
66
+ // ---------------------------------------------------------------------------
67
+
68
+ export async function runLicenseStatus(projectDir) {
69
+ const key = getLicenseKey(projectDir);
70
+
71
+ if (!key) {
72
+ console.log('\nLicense: Not activated');
73
+ console.log(' Get a free 14-day trial at https://chati.dev/pricing\n');
74
+ return;
75
+ }
76
+
77
+ console.log(`\nChecking license...`);
78
+
79
+ let status;
80
+ try {
81
+ status = await validateLicense(projectDir);
82
+ } catch {
83
+ // Fallback to cache
84
+ status = getLicenseStatus(projectDir) ?? { status: 'UNKNOWN' };
85
+ }
86
+
87
+ console.log('\nLicense Status');
88
+ console.log('='.repeat(30));
89
+ console.log(` Key: ${key}`);
90
+ console.log(` Status: ${status.status}`);
91
+ if (status.plan) console.log(` Plan: ${status.plan}`);
92
+ if (status.days_remaining != null) console.log(` Days remaining: ${status.days_remaining}`);
93
+ if (status.expires_at) console.log(` Expires: ${status.expires_at}`);
94
+ if (status.reason) console.log(` Reason: ${status.reason}`);
95
+ console.log();
96
+ }
@@ -0,0 +1,24 @@
1
+ import os from 'os';
2
+ import crypto from 'crypto';
3
+
4
+ /**
5
+ * Generates a stable 16-char machine fingerprint.
6
+ * Based on: hostname, platform, arch, CPU model, username, MAC address.
7
+ */
8
+ export function getMachineId() {
9
+ const ifaces = Object.values(os.networkInterfaces()).flat();
10
+ const mac = ifaces.find(i => i && !i.internal && i.mac !== '00:00:00:00:00:00')?.mac ?? 'no-mac';
11
+ const raw = [
12
+ os.hostname(),
13
+ os.platform(),
14
+ os.arch(),
15
+ os.cpus()[0]?.model ?? '',
16
+ os.userInfo().username,
17
+ mac,
18
+ ].join('|');
19
+ return crypto.createHash('sha256').update(raw).digest('hex').substring(0, 16);
20
+ }
21
+
22
+ export function getMachineName() {
23
+ return os.hostname();
24
+ }
@@ -9,7 +9,7 @@
9
9
  // Default Endpoint
10
10
  // ---------------------------------------------------------------------------
11
11
 
12
- export const DEFAULT_ENDPOINT = 'https://chati-telemetry.vercel.app/api/events';
12
+ export const DEFAULT_ENDPOINT = 'https://chati.dev/api/telemetry';
13
13
 
14
14
  // ---------------------------------------------------------------------------
15
15
  // Sender
@@ -11,6 +11,7 @@ import { initCollector, track as telemetryTrack, flush as telemetryFlush } from
11
11
  import { sendEvents } from '../telemetry/sender.js';
12
12
  import { getTelemetryConfig } from '../telemetry/config.js';
13
13
  import { t } from './i18n.js';
14
+ import { getLicenseKey, activateLicense } from '../license/client.js';
14
15
  import { DEFAULT_MCPS } from '../config/mcp-configs.js';
15
16
  import { IDE_CONFIGS, IDE_TO_PROVIDER } from '../config/ide-configs.js';
16
17
 
@@ -29,11 +30,14 @@ export async function runWizard(targetDir, options = {}) {
29
30
  logoText = 'chati.dev';
30
31
  }
31
32
 
32
- // Step 1: Logo + Language Selection (in English)
33
+ // Step 1: Logo + License Activation (first thing, before anything else)
33
34
  logBanner(logoText, VERSION);
34
35
 
35
36
  p.intro('Setting up chati.dev');
36
37
 
38
+ await runLicenseActivationStep();
39
+
40
+ // Step 2: Language Selection
37
41
  const language = options.language || await stepLanguage();
38
42
 
39
43
  // Step 2: Terms of Use (must accept to continue)
@@ -195,3 +199,37 @@ export async function runWizard(targetDir, options = {}) {
195
199
  return { success: false, error: err.message };
196
200
  }
197
201
  }
202
+
203
+ async function runLicenseActivationStep() {
204
+ const existingKey = getLicenseKey();
205
+ if (existingKey) return; // already activated, proceed silently
206
+
207
+ console.log('\n ┌─ License Activation ─────────────────────────────────────────┐');
208
+ console.log(' │ Your license key works across ALL projects on this machine. │');
209
+ console.log(' │ Get a free 14-day trial at: https://chati.dev/pricing │');
210
+ console.log(' └──────────────────────────────────────────────────────────────┘\n');
211
+
212
+ const keyInput = await p.text({
213
+ message: 'Enter your license key (press Enter to skip — activate later with: npx chati-dev activate):',
214
+ placeholder: 'CHATI-XXXX-XXXX-XXXX',
215
+ });
216
+
217
+ if (p.isCancel(keyInput) || !keyInput?.trim()) {
218
+ console.log(' Skipped. Run: npx chati-dev activate --key=YOUR-KEY when ready.\n');
219
+ return;
220
+ }
221
+
222
+ const activateSpinner = createSpinner('Activating license...');
223
+ activateSpinner.start();
224
+ try {
225
+ const result = await activateLicense(null, keyInput.trim());
226
+ activateSpinner.stop();
227
+ const plan = result.plan ?? 'trial';
228
+ const days = result.days_remaining ?? '?';
229
+ console.log(` ✓ License activated! Plan: ${plan} — ${days} day(s) remaining.\n`);
230
+ } catch (err) {
231
+ activateSpinner.stop();
232
+ console.log(` ✗ Activation failed: ${err.message}`);
233
+ console.log(' Run: npx chati-dev activate --key=YOUR-KEY to try again.\n');
234
+ }
235
+ }