chati-dev 4.0.9 → 4.0.11

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
@@ -362,7 +362,7 @@ Telemetry:
362
362
 
363
363
  case 'deactivate': {
364
364
  const { runDeactivate } = await import('../src/license/commands.js');
365
- runDeactivate(targetDir);
365
+ await runDeactivate(targetDir);
366
366
  break;
367
367
  }
368
368
 
@@ -370,7 +370,12 @@ Telemetry:
370
370
  const licSubCmd = args[1] || 'status';
371
371
  if (licSubCmd === 'deactivate') {
372
372
  const { runDeactivate } = await import('../src/license/commands.js');
373
- runDeactivate(targetDir);
373
+ await runDeactivate(targetDir);
374
+ } else if (args.includes('--refresh') || licSubCmd === 'refresh') {
375
+ // Force re-validate against API and update local cache
376
+ const { runLicenseStatus } = await import('../src/license/commands.js');
377
+ await runLicenseStatus(targetDir);
378
+ console.log(' Cache updated. Hook will use fresh status on next run.\n');
374
379
  } else {
375
380
  const { runLicenseStatus } = await import('../src/license/commands.js');
376
381
  await runLicenseStatus(targetDir);
@@ -20,6 +20,7 @@ import { homedir } from 'os';
20
20
  const API_BASE = 'https://chati.dev/api';
21
21
  const TELEMETRY_ENDPOINT = 'https://chati.dev/api/telemetry';
22
22
  const CACHE_TTL_MS = 24 * 60 * 60 * 1000; // 24 hours
23
+ const PING_THROTTLE_MS = 5 * 60 * 1000; // 5 minutes
23
24
 
24
25
  const GLOBAL_DIR = join(homedir(), '.chati-dev');
25
26
  const LICENSE_PATH = join(GLOBAL_DIR, 'license.yaml');
@@ -97,16 +98,17 @@ async function main() {
97
98
  }
98
99
 
99
100
  // ---------------------------------------------------------------------------
100
- // Session Ping (once per day per project)
101
+ // Session Ping (5-min throttle with phase-change detection)
101
102
  // ---------------------------------------------------------------------------
102
103
 
103
104
  async function allowWithPing(licenseKey) {
104
105
  const projectPath = process.cwd();
105
106
  const projectName = basename(projectPath) || 'unknown';
107
+ const session = readSessionContext();
106
108
 
107
- if (shouldPing(projectPath)) {
108
- recordPing(projectPath); // sync — prevents duplicate pings even if fetch fails
109
- const pingPromise = sendSessionPing(licenseKey, projectName);
109
+ if (shouldPing(projectPath, session)) {
110
+ recordPing(projectPath, session); // sync — prevents duplicate pings even if fetch fails
111
+ const pingPromise = sendSessionPing(licenseKey, projectName, session);
110
112
  allow();
111
113
  await pingPromise; // keep process alive until ping completes (max 3s)
112
114
  } else {
@@ -114,41 +116,93 @@ async function allowWithPing(licenseKey) {
114
116
  }
115
117
  }
116
118
 
117
- function shouldPing(projectPath) {
119
+ /**
120
+ * Reads .chati/session.yaml from cwd to extract pipeline phase, agent, and project type.
121
+ * Returns {} on any error (fail silently — this is optional enrichment).
122
+ */
123
+ function readSessionContext() {
124
+ try {
125
+ const sessionPath = join(process.cwd(), '.chati', 'session.yaml');
126
+ if (!existsSync(sessionPath)) return {};
127
+ const raw = readFileSync(sessionPath, 'utf-8');
128
+
129
+ // project.state → pipeline_phase
130
+ const stateMatch = raw.match(/^\s+state:\s*(.+)$/m);
131
+ const pipeline_phase = stateMatch ? stateMatch[1].trim().replace(/^["']|["']$/g, '') : null;
132
+
133
+ // current_agent (top-level)
134
+ const agentMatch = raw.match(/^current_agent:\s*(.+)$/m);
135
+ const current_agent = agentMatch ? agentMatch[1].trim().replace(/^["']|["']$/g, '') : null;
136
+
137
+ // project.type → project_type
138
+ const typeMatch = raw.match(/^\s+type:\s*(.+)$/m);
139
+ const project_type = typeMatch ? typeMatch[1].trim().replace(/^["']|["']$/g, '') : null;
140
+
141
+ return { pipeline_phase, current_agent, project_type };
142
+ } catch { return {}; }
143
+ }
144
+
145
+ /**
146
+ * Pings.yaml format: '/path/to/project': '2026-03-22T21:00:00.000Z|discover|greenfield-wu'
147
+ * Ping if: (1) no entry, (2) >5 min ago, OR (3) phase/agent changed
148
+ */
149
+ function shouldPing(projectPath, session = {}) {
118
150
  try {
119
151
  if (!existsSync(PINGS_PATH)) return true;
120
152
  const content = readFileSync(PINGS_PATH, 'utf-8');
121
- const today = new Date().toISOString().substring(0, 10);
122
153
  const escaped = projectPath.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
123
- const match = content.match(new RegExp(`^${escaped}:\\s*(.+)$`, 'm'));
124
- return !match || match[1].trim() !== today;
154
+ const match = content.match(new RegExp(`^${escaped}:\\s*'?([^'\\n]+)'?$`, 'm'));
155
+ if (!match) return true;
156
+
157
+ const [timestamp, lastPhase, lastAgent] = match[1].trim().split('|');
158
+ const age = Date.now() - new Date(timestamp).getTime();
159
+ if (age > PING_THROTTLE_MS) return true;
160
+
161
+ // Phase or agent changed → ping immediately
162
+ if (session.pipeline_phase && session.pipeline_phase !== lastPhase) return true;
163
+ if (session.current_agent && session.current_agent !== lastAgent) return true;
164
+
165
+ return false;
125
166
  } catch { return false; } // fail closed — don't add latency on parse error
126
167
  }
127
168
 
128
- function recordPing(projectPath) {
169
+ function recordPing(projectPath, session = {}) {
129
170
  try {
130
171
  mkdirSync(GLOBAL_DIR, { recursive: true });
131
- const today = new Date().toISOString().substring(0, 10);
172
+ const timestamp = new Date().toISOString();
173
+ const phase = session.pipeline_phase ?? '';
174
+ const agent = session.current_agent ?? '';
175
+ const value = `${timestamp}|${phase}|${agent}`;
176
+
132
177
  let content = existsSync(PINGS_PATH) ? readFileSync(PINGS_PATH, 'utf-8') : '';
133
178
  const escaped = projectPath.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
134
179
  if (new RegExp(`^${escaped}:`, 'm').test(content)) {
135
- content = content.replace(new RegExp(`^(${escaped}:).*$`, 'm'), `$1 ${today}`);
180
+ content = content.replace(new RegExp(`^(${escaped}:).*$`, 'm'), `$1 '${value}'`);
136
181
  } else {
137
- content += `${projectPath}: ${today}\n`;
182
+ content += `${projectPath}: '${value}'\n`;
138
183
  }
139
184
  writeFileSync(PINGS_PATH, content);
140
185
  } catch {} // fail silently
141
186
  }
142
187
 
143
- async function sendSessionPing(licenseKey, projectName) {
188
+ async function sendSessionPing(licenseKey, projectName, session = {}) {
144
189
  try {
190
+ const properties = {};
191
+ if (session.pipeline_phase) properties.pipeline_phase = session.pipeline_phase;
192
+ if (session.current_agent) properties.current_agent = session.current_agent;
193
+ if (session.project_type) properties.project_type = session.project_type;
194
+
145
195
  await fetch(TELEMETRY_ENDPOINT, {
146
196
  method: 'POST',
147
197
  headers: { 'Content-Type': 'application/json' },
148
198
  body: JSON.stringify({
149
199
  license_key: licenseKey,
150
200
  project_name: projectName,
151
- events: [{ type: 'session_active', timestamp: new Date().toISOString() }],
201
+ events: [{
202
+ type: 'session_active',
203
+ timestamp: new Date().toISOString(),
204
+ properties,
205
+ }],
152
206
  }),
153
207
  signal: AbortSignal.timeout(3000),
154
208
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "chati-dev",
3
- "version": "4.0.9",
3
+ "version": "4.0.11",
4
4
  "description": "AI-Powered Multi-Agent Orchestration System — Structured vibe coding for Full Stack Development",
5
5
  "type": "module",
6
6
  "bin": {
@@ -125,10 +125,28 @@ export async function validateLicense(_projectDir) {
125
125
  }
126
126
 
127
127
  /**
128
- * Remove license locally (does not deactivate machine slot on server).
128
+ * Remove license locally and deactivate machine slot on server (best-effort).
129
+ * Fails silently if API is unreachable.
129
130
  */
130
- export function deactivateLicense(_projectDir) {
131
+ export async function deactivateLicense(_projectDir) {
131
132
  const data = readGlobal();
133
+ const key = data.key && data.key !== 'null' ? data.key : null;
134
+
135
+ // Best-effort server deactivation (key + machine_id = proof of ownership)
136
+ if (key) {
137
+ try {
138
+ const machineId = getMachineId();
139
+ await fetch(`${API_BASE}/license/self-deactivate`, {
140
+ method: 'POST',
141
+ headers: { 'Content-Type': 'application/json' },
142
+ body: JSON.stringify({ key, machine_id: machineId }),
143
+ signal: AbortSignal.timeout(5000),
144
+ });
145
+ } catch {
146
+ // Fail silently — local deactivation still proceeds
147
+ }
148
+ }
149
+
132
150
  writeGlobal({
133
151
  ...data,
134
152
  key: null,
@@ -50,15 +50,15 @@ export async function runActivate(projectDir, keyArg) {
50
50
  // deactivate
51
51
  // ---------------------------------------------------------------------------
52
52
 
53
- export function runDeactivate(projectDir) {
53
+ export async function runDeactivate(projectDir) {
54
54
  const key = getLicenseKey(projectDir);
55
55
  if (!key) {
56
56
  console.log('No license key found locally. Nothing to remove.');
57
57
  return;
58
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`);
59
+ console.log('Deactivating...');
60
+ await deactivateLicense(projectDir);
61
+ console.log(`\n✓ License key removed and machine slot freed.\n`);
62
62
  }
63
63
 
64
64
  // ---------------------------------------------------------------------------
@@ -74,7 +74,7 @@ export async function runLicenseStatus(projectDir) {
74
74
  return;
75
75
  }
76
76
 
77
- console.log(`\nChecking license...`);
77
+ console.log(`\nChecking license (refreshing cache)...`);
78
78
 
79
79
  let status;
80
80
  try {
@@ -27,7 +27,6 @@ export function getTelemetryConfig(targetDir) {
27
27
  enabled: true,
28
28
  anonymousId: null,
29
29
  endpoint: process.env.CHATI_TELEMETRY_ENDPOINT || 'https://chati.dev/api/telemetry',
30
- apiKey: process.env.CHATI_TELEMETRY_KEY || '10b0b54ba4f392fa46379ba778062ab0af5ca61e79609a7dce4aadd660104b56',
31
30
  };
32
31
 
33
32
  if (!existsSync(configPath)) return defaults;
@@ -41,7 +40,6 @@ export function getTelemetryConfig(targetDir) {
41
40
  enabled: telemetry.enabled === true,
42
41
  anonymousId: telemetry.anonymous_id || null,
43
42
  endpoint: telemetry.endpoint || defaults.endpoint,
44
- apiKey: telemetry.api_key || defaults.apiKey,
45
43
  };
46
44
  } catch {
47
45
  return defaults;
@@ -58,24 +58,26 @@ export async function sendEvents(events, config) {
58
58
  events,
59
59
  };
60
60
 
61
- try {
62
- const controller = new AbortController();
63
- const timeout = setTimeout(() => controller.abort(), 5000);
61
+ for (let attempt = 1; attempt <= 3; attempt++) {
62
+ try {
63
+ const controller = new AbortController();
64
+ const timeout = setTimeout(() => controller.abort(), 5000);
64
65
 
65
- await fetch(endpoint, {
66
- method: 'POST',
67
- headers: {
68
- 'Content-Type': 'application/json',
69
- ...(config.apiKey ? { 'X-Telemetry-Key': config.apiKey } : {}),
70
- },
71
- body: JSON.stringify(payload),
72
- signal: controller.signal,
73
- });
66
+ const res = await fetch(endpoint, {
67
+ method: 'POST',
68
+ headers: { 'Content-Type': 'application/json' },
69
+ body: JSON.stringify(payload),
70
+ signal: controller.signal,
71
+ });
74
72
 
75
- clearTimeout(timeout);
76
- return true;
77
- } catch {
78
- // Silently failnever block user workflow
79
- return false;
73
+ clearTimeout(timeout);
74
+ if (res.ok || res.status < 500) return true; // 4xx = don't retry
75
+ } catch {
76
+ // Network error or timeout retry if attempts remain
77
+ if (attempt < 3) {
78
+ await new Promise(r => setTimeout(r, attempt * 500)); // 500ms, 1000ms
79
+ }
80
+ }
80
81
  }
82
+ return false;
81
83
  }