chati-dev 4.0.3 → 4.0.5

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.");
@@ -54,7 +54,32 @@ Implement each task from the approved task breakdown with high quality, followin
54
54
  ```
55
55
  For each task:
56
56
  1. Announce: "Starting T{X}: {title} — implementing now..."
57
- 2. Read task details and acceptance criteria
57
+ 1.5. Pre-Flight Spec Check (BEFORE any code):
58
+ a. Validate Given-When-Then criteria:
59
+ → Are all criteria specific and measurable? (not "works correctly", "looks good")
60
+ → If ANY criterion is vague/untestable → STOP. Escalate G01: "{criterion} is ambiguous."
61
+ → If task has NO criteria → STOP. Escalate G01: "T{X} has no testable acceptance criteria."
62
+ b. Check dependencies:
63
+ → For each T{x}.{y} in Dependencies: is it marked complete in tasks.md?
64
+ → If dependency incomplete → STOP. Escalate: "Cannot start T{X}: T{dep} not yet complete."
65
+ c. Set implementation strategy from task size:
66
+ → XS/S: standard flow — proceed to Step 2
67
+ → M (2-4h): output brief implementation outline (files + approach), then proceed
68
+ → L (4-8h): output full implementation plan, ask "[ready/clarify/skip]", wait for user
69
+ RULE: NEVER write code before Step 1.5 passes. If spec is unclear, fix the spec first.
70
+ OUTPUT: "Spec check passed. T{X} is {size} — {strategy}."
71
+ 2. Read task details, acceptance criteria, and verify architectural alignment:
72
+ Before coding, cross-check against chati.dev/artifacts/3-Architecture/architecture.md:
73
+ - API tasks → endpoint pattern, response format, error handling contract match Section 4?
74
+ - Database tasks → table/column names, relationships match Section 5 (Data Model)?
75
+ - Auth tasks → auth approach matches Section 6 (Authentication)?
76
+ - New modules/components → file structure matches Section 3 (System Components)?
77
+ If conflict detected → STOP. Escalate G05:
78
+ "T{X} conflicts with architecture decision: {specific conflict}.
79
+ Implement per architecture.md or per task spec?"
80
+ RULE: architecture.md is the source of truth. NEVER implement against architectural decisions.
81
+ RULE: Do NOT silently reconcile conflicts — always surface them.
82
+ If no architecture.md present → proceed with best practices, note in handoff.
58
83
  3. Implement code
59
84
  -> Output: "Implementation done. Running self-critique (5.5)..."
60
85
  4. Run self-critique (Step 5.5) — 1 fix pass, then proceed
@@ -89,7 +114,17 @@ WHILE tasks_pending:
89
114
 
90
115
  FOR attempt IN 1..3:
91
116
  1. Read task details and acceptance criteria
92
- 2. Implement code
117
+ 1.5. Pre-Flight Spec Check:
118
+ → If ANY criterion is vague/untestable → mark task blocked (G01), skip to next task
119
+ → If task has no Given-When-Then criteria → mark task blocked (G01), skip to next
120
+ → If dependency not complete → mark task blocked, skip to next independent task
121
+ → M-size tasks: output implementation outline before coding
122
+ → L-size tasks: output full implementation plan before coding (no user confirmation)
123
+ 2. Verify architectural alignment (cross-check architecture.md before coding):
124
+ → API/DB/Auth/module tasks: verify patterns match architecture.md
125
+ → If conflict → mark task blocked (G05), skip to next task
126
+ → If no architecture.md → proceed with best practices
127
+ 3. Implement code
93
128
  -> Output: "T{X} implementation done. Self-critique (5.5)..."
94
129
  3. Run self-critique (Step 5.5) — 1 fix pass, then proceed
95
130
  -> Output: "T{X} critique done. Running tests..."
@@ -282,9 +317,11 @@ Criteria:
282
317
  6. No lint errors
283
318
  7. Self-critique (5.5 + 6.5) completed
284
319
  8. No blockers remaining
320
+ 9. Pre-flight spec check passed: criteria were specific and testable before coding started
321
+ 10. Architectural alignment verified: no G05 conflicts, or G05 explicitly resolved before coding
285
322
 
286
323
  Score = criteria met / total criteria
287
- Threshold: >= 95% per task
324
+ Threshold: >= 95% per task (minimum 9/10)
288
325
  ```
289
326
 
290
327
  ---
@@ -325,7 +362,7 @@ agents:
325
362
  dev:
326
363
  status: in_progress | completed
327
364
  score: {average across all tasks}
328
- criteria_count: {total criteria across all tasks}
365
+ criteria_count: 10
329
366
  completed_at: "{timestamp when all tasks done}"
330
367
  ```
331
368
 
@@ -1,8 +1,8 @@
1
1
  # chati.dev Configuration
2
- version: "4.0.3"
2
+ version: "4.0.4"
3
3
  installed_at: "2026-02-07T10:00:00Z"
4
- updated_at: "2026-03-21T00:00:00Z"
5
- installer_version: "4.0.3"
4
+ updated_at: "2026-03-22T00:00:00Z"
5
+ installer_version: "4.0.4"
6
6
  project_type: greenfield
7
7
  language: en
8
8
  ides: [claude-code]
@@ -48,6 +48,19 @@ rules:
48
48
  text: "Iterate on implementation until acceptance criteria are met."
49
49
  priority: normal
50
50
 
51
+ - id: dev-spec-intake
52
+ text: "MUST validate task spec BEFORE writing code: verify all Given-When-Then criteria are specific and testable, check all task dependencies are complete. Escalate G01 immediately for ambiguous criteria, G02 for conflicting criteria — NEVER start coding with unclear spec."
53
+ priority: critical
54
+
55
+ - id: dev-size-strategy
56
+ text: "MUST adapt implementation approach by task size: XS/S = standard flow; M (2-4h) = output implementation outline before coding; L (4-8h) = output implementation plan and ask user confirmation before starting (autonomous mode: output plan and proceed without confirmation)."
57
+ priority: high
58
+
59
+ - id: dev-arch-coherence
60
+ text: "MUST verify implementation aligns with architecture.md before coding any API endpoints, database schemas, auth flows, or new modules. Escalate G05 immediately if conflict detected. NEVER implement against architectural decisions — always surface conflicts to user."
61
+ priority: high
62
+
51
63
  # Provider Preference (v3.0.0)
52
- - dev-provider: "Default provider: claude. Can run on gemini for large codebase tasks (1M context window)."
64
+ - id: dev-provider
65
+ text: "Default provider: claude. Can run on gemini for large codebase tasks (1M context window)."
53
66
  priority: normal
@@ -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.3",
3
+ "version": "4.0.5",
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
 
@@ -153,6 +154,9 @@ export async function runWizard(targetDir, options = {}) {
153
154
  console.log();
154
155
  p.outro(t('installer.success'));
155
156
 
157
+ // License activation step
158
+ await runLicenseActivationStep();
159
+
156
160
  // Show quick start — same experience across all providers
157
161
  const invokeCmdMap = {
158
162
  'codex-cli': '$chati',
@@ -195,3 +199,40 @@ 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) {
206
+ console.log(`\n ✓ License already active on this machine (${existingKey.substring(0, 12)}...)\n`);
207
+ return;
208
+ }
209
+
210
+ console.log('\n ┌─ License Activation ─────────────────────────────────────────┐');
211
+ console.log(' │ Your license key works across ALL projects on this machine. │');
212
+ console.log(' │ Get a free 14-day trial at: https://chati.dev/pricing │');
213
+ console.log(' └──────────────────────────────────────────────────────────────┘\n');
214
+
215
+ const keyInput = await p.text({
216
+ message: 'Enter your license key (press Enter to skip — activate later with: npx chati-dev activate):',
217
+ placeholder: 'CHATI-XXXX-XXXX-XXXX',
218
+ });
219
+
220
+ if (p.isCancel(keyInput) || !keyInput?.trim()) {
221
+ console.log(' Skipped. Run: npx chati-dev activate --key=YOUR-KEY when ready.\n');
222
+ return;
223
+ }
224
+
225
+ const activateSpinner = createSpinner('Activating license...');
226
+ activateSpinner.start();
227
+ try {
228
+ const result = await activateLicense(null, keyInput.trim());
229
+ activateSpinner.stop();
230
+ const plan = result.plan ?? 'trial';
231
+ const days = result.days_remaining ?? '?';
232
+ console.log(` ✓ License activated! Plan: ${plan} — ${days} day(s) remaining.\n`);
233
+ } catch (err) {
234
+ activateSpinner.stop();
235
+ console.log(` ✗ Activation failed: ${err.message}`);
236
+ console.log(' Run: npx chati-dev activate --key=YOUR-KEY to try again.\n');
237
+ }
238
+ }