chati-dev 4.0.7 → 4.0.9

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.
@@ -7,21 +7,23 @@
7
7
  *
8
8
  * Behavior:
9
9
  * - No key configured → allow (orchestrator handles inline activation)
10
- * - Cache VALID < 24h → allow silently
10
+ * - Cache VALID < 24h → allow silently + session ping (once/day/project)
11
11
  * - Cache EXPIRED/INVALID < 24h → block with renewal message
12
12
  * - Cache stale (> 24h) → call API, refresh cache, then allow or block
13
13
  * - API unreachable → fail open (allow), never block work
14
14
  */
15
15
 
16
16
  import { existsSync, readFileSync, writeFileSync, mkdirSync } from 'fs';
17
- import { join } from 'path';
17
+ import { join, basename } from 'path';
18
18
  import { homedir } from 'os';
19
19
 
20
20
  const API_BASE = 'https://chati.dev/api';
21
+ const TELEMETRY_ENDPOINT = 'https://chati.dev/api/telemetry';
21
22
  const CACHE_TTL_MS = 24 * 60 * 60 * 1000; // 24 hours
22
23
 
23
24
  const GLOBAL_DIR = join(homedir(), '.chati-dev');
24
25
  const LICENSE_PATH = join(GLOBAL_DIR, 'license.yaml');
26
+ const PINGS_PATH = join(GLOBAL_DIR, 'pings.yaml');
25
27
 
26
28
  async function main() {
27
29
  let input = '';
@@ -48,7 +50,10 @@ async function main() {
48
50
  const age = checkedAt ? Date.now() - new Date(checkedAt).getTime() : Infinity;
49
51
 
50
52
  if (age < CACHE_TTL_MS) {
51
- if (status === 'VALID') { allow(); return; }
53
+ if (status === 'VALID') {
54
+ await allowWithPing(licenseKey);
55
+ return;
56
+ }
52
57
  if (status === 'EXPIRED' || status === 'INVALID') {
53
58
  block(buildMessage(status, readYamlField(licenseRaw, 'reason')));
54
59
  return;
@@ -78,7 +83,10 @@ async function main() {
78
83
  mkdirSync(GLOBAL_DIR, { recursive: true });
79
84
  writeFileSync(LICENSE_PATH, dumpYaml(updated));
80
85
 
81
- if (data.status === 'VALID') { allow(); return; }
86
+ if (data.status === 'VALID') {
87
+ await allowWithPing(licenseKey);
88
+ return;
89
+ }
82
90
  block(buildMessage(data.status, data.reason));
83
91
  } catch {
84
92
  allow(); // Fail open — API unreachable
@@ -88,6 +96,69 @@ async function main() {
88
96
  }
89
97
  }
90
98
 
99
+ // ---------------------------------------------------------------------------
100
+ // Session Ping (once per day per project)
101
+ // ---------------------------------------------------------------------------
102
+
103
+ async function allowWithPing(licenseKey) {
104
+ const projectPath = process.cwd();
105
+ const projectName = basename(projectPath) || 'unknown';
106
+
107
+ if (shouldPing(projectPath)) {
108
+ recordPing(projectPath); // sync — prevents duplicate pings even if fetch fails
109
+ const pingPromise = sendSessionPing(licenseKey, projectName);
110
+ allow();
111
+ await pingPromise; // keep process alive until ping completes (max 3s)
112
+ } else {
113
+ allow();
114
+ }
115
+ }
116
+
117
+ function shouldPing(projectPath) {
118
+ try {
119
+ if (!existsSync(PINGS_PATH)) return true;
120
+ const content = readFileSync(PINGS_PATH, 'utf-8');
121
+ const today = new Date().toISOString().substring(0, 10);
122
+ const escaped = projectPath.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
123
+ const match = content.match(new RegExp(`^${escaped}:\\s*(.+)$`, 'm'));
124
+ return !match || match[1].trim() !== today;
125
+ } catch { return false; } // fail closed — don't add latency on parse error
126
+ }
127
+
128
+ function recordPing(projectPath) {
129
+ try {
130
+ mkdirSync(GLOBAL_DIR, { recursive: true });
131
+ const today = new Date().toISOString().substring(0, 10);
132
+ let content = existsSync(PINGS_PATH) ? readFileSync(PINGS_PATH, 'utf-8') : '';
133
+ const escaped = projectPath.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
134
+ if (new RegExp(`^${escaped}:`, 'm').test(content)) {
135
+ content = content.replace(new RegExp(`^(${escaped}:).*$`, 'm'), `$1 ${today}`);
136
+ } else {
137
+ content += `${projectPath}: ${today}\n`;
138
+ }
139
+ writeFileSync(PINGS_PATH, content);
140
+ } catch {} // fail silently
141
+ }
142
+
143
+ async function sendSessionPing(licenseKey, projectName) {
144
+ try {
145
+ await fetch(TELEMETRY_ENDPOINT, {
146
+ method: 'POST',
147
+ headers: { 'Content-Type': 'application/json' },
148
+ body: JSON.stringify({
149
+ license_key: licenseKey,
150
+ project_name: projectName,
151
+ events: [{ type: 'session_active', timestamp: new Date().toISOString() }],
152
+ }),
153
+ signal: AbortSignal.timeout(3000),
154
+ });
155
+ } catch {} // fire-and-forget, always fail silently
156
+ }
157
+
158
+ // ---------------------------------------------------------------------------
159
+ // Decision helpers
160
+ // ---------------------------------------------------------------------------
161
+
91
162
  function allow() {
92
163
  process.stdout.write(JSON.stringify({ decision: 'allow' }));
93
164
  }
@@ -103,6 +174,10 @@ function buildMessage(status, reason) {
103
174
  return `chati.dev license invalid${reason ? ` (${reason})` : ''}.\nRun: npx chati-dev activate --key=YOUR-KEY\nOr visit https://chati.dev/pricing`;
104
175
  }
105
176
 
177
+ // ---------------------------------------------------------------------------
178
+ // YAML helpers (no external deps)
179
+ // ---------------------------------------------------------------------------
180
+
106
181
  function readYamlField(raw, field) {
107
182
  const match = raw.match(new RegExp(`^${field}:\\s*(.+)$`, 'm'));
108
183
  return match ? match[1].trim().replace(/^["']|["']$/g, '') : null;
@@ -125,6 +200,10 @@ function dumpYaml(obj) {
125
200
  .join('\n') + '\n';
126
201
  }
127
202
 
203
+ // ---------------------------------------------------------------------------
204
+ // Machine ID
205
+ // ---------------------------------------------------------------------------
206
+
128
207
  async function computeMachineId() {
129
208
  const { createHash } = await import('crypto');
130
209
  const os = await import('os');
@@ -18,39 +18,6 @@ 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
-
54
21
  ### Step 1: Load Context
55
22
  ```
56
23
  1. Read .chati/session.yaml
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "chati-dev",
3
- "version": "4.0.7",
3
+ "version": "4.0.9",
4
4
  "description": "AI-Powered Multi-Agent Orchestration System — Structured vibe coding for Full Stack Development",
5
5
  "type": "module",
6
6
  "bin": {