@exreve/exk 1.0.6 → 1.0.8

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,117 @@
1
+ import { readFileSync, readdirSync, existsSync } from 'fs';
2
+ import * as path from 'path';
3
+ import { fileURLToPath } from 'url';
4
+ import { dirname } from 'path';
5
+ const SKILLS_DIR = dirname(fileURLToPath(import.meta.url)); // This file is in the skills directory
6
+ const skillCache = new Map();
7
+ /**
8
+ * Parse frontmatter and content from a skill markdown file
9
+ */
10
+ function parseSkillFile(content) {
11
+ // Check for YAML frontmatter between --- markers
12
+ const frontmatterRegex = /^---\s*\n([\s\S]*?)\n---\s*\n([\s\S]*)$/;
13
+ const match = content.match(frontmatterRegex);
14
+ if (!match) {
15
+ // No frontmatter, use filename as name
16
+ return null;
17
+ }
18
+ const frontmatter = match[1];
19
+ const skillContent = match[2].trim();
20
+ // Parse name and description from frontmatter
21
+ const nameMatch = frontmatter.match(/name:\s*(.+)/);
22
+ const descriptionMatch = frontmatter.match(/description:\s*(.+)/);
23
+ const name = nameMatch ? nameMatch[1].trim() : '';
24
+ const description = descriptionMatch ? descriptionMatch[1].trim() : '';
25
+ if (!name) {
26
+ return null;
27
+ }
28
+ return { name, description, content: skillContent };
29
+ }
30
+ /**
31
+ * Load a single skill by name
32
+ */
33
+ export function loadSkill(name) {
34
+ // Check cache first
35
+ if (skillCache.has(name)) {
36
+ return skillCache.get(name);
37
+ }
38
+ const skillPath = path.join(SKILLS_DIR, `${name}.md`);
39
+ if (!existsSync(skillPath)) {
40
+ return null;
41
+ }
42
+ try {
43
+ const content = readFileSync(skillPath, 'utf-8');
44
+ const parsed = parseSkillFile(content);
45
+ if (!parsed) {
46
+ return null;
47
+ }
48
+ const skill = {
49
+ name: parsed.name,
50
+ description: parsed.description,
51
+ content: parsed.content
52
+ };
53
+ skillCache.set(name, skill);
54
+ return skill;
55
+ }
56
+ catch (error) {
57
+ console.error(`Failed to load skill ${name}:`, error);
58
+ return null;
59
+ }
60
+ }
61
+ /**
62
+ * Load all available skills from the skills directory
63
+ */
64
+ export function loadAllSkills() {
65
+ const skills = [];
66
+ if (!existsSync(SKILLS_DIR)) {
67
+ return skills;
68
+ }
69
+ try {
70
+ const files = readdirSync(SKILLS_DIR);
71
+ for (const file of files) {
72
+ if (!file.endsWith('.md')) {
73
+ continue;
74
+ }
75
+ const name = file.replace('.md', '');
76
+ const skill = loadSkill(name);
77
+ if (skill) {
78
+ skills.push(skill);
79
+ }
80
+ }
81
+ }
82
+ catch (error) {
83
+ console.error('Failed to load skills:', error);
84
+ }
85
+ return skills;
86
+ }
87
+ /**
88
+ * Get skill content for injection into prompts
89
+ */
90
+ export function getSkillContent(names) {
91
+ if (names.length === 0) {
92
+ return '';
93
+ }
94
+ const contents = [];
95
+ for (const name of names) {
96
+ const skill = loadSkill(name);
97
+ if (skill) {
98
+ contents.push(`# Skill: ${skill.name}\n\n${skill.content}`);
99
+ }
100
+ }
101
+ if (contents.length === 0) {
102
+ return '';
103
+ }
104
+ return `## Active Skills\n\n${contents.join('\n\n---\n\n')}\n\n---\n\n`;
105
+ }
106
+ /**
107
+ * List all available skill names
108
+ */
109
+ export function listSkillNames() {
110
+ return loadAllSkills().map(s => s.name);
111
+ }
112
+ /**
113
+ * Get skill metadata (name, description) for all skills
114
+ */
115
+ export function getSkillMetadata() {
116
+ return loadAllSkills().map(s => ({ name: s.name, description: s.description }));
117
+ }
Binary file
@@ -0,0 +1,425 @@
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
+ const DEVICE_ID_FILE = path.join(CONFIG_DIR, 'device-id.json');
32
+ // File paths
33
+ const UPDATER_FILE = fileURLToPath(import.meta.url);
34
+ const APP_BUNDLE = path.join(__dirname, 'dist', 'app-child.js');
35
+ const APP_BUNDLE_BACKUP = APP_BUNDLE + '.backup';
36
+ const APP_BUNDLE_OLD = APP_BUNDLE + '.old';
37
+ const BUNDLE_HASHES_FILE = path.join(__dirname, 'binary-hashes.json');
38
+ const UPDATE_LOCK_FILE = path.join(CONFIG_DIR, '.update-lock');
39
+ // State
40
+ let childProcess = null;
41
+ let isUpdating = false;
42
+ let restartRequested = false;
43
+ async function readConfig() {
44
+ try {
45
+ const data = await fs.readFile(CONFIG_FILE, 'utf-8');
46
+ return JSON.parse(data);
47
+ }
48
+ catch {
49
+ return { apiUrl: 'https://api.talk-to-code.com' };
50
+ }
51
+ }
52
+ // ============ Hash Utilities ============
53
+ function calculateHash(filePath) {
54
+ try {
55
+ const fileData = fsSync.readFileSync(filePath);
56
+ return crypto.createHash('sha256').update(fileData).digest('hex');
57
+ }
58
+ catch {
59
+ return '';
60
+ }
61
+ }
62
+ async function getBundleHashes() {
63
+ try {
64
+ const data = await fs.readFile(BUNDLE_HASHES_FILE, 'utf-8');
65
+ return JSON.parse(data);
66
+ }
67
+ catch {
68
+ return {};
69
+ }
70
+ }
71
+ async function checkForUpdates() {
72
+ try {
73
+ const config = await readConfig();
74
+ const currentHash = calculateHash(APP_BUNDLE);
75
+ if (!currentHash) {
76
+ console.warn('āš ļø Cannot calculate current bundle hash');
77
+ return null;
78
+ }
79
+ const response = await fetch(`${config.apiUrl}/update/check`, {
80
+ method: 'POST',
81
+ headers: { 'Content-Type': 'application/json' },
82
+ body: JSON.stringify({
83
+ hash: currentHash,
84
+ platform: os.platform(),
85
+ arch: os.arch()
86
+ })
87
+ });
88
+ if (!response.ok) {
89
+ console.warn(`āš ļø Update check failed: HTTP ${response.status}`);
90
+ return null;
91
+ }
92
+ const info = await response.json();
93
+ return info;
94
+ }
95
+ catch (error) {
96
+ console.warn(`āš ļø Update check failed: ${error.message}`);
97
+ return null;
98
+ }
99
+ }
100
+ async function downloadUpdate(downloadUrl, expectedHash) {
101
+ console.log('šŸ“„ Downloading update...');
102
+ const response = await fetch(downloadUrl);
103
+ if (!response.ok) {
104
+ throw new Error(`Download failed: HTTP ${response.status}`);
105
+ }
106
+ const buffer = Buffer.from(await response.arrayBuffer());
107
+ const actualHash = crypto.createHash('sha256').update(buffer).digest('hex');
108
+ if (actualHash !== expectedHash) {
109
+ throw new Error(`Hash mismatch: expected ${expectedHash}, got ${actualHash}`);
110
+ }
111
+ console.log('āœ“ Download verified');
112
+ return buffer;
113
+ }
114
+ async function applyUpdate(newBundle) {
115
+ console.log('šŸ”„ Applying update...');
116
+ // Create backup of current version if it exists
117
+ if (fsSync.existsSync(APP_BUNDLE)) {
118
+ try {
119
+ // Rotate backups: .old -> .backup (if exists), current -> .old
120
+ if (fsSync.existsSync(APP_BUNDLE_OLD)) {
121
+ await fs.copyFile(APP_BUNDLE_OLD, APP_BUNDLE_BACKUP);
122
+ }
123
+ await fs.copyFile(APP_BUNDLE, APP_BUNDLE_OLD);
124
+ console.log('āœ“ Backup created');
125
+ }
126
+ catch (error) {
127
+ console.warn(`āš ļø Backup creation failed: ${error.message}`);
128
+ }
129
+ }
130
+ // Write new bundle
131
+ const tempPath = path.join(os.tmpdir(), `ttc-update-${Date.now()}.js`);
132
+ await fs.writeFile(tempPath, newBundle, { mode: 0o755 });
133
+ // Replace current bundle
134
+ await fs.rename(tempPath, APP_BUNDLE);
135
+ // Make executable
136
+ if (process.platform !== 'win32') {
137
+ await fs.chmod(APP_BUNDLE, 0o755);
138
+ }
139
+ console.log('āœ“ Update applied');
140
+ }
141
+ async function performUpdate(info) {
142
+ if (!info.downloadUrl || !info.hash) {
143
+ console.error('āŒ Invalid update info');
144
+ return false;
145
+ }
146
+ try {
147
+ const newBundle = await downloadUpdate(info.downloadUrl, info.hash);
148
+ await applyUpdate(newBundle);
149
+ if (info.version) {
150
+ console.log(`āœ“ Updated to version ${info.version}`);
151
+ }
152
+ if (info.changelog) {
153
+ console.log(`\nšŸ“ Changelog:\n${info.changelog}`);
154
+ }
155
+ return true;
156
+ }
157
+ catch (error) {
158
+ console.error(`āŒ Update failed: ${error.message}`);
159
+ // Attempt rollback
160
+ if (fsSync.existsSync(APP_BUNDLE_OLD)) {
161
+ console.log('šŸ”„ Rolling back to previous version...');
162
+ try {
163
+ await fs.copyFile(APP_BUNDLE_OLD, APP_BUNDLE);
164
+ console.log('āœ“ Rollback complete');
165
+ }
166
+ catch (rollbackError) {
167
+ console.error(`āŒ Rollback failed: ${rollbackError.message}`);
168
+ }
169
+ }
170
+ return false;
171
+ }
172
+ }
173
+ // ============ Child Process Management ============
174
+ function spawnChild() {
175
+ if (!fsSync.existsSync(APP_BUNDLE)) {
176
+ console.error(`āŒ App bundle not found: ${APP_BUNDLE}`);
177
+ console.error('Please run: npm run build:cli');
178
+ process.exit(1);
179
+ }
180
+ console.log(`šŸš€ Spawning child process: ${APP_BUNDLE}`);
181
+ const child = spawn(process.execPath, [APP_BUNDLE, ...process.argv.slice(2)], {
182
+ stdio: 'inherit',
183
+ env: {
184
+ ...process.env,
185
+ TTC_IS_CHILD: '1',
186
+ TTC_UPDATER_PID: process.pid.toString()
187
+ }
188
+ });
189
+ child.on('exit', (code, signal) => {
190
+ console.log(`\nšŸ“¦ Child process exited (code: ${code}, signal: ${signal})`);
191
+ childProcess = null;
192
+ // If update was requested, restart with new version
193
+ if (restartRequested) {
194
+ console.log('šŸ”„ Restarting with updated version...');
195
+ restartRequested = false;
196
+ childProcess = spawnChild();
197
+ return;
198
+ }
199
+ // If child crashed, try to recover
200
+ if (code !== 0 && code !== null && !isUpdating) {
201
+ console.log('āš ļø Child process crashed, attempting recovery...');
202
+ // Try backup version if current is corrupted
203
+ if (fsSync.existsSync(APP_BUNDLE_OLD)) {
204
+ console.log('šŸ”„ Attempting to recover with backup...');
205
+ fs.copyFile(APP_BUNDLE_OLD, APP_BUNDLE)
206
+ .then(() => {
207
+ console.log('āœ“ Recovered, restarting...');
208
+ childProcess = spawnChild();
209
+ })
210
+ .catch((err) => {
211
+ console.error('āŒ Recovery failed, giving up');
212
+ process.exit(1);
213
+ });
214
+ }
215
+ else {
216
+ console.error('āŒ No backup available, cannot recover');
217
+ process.exit(1);
218
+ }
219
+ }
220
+ else if (signal !== 'SIGTERM' && signal !== 'SIGINT') {
221
+ // Normal exit or intentional signal
222
+ process.exit(code || 0);
223
+ }
224
+ });
225
+ child.on('error', (error) => {
226
+ console.error('āŒ Child process error:', error);
227
+ childProcess = null;
228
+ });
229
+ return child;
230
+ }
231
+ // ============ Signal Handling ============
232
+ function setupSignalHandlers() {
233
+ // Forward signals to child
234
+ const signals = ['SIGINT', 'SIGTERM', 'SIGHUP', 'SIGUSR1', 'SIGUSR2'];
235
+ signals.forEach(signal => {
236
+ process.on(signal, () => {
237
+ if (childProcess) {
238
+ console.log(`\nšŸ“” Forwarding ${signal} to child process...`);
239
+ childProcess.kill(signal);
240
+ }
241
+ else {
242
+ process.exit(0);
243
+ }
244
+ });
245
+ });
246
+ // Handle uncaught exceptions
247
+ process.on('uncaughtException', (error) => {
248
+ console.error('āŒ Uncaught exception in updater:', error);
249
+ if (childProcess) {
250
+ childProcess.kill('SIGTERM');
251
+ }
252
+ process.exit(1);
253
+ });
254
+ process.on('unhandledRejection', (reason, promise) => {
255
+ console.error('āŒ Unhandled rejection in updater:', reason);
256
+ });
257
+ }
258
+ // ============ CLI Interface ============
259
+ async function cmdUpdate() {
260
+ console.log('šŸ” Checking for updates...');
261
+ const info = await checkForUpdates();
262
+ if (!info) {
263
+ console.log('āš ļø Unable to check for updates');
264
+ process.exit(1);
265
+ return;
266
+ }
267
+ if (!info.updateAvailable) {
268
+ console.log('āœ“ Already up to date');
269
+ process.exit(0);
270
+ return;
271
+ }
272
+ console.log('šŸ“¦ Update available!');
273
+ if (info.version) {
274
+ console.log(` Version: ${info.version}`);
275
+ }
276
+ if (info.changelog) {
277
+ console.log(`\nšŸ“ Changelog:\n${info.changelog}`);
278
+ }
279
+ // Stop child process if running
280
+ if (childProcess) {
281
+ console.log('šŸ›‘ Stopping child process...');
282
+ childProcess.kill('SIGTERM');
283
+ childProcess = null;
284
+ }
285
+ isUpdating = true;
286
+ const success = await performUpdate(info);
287
+ isUpdating = false;
288
+ if (success) {
289
+ console.log('\nāœ“ Update complete! Restarting...');
290
+ restartRequested = true;
291
+ childProcess = spawnChild();
292
+ }
293
+ else {
294
+ console.error('\nāŒ Update failed!');
295
+ process.exit(1);
296
+ }
297
+ }
298
+ async function cmdUpdateCheck() {
299
+ console.log('šŸ” Checking for updates...');
300
+ const info = await checkForUpdates();
301
+ if (!info) {
302
+ console.log('āš ļø Unable to check for updates');
303
+ process.exit(1);
304
+ return;
305
+ }
306
+ if (info.updateAvailable) {
307
+ console.log('šŸ“¦ Update available!');
308
+ if (info.version) {
309
+ console.log(` Version: ${info.version}`);
310
+ }
311
+ if (info.size) {
312
+ console.log(` Size: ${(info.size / 1024 / 1024).toFixed(2)} MB`);
313
+ }
314
+ if (info.changelog) {
315
+ console.log(`\nšŸ“ Changelog:\n${info.changelog}`);
316
+ }
317
+ console.log('\nRun "ttc update" to apply the update');
318
+ process.exit(0);
319
+ }
320
+ else {
321
+ console.log('āœ“ Already up to date');
322
+ process.exit(0);
323
+ }
324
+ }
325
+ async function cmdVersion() {
326
+ console.log(`TalkToCode CLI Updater`);
327
+ console.log(`Node: ${process.version}`);
328
+ console.log(`Platform: ${os.platform()} ${os.arch()}`);
329
+ const currentHash = calculateHash(APP_BUNDLE);
330
+ console.log(`Bundle hash: ${currentHash ? currentHash.substring(0, 16) + '...' : 'unknown'}`);
331
+ const hashes = await getBundleHashes();
332
+ if (hashes['js-bundle']) {
333
+ console.log(`Bundle version: ${hashes['js-bundle'].version || 'unknown'}`);
334
+ console.log(`Bundle date: ${hashes['js-bundle'].date || 'unknown'}`);
335
+ }
336
+ process.exit(0);
337
+ }
338
+ // ============ IPC from Child Process ============
339
+ /**
340
+ * The child process can request updates by writing to a special file
341
+ * or by sending a signal to the parent
342
+ */
343
+ function setupChildIPC() {
344
+ // Listen for update requests via USR1 signal
345
+ process.on('SIGUSR1', async () => {
346
+ console.log('\nšŸ“Ø Update requested by child process...');
347
+ const info = await checkForUpdates();
348
+ if (!info || !info.updateAvailable) {
349
+ console.log('āœ“ No updates available');
350
+ return;
351
+ }
352
+ console.log('šŸ“¦ Update available, applying...');
353
+ // Stop child process
354
+ if (childProcess) {
355
+ childProcess.kill('SIGTERM');
356
+ childProcess = null;
357
+ }
358
+ isUpdating = true;
359
+ const success = await performUpdate(info);
360
+ isUpdating = false;
361
+ if (success) {
362
+ console.log('āœ“ Update complete! Restarting...');
363
+ restartRequested = true;
364
+ childProcess = spawnChild();
365
+ }
366
+ });
367
+ }
368
+ // ============ Main Entry Point ============
369
+ async function main() {
370
+ const args = process.argv.slice(2);
371
+ const command = args[0];
372
+ // Handle updater-specific commands
373
+ if (command === 'update') {
374
+ await cmdUpdate();
375
+ return;
376
+ }
377
+ if (command === 'update:check' || command === 'check-update') {
378
+ await cmdUpdateCheck();
379
+ return;
380
+ }
381
+ if (command === 'version' || command === '--version' || command === '-v') {
382
+ await cmdVersion();
383
+ return;
384
+ }
385
+ if (command === 'help' || command === '--help' || command === '-h') {
386
+ console.log(`
387
+ TalkToCode CLI Updater
388
+ =====================
389
+
390
+ Commands:
391
+ ttc <command> Run command in child process (default)
392
+ ttc update Check and apply updates
393
+ ttc update:check Check for updates without applying
394
+ ttc version Show version information
395
+ ttc help Show this help message
396
+
397
+ The updater runs as the main process and spawns the app as a child.
398
+ This allows the updater to update itself even if the app is corrupted.
399
+ `);
400
+ process.exit(0);
401
+ return;
402
+ }
403
+ // Default: spawn child process
404
+ console.log('šŸŽÆ TalkToCode CLI Updater');
405
+ console.log('====================================\n');
406
+ // Silent update check on startup
407
+ checkForUpdates().then(info => {
408
+ if (info?.updateAvailable) {
409
+ console.log('šŸ“¦ Update available! Run "ttc update" to apply.\n');
410
+ }
411
+ }).catch(() => { });
412
+ setupSignalHandlers();
413
+ setupChildIPC();
414
+ childProcess = spawnChild();
415
+ // Keep updater alive
416
+ process.on('exit', () => {
417
+ if (childProcess) {
418
+ childProcess.kill('SIGTERM');
419
+ }
420
+ });
421
+ }
422
+ main().catch(error => {
423
+ console.error('āŒ Fatal error:', error);
424
+ process.exit(1);
425
+ });
@@ -97,7 +97,8 @@ module.exports = {
97
97
  HOME: "$HOME",
98
98
  USER: "${USER:-$(whoami)}",
99
99
  PATH: "$DAEMON_PATH",
100
- EXK_PKG_DIR: "$EXK_PKG_DIR"
100
+ EXK_PKG_DIR: "$EXK_PKG_DIR",
101
+ NODE_OPTIONS: "--no-experimental-strip-types"
101
102
  }
102
103
  }]
103
104
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@exreve/exk",
3
- "version": "1.0.6",
3
+ "version": "1.0.8",
4
4
  "description": "exk - Control Claude CLI with voice and programmable interfaces",
5
5
  "type": "module",
6
6
  "bin": {
@@ -8,15 +8,13 @@
8
8
  },
9
9
  "files": [
10
10
  "bin",
11
- "shared",
11
+ "dist",
12
12
  "skills",
13
- "*.ts",
14
13
  "install-service.sh",
15
- "container-entrypoint.sh",
16
- "tsconfig.json"
14
+ "container-entrypoint.sh"
17
15
  ],
18
16
  "scripts": {
19
- "build": "node build-cli-tarball.js",
17
+ "build": "tsc && node build-cli-tarball.js",
20
18
  "build:tsc": "tsc",
21
19
  "typecheck": "tsc --noEmit",
22
20
  "prepublishOnly": "node -e \"require('fs').chmodSync('bin/exk', 0o755)\""
@@ -38,7 +36,6 @@
38
36
  "@anthropic-ai/claude-agent-sdk": "^0.1.23",
39
37
  "@anthropic-ai/sdk": "^0.74.0",
40
38
  "@fastify/static": "^9.0.0",
41
- "@types/node": "^22.10.2",
42
39
  "@xenova/transformers": "^2.17.2",
43
40
  "chokidar": "^3.6.0",
44
41
  "commander": "^13.1.0",
@@ -47,16 +44,17 @@
47
44
  "node-fetch": "^3.3.2",
48
45
  "pino-pretty": "^11.0.0",
49
46
  "socket.io-client": "^4.8.1",
50
- "tsx": "^4.19.0",
51
47
  "uuid": "^11.0.3"
52
48
  },
53
49
  "devDependencies": {
50
+ "@types/node": "^22.10.2",
54
51
  "@types/chokidar": "^2.1.3",
55
52
  "@types/uuid": "^10.0.0",
56
53
  "@vercel/ncc": "^0.38.1",
57
54
  "esbuild": "^0.27.2",
58
55
  "js-confuser": "^1.1.0",
59
56
  "ts-node": "^10.9.2",
57
+ "tsx": "^4.19.0",
60
58
  "typescript": "^5.7.2"
61
59
  },
62
60
  "engines": {