@exreve/exk 1.0.50 → 1.0.52

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.
@@ -0,0 +1,82 @@
1
+ /**
2
+ * Update & Version Handlers Module
3
+ *
4
+ * Handles update:check, update:start, version:info operations.
5
+ */
6
+ import fsSync from 'fs';
7
+ import os from 'os';
8
+ import path from 'path';
9
+ import { createHash } from 'crypto';
10
+ import { fileURLToPath } from 'url';
11
+ export function registerUpdateHandlers(socket, _foreground, readConfig, requestUpdateFromParent) {
12
+ socket.on('update:check', async (_data, callback) => {
13
+ try {
14
+ const config = await readConfig();
15
+ const currentHash = createHash('sha256').update(fsSync.readFileSync(fileURLToPath(import.meta.url))).digest('hex');
16
+ const response = await fetch(`${config.apiUrl}/update/check`, {
17
+ method: 'POST',
18
+ headers: { 'Content-Type': 'application/json' },
19
+ body: JSON.stringify({
20
+ hash: currentHash,
21
+ platform: os.platform(),
22
+ arch: os.arch()
23
+ })
24
+ });
25
+ if (!response.ok) {
26
+ callback?.({ success: false, error: `HTTP ${response.status}` });
27
+ return;
28
+ }
29
+ const info = await response.json();
30
+ callback?.({
31
+ success: true,
32
+ updateAvailable: info.updateAvailable,
33
+ version: info.version,
34
+ changelog: info.changelog,
35
+ size: info.size
36
+ });
37
+ }
38
+ catch (error) {
39
+ callback?.({ success: false, error: error.message });
40
+ }
41
+ });
42
+ socket.on('update:start', async (_data, callback) => {
43
+ try {
44
+ requestUpdateFromParent();
45
+ callback?.({
46
+ success: true,
47
+ message: 'Update initiated. The CLI will restart automatically when complete.'
48
+ });
49
+ }
50
+ catch (error) {
51
+ callback?.({ success: false, error: error.message });
52
+ }
53
+ });
54
+ socket.on('version:info', async (_data, callback) => {
55
+ try {
56
+ const currentHash = createHash('sha256').update(fsSync.readFileSync(fileURLToPath(import.meta.url))).digest('hex');
57
+ let version = 'unknown';
58
+ let date = undefined;
59
+ try {
60
+ const hashesPath = path.join(path.dirname(fileURLToPath(import.meta.url)), 'binary-hashes.json');
61
+ const hashes = JSON.parse(fsSync.readFileSync(hashesPath, 'utf-8'));
62
+ if (hashes['js-bundle']) {
63
+ version = hashes['js-bundle'].version || version;
64
+ date = hashes['js-bundle'].date;
65
+ }
66
+ }
67
+ catch { }
68
+ callback?.({
69
+ success: true,
70
+ version,
71
+ hash: currentHash.substring(0, 16) + '...',
72
+ date,
73
+ nodeVersion: process.version,
74
+ platform: os.platform(),
75
+ arch: os.arch()
76
+ });
77
+ }
78
+ catch (error) {
79
+ callback?.({ success: false, error: error.message });
80
+ }
81
+ });
82
+ }
@@ -0,0 +1,422 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * TalkToCode CLI Updater - Standalone Update Process
4
+ *
5
+ * This is a minimal, robust updater that:
6
+ * 1. Runs as the main process
7
+ * 2. Spawns the app as a child process
8
+ * 3. Can update itself even if app is corrupted
9
+ * 4. Can recover from failed updates
10
+ *
11
+ * Architecture:
12
+ * - updater.ts (this file) -> Main process, handles updates
13
+ * - app-child.ts -> Child process, contains all app logic
14
+ *
15
+ * File structure:
16
+ * - ttc (updater) -> Main entry point
17
+ * - ttc-app.js -> Child bundle (updated via updates)
18
+ * - ttc-app.js.old -> Backup of last version
19
+ * - ttc-app.js.backup -> Secondary backup
20
+ */
21
+ import { spawn } from 'child_process';
22
+ import fs from 'fs/promises';
23
+ import fsSync from 'fs';
24
+ import path from 'path';
25
+ import os from 'os';
26
+ import crypto from 'crypto';
27
+ import { fileURLToPath } from 'url';
28
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
29
+ const CONFIG_DIR = path.join(os.homedir(), '.talk-to-code');
30
+ const CONFIG_FILE = path.join(CONFIG_DIR, 'config.json');
31
+ // File paths
32
+ const APP_BUNDLE = path.join(__dirname, 'dist', 'app-child.js');
33
+ const APP_BUNDLE_BACKUP = APP_BUNDLE + '.backup';
34
+ const APP_BUNDLE_OLD = APP_BUNDLE + '.old';
35
+ const BUNDLE_HASHES_FILE = path.join(__dirname, 'binary-hashes.json');
36
+ // State
37
+ let childProcess = null;
38
+ let isUpdating = false;
39
+ let restartRequested = false;
40
+ async function readConfig() {
41
+ try {
42
+ const data = await fs.readFile(CONFIG_FILE, 'utf-8');
43
+ return JSON.parse(data);
44
+ }
45
+ catch {
46
+ return { apiUrl: 'https://api.talk-to-code.com' };
47
+ }
48
+ }
49
+ // ============ Hash Utilities ============
50
+ function calculateHash(filePath) {
51
+ try {
52
+ const fileData = fsSync.readFileSync(filePath);
53
+ return crypto.createHash('sha256').update(fileData).digest('hex');
54
+ }
55
+ catch {
56
+ return '';
57
+ }
58
+ }
59
+ async function getBundleHashes() {
60
+ try {
61
+ const data = await fs.readFile(BUNDLE_HASHES_FILE, 'utf-8');
62
+ return JSON.parse(data);
63
+ }
64
+ catch {
65
+ return {};
66
+ }
67
+ }
68
+ async function checkForUpdates() {
69
+ try {
70
+ const config = await readConfig();
71
+ const currentHash = calculateHash(APP_BUNDLE);
72
+ if (!currentHash) {
73
+ console.warn('āš ļø Cannot calculate current bundle hash');
74
+ return null;
75
+ }
76
+ const response = await fetch(`${config.apiUrl}/update/check`, {
77
+ method: 'POST',
78
+ headers: { 'Content-Type': 'application/json' },
79
+ body: JSON.stringify({
80
+ hash: currentHash,
81
+ platform: os.platform(),
82
+ arch: os.arch()
83
+ })
84
+ });
85
+ if (!response.ok) {
86
+ console.warn(`āš ļø Update check failed: HTTP ${response.status}`);
87
+ return null;
88
+ }
89
+ const info = await response.json();
90
+ return info;
91
+ }
92
+ catch (error) {
93
+ console.warn(`āš ļø Update check failed: ${error.message}`);
94
+ return null;
95
+ }
96
+ }
97
+ async function downloadUpdate(downloadUrl, expectedHash) {
98
+ console.log('šŸ“„ Downloading update...');
99
+ const response = await fetch(downloadUrl);
100
+ if (!response.ok) {
101
+ throw new Error(`Download failed: HTTP ${response.status}`);
102
+ }
103
+ const buffer = Buffer.from(await response.arrayBuffer());
104
+ const actualHash = crypto.createHash('sha256').update(buffer).digest('hex');
105
+ if (actualHash !== expectedHash) {
106
+ throw new Error(`Hash mismatch: expected ${expectedHash}, got ${actualHash}`);
107
+ }
108
+ console.log('āœ“ Download verified');
109
+ return buffer;
110
+ }
111
+ async function applyUpdate(newBundle) {
112
+ console.log('šŸ”„ Applying update...');
113
+ // Create backup of current version if it exists
114
+ if (fsSync.existsSync(APP_BUNDLE)) {
115
+ try {
116
+ // Rotate backups: .old -> .backup (if exists), current -> .old
117
+ if (fsSync.existsSync(APP_BUNDLE_OLD)) {
118
+ await fs.copyFile(APP_BUNDLE_OLD, APP_BUNDLE_BACKUP);
119
+ }
120
+ await fs.copyFile(APP_BUNDLE, APP_BUNDLE_OLD);
121
+ console.log('āœ“ Backup created');
122
+ }
123
+ catch (error) {
124
+ console.warn(`āš ļø Backup creation failed: ${error.message}`);
125
+ }
126
+ }
127
+ // Write new bundle
128
+ const tempPath = path.join(os.tmpdir(), `ttc-update-${Date.now()}.js`);
129
+ await fs.writeFile(tempPath, newBundle, { mode: 0o755 });
130
+ // Replace current bundle
131
+ await fs.rename(tempPath, APP_BUNDLE);
132
+ // Make executable
133
+ if (process.platform !== 'win32') {
134
+ await fs.chmod(APP_BUNDLE, 0o755);
135
+ }
136
+ console.log('āœ“ Update applied');
137
+ }
138
+ async function performUpdate(info) {
139
+ if (!info.downloadUrl || !info.hash) {
140
+ console.error('āŒ Invalid update info');
141
+ return false;
142
+ }
143
+ try {
144
+ const newBundle = await downloadUpdate(info.downloadUrl, info.hash);
145
+ await applyUpdate(newBundle);
146
+ if (info.version) {
147
+ console.log(`āœ“ Updated to version ${info.version}`);
148
+ }
149
+ if (info.changelog) {
150
+ console.log(`\nšŸ“ Changelog:\n${info.changelog}`);
151
+ }
152
+ return true;
153
+ }
154
+ catch (error) {
155
+ console.error(`āŒ Update failed: ${error.message}`);
156
+ // Attempt rollback
157
+ if (fsSync.existsSync(APP_BUNDLE_OLD)) {
158
+ console.log('šŸ”„ Rolling back to previous version...');
159
+ try {
160
+ await fs.copyFile(APP_BUNDLE_OLD, APP_BUNDLE);
161
+ console.log('āœ“ Rollback complete');
162
+ }
163
+ catch (rollbackError) {
164
+ console.error(`āŒ Rollback failed: ${rollbackError.message}`);
165
+ }
166
+ }
167
+ return false;
168
+ }
169
+ }
170
+ // ============ Child Process Management ============
171
+ function spawnChild() {
172
+ if (!fsSync.existsSync(APP_BUNDLE)) {
173
+ console.error(`āŒ App bundle not found: ${APP_BUNDLE}`);
174
+ console.error('Please run: npm run build:cli');
175
+ process.exit(1);
176
+ }
177
+ console.log(`šŸš€ Spawning child process: ${APP_BUNDLE}`);
178
+ const child = spawn(process.execPath, [APP_BUNDLE, ...process.argv.slice(2)], {
179
+ stdio: 'inherit',
180
+ env: {
181
+ ...process.env,
182
+ TTC_IS_CHILD: '1',
183
+ TTC_UPDATER_PID: process.pid.toString()
184
+ }
185
+ });
186
+ child.on('exit', (code, signal) => {
187
+ console.log(`\nšŸ“¦ Child process exited (code: ${code}, signal: ${signal})`);
188
+ childProcess = null;
189
+ // If update was requested, restart with new version
190
+ if (restartRequested) {
191
+ console.log('šŸ”„ Restarting with updated version...');
192
+ restartRequested = false;
193
+ childProcess = spawnChild();
194
+ return;
195
+ }
196
+ // If child crashed, try to recover
197
+ if (code !== 0 && code !== null && !isUpdating) {
198
+ console.log('āš ļø Child process crashed, attempting recovery...');
199
+ // Try backup version if current is corrupted
200
+ if (fsSync.existsSync(APP_BUNDLE_OLD)) {
201
+ console.log('šŸ”„ Attempting to recover with backup...');
202
+ fs.copyFile(APP_BUNDLE_OLD, APP_BUNDLE)
203
+ .then(() => {
204
+ console.log('āœ“ Recovered, restarting...');
205
+ childProcess = spawnChild();
206
+ })
207
+ .catch((_err) => {
208
+ console.error('āŒ Recovery failed, giving up');
209
+ process.exit(1);
210
+ });
211
+ }
212
+ else {
213
+ console.error('āŒ No backup available, cannot recover');
214
+ process.exit(1);
215
+ }
216
+ }
217
+ else if (signal !== 'SIGTERM' && signal !== 'SIGINT') {
218
+ // Normal exit or intentional signal
219
+ process.exit(code || 0);
220
+ }
221
+ });
222
+ child.on('error', (error) => {
223
+ console.error('āŒ Child process error:', error);
224
+ childProcess = null;
225
+ });
226
+ return child;
227
+ }
228
+ // ============ Signal Handling ============
229
+ function setupSignalHandlers() {
230
+ // Forward signals to child
231
+ const signals = ['SIGINT', 'SIGTERM', 'SIGHUP', 'SIGUSR1', 'SIGUSR2'];
232
+ signals.forEach(signal => {
233
+ process.on(signal, () => {
234
+ if (childProcess) {
235
+ console.log(`\nšŸ“” Forwarding ${signal} to child process...`);
236
+ childProcess.kill(signal);
237
+ }
238
+ else {
239
+ process.exit(0);
240
+ }
241
+ });
242
+ });
243
+ // Handle uncaught exceptions
244
+ process.on('uncaughtException', (error) => {
245
+ console.error('āŒ Uncaught exception in updater:', error);
246
+ if (childProcess) {
247
+ childProcess.kill('SIGTERM');
248
+ }
249
+ process.exit(1);
250
+ });
251
+ process.on('unhandledRejection', (reason, _promise) => {
252
+ console.error('āŒ Unhandled rejection in updater:', reason);
253
+ });
254
+ }
255
+ // ============ CLI Interface ============
256
+ async function cmdUpdate() {
257
+ console.log('šŸ” Checking for updates...');
258
+ const info = await checkForUpdates();
259
+ if (!info) {
260
+ console.log('āš ļø Unable to check for updates');
261
+ process.exit(1);
262
+ return;
263
+ }
264
+ if (!info.updateAvailable) {
265
+ console.log('āœ“ Already up to date');
266
+ process.exit(0);
267
+ return;
268
+ }
269
+ console.log('šŸ“¦ Update available!');
270
+ if (info.version) {
271
+ console.log(` Version: ${info.version}`);
272
+ }
273
+ if (info.changelog) {
274
+ console.log(`\nšŸ“ Changelog:\n${info.changelog}`);
275
+ }
276
+ // Stop child process if running
277
+ if (childProcess) {
278
+ console.log('šŸ›‘ Stopping child process...');
279
+ childProcess.kill('SIGTERM');
280
+ childProcess = null;
281
+ }
282
+ isUpdating = true;
283
+ const success = await performUpdate(info);
284
+ isUpdating = false;
285
+ if (success) {
286
+ console.log('\nāœ“ Update complete! Restarting...');
287
+ restartRequested = true;
288
+ childProcess = spawnChild();
289
+ }
290
+ else {
291
+ console.error('\nāŒ Update failed!');
292
+ process.exit(1);
293
+ }
294
+ }
295
+ async function cmdUpdateCheck() {
296
+ console.log('šŸ” Checking for updates...');
297
+ const info = await checkForUpdates();
298
+ if (!info) {
299
+ console.log('āš ļø Unable to check for updates');
300
+ process.exit(1);
301
+ return;
302
+ }
303
+ if (info.updateAvailable) {
304
+ console.log('šŸ“¦ Update available!');
305
+ if (info.version) {
306
+ console.log(` Version: ${info.version}`);
307
+ }
308
+ if (info.size) {
309
+ console.log(` Size: ${(info.size / 1024 / 1024).toFixed(2)} MB`);
310
+ }
311
+ if (info.changelog) {
312
+ console.log(`\nšŸ“ Changelog:\n${info.changelog}`);
313
+ }
314
+ console.log('\nRun "ttc update" to apply the update');
315
+ process.exit(0);
316
+ }
317
+ else {
318
+ console.log('āœ“ Already up to date');
319
+ process.exit(0);
320
+ }
321
+ }
322
+ async function cmdVersion() {
323
+ console.log(`TalkToCode CLI Updater`);
324
+ console.log(`Node: ${process.version}`);
325
+ console.log(`Platform: ${os.platform()} ${os.arch()}`);
326
+ const currentHash = calculateHash(APP_BUNDLE);
327
+ console.log(`Bundle hash: ${currentHash ? currentHash.substring(0, 16) + '...' : 'unknown'}`);
328
+ const hashes = await getBundleHashes();
329
+ if (hashes['js-bundle']) {
330
+ console.log(`Bundle version: ${hashes['js-bundle'].version || 'unknown'}`);
331
+ console.log(`Bundle date: ${hashes['js-bundle'].date || 'unknown'}`);
332
+ }
333
+ process.exit(0);
334
+ }
335
+ // ============ IPC from Child Process ============
336
+ /**
337
+ * The child process can request updates by writing to a special file
338
+ * or by sending a signal to the parent
339
+ */
340
+ function setupChildIPC() {
341
+ // Listen for update requests via USR1 signal
342
+ process.on('SIGUSR1', async () => {
343
+ console.log('\nšŸ“Ø Update requested by child process...');
344
+ const info = await checkForUpdates();
345
+ if (!info || !info.updateAvailable) {
346
+ console.log('āœ“ No updates available');
347
+ return;
348
+ }
349
+ console.log('šŸ“¦ Update available, applying...');
350
+ // Stop child process
351
+ if (childProcess) {
352
+ childProcess.kill('SIGTERM');
353
+ childProcess = null;
354
+ }
355
+ isUpdating = true;
356
+ const success = await performUpdate(info);
357
+ isUpdating = false;
358
+ if (success) {
359
+ console.log('āœ“ Update complete! Restarting...');
360
+ restartRequested = true;
361
+ childProcess = spawnChild();
362
+ }
363
+ });
364
+ }
365
+ // ============ Main Entry Point ============
366
+ async function main() {
367
+ const args = process.argv.slice(2);
368
+ const command = args[0];
369
+ // Handle updater-specific commands
370
+ if (command === 'update') {
371
+ await cmdUpdate();
372
+ return;
373
+ }
374
+ if (command === 'update:check' || command === 'check-update') {
375
+ await cmdUpdateCheck();
376
+ return;
377
+ }
378
+ if (command === 'version' || command === '--version' || command === '-v') {
379
+ await cmdVersion();
380
+ return;
381
+ }
382
+ if (command === 'help' || command === '--help' || command === '-h') {
383
+ console.log(`
384
+ TalkToCode CLI Updater
385
+ =====================
386
+
387
+ Commands:
388
+ ttc <command> Run command in child process (default)
389
+ ttc update Check and apply updates
390
+ ttc update:check Check for updates without applying
391
+ ttc version Show version information
392
+ ttc help Show this help message
393
+
394
+ The updater runs as the main process and spawns the app as a child.
395
+ This allows the updater to update itself even if the app is corrupted.
396
+ `);
397
+ process.exit(0);
398
+ return;
399
+ }
400
+ // Default: spawn child process
401
+ console.log('šŸŽÆ TalkToCode CLI Updater');
402
+ console.log('====================================\n');
403
+ // Silent update check on startup
404
+ checkForUpdates().then(info => {
405
+ if (info?.updateAvailable) {
406
+ console.log('šŸ“¦ Update available! Run "ttc update" to apply.\n');
407
+ }
408
+ }).catch(() => { });
409
+ setupSignalHandlers();
410
+ setupChildIPC();
411
+ childProcess = spawnChild();
412
+ // Keep updater alive
413
+ process.on('exit', () => {
414
+ if (childProcess) {
415
+ childProcess.kill('SIGTERM');
416
+ }
417
+ });
418
+ }
419
+ main().catch(error => {
420
+ console.error('āŒ Fatal error:', error);
421
+ process.exit(1);
422
+ });
@@ -1,2 +1,35 @@
1
1
  // ============ Core Types ============
2
- export {};
2
+ // --- Type guard helpers ---
3
+ // These allow narrowing `unknown` to a specific tool output type at runtime.
4
+ export function isBashOutput(r) {
5
+ return typeof r === 'object' && r !== null && 'stdout' in r && 'interrupted' in r;
6
+ }
7
+ export function isGrepOutput(r) {
8
+ return typeof r === 'object' && r !== null && 'numFiles' in r && 'filenames' in r && 'mode' in r;
9
+ }
10
+ export function isGlobOutput(r) {
11
+ return typeof r === 'object' && r !== null && 'durationMs' in r && 'filenames' in r && 'truncated' in r;
12
+ }
13
+ export function isFileReadOutput(r) {
14
+ return typeof r === 'object' && r !== null && 'file' in r && typeof r.type === 'string';
15
+ }
16
+ export function isFileEditOutput(r) {
17
+ return typeof r === 'object' && r !== null && 'oldString' in r && 'newString' in r && 'structuredPatch' in r;
18
+ }
19
+ export function isFileWriteOutput(r) {
20
+ return typeof r === 'object' && r !== null
21
+ && ('type' in r && (r.type === 'create' || r.type === 'update'))
22
+ && 'filePath' in r && 'content' in r;
23
+ }
24
+ export function isTodoWriteOutput(r) {
25
+ return typeof r === 'object' && r !== null && 'oldTodos' in r && 'newTodos' in r;
26
+ }
27
+ export function isWebSearchOutput(r) {
28
+ return typeof r === 'object' && r !== null && 'query' in r && 'results' in r && 'durationSeconds' in r;
29
+ }
30
+ export function isWebFetchOutput(r) {
31
+ return typeof r === 'object' && r !== null && 'url' in r && 'result' in r && 'code' in r;
32
+ }
33
+ export function isAgentOutput(r) {
34
+ return typeof r === 'object' && r !== null && 'agentId' in r && 'status' in r && 'prompt' in r;
35
+ }
Binary file
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@exreve/exk",
3
- "version": "1.0.50",
3
+ "version": "1.0.52",
4
4
  "description": "exk - Control Claude CLI with voice and programmable interfaces",
5
5
  "type": "module",
6
6
  "bin": {