handsoff 0.0.1-beta.2 → 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/LICENSE +201 -0
- package/README.md +176 -87
- package/config.example.toml +15 -0
- package/dist/cli/index.js +611 -66
- package/dist/cli/index.js.map +1 -1
- package/dist/gateway/process.js +11783 -0
- package/dist/gateway/process.js.map +1 -0
- package/package.json +2 -2
package/dist/cli/index.js
CHANGED
|
@@ -160,8 +160,18 @@ var DEFAULT_CONFIG = {
|
|
|
160
160
|
permission: {
|
|
161
161
|
lowRiskTools: ["Read", "Glob", "Grep", "List", "WebSearch", "codesearch"]
|
|
162
162
|
}
|
|
163
|
+
},
|
|
164
|
+
codex: {
|
|
165
|
+
enabled: false,
|
|
166
|
+
command: ["codex", "app-server", "--listen", "stdio://"],
|
|
167
|
+
work_dir: "",
|
|
168
|
+
model: "",
|
|
169
|
+
approval_policy: "",
|
|
170
|
+
sandbox_mode: ""
|
|
163
171
|
}
|
|
164
|
-
}
|
|
172
|
+
},
|
|
173
|
+
channels: {},
|
|
174
|
+
bindings: {}
|
|
165
175
|
};
|
|
166
176
|
function getConfigPath() {
|
|
167
177
|
return join2(homedir2(), ".handsoff", "config.toml");
|
|
@@ -218,7 +228,31 @@ function mergeWithDefaults(raw) {
|
|
|
218
228
|
...DEFAULT_CONFIG.agent.claude.permission,
|
|
219
229
|
...raw.agent?.claude?.permission || {}
|
|
220
230
|
}
|
|
231
|
+
},
|
|
232
|
+
codex: {
|
|
233
|
+
...DEFAULT_CONFIG.agent.codex,
|
|
234
|
+
...raw.agent?.codex || {},
|
|
235
|
+
command: raw.agent?.codex?.command ? raw.agent.codex.command : DEFAULT_CONFIG.agent.codex.command,
|
|
236
|
+
work_dir: raw.agent?.codex?.work_dir || DEFAULT_CONFIG.agent.codex.work_dir,
|
|
237
|
+
model: raw.agent?.codex?.model || DEFAULT_CONFIG.agent.codex.model,
|
|
238
|
+
approval_policy: raw.agent?.codex?.approval_policy || DEFAULT_CONFIG.agent.codex.approval_policy,
|
|
239
|
+
sandbox_mode: raw.agent?.codex?.sandbox_mode || DEFAULT_CONFIG.agent.codex.sandbox_mode
|
|
221
240
|
}
|
|
241
|
+
},
|
|
242
|
+
channels: {
|
|
243
|
+
...DEFAULT_CONFIG.channels,
|
|
244
|
+
...raw.channels || {},
|
|
245
|
+
app: raw.channels?.app ? {
|
|
246
|
+
enabled: raw.channels.app.enabled === true,
|
|
247
|
+
channel_id: String(raw.channels.app.channel_id || ""),
|
|
248
|
+
auth_token: String(raw.channels.app.auth_token || ""),
|
|
249
|
+
heartbeat_interval_ms: raw.channels.app.heartbeat_interval_ms ? parseInt(String(raw.channels.app.heartbeat_interval_ms), 10) : void 0,
|
|
250
|
+
notify_types: Array.isArray(raw.channels.app.notify_types) ? raw.channels.app.notify_types.map(String) : void 0
|
|
251
|
+
} : DEFAULT_CONFIG.channels?.app
|
|
252
|
+
},
|
|
253
|
+
bindings: {
|
|
254
|
+
...DEFAULT_CONFIG.bindings,
|
|
255
|
+
...raw.bindings || {}
|
|
222
256
|
}
|
|
223
257
|
};
|
|
224
258
|
}
|
|
@@ -285,6 +319,17 @@ var en_default = {
|
|
|
285
319
|
required: "app_id and app_secret are required. Skipping."
|
|
286
320
|
}
|
|
287
321
|
},
|
|
322
|
+
codex: {
|
|
323
|
+
enable: "Enable Codex agent integration?",
|
|
324
|
+
command: "Codex app-server command:",
|
|
325
|
+
workDir: "Default Codex working directory (optional):",
|
|
326
|
+
model: "Default Codex model (optional):",
|
|
327
|
+
approvalPolicy: "Default Codex approval policy (optional):",
|
|
328
|
+
detected: "Codex CLI detected.",
|
|
329
|
+
notDetected: "Codex CLI not found in PATH.",
|
|
330
|
+
enabled: "Codex integration enabled.",
|
|
331
|
+
disabled: "Codex integration disabled."
|
|
332
|
+
},
|
|
288
333
|
notify: {
|
|
289
334
|
title: "Notification types for {{channel}}",
|
|
290
335
|
checkbox: "Notification types:",
|
|
@@ -415,6 +460,24 @@ var en_default = {
|
|
|
415
460
|
tipStop: 'Use "handsoff stop" to stop the daemon and restore settings',
|
|
416
461
|
detaching: "\nDetaching from Claude..."
|
|
417
462
|
},
|
|
463
|
+
codex: {
|
|
464
|
+
notInstalled: "Codex CLI is not installed",
|
|
465
|
+
installMethods: "Please install Codex CLI using one of these methods:",
|
|
466
|
+
option1npm: "Option 1 - npm (recommended): npm install -g @openai/codex",
|
|
467
|
+
option2brew: "Option 2 - Homebrew (macOS): brew install --cask codex",
|
|
468
|
+
alternative: "Alternatively, use Claude Code: handsoff claude",
|
|
469
|
+
alreadyRunning: "Handsoff daemon is already running",
|
|
470
|
+
starting: "Starting handsoff daemon...",
|
|
471
|
+
started: "Daemon started",
|
|
472
|
+
startFailed: "Failed to start daemon",
|
|
473
|
+
notEnabled: "Codex adapter is not enabled in handsoff config.",
|
|
474
|
+
enableHint: 'Enable it with "handsoff init" or by setting agent.codex.enabled = true in ~/.handsoff/config.toml, then restart the daemon.',
|
|
475
|
+
startingIn: "Starting Codex CLI {{version}} in {{dir}}",
|
|
476
|
+
pressCtrlC: "Press Ctrl+C to stop (handsoff daemon will keep running)",
|
|
477
|
+
exited: "\nCodex CLI exited with code {{code}}",
|
|
478
|
+
daemonRunning: "Handsoff daemon is still running in the background",
|
|
479
|
+
detaching: "\nDetaching from Codex..."
|
|
480
|
+
},
|
|
418
481
|
debug: {
|
|
419
482
|
starting: "Starting hook debug server on port {{port}}",
|
|
420
483
|
capturing: "This will capture all incoming webhook requests...",
|
|
@@ -565,9 +628,10 @@ import { homedir as homedir7 } from "os";
|
|
|
565
628
|
// src/cli/wizard/state.ts
|
|
566
629
|
function createInitialState(hasExistingConfig) {
|
|
567
630
|
return {
|
|
568
|
-
current: "
|
|
631
|
+
current: "agent-select",
|
|
569
632
|
hasExistingConfig,
|
|
570
633
|
pendingChanges: {},
|
|
634
|
+
bindings: {},
|
|
571
635
|
itemStatus: /* @__PURE__ */ new Map(),
|
|
572
636
|
sessionConfigured: /* @__PURE__ */ new Set(),
|
|
573
637
|
validationResults: /* @__PURE__ */ new Map()
|
|
@@ -580,24 +644,42 @@ import { join as join4 } from "path";
|
|
|
580
644
|
import { homedir as homedir4 } from "os";
|
|
581
645
|
import TOML2 from "@iarna/toml";
|
|
582
646
|
var ConfigApplicator = class {
|
|
583
|
-
apply(pending) {
|
|
647
|
+
apply(pending, bindings) {
|
|
584
648
|
const current = loadConfig();
|
|
585
649
|
const merged = {
|
|
586
650
|
...current,
|
|
587
651
|
general: { ...current.general },
|
|
588
652
|
channel: {
|
|
589
653
|
...current.channel,
|
|
590
|
-
|
|
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
|
|
591
658
|
},
|
|
592
659
|
agent: {
|
|
593
660
|
...current.agent,
|
|
594
|
-
...pending.agent
|
|
661
|
+
...pending.agent?.claude ? {
|
|
662
|
+
claude: { ...current.agent?.claude ?? {}, ...pending.agent.claude }
|
|
663
|
+
} : {},
|
|
664
|
+
...pending.agent?.codex ? {
|
|
665
|
+
codex: { ...current.agent?.codex ?? {}, ...pending.agent.codex }
|
|
666
|
+
} : {}
|
|
595
667
|
}
|
|
596
668
|
};
|
|
669
|
+
let finalConfig = merged;
|
|
670
|
+
if (bindings && Object.keys(bindings).length > 0) {
|
|
671
|
+
finalConfig = {
|
|
672
|
+
...merged,
|
|
673
|
+
bindings: {
|
|
674
|
+
...merged.bindings || {},
|
|
675
|
+
...bindings
|
|
676
|
+
}
|
|
677
|
+
};
|
|
678
|
+
}
|
|
597
679
|
const configDir = join4(homedir4(), ".handsoff");
|
|
598
680
|
mkdirSync3(configDir, { recursive: true });
|
|
599
681
|
const configPath = join4(configDir, "config.toml");
|
|
600
|
-
const content = TOML2.stringify(
|
|
682
|
+
const content = TOML2.stringify(finalConfig);
|
|
601
683
|
writeFileSync3(configPath, content);
|
|
602
684
|
}
|
|
603
685
|
};
|
|
@@ -695,6 +777,34 @@ function extractHandsoffHookInfo(entry) {
|
|
|
695
777
|
return { isHandsoff: false };
|
|
696
778
|
}
|
|
697
779
|
|
|
780
|
+
// src/cli/wizard/detectors/codex.ts
|
|
781
|
+
import { execSync as execSync2 } from "child_process";
|
|
782
|
+
function detectCodex() {
|
|
783
|
+
let binaryPath;
|
|
784
|
+
let installed = false;
|
|
785
|
+
let version;
|
|
786
|
+
try {
|
|
787
|
+
binaryPath = execSync2("which codex", { encoding: "utf-8" }).trim();
|
|
788
|
+
installed = !!binaryPath;
|
|
789
|
+
version = execSync2("codex --version", { encoding: "utf-8", stdio: "pipe" }).trim();
|
|
790
|
+
} catch {
|
|
791
|
+
}
|
|
792
|
+
if (!installed) {
|
|
793
|
+
return {
|
|
794
|
+
supported: false,
|
|
795
|
+
installed: false,
|
|
796
|
+
message: "Codex CLI not found in PATH. Please install Codex CLI first."
|
|
797
|
+
};
|
|
798
|
+
}
|
|
799
|
+
return {
|
|
800
|
+
supported: true,
|
|
801
|
+
installed: true,
|
|
802
|
+
message: `Codex CLI ${version} detected.`,
|
|
803
|
+
binaryPath,
|
|
804
|
+
version
|
|
805
|
+
};
|
|
806
|
+
}
|
|
807
|
+
|
|
698
808
|
// src/cli/wizard/prompts.ts
|
|
699
809
|
import { confirm, input, checkbox, select } from "@inquirer/prompts";
|
|
700
810
|
var prompts = {
|
|
@@ -710,6 +820,26 @@ var prompts = {
|
|
|
710
820
|
message: t("wizard.channel.feishu.allowedUsers"),
|
|
711
821
|
default: current || ""
|
|
712
822
|
}),
|
|
823
|
+
codexEnable: (enabled) => confirm({
|
|
824
|
+
message: t("wizard.codex.enable"),
|
|
825
|
+
default: enabled
|
|
826
|
+
}),
|
|
827
|
+
codexCommand: (current) => input({
|
|
828
|
+
message: t("wizard.codex.command"),
|
|
829
|
+
default: current || "codex app-server --listen stdio://"
|
|
830
|
+
}),
|
|
831
|
+
codexWorkDir: (current) => input({
|
|
832
|
+
message: t("wizard.codex.workDir"),
|
|
833
|
+
default: current || ""
|
|
834
|
+
}),
|
|
835
|
+
codexModel: (current) => input({
|
|
836
|
+
message: t("wizard.codex.model"),
|
|
837
|
+
default: current || ""
|
|
838
|
+
}),
|
|
839
|
+
codexApprovalPolicy: (current) => input({
|
|
840
|
+
message: t("wizard.codex.approvalPolicy"),
|
|
841
|
+
default: current || ""
|
|
842
|
+
}),
|
|
713
843
|
cliActionSelect: (hooked) => select({
|
|
714
844
|
message: t("wizard.cli.actionQuestion"),
|
|
715
845
|
choices: hooked ? [
|
|
@@ -774,6 +904,32 @@ var prompts = {
|
|
|
774
904
|
restartGateway: () => confirm({
|
|
775
905
|
message: t("wizard.menu.restartQuestion"),
|
|
776
906
|
default: true
|
|
907
|
+
}),
|
|
908
|
+
bindingAgentSelect: (agents) => select({
|
|
909
|
+
message: "Select an agent to bind:",
|
|
910
|
+
choices: agents.map((a) => ({ name: a.name, value: a.value }))
|
|
911
|
+
}),
|
|
912
|
+
bindingConfirm: (agentName, channelName) => select({
|
|
913
|
+
message: `\u{1F916} Bind ${agentName} to ${channelName} \u2014 confirm?`,
|
|
914
|
+
choices: [
|
|
915
|
+
{ name: "Confirm binding", value: "confirm" },
|
|
916
|
+
{ name: "Cancel", value: "cancel" }
|
|
917
|
+
]
|
|
918
|
+
}),
|
|
919
|
+
pairContinue: () => select({
|
|
920
|
+
message: "Add another pair?",
|
|
921
|
+
choices: [
|
|
922
|
+
{ name: "Add another pair", value: "continue" },
|
|
923
|
+
{ name: "Finish setup", value: "finish" }
|
|
924
|
+
]
|
|
925
|
+
}),
|
|
926
|
+
agentSelect: (agents) => select({
|
|
927
|
+
message: "Select an agent to configure:",
|
|
928
|
+
choices: agents.map((a) => ({ name: `${a.name} ${a.status}`, value: a.value }))
|
|
929
|
+
}),
|
|
930
|
+
channelSelect: (channels) => select({
|
|
931
|
+
message: "Select a channel to configure:",
|
|
932
|
+
choices: channels.map((c) => ({ name: `${c.name} ${c.status}`, value: c.value }))
|
|
777
933
|
})
|
|
778
934
|
};
|
|
779
935
|
|
|
@@ -782,10 +938,13 @@ import { writeFileSync as writeFileSync4, existsSync as existsSync5, readFileSyn
|
|
|
782
938
|
import { dirname as dirname3 } from "path";
|
|
783
939
|
import pc from "picocolors";
|
|
784
940
|
import ora from "ora";
|
|
941
|
+
var STEP = "STEP 2";
|
|
785
942
|
async function stepCli(state) {
|
|
786
943
|
const cliName = state.currentCliName || "claude";
|
|
787
944
|
console.log(pc.bold(`
|
|
788
|
-
${
|
|
945
|
+
${STEP} \u2014 Configure Agent
|
|
946
|
+
`));
|
|
947
|
+
console.log(pc.gray(`${"\u2500".repeat(50)}
|
|
789
948
|
`));
|
|
790
949
|
if (cliName === "claude") {
|
|
791
950
|
const detection = state.claudeDetection ?? detectClaude();
|
|
@@ -825,16 +984,77 @@ ${t("wizard.section.cli")}
|
|
|
825
984
|
spinner.succeed(t("wizard.cli.injected"));
|
|
826
985
|
state.itemStatus.set("claude", "configured");
|
|
827
986
|
state.sessionConfigured.add("claude");
|
|
828
|
-
state.pendingChanges.agent = {
|
|
987
|
+
state.pendingChanges.agent = {
|
|
988
|
+
...state.pendingChanges.agent || {},
|
|
989
|
+
claude: { adapter: "hooks" }
|
|
990
|
+
};
|
|
829
991
|
state.validationResults.set("claude", { ok: true });
|
|
992
|
+
return { action: "next", next: "channel-select" };
|
|
830
993
|
} catch (err) {
|
|
831
994
|
spinner.fail(t("wizard.cli.failed", { error: String(err) }));
|
|
832
995
|
state.itemStatus.set("claude", "error");
|
|
833
996
|
state.validationResults.set("claude", { ok: false, message: String(err) });
|
|
834
997
|
}
|
|
835
998
|
}
|
|
999
|
+
if (cliName === "codex") {
|
|
1000
|
+
const detection = state.codexDetection ?? detectCodex();
|
|
1001
|
+
const config = loadConfig();
|
|
1002
|
+
const current = config.agent.codex;
|
|
1003
|
+
if (!detection.installed) {
|
|
1004
|
+
console.log(pc.yellow(`! ${t("wizard.codex.notDetected")}`));
|
|
1005
|
+
console.log(pc.gray(` ${detection.message}
|
|
1006
|
+
`));
|
|
1007
|
+
state.itemStatus.set("codex", "not-found");
|
|
1008
|
+
state.validationResults.set("codex", { ok: false, message: detection.message });
|
|
1009
|
+
return { action: "next", next: "cli-menu" };
|
|
1010
|
+
}
|
|
1011
|
+
console.log(pc.green(`* ${t("wizard.codex.detected")}`));
|
|
1012
|
+
console.log(pc.gray(` Binary: ${detection.binaryPath}`));
|
|
1013
|
+
console.log(pc.gray(` Version: ${detection.version || "unknown"}`));
|
|
1014
|
+
console.log("");
|
|
1015
|
+
const enabled = await prompts.codexEnable(current?.enabled === true);
|
|
1016
|
+
if (!enabled) {
|
|
1017
|
+
state.pendingChanges.agent = {
|
|
1018
|
+
...state.pendingChanges.agent || {},
|
|
1019
|
+
codex: {
|
|
1020
|
+
...current || {},
|
|
1021
|
+
enabled: false
|
|
1022
|
+
}
|
|
1023
|
+
};
|
|
1024
|
+
state.itemStatus.set("codex", "unconfigured");
|
|
1025
|
+
state.validationResults.set("codex", { ok: true, message: t("wizard.codex.disabled") });
|
|
1026
|
+
console.log(pc.gray(` ${t("wizard.codex.disabled")}
|
|
1027
|
+
`));
|
|
1028
|
+
return { action: "next", next: "cli-menu" };
|
|
1029
|
+
}
|
|
1030
|
+
const commandInput = await prompts.codexCommand((current?.command || []).join(" "));
|
|
1031
|
+
const workDir = await prompts.codexWorkDir(current?.work_dir || "");
|
|
1032
|
+
const model = await prompts.codexModel(current?.model || "");
|
|
1033
|
+
const approvalPolicy = await prompts.codexApprovalPolicy(current?.approval_policy || "");
|
|
1034
|
+
state.pendingChanges.agent = {
|
|
1035
|
+
...state.pendingChanges.agent || {},
|
|
1036
|
+
codex: {
|
|
1037
|
+
...current || {},
|
|
1038
|
+
enabled: true,
|
|
1039
|
+
command: parseCommandInput(commandInput),
|
|
1040
|
+
work_dir: workDir,
|
|
1041
|
+
model,
|
|
1042
|
+
approval_policy: approvalPolicy
|
|
1043
|
+
}
|
|
1044
|
+
};
|
|
1045
|
+
state.itemStatus.set("codex", "configured");
|
|
1046
|
+
state.sessionConfigured.add("codex");
|
|
1047
|
+
state.validationResults.set("codex", { ok: true, message: t("wizard.codex.enabled") });
|
|
1048
|
+
console.log(pc.green(` ${t("wizard.codex.enabled")}
|
|
1049
|
+
`));
|
|
1050
|
+
return { action: "next", next: "channel-select" };
|
|
1051
|
+
}
|
|
836
1052
|
return { action: "next", next: "cli-menu" };
|
|
837
1053
|
}
|
|
1054
|
+
function parseCommandInput(input2) {
|
|
1055
|
+
const tokens = input2.trim().split(/\s+/).filter(Boolean);
|
|
1056
|
+
return tokens.length > 0 ? tokens : ["codex", "app-server", "--listen", "stdio://"];
|
|
1057
|
+
}
|
|
838
1058
|
function injectClaudeHooks(settingsPath, events) {
|
|
839
1059
|
const port = loadConfig().general.hook_server_port;
|
|
840
1060
|
const token = getOrCreateHookToken();
|
|
@@ -908,6 +1128,10 @@ function getChannelStatusLabel(status, error) {
|
|
|
908
1128
|
switch (status) {
|
|
909
1129
|
case "configured":
|
|
910
1130
|
return "[done]";
|
|
1131
|
+
case "detected":
|
|
1132
|
+
return "[detected]";
|
|
1133
|
+
case "not-found":
|
|
1134
|
+
return "[not found]";
|
|
911
1135
|
case "error":
|
|
912
1136
|
return `[error]${error ? ` (${error})` : ""}`;
|
|
913
1137
|
default:
|
|
@@ -917,39 +1141,22 @@ function getChannelStatusLabel(status, error) {
|
|
|
917
1141
|
|
|
918
1142
|
// src/cli/wizard/steps/channel-menu.ts
|
|
919
1143
|
import pc2 from "picocolors";
|
|
920
|
-
async function stepChannelMenu(state) {
|
|
921
|
-
console.log(pc2.bold(`
|
|
922
|
-
${t("wizard.section.channel")}
|
|
923
|
-
`));
|
|
924
|
-
const channelNames = ["logger", "telegram", "feishu"];
|
|
925
|
-
const channels = channelNames.map((name) => {
|
|
926
|
-
const status = state.itemStatus.get(name) || "unconfigured";
|
|
927
|
-
const validation = state.validationResults.get(name);
|
|
928
|
-
return {
|
|
929
|
-
name,
|
|
930
|
-
status: getChannelStatusLabel(status, validation?.message)
|
|
931
|
-
};
|
|
932
|
-
});
|
|
933
|
-
const choice = await prompts.channelMenuSelect(channels);
|
|
934
|
-
if (choice === "__next__") {
|
|
935
|
-
return { action: "next", next: "final" };
|
|
936
|
-
}
|
|
937
|
-
state.currentChannelName = choice;
|
|
938
|
-
return { action: "next", next: "channel-config" };
|
|
939
|
-
}
|
|
940
1144
|
async function stepCliMenu(state) {
|
|
941
1145
|
console.log(pc2.bold(`
|
|
942
1146
|
${t("wizard.section.cli")}
|
|
943
1147
|
`));
|
|
944
|
-
const
|
|
945
|
-
let
|
|
1148
|
+
const claudeStatus = state.itemStatus.get("claude") || "unconfigured";
|
|
1149
|
+
let claudeLabel = getChannelStatusLabel(claudeStatus);
|
|
946
1150
|
if (state.claudeDetection?.tokenMismatch) {
|
|
947
|
-
|
|
948
|
-
}
|
|
949
|
-
const
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
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);
|
|
953
1160
|
if (choice === "__next__") {
|
|
954
1161
|
return { action: "next", next: "channel-menu" };
|
|
955
1162
|
}
|
|
@@ -960,10 +1167,13 @@ ${t("wizard.section.cli")}
|
|
|
960
1167
|
// src/cli/wizard/steps/channel-config.ts
|
|
961
1168
|
import pc3 from "picocolors";
|
|
962
1169
|
import ora2 from "ora";
|
|
1170
|
+
var STEP2 = "STEP 4";
|
|
963
1171
|
async function stepChannelConfig(state) {
|
|
964
1172
|
const channelName = state.currentChannelName || "telegram";
|
|
965
1173
|
console.log(pc3.bold(`
|
|
966
|
-
${
|
|
1174
|
+
${STEP2} \u2014 Configure Channel
|
|
1175
|
+
`));
|
|
1176
|
+
console.log(pc3.gray(`${"\u2500".repeat(50)}
|
|
967
1177
|
`));
|
|
968
1178
|
while (true) {
|
|
969
1179
|
const cfg = loadConfig();
|
|
@@ -995,7 +1205,7 @@ ${t("wizard.channel.configuring", { channel: channelName })}
|
|
|
995
1205
|
continue;
|
|
996
1206
|
}
|
|
997
1207
|
state.itemStatus.set(channelName, "unconfigured");
|
|
998
|
-
return { action: "next", next: "channel-
|
|
1208
|
+
return { action: "next", next: "channel-select" };
|
|
999
1209
|
}
|
|
1000
1210
|
const msgSpinner = ora2(t("wizard.channel.telegram.sendingTest")).start();
|
|
1001
1211
|
try {
|
|
@@ -1016,7 +1226,7 @@ ${t("wizard.channel.configuring", { channel: channelName })}
|
|
|
1016
1226
|
state.itemStatus.set("telegram", "configured");
|
|
1017
1227
|
state.sessionConfigured.add("telegram");
|
|
1018
1228
|
state.validationResults.set("telegram", { ok: true });
|
|
1019
|
-
return { action: "next", next: "
|
|
1229
|
+
return { action: "next", next: "binding-confirm" };
|
|
1020
1230
|
}
|
|
1021
1231
|
if (channelName === "logger") {
|
|
1022
1232
|
state.pendingChanges.channel = {
|
|
@@ -1025,7 +1235,7 @@ ${t("wizard.channel.configuring", { channel: channelName })}
|
|
|
1025
1235
|
};
|
|
1026
1236
|
state.itemStatus.set("logger", "configured");
|
|
1027
1237
|
state.sessionConfigured.add("logger");
|
|
1028
|
-
return { action: "next", next: "
|
|
1238
|
+
return { action: "next", next: "binding-confirm" };
|
|
1029
1239
|
}
|
|
1030
1240
|
if (channelName === "feishu") {
|
|
1031
1241
|
const appId = await prompts.feishuAppId(String(currentConfig.app_id || ""));
|
|
@@ -1035,7 +1245,7 @@ ${t("wizard.channel.configuring", { channel: channelName })}
|
|
|
1035
1245
|
console.log(pc3.yellow(`! ${t("wizard.channel.feishu.required")}
|
|
1036
1246
|
`));
|
|
1037
1247
|
state.itemStatus.set("feishu", "unconfigured");
|
|
1038
|
-
return { action: "next", next: "channel-
|
|
1248
|
+
return { action: "next", next: "channel-select" };
|
|
1039
1249
|
}
|
|
1040
1250
|
const allowedUsers = allowedUsersInput.split(",").map((s) => s.trim()).filter((s) => s.length > 0);
|
|
1041
1251
|
state.pendingChanges.channel = {
|
|
@@ -1044,9 +1254,9 @@ ${t("wizard.channel.configuring", { channel: channelName })}
|
|
|
1044
1254
|
};
|
|
1045
1255
|
state.itemStatus.set("feishu", "configured");
|
|
1046
1256
|
state.sessionConfigured.add("feishu");
|
|
1047
|
-
return { action: "next", next: "
|
|
1257
|
+
return { action: "next", next: "binding-confirm" };
|
|
1048
1258
|
}
|
|
1049
|
-
return { action: "next", next: "
|
|
1259
|
+
return { action: "next", next: "agent-select" };
|
|
1050
1260
|
}
|
|
1051
1261
|
}
|
|
1052
1262
|
|
|
@@ -1073,6 +1283,167 @@ ${t("wizard.notify.title", { channel: channelName })}
|
|
|
1073
1283
|
return { action: "next", next: "channel-menu" };
|
|
1074
1284
|
}
|
|
1075
1285
|
|
|
1286
|
+
// src/cli/wizard/steps/agent-select.ts
|
|
1287
|
+
import pc5 from "picocolors";
|
|
1288
|
+
var STEP3 = "STEP 1";
|
|
1289
|
+
async function stepAgentSelect(state) {
|
|
1290
|
+
console.log(pc5.bold(`
|
|
1291
|
+
${STEP3} \u2014 Select Agent
|
|
1292
|
+
`));
|
|
1293
|
+
console.log(pc5.gray(`${"\u2500".repeat(50)}
|
|
1294
|
+
`));
|
|
1295
|
+
const config = loadConfig();
|
|
1296
|
+
const currentBindings = config.bindings ?? {};
|
|
1297
|
+
const agents = [];
|
|
1298
|
+
const claudeConfigured = !!config.agent.claude;
|
|
1299
|
+
const claudeBoundTo = currentBindings["claude"];
|
|
1300
|
+
const claudeStatus = claudeBoundTo ? pc5.green(`[bound to ${claudeBoundTo}]`) : claudeConfigured ? pc5.green("[configured]") : pc5.gray("[not configured]");
|
|
1301
|
+
agents.push({ name: "claude", value: "claude", status: claudeStatus });
|
|
1302
|
+
const codexDetection = detectCodex();
|
|
1303
|
+
const codexConfigured = !!config.agent.codex?.enabled;
|
|
1304
|
+
const codexBoundTo = currentBindings["codex"];
|
|
1305
|
+
const codexStatus = codexBoundTo ? pc5.green(`[bound to ${codexBoundTo}]`) : codexConfigured ? pc5.green("[configured]") : codexDetection.installed ? pc5.gray("[not configured]") : pc5.red("[not installed]");
|
|
1306
|
+
agents.push({ name: "codex", value: "codex", status: codexStatus });
|
|
1307
|
+
const choice = await prompts.agentSelect(agents);
|
|
1308
|
+
state.currentAgentName = choice;
|
|
1309
|
+
state.currentCliName = choice;
|
|
1310
|
+
const result = await stepCli(state);
|
|
1311
|
+
if (result.action === "back") {
|
|
1312
|
+
return { action: "next", next: "pair-continue" };
|
|
1313
|
+
}
|
|
1314
|
+
if (result.next === "cli-menu") {
|
|
1315
|
+
return { action: "next", next: "agent-select" };
|
|
1316
|
+
}
|
|
1317
|
+
return { action: "next", next: "channel-select" };
|
|
1318
|
+
}
|
|
1319
|
+
|
|
1320
|
+
// src/shared/channelInstance.ts
|
|
1321
|
+
import { createHash } from "crypto";
|
|
1322
|
+
function getChannelInstanceId(channelType, credential) {
|
|
1323
|
+
const hash = createHash("sha256").update(credential).digest("hex").slice(0, 12);
|
|
1324
|
+
return `${channelType}:${hash}`;
|
|
1325
|
+
}
|
|
1326
|
+
|
|
1327
|
+
// src/cli/wizard/steps/channel-select.ts
|
|
1328
|
+
import pc6 from "picocolors";
|
|
1329
|
+
var STEP4 = "STEP 3";
|
|
1330
|
+
async function stepChannelSelect(state) {
|
|
1331
|
+
console.log(pc6.bold(`
|
|
1332
|
+
${STEP4} \u2014 Select Channel
|
|
1333
|
+
`));
|
|
1334
|
+
console.log(pc6.gray(`${"\u2500".repeat(50)}
|
|
1335
|
+
`));
|
|
1336
|
+
const config = loadConfig();
|
|
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;
|
|
1342
|
+
const channels = [];
|
|
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);
|
|
1350
|
+
const boundAgent = Object.entries(currentBindings).find(([, v]) => v === instanceId)?.[0];
|
|
1351
|
+
const status = boundAgent ? pc6.green(`[bound to ${boundAgent}]`) : pc6.green("[configured]");
|
|
1352
|
+
channels.push({ name: "telegram", value: "telegram", status });
|
|
1353
|
+
} else {
|
|
1354
|
+
channels.push({ name: "telegram", value: "telegram", status: pc6.gray("[not configured]") });
|
|
1355
|
+
}
|
|
1356
|
+
if (mergedFeishu?.enabled && mergedFeishu.app_id) {
|
|
1357
|
+
const instanceId = getChannelInstanceId("feishu", mergedFeishu.app_id);
|
|
1358
|
+
const boundAgent = Object.entries(currentBindings).find(([, v]) => v === instanceId)?.[0];
|
|
1359
|
+
const status = boundAgent ? pc6.green(`[bound to ${boundAgent}]`) : pc6.green("[configured]");
|
|
1360
|
+
channels.push({ name: "feishu", value: "feishu", status });
|
|
1361
|
+
} else {
|
|
1362
|
+
channels.push({ name: "feishu", value: "feishu", status: pc6.gray("[not configured]") });
|
|
1363
|
+
}
|
|
1364
|
+
const choice = await prompts.channelSelect(channels);
|
|
1365
|
+
state.currentChannelName = choice;
|
|
1366
|
+
const result = await stepChannelConfig(state);
|
|
1367
|
+
if (result.action === "back") {
|
|
1368
|
+
return { action: "next", next: "agent-select" };
|
|
1369
|
+
}
|
|
1370
|
+
return { action: "next", next: "binding-confirm" };
|
|
1371
|
+
}
|
|
1372
|
+
|
|
1373
|
+
// src/cli/wizard/steps/binding-confirm.ts
|
|
1374
|
+
import pc7 from "picocolors";
|
|
1375
|
+
var STEP5 = "STEP 5";
|
|
1376
|
+
async function stepBindingConfirm(state) {
|
|
1377
|
+
const agentName = state.currentAgentName;
|
|
1378
|
+
const channelName = state.currentChannelName;
|
|
1379
|
+
console.log(pc7.bold(`
|
|
1380
|
+
${STEP5} \u2014 Confirm Binding
|
|
1381
|
+
`));
|
|
1382
|
+
console.log(pc7.gray(`${"\u2500".repeat(50)}
|
|
1383
|
+
`));
|
|
1384
|
+
const config = loadConfig();
|
|
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;
|
|
1392
|
+
const agentCurrentChannel = currentBindings[agentName];
|
|
1393
|
+
let channelCurrentAgent;
|
|
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);
|
|
1400
|
+
channelCurrentAgent = Object.entries(currentBindings).find(([, v]) => v === instanceId)?.[0];
|
|
1401
|
+
} else if (channelName === "logger") {
|
|
1402
|
+
instanceId = "logger:default";
|
|
1403
|
+
channelCurrentAgent = Object.entries(currentBindings).find(([, v]) => v === instanceId)?.[0];
|
|
1404
|
+
}
|
|
1405
|
+
console.log(` ${pc7.cyan(agentName)} current binding: ${agentCurrentChannel ? pc7.yellow(agentCurrentChannel) : pc7.gray("none")}`);
|
|
1406
|
+
console.log(` ${pc7.cyan(channelName)} current binding: ${channelCurrentAgent ? pc7.yellow(channelCurrentAgent) : pc7.gray("none")}`);
|
|
1407
|
+
console.log("");
|
|
1408
|
+
const choice = await prompts.bindingConfirm(agentName, channelName);
|
|
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];
|
|
1414
|
+
}
|
|
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}`));
|
|
1422
|
+
} else {
|
|
1423
|
+
console.log(pc7.gray(` Binding cancelled`));
|
|
1424
|
+
}
|
|
1425
|
+
console.log("");
|
|
1426
|
+
return { action: "next", next: "pair-continue" };
|
|
1427
|
+
}
|
|
1428
|
+
|
|
1429
|
+
// src/cli/wizard/steps/pair-continue.ts
|
|
1430
|
+
import pc8 from "picocolors";
|
|
1431
|
+
var STEP6 = "STEP 6";
|
|
1432
|
+
async function stepPairContinue(state) {
|
|
1433
|
+
console.log(pc8.bold(`
|
|
1434
|
+
${STEP6} \u2014 Add Another Pair?
|
|
1435
|
+
`));
|
|
1436
|
+
console.log(pc8.gray(`${"\u2500".repeat(50)}
|
|
1437
|
+
`));
|
|
1438
|
+
const choice = await prompts.pairContinue();
|
|
1439
|
+
if (choice === "continue") {
|
|
1440
|
+
state.currentAgentName = void 0;
|
|
1441
|
+
state.currentChannelName = void 0;
|
|
1442
|
+
return { action: "next", next: "agent-select" };
|
|
1443
|
+
}
|
|
1444
|
+
return { action: "next", next: "final" };
|
|
1445
|
+
}
|
|
1446
|
+
|
|
1076
1447
|
// src/cli/daemon.ts
|
|
1077
1448
|
import { spawn } from "child_process";
|
|
1078
1449
|
import { exec } from "child_process";
|
|
@@ -1155,14 +1526,14 @@ var DaemonManager = class {
|
|
|
1155
1526
|
}
|
|
1156
1527
|
}
|
|
1157
1528
|
try {
|
|
1158
|
-
const
|
|
1159
|
-
const logDir = join6(
|
|
1529
|
+
const homedir15 = process.env.HOME || process.env.USERPROFILE || "";
|
|
1530
|
+
const logDir = join6(homedir15, ".handsoff", "logs");
|
|
1160
1531
|
mkdirSync5(logDir, { recursive: true });
|
|
1161
1532
|
removePidFile(this.pidFilePath);
|
|
1162
1533
|
const __dirname3 = dirname4(fileURLToPath(import.meta.url));
|
|
1163
|
-
let daemonScript = join6(__dirname3, "..", "
|
|
1534
|
+
let daemonScript = join6(__dirname3, "..", "gateway", "process.js");
|
|
1164
1535
|
if (!existsSync7(daemonScript)) {
|
|
1165
|
-
daemonScript = join6(__dirname3, "..", "..", "src", "
|
|
1536
|
+
daemonScript = join6(__dirname3, "..", "..", "src", "gateway", "process.ts");
|
|
1166
1537
|
}
|
|
1167
1538
|
if (!existsSync7(daemonScript)) {
|
|
1168
1539
|
this.lastError = t("daemon.scriptNotFound", { path: daemonScript });
|
|
@@ -1244,20 +1615,20 @@ var DaemonManager = class {
|
|
|
1244
1615
|
// src/cli/wizard/steps/final.ts
|
|
1245
1616
|
import { join as join7 } from "path";
|
|
1246
1617
|
import { homedir as homedir6 } from "os";
|
|
1247
|
-
import
|
|
1618
|
+
import pc9 from "picocolors";
|
|
1248
1619
|
import ora3 from "ora";
|
|
1249
1620
|
async function stepFinal(state) {
|
|
1250
|
-
console.log(
|
|
1621
|
+
console.log(pc9.bold(`
|
|
1251
1622
|
${t("wizard.section.complete")}
|
|
1252
1623
|
`));
|
|
1253
1624
|
if (state.sessionConfigured.size === 0) {
|
|
1254
|
-
console.log(
|
|
1625
|
+
console.log(pc9.gray(` ${t("wizard.final.noItems")}
|
|
1255
1626
|
`));
|
|
1256
1627
|
} else {
|
|
1257
1628
|
for (const item of state.sessionConfigured) {
|
|
1258
1629
|
const result = state.validationResults.get(item);
|
|
1259
1630
|
if (result?.ok) {
|
|
1260
|
-
console.log(
|
|
1631
|
+
console.log(pc9.green(`* ${t("wizard.final.itemOk", { item })}`));
|
|
1261
1632
|
}
|
|
1262
1633
|
}
|
|
1263
1634
|
console.log("");
|
|
@@ -1265,12 +1636,12 @@ ${t("wizard.section.complete")}
|
|
|
1265
1636
|
const pidFilePath = join7(homedir6(), ".handsoff", "handsoff.pid");
|
|
1266
1637
|
const port = loadConfig().general.hook_server_port;
|
|
1267
1638
|
const daemon = new DaemonManager(pidFilePath, port);
|
|
1268
|
-
const hasChanges = state.sessionConfigured.size > 0 || Object.keys(state.pendingChanges).length > 0;
|
|
1639
|
+
const hasChanges = state.sessionConfigured.size > 0 || Object.keys(state.pendingChanges).length > 0 || Object.keys(state.bindings).length > 0;
|
|
1269
1640
|
if (daemon.isRunning()) {
|
|
1270
1641
|
if (hasChanges) {
|
|
1271
|
-
console.log(
|
|
1642
|
+
console.log(pc9.blue(`i ${t("wizard.final.runningChanges")}
|
|
1272
1643
|
`));
|
|
1273
|
-
console.log(
|
|
1644
|
+
console.log(pc9.yellow(` ${t("wizard.final.configModified")}
|
|
1274
1645
|
`));
|
|
1275
1646
|
const shouldRestart = await prompts.restartGateway();
|
|
1276
1647
|
if (shouldRestart) {
|
|
@@ -1283,20 +1654,20 @@ ${t("wizard.section.complete")}
|
|
|
1283
1654
|
spinner.fail(t("wizard.final.startFailed", { error: daemon.getLastError() ?? "unknown" }));
|
|
1284
1655
|
}
|
|
1285
1656
|
} else {
|
|
1286
|
-
console.log(
|
|
1657
|
+
console.log(pc9.gray(` ${t("wizard.final.restartTip")}
|
|
1287
1658
|
`));
|
|
1288
1659
|
}
|
|
1289
1660
|
} else {
|
|
1290
|
-
console.log(
|
|
1661
|
+
console.log(pc9.blue(`i ${t("wizard.final.running")}
|
|
1291
1662
|
`));
|
|
1292
|
-
console.log(
|
|
1663
|
+
console.log(pc9.green(` ${t("wizard.final.runningReady")}
|
|
1293
1664
|
`));
|
|
1294
1665
|
}
|
|
1295
1666
|
} else {
|
|
1296
|
-
console.log(
|
|
1667
|
+
console.log(pc9.blue(`i ${t("wizard.final.notRunning")}
|
|
1297
1668
|
`));
|
|
1298
1669
|
if (hasChanges) {
|
|
1299
|
-
console.log(
|
|
1670
|
+
console.log(pc9.yellow(` ${t("wizard.final.configModifiedStart")}
|
|
1300
1671
|
`));
|
|
1301
1672
|
}
|
|
1302
1673
|
const shouldStart = await prompts.restartGateway();
|
|
@@ -1309,11 +1680,11 @@ ${t("wizard.section.complete")}
|
|
|
1309
1680
|
spinner2.fail(t("wizard.final.startFailed", { error: daemon.getLastError() ?? "unknown" }));
|
|
1310
1681
|
}
|
|
1311
1682
|
} else {
|
|
1312
|
-
console.log(
|
|
1683
|
+
console.log(pc9.gray(` ${t("wizard.final.startTip")}
|
|
1313
1684
|
`));
|
|
1314
1685
|
}
|
|
1315
1686
|
}
|
|
1316
|
-
console.log(
|
|
1687
|
+
console.log(pc9.green(`
|
|
1317
1688
|
${t("wizard.final.done")}
|
|
1318
1689
|
`));
|
|
1319
1690
|
return { action: "next", next: "done" };
|
|
@@ -1333,23 +1704,30 @@ var WizardEngine = class {
|
|
|
1333
1704
|
await this.detectAllStatus();
|
|
1334
1705
|
while (this.state.current !== "done" && this.state.current !== "quit") {
|
|
1335
1706
|
if (this.state.current === "final" && this.hasChanges()) {
|
|
1336
|
-
this.applicator.apply(this.state.pendingChanges);
|
|
1707
|
+
this.applicator.apply(this.state.pendingChanges, this.state.bindings);
|
|
1337
1708
|
}
|
|
1338
1709
|
const result = await this.runStep(this.state.current);
|
|
1339
1710
|
if (result.action === "next") {
|
|
1340
1711
|
this.state.previous = this.state.current;
|
|
1341
1712
|
this.state.current = result.next;
|
|
1342
1713
|
} else if (result.action === "back") {
|
|
1343
|
-
this.state.current = this.state.previous || "
|
|
1714
|
+
this.state.current = this.state.previous || "agent-select";
|
|
1344
1715
|
} else if (result.action === "quit") {
|
|
1345
1716
|
this.state.current = "quit";
|
|
1346
1717
|
}
|
|
1347
1718
|
}
|
|
1348
1719
|
}
|
|
1349
1720
|
async detectAllStatus() {
|
|
1721
|
+
const config = loadConfig();
|
|
1350
1722
|
const claude = detectClaude();
|
|
1351
1723
|
this.state.claudeDetection = claude;
|
|
1352
1724
|
this.state.itemStatus.set("claude", claude.hooked ? "configured" : "unconfigured");
|
|
1725
|
+
const codex = detectCodex();
|
|
1726
|
+
this.state.codexDetection = codex;
|
|
1727
|
+
this.state.itemStatus.set(
|
|
1728
|
+
"codex",
|
|
1729
|
+
config.agent.codex?.enabled ? "configured" : codex.installed ? "detected" : "not-found"
|
|
1730
|
+
);
|
|
1353
1731
|
const channelNames = ["logger", "telegram", "feishu"];
|
|
1354
1732
|
for (const name of channelNames) {
|
|
1355
1733
|
const detection = await detectChannel(name);
|
|
@@ -1362,12 +1740,18 @@ var WizardEngine = class {
|
|
|
1362
1740
|
}
|
|
1363
1741
|
async runStep(step) {
|
|
1364
1742
|
switch (step) {
|
|
1743
|
+
case "agent-select":
|
|
1744
|
+
return await stepAgentSelect(this.state);
|
|
1745
|
+
case "channel-select":
|
|
1746
|
+
return await stepChannelSelect(this.state);
|
|
1747
|
+
case "binding-confirm":
|
|
1748
|
+
return await stepBindingConfirm(this.state);
|
|
1749
|
+
case "pair-continue":
|
|
1750
|
+
return await stepPairContinue(this.state);
|
|
1365
1751
|
case "cli":
|
|
1366
1752
|
return await stepCli(this.state);
|
|
1367
1753
|
case "cli-menu":
|
|
1368
1754
|
return await stepCliMenu(this.state);
|
|
1369
|
-
case "channel-menu":
|
|
1370
|
-
return await stepChannelMenu(this.state);
|
|
1371
1755
|
case "channel-config":
|
|
1372
1756
|
return await stepChannelConfig(this.state);
|
|
1373
1757
|
case "channel-notify":
|
|
@@ -1379,7 +1763,7 @@ var WizardEngine = class {
|
|
|
1379
1763
|
}
|
|
1380
1764
|
}
|
|
1381
1765
|
hasChanges() {
|
|
1382
|
-
return this.state.sessionConfigured.size > 0 || Object.keys(this.state.pendingChanges).length > 0;
|
|
1766
|
+
return this.state.sessionConfigured.size > 0 || Object.keys(this.state.pendingChanges).length > 0 || Object.keys(this.state.bindings).length > 0;
|
|
1383
1767
|
}
|
|
1384
1768
|
};
|
|
1385
1769
|
|
|
@@ -1921,9 +2305,170 @@ function registerClaudeCommand(program2) {
|
|
|
1921
2305
|
});
|
|
1922
2306
|
}
|
|
1923
2307
|
|
|
2308
|
+
// src/cli/agent/codex.ts
|
|
2309
|
+
import { spawn as spawn4 } from "child_process";
|
|
2310
|
+
import { homedir as homedir14 } from "os";
|
|
2311
|
+
import { join as join16 } from "path";
|
|
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
|
|
2388
|
+
function isCodexInstalled() {
|
|
2389
|
+
try {
|
|
2390
|
+
execSync3("codex --version", { encoding: "utf8", stdio: "pipe", windowsHide: true });
|
|
2391
|
+
return true;
|
|
2392
|
+
} catch {
|
|
2393
|
+
return false;
|
|
2394
|
+
}
|
|
2395
|
+
}
|
|
2396
|
+
function getCodexVersion() {
|
|
2397
|
+
try {
|
|
2398
|
+
return execSync3("codex --version", { encoding: "utf8", stdio: "pipe", windowsHide: true }).trim();
|
|
2399
|
+
} catch {
|
|
2400
|
+
return void 0;
|
|
2401
|
+
}
|
|
2402
|
+
}
|
|
2403
|
+
function registerCodexCommand(program2) {
|
|
2404
|
+
program2.command("codex").description("Start Codex CLI with handsoff integration").option("-d, --directory <dir>", "Working directory for Codex").action(async (options) => {
|
|
2405
|
+
const version = getCodexVersion();
|
|
2406
|
+
if (!isCodexInstalled()) {
|
|
2407
|
+
console.error("\n" + t("cli.codex.notInstalled") + "\n");
|
|
2408
|
+
console.error(t("cli.codex.installMethods") + "\n");
|
|
2409
|
+
console.error(t("cli.codex.option1npm") + "\n");
|
|
2410
|
+
console.error(t("cli.codex.option2brew") + "\n");
|
|
2411
|
+
console.error(t("cli.codex.alternative") + "\n");
|
|
2412
|
+
process.exit(1);
|
|
2413
|
+
}
|
|
2414
|
+
const config = loadConfig();
|
|
2415
|
+
const port = config.general.hook_server_port;
|
|
2416
|
+
const pidFilePath = join16(homedir14(), ".handsoff", "handsoff.pid");
|
|
2417
|
+
const daemon = new DaemonManager(pidFilePath, port);
|
|
2418
|
+
let daemonStarted = false;
|
|
2419
|
+
if (daemon.isRunning()) {
|
|
2420
|
+
console.log(t("cli.codex.alreadyRunning"));
|
|
2421
|
+
daemonStarted = true;
|
|
2422
|
+
} else {
|
|
2423
|
+
console.log(t("cli.codex.starting"));
|
|
2424
|
+
daemonStarted = await daemon.start();
|
|
2425
|
+
if (!daemonStarted) {
|
|
2426
|
+
console.error(t("cli.codex.startFailed"));
|
|
2427
|
+
process.exit(1);
|
|
2428
|
+
}
|
|
2429
|
+
console.log(t("cli.codex.started"));
|
|
2430
|
+
}
|
|
2431
|
+
if (!config.agent.codex?.enabled) {
|
|
2432
|
+
console.error(t("cli.codex.notEnabled"));
|
|
2433
|
+
console.error(t("cli.codex.enableHint"));
|
|
2434
|
+
process.exit(1);
|
|
2435
|
+
}
|
|
2436
|
+
const workDir = options.directory || process.cwd();
|
|
2437
|
+
const codexCommand = config.agent.codex?.command?.[0] || "codex";
|
|
2438
|
+
const codexArgs = config.agent.codex?.command?.slice(1) || ["app-server", "--listen", "stdio://"];
|
|
2439
|
+
console.log(t("cli.codex.startingIn", { dir: workDir, version: version || "unknown" }));
|
|
2440
|
+
console.log(t("cli.codex.pressCtrlC") + "\n");
|
|
2441
|
+
const codexProcess = spawn4(codexCommand, codexArgs, {
|
|
2442
|
+
cwd: workDir,
|
|
2443
|
+
stdio: "inherit",
|
|
2444
|
+
shell: true
|
|
2445
|
+
});
|
|
2446
|
+
const logger = createAgentLogger("codex");
|
|
2447
|
+
logger.info({ command: codexCommand, args: codexArgs, cwd: workDir }, "Codex CLI spawned");
|
|
2448
|
+
codexProcess.on("exit", (code) => {
|
|
2449
|
+
logger.info({ code }, "Codex CLI exited");
|
|
2450
|
+
console.log(t("cli.codex.exited", { code }));
|
|
2451
|
+
console.log(t("cli.codex.daemonRunning"));
|
|
2452
|
+
process.exit(code || 0);
|
|
2453
|
+
});
|
|
2454
|
+
codexProcess.on("error", (err) => {
|
|
2455
|
+
logger.error({ error: err.message }, "Codex CLI spawn failed");
|
|
2456
|
+
});
|
|
2457
|
+
process.on("SIGINT", () => {
|
|
2458
|
+
console.log(t("cli.codex.detaching"));
|
|
2459
|
+
codexProcess.kill("SIGINT");
|
|
2460
|
+
});
|
|
2461
|
+
process.on("SIGTERM", () => {
|
|
2462
|
+
console.log(t("cli.codex.detaching"));
|
|
2463
|
+
codexProcess.kill("SIGTERM");
|
|
2464
|
+
});
|
|
2465
|
+
});
|
|
2466
|
+
}
|
|
2467
|
+
|
|
1924
2468
|
// src/cli/agent/index.ts
|
|
1925
2469
|
function registerAgentCommands(program2) {
|
|
1926
2470
|
registerClaudeCommand(program2);
|
|
2471
|
+
registerCodexCommand(program2);
|
|
1927
2472
|
}
|
|
1928
2473
|
|
|
1929
2474
|
// src/cli/index.ts
|