@kolisachint/hoocode-agent 0.4.21 → 0.4.24
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/CHANGELOG.md +21 -0
- package/dist/cli/args.d.ts +1 -0
- package/dist/cli/args.d.ts.map +1 -1
- package/dist/cli/args.js +5 -0
- package/dist/cli/args.js.map +1 -1
- package/dist/config.d.ts +2 -6
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +5 -9
- package/dist/config.js.map +1 -1
- package/dist/core/agent-frontmatter.d.ts +3 -0
- package/dist/core/agent-frontmatter.d.ts.map +1 -1
- package/dist/core/agent-frontmatter.js +41 -1
- package/dist/core/agent-frontmatter.js.map +1 -1
- package/dist/core/agent-manifest-paths.d.ts +17 -0
- package/dist/core/agent-manifest-paths.d.ts.map +1 -0
- package/dist/core/agent-manifest-paths.js +27 -0
- package/dist/core/agent-manifest-paths.js.map +1 -0
- package/dist/core/agent-registry.d.ts +14 -7
- package/dist/core/agent-registry.d.ts.map +1 -1
- package/dist/core/agent-registry.js +114 -8
- package/dist/core/agent-registry.js.map +1 -1
- package/dist/core/agent-session.d.ts.map +1 -1
- package/dist/core/agent-session.js +23 -0
- package/dist/core/agent-session.js.map +1 -1
- package/dist/core/extensions/index.d.ts +1 -1
- package/dist/core/extensions/index.d.ts.map +1 -1
- package/dist/core/extensions/index.js.map +1 -1
- package/dist/core/extensions/runner.d.ts.map +1 -1
- package/dist/core/extensions/runner.js +1 -0
- package/dist/core/extensions/runner.js.map +1 -1
- package/dist/core/extensions/types.d.ts +26 -0
- package/dist/core/extensions/types.d.ts.map +1 -1
- package/dist/core/extensions/types.js.map +1 -1
- package/dist/core/keybindings.d.ts +8 -0
- package/dist/core/keybindings.d.ts.map +1 -1
- package/dist/core/keybindings.js +2 -0
- package/dist/core/keybindings.js.map +1 -1
- package/dist/core/package-manager.d.ts +2 -1
- package/dist/core/package-manager.d.ts.map +1 -1
- package/dist/core/package-manager.js +38 -9
- package/dist/core/package-manager.js.map +1 -1
- package/dist/core/provider-health.d.ts +36 -0
- package/dist/core/provider-health.d.ts.map +1 -0
- package/dist/core/provider-health.js +54 -0
- package/dist/core/provider-health.js.map +1 -0
- package/dist/core/resource-loader.d.ts +14 -0
- package/dist/core/resource-loader.d.ts.map +1 -1
- package/dist/core/resource-loader.js +12 -0
- package/dist/core/resource-loader.js.map +1 -1
- package/dist/core/sdk.d.ts +2 -0
- package/dist/core/sdk.d.ts.map +1 -1
- package/dist/core/sdk.js +1 -1
- package/dist/core/sdk.js.map +1 -1
- package/dist/core/skills.d.ts +9 -0
- package/dist/core/skills.d.ts.map +1 -1
- package/dist/core/skills.js +32 -1
- package/dist/core/skills.js.map +1 -1
- package/dist/core/source-info.d.ts +1 -1
- package/dist/core/source-info.d.ts.map +1 -1
- package/dist/core/source-info.js.map +1 -1
- package/dist/core/subagent-pool-instance.d.ts +7 -0
- package/dist/core/subagent-pool-instance.d.ts.map +1 -1
- package/dist/core/subagent-pool-instance.js +14 -1
- package/dist/core/subagent-pool-instance.js.map +1 -1
- package/dist/core/subagent-pool.d.ts +16 -0
- package/dist/core/subagent-pool.d.ts.map +1 -1
- package/dist/core/subagent-pool.js +42 -2
- package/dist/core/subagent-pool.js.map +1 -1
- package/dist/core/subagent-result.d.ts.map +1 -1
- package/dist/core/subagent-result.js +32 -2
- package/dist/core/subagent-result.js.map +1 -1
- package/dist/core/system-prompt.d.ts +7 -0
- package/dist/core/system-prompt.d.ts.map +1 -1
- package/dist/core/system-prompt.js +15 -3
- package/dist/core/system-prompt.js.map +1 -1
- package/dist/core/tools/bash.d.ts +10 -0
- package/dist/core/tools/bash.d.ts.map +1 -1
- package/dist/core/tools/bash.js +34 -0
- package/dist/core/tools/bash.js.map +1 -1
- package/dist/core/tools/subagent.d.ts.map +1 -1
- package/dist/core/tools/subagent.js +26 -0
- package/dist/core/tools/subagent.js.map +1 -1
- package/dist/extensions/core/hoo-core.d.ts +10 -3
- package/dist/extensions/core/hoo-core.d.ts.map +1 -1
- package/dist/extensions/core/hoo-core.js +254 -13
- package/dist/extensions/core/hoo-core.js.map +1 -1
- package/dist/init-templates.generated.d.ts.map +1 -1
- package/dist/init-templates.generated.js +5 -4
- package/dist/init-templates.generated.js.map +1 -1
- package/dist/init.d.ts.map +1 -1
- package/dist/init.js +6 -2
- package/dist/init.js.map +1 -1
- package/dist/main.d.ts.map +1 -1
- package/dist/main.js +4 -0
- package/dist/main.js.map +1 -1
- package/dist/modes/interactive/components/ask-options.d.ts +44 -0
- package/dist/modes/interactive/components/ask-options.d.ts.map +1 -0
- package/dist/modes/interactive/components/ask-options.js +202 -0
- package/dist/modes/interactive/components/ask-options.js.map +1 -0
- package/dist/modes/interactive/components/config-selector.d.ts +1 -1
- package/dist/modes/interactive/components/config-selector.d.ts.map +1 -1
- package/dist/modes/interactive/components/config-selector.js.map +1 -1
- package/dist/modes/interactive/components/index.d.ts +1 -0
- package/dist/modes/interactive/components/index.d.ts.map +1 -1
- package/dist/modes/interactive/components/index.js +1 -0
- package/dist/modes/interactive/components/index.js.map +1 -1
- package/dist/modes/interactive/components/task-panel.d.ts +15 -4
- package/dist/modes/interactive/components/task-panel.d.ts.map +1 -1
- package/dist/modes/interactive/components/task-panel.js +178 -63
- package/dist/modes/interactive/components/task-panel.js.map +1 -1
- package/dist/modes/interactive/interactive-mode.d.ts +10 -0
- package/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
- package/dist/modes/interactive/interactive-mode.js +50 -1
- package/dist/modes/interactive/interactive-mode.js.map +1 -1
- package/dist/modes/rpc/rpc-mode.d.ts.map +1 -1
- package/dist/modes/rpc/rpc-mode.js +26 -0
- package/dist/modes/rpc/rpc-mode.js.map +1 -1
- package/examples/extensions/custom-provider-anthropic/package.json +1 -1
- package/examples/extensions/custom-provider-gitlab-duo/package.json +1 -1
- package/examples/extensions/sandbox/package.json +1 -1
- package/examples/extensions/with-deps/package.json +1 -1
- package/examples/sdk/12-full-control.ts +2 -0
- package/package.json +4 -4
- package/templates/agents/doc.md +1 -1
- package/templates/agents/edit.md +1 -0
- package/templates/agents/explore.md +3 -3
- package/templates/agents/general-purpose.md +37 -0
- package/templates/agents/review.md +2 -2
|
@@ -11,8 +11,8 @@
|
|
|
11
11
|
* and /approve commands
|
|
12
12
|
*
|
|
13
13
|
* Config merge order (lowest → highest priority):
|
|
14
|
-
* 1. ~/.hoocode/
|
|
15
|
-
* 2. ./.hoocode/config.json
|
|
14
|
+
* 1. ~/.hoocode/hoo-config.json (global defaults)
|
|
15
|
+
* 2. ./.hoocode/hoo-config.json (project overrides — scalars win; arrays union)
|
|
16
16
|
*/
|
|
17
17
|
import { spawn } from "node:child_process";
|
|
18
18
|
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
@@ -53,7 +53,7 @@ To fix, switch to /mode build.`,
|
|
|
53
53
|
// Shared paths
|
|
54
54
|
// ============================================================================
|
|
55
55
|
const HOOCODE_DIR = getHooCodeDir();
|
|
56
|
-
const GLOBAL_CONFIG_PATH = join(HOOCODE_DIR, "
|
|
56
|
+
const GLOBAL_CONFIG_PATH = join(HOOCODE_DIR, "hoo-config.json");
|
|
57
57
|
/**
|
|
58
58
|
* Per-session plan file path. Keying on sessionId lets concurrent or resumed
|
|
59
59
|
* plan sessions keep distinct plans instead of clobbering each other.
|
|
@@ -77,9 +77,8 @@ function readConfig() {
|
|
|
77
77
|
}
|
|
78
78
|
}
|
|
79
79
|
function writeConfig(config) {
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
mkdirSync(dir, { recursive: true });
|
|
80
|
+
if (!existsSync(HOOCODE_DIR))
|
|
81
|
+
mkdirSync(HOOCODE_DIR, { recursive: true });
|
|
83
82
|
writeFileSync(GLOBAL_CONFIG_PATH, `${JSON.stringify(config, null, 2)}\n`, "utf8");
|
|
84
83
|
}
|
|
85
84
|
/**
|
|
@@ -109,6 +108,12 @@ export function mergeConfigs(global, project) {
|
|
|
109
108
|
allowed_write_paths: Array.from(new Set([...(globalCfg.allowed_write_paths ?? []), ...(projectCfg.allowed_write_paths ?? [])])),
|
|
110
109
|
// enabled_tools: project wins if set, else falls back to global
|
|
111
110
|
enabled_tools: projectCfg.enabled_tools ?? globalCfg.enabled_tools,
|
|
111
|
+
// denied_tools: union so project can add more denied tools on top of global
|
|
112
|
+
denied_tools: Array.from(new Set([...(globalCfg.denied_tools ?? []), ...(projectCfg.denied_tools ?? [])])),
|
|
113
|
+
// allowed_bash_commands: project wins if set, else falls back to global
|
|
114
|
+
allowed_bash_commands: projectCfg.allowed_bash_commands ?? globalCfg.allowed_bash_commands,
|
|
115
|
+
// denied_bash_commands: union so project can add more denied patterns on top of global
|
|
116
|
+
denied_bash_commands: Array.from(new Set([...(globalCfg.denied_bash_commands ?? []), ...(projectCfg.denied_bash_commands ?? [])])),
|
|
112
117
|
};
|
|
113
118
|
}
|
|
114
119
|
}
|
|
@@ -140,12 +145,12 @@ function mergeSearchPaths(...sources) {
|
|
|
140
145
|
}
|
|
141
146
|
/**
|
|
142
147
|
* Reads the global config and optionally overlays the project-local config at
|
|
143
|
-
* `./.hoocode/config.json`. Project values win on all scalar fields; arrays are
|
|
148
|
+
* `./.hoocode/hoo-config.json`. Project values win on all scalar fields; arrays are
|
|
144
149
|
* unioned (see mergeConfigs for full rules).
|
|
145
150
|
*/
|
|
146
151
|
export function readMergedConfig(cwd) {
|
|
147
152
|
const global = readConfig();
|
|
148
|
-
const projectPath = join(cwd, ".hoocode", "config.json");
|
|
153
|
+
const projectPath = join(cwd, ".hoocode", "hoo-config.json");
|
|
149
154
|
if (!existsSync(projectPath))
|
|
150
155
|
return global;
|
|
151
156
|
try {
|
|
@@ -180,6 +185,18 @@ function matchesAllowedPath(filePath, allowedPatterns) {
|
|
|
180
185
|
}
|
|
181
186
|
return false;
|
|
182
187
|
}
|
|
188
|
+
/**
|
|
189
|
+
* Tests a bash command string against a regex pattern string.
|
|
190
|
+
* Returns false (no match) if the pattern is an invalid regex.
|
|
191
|
+
*/
|
|
192
|
+
function matchesBashPattern(pattern, command) {
|
|
193
|
+
try {
|
|
194
|
+
return new RegExp(pattern).test(command);
|
|
195
|
+
}
|
|
196
|
+
catch {
|
|
197
|
+
return false;
|
|
198
|
+
}
|
|
199
|
+
}
|
|
183
200
|
function describeTool(event) {
|
|
184
201
|
if (isToolCallEventType("bash", event)) {
|
|
185
202
|
return `$ ${event.input.command.replace(/\s+/g, " ").slice(0, 100)}`;
|
|
@@ -196,12 +213,57 @@ function describeTool(event) {
|
|
|
196
213
|
}
|
|
197
214
|
export function setupPermissionGate(pi) {
|
|
198
215
|
pi.on("tool_call", async (event, ctx) => {
|
|
199
|
-
|
|
200
|
-
return;
|
|
201
|
-
// Use the merged config so project-local auto_allow entries are respected
|
|
216
|
+
// Use the merged config so project-local entries are respected
|
|
202
217
|
const config = readMergedConfig(ctx.cwd);
|
|
203
218
|
const mode = config.active_mode ?? "build";
|
|
204
219
|
const modeCfg = config.modes?.[mode];
|
|
220
|
+
// ── Hard enforcement (always applies, regardless of UI) ───────────────────
|
|
221
|
+
// Explicitly denied tools are blocked unconditionally
|
|
222
|
+
if (modeCfg?.denied_tools?.includes(event.toolName)) {
|
|
223
|
+
return {
|
|
224
|
+
block: true,
|
|
225
|
+
reason: `Tool "${event.toolName}" is denied in mode "${mode}".`,
|
|
226
|
+
};
|
|
227
|
+
}
|
|
228
|
+
// enabled_tools acts as a strict allowlist: only listed tools may execute
|
|
229
|
+
if (modeCfg?.enabled_tools &&
|
|
230
|
+
modeCfg.enabled_tools.length > 0 &&
|
|
231
|
+
!modeCfg.enabled_tools.includes(event.toolName)) {
|
|
232
|
+
return {
|
|
233
|
+
block: true,
|
|
234
|
+
reason: `Tool "${event.toolName}" is not enabled in mode "${mode}" ` +
|
|
235
|
+
`(enabled: ${modeCfg.enabled_tools.join(", ")}).`,
|
|
236
|
+
};
|
|
237
|
+
}
|
|
238
|
+
// Bash command-level filtering
|
|
239
|
+
if (isToolCallEventType("bash", event)) {
|
|
240
|
+
const command = event.input.command ?? "";
|
|
241
|
+
// denied_bash_commands: block if any pattern matches
|
|
242
|
+
if (modeCfg?.denied_bash_commands?.length) {
|
|
243
|
+
for (const pattern of modeCfg.denied_bash_commands) {
|
|
244
|
+
if (matchesBashPattern(pattern, command)) {
|
|
245
|
+
return {
|
|
246
|
+
block: true,
|
|
247
|
+
reason: `Bash command matches a denied pattern in mode "${mode}": ${pattern}`,
|
|
248
|
+
};
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
// allowed_bash_commands: block unless at least one pattern matches
|
|
253
|
+
if (modeCfg?.allowed_bash_commands?.length) {
|
|
254
|
+
const permitted = modeCfg.allowed_bash_commands.some((p) => matchesBashPattern(p, command));
|
|
255
|
+
if (!permitted) {
|
|
256
|
+
return {
|
|
257
|
+
block: true,
|
|
258
|
+
reason: `Bash command is not permitted in mode "${mode}". ` +
|
|
259
|
+
`Allowed patterns: ${modeCfg.allowed_bash_commands.join(", ")}`,
|
|
260
|
+
};
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
// ── UI-based permission prompting (interactive sessions only) ─────────────
|
|
265
|
+
if (!GATED_TOOLS.has(event.toolName) || !ctx.hasUI)
|
|
266
|
+
return;
|
|
205
267
|
const autoAllow = modeCfg?.auto_allow ?? [];
|
|
206
268
|
// Check allowed_write_paths for write/edit operations
|
|
207
269
|
if ((event.toolName === "write" || event.toolName === "edit") && modeCfg?.allowed_write_paths) {
|
|
@@ -521,8 +583,8 @@ export function setupMode(pi) {
|
|
|
521
583
|
let cachedPlanPath;
|
|
522
584
|
// ── session_start ─────────────────────────────────────────────────────────
|
|
523
585
|
// Config resolution order:
|
|
524
|
-
// 1. Read global config (~/.hoocode/
|
|
525
|
-
// 2. Read project config (./.hoocode/config.json) if present
|
|
586
|
+
// 1. Read global config (~/.hoocode/hoo-config.json)
|
|
587
|
+
// 2. Read project config (./.hoocode/hoo-config.json) if present
|
|
526
588
|
// 3. Merge — project scalars win; arrays are unioned
|
|
527
589
|
// 4. Re-resolve active_mode from the merged result
|
|
528
590
|
pi.on("session_start", (_event, ctx) => {
|
|
@@ -700,12 +762,191 @@ export function setupMode(pi) {
|
|
|
700
762
|
});
|
|
701
763
|
}
|
|
702
764
|
// ============================================================================
|
|
765
|
+
// Scaffold commands — /new-skill and /new-agent
|
|
766
|
+
// ============================================================================
|
|
767
|
+
/** Validates a resource name: lowercase a-z, 0-9, hyphens, no leading/trailing/double hyphens. */
|
|
768
|
+
function validateResourceName(name) {
|
|
769
|
+
if (!name)
|
|
770
|
+
return "name is required";
|
|
771
|
+
if (!/^[a-z0-9-]+$/.test(name))
|
|
772
|
+
return "name must be lowercase a-z, 0-9, and hyphens only";
|
|
773
|
+
if (name.startsWith("-") || name.endsWith("-"))
|
|
774
|
+
return "name must not start or end with a hyphen";
|
|
775
|
+
if (name.includes("--"))
|
|
776
|
+
return "name must not contain consecutive hyphens";
|
|
777
|
+
return null;
|
|
778
|
+
}
|
|
779
|
+
function setupScaffold(pi) {
|
|
780
|
+
// ── /new-skill <name> ─────────────────────────────────────────────────────
|
|
781
|
+
// Creates .hoocode/skills/<name>/SKILL.md with a valid Agent Skills frontmatter
|
|
782
|
+
// template so the file is ready to edit and will be picked up on next reload.
|
|
783
|
+
pi.registerCommand("new-skill", {
|
|
784
|
+
description: "Scaffold a new skill. Usage: /new-skill <name>",
|
|
785
|
+
getArgumentCompletions: () => [],
|
|
786
|
+
handler: async (args, ctx) => {
|
|
787
|
+
const name = args.trim();
|
|
788
|
+
const error = validateResourceName(name);
|
|
789
|
+
if (error) {
|
|
790
|
+
ctx.ui.notify(`/new-skill: ${error}. Usage: /new-skill <name>`, "warning");
|
|
791
|
+
return;
|
|
792
|
+
}
|
|
793
|
+
const skillDir = join(ctx.cwd, ".hoocode", "skills", name);
|
|
794
|
+
const skillFile = join(skillDir, "SKILL.md");
|
|
795
|
+
if (existsSync(skillFile)) {
|
|
796
|
+
ctx.ui.notify(`/new-skill: ${skillFile} already exists`, "warning");
|
|
797
|
+
return;
|
|
798
|
+
}
|
|
799
|
+
mkdirSync(skillDir, { recursive: true });
|
|
800
|
+
writeFileSync(skillFile, [
|
|
801
|
+
"---",
|
|
802
|
+
`name: ${name}`,
|
|
803
|
+
"description: |",
|
|
804
|
+
" TODO: describe when to use this skill — one clear sentence per bullet.",
|
|
805
|
+
" The model reads this to decide whether to load the skill.",
|
|
806
|
+
"allowed-tools: read, bash",
|
|
807
|
+
"---",
|
|
808
|
+
"",
|
|
809
|
+
`# ${name}`,
|
|
810
|
+
"",
|
|
811
|
+
"TODO: write the skill instructions here.",
|
|
812
|
+
"",
|
|
813
|
+
"When relative paths appear below, they are resolved from this file's directory.",
|
|
814
|
+
"",
|
|
815
|
+
].join("\n"), "utf8");
|
|
816
|
+
ctx.ui.notify(`Skill created: ${join(".hoocode", "skills", name, "SKILL.md")}\nEdit the file, then run /reload to activate it.`, "info");
|
|
817
|
+
},
|
|
818
|
+
});
|
|
819
|
+
// ── /new-agent <name> ─────────────────────────────────────────────────────
|
|
820
|
+
// Creates .hoocode/agents/<name>.md following the Claude Code subagent standard
|
|
821
|
+
// (name, description, tools comma-string, model alias, optional background).
|
|
822
|
+
pi.registerCommand("new-agent", {
|
|
823
|
+
description: "Scaffold a new subagent. Usage: /new-agent <name>",
|
|
824
|
+
getArgumentCompletions: () => [],
|
|
825
|
+
handler: async (args, ctx) => {
|
|
826
|
+
const name = args.trim();
|
|
827
|
+
const error = validateResourceName(name);
|
|
828
|
+
if (error) {
|
|
829
|
+
ctx.ui.notify(`/new-agent: ${error}. Usage: /new-agent <name>`, "warning");
|
|
830
|
+
return;
|
|
831
|
+
}
|
|
832
|
+
const agentsDir = join(ctx.cwd, ".hoocode", "agents");
|
|
833
|
+
const agentFile = join(agentsDir, `${name}.md`);
|
|
834
|
+
if (existsSync(agentFile)) {
|
|
835
|
+
ctx.ui.notify(`/new-agent: ${agentFile} already exists`, "warning");
|
|
836
|
+
return;
|
|
837
|
+
}
|
|
838
|
+
mkdirSync(agentsDir, { recursive: true });
|
|
839
|
+
writeFileSync(agentFile, [
|
|
840
|
+
"---",
|
|
841
|
+
`name: ${name}`,
|
|
842
|
+
"description: |",
|
|
843
|
+
" Use this subagent ONLY when:",
|
|
844
|
+
" - TODO: describe the task(s) to delegate here",
|
|
845
|
+
"",
|
|
846
|
+
" DO NOT use for:",
|
|
847
|
+
" - TODO: describe what this agent should NOT handle",
|
|
848
|
+
"tools: read, bash",
|
|
849
|
+
"model: sonnet",
|
|
850
|
+
"---",
|
|
851
|
+
`You are a ${name} subagent running inside hoocode.`,
|
|
852
|
+
"You run in an isolated context and cannot see the parent conversation.",
|
|
853
|
+
"",
|
|
854
|
+
"TODO: write the system prompt here.",
|
|
855
|
+
"",
|
|
856
|
+
"Your final message must contain ONLY your answer — it is the only output",
|
|
857
|
+
"the caller receives. Do not include intermediate reasoning or tool logs.",
|
|
858
|
+
"",
|
|
859
|
+
].join("\n"), "utf8");
|
|
860
|
+
ctx.ui.notify(`Agent created: ${join(".hoocode", "agents", `${name}.md`)}\nEdit the file, then run /reload to activate it.`, "info");
|
|
861
|
+
},
|
|
862
|
+
});
|
|
863
|
+
}
|
|
864
|
+
// ============================================================================
|
|
865
|
+
// D. Options pane — ask_options tool
|
|
866
|
+
// ============================================================================
|
|
867
|
+
// The model calls this tool when it needs the user to make a decision before
|
|
868
|
+
// continuing. Each question is shown in an inline options pane where the user
|
|
869
|
+
// moves with up/down, advances with right, and may type a custom answer.
|
|
870
|
+
const askOptionsSchema = Type.Object({
|
|
871
|
+
questions: Type.Array(Type.Object({
|
|
872
|
+
question: Type.String({ description: "The question to ask the user." }),
|
|
873
|
+
detail: Type.Optional(Type.String({ description: "Optional clarifying sub-text shown under the question." })),
|
|
874
|
+
options: Type.Array(Type.Object({
|
|
875
|
+
label: Type.String({ description: "The option text; returned verbatim when chosen." }),
|
|
876
|
+
description: Type.Optional(Type.String({ description: "Optional short description shown next to the option." })),
|
|
877
|
+
}), { description: "The options the user can choose from." }),
|
|
878
|
+
allow_custom: Type.Optional(Type.Boolean({
|
|
879
|
+
description: "When true, the user can type a free-form answer instead of choosing an option.",
|
|
880
|
+
})),
|
|
881
|
+
}), { description: "One or more decisions to ask the user, in order." }),
|
|
882
|
+
});
|
|
883
|
+
export function setupAskOptions(pi) {
|
|
884
|
+
// Capture the latest context so the tool can reach the interactive UI.
|
|
885
|
+
let activeCtx;
|
|
886
|
+
pi.on("session_start", (_event, ctx) => {
|
|
887
|
+
activeCtx = ctx;
|
|
888
|
+
});
|
|
889
|
+
pi.registerTool({
|
|
890
|
+
name: "ask_options",
|
|
891
|
+
label: "Ask the user",
|
|
892
|
+
description: "Ask the user to make one or more decisions before continuing. Each question is presented " +
|
|
893
|
+
"in an interactive options pane where the user selects an option (or types a custom answer). " +
|
|
894
|
+
"Use this when you genuinely need input to proceed and cannot reasonably decide yourself. " +
|
|
895
|
+
"Returns the user's answer for each question; if the user skips, no answers are returned.",
|
|
896
|
+
parameters: askOptionsSchema,
|
|
897
|
+
async execute(_toolCallId, params, signal, _onUpdate) {
|
|
898
|
+
if (!activeCtx || !activeCtx.hasUI) {
|
|
899
|
+
return {
|
|
900
|
+
content: [
|
|
901
|
+
{
|
|
902
|
+
type: "text",
|
|
903
|
+
text: "Cannot ask the user: no interactive UI is available in this session. Proceed using your best judgement.",
|
|
904
|
+
},
|
|
905
|
+
],
|
|
906
|
+
details: undefined,
|
|
907
|
+
};
|
|
908
|
+
}
|
|
909
|
+
if (!params.questions.length) {
|
|
910
|
+
return {
|
|
911
|
+
content: [{ type: "text", text: "No questions were provided." }],
|
|
912
|
+
details: undefined,
|
|
913
|
+
};
|
|
914
|
+
}
|
|
915
|
+
const questions = params.questions.map((q) => ({
|
|
916
|
+
question: q.question,
|
|
917
|
+
detail: q.detail,
|
|
918
|
+
options: q.options.map((o) => ({ label: o.label, description: o.description })),
|
|
919
|
+
allowCustom: q.allow_custom,
|
|
920
|
+
}));
|
|
921
|
+
const answers = await activeCtx.ui.askOptions(questions, { signal });
|
|
922
|
+
if (!answers) {
|
|
923
|
+
return {
|
|
924
|
+
content: [
|
|
925
|
+
{
|
|
926
|
+
type: "text",
|
|
927
|
+
text: "The user skipped the question(s) without answering. Ask how they would like to proceed.",
|
|
928
|
+
},
|
|
929
|
+
],
|
|
930
|
+
details: undefined,
|
|
931
|
+
};
|
|
932
|
+
}
|
|
933
|
+
const text = questions.map((q, i) => `${q.question}\n \u2192 ${answers[i] ?? "(no answer)"}`).join("\n\n");
|
|
934
|
+
return {
|
|
935
|
+
content: [{ type: "text", text }],
|
|
936
|
+
details: undefined,
|
|
937
|
+
};
|
|
938
|
+
},
|
|
939
|
+
});
|
|
940
|
+
}
|
|
941
|
+
// ============================================================================
|
|
703
942
|
// Extension entry point
|
|
704
943
|
// ============================================================================
|
|
705
944
|
function hooCore(pi) {
|
|
706
945
|
setupPermissionGate(pi);
|
|
707
946
|
setupMcpLoader(pi);
|
|
708
947
|
setupMode(pi);
|
|
948
|
+
setupScaffold(pi);
|
|
949
|
+
setupAskOptions(pi);
|
|
709
950
|
}
|
|
710
951
|
hooCore.displayName = "hoo-core";
|
|
711
952
|
export default hooCore;
|