handsoff 0.0.1-beta.3 → 0.1.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
@@ -615,15 +615,15 @@ var attachCommand = new Command("attach").description("Attach an agent to the Ga
615
615
 
616
616
  // src/cli/init.ts
617
617
  import { Command as Command2 } from "commander";
618
- import { existsSync as existsSync10 } from "fs";
619
- import { join as join10 } from "path";
620
- import { homedir as homedir9 } from "os";
621
- import { confirm as confirm2 } from "@inquirer/prompts";
622
-
623
- // src/cli/wizard/engine.ts
624
618
  import { existsSync as existsSync9 } from "fs";
625
619
  import { join as join9 } from "path";
626
620
  import { homedir as homedir8 } from "os";
621
+ import { confirm as confirm2 } from "@inquirer/prompts";
622
+
623
+ // src/cli/wizard/engine.ts
624
+ import { existsSync as existsSync8 } from "fs";
625
+ import { join as join8 } from "path";
626
+ import { homedir as homedir7 } from "os";
627
627
 
628
628
  // src/cli/wizard/state.ts
629
629
  function createInitialState(hasExistingConfig) {
@@ -651,7 +651,10 @@ var ConfigApplicator = class {
651
651
  general: { ...current.general },
652
652
  channel: {
653
653
  ...current.channel,
654
- ...pending.channel || {}
654
+ logger: pending.channel?.logger ? { ...current.channel?.logger ?? {}, ...pending.channel.logger } : current.channel?.logger,
655
+ telegram: pending.channel?.telegram ? { ...current.channel?.telegram ?? {}, ...pending.channel.telegram } : current.channel?.telegram,
656
+ feishu: pending.channel?.feishu ? { ...current.channel?.feishu ?? {}, ...pending.channel.feishu } : current.channel?.feishu,
657
+ permission: pending.channel?.permission ? { ...current.channel?.permission ?? {}, ...pending.channel.permission } : current.channel?.permission
655
658
  },
656
659
  agent: {
657
660
  ...current.agent,
@@ -986,6 +989,7 @@ ${STEP} \u2014 Configure Agent
986
989
  claude: { adapter: "hooks" }
987
990
  };
988
991
  state.validationResults.set("claude", { ok: true });
992
+ return { action: "next", next: "channel-select" };
989
993
  } catch (err) {
990
994
  spinner.fail(t("wizard.cli.failed", { error: String(err) }));
991
995
  state.itemStatus.set("claude", "error");
@@ -1043,6 +1047,7 @@ ${STEP} \u2014 Configure Agent
1043
1047
  state.validationResults.set("codex", { ok: true, message: t("wizard.codex.enabled") });
1044
1048
  console.log(pc.green(` ${t("wizard.codex.enabled")}
1045
1049
  `));
1050
+ return { action: "next", next: "channel-select" };
1046
1051
  }
1047
1052
  return { action: "next", next: "cli-menu" };
1048
1053
  }
@@ -1140,15 +1145,18 @@ async function stepCliMenu(state) {
1140
1145
  console.log(pc2.bold(`
1141
1146
  ${t("wizard.section.cli")}
1142
1147
  `));
1143
- const cliStatus = state.itemStatus.get("claude") || "unconfigured";
1144
- let label = getChannelStatusLabel(cliStatus);
1148
+ const claudeStatus = state.itemStatus.get("claude") || "unconfigured";
1149
+ let claudeLabel = getChannelStatusLabel(claudeStatus);
1145
1150
  if (state.claudeDetection?.tokenMismatch) {
1146
- label = pc2.yellow("[update needed]");
1147
- }
1148
- const choice = await prompts.channelMenuSelect([{
1149
- name: "claude",
1150
- status: label
1151
- }]);
1151
+ claudeLabel = pc2.yellow("[update needed]");
1152
+ }
1153
+ const codexStatus = state.itemStatus.get("codex") || "not-found";
1154
+ const codexLabel = getChannelStatusLabel(codexStatus, state.validationResults.get("codex")?.message);
1155
+ const cliOptions = [
1156
+ { name: "claude", status: claudeLabel },
1157
+ { name: "codex", status: codexLabel }
1158
+ ];
1159
+ const choice = await prompts.channelMenuSelect(cliOptions);
1152
1160
  if (choice === "__next__") {
1153
1161
  return { action: "next", next: "channel-menu" };
1154
1162
  }
@@ -1197,7 +1205,7 @@ ${STEP2} \u2014 Configure Channel
1197
1205
  continue;
1198
1206
  }
1199
1207
  state.itemStatus.set(channelName, "unconfigured");
1200
- return { action: "next", next: "agent-select" };
1208
+ return { action: "next", next: "channel-select" };
1201
1209
  }
1202
1210
  const msgSpinner = ora2(t("wizard.channel.telegram.sendingTest")).start();
1203
1211
  try {
@@ -1237,7 +1245,7 @@ ${STEP2} \u2014 Configure Channel
1237
1245
  console.log(pc3.yellow(`! ${t("wizard.channel.feishu.required")}
1238
1246
  `));
1239
1247
  state.itemStatus.set("feishu", "unconfigured");
1240
- return { action: "next", next: "agent-select" };
1248
+ return { action: "next", next: "channel-select" };
1241
1249
  }
1242
1250
  const allowedUsers = allowedUsersInput.split(",").map((s) => s.trim()).filter((s) => s.length > 0);
1243
1251
  state.pendingChanges.channel = {
@@ -1303,6 +1311,9 @@ ${STEP3} \u2014 Select Agent
1303
1311
  if (result.action === "back") {
1304
1312
  return { action: "next", next: "pair-continue" };
1305
1313
  }
1314
+ if (result.next === "cli-menu") {
1315
+ return { action: "next", next: "agent-select" };
1316
+ }
1306
1317
  return { action: "next", next: "channel-select" };
1307
1318
  }
1308
1319
 
@@ -1324,17 +1335,26 @@ ${STEP4} \u2014 Select Channel
1324
1335
  `));
1325
1336
  const config = loadConfig();
1326
1337
  const currentBindings = config.bindings ?? {};
1338
+ const pendingChannels = state.pendingChanges.channel ?? {};
1339
+ const mergedTelegram = pendingChannels.telegram ? { ...config.channel.telegram, ...pendingChannels.telegram } : config.channel.telegram;
1340
+ const mergedFeishu = pendingChannels.feishu ? { ...config.channel.feishu, ...pendingChannels.feishu } : config.channel.feishu;
1341
+ const mergedLogger = pendingChannels.logger ? { ...config.channel.logger, ...pendingChannels.logger } : config.channel.logger;
1327
1342
  const channels = [];
1328
- if (config.channel.telegram?.enabled && config.channel.telegram.bot_token) {
1329
- const instanceId = getChannelInstanceId("telegram", config.channel.telegram.bot_token);
1343
+ const loggerEnabled = mergedLogger?.enabled === true;
1344
+ const loggerInstanceId = "logger:default";
1345
+ const loggerBoundAgent = Object.entries(currentBindings).find(([, v]) => v === loggerInstanceId)?.[0];
1346
+ const loggerStatus = loggerBoundAgent ? pc6.green(`[bound to ${loggerBoundAgent}]`) : loggerEnabled ? pc6.green("[configured]") : pc6.gray("[not configured]");
1347
+ channels.push({ name: "logger", value: "logger", status: loggerStatus });
1348
+ if (mergedTelegram?.enabled && mergedTelegram.bot_token) {
1349
+ const instanceId = getChannelInstanceId("telegram", mergedTelegram.bot_token);
1330
1350
  const boundAgent = Object.entries(currentBindings).find(([, v]) => v === instanceId)?.[0];
1331
1351
  const status = boundAgent ? pc6.green(`[bound to ${boundAgent}]`) : pc6.green("[configured]");
1332
1352
  channels.push({ name: "telegram", value: "telegram", status });
1333
1353
  } else {
1334
1354
  channels.push({ name: "telegram", value: "telegram", status: pc6.gray("[not configured]") });
1335
1355
  }
1336
- if (config.channel.feishu?.enabled && config.channel.feishu.app_id) {
1337
- const instanceId = getChannelInstanceId("feishu", config.channel.feishu.app_id);
1356
+ if (mergedFeishu?.enabled && mergedFeishu.app_id) {
1357
+ const instanceId = getChannelInstanceId("feishu", mergedFeishu.app_id);
1338
1358
  const boundAgent = Object.entries(currentBindings).find(([, v]) => v === instanceId)?.[0];
1339
1359
  const status = boundAgent ? pc6.green(`[bound to ${boundAgent}]`) : pc6.green("[configured]");
1340
1360
  channels.push({ name: "feishu", value: "feishu", status });
@@ -1350,223 +1370,6 @@ ${STEP4} \u2014 Select Channel
1350
1370
  return { action: "next", next: "binding-confirm" };
1351
1371
  }
1352
1372
 
1353
- // src/shared/logger.ts
1354
- import pino from "pino";
1355
- import { mkdirSync as mkdirSync5, existsSync as existsSync6, appendFileSync } from "fs";
1356
- import { dirname as dirname4 } from "path";
1357
- import { hostname } from "os";
1358
- var LOG_LEVEL_MAP = {
1359
- debug: 20,
1360
- info: 30,
1361
- warn: 40,
1362
- error: 50
1363
- };
1364
- var fileLogger = null;
1365
- var consoleLogger = null;
1366
- var currentLogLevel = "info" /* INFO */;
1367
- function createSyncLogger(logFilePath, minLevel = "info" /* INFO */) {
1368
- const hostnameValue = hostname();
1369
- const writeLog = (level, msg, obj) => {
1370
- try {
1371
- const logDir = dirname4(logFilePath);
1372
- if (!existsSync6(logDir)) {
1373
- mkdirSync5(logDir, { recursive: true });
1374
- }
1375
- const logEntry = {
1376
- level,
1377
- time: Date.now(),
1378
- msg,
1379
- pid: process.pid,
1380
- hostname: hostnameValue,
1381
- ...obj
1382
- };
1383
- const line = JSON.stringify(logEntry) + "\n";
1384
- appendFileSync(logFilePath, line);
1385
- } catch (err) {
1386
- console.error(`[Logger] Failed to write to ${logFilePath}:`, err);
1387
- }
1388
- };
1389
- const log = (logLevel) => {
1390
- const minLevelNum = LOG_LEVEL_MAP[minLevel] ?? LOG_LEVEL_MAP.info;
1391
- const callLevelNum = LOG_LEVEL_MAP[logLevel] ?? LOG_LEVEL_MAP.info;
1392
- return (obj, msg) => {
1393
- if (callLevelNum < minLevelNum) return;
1394
- if (typeof obj === "string") {
1395
- writeLog(logLevel, obj);
1396
- } else {
1397
- writeLog(logLevel, msg || "", obj);
1398
- }
1399
- };
1400
- };
1401
- const createChild = (_bindings) => {
1402
- return {
1403
- info: log("info"),
1404
- warn: log("warn"),
1405
- error: log("error"),
1406
- debug: log("debug"),
1407
- child: createChild,
1408
- level: "debug",
1409
- levels: LOG_LEVEL_MAP
1410
- };
1411
- };
1412
- return {
1413
- info: log("info"),
1414
- warn: log("warn"),
1415
- error: log("error"),
1416
- debug: log("debug"),
1417
- child: createChild,
1418
- level: "debug",
1419
- levels: LOG_LEVEL_MAP
1420
- };
1421
- }
1422
- function createAgentLogger(agentType) {
1423
- const homedir16 = process.env.HOME || process.env.USERPROFILE || "";
1424
- const logFile = `${homedir16}/.handsoff/logs/agents/${agentType}.log`;
1425
- return createSyncLogger(logFile, currentLogLevel);
1426
- }
1427
- function getLogger(_level) {
1428
- if (!consoleLogger) {
1429
- const effectiveLevel = _level ?? (fileLogger ? fileLogger.level : "info" /* INFO */);
1430
- consoleLogger = pino({
1431
- level: effectiveLevel
1432
- });
1433
- }
1434
- const delegate = (method) => {
1435
- return (obj, msg) => {
1436
- const target = fileLogger || consoleLogger;
1437
- if (target) {
1438
- target[method](obj, msg);
1439
- }
1440
- };
1441
- };
1442
- return {
1443
- info: delegate("info"),
1444
- warn: delegate("warn"),
1445
- error: delegate("error"),
1446
- debug: delegate("debug"),
1447
- child: () => getLogger(_level),
1448
- level: "debug",
1449
- levels: LOG_LEVEL_MAP
1450
- };
1451
- }
1452
-
1453
- // src/core/binding/BindingService.ts
1454
- import { writeFileSync as writeFileSync5 } from "fs";
1455
- import { join as join6 } from "path";
1456
- import { homedir as homedir6 } from "os";
1457
- import TOML3 from "@iarna/toml";
1458
- var BindingService = class {
1459
- logger;
1460
- /** Cache the config to avoid repeated disk reads. Refreshed on each bind/unbind since we write to disk. */
1461
- cachedConfig = null;
1462
- constructor(logger) {
1463
- this.logger = logger ?? getLogger();
1464
- }
1465
- getConfig() {
1466
- if (!this.cachedConfig) {
1467
- this.cachedConfig = loadConfig();
1468
- }
1469
- return this.cachedConfig;
1470
- }
1471
- /** Invalidate cache after writes so the next read picks up fresh data. */
1472
- invalidateCache() {
1473
- this.cachedConfig = null;
1474
- }
1475
- /**
1476
- * Get the current bindings config.
1477
- */
1478
- getBindings() {
1479
- const config = this.getConfig();
1480
- const bindings = config.bindings;
1481
- if (!bindings || Object.keys(bindings).length === 0) {
1482
- return {};
1483
- }
1484
- return bindings;
1485
- }
1486
- /**
1487
- * Get the channel instance ID that a given agent is bound to.
1488
- */
1489
- getBoundChannel(agent) {
1490
- return this.getBindings()[agent];
1491
- }
1492
- /**
1493
- * Get the agent bound to a given channel instance ID.
1494
- */
1495
- getBoundAgent(channelInstanceId) {
1496
- const bindings = this.getBindings();
1497
- for (const [agent, channelId] of Object.entries(bindings)) {
1498
- if (channelId === channelInstanceId) {
1499
- return agent;
1500
- }
1501
- }
1502
- return void 0;
1503
- }
1504
- /**
1505
- * Bind an agent to a channel instance.
1506
- * If the agent is already bound to another channel, it will be migrated.
1507
- * If the target channel is already bound to another agent, that binding is replaced.
1508
- * Returns the old channel instance ID that the agent was bound to (if any).
1509
- */
1510
- bindAgent(agent, channelInstanceId) {
1511
- if (!channelInstanceId.includes(":")) {
1512
- this.logger.error({ channelInstanceId }, "[Binding] Invalid channel instance ID format");
1513
- return void 0;
1514
- }
1515
- const config = this.getConfig();
1516
- const bindings = { ...config.bindings ?? {} };
1517
- const oldChannelId = bindings[agent];
1518
- for (const [a, cid] of Object.entries(bindings)) {
1519
- if (cid === channelInstanceId && a !== agent) {
1520
- this.logger.debug({ agent: a, oldChannel: cid }, "[Binding] Removing conflicting binding");
1521
- delete bindings[a];
1522
- }
1523
- }
1524
- bindings[agent] = channelInstanceId;
1525
- this.saveBindings(bindings);
1526
- return oldChannelId;
1527
- }
1528
- /**
1529
- * Unbind an agent from its current channel.
1530
- * Returns the old channel instance ID (if any).
1531
- */
1532
- unbindAgent(agent) {
1533
- const config = this.getConfig();
1534
- const bindings = { ...config.bindings ?? {} };
1535
- const oldChannelId = bindings[agent];
1536
- if (!oldChannelId) {
1537
- return void 0;
1538
- }
1539
- delete bindings[agent];
1540
- this.saveBindings(bindings);
1541
- this.logger.info({ agent, oldChannelId }, "[Binding] Agent unbound");
1542
- return oldChannelId;
1543
- }
1544
- /**
1545
- * Check if an agent is configured (i.e., can be bound).
1546
- * An agent is configurable if it has a non-empty config entry.
1547
- */
1548
- isAgentConfigurable(agent) {
1549
- const config = this.getConfig();
1550
- if (agent === "claude") {
1551
- return !!config.agent.claude;
1552
- }
1553
- if (agent === "codex") {
1554
- return config.agent.codex?.enabled === true;
1555
- }
1556
- return false;
1557
- }
1558
- saveBindings(bindings) {
1559
- const config = this.getConfig();
1560
- const merged = {
1561
- ...config,
1562
- bindings
1563
- };
1564
- const configPath = join6(homedir6(), ".handsoff", "config.toml");
1565
- writeFileSync5(configPath, TOML3.stringify(merged));
1566
- this.invalidateCache();
1567
- }
1568
- };
1569
-
1570
1373
  // src/cli/wizard/steps/binding-confirm.ts
1571
1374
  import pc7 from "picocolors";
1572
1375
  var STEP5 = "STEP 5";
@@ -1579,37 +1382,43 @@ ${STEP5} \u2014 Confirm Binding
1579
1382
  console.log(pc7.gray(`${"\u2500".repeat(50)}
1580
1383
  `));
1581
1384
  const config = loadConfig();
1582
- const bindingService = new BindingService();
1583
- const currentBindings = config.bindings ?? {};
1385
+ const currentBindings = { ...config.bindings ?? {} };
1386
+ for (const [a, cid] of Object.entries(state.bindings)) {
1387
+ if (cid) currentBindings[a] = cid;
1388
+ }
1389
+ const pendingChannel = state.pendingChanges.channel?.[channelName];
1390
+ const telegramConfig = pendingChannel && channelName === "telegram" ? { ...config.channel.telegram, ...pendingChannel } : config.channel.telegram;
1391
+ const feishuConfig = pendingChannel && channelName === "feishu" ? { ...config.channel.feishu, ...pendingChannel } : config.channel.feishu;
1584
1392
  const agentCurrentChannel = currentBindings[agentName];
1585
1393
  let channelCurrentAgent;
1586
- if (channelName === "telegram" && config.channel.telegram?.bot_token) {
1587
- const instanceId = getChannelInstanceId("telegram", config.channel.telegram.bot_token);
1394
+ let instanceId;
1395
+ if (channelName === "telegram" && telegramConfig?.bot_token) {
1396
+ instanceId = getChannelInstanceId("telegram", telegramConfig.bot_token);
1397
+ channelCurrentAgent = Object.entries(currentBindings).find(([, v]) => v === instanceId)?.[0];
1398
+ } else if (channelName === "feishu" && feishuConfig?.app_id) {
1399
+ instanceId = getChannelInstanceId("feishu", feishuConfig.app_id);
1588
1400
  channelCurrentAgent = Object.entries(currentBindings).find(([, v]) => v === instanceId)?.[0];
1589
- } else if (channelName === "feishu" && config.channel.feishu?.app_id) {
1590
- const instanceId = getChannelInstanceId("feishu", config.channel.feishu.app_id);
1401
+ } else if (channelName === "logger") {
1402
+ instanceId = "logger:default";
1591
1403
  channelCurrentAgent = Object.entries(currentBindings).find(([, v]) => v === instanceId)?.[0];
1592
1404
  }
1593
1405
  console.log(` ${pc7.cyan(agentName)} current binding: ${agentCurrentChannel ? pc7.yellow(agentCurrentChannel) : pc7.gray("none")}`);
1594
1406
  console.log(` ${pc7.cyan(channelName)} current binding: ${channelCurrentAgent ? pc7.yellow(channelCurrentAgent) : pc7.gray("none")}`);
1595
1407
  console.log("");
1596
1408
  const choice = await prompts.bindingConfirm(agentName, channelName);
1597
- if (choice === "confirm") {
1598
- let instanceId;
1599
- if (channelName === "telegram" && config.channel.telegram?.bot_token) {
1600
- instanceId = getChannelInstanceId("telegram", config.channel.telegram.bot_token);
1601
- } else if (channelName === "feishu" && config.channel.feishu?.app_id) {
1602
- instanceId = getChannelInstanceId("feishu", config.channel.feishu.app_id);
1603
- }
1604
- if (instanceId) {
1605
- const oldChannelId = bindingService.bindAgent(agentName, instanceId);
1606
- if (oldChannelId && oldChannelId !== instanceId) {
1607
- console.log(pc7.yellow(` \u26A0 ${agentName} migrated from ${oldChannelId} to ${instanceId}`));
1409
+ if (choice === "confirm" && instanceId) {
1410
+ const oldChannelId = currentBindings[agentName];
1411
+ for (const [a, cid] of Object.entries(currentBindings)) {
1412
+ if (cid === instanceId && a !== agentName) {
1413
+ delete state.bindings[a];
1608
1414
  }
1609
- state.bindings[agentName] = instanceId;
1610
- state.sessionConfigured.add(`binding:${agentName}`);
1611
- console.log(pc7.green(` \u2713 ${agentName} bound to ${channelName}`));
1612
1415
  }
1416
+ if (oldChannelId && oldChannelId !== instanceId) {
1417
+ console.log(pc7.yellow(` \u26A0 ${agentName} migrated from ${oldChannelId} to ${instanceId}`));
1418
+ }
1419
+ state.bindings[agentName] = instanceId;
1420
+ state.sessionConfigured.add(`binding:${agentName}`);
1421
+ console.log(pc7.green(` \u2713 ${agentName} bound to ${channelName}`));
1613
1422
  } else {
1614
1423
  console.log(pc7.gray(` Binding cancelled`));
1615
1424
  }
@@ -1638,14 +1447,14 @@ ${STEP6} \u2014 Add Another Pair?
1638
1447
  // src/cli/daemon.ts
1639
1448
  import { spawn } from "child_process";
1640
1449
  import { exec } from "child_process";
1641
- import { join as join7, dirname as dirname5 } from "path";
1450
+ import { join as join6, dirname as dirname4 } from "path";
1642
1451
  import { fileURLToPath } from "url";
1643
- import { mkdirSync as mkdirSync6, existsSync as existsSync8 } from "fs";
1452
+ import { mkdirSync as mkdirSync5, existsSync as existsSync7 } from "fs";
1644
1453
 
1645
1454
  // src/shared/pidfile.ts
1646
- import { writeFileSync as writeFileSync6, readFileSync as readFileSync6, existsSync as existsSync7, unlinkSync } from "fs";
1455
+ import { writeFileSync as writeFileSync5, readFileSync as readFileSync6, existsSync as existsSync6, unlinkSync } from "fs";
1647
1456
  function readPidFile(pidFilePath) {
1648
- if (!existsSync7(pidFilePath)) {
1457
+ if (!existsSync6(pidFilePath)) {
1649
1458
  return null;
1650
1459
  }
1651
1460
  try {
@@ -1656,7 +1465,7 @@ function readPidFile(pidFilePath) {
1656
1465
  }
1657
1466
  }
1658
1467
  function removePidFile(pidFilePath) {
1659
- if (existsSync7(pidFilePath)) {
1468
+ if (existsSync6(pidFilePath)) {
1660
1469
  unlinkSync(pidFilePath);
1661
1470
  }
1662
1471
  }
@@ -1717,23 +1526,23 @@ var DaemonManager = class {
1717
1526
  }
1718
1527
  }
1719
1528
  try {
1720
- const homedir16 = process.env.HOME || process.env.USERPROFILE || "";
1721
- const logDir = join7(homedir16, ".handsoff", "logs");
1722
- mkdirSync6(logDir, { recursive: true });
1529
+ const homedir15 = process.env.HOME || process.env.USERPROFILE || "";
1530
+ const logDir = join6(homedir15, ".handsoff", "logs");
1531
+ mkdirSync5(logDir, { recursive: true });
1723
1532
  removePidFile(this.pidFilePath);
1724
- const __dirname3 = dirname5(fileURLToPath(import.meta.url));
1725
- let daemonScript = join7(__dirname3, "..", "gateway", "process.js");
1726
- if (!existsSync8(daemonScript)) {
1727
- daemonScript = join7(__dirname3, "..", "..", "src", "gateway", "process.ts");
1533
+ const __dirname3 = dirname4(fileURLToPath(import.meta.url));
1534
+ let daemonScript = join6(__dirname3, "..", "gateway", "process.js");
1535
+ if (!existsSync7(daemonScript)) {
1536
+ daemonScript = join6(__dirname3, "..", "..", "src", "gateway", "process.ts");
1728
1537
  }
1729
- if (!existsSync8(daemonScript)) {
1538
+ if (!existsSync7(daemonScript)) {
1730
1539
  this.lastError = t("daemon.scriptNotFound", { path: daemonScript });
1731
1540
  console.error(this.lastError);
1732
1541
  return false;
1733
1542
  }
1734
- const logFile = join7(logDir, "gateway.log");
1543
+ const logFile = join6(logDir, "gateway.log");
1735
1544
  const isDev = daemonScript.endsWith(".ts");
1736
- const execPath = isDev ? join7(__dirname3, "..", "..", "node_modules", ".bin", "tsx") : process.execPath;
1545
+ const execPath = isDev ? join6(__dirname3, "..", "..", "node_modules", ".bin", "tsx") : process.execPath;
1737
1546
  const child = spawn(execPath, [daemonScript], {
1738
1547
  detached: true,
1739
1548
  stdio: "ignore",
@@ -1804,8 +1613,8 @@ var DaemonManager = class {
1804
1613
  };
1805
1614
 
1806
1615
  // src/cli/wizard/steps/final.ts
1807
- import { join as join8 } from "path";
1808
- import { homedir as homedir7 } from "os";
1616
+ import { join as join7 } from "path";
1617
+ import { homedir as homedir6 } from "os";
1809
1618
  import pc9 from "picocolors";
1810
1619
  import ora3 from "ora";
1811
1620
  async function stepFinal(state) {
@@ -1824,7 +1633,7 @@ ${t("wizard.section.complete")}
1824
1633
  }
1825
1634
  console.log("");
1826
1635
  }
1827
- const pidFilePath = join8(homedir7(), ".handsoff", "handsoff.pid");
1636
+ const pidFilePath = join7(homedir6(), ".handsoff", "handsoff.pid");
1828
1637
  const port = loadConfig().general.hook_server_port;
1829
1638
  const daemon = new DaemonManager(pidFilePath, port);
1830
1639
  const hasChanges = state.sessionConfigured.size > 0 || Object.keys(state.pendingChanges).length > 0 || Object.keys(state.bindings).length > 0;
@@ -1886,8 +1695,8 @@ var WizardEngine = class {
1886
1695
  state;
1887
1696
  applicator;
1888
1697
  constructor() {
1889
- const configPath = join9(homedir8(), ".handsoff", "config.toml");
1890
- const hasExistingConfig = existsSync9(configPath);
1698
+ const configPath = join8(homedir7(), ".handsoff", "config.toml");
1699
+ const hasExistingConfig = existsSync8(configPath);
1891
1700
  this.state = createInitialState(hasExistingConfig);
1892
1701
  this.applicator = new ConfigApplicator();
1893
1702
  }
@@ -1960,8 +1769,8 @@ var WizardEngine = class {
1960
1769
 
1961
1770
  // src/cli/init.ts
1962
1771
  var initCommand = new Command2("init").description("Initialize handsoff configuration").option("-f, --force", "Force reinitialize (regenerate token)").action(async (options) => {
1963
- const configPath = join10(homedir9(), ".handsoff", "config.toml");
1964
- if (existsSync10(configPath) && !options.force) {
1772
+ const configPath = join9(homedir8(), ".handsoff", "config.toml");
1773
+ if (existsSync9(configPath) && !options.force) {
1965
1774
  const overwrite = await confirm2({
1966
1775
  message: t("cli.init.configExists"),
1967
1776
  default: false
@@ -1984,10 +1793,10 @@ var initCommand = new Command2("init").description("Initialize handsoff configur
1984
1793
  import { Command as Command3 } from "commander";
1985
1794
  import { createReadStream } from "fs";
1986
1795
  import { stat, readFile } from "fs/promises";
1987
- import { homedir as homedir10 } from "os";
1988
- import { join as join11 } from "path";
1989
- import { readdirSync, statSync, existsSync as existsSync11 } from "fs";
1990
- var SESSIONS_DIR = join11(homedir10(), ".handsoff", "sessions");
1796
+ import { homedir as homedir9 } from "os";
1797
+ import { join as join10 } from "path";
1798
+ import { readdirSync, statSync, existsSync as existsSync10 } from "fs";
1799
+ var SESSIONS_DIR = join10(homedir9(), ".handsoff", "sessions");
1991
1800
  function formatSize(bytes) {
1992
1801
  if (bytes === 0) return "0B";
1993
1802
  const units = ["B", "KB", "MB", "GB"];
@@ -2000,18 +1809,18 @@ function sleep(ms) {
2000
1809
  }
2001
1810
  function listSessions() {
2002
1811
  const sessions = [];
2003
- if (!existsSync11(SESSIONS_DIR)) {
1812
+ if (!existsSync10(SESSIONS_DIR)) {
2004
1813
  return sessions;
2005
1814
  }
2006
1815
  const entries = readdirSync(SESSIONS_DIR, { withFileTypes: true });
2007
1816
  for (const entry of entries) {
2008
1817
  if (entry.isDirectory() && /^\d{4}-\d{2}-\d{2}$/.test(entry.name)) {
2009
1818
  const date = entry.name;
2010
- const dateDir = join11(SESSIONS_DIR, date);
1819
+ const dateDir = join10(SESSIONS_DIR, date);
2011
1820
  const files = readdirSync(dateDir);
2012
1821
  for (const fileName of files) {
2013
1822
  if (fileName.endsWith(".jsonl")) {
2014
- const filePath = join11(dateDir, fileName);
1823
+ const filePath = join10(dateDir, fileName);
2015
1824
  const stats = statSync(filePath);
2016
1825
  const match = fileName.match(/^([^-]+)-(.+)\.jsonl$/);
2017
1826
  if (match) {
@@ -2077,7 +1886,7 @@ function printEvent(event) {
2077
1886
  console.log(`[${timeStr}] ${typeStr} ${content}`);
2078
1887
  }
2079
1888
  async function* readEvents(filePath) {
2080
- if (!existsSync11(filePath)) {
1889
+ if (!existsSync10(filePath)) {
2081
1890
  return;
2082
1891
  }
2083
1892
  const content = await readFile(filePath, "utf-8");
@@ -2217,15 +2026,15 @@ var logsCommand = new Command3("logs").description("View session logs").option("
2217
2026
 
2218
2027
  // src/cli/status.ts
2219
2028
  import { Command as Command4 } from "commander";
2220
- import { homedir as homedir11 } from "os";
2221
- import { join as join12 } from "path";
2222
- import { existsSync as existsSync12 } from "fs";
2029
+ import { homedir as homedir10 } from "os";
2030
+ import { join as join11 } from "path";
2031
+ import { existsSync as existsSync11 } from "fs";
2223
2032
  function getSettingsPath2() {
2224
- return join12(homedir11(), ".claude", "settings.json");
2033
+ return join11(homedir10(), ".claude", "settings.json");
2225
2034
  }
2226
2035
  var statusCommand = new Command4("status").description("Show current status").action(() => {
2227
2036
  const config = loadConfig();
2228
- const pidFilePath = join12(homedir11(), ".handsoff", "handsoff.pid");
2037
+ const pidFilePath = join11(homedir10(), ".handsoff", "handsoff.pid");
2229
2038
  const daemon = new DaemonManager(pidFilePath, config.general.hook_server_port);
2230
2039
  console.log(`
2231
2040
  ${t("status.title")}
@@ -2240,12 +2049,12 @@ ${t("status.title")}
2240
2049
  `);
2241
2050
  const settingsPath = getSettingsPath2();
2242
2051
  const backupPath = settingsPath + ".handsoff-backup";
2243
- if (existsSync12(backupPath)) {
2052
+ if (existsSync11(backupPath)) {
2244
2053
  console.log(t("status.settingsBackup"));
2245
2054
  } else {
2246
2055
  console.log(t("status.settingsNoBackup"));
2247
2056
  }
2248
- const home = homedir11();
2057
+ const home = homedir10();
2249
2058
  console.log(`
2250
2059
  ${t("status.paths")}`);
2251
2060
  console.log(` ${t("status.pathConfig", { path: `${home}/.handsoff/config.toml` })}`);
@@ -2256,11 +2065,11 @@ ${t("status.paths")}`);
2256
2065
 
2257
2066
  // src/cli/stop.ts
2258
2067
  import { Command as Command5 } from "commander";
2259
- import { homedir as homedir12 } from "os";
2260
- import { join as join13 } from "path";
2068
+ import { homedir as homedir11 } from "os";
2069
+ import { join as join12 } from "path";
2261
2070
  var stopCommand = new Command5("stop").description("Stop the handsoff daemon and restore settings").action(async () => {
2262
2071
  const config = loadConfig();
2263
- const pidFilePath = join13(homedir12(), ".handsoff", "handsoff.pid");
2072
+ const pidFilePath = join12(homedir11(), ".handsoff", "handsoff.pid");
2264
2073
  const daemon = new DaemonManager(pidFilePath, config.general.hook_server_port);
2265
2074
  if (!daemon.isRunning()) {
2266
2075
  console.log(t("cli.stop.notRunning"));
@@ -2277,9 +2086,9 @@ var stopCommand = new Command5("stop").description("Stop the handsoff daemon and
2277
2086
 
2278
2087
  // src/cli/gateway.ts
2279
2088
  import { Command as Command6 } from "commander";
2280
- import { join as join14 } from "path";
2281
- import { homedir as homedir13 } from "os";
2282
- var PID_FILE = join14(homedir13(), ".handsoff", "handsoff.pid");
2089
+ import { join as join13 } from "path";
2090
+ import { homedir as homedir12 } from "os";
2091
+ var PID_FILE = join13(homedir12(), ".handsoff", "handsoff.pid");
2283
2092
  var gatewayCommand = new Command6("gateway").description("Manage handsoff Gateway daemon").addCommand(
2284
2093
  new Command6("start").description("Start or restart the Gateway daemon").option("-p, --port <port>", "Port to listen on").action(async (options) => {
2285
2094
  const config = loadConfig();
@@ -2298,7 +2107,7 @@ var gatewayCommand = new Command6("gateway").description("Manage handsoff Gatewa
2298
2107
  if (error) {
2299
2108
  console.error(t("gateway.error", { error }));
2300
2109
  }
2301
- const logPath = join14(homedir13(), ".handsoff", "logs", "gateway.log");
2110
+ const logPath = join13(homedir12(), ".handsoff", "logs", "gateway.log");
2302
2111
  console.error(` ${t("gateway.checkLogs", { path: logPath })}`);
2303
2112
  process.exit(1);
2304
2113
  }
@@ -2335,7 +2144,7 @@ var gatewayCommand = new Command6("gateway").description("Manage handsoff Gatewa
2335
2144
  if (error) {
2336
2145
  console.error(t("gateway.error", { error }));
2337
2146
  }
2338
- const logPath = join14(homedir13(), ".handsoff", "logs", "gateway.log");
2147
+ const logPath = join13(homedir12(), ".handsoff", "logs", "gateway.log");
2339
2148
  console.error(` ${t("gateway.checkLogs", { path: logPath })}`);
2340
2149
  process.exit(1);
2341
2150
  }
@@ -2373,16 +2182,16 @@ var gatewayCommand = new Command6("gateway").description("Manage handsoff Gatewa
2373
2182
  import { Command as Command7 } from "commander";
2374
2183
  import { spawn as spawn2 } from "child_process";
2375
2184
  import { fileURLToPath as fileURLToPath2 } from "url";
2376
- import { dirname as dirname6, join as join15 } from "path";
2185
+ import { dirname as dirname5, join as join14 } from "path";
2377
2186
  var __filename2 = fileURLToPath2(import.meta.url);
2378
- var __dirname2 = dirname6(__filename2);
2187
+ var __dirname2 = dirname5(__filename2);
2379
2188
  var debugCommand = new Command7("debug").description("Debug tools for Handsoff").addCommand(
2380
2189
  new Command7("hooks").description("Capture and log raw hook payloads for debugging").option("-p, --port <port>", "Port to listen on", "16792").action(async (options) => {
2381
2190
  const port = parseInt(options.port, 10);
2382
2191
  console.log(t("cli.debug.starting", { port }));
2383
2192
  console.log(t("cli.debug.capturing"));
2384
2193
  console.log(t("cli.debug.pressCtrlC"));
2385
- const captureScript = join15(__dirname2, "hook-capture.js");
2194
+ const captureScript = join14(__dirname2, "hook-capture.js");
2386
2195
  const proc = spawn2("node", [captureScript], {
2387
2196
  stdio: "inherit",
2388
2197
  env: { ...process.env, HANDSOFF_DEBUG_PORT: String(port) }
@@ -2399,21 +2208,21 @@ var debugCommand = new Command7("debug").description("Debug tools for Handsoff")
2399
2208
 
2400
2209
  // src/cli/agent/claude.ts
2401
2210
  import { spawn as spawn3 } from "child_process";
2402
- import { homedir as homedir14 } from "os";
2403
- import { join as join16, dirname as dirname7 } from "path";
2404
- import { readFileSync as readFileSync7, existsSync as existsSync13, writeFileSync as writeFileSync7, mkdirSync as mkdirSync7 } from "fs";
2211
+ import { homedir as homedir13 } from "os";
2212
+ import { join as join15, dirname as dirname6 } from "path";
2213
+ import { readFileSync as readFileSync7, existsSync as existsSync12, writeFileSync as writeFileSync6, mkdirSync as mkdirSync6 } from "fs";
2405
2214
  function getSettingsPath3() {
2406
- return join16(homedir14(), ".claude", "settings.json");
2215
+ return join15(homedir13(), ".claude", "settings.json");
2407
2216
  }
2408
2217
  function backupSettings2(settingsPath) {
2409
2218
  const backupPath = settingsPath + ".handsoff-backup";
2410
- if (existsSync13(settingsPath)) {
2411
- writeFileSync7(backupPath, readFileSync7(settingsPath, "utf-8"));
2219
+ if (existsSync12(settingsPath)) {
2220
+ writeFileSync6(backupPath, readFileSync7(settingsPath, "utf-8"));
2412
2221
  }
2413
2222
  }
2414
2223
  function writeSettings2(settingsPath, settings) {
2415
- mkdirSync7(dirname7(settingsPath), { recursive: true });
2416
- writeFileSync7(settingsPath, JSON.stringify(settings, null, 2));
2224
+ mkdirSync6(dirname6(settingsPath), { recursive: true });
2225
+ writeFileSync6(settingsPath, JSON.stringify(settings, null, 2));
2417
2226
  }
2418
2227
  function generateHooksConfig3(port, token) {
2419
2228
  const unifiedUrl = `http://localhost:${port}/hook/${token}/event`;
@@ -2439,7 +2248,7 @@ function registerClaudeCommand(program2) {
2439
2248
  program2.command("claude").description("Start Claude Code with handsoff integration").option("-d, --directory <dir>", "Working directory for Claude").action(async (options) => {
2440
2249
  const config = loadConfig();
2441
2250
  const port = config.general.hook_server_port;
2442
- const pidFilePath = join16(homedir14(), ".handsoff", "handsoff.pid");
2251
+ const pidFilePath = join15(homedir13(), ".handsoff", "handsoff.pid");
2443
2252
  const daemon = new DaemonManager(pidFilePath, port);
2444
2253
  let daemonStarted = false;
2445
2254
  if (daemon.isRunning()) {
@@ -2457,7 +2266,7 @@ function registerClaudeCommand(program2) {
2457
2266
  console.log(t("cli.agent.configuring"));
2458
2267
  const settingsPath = getSettingsPath3();
2459
2268
  const backupPath = settingsPath + ".handsoff-backup";
2460
- if (!existsSync13(backupPath)) {
2269
+ if (!existsSync12(backupPath)) {
2461
2270
  backupSettings2(settingsPath);
2462
2271
  console.log(t("cli.agent.settingsBacked"));
2463
2272
  }
@@ -2469,7 +2278,7 @@ function registerClaudeCommand(program2) {
2469
2278
  }
2470
2279
  const hooksConfig = generateHooksConfig3(port, token);
2471
2280
  let currentSettings = {};
2472
- if (existsSync13(settingsPath)) {
2281
+ if (existsSync12(settingsPath)) {
2473
2282
  currentSettings = JSON.parse(readFileSync7(settingsPath, "utf-8"));
2474
2283
  }
2475
2284
  const mergedSettings = mergeHooks(currentSettings, hooksConfig);
@@ -2498,9 +2307,84 @@ function registerClaudeCommand(program2) {
2498
2307
 
2499
2308
  // src/cli/agent/codex.ts
2500
2309
  import { spawn as spawn4 } from "child_process";
2501
- import { homedir as homedir15 } from "os";
2502
- import { join as join17 } from "path";
2310
+ import { homedir as homedir14 } from "os";
2311
+ import { join as join16 } from "path";
2503
2312
  import { execSync as execSync3 } from "child_process";
2313
+
2314
+ // src/shared/logger.ts
2315
+ import pino from "pino";
2316
+ import { mkdirSync as mkdirSync7, existsSync as existsSync13, appendFileSync } from "fs";
2317
+ import { dirname as dirname7 } from "path";
2318
+ import { hostname } from "os";
2319
+ var LOG_LEVEL_MAP = {
2320
+ debug: 20,
2321
+ info: 30,
2322
+ warn: 40,
2323
+ error: 50
2324
+ };
2325
+ var currentLogLevel = "info" /* INFO */;
2326
+ function createSyncLogger(logFilePath, minLevel = "info" /* INFO */) {
2327
+ const hostnameValue = hostname();
2328
+ const writeLog = (level, msg, obj) => {
2329
+ try {
2330
+ const logDir = dirname7(logFilePath);
2331
+ if (!existsSync13(logDir)) {
2332
+ mkdirSync7(logDir, { recursive: true });
2333
+ }
2334
+ const logEntry = {
2335
+ level,
2336
+ time: Date.now(),
2337
+ msg,
2338
+ pid: process.pid,
2339
+ hostname: hostnameValue,
2340
+ ...obj
2341
+ };
2342
+ const line = JSON.stringify(logEntry) + "\n";
2343
+ appendFileSync(logFilePath, line);
2344
+ } catch (err) {
2345
+ console.error(`[Logger] Failed to write to ${logFilePath}:`, err);
2346
+ }
2347
+ };
2348
+ const log = (logLevel) => {
2349
+ const minLevelNum = LOG_LEVEL_MAP[minLevel] ?? LOG_LEVEL_MAP.info;
2350
+ const callLevelNum = LOG_LEVEL_MAP[logLevel] ?? LOG_LEVEL_MAP.info;
2351
+ return (obj, msg) => {
2352
+ if (callLevelNum < minLevelNum) return;
2353
+ if (typeof obj === "string") {
2354
+ writeLog(logLevel, obj);
2355
+ } else {
2356
+ writeLog(logLevel, msg || "", obj);
2357
+ }
2358
+ };
2359
+ };
2360
+ const createChild = (_bindings) => {
2361
+ return {
2362
+ info: log("info"),
2363
+ warn: log("warn"),
2364
+ error: log("error"),
2365
+ debug: log("debug"),
2366
+ child: createChild,
2367
+ level: "debug",
2368
+ levels: LOG_LEVEL_MAP
2369
+ };
2370
+ };
2371
+ return {
2372
+ info: log("info"),
2373
+ warn: log("warn"),
2374
+ error: log("error"),
2375
+ debug: log("debug"),
2376
+ child: createChild,
2377
+ level: "debug",
2378
+ levels: LOG_LEVEL_MAP
2379
+ };
2380
+ }
2381
+ function createAgentLogger(agentType) {
2382
+ const homedir15 = process.env.HOME || process.env.USERPROFILE || "";
2383
+ const logFile = `${homedir15}/.handsoff/logs/agents/${agentType}.log`;
2384
+ return createSyncLogger(logFile, currentLogLevel);
2385
+ }
2386
+
2387
+ // src/cli/agent/codex.ts
2504
2388
  function isCodexInstalled() {
2505
2389
  try {
2506
2390
  execSync3("codex --version", { encoding: "utf8", stdio: "pipe", windowsHide: true });
@@ -2529,7 +2413,7 @@ function registerCodexCommand(program2) {
2529
2413
  }
2530
2414
  const config = loadConfig();
2531
2415
  const port = config.general.hook_server_port;
2532
- const pidFilePath = join17(homedir15(), ".handsoff", "handsoff.pid");
2416
+ const pidFilePath = join16(homedir14(), ".handsoff", "handsoff.pid");
2533
2417
  const daemon = new DaemonManager(pidFilePath, port);
2534
2418
  let daemonStarted = false;
2535
2419
  if (daemon.isRunning()) {