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 +31 -0
- package/framework/agents/build/dev.md +41 -4
- package/framework/config.yaml +3 -3
- package/framework/domains/agents/dev.yaml +14 -1
- package/framework/hooks/license-guard.js +140 -0
- package/framework/hooks/settings.json +4 -0
- package/framework/orchestrator/chati.md +33 -0
- package/package.json +1 -1
- package/src/license/client.js +138 -0
- package/src/license/commands.js +96 -0
- package/src/license/machine-id.js +24 -0
- package/src/telemetry/sender.js +1 -1
- package/src/wizard/index.js +41 -0
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
|
-
|
|
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
|
-
|
|
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:
|
|
365
|
+
criteria_count: 10
|
|
329
366
|
completed_at: "{timestamp when all tasks done}"
|
|
330
367
|
```
|
|
331
368
|
|
package/framework/config.yaml
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
# chati.dev Configuration
|
|
2
|
-
version: "4.0.
|
|
2
|
+
version: "4.0.4"
|
|
3
3
|
installed_at: "2026-02-07T10:00:00Z"
|
|
4
|
-
updated_at: "2026-03-
|
|
5
|
-
installer_version: "4.0.
|
|
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
|
|
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
|
+
}
|
|
@@ -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
|
@@ -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
|
+
}
|
package/src/telemetry/sender.js
CHANGED
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
// Default Endpoint
|
|
10
10
|
// ---------------------------------------------------------------------------
|
|
11
11
|
|
|
12
|
-
export const DEFAULT_ENDPOINT = 'https://chati
|
|
12
|
+
export const DEFAULT_ENDPOINT = 'https://chati.dev/api/telemetry';
|
|
13
13
|
|
|
14
14
|
// ---------------------------------------------------------------------------
|
|
15
15
|
// Sender
|
package/src/wizard/index.js
CHANGED
|
@@ -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
|
+
}
|