@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.
Files changed (128) hide show
  1. package/CHANGELOG.md +21 -0
  2. package/dist/cli/args.d.ts +1 -0
  3. package/dist/cli/args.d.ts.map +1 -1
  4. package/dist/cli/args.js +5 -0
  5. package/dist/cli/args.js.map +1 -1
  6. package/dist/config.d.ts +2 -6
  7. package/dist/config.d.ts.map +1 -1
  8. package/dist/config.js +5 -9
  9. package/dist/config.js.map +1 -1
  10. package/dist/core/agent-frontmatter.d.ts +3 -0
  11. package/dist/core/agent-frontmatter.d.ts.map +1 -1
  12. package/dist/core/agent-frontmatter.js +41 -1
  13. package/dist/core/agent-frontmatter.js.map +1 -1
  14. package/dist/core/agent-manifest-paths.d.ts +17 -0
  15. package/dist/core/agent-manifest-paths.d.ts.map +1 -0
  16. package/dist/core/agent-manifest-paths.js +27 -0
  17. package/dist/core/agent-manifest-paths.js.map +1 -0
  18. package/dist/core/agent-registry.d.ts +14 -7
  19. package/dist/core/agent-registry.d.ts.map +1 -1
  20. package/dist/core/agent-registry.js +114 -8
  21. package/dist/core/agent-registry.js.map +1 -1
  22. package/dist/core/agent-session.d.ts.map +1 -1
  23. package/dist/core/agent-session.js +23 -0
  24. package/dist/core/agent-session.js.map +1 -1
  25. package/dist/core/extensions/index.d.ts +1 -1
  26. package/dist/core/extensions/index.d.ts.map +1 -1
  27. package/dist/core/extensions/index.js.map +1 -1
  28. package/dist/core/extensions/runner.d.ts.map +1 -1
  29. package/dist/core/extensions/runner.js +1 -0
  30. package/dist/core/extensions/runner.js.map +1 -1
  31. package/dist/core/extensions/types.d.ts +26 -0
  32. package/dist/core/extensions/types.d.ts.map +1 -1
  33. package/dist/core/extensions/types.js.map +1 -1
  34. package/dist/core/keybindings.d.ts +8 -0
  35. package/dist/core/keybindings.d.ts.map +1 -1
  36. package/dist/core/keybindings.js +2 -0
  37. package/dist/core/keybindings.js.map +1 -1
  38. package/dist/core/package-manager.d.ts +2 -1
  39. package/dist/core/package-manager.d.ts.map +1 -1
  40. package/dist/core/package-manager.js +38 -9
  41. package/dist/core/package-manager.js.map +1 -1
  42. package/dist/core/provider-health.d.ts +36 -0
  43. package/dist/core/provider-health.d.ts.map +1 -0
  44. package/dist/core/provider-health.js +54 -0
  45. package/dist/core/provider-health.js.map +1 -0
  46. package/dist/core/resource-loader.d.ts +14 -0
  47. package/dist/core/resource-loader.d.ts.map +1 -1
  48. package/dist/core/resource-loader.js +12 -0
  49. package/dist/core/resource-loader.js.map +1 -1
  50. package/dist/core/sdk.d.ts +2 -0
  51. package/dist/core/sdk.d.ts.map +1 -1
  52. package/dist/core/sdk.js +1 -1
  53. package/dist/core/sdk.js.map +1 -1
  54. package/dist/core/skills.d.ts +9 -0
  55. package/dist/core/skills.d.ts.map +1 -1
  56. package/dist/core/skills.js +32 -1
  57. package/dist/core/skills.js.map +1 -1
  58. package/dist/core/source-info.d.ts +1 -1
  59. package/dist/core/source-info.d.ts.map +1 -1
  60. package/dist/core/source-info.js.map +1 -1
  61. package/dist/core/subagent-pool-instance.d.ts +7 -0
  62. package/dist/core/subagent-pool-instance.d.ts.map +1 -1
  63. package/dist/core/subagent-pool-instance.js +14 -1
  64. package/dist/core/subagent-pool-instance.js.map +1 -1
  65. package/dist/core/subagent-pool.d.ts +16 -0
  66. package/dist/core/subagent-pool.d.ts.map +1 -1
  67. package/dist/core/subagent-pool.js +42 -2
  68. package/dist/core/subagent-pool.js.map +1 -1
  69. package/dist/core/subagent-result.d.ts.map +1 -1
  70. package/dist/core/subagent-result.js +32 -2
  71. package/dist/core/subagent-result.js.map +1 -1
  72. package/dist/core/system-prompt.d.ts +7 -0
  73. package/dist/core/system-prompt.d.ts.map +1 -1
  74. package/dist/core/system-prompt.js +15 -3
  75. package/dist/core/system-prompt.js.map +1 -1
  76. package/dist/core/tools/bash.d.ts +10 -0
  77. package/dist/core/tools/bash.d.ts.map +1 -1
  78. package/dist/core/tools/bash.js +34 -0
  79. package/dist/core/tools/bash.js.map +1 -1
  80. package/dist/core/tools/subagent.d.ts.map +1 -1
  81. package/dist/core/tools/subagent.js +26 -0
  82. package/dist/core/tools/subagent.js.map +1 -1
  83. package/dist/extensions/core/hoo-core.d.ts +10 -3
  84. package/dist/extensions/core/hoo-core.d.ts.map +1 -1
  85. package/dist/extensions/core/hoo-core.js +254 -13
  86. package/dist/extensions/core/hoo-core.js.map +1 -1
  87. package/dist/init-templates.generated.d.ts.map +1 -1
  88. package/dist/init-templates.generated.js +5 -4
  89. package/dist/init-templates.generated.js.map +1 -1
  90. package/dist/init.d.ts.map +1 -1
  91. package/dist/init.js +6 -2
  92. package/dist/init.js.map +1 -1
  93. package/dist/main.d.ts.map +1 -1
  94. package/dist/main.js +4 -0
  95. package/dist/main.js.map +1 -1
  96. package/dist/modes/interactive/components/ask-options.d.ts +44 -0
  97. package/dist/modes/interactive/components/ask-options.d.ts.map +1 -0
  98. package/dist/modes/interactive/components/ask-options.js +202 -0
  99. package/dist/modes/interactive/components/ask-options.js.map +1 -0
  100. package/dist/modes/interactive/components/config-selector.d.ts +1 -1
  101. package/dist/modes/interactive/components/config-selector.d.ts.map +1 -1
  102. package/dist/modes/interactive/components/config-selector.js.map +1 -1
  103. package/dist/modes/interactive/components/index.d.ts +1 -0
  104. package/dist/modes/interactive/components/index.d.ts.map +1 -1
  105. package/dist/modes/interactive/components/index.js +1 -0
  106. package/dist/modes/interactive/components/index.js.map +1 -1
  107. package/dist/modes/interactive/components/task-panel.d.ts +15 -4
  108. package/dist/modes/interactive/components/task-panel.d.ts.map +1 -1
  109. package/dist/modes/interactive/components/task-panel.js +178 -63
  110. package/dist/modes/interactive/components/task-panel.js.map +1 -1
  111. package/dist/modes/interactive/interactive-mode.d.ts +10 -0
  112. package/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
  113. package/dist/modes/interactive/interactive-mode.js +50 -1
  114. package/dist/modes/interactive/interactive-mode.js.map +1 -1
  115. package/dist/modes/rpc/rpc-mode.d.ts.map +1 -1
  116. package/dist/modes/rpc/rpc-mode.js +26 -0
  117. package/dist/modes/rpc/rpc-mode.js.map +1 -1
  118. package/examples/extensions/custom-provider-anthropic/package.json +1 -1
  119. package/examples/extensions/custom-provider-gitlab-duo/package.json +1 -1
  120. package/examples/extensions/sandbox/package.json +1 -1
  121. package/examples/extensions/with-deps/package.json +1 -1
  122. package/examples/sdk/12-full-control.ts +2 -0
  123. package/package.json +4 -4
  124. package/templates/agents/doc.md +1 -1
  125. package/templates/agents/edit.md +1 -0
  126. package/templates/agents/explore.md +3 -3
  127. package/templates/agents/general-purpose.md +37 -0
  128. 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/agent/hoo-config.json (global defaults)
15
- * 2. ./.hoocode/config.json (project overrides — scalars win; arrays union)
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, "agent", "hoo-config.json");
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
- const dir = join(HOOCODE_DIR, "agent");
81
- if (!existsSync(dir))
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
- if (!GATED_TOOLS.has(event.toolName) || !ctx.hasUI)
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/agent/hoo-config.json)
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;