handsoff 0.1.3 → 0.1.6-beta.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.
package/dist/cli/index.js CHANGED
@@ -122,12 +122,12 @@ function resetCredentials() {
122
122
  }
123
123
 
124
124
  // src/cli/attach.ts
125
- import { existsSync as existsSync3, readFileSync as readFileSync3, writeFileSync as writeFileSync2, mkdirSync as mkdirSync2 } from "fs";
125
+ import { existsSync as existsSync3, readFileSync as readFileSync3, writeFileSync as writeFileSync3, mkdirSync as mkdirSync2 } from "fs";
126
126
  import { homedir as homedir3 } from "os";
127
127
  import { join as join3, dirname as dirname2 } from "path";
128
128
 
129
129
  // src/config.ts
130
- import { readFileSync as readFileSync2, existsSync as existsSync2 } from "fs";
130
+ import { readFileSync as readFileSync2, writeFileSync as writeFileSync2, existsSync as existsSync2 } from "fs";
131
131
  import { join as join2 } from "path";
132
132
  import { homedir as homedir2 } from "os";
133
133
  import TOML from "@iarna/toml";
@@ -151,8 +151,7 @@ var DEFAULT_CONFIG = {
151
151
  enabled: false,
152
152
  app_id: "",
153
153
  app_secret: "",
154
- allowed_users: [],
155
- notify_types: []
154
+ allowed_users: []
156
155
  },
157
156
  permission: {
158
157
  timeout_ms: 3e5,
@@ -199,23 +198,20 @@ function mergeWithDefaults(raw) {
199
198
  channel: {
200
199
  logger: {
201
200
  ...DEFAULT_CONFIG.channel.logger,
202
- ...raw.channel?.logger || {},
203
- notify_types: raw.channel?.logger?.notify_types
201
+ ...raw.channel?.logger || {}
204
202
  },
205
203
  telegram: {
206
204
  ...DEFAULT_CONFIG.channel.telegram,
207
205
  ...raw.channel?.telegram || {},
208
206
  allowed_users: (raw.channel?.telegram?.allowed_users || []).map(String),
209
- bot_token: process.env.HANDSOFF_TELEGRAM_BOT_TOKEN || raw.channel?.telegram?.bot_token || "",
210
- notify_types: raw.channel?.telegram?.notify_types
207
+ bot_token: process.env.HANDSOFF_TELEGRAM_BOT_TOKEN || raw.channel?.telegram?.bot_token || ""
211
208
  },
212
209
  feishu: {
213
210
  ...DEFAULT_CONFIG.channel.feishu,
214
211
  ...raw.channel?.feishu || {},
215
212
  allowed_users: raw.channel?.feishu?.allowed_users || [],
216
213
  app_id: process.env.HANDSOFF_FEISHU_APP_ID || raw.channel?.feishu?.app_id || "",
217
- app_secret: process.env.HANDSOFF_FEISHU_APP_SECRET || raw.channel?.feishu?.app_secret || "",
218
- notify_types: raw.channel?.feishu?.notify_types
214
+ app_secret: process.env.HANDSOFF_FEISHU_APP_SECRET || raw.channel?.feishu?.app_secret || ""
219
215
  },
220
216
  permission: {
221
217
  ...DEFAULT_CONFIG.channel.permission,
@@ -235,8 +231,7 @@ function mergeWithDefaults(raw) {
235
231
  enabled: raw.channel?.app?.enabled === true,
236
232
  channel_id: String(raw.channel?.app?.channel_id || ""),
237
233
  auth_token: String(raw.channel?.app?.auth_token || ""),
238
- heartbeat_interval_ms: raw.channel?.app?.heartbeat_interval_ms ? parseInt(String(raw.channel?.app?.heartbeat_interval_ms), 10) : void 0,
239
- notify_types: Array.isArray(raw.channel?.app?.notify_types) ? raw.channel.app.notify_types.map(String) : void 0
234
+ heartbeat_interval_ms: raw.channel?.app?.heartbeat_interval_ms ? parseInt(String(raw.channel?.app?.heartbeat_interval_ms), 10) : void 0
240
235
  }
241
236
  },
242
237
  agent: {
@@ -256,9 +251,44 @@ function mergeWithDefaults(raw) {
256
251
  bindings: {
257
252
  ...DEFAULT_CONFIG.bindings,
258
253
  ...raw.bindings || {}
259
- }
254
+ },
255
+ workspaces: raw.workspaces || {}
260
256
  };
261
257
  }
258
+ function migrateLegacyBindings(config) {
259
+ if (!config.bindings) return config;
260
+ const entries = Object.entries(config.bindings);
261
+ const isLegacy = entries.some(
262
+ ([k, v]) => !k.includes(":") && String(v).includes(":")
263
+ );
264
+ if (!isLegacy) return config;
265
+ const newConfig = {
266
+ ...config,
267
+ workspaces: { ...config.workspaces || {} },
268
+ bindings: {}
269
+ };
270
+ for (const [agentType, channelId] of entries) {
271
+ const wsId = agentType;
272
+ newConfig.workspaces[wsId] = {
273
+ name: agentType,
274
+ description: "Migrated from legacy binding"
275
+ };
276
+ if (channelId) {
277
+ newConfig.bindings[channelId] = wsId;
278
+ }
279
+ }
280
+ try {
281
+ const configPath = getConfigPath();
282
+ const raw = TOML.parse(readFileSync2(configPath, "utf-8"));
283
+ raw.workspaces = newConfig.workspaces;
284
+ raw.bindings = newConfig.bindings;
285
+ writeFileSync2(configPath, TOML.stringify(raw), "utf-8");
286
+ console.log("[Config] Migrated legacy bindings to workspace format");
287
+ } catch (err) {
288
+ console.warn("[Config] Failed to persist migrated config:", err);
289
+ }
290
+ return newConfig;
291
+ }
262
292
  function loadConfig() {
263
293
  const configPath = getConfigPath();
264
294
  if (!existsSync2(configPath)) {
@@ -267,7 +297,8 @@ function loadConfig() {
267
297
  try {
268
298
  const content = readFileSync2(configPath, "utf-8");
269
299
  const raw = TOML.parse(content);
270
- return mergeWithDefaults(raw);
300
+ const config = mergeWithDefaults(raw);
301
+ return migrateLegacyBindings(config);
271
302
  } catch (error) {
272
303
  console.error(`Failed to load config from ${configPath}:`, error);
273
304
  return DEFAULT_CONFIG;
@@ -285,63 +316,6 @@ var en_default = {
285
316
  channel: "[ Channel Configuration ]",
286
317
  complete: "[ Setup Complete ]"
287
318
  },
288
- cli: {
289
- runAgain: "Run init again when Claude Code is installed.",
290
- detected: "Claude Code detected.",
291
- hooksInstalled: "Hooks already installed.",
292
- hooksNotInstalled: "Hooks not installed.",
293
- hooksTokenMismatch: "Hooks have outdated token - reconfiguration required.",
294
- actionQuestion: "Select action:",
295
- injectAction: "Inject hooks configuration",
296
- updateAction: "Update hooks configuration",
297
- backAction: "Back",
298
- hookCheckbox: "Select hooks:",
299
- noHooksSelected: "No hooks selected, skipping injection.",
300
- injecting: "Injecting hooks...",
301
- injected: "Hooks injected",
302
- failed: "Failed: {{error}}",
303
- remoteModeSkipHooks: "Remote mode does not require hooks; skipping injection.",
304
- cleaningHooks: "Cleaning up hooks from previous mode...",
305
- hooksCleaned: "Hooks cleaned",
306
- cleanupWarning: "Hook cleanup warning: {{error}}"
307
- },
308
- channel: {
309
- configuring: "Configuring {{channel}}",
310
- telegram: {
311
- botToken: "Telegram bot token:",
312
- allowedUsers: "Allowed Telegram user IDs (comma-separated):",
313
- testing: "Testing Telegram connection...",
314
- connected: "Connected to Telegram",
315
- sendingTest: "Sending test message...",
316
- testSent: "Test message sent",
317
- testContent: "Handsoff test message",
318
- testWarning: "Could not send test message (user may not have messaged bot yet)",
319
- apiError: "API error: {{status}}",
320
- connFailed: "Connection failed: {{error}}"
321
- },
322
- feishu: {
323
- appId: "Feishu App ID:",
324
- appSecret: "Feishu App Secret:",
325
- allowedUsers: "Allowed User IDs (comma-separated, e.g., ou_xxx,oc_xxx):",
326
- required: "app_id and app_secret are required. Skipping."
327
- }
328
- },
329
- codex: {
330
- enable: "Enable Codex agent integration?",
331
- command: "Codex app-server command:",
332
- workDir: "Default Codex working directory (optional):",
333
- model: "Default Codex model (optional):",
334
- approvalPolicy: "Default Codex approval policy (optional):",
335
- detected: "Codex CLI detected.",
336
- notDetected: "Codex CLI not found in PATH.",
337
- enabled: "Codex integration enabled.",
338
- disabled: "Codex integration disabled."
339
- },
340
- notify: {
341
- title: "Notification types for {{channel}}",
342
- checkbox: "Notification types:",
343
- saved: "Notification types saved for {{channel}}"
344
- },
345
319
  menu: {
346
320
  select: "Select:",
347
321
  next: "--- Next ---",
@@ -351,81 +325,137 @@ var en_default = {
351
325
  backToMenu: "Back to menu",
352
326
  restartQuestion: "Restart Gateway now?"
353
327
  },
354
- final: {
355
- noItems: "No items configured.",
356
- itemOk: "{{item}}: OK",
357
- running: "Gateway is running",
358
- runningChanges: "Gateway is running with pending changes",
359
- configModified: "Configuration was modified. Restart required to apply changes.",
360
- runningReady: "Gateway is already running and ready.",
361
- notRunning: "Gateway is not running",
362
- configModifiedStart: "Configuration was modified. Starting Gateway with new configuration.",
363
- restarting: "Restarting...",
364
- restarted: "Gateway restarted with new configuration",
365
- starting: "Starting...",
366
- started: "Gateway started",
367
- startFailed: "Failed: {{error}}",
368
- restartTip: "Run `handsoff gateway start` to restart.",
369
- startTip: "Run `handsoff gateway start` to start.",
370
- done: "Done."
371
- }
372
- },
373
- gateway: {
374
- stopping: "Stopping daemon...",
375
- startedOnPort: "Gateway started on port {{port}}",
376
- stopped: "Daemon stopped",
377
- notRunning: "Daemon is not running",
378
- restartedOnPort: "Gateway restarted on port {{port}}",
379
- startFailed: "Failed to start Gateway",
380
- stopFailed: "Failed to stop Gateway",
381
- restartFailed: "Failed to restart Gateway",
382
- error: " Error: {{error}}",
383
- checkLogs: "Check logs at: {{path}}",
384
- status: {
385
- title: "Gateway Status:",
386
- running: "Running: {{value}}",
387
- sessions: "Sessions: {{count}}",
388
- clients: "Connected clients: {{count}}",
389
- activeSessions: "Active sessions:",
390
- notResponding: "Gateway is not responding",
391
- notRunning: "Gateway is not running"
328
+ step: {
329
+ cli: {
330
+ runAgain: "Run init again when Claude Code is installed.",
331
+ detected: "Claude Code detected.",
332
+ hooksInstalled: "Hooks already installed.",
333
+ hooksNotInstalled: "Hooks not installed.",
334
+ hooksTokenMismatch: "Hooks have outdated token - reconfiguration required.",
335
+ actionQuestion: "Select action:",
336
+ injectAction: "Inject hooks configuration",
337
+ updateAction: "Update hooks configuration",
338
+ backAction: "Back",
339
+ hookCheckbox: "Select hooks:",
340
+ noHooksSelected: "No hooks selected, skipping injection.",
341
+ injecting: "Injecting hooks...",
342
+ injected: "Hooks injected",
343
+ failed: "Failed: {{error}}",
344
+ remoteModeSkipHooks: "Remote mode does not require hooks; skipping injection.",
345
+ cleaningHooks: "Cleaning up hooks from previous mode...",
346
+ hooksCleaned: "Hooks cleaned",
347
+ cleanupWarning: "Hook cleanup warning: {{error}}"
348
+ },
349
+ channel: {
350
+ configuring: "Configuring {{channel}}",
351
+ telegram: {
352
+ botToken: "Telegram bot token:",
353
+ allowedUsers: "Allowed Telegram user IDs (comma-separated):",
354
+ testing: "Testing Telegram connection...",
355
+ connected: "Connected to Telegram",
356
+ sendingTest: "Sending test message...",
357
+ testSent: "Test message sent",
358
+ testContent: "Handsoff test message",
359
+ testWarning: "Could not send test message (user may not have messaged bot yet)",
360
+ apiError: "API error: {{status}}",
361
+ connFailed: "Connection failed: {{error}}"
362
+ },
363
+ feishu: {
364
+ appId: "Feishu App ID:",
365
+ appSecret: "Feishu App Secret:",
366
+ allowedUsers: "Allowed User IDs (comma-separated, e.g., ou_xxx,oc_xxx):",
367
+ required: "app_id and app_secret are required. Skipping."
368
+ },
369
+ subscribedEvents: "Notification types:"
370
+ },
371
+ codex: {
372
+ enable: "Enable Codex agent integration?",
373
+ command: "Codex app-server command:",
374
+ workDir: "Default Codex working directory (optional):",
375
+ model: "Default Codex model (optional):",
376
+ approvalPolicy: "Default Codex approval policy (optional):",
377
+ detected: "Codex CLI detected.",
378
+ notDetected: "Codex CLI not found in PATH.",
379
+ enabled: "Codex integration enabled.",
380
+ disabled: "Codex integration disabled."
381
+ },
382
+ final: {
383
+ noItems: "No items configured.",
384
+ itemOk: "{{item}}: OK",
385
+ running: "Gateway is running",
386
+ runningChanges: "Gateway is running with pending changes",
387
+ configModified: "Configuration was modified. Restart required to apply changes.",
388
+ runningReady: "Gateway is already running and ready.",
389
+ notRunning: "Gateway is not running",
390
+ configModifiedStart: "Configuration was modified. Starting Gateway with new configuration.",
391
+ restarting: "Restarting...",
392
+ restarted: "Gateway restarted with new configuration",
393
+ starting: "Starting...",
394
+ started: "Gateway started",
395
+ startFailed: "Failed: {{error}}",
396
+ restartTip: "Run `handsoff gateway start` to restart.",
397
+ startTip: "Run `handsoff gateway start` to start.",
398
+ done: "Done."
399
+ }
392
400
  }
393
401
  },
394
- daemon: {
395
- alreadyRunning: "Daemon is already running",
396
- startedOnPort: "Daemon started on port {{port}}",
397
- startFailed: "Daemon failed to start. Check logs for details.",
398
- checkLogs: "Check logs at: {{path}}",
399
- portInUse: "Warning: Port {{port}} still in use after {{ms}}ms",
400
- startError: "Failed to start daemon: {{error}}",
401
- lackingToken: "Daemon is running but PID file lacks token, restarting...",
402
- scriptNotFound: "Daemon script not found: {{path}}"
403
- },
404
- status: {
405
- title: "=== Handsoff Status ===",
406
- daemon: {
407
- running: "Daemon: \u2713 Running",
408
- notRunning: "Daemon: \u2717 Not running"
402
+ cli: {
403
+ gateway: {
404
+ stopping: "Stopping daemon...",
405
+ startedOnPort: "Gateway started on port {{port}}",
406
+ stopped: "Daemon stopped",
407
+ notRunning: "Daemon is not running",
408
+ restartedOnPort: "Gateway restarted on port {{port}}",
409
+ startFailed: "Failed to start Gateway",
410
+ stopFailed: "Failed to stop Gateway",
411
+ restartFailed: "Failed to restart Gateway",
412
+ error: " Error: {{error}}",
413
+ checkLogs: "Check logs at: {{path}}",
414
+ status: {
415
+ title: "Gateway Status:",
416
+ running: "Running: {{value}}",
417
+ sessions: "Sessions: {{count}}",
418
+ clients: "Connected clients: {{count}}",
419
+ activeSessions: "Active sessions:",
420
+ notResponding: "Gateway is not responding",
421
+ notRunning: "Gateway is not running"
422
+ }
409
423
  },
410
- port: "Port: {{port}}",
411
- logLevel: "Log Level: {{level}}",
412
- channels: "Channels:",
413
- telegram: {
414
- enabled: "Telegram: \u2713 Enabled",
415
- disabled: "Telegram: \u2717 Disabled"
424
+ daemon: {
425
+ alreadyRunning: "Daemon is already running",
426
+ startedOnPort: "Daemon started on port {{port}}",
427
+ startFailed: "Daemon failed to start. Check logs for details.",
428
+ checkLogs: "Check logs at: {{path}}",
429
+ portInUse: "Warning: Port {{port}} still in use after {{ms}}ms",
430
+ startError: "Failed to start daemon: {{error}}",
431
+ lackingToken: "Daemon is running but PID file lacks token, restarting...",
432
+ staleProcess: "Daemon process exists but HTTP server is not responding, restarting...",
433
+ scriptNotFound: "Daemon script not found: {{path}}"
416
434
  },
417
- feishu: {
418
- enabled: "Feishu: \u2713 Enabled",
419
- disabled: "Feishu: \u2717 Disabled"
435
+ status: {
436
+ title: "=== Handsoff Status ===",
437
+ daemon: {
438
+ running: "Daemon: \u2713 Running",
439
+ notRunning: "Daemon: \u2717 Not running"
440
+ },
441
+ port: "Port: {{port}}",
442
+ logLevel: "Log Level: {{level}}",
443
+ channels: "Channels:",
444
+ telegram: {
445
+ enabled: "Telegram: \u2713 Enabled",
446
+ disabled: "Telegram: \u2717 Disabled"
447
+ },
448
+ feishu: {
449
+ enabled: "Feishu: \u2713 Enabled",
450
+ disabled: "Feishu: \u2717 Disabled"
451
+ },
452
+ settingsBackup: "Settings: \u2713 Backup exists",
453
+ settingsNoBackup: "Settings: \u2717 No backup",
454
+ paths: "Paths:",
455
+ pathConfig: "Config: {{path}}",
456
+ pathLogs: "Logs: {{path}}",
457
+ pathBackup: "Backup: {{path}}"
420
458
  },
421
- settingsBackup: "Settings: \u2713 Backup exists",
422
- settingsNoBackup: "Settings: \u2717 No backup",
423
- paths: "Paths:",
424
- pathConfig: "Config: {{path}}",
425
- pathLogs: "Logs: {{path}}",
426
- pathBackup: "Backup: {{path}}"
427
- },
428
- cli: {
429
459
  init: {
430
460
  configExists: "Configuration exists. Continue?",
431
461
  cancelled: "Cancelled."
@@ -492,7 +522,72 @@ var en_default = {
492
522
  stopping: "\nStopping debug server..."
493
523
  }
494
524
  },
495
- notifications: {
525
+ commands: {
526
+ start: {
527
+ title: "Handsoff",
528
+ hasWorkspace: "Current workspace: **{{name}}** (`{{id}}`).",
529
+ noWorkspace: "No workspace is bound to this channel yet.",
530
+ noWorkspaceWithList: "No workspace is selected for this channel. Available workspaces:",
531
+ switchPrompt: "Use `/workspace switch <id>` to select one.",
532
+ howToUse: "How to use it",
533
+ sendText: "Send plain text to start a session with the default agent (claude).",
534
+ useAgentPrefix: "Use `/claude <text>` or `/codex <text>` to talk to a specific agent.",
535
+ createWorkspace: "Use `/workspace create <name>` to create a workspace.",
536
+ listWorkspaces: "Use `/workspace list` to see available workspaces.",
537
+ switchWorkspace: "Use `/workspace switch <id>` to switch workspace.",
538
+ startFresh: "Use `/new` or `/clear` to start fresh in this chat.",
539
+ inspectSession: "Use `/session` to inspect the current chat session.",
540
+ seeHelp: "Use `/help` to see system commands."
541
+ },
542
+ workspace: {
543
+ noWorkspaces: "No workspaces yet. Use `/workspace create <name>` to create one.",
544
+ listTitle: "Workspaces",
545
+ listItem: "- `{{id}}` \u2014 {{name}} ({{cwd}})",
546
+ noWorkspaceBound: "No workspace is bound to this channel. Use `/workspace switch <id>`.",
547
+ currentTitle: "Current Workspace",
548
+ currentId: "ID: `{{id}}`",
549
+ currentName: "Name: {{name}}",
550
+ currentCwd: "CWD: `{{cwd}}`",
551
+ currentDescription: "Description: {{description}}",
552
+ switchUsage: "Usage: /workspace switch <id>",
553
+ notFound: "Workspace `{{id}}` not found.",
554
+ availableWorkspaces: "Available workspaces:",
555
+ switched: "Switched to workspace '{{name}}' ({{id}}).",
556
+ createUsage: "Usage: /workspace create <name> [--cwd <path>] [--id <id>] [--desc <text>]",
557
+ alreadyExists: "Workspace `{{id}}` already exists.",
558
+ created: "Created workspace '{{name}}' (`{{id}}`) at {{cwd}}",
559
+ createdAndBound: "Created workspace '{{name}}' (`{{id}}`) at {{cwd}} and auto-bound to this channel. You can start sending messages now.",
560
+ deleteUsage: "Usage: /workspace delete <id>",
561
+ deleted: "Deleted workspace '{{name}}' (`{{id}}`).",
562
+ infoNoWorkspaceBound: "No workspace bound to this channel. Use `/workspace switch <id>` or `/workspace info <id>`.",
563
+ infoTitle: "Workspace: {{name}}",
564
+ infoCreated: "Created: {{createdAt}}",
565
+ infoPermissionMode: "Permission Mode: {{mode}}",
566
+ unbindNoWorkspace: "No workspace is bound to this channel.",
567
+ unbound: "Unbound workspace '{{name}}' from this channel.",
568
+ rootNoWorkspace: "No workspace bound. Use `/workspace list` to see available workspaces, or `/workspace switch <id>` to bind one.",
569
+ rootCurrent: "Current workspace: '{{name}}' (`{{id}}`). Use /workspace <subcommand> for actions."
570
+ }
571
+ },
572
+ notify: {
573
+ titles: {
574
+ sessionStart: "\u{1F680} Session Started",
575
+ sessionEnd: "\u{1F44B} Session Ended",
576
+ turnStarted: "\u{1F504} Turn Started",
577
+ turnFinished: "\u2705 Task Completed",
578
+ turnThinking: "\u{1F4AD} Thinking...",
579
+ toolPost: "\u{1F527} Tool Call",
580
+ toolExecuted: "\u2705 Tool Executed",
581
+ toolFailure: "\u274C Tool Failed",
582
+ permissionRequest: "\u{1F510} Permission Request",
583
+ permissionResponse: "\u{1F4CB} Permission Response",
584
+ questionRequest: "\u2753 Question",
585
+ questionResponse: "\u{1F4CB} Question Response",
586
+ systemEvent: "\u{1F4CB} System Event",
587
+ agentMessage: "\u{1F916} Agent Message",
588
+ error: "\u26A0\uFE0F Error",
589
+ fallback: "\u{1F4CB} Notification"
590
+ },
496
591
  permission: {
497
592
  title: "\u{1F510} Permission Request",
498
593
  session: "Session: {{sessionId}}",
@@ -521,23 +616,62 @@ var en_default = {
521
616
  },
522
617
  unauthorized: "You are not authorized to use this bot.",
523
618
  received: "Message received.",
524
- processingError: "Error processing message."
619
+ processingError: "Error processing message.",
620
+ fallback: {
621
+ permission: "Reply /approve or /deny"
622
+ }
525
623
  },
526
- eventTitles: {
527
- sessionStart: "\u{1F680} Session Started",
528
- sessionEnd: "\u{1F44B} Session Ended",
529
- turnFinished: "\u2705 Task Completed",
530
- turnThinking: "\u{1F4AD} Thinking...",
531
- toolPost: "\u{1F527} Tool Call",
532
- toolExecuted: "\u2705 Tool Executed",
533
- toolFailure: "\u274C Tool Failed",
534
- permissionRequest: "\u{1F510} Permission Request",
535
- permissionResponse: "\u{1F4CB} Permission Response",
536
- questionRequest: "\u2753 Question",
537
- questionResponse: "\u{1F4CB} Question Response",
538
- agentMessage: "\u{1F916} Agent Message",
539
- error: "\u26A0\uFE0F Error",
540
- fallback: "\u{1F4CB} Notification"
624
+ agent: {
625
+ claude: {
626
+ command: {
627
+ model: {
628
+ description: "Set default model for Claude",
629
+ usage: "Usage: /model <model-name>",
630
+ usageHint: "Usage: /model <model-name>",
631
+ success: "Model set to {{model}}"
632
+ },
633
+ permission: {
634
+ description: "Set permission mode for Claude",
635
+ usage: "Usage: /permission <mode>",
636
+ invalidMode: "Invalid mode: {{mode}}. Valid modes: {{modes}}",
637
+ success: "Permission mode set to {{mode}}"
638
+ }
639
+ },
640
+ concurrentTurn: "Claude is still processing the previous message. Please wait.",
641
+ sessionEndedUnexpectedly: "Claude session ended unexpectedly."
642
+ },
643
+ codex: {
644
+ clientTitle: "Handsoff Codex Client",
645
+ concurrentTurn: "Codex is still processing the previous message. Please wait."
646
+ }
647
+ },
648
+ channel: {
649
+ fallback: {
650
+ replyByNumber: "Please reply with the number:"
651
+ },
652
+ feishu: {
653
+ card: {
654
+ taskCompleted: "\u2705 Task Completed",
655
+ noOutput: "No output",
656
+ permissionRequired: "Permission Required",
657
+ questionTitle: "Question",
658
+ errorTitle: "\u274C Error",
659
+ submit: "Submit"
660
+ },
661
+ confirm: {
662
+ denyTitle: "Confirm Deny",
663
+ denyText: "Are you sure you want to deny?",
664
+ alwaysAllowTitle: "Confirm Always Allow",
665
+ alwaysAllowText: "Are you sure you want to always allow?"
666
+ },
667
+ toast: {
668
+ processed: "Processed"
669
+ }
670
+ },
671
+ telegram: {
672
+ invalidRequest: "Invalid request",
673
+ unauthorized: "Unauthorized"
674
+ }
541
675
  }
542
676
  };
543
677
 
@@ -559,12 +693,12 @@ function getSettingsPath() {
559
693
  function backupSettings(settingsPath) {
560
694
  const backupPath = settingsPath + ".handsoff-backup";
561
695
  if (existsSync3(settingsPath)) {
562
- writeFileSync2(backupPath, readFileSync3(settingsPath, "utf-8"));
696
+ writeFileSync3(backupPath, readFileSync3(settingsPath, "utf-8"));
563
697
  }
564
698
  }
565
699
  function writeSettings(settingsPath, settings) {
566
700
  mkdirSync2(dirname2(settingsPath), { recursive: true });
567
- writeFileSync2(settingsPath, JSON.stringify(settings, null, 2));
701
+ writeFileSync3(settingsPath, JSON.stringify(settings, null, 2));
568
702
  }
569
703
  function getGatewayBaseUrl(port) {
570
704
  return `http://localhost:${port}`;
@@ -662,7 +796,7 @@ function createInitialState(hasExistingConfig) {
662
796
  }
663
797
 
664
798
  // src/cli/wizard/applicator.ts
665
- import { writeFileSync as writeFileSync3, mkdirSync as mkdirSync3 } from "fs";
799
+ import { writeFileSync as writeFileSync4, mkdirSync as mkdirSync3 } from "fs";
666
800
  import { join as join4 } from "path";
667
801
  import { homedir as homedir4 } from "os";
668
802
  import TOML2 from "@iarna/toml";
@@ -709,7 +843,7 @@ var ConfigApplicator = class {
709
843
  mkdirSync3(configDir, { recursive: true });
710
844
  const configPath = join4(configDir, "config.toml");
711
845
  const content = TOML2.stringify(finalConfig);
712
- writeFileSync3(configPath, content);
846
+ writeFileSync4(configPath, content);
713
847
  }
714
848
  };
715
849
 
@@ -846,47 +980,61 @@ function detectCodex() {
846
980
 
847
981
  // src/cli/wizard/prompts.ts
848
982
  import { confirm, input, checkbox, select } from "@inquirer/prompts";
983
+ var EVENT_TYPE_CHOICES = [
984
+ { name: t("notify.titles.sessionStart"), value: "session:start" },
985
+ { name: t("notify.titles.sessionEnd"), value: "session:end" },
986
+ { name: t("notify.titles.turnStarted"), value: "turn:started" },
987
+ { name: t("notify.titles.turnThinking"), value: "turn:thinking" },
988
+ { name: t("notify.titles.turnFinished"), value: "turn:finished" },
989
+ { name: t("notify.titles.toolPost"), value: "tool:post" },
990
+ { name: t("notify.titles.toolExecuted"), value: "tool:executed" },
991
+ { name: t("notify.titles.toolFailure"), value: "tool:failure" },
992
+ { name: t("notify.titles.permissionRequest"), value: "permission:request" },
993
+ { name: t("notify.titles.questionRequest"), value: "question:request" },
994
+ { name: t("notify.titles.systemEvent"), value: "system:event" },
995
+ { name: t("notify.titles.error"), value: "error" }
996
+ ];
849
997
  var prompts = {
850
998
  feishuAppId: (current) => input({
851
- message: t("wizard.channel.feishu.appId"),
999
+ message: t("wizard.step.channel.feishu.appId"),
852
1000
  default: current || ""
853
1001
  }),
854
1002
  feishuAppSecret: (current) => input({
855
- message: t("wizard.channel.feishu.appSecret"),
1003
+ message: t("wizard.step.channel.feishu.appSecret"),
856
1004
  default: current || ""
857
1005
  }),
858
1006
  feishuAllowedUsers: (current) => input({
859
- message: t("wizard.channel.feishu.allowedUsers"),
1007
+ message: t("wizard.step.channel.feishu.allowedUsers"),
860
1008
  default: current || ""
861
1009
  }),
862
1010
  codexEnable: (enabled) => confirm({
863
- message: t("wizard.codex.enable"),
1011
+ message: t("wizard.step.codex.enable"),
864
1012
  default: enabled
865
1013
  }),
866
1014
  codexCommand: (current) => input({
867
- message: t("wizard.codex.command"),
1015
+ message: t("wizard.step.codex.command"),
868
1016
  default: current || "codex app-server --listen stdio://"
869
1017
  }),
870
1018
  codexWorkDir: (current) => input({
871
- message: t("wizard.codex.workDir"),
1019
+ message: t("wizard.step.codex.workDir"),
872
1020
  default: current || ""
873
1021
  }),
874
1022
  codexModel: (current) => input({
875
- message: t("wizard.codex.model"),
1023
+ message: t("wizard.step.codex.model"),
876
1024
  default: current || ""
877
1025
  }),
878
1026
  codexApprovalPolicy: (current) => input({
879
- message: t("wizard.codex.approvalPolicy"),
1027
+ message: t("wizard.step.codex.approvalPolicy"),
880
1028
  default: current || ""
881
1029
  }),
882
1030
  cliActionSelect: (hooked) => select({
883
- message: t("wizard.cli.actionQuestion"),
1031
+ message: t("wizard.step.cli.actionQuestion"),
884
1032
  choices: hooked ? [
885
- { name: t("wizard.cli.updateAction"), value: "update" },
886
- { name: t("wizard.cli.backAction"), value: "back" }
1033
+ { name: t("wizard.step.cli.updateAction"), value: "update" },
1034
+ { name: t("wizard.step.cli.backAction"), value: "back" }
887
1035
  ] : [
888
- { name: t("wizard.cli.injectAction"), value: "inject" },
889
- { name: t("wizard.cli.backAction"), value: "back" }
1036
+ { name: t("wizard.step.cli.injectAction"), value: "inject" },
1037
+ { name: t("wizard.step.cli.backAction"), value: "back" }
890
1038
  ]
891
1039
  }),
892
1040
  claudeMode: () => select({
@@ -909,7 +1057,7 @@ var prompts = {
909
1057
  default: defaultValue ?? "acceptEdits"
910
1058
  }),
911
1059
  cliHookSelect: (selected) => checkbox({
912
- message: t("wizard.cli.hookCheckbox"),
1060
+ message: t("wizard.step.cli.hookCheckbox"),
913
1061
  choices: [
914
1062
  { name: "Stop", value: "Stop", checked: selected?.includes("Stop") ?? true },
915
1063
  { name: "SessionStart", value: "SessionStart", checked: selected?.includes("SessionStart") ?? true },
@@ -924,11 +1072,11 @@ var prompts = {
924
1072
  ]
925
1073
  }),
926
1074
  botToken: (current) => input({
927
- message: t("wizard.channel.telegram.botToken"),
1075
+ message: t("wizard.step.channel.telegram.botToken"),
928
1076
  default: current || ""
929
1077
  }),
930
1078
  allowedUsers: (current) => input({
931
- message: t("wizard.channel.telegram.allowedUsers"),
1079
+ message: t("wizard.step.channel.telegram.allowedUsers"),
932
1080
  default: current || ""
933
1081
  }),
934
1082
  channelMenuSelect: (channels) => select({
@@ -941,16 +1089,6 @@ var prompts = {
941
1089
  { name: t("wizard.menu.next"), value: "__next__" }
942
1090
  ]
943
1091
  }),
944
- notifyTypes: (selected) => checkbox({
945
- message: t("wizard.notify.checkbox"),
946
- choices: [
947
- { name: "permission:request", value: "permission:request", checked: selected?.includes("permission:request") },
948
- { name: "question:request", value: "question:request", checked: selected?.includes("question:request") },
949
- { name: "turn:finished", value: "turn:finished", checked: selected?.includes("turn:finished") },
950
- { name: "session:start", value: "session:start", checked: selected?.includes("session:start") },
951
- { name: "error", value: "error", checked: selected?.includes("error") }
952
- ]
953
- }),
954
1092
  channelConfigError: () => select({
955
1093
  message: t("wizard.menu.testFailed"),
956
1094
  choices: [
@@ -988,11 +1126,18 @@ var prompts = {
988
1126
  channelSelect: (channels) => select({
989
1127
  message: "Select a channel to configure:",
990
1128
  choices: channels.map((c) => ({ name: `${c.name} ${c.status}`, value: c.value }))
1129
+ }),
1130
+ subscribedEventsSelect: (selected) => checkbox({
1131
+ message: t("wizard.step.channel.subscribedEvents"),
1132
+ choices: EVENT_TYPE_CHOICES.map((choice) => ({
1133
+ ...choice,
1134
+ checked: selected?.includes(choice.value) ?? true
1135
+ }))
991
1136
  })
992
1137
  };
993
1138
 
994
1139
  // src/cli/wizard/steps/cli.ts
995
- import { writeFileSync as writeFileSync4, existsSync as existsSync5, readFileSync as readFileSync5, mkdirSync as mkdirSync4 } from "fs";
1140
+ import { writeFileSync as writeFileSync5, existsSync as existsSync5, readFileSync as readFileSync5, mkdirSync as mkdirSync4 } from "fs";
996
1141
  import { dirname as dirname3 } from "path";
997
1142
  import pc from "picocolors";
998
1143
  import ora from "ora";
@@ -1009,21 +1154,21 @@ ${STEP} \u2014 Configure Agent
1009
1154
  const detection = state.claudeDetection ?? detectClaude();
1010
1155
  if (!detection.supported) {
1011
1156
  console.log(pc.yellow(`! ${detection.message}`));
1012
- console.log(pc.gray(` ${t("wizard.cli.runAgain")}
1157
+ console.log(pc.gray(` ${t("wizard.step.cli.runAgain")}
1013
1158
  `));
1014
1159
  return { action: "next", next: "cli-menu" };
1015
1160
  }
1016
- console.log(pc.green(`* ${t("wizard.cli.detected")}`));
1161
+ console.log(pc.green(`* ${t("wizard.step.cli.detected")}`));
1017
1162
  console.log(pc.gray(` Settings: ${detection.settingsPath}`));
1018
1163
  const port = loadConfig().general.hook_server_port;
1019
1164
  const token = getHookToken();
1020
1165
  console.log(pc.gray(` Gateway: http://localhost:${port}/hook/${token ? token.substring(0, 8) + "..." : "unknown"}`));
1021
1166
  if (detection.hooked) {
1022
- console.log(pc.green(` ${t("wizard.cli.hooksInstalled")}`));
1167
+ console.log(pc.green(` ${t("wizard.step.cli.hooksInstalled")}`));
1023
1168
  } else if (detection.tokenMismatch) {
1024
- console.log(pc.red(` ${t("wizard.cli.hooksTokenMismatch")}`));
1169
+ console.log(pc.red(` ${t("wizard.step.cli.hooksTokenMismatch")}`));
1025
1170
  } else {
1026
- console.log(pc.yellow(` ${t("wizard.cli.hooksNotInstalled")}`));
1171
+ console.log(pc.yellow(` ${t("wizard.step.cli.hooksNotInstalled")}`));
1027
1172
  }
1028
1173
  console.log("");
1029
1174
  const mode = await prompts.claudeMode();
@@ -1035,29 +1180,29 @@ ${STEP} \u2014 Configure Agent
1035
1180
  }
1036
1181
  const selectedHooks = await prompts.cliHookSelect();
1037
1182
  if (selectedHooks.length === 0) {
1038
- console.log(pc.yellow(`! ${t("wizard.cli.noHooksSelected")}
1183
+ console.log(pc.yellow(`! ${t("wizard.step.cli.noHooksSelected")}
1039
1184
  `));
1040
1185
  return { action: "next", next: "cli-menu" };
1041
1186
  }
1042
- const spinner = ora(t("wizard.cli.injecting")).start();
1187
+ const spinner = ora(t("wizard.step.cli.injecting")).start();
1043
1188
  try {
1044
1189
  injectClaudeHooks(detection.settingsPath, selectedHooks);
1045
- spinner.succeed(t("wizard.cli.injected"));
1190
+ spinner.succeed(t("wizard.step.cli.injected"));
1046
1191
  } catch (err) {
1047
- spinner.fail(t("wizard.cli.failed", { error: String(err) }));
1192
+ spinner.fail(t("wizard.step.cli.failed", { error: String(err) }));
1048
1193
  state.itemStatus.set("claude", "error");
1049
1194
  state.validationResults.set("claude", { ok: false, message: String(err) });
1050
1195
  return { action: "next", next: "cli-menu" };
1051
1196
  }
1052
1197
  } else {
1053
- const spinner = ora(t("wizard.cli.cleaningHooks")).start();
1198
+ const spinner = ora(t("wizard.step.cli.cleaningHooks")).start();
1054
1199
  try {
1055
1200
  cleanupClaudeHooks(detection.settingsPath);
1056
- spinner.succeed(t("wizard.cli.hooksCleaned"));
1201
+ spinner.succeed(t("wizard.step.cli.hooksCleaned"));
1057
1202
  } catch (err) {
1058
- spinner.warn(t("wizard.cli.cleanupWarning", { error: String(err) }));
1203
+ spinner.warn(t("wizard.step.cli.cleanupWarning", { error: String(err) }));
1059
1204
  }
1060
- console.log(pc.yellow(`! ${t("wizard.cli.remoteModeSkipHooks")}`));
1205
+ console.log(pc.yellow(`! ${t("wizard.step.cli.remoteModeSkipHooks")}`));
1061
1206
  try {
1062
1207
  execSync3("claude --version", { stdio: "ignore" });
1063
1208
  } catch {
@@ -1084,14 +1229,14 @@ ${STEP} \u2014 Configure Agent
1084
1229
  const config = loadConfig();
1085
1230
  const current = config.agent.codex;
1086
1231
  if (!detection.installed) {
1087
- console.log(pc.yellow(`! ${t("wizard.codex.notDetected")}`));
1232
+ console.log(pc.yellow(`! ${t("wizard.step.codex.notDetected")}`));
1088
1233
  console.log(pc.gray(` ${detection.message}
1089
1234
  `));
1090
1235
  state.itemStatus.set("codex", "not-found");
1091
1236
  state.validationResults.set("codex", { ok: false, message: detection.message });
1092
1237
  return { action: "next", next: "cli-menu" };
1093
1238
  }
1094
- console.log(pc.green(`* ${t("wizard.codex.detected")}`));
1239
+ console.log(pc.green(`* ${t("wizard.step.codex.detected")}`));
1095
1240
  console.log(pc.gray(` Binary: ${detection.binaryPath}`));
1096
1241
  console.log(pc.gray(` Version: ${detection.version || "unknown"}`));
1097
1242
  console.log("");
@@ -1105,8 +1250,8 @@ ${STEP} \u2014 Configure Agent
1105
1250
  }
1106
1251
  };
1107
1252
  state.itemStatus.set("codex", "unconfigured");
1108
- state.validationResults.set("codex", { ok: true, message: t("wizard.codex.disabled") });
1109
- console.log(pc.gray(` ${t("wizard.codex.disabled")}
1253
+ state.validationResults.set("codex", { ok: true, message: t("wizard.step.codex.disabled") });
1254
+ console.log(pc.gray(` ${t("wizard.step.codex.disabled")}
1110
1255
  `));
1111
1256
  return { action: "next", next: "cli-menu" };
1112
1257
  }
@@ -1127,8 +1272,8 @@ ${STEP} \u2014 Configure Agent
1127
1272
  };
1128
1273
  state.itemStatus.set("codex", "configured");
1129
1274
  state.sessionConfigured.add("codex");
1130
- state.validationResults.set("codex", { ok: true, message: t("wizard.codex.enabled") });
1131
- console.log(pc.green(` ${t("wizard.codex.enabled")}
1275
+ state.validationResults.set("codex", { ok: true, message: t("wizard.step.codex.enabled") });
1276
+ console.log(pc.green(` ${t("wizard.step.codex.enabled")}
1132
1277
  `));
1133
1278
  return { action: "next", next: "channel-select" };
1134
1279
  }
@@ -1154,7 +1299,7 @@ function injectClaudeHooks(settingsPath, events) {
1154
1299
  hooksConfig
1155
1300
  );
1156
1301
  mkdirSync4(dirname3(settingsPath), { recursive: true });
1157
- writeFileSync4(settingsPath, JSON.stringify(mergedSettings, null, 2));
1302
+ writeFileSync5(settingsPath, JSON.stringify(mergedSettings, null, 2));
1158
1303
  }
1159
1304
  var COMMAND_ONLY_HOOKS = ["SessionStart", "Setup"];
1160
1305
  function generateHooksConfig2(port, token, events) {
@@ -1188,7 +1333,7 @@ function cleanupClaudeHooks(settingsPath) {
1188
1333
  delete cleanedSettings.hooks;
1189
1334
  }
1190
1335
  mkdirSync4(dirname3(settingsPath), { recursive: true });
1191
- writeFileSync4(settingsPath, JSON.stringify(cleanedSettings, null, 2));
1336
+ writeFileSync5(settingsPath, JSON.stringify(cleanedSettings, null, 2));
1192
1337
  }
1193
1338
 
1194
1339
  // src/cli/wizard/detectors/channel.ts
@@ -1288,20 +1433,20 @@ ${STEP2} \u2014 Configure Channel
1288
1433
  const botToken = await prompts.botToken(currentConfig.bot_token);
1289
1434
  const allowedUsers = await prompts.allowedUsers(String(currentConfig.allowed_users || ""));
1290
1435
  const userIds = allowedUsers.split(",").map((s) => s.trim()).filter((s) => s.length > 0 && /^\d+$/.test(s));
1291
- const spinner = ora2(t("wizard.channel.telegram.testing")).start();
1436
+ const spinner = ora2(t("wizard.step.channel.telegram.testing")).start();
1292
1437
  let connected = false;
1293
1438
  try {
1294
1439
  const response = await fetch(`https://api.telegram.org/bot${botToken}/getMe`, {
1295
1440
  signal: AbortSignal.timeout(8e3)
1296
1441
  });
1297
1442
  if (!response.ok) {
1298
- spinner.fail(t("wizard.channel.telegram.apiError", { status: response.status }));
1443
+ spinner.fail(t("wizard.step.channel.telegram.apiError", { status: response.status }));
1299
1444
  } else {
1300
- spinner.succeed(t("wizard.channel.telegram.connected"));
1445
+ spinner.succeed(t("wizard.step.channel.telegram.connected"));
1301
1446
  connected = true;
1302
1447
  }
1303
1448
  } catch (err) {
1304
- spinner.fail(t("wizard.channel.telegram.connFailed", { error: String(err) }));
1449
+ spinner.fail(t("wizard.step.channel.telegram.connFailed", { error: String(err) }));
1305
1450
  }
1306
1451
  if (!connected) {
1307
1452
  state.itemStatus.set(channelName, "error");
@@ -1312,21 +1457,23 @@ ${STEP2} \u2014 Configure Channel
1312
1457
  state.itemStatus.set(channelName, "unconfigured");
1313
1458
  return { action: "next", next: "channel-select" };
1314
1459
  }
1315
- const msgSpinner = ora2(t("wizard.channel.telegram.sendingTest")).start();
1460
+ const msgSpinner = ora2(t("wizard.step.channel.telegram.sendingTest")).start();
1316
1461
  try {
1317
1462
  await fetch(`https://api.telegram.org/bot${botToken}/sendMessage`, {
1318
1463
  method: "POST",
1319
1464
  headers: { "Content-Type": "application/json" },
1320
- body: JSON.stringify({ chat_id: userIds[0] || 0, text: t("wizard.channel.telegram.testContent") }),
1465
+ body: JSON.stringify({ chat_id: userIds[0] || 0, text: t("wizard.step.channel.telegram.testContent") }),
1321
1466
  signal: AbortSignal.timeout(8e3)
1322
1467
  });
1323
- msgSpinner.succeed(t("wizard.channel.telegram.testSent"));
1468
+ msgSpinner.succeed(t("wizard.step.channel.telegram.testSent"));
1324
1469
  } catch {
1325
- msgSpinner.warn(t("wizard.channel.telegram.testWarning"));
1470
+ msgSpinner.warn(t("wizard.step.channel.telegram.testWarning"));
1326
1471
  }
1472
+ const existingEvents = Array.isArray(currentConfig.subscribed_events) ? currentConfig.subscribed_events : void 0;
1473
+ const selectedEvents = await prompts.subscribedEventsSelect(existingEvents);
1327
1474
  state.pendingChanges.channel = {
1328
1475
  ...state.pendingChanges.channel || {},
1329
- telegram: { enabled: true, bot_token: botToken, allowed_users: userIds }
1476
+ telegram: { enabled: true, bot_token: botToken, allowed_users: userIds, subscribed_events: selectedEvents }
1330
1477
  };
1331
1478
  state.itemStatus.set("telegram", "configured");
1332
1479
  state.sessionConfigured.add("telegram");
@@ -1347,15 +1494,17 @@ ${STEP2} \u2014 Configure Channel
1347
1494
  const appSecret = await prompts.feishuAppSecret(String(currentConfig.app_secret || ""));
1348
1495
  const allowedUsersInput = await prompts.feishuAllowedUsers(String(currentConfig.allowed_users || ""));
1349
1496
  if (!appId || !appSecret) {
1350
- console.log(pc3.yellow(`! ${t("wizard.channel.feishu.required")}
1497
+ console.log(pc3.yellow(`! ${t("wizard.step.channel.feishu.required")}
1351
1498
  `));
1352
1499
  state.itemStatus.set("feishu", "unconfigured");
1353
1500
  return { action: "next", next: "channel-select" };
1354
1501
  }
1355
1502
  const allowedUsers = allowedUsersInput.split(",").map((s) => s.trim()).filter((s) => s.length > 0);
1503
+ const existingEvents = Array.isArray(currentConfig.subscribed_events) ? currentConfig.subscribed_events : void 0;
1504
+ const selectedEvents = await prompts.subscribedEventsSelect(existingEvents);
1356
1505
  state.pendingChanges.channel = {
1357
1506
  ...state.pendingChanges.channel || {},
1358
- feishu: { enabled: true, app_id: appId, app_secret: appSecret, allowed_users: allowedUsers }
1507
+ feishu: { enabled: true, app_id: appId, app_secret: appSecret, allowed_users: allowedUsers, subscribed_events: selectedEvents }
1359
1508
  };
1360
1509
  state.itemStatus.set("feishu", "configured");
1361
1510
  state.sessionConfigured.add("feishu");
@@ -1365,49 +1514,26 @@ ${STEP2} \u2014 Configure Channel
1365
1514
  }
1366
1515
  }
1367
1516
 
1368
- // src/cli/wizard/steps/channel-notify.ts
1369
- import pc4 from "picocolors";
1370
- async function stepChannelNotify(state) {
1371
- const channelName = state.currentChannelName || "telegram";
1372
- console.log(pc4.bold(`
1373
- ${t("wizard.notify.title", { channel: channelName })}
1374
- `));
1375
- const cfg = loadConfig();
1376
- const channelConfig = cfg.channel[channelName] || {};
1377
- const currentTypes = channelConfig.notify_types || [];
1378
- const selected = await prompts.notifyTypes(currentTypes);
1379
- if (!state.pendingChanges.channel) state.pendingChanges.channel = {};
1380
- if (!state.pendingChanges.channel[channelName]) state.pendingChanges.channel[channelName] = {};
1381
- state.pendingChanges.channel[channelName] = {
1382
- ...state.pendingChanges.channel[channelName],
1383
- notify_types: selected
1384
- };
1385
- console.log(pc4.green(`
1386
- \u2713 ${t("wizard.notify.saved", { channel: channelName })}
1387
- `));
1388
- return { action: "next", next: "channel-menu" };
1389
- }
1390
-
1391
1517
  // src/cli/wizard/steps/agent-select.ts
1392
- import pc5 from "picocolors";
1518
+ import pc4 from "picocolors";
1393
1519
  var STEP3 = "STEP 1";
1394
1520
  async function stepAgentSelect(state) {
1395
- console.log(pc5.bold(`
1521
+ console.log(pc4.bold(`
1396
1522
  ${STEP3} \u2014 Select Agent
1397
1523
  `));
1398
- console.log(pc5.gray(`${"\u2500".repeat(50)}
1524
+ console.log(pc4.gray(`${"\u2500".repeat(50)}
1399
1525
  `));
1400
1526
  const config = loadConfig();
1401
1527
  const currentBindings = config.bindings ?? {};
1402
1528
  const agents = [];
1403
1529
  const claudeConfigured = !!config.agent.claude;
1404
1530
  const claudeBoundTo = currentBindings["claude"];
1405
- const claudeStatus = claudeBoundTo ? pc5.green(`[bound to ${claudeBoundTo}]`) : claudeConfigured ? pc5.green("[configured]") : pc5.gray("[not configured]");
1531
+ const claudeStatus = claudeBoundTo ? pc4.green(`[bound to ${claudeBoundTo}]`) : claudeConfigured ? pc4.green("[configured]") : pc4.gray("[not configured]");
1406
1532
  agents.push({ name: "claude", value: "claude", status: claudeStatus });
1407
1533
  const codexDetection = detectCodex();
1408
1534
  const codexConfigured = !!config.agent.codex?.enabled;
1409
1535
  const codexBoundTo = currentBindings["codex"];
1410
- const codexStatus = codexBoundTo ? pc5.green(`[bound to ${codexBoundTo}]`) : codexConfigured ? pc5.green("[configured]") : codexDetection.installed ? pc5.gray("[not configured]") : pc5.red("[not installed]");
1536
+ const codexStatus = codexBoundTo ? pc4.green(`[bound to ${codexBoundTo}]`) : codexConfigured ? pc4.green("[configured]") : codexDetection.installed ? pc4.gray("[not configured]") : pc4.red("[not installed]");
1411
1537
  agents.push({ name: "codex", value: "codex", status: codexStatus });
1412
1538
  const choice = await prompts.agentSelect(agents);
1413
1539
  state.currentAgentName = choice;
@@ -1430,13 +1556,13 @@ function getChannelInstanceId(channelType, credential) {
1430
1556
  }
1431
1557
 
1432
1558
  // src/cli/wizard/steps/channel-select.ts
1433
- import pc6 from "picocolors";
1559
+ import pc5 from "picocolors";
1434
1560
  var STEP4 = "STEP 3";
1435
1561
  async function stepChannelSelect(state) {
1436
- console.log(pc6.bold(`
1562
+ console.log(pc5.bold(`
1437
1563
  ${STEP4} \u2014 Select Channel
1438
1564
  `));
1439
- console.log(pc6.gray(`${"\u2500".repeat(50)}
1565
+ console.log(pc5.gray(`${"\u2500".repeat(50)}
1440
1566
  `));
1441
1567
  const config = loadConfig();
1442
1568
  const currentBindings = config.bindings ?? {};
@@ -1448,22 +1574,22 @@ ${STEP4} \u2014 Select Channel
1448
1574
  const loggerEnabled = mergedLogger?.enabled === true;
1449
1575
  const loggerInstanceId = "logger:default";
1450
1576
  const loggerBoundAgent = Object.entries(currentBindings).find(([, v]) => v === loggerInstanceId)?.[0];
1451
- const loggerStatus = loggerBoundAgent ? pc6.green(`[bound to ${loggerBoundAgent}]`) : loggerEnabled ? pc6.green("[configured]") : pc6.gray("[not configured]");
1577
+ const loggerStatus = loggerBoundAgent ? pc5.green(`[bound to ${loggerBoundAgent}]`) : loggerEnabled ? pc5.green("[configured]") : pc5.gray("[not configured]");
1452
1578
  if (mergedTelegram?.enabled && mergedTelegram.bot_token) {
1453
1579
  const instanceId = getChannelInstanceId("telegram", mergedTelegram.bot_token);
1454
1580
  const boundAgent = Object.entries(currentBindings).find(([, v]) => v === instanceId)?.[0];
1455
- const status = boundAgent ? pc6.green(`[bound to ${boundAgent}]`) : pc6.green("[configured]");
1581
+ const status = boundAgent ? pc5.green(`[bound to ${boundAgent}]`) : pc5.green("[configured]");
1456
1582
  channels.push({ name: "telegram", value: "telegram", status });
1457
1583
  } else {
1458
- channels.push({ name: "telegram", value: "telegram", status: pc6.gray("[not configured]") });
1584
+ channels.push({ name: "telegram", value: "telegram", status: pc5.gray("[not configured]") });
1459
1585
  }
1460
1586
  if (mergedFeishu?.enabled && mergedFeishu.app_id) {
1461
1587
  const instanceId = getChannelInstanceId("feishu", mergedFeishu.app_id);
1462
1588
  const boundAgent = Object.entries(currentBindings).find(([, v]) => v === instanceId)?.[0];
1463
- const status = boundAgent ? pc6.green(`[bound to ${boundAgent}]`) : pc6.green("[configured]");
1589
+ const status = boundAgent ? pc5.green(`[bound to ${boundAgent}]`) : pc5.green("[configured]");
1464
1590
  channels.push({ name: "feishu", value: "feishu", status });
1465
1591
  } else {
1466
- channels.push({ name: "feishu", value: "feishu", status: pc6.gray("[not configured]") });
1592
+ channels.push({ name: "feishu", value: "feishu", status: pc5.gray("[not configured]") });
1467
1593
  }
1468
1594
  channels.push({ name: "logger", value: "logger", status: loggerStatus });
1469
1595
  const choice = await prompts.channelSelect(channels);
@@ -1476,15 +1602,15 @@ ${STEP4} \u2014 Select Channel
1476
1602
  }
1477
1603
 
1478
1604
  // src/cli/wizard/steps/binding-confirm.ts
1479
- import pc7 from "picocolors";
1605
+ import pc6 from "picocolors";
1480
1606
  var STEP5 = "STEP 5";
1481
1607
  async function stepBindingConfirm(state) {
1482
1608
  const agentName = state.currentAgentName;
1483
1609
  const channelName = state.currentChannelName;
1484
- console.log(pc7.bold(`
1610
+ console.log(pc6.bold(`
1485
1611
  ${STEP5} \u2014 Confirm Binding
1486
1612
  `));
1487
- console.log(pc7.gray(`${"\u2500".repeat(50)}
1613
+ console.log(pc6.gray(`${"\u2500".repeat(50)}
1488
1614
  `));
1489
1615
  const config = loadConfig();
1490
1616
  const currentBindings = { ...config.bindings ?? {} };
@@ -1507,8 +1633,8 @@ ${STEP5} \u2014 Confirm Binding
1507
1633
  instanceId = "logger:default";
1508
1634
  channelCurrentAgent = Object.entries(currentBindings).find(([, v]) => v === instanceId)?.[0];
1509
1635
  }
1510
- console.log(` ${pc7.cyan(agentName)} current binding: ${agentCurrentChannel ? pc7.yellow(agentCurrentChannel) : pc7.gray("none")}`);
1511
- console.log(` ${pc7.cyan(channelName)} current binding: ${channelCurrentAgent ? pc7.yellow(channelCurrentAgent) : pc7.gray("none")}`);
1636
+ console.log(` ${pc6.cyan(agentName)} current binding: ${agentCurrentChannel ? pc6.yellow(agentCurrentChannel) : pc6.gray("none")}`);
1637
+ console.log(` ${pc6.cyan(channelName)} current binding: ${channelCurrentAgent ? pc6.yellow(channelCurrentAgent) : pc6.gray("none")}`);
1512
1638
  console.log("");
1513
1639
  const choice = await prompts.bindingConfirm(agentName, channelName);
1514
1640
  if (choice === "confirm" && instanceId) {
@@ -1519,26 +1645,26 @@ ${STEP5} \u2014 Confirm Binding
1519
1645
  }
1520
1646
  }
1521
1647
  if (oldChannelId && oldChannelId !== instanceId) {
1522
- console.log(pc7.yellow(` \u26A0 ${agentName} migrated from ${oldChannelId} to ${instanceId}`));
1648
+ console.log(pc6.yellow(` \u26A0 ${agentName} migrated from ${oldChannelId} to ${instanceId}`));
1523
1649
  }
1524
1650
  state.bindings[agentName] = instanceId;
1525
1651
  state.sessionConfigured.add(`binding:${agentName}`);
1526
- console.log(pc7.green(` \u2713 ${agentName} bound to ${channelName}`));
1652
+ console.log(pc6.green(` \u2713 ${agentName} bound to ${channelName}`));
1527
1653
  } else {
1528
- console.log(pc7.gray(` Binding cancelled`));
1654
+ console.log(pc6.gray(` Binding cancelled`));
1529
1655
  }
1530
1656
  console.log("");
1531
1657
  return { action: "next", next: "pair-continue" };
1532
1658
  }
1533
1659
 
1534
1660
  // src/cli/wizard/steps/pair-continue.ts
1535
- import pc8 from "picocolors";
1661
+ import pc7 from "picocolors";
1536
1662
  var STEP6 = "STEP 6";
1537
1663
  async function stepPairContinue(state) {
1538
- console.log(pc8.bold(`
1664
+ console.log(pc7.bold(`
1539
1665
  ${STEP6} \u2014 Add Another Pair?
1540
1666
  `));
1541
- console.log(pc8.gray(`${"\u2500".repeat(50)}
1667
+ console.log(pc7.gray(`${"\u2500".repeat(50)}
1542
1668
  `));
1543
1669
  const choice = await prompts.pairContinue();
1544
1670
  if (choice === "continue") {
@@ -1557,7 +1683,7 @@ import { fileURLToPath } from "url";
1557
1683
  import { mkdirSync as mkdirSync5, existsSync as existsSync7 } from "fs";
1558
1684
 
1559
1685
  // src/shared/pidfile.ts
1560
- import { writeFileSync as writeFileSync5, readFileSync as readFileSync6, existsSync as existsSync6, unlinkSync } from "fs";
1686
+ import { writeFileSync as writeFileSync6, readFileSync as readFileSync6, existsSync as existsSync6, unlinkSync } from "fs";
1561
1687
  function readPidFile(pidFilePath) {
1562
1688
  if (!existsSync6(pidFilePath)) {
1563
1689
  return null;
@@ -1591,6 +1717,18 @@ function isPortInUse(port) {
1591
1717
  });
1592
1718
  });
1593
1719
  }
1720
+ async function isHttpReady(port, timeoutMs = 2e3) {
1721
+ const controller = new AbortController();
1722
+ const timer = setTimeout(() => controller.abort(), timeoutMs);
1723
+ try {
1724
+ const response = await fetch(`http://localhost:${port}/health`, { signal: controller.signal });
1725
+ clearTimeout(timer);
1726
+ return response.ok;
1727
+ } catch {
1728
+ clearTimeout(timer);
1729
+ return false;
1730
+ }
1731
+ }
1594
1732
  var DaemonManager = class {
1595
1733
  pidFilePath;
1596
1734
  port;
@@ -1622,12 +1760,18 @@ var DaemonManager = class {
1622
1760
  const existingPidData = readPidFile(this.pidFilePath);
1623
1761
  if (existingPidData && isProcessRunning(existingPidData.pid)) {
1624
1762
  if (!existingPidData.token) {
1625
- console.log(t("daemon.lackingToken"));
1763
+ console.log(t("cli.daemon.lackingToken"));
1626
1764
  await this.stop();
1627
1765
  await this.waitForPortFree();
1628
1766
  } else {
1629
- console.log(t("daemon.alreadyRunning"));
1630
- return true;
1767
+ const ready = await isHttpReady(this.port, 3e3);
1768
+ if (ready) {
1769
+ console.log(t("cli.daemon.alreadyRunning"));
1770
+ return true;
1771
+ }
1772
+ console.log(t("cli.daemon.staleProcess"));
1773
+ await this.stop();
1774
+ await this.waitForPortFree();
1631
1775
  }
1632
1776
  }
1633
1777
  try {
@@ -1641,7 +1785,7 @@ var DaemonManager = class {
1641
1785
  daemonScript = join6(__dirname3, "..", "..", "src", "gateway", "process.ts");
1642
1786
  }
1643
1787
  if (!existsSync7(daemonScript)) {
1644
- this.lastError = t("daemon.scriptNotFound", { path: daemonScript });
1788
+ this.lastError = t("cli.daemon.scriptNotFound", { path: daemonScript });
1645
1789
  console.error(this.lastError);
1646
1790
  return false;
1647
1791
  }
@@ -1660,23 +1804,26 @@ var DaemonManager = class {
1660
1804
  });
1661
1805
  child.unref();
1662
1806
  let pidData = null;
1663
- for (let i = 0; i < 30; i++) {
1664
- await new Promise((resolve) => setTimeout(resolve, 1e3));
1807
+ for (let i = 0; i < 150; i++) {
1808
+ await new Promise((resolve) => setTimeout(resolve, 200));
1665
1809
  pidData = readPidFile(this.pidFilePath);
1666
1810
  if (pidData) {
1667
1811
  if (isProcessRunning(pidData.pid)) {
1668
- console.log(t("daemon.startedOnPort", { port: this.port }));
1669
- return true;
1812
+ const httpReady = await isHttpReady(this.port, 2e3);
1813
+ if (httpReady) {
1814
+ console.log(t("cli.daemon.startedOnPort", { port: this.port }));
1815
+ return true;
1816
+ }
1670
1817
  }
1671
1818
  }
1672
1819
  }
1673
- this.lastError = t("daemon.startFailed");
1820
+ this.lastError = t("cli.daemon.startFailed");
1674
1821
  console.error(this.lastError);
1675
- console.error(t("daemon.checkLogs", { path: logFile }));
1822
+ console.error(t("cli.daemon.checkLogs", { path: logFile }));
1676
1823
  return false;
1677
1824
  } catch (error) {
1678
1825
  const errorMsg = error instanceof Error ? error.message : String(error);
1679
- this.lastError = t("daemon.startError", { error: errorMsg });
1826
+ this.lastError = t("cli.daemon.startError", { error: errorMsg });
1680
1827
  console.error(this.lastError);
1681
1828
  return false;
1682
1829
  }
@@ -1709,7 +1856,7 @@ var DaemonManager = class {
1709
1856
  const startTime = Date.now();
1710
1857
  while (await isPortInUse(this.port)) {
1711
1858
  if (Date.now() - startTime > maxWaitMs) {
1712
- console.log(t("daemon.portInUse", { port: this.port, ms: maxWaitMs }));
1859
+ console.log(t("cli.daemon.portInUse", { port: this.port, ms: maxWaitMs }));
1713
1860
  break;
1714
1861
  }
1715
1862
  await new Promise((resolve) => setTimeout(resolve, 200));
@@ -1720,20 +1867,20 @@ var DaemonManager = class {
1720
1867
  // src/cli/wizard/steps/final.ts
1721
1868
  import { join as join7 } from "path";
1722
1869
  import { homedir as homedir6 } from "os";
1723
- import pc9 from "picocolors";
1870
+ import pc8 from "picocolors";
1724
1871
  import ora3 from "ora";
1725
1872
  async function stepFinal(state) {
1726
- console.log(pc9.bold(`
1873
+ console.log(pc8.bold(`
1727
1874
  ${t("wizard.section.complete")}
1728
1875
  `));
1729
1876
  if (state.sessionConfigured.size === 0) {
1730
- console.log(pc9.gray(` ${t("wizard.final.noItems")}
1877
+ console.log(pc8.gray(` ${t("wizard.step.final.noItems")}
1731
1878
  `));
1732
1879
  } else {
1733
1880
  for (const item of state.sessionConfigured) {
1734
1881
  const result = state.validationResults.get(item);
1735
1882
  if (result?.ok) {
1736
- console.log(pc9.green(`* ${t("wizard.final.itemOk", { item })}`));
1883
+ console.log(pc8.green(`* ${t("wizard.step.final.itemOk", { item })}`));
1737
1884
  }
1738
1885
  }
1739
1886
  console.log("");
@@ -1744,74 +1891,74 @@ ${t("wizard.section.complete")}
1744
1891
  const hasChanges = state.sessionConfigured.size > 0 || Object.keys(state.pendingChanges).length > 0 || Object.keys(state.bindings).length > 0;
1745
1892
  if (daemon.isRunning()) {
1746
1893
  if (hasChanges) {
1747
- console.log(pc9.blue(`i ${t("wizard.final.runningChanges")}
1894
+ console.log(pc8.blue(`i ${t("wizard.step.final.runningChanges")}
1748
1895
  `));
1749
- console.log(pc9.yellow(` ${t("wizard.final.configModified")}
1896
+ console.log(pc8.yellow(` ${t("wizard.step.final.configModified")}
1750
1897
  `));
1751
1898
  const shouldRestart = await prompts.restartGateway();
1752
1899
  if (shouldRestart) {
1753
- const spinner = ora3(t("wizard.final.restarting")).start();
1900
+ const spinner = ora3(t("wizard.step.final.restarting")).start();
1754
1901
  await daemon.stop();
1755
1902
  const started = await daemon.start();
1756
1903
  if (started) {
1757
- spinner.succeed(t("wizard.final.restarted"));
1904
+ spinner.succeed(t("wizard.step.final.restarted"));
1758
1905
  } else {
1759
- spinner.fail(t("wizard.final.startFailed", { error: daemon.getLastError() ?? "unknown" }));
1906
+ spinner.fail(t("wizard.step.final.startFailed", { error: daemon.getLastError() ?? "unknown" }));
1760
1907
  }
1761
1908
  } else {
1762
- console.log(pc9.gray(` ${t("wizard.final.restartTip")}
1909
+ console.log(pc8.gray(` ${t("wizard.step.final.restartTip")}
1763
1910
  `));
1764
1911
  }
1765
1912
  } else {
1766
- console.log(pc9.blue(`i ${t("wizard.final.running")}
1913
+ console.log(pc8.blue(`i ${t("wizard.step.final.running")}
1767
1914
  `));
1768
- console.log(pc9.green(` ${t("wizard.final.runningReady")}
1915
+ console.log(pc8.green(` ${t("wizard.step.final.runningReady")}
1769
1916
  `));
1770
1917
  }
1771
1918
  } else {
1772
- console.log(pc9.blue(`i ${t("wizard.final.notRunning")}
1919
+ console.log(pc8.blue(`i ${t("wizard.step.final.notRunning")}
1773
1920
  `));
1774
1921
  if (hasChanges) {
1775
- console.log(pc9.yellow(` ${t("wizard.final.configModifiedStart")}
1922
+ console.log(pc8.yellow(` ${t("wizard.step.final.configModifiedStart")}
1776
1923
  `));
1777
1924
  }
1778
1925
  const shouldStart = await prompts.restartGateway();
1779
1926
  if (shouldStart) {
1780
- const spinner2 = ora3(t("wizard.final.starting")).start();
1927
+ const spinner2 = ora3(t("wizard.step.final.starting")).start();
1781
1928
  const started = await daemon.start();
1782
1929
  if (started) {
1783
- spinner2.succeed(t("wizard.final.started"));
1930
+ spinner2.succeed(t("wizard.step.final.started"));
1784
1931
  } else {
1785
- spinner2.fail(t("wizard.final.startFailed", { error: daemon.getLastError() ?? "unknown" }));
1932
+ spinner2.fail(t("wizard.step.final.startFailed", { error: daemon.getLastError() ?? "unknown" }));
1786
1933
  }
1787
1934
  } else {
1788
- console.log(pc9.gray(` ${t("wizard.final.startTip")}
1935
+ console.log(pc8.gray(` ${t("wizard.step.final.startTip")}
1789
1936
  `));
1790
1937
  }
1791
1938
  }
1792
- console.log(pc9.green(`
1793
- ${t("wizard.final.done")}
1939
+ console.log(pc8.green(`
1940
+ ${t("wizard.step.final.done")}
1794
1941
  `));
1795
1942
  return { action: "next", next: "done" };
1796
1943
  }
1797
1944
 
1798
1945
  // src/cli/wizard/steps/permission-mode.ts
1799
- import pc10 from "picocolors";
1946
+ import pc9 from "picocolors";
1800
1947
  var STEP7 = "STEP 3";
1801
1948
  async function stepPermissionMode(state) {
1802
- console.log(pc10.bold(`
1949
+ console.log(pc9.bold(`
1803
1950
  ${STEP7} \u2014 Permission Mode
1804
1951
  `));
1805
- console.log(pc10.gray(`${"\u2500".repeat(50)}
1952
+ console.log(pc9.gray(`${"\u2500".repeat(50)}
1806
1953
  `));
1807
1954
  const config = loadConfig();
1808
1955
  const current = state.pendingChanges?.agent?.claude;
1809
1956
  const saved = current?.permission_mode ?? config.agent.claude?.permission_mode ?? null;
1810
1957
  const detected = detectPermissionMode();
1811
1958
  const initial = saved ?? detected ?? "acceptEdits";
1812
- console.log(pc10.gray("Controls how Claude Code handles permission requests.\n"));
1959
+ console.log(pc9.gray("Controls how Claude Code handles permission requests.\n"));
1813
1960
  if (detected && !saved) {
1814
- console.log(pc10.gray(`Detected from ~/.claude/settings.json: ${detected}
1961
+ console.log(pc9.gray(`Detected from ~/.claude/settings.json: ${detected}
1815
1962
  `));
1816
1963
  }
1817
1964
  const selected = await prompts.permissionModeSelect(initial);
@@ -1888,8 +2035,6 @@ var WizardEngine = class {
1888
2035
  return await stepCliMenu(this.state);
1889
2036
  case "channel-config":
1890
2037
  return await stepChannelConfig(this.state);
1891
- case "channel-notify":
1892
- return await stepChannelNotify(this.state);
1893
2038
  case "final":
1894
2039
  return await stepFinal(this.state);
1895
2040
  default:
@@ -2171,29 +2316,29 @@ var statusCommand = new Command4("status").description("Show current status").ac
2171
2316
  const pidFilePath = join11(homedir10(), ".handsoff", "handsoff.pid");
2172
2317
  const daemon = new DaemonManager(pidFilePath, config.general.hook_server_port);
2173
2318
  console.log(`
2174
- ${t("status.title")}
2319
+ ${t("cli.status.title")}
2175
2320
  `);
2176
- console.log(daemon.isRunning() ? t("status.daemon.running") : t("status.daemon.notRunning"));
2177
- console.log(t("status.port", { port: config.general.hook_server_port }));
2178
- console.log(`${t("status.logLevel", { level: config.general.log_level })}
2321
+ console.log(daemon.isRunning() ? t("cli.status.daemon.running") : t("cli.status.daemon.notRunning"));
2322
+ console.log(t("cli.status.port", { port: config.general.hook_server_port }));
2323
+ console.log(`${t("cli.status.logLevel", { level: config.general.log_level })}
2179
2324
  `);
2180
- console.log(t("status.channels"));
2181
- console.log(` ${config.channel.telegram?.enabled ? t("status.telegram.enabled") : t("status.telegram.disabled")}`);
2182
- console.log(` ${config.channel.feishu?.enabled ? t("status.feishu.enabled") : t("status.feishu.disabled")}
2325
+ console.log(t("cli.status.channels"));
2326
+ console.log(` ${config.channel.telegram?.enabled ? t("cli.status.telegram.enabled") : t("cli.status.telegram.disabled")}`);
2327
+ console.log(` ${config.channel.feishu?.enabled ? t("cli.status.feishu.enabled") : t("cli.status.feishu.disabled")}
2183
2328
  `);
2184
2329
  const settingsPath = getSettingsPath2();
2185
2330
  const backupPath = settingsPath + ".handsoff-backup";
2186
2331
  if (existsSync11(backupPath)) {
2187
- console.log(t("status.settingsBackup"));
2332
+ console.log(t("cli.status.settingsBackup"));
2188
2333
  } else {
2189
- console.log(t("status.settingsNoBackup"));
2334
+ console.log(t("cli.status.settingsNoBackup"));
2190
2335
  }
2191
2336
  const home = homedir10();
2192
2337
  console.log(`
2193
- ${t("status.paths")}`);
2194
- console.log(` ${t("status.pathConfig", { path: `${home}/.handsoff/config.toml` })}`);
2195
- console.log(` ${t("status.pathLogs", { path: `${home}/.handsoff/logs/gateway.log` })}`);
2196
- console.log(` ${t("status.pathBackup", { path: `${home}/.claude/settings.json.handsoff-backup` })}
2338
+ ${t("cli.status.paths")}`);
2339
+ console.log(` ${t("cli.status.pathConfig", { path: `${home}/.handsoff/config.toml` })}`);
2340
+ console.log(` ${t("cli.status.pathLogs", { path: `${home}/.handsoff/logs/gateway.log` })}`);
2341
+ console.log(` ${t("cli.status.pathBackup", { path: `${home}/.claude/settings.json.handsoff-backup` })}
2197
2342
  `);
2198
2343
  });
2199
2344
 
@@ -2229,20 +2374,20 @@ var gatewayCommand = new Command6("gateway").description("Manage handsoff Gatewa
2229
2374
  const port = options.port ? parseInt(options.port, 10) : config.general.hook_server_port;
2230
2375
  const daemon = new DaemonManager(PID_FILE, port);
2231
2376
  if (daemon.isRunning()) {
2232
- console.log(t("gateway.stopping"));
2377
+ console.log(t("cli.gateway.stopping"));
2233
2378
  await daemon.stop();
2234
2379
  }
2235
2380
  const started = await daemon.start();
2236
2381
  if (started) {
2237
- console.log(t("gateway.startedOnPort", { port }));
2382
+ console.log(t("cli.gateway.startedOnPort", { port }));
2238
2383
  } else {
2239
2384
  const error = daemon.getLastError();
2240
- console.error(t("gateway.startFailed"));
2385
+ console.error(t("cli.gateway.startFailed"));
2241
2386
  if (error) {
2242
- console.error(t("gateway.error", { error }));
2387
+ console.error(t("cli.gateway.error", { error }));
2243
2388
  }
2244
2389
  const logPath = join13(homedir12(), ".handsoff", "logs", "gateway.log");
2245
- console.error(` ${t("gateway.checkLogs", { path: logPath })}`);
2390
+ console.error(` ${t("cli.gateway.checkLogs", { path: logPath })}`);
2246
2391
  process.exit(1);
2247
2392
  }
2248
2393
  })
@@ -2251,15 +2396,15 @@ var gatewayCommand = new Command6("gateway").description("Manage handsoff Gatewa
2251
2396
  const config = loadConfig();
2252
2397
  const daemon = new DaemonManager(PID_FILE, config.general.hook_server_port);
2253
2398
  if (!daemon.isRunning()) {
2254
- console.log(t("gateway.notRunning"));
2399
+ console.log(t("cli.gateway.notRunning"));
2255
2400
  return;
2256
2401
  }
2257
- console.log(t("gateway.stopping"));
2402
+ console.log(t("cli.gateway.stopping"));
2258
2403
  const stopped = await daemon.stop();
2259
2404
  if (stopped) {
2260
- console.log(t("gateway.stopped"));
2405
+ console.log(t("cli.gateway.stopped"));
2261
2406
  } else {
2262
- console.error(t("gateway.stopFailed"));
2407
+ console.error(t("cli.gateway.stopFailed"));
2263
2408
  process.exit(1);
2264
2409
  }
2265
2410
  })
@@ -2271,15 +2416,15 @@ var gatewayCommand = new Command6("gateway").description("Manage handsoff Gatewa
2271
2416
  await daemon.stop();
2272
2417
  const started = await daemon.start();
2273
2418
  if (started) {
2274
- console.log(t("gateway.restartedOnPort", { port }));
2419
+ console.log(t("cli.gateway.restartedOnPort", { port }));
2275
2420
  } else {
2276
2421
  const error = daemon.getLastError();
2277
- console.error(t("gateway.restartFailed"));
2422
+ console.error(t("cli.gateway.restartFailed"));
2278
2423
  if (error) {
2279
- console.error(t("gateway.error", { error }));
2424
+ console.error(t("cli.gateway.error", { error }));
2280
2425
  }
2281
2426
  const logPath = join13(homedir12(), ".handsoff", "logs", "gateway.log");
2282
- console.error(` ${t("gateway.checkLogs", { path: logPath })}`);
2427
+ console.error(` ${t("cli.gateway.checkLogs", { path: logPath })}`);
2283
2428
  process.exit(1);
2284
2429
  }
2285
2430
  })
@@ -2290,13 +2435,13 @@ var gatewayCommand = new Command6("gateway").description("Manage handsoff Gatewa
2290
2435
  const response = await fetch(`http://localhost:${config.general.hook_server_port}/status`);
2291
2436
  if (response.ok) {
2292
2437
  const status = await response.json();
2293
- console.log(t("gateway.status.title"));
2294
- console.log(` ${t("gateway.status.running", { value: status.running ?? true })}`);
2295
- console.log(` ${t("gateway.status.sessions", { count: status.sessions?.length ?? (typeof status.sessions === "number" ? status.sessions : 0) })}`);
2296
- console.log(` ${t("gateway.status.clients", { count: status.clients ?? 0 })}`);
2438
+ console.log(t("cli.gateway.status.title"));
2439
+ console.log(` ${t("cli.gateway.status.running", { value: status.running ?? true })}`);
2440
+ console.log(` ${t("cli.gateway.status.sessions", { count: status.sessions?.length ?? (typeof status.sessions === "number" ? status.sessions : 0) })}`);
2441
+ console.log(` ${t("cli.gateway.status.clients", { count: status.clients ?? 0 })}`);
2297
2442
  if (status.sessions && status.sessions.length > 0) {
2298
2443
  console.log(`
2299
- ${t("gateway.status.activeSessions")}`);
2444
+ ${t("cli.gateway.status.activeSessions")}`);
2300
2445
  for (const session of status.sessions) {
2301
2446
  const agentType = session.agentType || "unknown";
2302
2447
  const sessionId = session.sessionId || session.id || "unknown";
@@ -2304,10 +2449,10 @@ var gatewayCommand = new Command6("gateway").description("Manage handsoff Gatewa
2304
2449
  }
2305
2450
  }
2306
2451
  } else {
2307
- console.log(t("gateway.status.notResponding"));
2452
+ console.log(t("cli.gateway.status.notResponding"));
2308
2453
  }
2309
2454
  } catch {
2310
- console.log(t("gateway.status.notRunning"));
2455
+ console.log(t("cli.gateway.status.notRunning"));
2311
2456
  }
2312
2457
  })
2313
2458
  );
@@ -2344,19 +2489,19 @@ var debugCommand = new Command7("debug").description("Debug tools for Handsoff")
2344
2489
  import { spawn as spawn3 } from "child_process";
2345
2490
  import { homedir as homedir13 } from "os";
2346
2491
  import { join as join15, dirname as dirname6 } from "path";
2347
- import { readFileSync as readFileSync7, existsSync as existsSync12, writeFileSync as writeFileSync6, mkdirSync as mkdirSync6 } from "fs";
2492
+ import { readFileSync as readFileSync7, existsSync as existsSync12, writeFileSync as writeFileSync7, mkdirSync as mkdirSync6 } from "fs";
2348
2493
  function getSettingsPath3() {
2349
2494
  return join15(homedir13(), ".claude", "settings.json");
2350
2495
  }
2351
2496
  function backupSettings2(settingsPath) {
2352
2497
  const backupPath = settingsPath + ".handsoff-backup";
2353
2498
  if (existsSync12(settingsPath)) {
2354
- writeFileSync6(backupPath, readFileSync7(settingsPath, "utf-8"));
2499
+ writeFileSync7(backupPath, readFileSync7(settingsPath, "utf-8"));
2355
2500
  }
2356
2501
  }
2357
2502
  function writeSettings2(settingsPath, settings) {
2358
2503
  mkdirSync6(dirname6(settingsPath), { recursive: true });
2359
- writeFileSync6(settingsPath, JSON.stringify(settings, null, 2));
2504
+ writeFileSync7(settingsPath, JSON.stringify(settings, null, 2));
2360
2505
  }
2361
2506
  function generateHooksConfig3(port, token) {
2362
2507
  const unifiedUrl = `http://localhost:${port}/hook/${token}/event`;