@exreve/exk 1.0.0

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