handsoff 0.1.1 → 0.1.2-beta.1
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/config.example.toml +15 -22
- package/dist/cli/index.js +230 -89
- package/dist/cli/index.js.map +1 -1
- package/dist/gateway/process.js +2773 -2419
- package/dist/gateway/process.js.map +1 -1
- package/package.json +1 -1
package/config.example.toml
CHANGED
|
@@ -2,20 +2,9 @@
|
|
|
2
2
|
# Copy to ~/.handsoff/config.toml and fill in your credentials
|
|
3
3
|
|
|
4
4
|
[general]
|
|
5
|
-
default_agent = "claude"
|
|
6
5
|
log_level = "info"
|
|
7
6
|
hook_server_port = 9876
|
|
8
7
|
|
|
9
|
-
[channel.telegram]
|
|
10
|
-
enabled = true
|
|
11
|
-
bot_token = "YOUR_BOT_TOKEN"
|
|
12
|
-
allowed_users = [123456789]
|
|
13
|
-
|
|
14
|
-
[channel.feishu]
|
|
15
|
-
enabled = false
|
|
16
|
-
app_id = ""
|
|
17
|
-
app_secret = ""
|
|
18
|
-
|
|
19
8
|
[agent.claude]
|
|
20
9
|
adapter = "hooks"
|
|
21
10
|
binary = "claude"
|
|
@@ -23,22 +12,26 @@ work_dir = ""
|
|
|
23
12
|
model = ""
|
|
24
13
|
permission_mode = ""
|
|
25
14
|
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
15
|
+
[agent.codex]
|
|
16
|
+
enabled = false
|
|
17
|
+
command = ["codex", "app-server", "--listen", "stdio://"]
|
|
18
|
+
work_dir = ""
|
|
19
|
+
model = ""
|
|
20
|
+
approval_policy = ""
|
|
30
21
|
|
|
31
|
-
[
|
|
22
|
+
[channel.app]
|
|
32
23
|
enabled = false
|
|
33
24
|
channel_id = "app:desktop-001"
|
|
34
25
|
auth_token = "change-me"
|
|
35
26
|
notify_types = ["*"]
|
|
36
27
|
heartbeat_interval_ms = 30000
|
|
37
28
|
|
|
38
|
-
[
|
|
29
|
+
[channel.telegram]
|
|
30
|
+
enabled = true
|
|
31
|
+
bot_token = "YOUR_BOT_TOKEN"
|
|
32
|
+
allowed_users = [123456789]
|
|
33
|
+
|
|
34
|
+
[channel.feishu]
|
|
39
35
|
enabled = false
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
model = ""
|
|
43
|
-
approval_policy = ""
|
|
44
|
-
sandbox_mode = ""
|
|
36
|
+
app_id = ""
|
|
37
|
+
app_secret = ""
|
package/dist/cli/index.js
CHANGED
|
@@ -8,7 +8,13 @@ import { Command } from "commander";
|
|
|
8
8
|
|
|
9
9
|
// src/shared/settings-merge.ts
|
|
10
10
|
function isHandsoffHook(hook) {
|
|
11
|
-
|
|
11
|
+
if (hook.type === "http" && hook.url) {
|
|
12
|
+
return hook.url.includes("/hook/");
|
|
13
|
+
}
|
|
14
|
+
if (hook.type === "command" && hook.command) {
|
|
15
|
+
return hook.command.includes("/hook/") && hook.command.includes("curl");
|
|
16
|
+
}
|
|
17
|
+
return false;
|
|
12
18
|
}
|
|
13
19
|
function isHandsoffEntry(entry) {
|
|
14
20
|
return entry.hooks.some((hook) => isHandsoffHook(hook));
|
|
@@ -35,10 +41,15 @@ function mergeHooks(existing, newHooks) {
|
|
|
35
41
|
const existingEntries = cleaned.hooks?.[eventName] || [];
|
|
36
42
|
const newEntries = newHooks[eventName] || [];
|
|
37
43
|
const existingUrls = new Set(
|
|
38
|
-
existingEntries.flatMap((e) => e.hooks.map((h) => h.url))
|
|
44
|
+
existingEntries.flatMap((e) => e.hooks.map((h) => h.url).filter(Boolean))
|
|
45
|
+
);
|
|
46
|
+
const existingCommands = new Set(
|
|
47
|
+
existingEntries.flatMap((e) => e.hooks.map((h) => h.command).filter(Boolean))
|
|
39
48
|
);
|
|
40
49
|
const uniqueNewEntries = newEntries.filter(
|
|
41
|
-
(entry) => !entry.hooks.some(
|
|
50
|
+
(entry) => !entry.hooks.some(
|
|
51
|
+
(h) => h.url && existingUrls.has(h.url) || h.command && existingCommands.has(h.command)
|
|
52
|
+
)
|
|
42
53
|
);
|
|
43
54
|
const merged = [...uniqueNewEntries, ...existingEntries];
|
|
44
55
|
if (merged.length > 0) {
|
|
@@ -124,7 +135,6 @@ import dotenv from "dotenv";
|
|
|
124
135
|
dotenv.config();
|
|
125
136
|
var DEFAULT_CONFIG = {
|
|
126
137
|
general: {
|
|
127
|
-
default_agent: "claude",
|
|
128
138
|
log_level: "info",
|
|
129
139
|
hook_server_port: 9876
|
|
130
140
|
},
|
|
@@ -148,6 +158,11 @@ var DEFAULT_CONFIG = {
|
|
|
148
158
|
timeout_ms: 3e5,
|
|
149
159
|
// 5 minutes
|
|
150
160
|
default_on_timeout: "deny"
|
|
161
|
+
},
|
|
162
|
+
app: {
|
|
163
|
+
enabled: false,
|
|
164
|
+
channel_id: "",
|
|
165
|
+
auth_token: ""
|
|
151
166
|
}
|
|
152
167
|
},
|
|
153
168
|
agent: {
|
|
@@ -156,21 +171,16 @@ var DEFAULT_CONFIG = {
|
|
|
156
171
|
binary: "claude",
|
|
157
172
|
work_dir: "",
|
|
158
173
|
model: "",
|
|
159
|
-
permission_mode: ""
|
|
160
|
-
permission: {
|
|
161
|
-
lowRiskTools: ["Read", "Glob", "Grep", "List", "WebSearch", "codesearch"]
|
|
162
|
-
}
|
|
174
|
+
permission_mode: ""
|
|
163
175
|
},
|
|
164
176
|
codex: {
|
|
165
177
|
enabled: false,
|
|
166
178
|
command: ["codex", "app-server", "--listen", "stdio://"],
|
|
167
179
|
work_dir: "",
|
|
168
180
|
model: "",
|
|
169
|
-
approval_policy: ""
|
|
170
|
-
sandbox_mode: ""
|
|
181
|
+
approval_policy: ""
|
|
171
182
|
}
|
|
172
183
|
},
|
|
173
|
-
channels: {},
|
|
174
184
|
bindings: {}
|
|
175
185
|
};
|
|
176
186
|
function getConfigPath() {
|
|
@@ -218,16 +228,21 @@ function mergeWithDefaults(raw) {
|
|
|
218
228
|
10
|
|
219
229
|
),
|
|
220
230
|
default_on_timeout: process.env.HANDSOFF_PERMISSION_DEFAULT || raw.channel?.permission?.default_on_timeout || DEFAULT_CONFIG.channel.permission.default_on_timeout
|
|
231
|
+
},
|
|
232
|
+
app: {
|
|
233
|
+
...DEFAULT_CONFIG.channel.app,
|
|
234
|
+
...raw.channel?.app || {},
|
|
235
|
+
enabled: raw.channel?.app?.enabled === true,
|
|
236
|
+
channel_id: String(raw.channel?.app?.channel_id || ""),
|
|
237
|
+
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
|
|
221
240
|
}
|
|
222
241
|
},
|
|
223
242
|
agent: {
|
|
224
243
|
claude: {
|
|
225
244
|
...DEFAULT_CONFIG.agent.claude,
|
|
226
|
-
...raw.agent?.claude || {}
|
|
227
|
-
permission: {
|
|
228
|
-
...DEFAULT_CONFIG.agent.claude.permission,
|
|
229
|
-
...raw.agent?.claude?.permission || {}
|
|
230
|
-
}
|
|
245
|
+
...raw.agent?.claude || {}
|
|
231
246
|
},
|
|
232
247
|
codex: {
|
|
233
248
|
...DEFAULT_CONFIG.agent.codex,
|
|
@@ -235,21 +250,9 @@ function mergeWithDefaults(raw) {
|
|
|
235
250
|
command: raw.agent?.codex?.command ? raw.agent.codex.command : DEFAULT_CONFIG.agent.codex.command,
|
|
236
251
|
work_dir: raw.agent?.codex?.work_dir || DEFAULT_CONFIG.agent.codex.work_dir,
|
|
237
252
|
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
|
|
253
|
+
approval_policy: raw.agent?.codex?.approval_policy || DEFAULT_CONFIG.agent.codex.approval_policy
|
|
240
254
|
}
|
|
241
255
|
},
|
|
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
256
|
bindings: {
|
|
254
257
|
...DEFAULT_CONFIG.bindings,
|
|
255
258
|
...raw.bindings || {}
|
|
@@ -296,7 +299,11 @@ var en_default = {
|
|
|
296
299
|
noHooksSelected: "No hooks selected, skipping injection.",
|
|
297
300
|
injecting: "Injecting hooks...",
|
|
298
301
|
injected: "Hooks injected",
|
|
299
|
-
failed: "Failed: {{error}}"
|
|
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}}"
|
|
300
307
|
},
|
|
301
308
|
channel: {
|
|
302
309
|
configuring: "Configuring {{channel}}",
|
|
@@ -515,6 +522,22 @@ var en_default = {
|
|
|
515
522
|
unauthorized: "You are not authorized to use this bot.",
|
|
516
523
|
received: "Message received.",
|
|
517
524
|
processingError: "Error processing message."
|
|
525
|
+
},
|
|
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"
|
|
518
541
|
}
|
|
519
542
|
};
|
|
520
543
|
|
|
@@ -668,12 +691,18 @@ var ConfigApplicator = class {
|
|
|
668
691
|
};
|
|
669
692
|
let finalConfig = merged;
|
|
670
693
|
if (bindings && Object.keys(bindings).length > 0) {
|
|
694
|
+
const finalBindings = { ...merged.bindings || {} };
|
|
695
|
+
for (const [agent, channelId] of Object.entries(bindings)) {
|
|
696
|
+
for (const [a, cid] of Object.entries(finalBindings)) {
|
|
697
|
+
if (cid === channelId && a !== agent) {
|
|
698
|
+
delete finalBindings[a];
|
|
699
|
+
}
|
|
700
|
+
}
|
|
701
|
+
finalBindings[agent] = channelId;
|
|
702
|
+
}
|
|
671
703
|
finalConfig = {
|
|
672
704
|
...merged,
|
|
673
|
-
bindings:
|
|
674
|
-
...merged.bindings || {},
|
|
675
|
-
...bindings
|
|
676
|
-
}
|
|
705
|
+
bindings: finalBindings
|
|
677
706
|
};
|
|
678
707
|
}
|
|
679
708
|
const configDir = join4(homedir4(), ".handsoff");
|
|
@@ -776,6 +805,16 @@ function extractHandsoffHookInfo(entry) {
|
|
|
776
805
|
}
|
|
777
806
|
return { isHandsoff: false };
|
|
778
807
|
}
|
|
808
|
+
function detectPermissionMode() {
|
|
809
|
+
const settingsPath = join5(homedir5(), ".claude", "settings.json");
|
|
810
|
+
if (!existsSync4(settingsPath)) return null;
|
|
811
|
+
try {
|
|
812
|
+
const settings = JSON.parse(readFileSync4(settingsPath, "utf-8"));
|
|
813
|
+
return settings.permissionMode ?? null;
|
|
814
|
+
} catch {
|
|
815
|
+
return null;
|
|
816
|
+
}
|
|
817
|
+
}
|
|
779
818
|
|
|
780
819
|
// src/cli/wizard/detectors/codex.ts
|
|
781
820
|
import { execSync as execSync2 } from "child_process";
|
|
@@ -850,6 +889,25 @@ var prompts = {
|
|
|
850
889
|
{ name: t("wizard.cli.backAction"), value: "back" }
|
|
851
890
|
]
|
|
852
891
|
}),
|
|
892
|
+
claudeMode: () => select({
|
|
893
|
+
message: "Choose Claude integration mode:",
|
|
894
|
+
choices: [
|
|
895
|
+
{ name: "Hook mode (local one-way)", value: "hooks" },
|
|
896
|
+
{ name: "Remote mode (managed two-way)", value: "remote" }
|
|
897
|
+
]
|
|
898
|
+
}),
|
|
899
|
+
permissionModeSelect: (defaultValue) => select({
|
|
900
|
+
message: "Permission Mode",
|
|
901
|
+
choices: [
|
|
902
|
+
{ name: "acceptEdits \u2014 Auto-approve safe edits (Recommended)", value: "acceptEdits" },
|
|
903
|
+
{ name: "default \u2014 Ask via TTY or hook", value: "default" },
|
|
904
|
+
{ name: "bypassPermissions \u2014 Approve all, no prompts", value: "bypassPermissions" },
|
|
905
|
+
{ name: "dontAsk \u2014 Deny all tool execution", value: "dontAsk" },
|
|
906
|
+
{ name: "plan \u2014 Plan only, no tool execution", value: "plan" },
|
|
907
|
+
{ name: "auto \u2014 Auto-handle based on risk level", value: "auto" }
|
|
908
|
+
],
|
|
909
|
+
default: defaultValue ?? "acceptEdits"
|
|
910
|
+
}),
|
|
853
911
|
cliHookSelect: (selected) => checkbox({
|
|
854
912
|
message: t("wizard.cli.hookCheckbox"),
|
|
855
913
|
choices: [
|
|
@@ -886,10 +944,10 @@ var prompts = {
|
|
|
886
944
|
notifyTypes: (selected) => checkbox({
|
|
887
945
|
message: t("wizard.notify.checkbox"),
|
|
888
946
|
choices: [
|
|
889
|
-
{ name: "
|
|
890
|
-
{ name: "
|
|
891
|
-
{ name: "finished", value: "finished", checked: selected?.includes("finished") },
|
|
892
|
-
{ name: "
|
|
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") },
|
|
893
951
|
{ name: "error", value: "error", checked: selected?.includes("error") }
|
|
894
952
|
]
|
|
895
953
|
}),
|
|
@@ -919,8 +977,8 @@ var prompts = {
|
|
|
919
977
|
pairContinue: () => select({
|
|
920
978
|
message: "Add another pair?",
|
|
921
979
|
choices: [
|
|
922
|
-
{ name: "
|
|
923
|
-
{ name: "
|
|
980
|
+
{ name: "Finish setup", value: "finish" },
|
|
981
|
+
{ name: "Add another pair", value: "continue" }
|
|
924
982
|
]
|
|
925
983
|
}),
|
|
926
984
|
agentSelect: (agents) => select({
|
|
@@ -938,6 +996,7 @@ import { writeFileSync as writeFileSync4, existsSync as existsSync5, readFileSyn
|
|
|
938
996
|
import { dirname as dirname3 } from "path";
|
|
939
997
|
import pc from "picocolors";
|
|
940
998
|
import ora from "ora";
|
|
999
|
+
import { execSync as execSync3 } from "child_process";
|
|
941
1000
|
var STEP = "STEP 2";
|
|
942
1001
|
async function stepCli(state) {
|
|
943
1002
|
const cliName = state.currentCliName || "claude";
|
|
@@ -967,34 +1026,58 @@ ${STEP} \u2014 Configure Agent
|
|
|
967
1026
|
console.log(pc.yellow(` ${t("wizard.cli.hooksNotInstalled")}`));
|
|
968
1027
|
}
|
|
969
1028
|
console.log("");
|
|
970
|
-
const
|
|
971
|
-
if (
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
1029
|
+
const mode = await prompts.claudeMode();
|
|
1030
|
+
if (mode === "hooks") {
|
|
1031
|
+
const action = await prompts.cliActionSelect(detection.hooked);
|
|
1032
|
+
if (action === "back") {
|
|
1033
|
+
console.log("");
|
|
1034
|
+
return { action: "next", next: "cli-menu" };
|
|
1035
|
+
}
|
|
1036
|
+
const selectedHooks = await prompts.cliHookSelect();
|
|
1037
|
+
if (selectedHooks.length === 0) {
|
|
1038
|
+
console.log(pc.yellow(`! ${t("wizard.cli.noHooksSelected")}
|
|
978
1039
|
`));
|
|
979
|
-
|
|
1040
|
+
return { action: "next", next: "cli-menu" };
|
|
1041
|
+
}
|
|
1042
|
+
const spinner = ora(t("wizard.cli.injecting")).start();
|
|
1043
|
+
try {
|
|
1044
|
+
injectClaudeHooks(detection.settingsPath, selectedHooks);
|
|
1045
|
+
spinner.succeed(t("wizard.cli.injected"));
|
|
1046
|
+
} catch (err) {
|
|
1047
|
+
spinner.fail(t("wizard.cli.failed", { error: String(err) }));
|
|
1048
|
+
state.itemStatus.set("claude", "error");
|
|
1049
|
+
state.validationResults.set("claude", { ok: false, message: String(err) });
|
|
1050
|
+
return { action: "next", next: "cli-menu" };
|
|
1051
|
+
}
|
|
1052
|
+
} else {
|
|
1053
|
+
const spinner = ora(t("wizard.cli.cleaningHooks")).start();
|
|
1054
|
+
try {
|
|
1055
|
+
cleanupClaudeHooks(detection.settingsPath);
|
|
1056
|
+
spinner.succeed(t("wizard.cli.hooksCleaned"));
|
|
1057
|
+
} catch (err) {
|
|
1058
|
+
spinner.warn(t("wizard.cli.cleanupWarning", { error: String(err) }));
|
|
1059
|
+
}
|
|
1060
|
+
console.log(pc.yellow(`! ${t("wizard.cli.remoteModeSkipHooks")}`));
|
|
1061
|
+
try {
|
|
1062
|
+
execSync3("claude --version", { stdio: "ignore" });
|
|
1063
|
+
} catch {
|
|
1064
|
+
console.log(pc.yellow("! claude CLI not found in PATH; remote mode may not work until it is installed."));
|
|
1065
|
+
}
|
|
980
1066
|
}
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
return { action: "next", next: "
|
|
993
|
-
} catch (err) {
|
|
994
|
-
spinner.fail(t("wizard.cli.failed", { error: String(err) }));
|
|
995
|
-
state.itemStatus.set("claude", "error");
|
|
996
|
-
state.validationResults.set("claude", { ok: false, message: String(err) });
|
|
1067
|
+
state.itemStatus.set("claude", "configured");
|
|
1068
|
+
state.sessionConfigured.add("claude");
|
|
1069
|
+
state.pendingChanges.agent = {
|
|
1070
|
+
...state.pendingChanges.agent || {},
|
|
1071
|
+
claude: {
|
|
1072
|
+
...state.pendingChanges.agent?.claude || {},
|
|
1073
|
+
adapter: mode
|
|
1074
|
+
}
|
|
1075
|
+
};
|
|
1076
|
+
state.validationResults.set("claude", { ok: true });
|
|
1077
|
+
if (mode === "remote") {
|
|
1078
|
+
return { action: "next", next: "permission-mode" };
|
|
997
1079
|
}
|
|
1080
|
+
return { action: "next", next: "channel-select" };
|
|
998
1081
|
}
|
|
999
1082
|
if (cliName === "codex") {
|
|
1000
1083
|
const detection = state.codexDetection ?? detectCodex();
|
|
@@ -1058,8 +1141,7 @@ function parseCommandInput(input2) {
|
|
|
1058
1141
|
function injectClaudeHooks(settingsPath, events) {
|
|
1059
1142
|
const port = loadConfig().general.hook_server_port;
|
|
1060
1143
|
const token = getOrCreateHookToken();
|
|
1061
|
-
const
|
|
1062
|
-
const hooksConfig = generateHooksConfig2(baseUrl, events);
|
|
1144
|
+
const hooksConfig = generateHooksConfig2(port, token, events);
|
|
1063
1145
|
let currentSettings = {};
|
|
1064
1146
|
if (existsSync5(settingsPath)) {
|
|
1065
1147
|
try {
|
|
@@ -1074,17 +1156,40 @@ function injectClaudeHooks(settingsPath, events) {
|
|
|
1074
1156
|
mkdirSync4(dirname3(settingsPath), { recursive: true });
|
|
1075
1157
|
writeFileSync4(settingsPath, JSON.stringify(mergedSettings, null, 2));
|
|
1076
1158
|
}
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1159
|
+
var COMMAND_ONLY_HOOKS = ["SessionStart", "Setup"];
|
|
1160
|
+
function generateHooksConfig2(port, token, events) {
|
|
1161
|
+
const unifiedUrl = `http://localhost:${port}/hook/${token}/event`;
|
|
1162
|
+
const curlCommand = `curl -s -X POST -H 'Content-Type: application/json' -d @- ${unifiedUrl}`;
|
|
1163
|
+
const createEntry = (event) => {
|
|
1164
|
+
const isCommandOnly = COMMAND_ONLY_HOOKS.includes(event);
|
|
1165
|
+
return {
|
|
1166
|
+
matcher: "",
|
|
1167
|
+
hooks: [{
|
|
1168
|
+
type: isCommandOnly ? "command" : "http",
|
|
1169
|
+
...isCommandOnly ? { command: curlCommand } : { url: unifiedUrl }
|
|
1170
|
+
}]
|
|
1171
|
+
};
|
|
1172
|
+
};
|
|
1082
1173
|
const config = {};
|
|
1083
1174
|
for (const event of events) {
|
|
1084
1175
|
config[event] = [createEntry(event)];
|
|
1085
1176
|
}
|
|
1086
1177
|
return config;
|
|
1087
1178
|
}
|
|
1179
|
+
function cleanupClaudeHooks(settingsPath) {
|
|
1180
|
+
if (!existsSync5(settingsPath)) return;
|
|
1181
|
+
let currentSettings = {};
|
|
1182
|
+
try {
|
|
1183
|
+
currentSettings = JSON.parse(readFileSync5(settingsPath, "utf-8"));
|
|
1184
|
+
} catch {
|
|
1185
|
+
}
|
|
1186
|
+
const cleanedSettings = removeHandsoffHooks(currentSettings);
|
|
1187
|
+
if (!cleanedSettings.hooks || Object.keys(cleanedSettings.hooks).length === 0) {
|
|
1188
|
+
delete cleanedSettings.hooks;
|
|
1189
|
+
}
|
|
1190
|
+
mkdirSync4(dirname3(settingsPath), { recursive: true });
|
|
1191
|
+
writeFileSync4(settingsPath, JSON.stringify(cleanedSettings, null, 2));
|
|
1192
|
+
}
|
|
1088
1193
|
|
|
1089
1194
|
// src/cli/wizard/detectors/channel.ts
|
|
1090
1195
|
async function detectChannel(name) {
|
|
@@ -1314,7 +1419,7 @@ ${STEP3} \u2014 Select Agent
|
|
|
1314
1419
|
if (result.next === "cli-menu") {
|
|
1315
1420
|
return { action: "next", next: "agent-select" };
|
|
1316
1421
|
}
|
|
1317
|
-
return { action: "next", next:
|
|
1422
|
+
return { action: "next", next: result.next };
|
|
1318
1423
|
}
|
|
1319
1424
|
|
|
1320
1425
|
// src/shared/channelInstance.ts
|
|
@@ -1344,7 +1449,6 @@ ${STEP4} \u2014 Select Channel
|
|
|
1344
1449
|
const loggerInstanceId = "logger:default";
|
|
1345
1450
|
const loggerBoundAgent = Object.entries(currentBindings).find(([, v]) => v === loggerInstanceId)?.[0];
|
|
1346
1451
|
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
1452
|
if (mergedTelegram?.enabled && mergedTelegram.bot_token) {
|
|
1349
1453
|
const instanceId = getChannelInstanceId("telegram", mergedTelegram.bot_token);
|
|
1350
1454
|
const boundAgent = Object.entries(currentBindings).find(([, v]) => v === instanceId)?.[0];
|
|
@@ -1361,6 +1465,7 @@ ${STEP4} \u2014 Select Channel
|
|
|
1361
1465
|
} else {
|
|
1362
1466
|
channels.push({ name: "feishu", value: "feishu", status: pc6.gray("[not configured]") });
|
|
1363
1467
|
}
|
|
1468
|
+
channels.push({ name: "logger", value: "logger", status: loggerStatus });
|
|
1364
1469
|
const choice = await prompts.channelSelect(channels);
|
|
1365
1470
|
state.currentChannelName = choice;
|
|
1366
1471
|
const result = await stepChannelConfig(state);
|
|
@@ -1690,6 +1795,33 @@ ${t("wizard.final.done")}
|
|
|
1690
1795
|
return { action: "next", next: "done" };
|
|
1691
1796
|
}
|
|
1692
1797
|
|
|
1798
|
+
// src/cli/wizard/steps/permission-mode.ts
|
|
1799
|
+
import pc10 from "picocolors";
|
|
1800
|
+
var STEP7 = "STEP 3";
|
|
1801
|
+
async function stepPermissionMode(state) {
|
|
1802
|
+
console.log(pc10.bold(`
|
|
1803
|
+
${STEP7} \u2014 Permission Mode
|
|
1804
|
+
`));
|
|
1805
|
+
console.log(pc10.gray(`${"\u2500".repeat(50)}
|
|
1806
|
+
`));
|
|
1807
|
+
const config = loadConfig();
|
|
1808
|
+
const current = state.pendingChanges?.agent?.claude;
|
|
1809
|
+
const saved = current?.permission_mode ?? config.agent.claude?.permission_mode ?? null;
|
|
1810
|
+
const detected = detectPermissionMode();
|
|
1811
|
+
const initial = saved ?? detected ?? "acceptEdits";
|
|
1812
|
+
console.log(pc10.gray("Controls how Claude Code handles permission requests.\n"));
|
|
1813
|
+
if (detected && !saved) {
|
|
1814
|
+
console.log(pc10.gray(`Detected from ~/.claude/settings.json: ${detected}
|
|
1815
|
+
`));
|
|
1816
|
+
}
|
|
1817
|
+
const selected = await prompts.permissionModeSelect(initial);
|
|
1818
|
+
state.pendingChanges ??= {};
|
|
1819
|
+
state.pendingChanges.agent ??= {};
|
|
1820
|
+
state.pendingChanges.agent.claude ??= {};
|
|
1821
|
+
state.pendingChanges.agent.claude.permission_mode = selected;
|
|
1822
|
+
return { action: "next", next: "channel-select" };
|
|
1823
|
+
}
|
|
1824
|
+
|
|
1693
1825
|
// src/cli/wizard/engine.ts
|
|
1694
1826
|
var WizardEngine = class {
|
|
1695
1827
|
state;
|
|
@@ -1750,6 +1882,8 @@ var WizardEngine = class {
|
|
|
1750
1882
|
return await stepPairContinue(this.state);
|
|
1751
1883
|
case "cli":
|
|
1752
1884
|
return await stepCli(this.state);
|
|
1885
|
+
case "permission-mode":
|
|
1886
|
+
return await stepPermissionMode(this.state);
|
|
1753
1887
|
case "cli-menu":
|
|
1754
1888
|
return await stepCliMenu(this.state);
|
|
1755
1889
|
case "channel-config":
|
|
@@ -2226,22 +2360,29 @@ function writeSettings2(settingsPath, settings) {
|
|
|
2226
2360
|
}
|
|
2227
2361
|
function generateHooksConfig3(port, token) {
|
|
2228
2362
|
const unifiedUrl = `http://localhost:${port}/hook/${token}/event`;
|
|
2229
|
-
const
|
|
2363
|
+
const createHttpHookEntry = () => ({
|
|
2230
2364
|
matcher: "",
|
|
2231
2365
|
hooks: [{ type: "http", url: unifiedUrl }]
|
|
2232
2366
|
});
|
|
2367
|
+
const curlCommand = `curl -s -X POST -H 'Content-Type: application/json' -d @- ${unifiedUrl}`;
|
|
2368
|
+
const createCommandHookEntry = () => ({
|
|
2369
|
+
matcher: "",
|
|
2370
|
+
hooks: [{ type: "command", command: curlCommand }]
|
|
2371
|
+
});
|
|
2233
2372
|
return {
|
|
2234
|
-
|
|
2235
|
-
|
|
2236
|
-
|
|
2237
|
-
|
|
2238
|
-
|
|
2239
|
-
|
|
2240
|
-
|
|
2241
|
-
|
|
2242
|
-
SessionEnd: [
|
|
2243
|
-
UserPromptSubmit: [
|
|
2244
|
-
PermissionRequest: [
|
|
2373
|
+
// HTTP type hooks
|
|
2374
|
+
Stop: [createHttpHookEntry()],
|
|
2375
|
+
PostToolUse: [createHttpHookEntry()],
|
|
2376
|
+
PostToolUseFailure: [createHttpHookEntry()],
|
|
2377
|
+
Notification: [createHttpHookEntry()],
|
|
2378
|
+
PreToolUse: [createHttpHookEntry()],
|
|
2379
|
+
SubagentStart: [createHttpHookEntry()],
|
|
2380
|
+
SubagentStop: [createHttpHookEntry()],
|
|
2381
|
+
SessionEnd: [createHttpHookEntry()],
|
|
2382
|
+
UserPromptSubmit: [createHttpHookEntry()],
|
|
2383
|
+
PermissionRequest: [createHttpHookEntry()],
|
|
2384
|
+
// Command type hooks (HTTP not supported by Claude)
|
|
2385
|
+
SessionStart: [createCommandHookEntry()]
|
|
2245
2386
|
};
|
|
2246
2387
|
}
|
|
2247
2388
|
function registerClaudeCommand(program2) {
|
|
@@ -2309,7 +2450,7 @@ function registerClaudeCommand(program2) {
|
|
|
2309
2450
|
import { spawn as spawn4 } from "child_process";
|
|
2310
2451
|
import { homedir as homedir14 } from "os";
|
|
2311
2452
|
import { join as join16 } from "path";
|
|
2312
|
-
import { execSync as
|
|
2453
|
+
import { execSync as execSync4 } from "child_process";
|
|
2313
2454
|
|
|
2314
2455
|
// src/shared/logger.ts
|
|
2315
2456
|
import pino from "pino";
|
|
@@ -2387,7 +2528,7 @@ function createAgentLogger(agentType) {
|
|
|
2387
2528
|
// src/cli/agent/codex.ts
|
|
2388
2529
|
function isCodexInstalled() {
|
|
2389
2530
|
try {
|
|
2390
|
-
|
|
2531
|
+
execSync4("codex --version", { encoding: "utf8", stdio: "pipe", windowsHide: true });
|
|
2391
2532
|
return true;
|
|
2392
2533
|
} catch {
|
|
2393
2534
|
return false;
|
|
@@ -2395,7 +2536,7 @@ function isCodexInstalled() {
|
|
|
2395
2536
|
}
|
|
2396
2537
|
function getCodexVersion() {
|
|
2397
2538
|
try {
|
|
2398
|
-
return
|
|
2539
|
+
return execSync4("codex --version", { encoding: "utf8", stdio: "pipe", windowsHide: true }).trim();
|
|
2399
2540
|
} catch {
|
|
2400
2541
|
return void 0;
|
|
2401
2542
|
}
|