@exreve/exk 1.0.1 → 1.0.3

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/agentSession.ts CHANGED
@@ -1,12 +1,12 @@
1
1
  import { query } from '@anthropic-ai/claude-agent-sdk'
2
- import type { SessionOutput } from './shared/types'
2
+ import type { SessionOutput } from './shared/types.js'
3
3
  import { execSync, spawn } from 'child_process'
4
4
  import { existsSync, realpathSync, mkdirSync, writeFileSync, readFileSync } from 'fs'
5
5
  import { symlink as fsSymlink } from 'fs'
6
- // import { AgentLogger } from './agentLogger' // DISABLED: File logging removed for performance
6
+ import type { AgentLogger } from './agentLogger.js' // type-only; runtime logging disabled elsewhere
7
7
  // Memory system removed for performance
8
- import { getSkillContent } from './skills/index'
9
- import { createModuleMcpServer, type ChoiceRequest, type ChoiceResponse, type ModuleMcpServerConfig } from './moduleMcpServer'
8
+ import { getSkillContent } from './skills/index.js'
9
+ import { createModuleMcpServer, type ChoiceRequest, type ChoiceResponse, type ModuleMcpServerConfig } from './moduleMcpServer.js'
10
10
  import path from 'path'
11
11
  import os from 'os'
12
12
  import { createRequire } from 'module'
@@ -78,31 +78,41 @@ export interface SessionHandler {
78
78
  }
79
79
 
80
80
  // AI config - loaded from server after registration, stored in ~/.talk-to-code/ai-config.json
81
+ // (Do not read ANTHROPIC_* / CLAUDE_MODEL from the host environment — only this file + code default model.)
81
82
  const AI_CONFIG_PATH = path.join(os.homedir(), '.talk-to-code', 'ai-config.json')
83
+ const DEFAULT_AI_MODEL = 'glm-5.1'
82
84
 
83
- function loadAiConfig(): { ANTHROPIC_AUTH_TOKEN: string; ANTHROPIC_BASE_URL: string; MODEL: string } {
85
+ function loadAiConfig(): { apiKey: string; baseUrl: string; model: string } {
84
86
  try {
85
87
  const data = readFileSync(AI_CONFIG_PATH, 'utf-8')
86
- const config = JSON.parse(data)
87
- return {
88
- ANTHROPIC_AUTH_TOKEN: config.authToken || process.env.ANTHROPIC_API_KEY || '',
89
- ANTHROPIC_BASE_URL: config.baseUrl || process.env.ANTHROPIC_BASE_URL || '',
90
- MODEL: config.model || process.env.CLAUDE_MODEL || 'glm-5.1',
91
- }
88
+ const config = JSON.parse(data) as { authToken?: string; baseUrl?: string; model?: string }
89
+ const apiKey = typeof config.authToken === 'string' ? config.authToken.trim() : ''
90
+ const baseUrl = typeof config.baseUrl === 'string' ? config.baseUrl.trim() : ''
91
+ const model =
92
+ typeof config.model === 'string' && config.model.trim() ? config.model.trim() : DEFAULT_AI_MODEL
93
+ return { apiKey, baseUrl, model }
92
94
  } catch {
93
- return {
94
- ANTHROPIC_AUTH_TOKEN: process.env.ANTHROPIC_API_KEY || '',
95
- ANTHROPIC_BASE_URL: process.env.ANTHROPIC_BASE_URL || '',
96
- MODEL: process.env.CLAUDE_MODEL || 'glm-5.1',
97
- }
95
+ return { apiKey: '', baseUrl: '', model: DEFAULT_AI_MODEL }
98
96
  }
99
97
  }
100
98
 
99
+ /** Env for the Claude Code child: copy of host env with host ANTHROPIC_* stripped, then inject from ai-config only. */
100
+ function envForClaudeCodeChild(): NodeJS.ProcessEnv {
101
+ const env = { ...process.env } as NodeJS.ProcessEnv
102
+ delete env.ANTHROPIC_API_KEY
103
+ delete env.ANTHROPIC_BASE_URL
104
+ delete env.ANTHROPIC_AUTH_TOKEN
105
+ const { apiKey, baseUrl } = loadAiConfig()
106
+ if (apiKey) env.ANTHROPIC_API_KEY = apiKey
107
+ if (baseUrl) env.ANTHROPIC_BASE_URL = baseUrl
108
+ return env
109
+ }
110
+
101
111
  // Lazy config getter - reloads from file each time (so daemon picks up changes without restart)
102
112
  const CLAUDE_CONFIG = {
103
- get ANTHROPIC_AUTH_TOKEN() { return loadAiConfig().ANTHROPIC_AUTH_TOKEN },
104
- get ANTHROPIC_BASE_URL() { return loadAiConfig().ANTHROPIC_BASE_URL },
105
- get MODEL() { return loadAiConfig().MODEL },
113
+ get apiKey() { return loadAiConfig().apiKey },
114
+ get baseUrl() { return loadAiConfig().baseUrl },
115
+ get model() { return loadAiConfig().model },
106
116
  }
107
117
 
108
118
  interface ConversationMessage {
@@ -160,7 +170,7 @@ export class AgentSessionManager {
160
170
 
161
171
  async createSession(handler: SessionHandler): Promise<void> {
162
172
  const { sessionId, projectPath, model } = handler
163
- const sessionModel = model || CLAUDE_CONFIG.MODEL
173
+ const sessionModel = model || CLAUDE_CONFIG.model
164
174
 
165
175
  // Ensure project directory exists - prevents ENOENT errors when SDK spawns process
166
176
  if (!existsSync(projectPath)) {
@@ -206,7 +216,7 @@ export class AgentSessionManager {
206
216
  // DISABLED: File logging removed for performance
207
217
  // const logger = new AgentLogger(sessionId)
208
218
  // await logger.logSessionCreated(projectPath)
209
- // await logger.logModelConfig(sessionModel, CLAUDE_CONFIG.ANTHROPIC_BASE_URL)
219
+ // await logger.logModelConfig(sessionModel, CLAUDE_CONFIG.baseUrl)
210
220
  const logger = undefined
211
221
 
212
222
  // Store the handler for this session
@@ -427,12 +437,6 @@ export class AgentSessionManager {
427
437
  }
428
438
  })
429
439
 
430
- // Setup environment
431
- process.env.ANTHROPIC_API_KEY = CLAUDE_CONFIG.ANTHROPIC_AUTH_TOKEN
432
- if (CLAUDE_CONFIG.ANTHROPIC_BASE_URL) {
433
- process.env.ANTHROPIC_BASE_URL = CLAUDE_CONFIG.ANTHROPIC_BASE_URL
434
- }
435
-
436
440
  // Use cached Claude executable path (resolved at module load time for performance)
437
441
  const pathToClaudeCodeExecutable = CACHED_CLAUDE_PATH
438
442
  // DISABLED: File logging removed for performance
@@ -447,8 +451,8 @@ export class AgentSessionManager {
447
451
  const queryOptions: any = {
448
452
  signal: abortController.signal, // Pass abort signal to SDK for interruption
449
453
  cwd: projectPath,
450
- apiKey: CLAUDE_CONFIG.ANTHROPIC_AUTH_TOKEN,
451
- model: CLAUDE_CONFIG.MODEL, // Use Claude Opus 4.5 (configurable via CLAUDE_MODEL env var)
454
+ apiKey: CLAUDE_CONFIG.apiKey,
455
+ model: CLAUDE_CONFIG.model,
452
456
  tools: { type: 'preset', preset: 'claude_code' },
453
457
  disallowedTools: ['AskUserQuestion'],
454
458
  settingSources: ['project'], // Enable CLAUDE.md loading
@@ -555,11 +559,7 @@ export class AgentSessionManager {
555
559
 
556
560
  return child
557
561
  },
558
- env: {
559
- ...process.env,
560
- ANTHROPIC_API_KEY: CLAUDE_CONFIG.ANTHROPIC_AUTH_TOKEN,
561
- ANTHROPIC_BASE_URL: CLAUDE_CONFIG.ANTHROPIC_BASE_URL,
562
- },
562
+ env: envForClaudeCodeChild(),
563
563
  hooks: {
564
564
  // PostToolUse hook DISABLED for tool_result emission.
565
565
  // tool_result events are already emitted via the user message handler (line ~752)
@@ -587,10 +587,10 @@ export class AgentSessionManager {
587
587
  }
588
588
 
589
589
  // Log model being used for debugging
590
- const sessionModel = session.model || CLAUDE_CONFIG.MODEL
590
+ const sessionModel = session.model || CLAUDE_CONFIG.model
591
591
  // DISABLED: File logging removed for performance
592
592
  // if (session.logger) {
593
- // await session.logger.logModelConfig(sessionModel, CLAUDE_CONFIG.ANTHROPIC_BASE_URL)
593
+ // await session.logger.logModelConfig(sessionModel, CLAUDE_CONFIG.baseUrl)
594
594
  // }
595
595
 
596
596
  // Create query stream - resume session if we have a Claude session ID
package/app-child.ts CHANGED
@@ -31,12 +31,12 @@ import type {
31
31
  SessionResponse,
32
32
  SessionsListResponse,
33
33
  DevicesListResponse,
34
- } from './shared/types'
35
- import { createProject, deleteProject } from './projectManager'
36
- import { agentSessionManager } from './agentSession'
37
- import { getProjectConfig, analyzeProjectWithClaude, saveProjectConfig } from './projectAnalyzer'
38
- import { generateRunnerCode } from './runnerGenerator'
39
- import { startApp, stopApp, restartApp, getAppStatuses, getAppLogs } from './appManager'
34
+ } from './shared/types.js'
35
+ import { createProject, deleteProject } from './projectManager.js'
36
+ import { agentSessionManager } from './agentSession.js'
37
+ import { getProjectConfig, analyzeProjectWithClaude, saveProjectConfig } from './projectAnalyzer.js'
38
+ import { generateRunnerCode } from './runnerGenerator.js'
39
+ import { startApp, stopApp, restartApp, getAppStatuses, getAppLogs } from './appManager.js'
40
40
  import { spawn, execSync } from 'child_process'
41
41
  import readline from 'readline'
42
42
  import { fileURLToPath } from 'url'
@@ -89,6 +89,8 @@ interface DeviceData {
89
89
  const CONFIG_DIR = path.join(os.homedir(), '.talk-to-code')
90
90
  const CONFIG_FILE = path.join(CONFIG_DIR, 'config.json')
91
91
  const DEVICE_ID_FILE = path.join(CONFIG_DIR, 'device-id.json')
92
+ const DEVICE_CONFIG_FILE = path.join(CONFIG_DIR, 'device-config.json')
93
+ const AI_CONFIG_FILE = path.join(CONFIG_DIR, 'ai-config.json')
92
94
  const DEFAULT_API_URL = 'https://api.talk-to-code.com'
93
95
 
94
96
  // ============ Helpers ============
@@ -107,6 +109,104 @@ async function writeConfig(config: Config): Promise<void> {
107
109
  await fs.writeFile(CONFIG_FILE, JSON.stringify(config, null, 2))
108
110
  }
109
111
 
112
+ /**
113
+ * Fetch AI config from server and save to ~/.talk-to-code/ai-config.json
114
+ */
115
+ async function fetchAiConfig(authToken: string): Promise<boolean> {
116
+ try {
117
+ const config = await readConfig()
118
+ const res = await fetch(`${config.apiUrl}/config/ai`, {
119
+ headers: { Authorization: `Bearer ${authToken}` },
120
+ })
121
+ if (!res.ok) {
122
+ console.log(`[fetchAiConfig] Server returned ${res.status}`)
123
+ return false
124
+ }
125
+ const data = await res.json() as {
126
+ success: boolean
127
+ aiConfig?: { authToken: string; baseUrl: string; model: string }
128
+ }
129
+ if (!data.success || !data.aiConfig) {
130
+ console.log('[fetchAiConfig] No AI config available for this user')
131
+ return false
132
+ }
133
+ await fs.writeFile(AI_CONFIG_FILE, JSON.stringify(data.aiConfig, null, 2))
134
+ console.log('[fetchAiConfig] AI config saved successfully')
135
+ return true
136
+ } catch (error: any) {
137
+ console.log(`[fetchAiConfig] Failed: ${error.message}`)
138
+ return false
139
+ }
140
+ }
141
+
142
+ function hasAiCredentials(): boolean {
143
+ try {
144
+ const raw = fsSync.readFileSync(AI_CONFIG_FILE, 'utf-8')
145
+ const j = JSON.parse(raw) as { authToken?: string }
146
+ return typeof j.authToken === 'string' && j.authToken.trim().length > 0
147
+ } catch {
148
+ return false
149
+ }
150
+ }
151
+
152
+ async function readDeviceAuthToken(): Promise<string | undefined> {
153
+ const env = process.env.TTC_AUTH_TOKEN?.trim()
154
+ if (env) return env
155
+ try {
156
+ const raw = await fs.readFile(DEVICE_CONFIG_FILE, 'utf-8')
157
+ const j = JSON.parse(raw) as { authToken?: string }
158
+ return typeof j.authToken === 'string' ? j.authToken.trim() : undefined
159
+ } catch {
160
+ return undefined
161
+ }
162
+ }
163
+
164
+ type DiskDeviceConfig = { email?: string; authToken?: string }
165
+
166
+ async function readDiskDeviceConfig(): Promise<DiskDeviceConfig> {
167
+ try {
168
+ const raw = await fs.readFile(DEVICE_CONFIG_FILE, 'utf-8')
169
+ return JSON.parse(raw) as DiskDeviceConfig
170
+ } catch {
171
+ return {}
172
+ }
173
+ }
174
+
175
+ async function writeDeviceConfigMerged(partial: { email?: string; authToken?: string }): Promise<void> {
176
+ const prev = await readDiskDeviceConfig()
177
+ const email = partial.email !== undefined ? partial.email : prev.email
178
+ let token: string | undefined
179
+ if (partial.authToken !== undefined) {
180
+ token = partial.authToken.trim() || undefined
181
+ } else {
182
+ token = typeof prev.authToken === 'string' && prev.authToken.trim() ? prev.authToken.trim() : undefined
183
+ }
184
+ const out: DiskDeviceConfig = {}
185
+ if (email) out.email = email
186
+ if (token) out.authToken = token
187
+ await fs.mkdir(CONFIG_DIR, { recursive: true })
188
+ await fs.writeFile(DEVICE_CONFIG_FILE, JSON.stringify(out, null, 2))
189
+ }
190
+
191
+ async function maybeFetchAiConfigIfMissing(): Promise<void> {
192
+ if (hasAiCredentials()) return
193
+ const jwt = await readDeviceAuthToken()
194
+ if (!jwt) return
195
+ const ok = await fetchAiConfig(jwt)
196
+ if (!ok && !hasAiCredentials()) {
197
+ const cfg = await readConfig()
198
+ console.warn(`[CLI] No AI key in ai-config.json yet. Could not sync from ${cfg.apiUrl}/config/ai — agent prompts will fail until this succeeds.`)
199
+ }
200
+ }
201
+
202
+ function scheduleAiConfigSync(authToken: string): void {
203
+ void fetchAiConfig(authToken).then((ok) => {
204
+ if (!ok && !hasAiCredentials()) {
205
+ console.warn('[CLI] AI config sync failed; check backend /config/ai and apiUrl.')
206
+ }
207
+ })
208
+ }
209
+
110
210
  async function getDeviceId(): Promise<string> {
111
211
  try {
112
212
  const data = await fs.readFile(DEVICE_ID_FILE, 'utf-8')
@@ -271,9 +371,8 @@ async function registerDevice(name: string | undefined, email: string | undefine
271
371
  let deviceEmail = email
272
372
 
273
373
  // If we already have a valid token, skip approval email and just register
274
- const deviceConfigFile = path.join(CONFIG_DIR, 'device-config.json')
275
374
  try {
276
- const deviceConfig = JSON.parse(await fs.readFile(deviceConfigFile, 'utf-8'))
375
+ const deviceConfig = await readDiskDeviceConfig()
277
376
  const existingToken = deviceConfig.authToken
278
377
  if (existingToken && typeof existingToken === 'string') {
279
378
  const config = await readConfig()
@@ -309,6 +408,7 @@ async function registerDevice(name: string | undefined, email: string | undefine
309
408
  }
310
409
  })
311
410
  })
411
+ await fetchAiConfig(existingToken)
312
412
  return
313
413
  }
314
414
  }
@@ -396,11 +496,7 @@ async function registerDevice(name: string | undefined, email: string | undefine
396
496
  const token = await checkApproval()
397
497
  if (token) {
398
498
  // Device approved! Store permanent token
399
- const deviceConfigFile = path.join(CONFIG_DIR, 'device-config.json')
400
- await fs.writeFile(deviceConfigFile, JSON.stringify({
401
- email: deviceEmail,
402
- authToken: token
403
- }, null, 2))
499
+ await writeDeviceConfigMerged({ email: deviceEmail, authToken: token })
404
500
 
405
501
  console.log(`\n✓ Device approved!`)
406
502
 
@@ -434,6 +530,7 @@ async function registerDevice(name: string | undefined, email: string | undefine
434
530
  })
435
531
  })
436
532
 
533
+ await fetchAiConfig(token)
437
534
  return // Successfully registered
438
535
  }
439
536
 
@@ -632,6 +729,8 @@ async function runDaemon(foreground = false, email?: string): Promise<void> {
632
729
  }
633
730
  console.log('')
634
731
 
732
+ await maybeFetchAiConfigIfMissing()
733
+
635
734
  let socket: Socket | null = null
636
735
  let reconnectTimeout: ReturnType<typeof setTimeout> | null = null
637
736
  let reconnectAttempts = 0
@@ -1701,7 +1800,6 @@ async function runDaemon(foreground = false, email?: string): Promise<void> {
1701
1800
  }
1702
1801
 
1703
1802
  // Check for auth token: env TTC_AUTH_TOKEN or device-config.json (from previous approval)
1704
- const deviceConfigFile = path.join(CONFIG_DIR, 'device-config.json')
1705
1803
  let authToken = process.env.TTC_AUTH_TOKEN
1706
1804
  let deviceEmail: string | undefined = email
1707
1805
  let skipEmailFlow = false
@@ -1709,7 +1807,7 @@ async function runDaemon(foreground = false, email?: string): Promise<void> {
1709
1807
  // Load from device-config if no env token (reuse state from previous approval)
1710
1808
  if (!authToken) {
1711
1809
  try {
1712
- const deviceConfig = JSON.parse(await fs.readFile(deviceConfigFile, 'utf-8'))
1810
+ const deviceConfig = await readDiskDeviceConfig()
1713
1811
  authToken = deviceConfig.authToken
1714
1812
  if (!deviceEmail && deviceConfig.email) deviceEmail = deviceConfig.email
1715
1813
  } catch {
@@ -1741,7 +1839,7 @@ async function runDaemon(foreground = false, email?: string): Promise<void> {
1741
1839
  // Load device email from config if not using token and not provided
1742
1840
  if (!deviceEmail && !skipEmailFlow) {
1743
1841
  try {
1744
- const deviceConfig = JSON.parse(await fs.readFile(deviceConfigFile, 'utf-8'))
1842
+ const deviceConfig = await readDiskDeviceConfig()
1745
1843
  deviceEmail = deviceConfig.email
1746
1844
  } catch {
1747
1845
  // No device config yet
@@ -1782,13 +1880,18 @@ async function runDaemon(foreground = false, email?: string): Promise<void> {
1782
1880
  }
1783
1881
  // Save config: email + authToken (preserve token so next run can reuse)
1784
1882
  if (device.email) {
1785
- const toSave = { email: device.email, ...(authToken ? { authToken } : {}) }
1786
- fs.writeFile(deviceConfigFile, JSON.stringify(toSave, null, 2)).catch(() => {})
1883
+ void writeDeviceConfigMerged({
1884
+ email: device.email,
1885
+ ...(authToken ? { authToken } : {}),
1886
+ }).catch(() => {})
1887
+ }
1888
+ if (authToken) {
1889
+ scheduleAiConfigSync(authToken)
1787
1890
  }
1788
1891
  } else if (needsApproval) {
1789
1892
  // Persist email when approval was requested so restarts don't prompt again
1790
1893
  if (deviceEmail) {
1791
- fs.writeFile(deviceConfigFile, JSON.stringify({ email: deviceEmail }, null, 2)).catch(() => {})
1894
+ void writeDeviceConfigMerged({ email: deviceEmail }).catch(() => {})
1792
1895
  }
1793
1896
  // Only log once to avoid flooding logs every 30s on heartbeat
1794
1897
  if (!needsApprovalLoggedOnce) {
package/appManager.ts CHANGED
@@ -1,8 +1,8 @@
1
1
  import fs from 'fs/promises'
2
2
  import path from 'path'
3
3
  import os from 'os'
4
- import type { ProjectApp } from './shared/types'
5
- import { createRunner, BaseRunner, RunnerOutput, RunnerStats } from './appRunner'
4
+ import type { ProjectApp } from './shared/types.js'
5
+ import { createRunner, BaseRunner, RunnerOutput, RunnerStats } from './appRunner.js'
6
6
 
7
7
  type RunningApp = {
8
8
  runner: BaseRunner
package/appRunner.ts CHANGED
@@ -2,7 +2,7 @@ import { spawn, ChildProcess } from 'child_process'
2
2
  import fs from 'fs/promises'
3
3
  import path from 'path'
4
4
  import os from 'os'
5
- import type { ProjectApp } from './shared/types'
5
+ import type { ProjectApp } from './shared/types.js'
6
6
 
7
7
  /** Cross-platform: run a shell command (sh -c on Unix, cmd /c on Windows) */
8
8
  function shellSpawnOpts(command: string): { shell: string; args: string[] } {
package/bin/exk CHANGED
@@ -2,11 +2,15 @@
2
2
  // exk CLI - entry point for npm global install
3
3
  // Runs the TypeScript CLI via tsx
4
4
 
5
- const { resolve, dirname } = require('path')
6
- const { spawn } = require('child_process')
7
- const fs = require('fs')
5
+ import { resolve, dirname } from 'path'
6
+ import { spawn } from 'child_process'
7
+ import fs from 'fs'
8
+ import { fileURLToPath } from 'url'
8
9
 
9
- const cliDir = dirname(__filename)
10
+ const __filename = fileURLToPath(import.meta.url)
11
+ const __dirname = dirname(__filename)
12
+
13
+ const cliDir = __dirname
10
14
  const pkgDir = resolve(cliDir, '..')
11
15
  const entryPoint = resolve(pkgDir, 'index.ts')
12
16
 
@@ -36,7 +40,7 @@ const child = spawn(tsxBin, [entryPoint, ...args], {
36
40
  })
37
41
 
38
42
  child.on('exit', (code) => {
39
- process.exit(code || 0)
43
+ process.exit(code ?? 0)
40
44
  })
41
45
 
42
46
  child.on('error', (err) => {
package/index.ts CHANGED
@@ -22,12 +22,12 @@ import type {
22
22
  SessionResponse,
23
23
  SessionsListResponse,
24
24
  DevicesListResponse,
25
- } from './shared/types'
26
- import { createProject, deleteProject } from './projectManager'
27
- import { agentSessionManager } from './agentSession'
28
- import { getProjectConfig, analyzeProjectWithClaude, saveProjectConfig } from './projectAnalyzer'
29
- import { generateRunnerCode } from './runnerGenerator'
30
- import { startApp, stopApp, restartApp, getAppStatuses, getAppLogs } from './appManager'
25
+ } from './shared/types.js'
26
+ import { createProject, deleteProject } from './projectManager.js'
27
+ import { agentSessionManager } from './agentSession.js'
28
+ import { getProjectConfig, analyzeProjectWithClaude, saveProjectConfig } from './projectAnalyzer.js'
29
+ import { generateRunnerCode } from './runnerGenerator.js'
30
+ import { startApp, stopApp, restartApp, getAppStatuses, getAppLogs } from './appManager.js'
31
31
  import { spawn, execSync } from 'child_process'
32
32
  import readline from 'readline'
33
33
  import { fileURLToPath } from 'url'
@@ -48,6 +48,7 @@ interface DeviceData {
48
48
  const CONFIG_DIR = path.join(os.homedir(), '.talk-to-code')
49
49
  const CONFIG_FILE = path.join(CONFIG_DIR, 'config.json')
50
50
  const DEVICE_ID_FILE = path.join(CONFIG_DIR, 'device-id.json')
51
+ const DEVICE_CONFIG_FILE = path.join(CONFIG_DIR, 'device-config.json')
51
52
  const DEFAULT_API_URL = 'https://api.talk-to-code.com'
52
53
 
53
54
  // ============ Helpers ============
@@ -71,7 +72,6 @@ async function writeConfig(config: Config): Promise<void> {
71
72
  * Called after device registration and on daemon start
72
73
  */
73
74
  async function fetchAiConfig(authToken: string): Promise<boolean> {
74
- const AI_CONFIG_FILE = path.join(CONFIG_DIR, 'ai-config.json')
75
75
  try {
76
76
  const config = await readConfig()
77
77
  const res = await fetch(`${config.apiUrl}/config/ai`, {
@@ -95,6 +95,82 @@ async function fetchAiConfig(authToken: string): Promise<boolean> {
95
95
  }
96
96
  }
97
97
 
98
+ const AI_CONFIG_FILE = path.join(CONFIG_DIR, 'ai-config.json')
99
+
100
+ /** True if ai-config.json has a model API key (not read from host ANTHROPIC_* env). */
101
+ function hasAiCredentials(): boolean {
102
+ try {
103
+ const raw = fsSync.readFileSync(AI_CONFIG_FILE, 'utf-8')
104
+ const j = JSON.parse(raw) as { authToken?: string }
105
+ return typeof j.authToken === 'string' && j.authToken.trim().length > 0
106
+ } catch {
107
+ return false
108
+ }
109
+ }
110
+
111
+ async function readDeviceAuthToken(): Promise<string | undefined> {
112
+ const env = process.env.TTC_AUTH_TOKEN?.trim()
113
+ if (env) return env
114
+ try {
115
+ const raw = await fs.readFile(DEVICE_CONFIG_FILE, 'utf-8')
116
+ const j = JSON.parse(raw) as { authToken?: string }
117
+ return typeof j.authToken === 'string' ? j.authToken.trim() : undefined
118
+ } catch {
119
+ return undefined
120
+ }
121
+ }
122
+
123
+ type DiskDeviceConfig = { email?: string; authToken?: string }
124
+
125
+ async function readDiskDeviceConfig(): Promise<DiskDeviceConfig> {
126
+ try {
127
+ const raw = await fs.readFile(DEVICE_CONFIG_FILE, 'utf-8')
128
+ return JSON.parse(raw) as DiskDeviceConfig
129
+ } catch {
130
+ return {}
131
+ }
132
+ }
133
+
134
+ /**
135
+ * Update device-config.json without dropping an existing authToken.
136
+ * Omit authToken to keep the file’s current token; pass authToken: '' to clear (rare).
137
+ */
138
+ async function writeDeviceConfigMerged(partial: { email?: string; authToken?: string }): Promise<void> {
139
+ const prev = await readDiskDeviceConfig()
140
+ const email = partial.email !== undefined ? partial.email : prev.email
141
+ let token: string | undefined
142
+ if (partial.authToken !== undefined) {
143
+ token = partial.authToken.trim() || undefined
144
+ } else {
145
+ token = typeof prev.authToken === 'string' && prev.authToken.trim() ? prev.authToken.trim() : undefined
146
+ }
147
+ const out: DiskDeviceConfig = {}
148
+ if (email) out.email = email
149
+ if (token) out.authToken = token
150
+ await fs.mkdir(CONFIG_DIR, { recursive: true })
151
+ await fs.writeFile(DEVICE_CONFIG_FILE, JSON.stringify(out, null, 2))
152
+ }
153
+
154
+ /** If AI cache is empty but we have a device JWT, try GET /config/ai once (non-fatal). */
155
+ async function maybeFetchAiConfigIfMissing(): Promise<void> {
156
+ if (hasAiCredentials()) return
157
+ const jwt = await readDeviceAuthToken()
158
+ if (!jwt) return
159
+ const ok = await fetchAiConfig(jwt)
160
+ if (!ok && !hasAiCredentials()) {
161
+ const cfg = await readConfig()
162
+ console.warn(`[CLI] No AI key in ai-config.json yet. Could not sync from ${cfg.apiUrl}/config/ai — agent prompts will fail until this succeeds.`)
163
+ }
164
+ }
165
+
166
+ function scheduleAiConfigSync(authToken: string): void {
167
+ void fetchAiConfig(authToken).then((ok) => {
168
+ if (!ok && !hasAiCredentials()) {
169
+ console.warn('[CLI] AI config sync failed; check backend /config/ai and apiUrl.')
170
+ }
171
+ })
172
+ }
173
+
98
174
  async function getDeviceId(): Promise<string> {
99
175
  try {
100
176
  const data = await fs.readFile(DEVICE_ID_FILE, 'utf-8')
@@ -147,6 +223,21 @@ async function connect(): Promise<Socket> {
147
223
  })
148
224
  }
149
225
 
226
+ // ============ Version Helpers ============
227
+
228
+ /** Read CLI version from the npm package.json (works when installed via npm i -g @exreve/exk) */
229
+ function getCliVersion(): string {
230
+ try {
231
+ // When installed via npm, package.json is at ../package.json relative to index.ts
232
+ const pkgPath = path.join(__dirname, 'package.json')
233
+ const raw = fsSync.readFileSync(pkgPath, 'utf-8')
234
+ const pkg = JSON.parse(raw) as { version?: string }
235
+ return pkg.version || '0.0.0'
236
+ } catch {
237
+ return '0.0.0'
238
+ }
239
+ }
240
+
150
241
  // ============ Update Helpers ============
151
242
 
152
243
  type UpdateCheckResponse = {
@@ -262,9 +353,8 @@ async function registerDevice(name: string | undefined, email: string | undefine
262
353
  let deviceEmail = email
263
354
 
264
355
  // If we already have a valid token, skip approval email and just register
265
- const deviceConfigFile = path.join(CONFIG_DIR, 'device-config.json')
266
356
  try {
267
- const deviceConfig = JSON.parse(await fs.readFile(deviceConfigFile, 'utf-8'))
357
+ const deviceConfig = await readDiskDeviceConfig()
268
358
  const existingToken = deviceConfig.authToken
269
359
  if (existingToken && typeof existingToken === 'string') {
270
360
  const config = await readConfig()
@@ -282,7 +372,8 @@ async function registerDevice(name: string | undefined, email: string | undefine
282
372
  name: name || `CLI Device ${deviceId.slice(0, 8)}`,
283
373
  ipAddress: getLocalIpAddress(),
284
374
  hostname: getHostname(),
285
- email: deviceEmail
375
+ email: deviceEmail,
376
+ cliVersion: getCliVersion()
286
377
  }, ({ success, device, error }: {
287
378
  success: boolean
288
379
  device?: Device
@@ -391,11 +482,7 @@ async function registerDevice(name: string | undefined, email: string | undefine
391
482
  const token = await checkApproval()
392
483
  if (token) {
393
484
  // Device approved! Store permanent token
394
- const deviceConfigFile = path.join(CONFIG_DIR, 'device-config.json')
395
- await fs.writeFile(deviceConfigFile, JSON.stringify({
396
- email: deviceEmail,
397
- authToken: token
398
- }, null, 2))
485
+ await writeDeviceConfigMerged({ email: deviceEmail, authToken: token })
399
486
 
400
487
  console.log(`\n✓ Device approved!`)
401
488
 
@@ -408,7 +495,8 @@ async function registerDevice(name: string | undefined, email: string | undefine
408
495
  name: name || `CLI Device ${deviceId.slice(0, 8)}`,
409
496
  ipAddress: getLocalIpAddress(),
410
497
  hostname: getHostname(),
411
- email: deviceEmail
498
+ email: deviceEmail,
499
+ cliVersion: getCliVersion()
412
500
  }, ({ success, device, error }: {
413
501
  success: boolean
414
502
  device?: Device
@@ -630,6 +718,8 @@ async function runDaemon(foreground = false, email?: string): Promise<void> {
630
718
  }
631
719
  console.log('')
632
720
 
721
+ await maybeFetchAiConfigIfMissing()
722
+
633
723
  let socket: Socket | null = null
634
724
  let reconnectTimeout: ReturnType<typeof setTimeout> | null = null
635
725
  let reconnectAttempts = 0
@@ -1681,7 +1771,6 @@ async function runDaemon(foreground = false, email?: string): Promise<void> {
1681
1771
  }
1682
1772
 
1683
1773
  // Check for auth token: env TTC_AUTH_TOKEN or device-config.json (from previous approval)
1684
- const deviceConfigFile = path.join(CONFIG_DIR, 'device-config.json')
1685
1774
  let authToken = process.env.TTC_AUTH_TOKEN
1686
1775
  let deviceEmail: string | undefined = email
1687
1776
  let skipEmailFlow = false
@@ -1689,7 +1778,7 @@ async function runDaemon(foreground = false, email?: string): Promise<void> {
1689
1778
  // Load from device-config if no env token (reuse state from previous approval)
1690
1779
  if (!authToken) {
1691
1780
  try {
1692
- const deviceConfig = JSON.parse(await fs.readFile(deviceConfigFile, 'utf-8'))
1781
+ const deviceConfig = await readDiskDeviceConfig()
1693
1782
  authToken = deviceConfig.authToken
1694
1783
  if (!deviceEmail && deviceConfig.email) deviceEmail = deviceConfig.email
1695
1784
  } catch {
@@ -1721,7 +1810,7 @@ async function runDaemon(foreground = false, email?: string): Promise<void> {
1721
1810
  // Load device email from config if not using token and not provided
1722
1811
  if (!deviceEmail && !skipEmailFlow) {
1723
1812
  try {
1724
- const deviceConfig = JSON.parse(await fs.readFile(deviceConfigFile, 'utf-8'))
1813
+ const deviceConfig = await readDiskDeviceConfig()
1725
1814
  deviceEmail = deviceConfig.email
1726
1815
  } catch {
1727
1816
  // No device config yet
@@ -1741,6 +1830,7 @@ async function runDaemon(foreground = false, email?: string): Promise<void> {
1741
1830
  ipAddress,
1742
1831
  hostname,
1743
1832
  email: deviceEmail,
1833
+ cliVersion: getCliVersion(),
1744
1834
  // When using auth token, include the token for verification
1745
1835
  authToken: skipEmailFlow ? authToken : undefined
1746
1836
  }, ({ success, needsApproval, message, device, error }: {
@@ -1760,19 +1850,20 @@ async function runDaemon(foreground = false, email?: string): Promise<void> {
1760
1850
  } else {
1761
1851
  console.log(`Device registered: ${device.name}${device.approved ? ' (approved)' : ' (pending approval)'}`)
1762
1852
  }
1763
- // Save config: email + authToken (preserve token so next run can reuse)
1853
+ // Save config: merge email; keep existing file authToken if in-memory token absent
1764
1854
  if (device.email) {
1765
- const toSave = { email: device.email, ...(authToken ? { authToken } : {}) }
1766
- fs.writeFile(deviceConfigFile, JSON.stringify(toSave, null, 2)).catch(() => {})
1855
+ void writeDeviceConfigMerged({
1856
+ email: device.email,
1857
+ ...(authToken ? { authToken } : {}),
1858
+ }).catch(() => {})
1767
1859
  }
1768
- // Fetch AI config from server (non-blocking, runs on each heartbeat)
1769
1860
  if (authToken) {
1770
- fetchAiConfig(authToken).catch(() => {})
1861
+ scheduleAiConfigSync(authToken)
1771
1862
  }
1772
1863
  } else if (needsApproval) {
1773
- // Persist email when approval was requested so restarts don't prompt again
1864
+ // Persist email when approval was requested; do not wipe authToken
1774
1865
  if (deviceEmail) {
1775
- fs.writeFile(deviceConfigFile, JSON.stringify({ email: deviceEmail }, null, 2)).catch(() => {})
1866
+ void writeDeviceConfigMerged({ email: deviceEmail }).catch(() => {})
1776
1867
  }
1777
1868
  // Only log once to avoid flooding logs every 30s on heartbeat
1778
1869
  if (!needsApprovalLoggedOnce) {
@@ -1809,6 +1900,117 @@ async function runDaemon(foreground = false, email?: string): Promise<void> {
1809
1900
  ;(socket as any).heartbeatInterval = heartbeatInterval
1810
1901
  })
1811
1902
 
1903
+ // ========== Version & Update Handlers ==========
1904
+
1905
+ // Respond with CLI version info
1906
+ socket.on('version:info', (_data: Record<string, never>, callback: (response: {
1907
+ success: boolean
1908
+ version?: string
1909
+ hash?: string
1910
+ date?: string
1911
+ nodeVersion?: string
1912
+ platform?: string
1913
+ arch?: string
1914
+ error?: string
1915
+ }) => void) => {
1916
+ callback({
1917
+ success: true,
1918
+ version: getCliVersion(),
1919
+ hash: getCliHash(),
1920
+ date: new Date().toISOString(),
1921
+ nodeVersion: process.version,
1922
+ platform: os.platform(),
1923
+ arch: os.arch()
1924
+ })
1925
+ })
1926
+
1927
+ // Force update: npm update -g @exreve/exk then restart PM2
1928
+ socket.on('force-update', (_data: Record<string, never>, callback: (response: {
1929
+ success: boolean
1930
+ message?: string
1931
+ error?: string
1932
+ }) => void) => {
1933
+ if (foreground) {
1934
+ console.log('[force-update] Received force update command from server')
1935
+ }
1936
+
1937
+ callback?.({ success: true, message: 'Update initiated' })
1938
+
1939
+ // Run npm update in background, then restart
1940
+ const npmPaths = [
1941
+ path.join(os.homedir(), '.nvm/versions/node/v22/bin/npm'),
1942
+ path.join(os.homedir(), '.nvm/versions/node/v20/bin/npm')
1943
+ ]
1944
+ const npmBin = npmPaths.find(p => fsSync.existsSync(p)) || 'npm'
1945
+
1946
+ const updateProcess = spawn(npmBin, ['update', '-g', '@exreve/exk'], {
1947
+ stdio: 'pipe',
1948
+ detached: true
1949
+ })
1950
+
1951
+ let output = ''
1952
+ updateProcess.stdout?.on('data', (d: Buffer) => { output += d.toString() })
1953
+ updateProcess.stderr?.on('data', (d: Buffer) => { output += d.toString() })
1954
+
1955
+ updateProcess.on('close', (code) => {
1956
+ if (foreground) {
1957
+ console.log(`[force-update] npm update exited with code ${code}`)
1958
+ if (output) console.log(output)
1959
+ }
1960
+
1961
+ // Restart PM2 process "cli" after a short delay
1962
+ setTimeout(() => {
1963
+ try {
1964
+ execSync('pm2 restart cli', { stdio: 'inherit' })
1965
+ } catch {
1966
+ // If pm2 restart fails, just exit and let pm2 restart us
1967
+ process.exit(0)
1968
+ }
1969
+ }, 2000)
1970
+ })
1971
+
1972
+ updateProcess.on('error', (err) => {
1973
+ if (foreground) {
1974
+ console.error(`[force-update] npm update failed: ${err.message}`)
1975
+ }
1976
+ })
1977
+ })
1978
+
1979
+ // ========== update:start handler (legacy compatibility) ==========
1980
+ socket.on('update:start', (_data: Record<string, never>, callback: (response: {
1981
+ success: boolean
1982
+ message?: string
1983
+ error?: string
1984
+ }) => void) => {
1985
+ // Use npm-based self-update
1986
+ if (foreground) {
1987
+ console.log('[update:start] Starting npm self-update...')
1988
+ }
1989
+
1990
+ callback?.({ success: true, message: 'Update started via npm' })
1991
+
1992
+ const npmPaths = [
1993
+ path.join(os.homedir(), '.nvm/versions/node/v22/bin/npm'),
1994
+ path.join(os.homedir(), '.nvm/versions/node/v20/bin/npm')
1995
+ ]
1996
+ const npmBin = npmPaths.find(p => fsSync.existsSync(p)) || 'npm'
1997
+
1998
+ const updateProcess = spawn(npmBin, ['update', '-g', '@exreve/exk'], {
1999
+ stdio: 'pipe',
2000
+ detached: true
2001
+ })
2002
+
2003
+ updateProcess.on('close', () => {
2004
+ setTimeout(() => {
2005
+ try {
2006
+ execSync('pm2 restart cli', { stdio: 'inherit' })
2007
+ } catch {
2008
+ process.exit(0)
2009
+ }
2010
+ }, 2000)
2011
+ })
2012
+ })
2013
+
1812
2014
  // Cloudflared handlers
1813
2015
  socket.on('cloudflared:check:request', async () => {
1814
2016
  try {
@@ -2626,7 +2828,7 @@ const program = new Command()
2626
2828
  program
2627
2829
  .name('exk')
2628
2830
  .description('exk - Control Claude CLI with voice and programmable interfaces')
2629
- .version('1.0.0')
2831
+ .version(getCliVersion())
2630
2832
 
2631
2833
  // Config command
2632
2834
  program
@@ -2655,41 +2857,48 @@ program
2655
2857
  // First register the device
2656
2858
  await registerDevice(undefined, email)
2657
2859
 
2658
- // After successful registration, install as service (Linux only)
2860
+ // After successful registration, install as PM2 service (Linux/macOS only)
2659
2861
  if (process.platform === 'win32') {
2660
2862
  console.log('\n✓ Device registered.')
2661
2863
  console.log('On Windows, run the daemon manually: exk daemon')
2662
2864
  } else {
2663
2865
  console.log('\n📦 Installing exk daemon with pm2...')
2664
- // install-service.sh: bundled at __dirname (npm package root)
2665
- let installScript = path.join(__dirname, 'install-service.sh')
2666
- const scriptDir = path.dirname(installScript)
2667
2866
 
2668
- // Determine paths for the npm-installed package
2669
- const pkgDir = path.resolve(__dirname)
2670
2867
  const exkBin = process.argv[1] || 'exk'
2671
2868
 
2672
2869
  try {
2673
- await fs.access(installScript)
2674
- const { execSync } = await import('child_process')
2675
- execSync(`bash "${installScript}"`, {
2676
- stdio: 'inherit',
2677
- cwd: scriptDir,
2678
- env: {
2679
- ...process.env,
2680
- INSTALL_DIR: CONFIG_DIR,
2681
- EXK_PKG_DIR: pkgDir,
2682
- EXK_BIN: exkBin,
2683
- BINARY_PATH: exkBin
2684
- }
2870
+ // Check if pm2 is installed
2871
+ try {
2872
+ execSync('which pm2', { stdio: 'ignore' })
2873
+ } catch {
2874
+ console.log('Installing pm2...')
2875
+ execSync('npm i -g pm2', { stdio: 'inherit' })
2876
+ }
2877
+
2878
+ // Start exk daemon with pm2
2879
+ execSync(`pm2 start "${exkBin}" --name cli -- daemon`, {
2880
+ stdio: 'inherit'
2685
2881
  })
2882
+
2883
+ // Save pm2 process list for auto-restart on reboot
2884
+ try {
2885
+ execSync('pm2 save', { stdio: 'inherit' })
2886
+ } catch {
2887
+ // Non-critical
2888
+ }
2889
+
2686
2890
  console.log('\n✓ exk installation complete!')
2687
2891
  console.log('The service is now running in the background.')
2688
- } catch (scriptErr: unknown) {
2689
- const err = scriptErr as Error
2690
- console.log('\n⚠ Service installation had issues, but device is registered.')
2691
- const execErr = err as { stderr?: Buffer; stdout?: Buffer }
2692
- const errDetail = execErr.stderr?.toString() || execErr.stdout?.toString() || err.message
2892
+ console.log('\nUseful commands:')
2893
+ console.log(' pm2 logs cli - View logs')
2894
+ console.log(' pm2 restart cli - Restart service')
2895
+ console.log(' pm2 stop cli - Stop service')
2896
+ console.log(' pm2 delete cli - Remove service')
2897
+ } catch (err: unknown) {
2898
+ const error = err as Error
2899
+ console.log('\n⚠ PM2 installation had issues, but device is registered.')
2900
+ const execErr = error as { stderr?: Buffer; stdout?: Buffer }
2901
+ const errDetail = execErr.stderr?.toString() || execErr.stdout?.toString() || error.message
2693
2902
  if (errDetail) console.error('Error:', errDetail)
2694
2903
  console.log('You can start the daemon manually with: exk daemon')
2695
2904
  }
@@ -2720,23 +2929,18 @@ program
2720
2929
 
2721
2930
  if (process.platform === 'win32') {
2722
2931
  console.log('On Windows there is no service to remove.')
2723
- console.log('To remove exk completely: npm uninstall -g exk')
2932
+ console.log('To remove exk completely: npm uninstall -g @exreve/exk')
2724
2933
  } else {
2725
- let installScript = path.join(__dirname, 'install-service.sh')
2726
- const scriptDir = path.dirname(installScript)
2727
2934
  try {
2728
- await fs.access(installScript)
2729
- const { execSync } = await import('child_process')
2730
- execSync(`bash "${installScript}" --uninstall`, {
2731
- stdio: 'inherit',
2732
- cwd: scriptDir,
2733
- env: { ...process.env, PATH: process.env.PATH }
2734
- })
2935
+ execSync('pm2 delete cli', { stdio: 'inherit' })
2936
+ try {
2937
+ execSync('pm2 save', { stdio: 'inherit' })
2938
+ } catch { /* non-critical */ }
2735
2939
  console.log('\n✓ Service uninstalled!')
2736
- console.log('To remove the package completely: npm uninstall -g exk')
2737
- } catch (error) {
2738
- console.log('\n⚠ Service uninstall script not found or failed.')
2739
- console.log('To remove exk: npm uninstall -g exk')
2940
+ console.log('To remove the package completely: npm uninstall -g @exreve/exk')
2941
+ } catch {
2942
+ console.log('\n⚠ PM2 service not found or already removed.')
2943
+ console.log('To remove exk: npm uninstall -g @exreve/exk')
2740
2944
  console.log(' rm -rf ~/.talk-to-code')
2741
2945
  }
2742
2946
  }
@@ -2745,13 +2949,48 @@ program
2745
2949
  process.exit(0)
2746
2950
  })
2747
2951
 
2748
- // Update command
2952
+ // Update command - uses npm to update the global package
2749
2953
  program
2750
2954
  .command('update')
2751
- .description('Check for and apply CLI updates')
2955
+ .description('Update exk to the latest version via npm')
2752
2956
  .option('-f, --force', 'Update without confirmation')
2753
2957
  .action(async (options: { force?: boolean }) => {
2754
- await selfUpdate(options.force)
2958
+ const currentVersion = getCliVersion()
2959
+ console.log(`Current version: ${currentVersion}`)
2960
+
2961
+ // Check latest version from npm
2962
+ try {
2963
+ const latestVersion = execSync('npm view @exreve/exk version', { encoding: 'utf-8' }).trim()
2964
+ console.log(`Latest version: ${latestVersion}`)
2965
+
2966
+ if (latestVersion === currentVersion) {
2967
+ console.log('✓ Already up to date')
2968
+ process.exit(0)
2969
+ }
2970
+
2971
+ if (!options.force && process.stdin.isTTY) {
2972
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout })
2973
+ const answer = await new Promise<string>(r => rl.question('Update now? [Y/n] ', a => { rl.close(); r(a.trim()) }))
2974
+ if (answer.toLowerCase().startsWith('n')) {
2975
+ process.exit(0)
2976
+ }
2977
+ }
2978
+
2979
+ console.log('Updating...')
2980
+ execSync('npm i -g @exreve/exk@latest', { stdio: 'inherit' })
2981
+ console.log(`\n✓ Updated to ${latestVersion}`)
2982
+
2983
+ // Restart PM2 if running
2984
+ try {
2985
+ execSync('pm2 restart cli', { stdio: 'inherit' })
2986
+ } catch {
2987
+ // Not running under PM2, that's fine
2988
+ }
2989
+ } catch (error: any) {
2990
+ console.error('Update failed:', error.message)
2991
+ process.exit(1)
2992
+ }
2993
+
2755
2994
  process.exit(0)
2756
2995
  })
2757
2996
 
@@ -2759,14 +2998,23 @@ program
2759
2998
  .command('check-update')
2760
2999
  .description('Check for updates')
2761
3000
  .action(async () => {
2762
- const info = await checkForUpdate()
2763
- if (!info) process.exit(1)
2764
- if (!info.updateAvailable) {
2765
- console.log('✓ Up to date')
2766
- process.exit(0)
3001
+ const currentVersion = getCliVersion()
3002
+ console.log(`Current version: ${currentVersion}`)
3003
+
3004
+ try {
3005
+ const latestVersion = execSync('npm view @exreve/exk version', { encoding: 'utf-8' }).trim()
3006
+ console.log(`Latest version: ${latestVersion}`)
3007
+
3008
+ if (latestVersion === currentVersion) {
3009
+ console.log('✓ Up to date')
3010
+ } else {
3011
+ console.log(`📦 Update available: ${currentVersion} → ${latestVersion}`)
3012
+ console.log('Run "exk update" to install')
3013
+ }
3014
+ } catch (error: any) {
3015
+ console.error('Failed to check for updates:', error.message)
3016
+ process.exit(1)
2767
3017
  }
2768
- console.log('📦 Update available')
2769
- console.log('Run "exk update" to install')
2770
3018
  process.exit(0)
2771
3019
  })
2772
3020
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@exreve/exk",
3
- "version": "1.0.1",
3
+ "version": "1.0.3",
4
4
  "description": "exk - Control Claude CLI with voice and programmable interfaces",
5
5
  "type": "module",
6
6
  "bin": {
@@ -1,9 +1,9 @@
1
1
  import { v4 as uuidv4 } from 'uuid'
2
2
  import fs from 'fs/promises'
3
3
  import path from 'path'
4
- import type { ProjectConfig } from './shared/types'
5
- import { agentSessionManager } from './agentSession'
6
- import type { SessionOutput } from './shared/types'
4
+ import type { ProjectConfig } from './shared/types.js'
5
+ import { agentSessionManager } from './agentSession.js'
6
+ import type { SessionOutput } from './shared/types.js'
7
7
 
8
8
  const CONFIG_FILENAME = '.claude-voice.json'
9
9
 
@@ -1,4 +1,4 @@
1
- import type { ProjectApp } from './shared/types'
1
+ import type { ProjectApp } from './shared/types.js'
2
2
 
3
3
  /**
4
4
  * Generate TypeScript runner code for an app