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 +7 -2
- package/framework/hooks/license-guard.js +68 -14
- package/package.json +1 -1
- package/src/license/client.js +20 -2
- package/src/license/commands.js +5 -5
- package/src/telemetry/config.js +0 -2
- package/src/telemetry/sender.js +19 -17
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 (
|
|
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
|
-
|
|
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*(
|
|
124
|
-
|
|
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
|
|
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 ${
|
|
180
|
+
content = content.replace(new RegExp(`^(${escaped}:).*$`, 'm'), `$1 '${value}'`);
|
|
136
181
|
} else {
|
|
137
|
-
content += `${projectPath}: ${
|
|
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: [{
|
|
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
package/src/license/client.js
CHANGED
|
@@ -125,10 +125,28 @@ export async function validateLicense(_projectDir) {
|
|
|
125
125
|
}
|
|
126
126
|
|
|
127
127
|
/**
|
|
128
|
-
* Remove license locally
|
|
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,
|
package/src/license/commands.js
CHANGED
|
@@ -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
|
-
|
|
60
|
-
|
|
61
|
-
console.log(
|
|
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 {
|
package/src/telemetry/config.js
CHANGED
|
@@ -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;
|
package/src/telemetry/sender.js
CHANGED
|
@@ -58,24 +58,26 @@ export async function sendEvents(events, config) {
|
|
|
58
58
|
events,
|
|
59
59
|
};
|
|
60
60
|
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
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
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
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
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
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
|
}
|