yzcode-cli 1.0.1 → 1.0.3

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 (117) hide show
  1. package/assistant/sessionHistory.ts +87 -0
  2. package/bootstrap/state.ts +1769 -0
  3. package/bridge/bridgeApi.ts +539 -0
  4. package/bridge/bridgeConfig.ts +48 -0
  5. package/bridge/bridgeDebug.ts +135 -0
  6. package/bridge/bridgeEnabled.ts +202 -0
  7. package/bridge/bridgeMain.ts +2999 -0
  8. package/bridge/bridgeMessaging.ts +461 -0
  9. package/bridge/bridgePermissionCallbacks.ts +43 -0
  10. package/bridge/bridgePointer.ts +210 -0
  11. package/bridge/bridgeStatusUtil.ts +163 -0
  12. package/bridge/bridgeUI.ts +530 -0
  13. package/bridge/capacityWake.ts +56 -0
  14. package/bridge/codeSessionApi.ts +168 -0
  15. package/bridge/createSession.ts +384 -0
  16. package/bridge/debugUtils.ts +141 -0
  17. package/bridge/envLessBridgeConfig.ts +165 -0
  18. package/bridge/flushGate.ts +71 -0
  19. package/bridge/inboundAttachments.ts +175 -0
  20. package/bridge/inboundMessages.ts +80 -0
  21. package/bridge/initReplBridge.ts +569 -0
  22. package/bridge/jwtUtils.ts +256 -0
  23. package/bridge/pollConfig.ts +110 -0
  24. package/bridge/pollConfigDefaults.ts +82 -0
  25. package/bridge/remoteBridgeCore.ts +1008 -0
  26. package/bridge/replBridge.ts +2406 -0
  27. package/bridge/replBridgeHandle.ts +36 -0
  28. package/bridge/replBridgeTransport.ts +370 -0
  29. package/bridge/sessionIdCompat.ts +57 -0
  30. package/bridge/sessionRunner.ts +550 -0
  31. package/bridge/trustedDevice.ts +210 -0
  32. package/bridge/types.ts +262 -0
  33. package/bridge/workSecret.ts +127 -0
  34. package/buddy/CompanionSprite.tsx +371 -0
  35. package/buddy/companion.ts +133 -0
  36. package/buddy/prompt.ts +36 -0
  37. package/buddy/sprites.ts +514 -0
  38. package/buddy/types.ts +148 -0
  39. package/buddy/useBuddyNotification.tsx +98 -0
  40. package/coordinator/coordinatorMode.ts +369 -0
  41. package/memdir/findRelevantMemories.ts +141 -0
  42. package/memdir/memdir.ts +507 -0
  43. package/memdir/memoryAge.ts +53 -0
  44. package/memdir/memoryScan.ts +94 -0
  45. package/memdir/memoryTypes.ts +271 -0
  46. package/memdir/paths.ts +278 -0
  47. package/memdir/teamMemPaths.ts +292 -0
  48. package/memdir/teamMemPrompts.ts +100 -0
  49. package/migrations/migrateAutoUpdatesToSettings.ts +61 -0
  50. package/migrations/migrateBypassPermissionsAcceptedToSettings.ts +40 -0
  51. package/migrations/migrateEnableAllProjectMcpServersToSettings.ts +118 -0
  52. package/migrations/migrateFennecToOpus.ts +45 -0
  53. package/migrations/migrateLegacyOpusToCurrent.ts +57 -0
  54. package/migrations/migrateOpusToOpus1m.ts +43 -0
  55. package/migrations/migrateReplBridgeEnabledToRemoteControlAtStartup.ts +22 -0
  56. package/migrations/migrateSonnet1mToSonnet45.ts +48 -0
  57. package/migrations/migrateSonnet45ToSonnet46.ts +67 -0
  58. package/migrations/resetAutoModeOptInForDefaultOffer.ts +51 -0
  59. package/migrations/resetProToOpusDefault.ts +51 -0
  60. package/native-ts/color-diff/index.ts +999 -0
  61. package/native-ts/file-index/index.ts +370 -0
  62. package/native-ts/yoga-layout/enums.ts +134 -0
  63. package/native-ts/yoga-layout/index.ts +2578 -0
  64. package/outputStyles/loadOutputStylesDir.ts +98 -0
  65. package/package.json +22 -5
  66. package/plugins/builtinPlugins.ts +159 -0
  67. package/plugins/bundled/index.ts +23 -0
  68. package/schemas/hooks.ts +222 -0
  69. package/screens/Doctor.tsx +575 -0
  70. package/screens/REPL.tsx +5006 -0
  71. package/screens/ResumeConversation.tsx +399 -0
  72. package/server/createDirectConnectSession.ts +88 -0
  73. package/server/directConnectManager.ts +213 -0
  74. package/server/types.ts +57 -0
  75. package/skills/bundled/batch.ts +124 -0
  76. package/skills/bundled/claudeApi.ts +196 -0
  77. package/skills/bundled/claudeApiContent.ts +75 -0
  78. package/skills/bundled/claudeInChrome.ts +34 -0
  79. package/skills/bundled/debug.ts +103 -0
  80. package/skills/bundled/index.ts +79 -0
  81. package/skills/bundled/keybindings.ts +339 -0
  82. package/skills/bundled/loop.ts +92 -0
  83. package/skills/bundled/loremIpsum.ts +282 -0
  84. package/skills/bundled/remember.ts +82 -0
  85. package/skills/bundled/scheduleRemoteAgents.ts +447 -0
  86. package/skills/bundled/simplify.ts +69 -0
  87. package/skills/bundled/skillify.ts +197 -0
  88. package/skills/bundled/stuck.ts +79 -0
  89. package/skills/bundled/updateConfig.ts +475 -0
  90. package/skills/bundled/verify/SKILL.md +3 -0
  91. package/skills/bundled/verify/examples/cli.md +3 -0
  92. package/skills/bundled/verify/examples/server.md +3 -0
  93. package/skills/bundled/verify.ts +30 -0
  94. package/skills/bundled/verifyContent.ts +13 -0
  95. package/skills/bundledSkills.ts +220 -0
  96. package/skills/loadSkillsDir.ts +1086 -0
  97. package/skills/mcpSkillBuilders.ts +44 -0
  98. package/tasks/DreamTask/DreamTask.ts +157 -0
  99. package/tasks/InProcessTeammateTask/InProcessTeammateTask.tsx +126 -0
  100. package/tasks/InProcessTeammateTask/types.ts +121 -0
  101. package/tasks/LocalAgentTask/LocalAgentTask.tsx +683 -0
  102. package/tasks/LocalMainSessionTask.ts +479 -0
  103. package/tasks/LocalShellTask/LocalShellTask.tsx +523 -0
  104. package/tasks/LocalShellTask/guards.ts +41 -0
  105. package/tasks/LocalShellTask/killShellTasks.ts +76 -0
  106. package/tasks/RemoteAgentTask/RemoteAgentTask.tsx +856 -0
  107. package/tasks/pillLabel.ts +82 -0
  108. package/tasks/stopTask.ts +100 -0
  109. package/tasks/types.ts +46 -0
  110. package/upstreamproxy/relay.ts +455 -0
  111. package/upstreamproxy/upstreamproxy.ts +285 -0
  112. package/vim/motions.ts +82 -0
  113. package/vim/operators.ts +556 -0
  114. package/vim/textObjects.ts +186 -0
  115. package/vim/transitions.ts +490 -0
  116. package/vim/types.ts +199 -0
  117. package/voice/voiceModeEnabled.ts +54 -0
@@ -0,0 +1,98 @@
1
+ import memoize from 'lodash-es/memoize.js'
2
+ import { basename } from 'path'
3
+ import type { OutputStyleConfig } from '../constants/outputStyles.js'
4
+ import { logForDebugging } from '../utils/debug.js'
5
+ import { coerceDescriptionToString } from '../utils/frontmatterParser.js'
6
+ import { logError } from '../utils/log.js'
7
+ import {
8
+ extractDescriptionFromMarkdown,
9
+ loadMarkdownFilesForSubdir,
10
+ } from '../utils/markdownConfigLoader.js'
11
+ import { clearPluginOutputStyleCache } from '../utils/plugins/loadPluginOutputStyles.js'
12
+
13
+ /**
14
+ * Loads markdown files from .claude/output-styles directories throughout the project
15
+ * and from ~/.claude/output-styles directory and converts them to output styles.
16
+ *
17
+ * Each filename becomes a style name, and the file content becomes the style prompt.
18
+ * The frontmatter provides name and description.
19
+ *
20
+ * Structure:
21
+ * - Project .claude/output-styles/*.md -> project styles
22
+ * - User ~/.claude/output-styles/*.md -> user styles (overridden by project styles)
23
+ *
24
+ * @param cwd Current working directory for project directory traversal
25
+ */
26
+ export const getOutputStyleDirStyles = memoize(
27
+ async (cwd: string): Promise<OutputStyleConfig[]> => {
28
+ try {
29
+ const markdownFiles = await loadMarkdownFilesForSubdir(
30
+ 'output-styles',
31
+ cwd,
32
+ )
33
+
34
+ const styles = markdownFiles
35
+ .map(({ filePath, frontmatter, content, source }) => {
36
+ try {
37
+ const fileName = basename(filePath)
38
+ const styleName = fileName.replace(/\.md$/, '')
39
+
40
+ // Get style configuration from frontmatter
41
+ const name = (frontmatter['name'] || styleName) as string
42
+ const description =
43
+ coerceDescriptionToString(
44
+ frontmatter['description'],
45
+ styleName,
46
+ ) ??
47
+ extractDescriptionFromMarkdown(
48
+ content,
49
+ `Custom ${styleName} output style`,
50
+ )
51
+
52
+ // Parse keep-coding-instructions flag (supports both boolean and string values)
53
+ const keepCodingInstructionsRaw =
54
+ frontmatter['keep-coding-instructions']
55
+ const keepCodingInstructions =
56
+ keepCodingInstructionsRaw === true ||
57
+ keepCodingInstructionsRaw === 'true'
58
+ ? true
59
+ : keepCodingInstructionsRaw === false ||
60
+ keepCodingInstructionsRaw === 'false'
61
+ ? false
62
+ : undefined
63
+
64
+ // Warn if force-for-plugin is set on non-plugin output style
65
+ if (frontmatter['force-for-plugin'] !== undefined) {
66
+ logForDebugging(
67
+ `Output style "${name}" has force-for-plugin set, but this option only applies to plugin output styles. Ignoring.`,
68
+ { level: 'warn' },
69
+ )
70
+ }
71
+
72
+ return {
73
+ name,
74
+ description,
75
+ prompt: content.trim(),
76
+ source,
77
+ keepCodingInstructions,
78
+ }
79
+ } catch (error) {
80
+ logError(error)
81
+ return null
82
+ }
83
+ })
84
+ .filter(style => style !== null)
85
+
86
+ return styles
87
+ } catch (error) {
88
+ logError(error)
89
+ return []
90
+ }
91
+ },
92
+ )
93
+
94
+ export function clearOutputStyleCaches(): void {
95
+ getOutputStyleDirStyles.cache?.clear?.()
96
+ loadMarkdownFilesForSubdir.cache?.clear?.()
97
+ clearPluginOutputStyleCache()
98
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "yzcode-cli",
3
- "version": "1.0.1",
3
+ "version": "1.0.3",
4
4
  "description": "YZcode - AI编程助手命令行工具",
5
5
  "type": "module",
6
6
  "bin": {
@@ -8,9 +8,9 @@
8
8
  "yzcode-config": "./bin/yzcode-config.js"
9
9
  },
10
10
  "dependencies": {
11
- "@anthropic-ai/claude-agent-sdk": "^0.2.88",
12
- "@anthropic-ai/mcpb": "^2.1.2",
13
- "@anthropic-ai/sdk": "^0.80.0",
11
+ "@anthropic-ai/claude-agent-sdk": "0.2.88",
12
+ "@anthropic-ai/mcpb": "2.1.2",
13
+ "@anthropic-ai/sdk": "0.80.0",
14
14
  "@aws-sdk/client-bedrock-runtime": "^3.709.0",
15
15
  "@commander-js/extra-typings": "^12.1.0",
16
16
  "@growthbook/growthbook": "^0.33.0",
@@ -91,6 +91,23 @@
91
91
  "*.ts",
92
92
  "*.tsx",
93
93
  "bin",
94
- "node_modules"
94
+ "bootstrap",
95
+ "assistant",
96
+ "buddy",
97
+ "bridge",
98
+ "coordinator",
99
+ "memdir",
100
+ "migrations",
101
+ "native-ts",
102
+ "outputStyles",
103
+ "plugins",
104
+ "schemas",
105
+ "screens",
106
+ "server",
107
+ "skills",
108
+ "tasks",
109
+ "upstreamproxy",
110
+ "vim",
111
+ "voice"
95
112
  ]
96
113
  }
@@ -0,0 +1,159 @@
1
+ /**
2
+ * Built-in Plugin Registry
3
+ *
4
+ * Manages built-in plugins that ship with the CLI and can be enabled/disabled
5
+ * by users via the /plugin UI.
6
+ *
7
+ * Built-in plugins differ from bundled skills (src/skills/bundled/) in that:
8
+ * - They appear in the /plugin UI under a "Built-in" section
9
+ * - Users can enable/disable them (persisted to user settings)
10
+ * - They can provide multiple components (skills, hooks, MCP servers)
11
+ *
12
+ * Plugin IDs use the format `{name}@builtin` to distinguish them from
13
+ * marketplace plugins (`{name}@{marketplace}`).
14
+ */
15
+
16
+ import type { Command } from '../commands.js'
17
+ import type { BundledSkillDefinition } from '../skills/bundledSkills.js'
18
+ import type { BuiltinPluginDefinition, LoadedPlugin } from '../types/plugin.js'
19
+ import { getSettings_DEPRECATED } from '../utils/settings/settings.js'
20
+
21
+ const BUILTIN_PLUGINS: Map<string, BuiltinPluginDefinition> = new Map()
22
+
23
+ export const BUILTIN_MARKETPLACE_NAME = 'builtin'
24
+
25
+ /**
26
+ * Register a built-in plugin. Call this from initBuiltinPlugins() at startup.
27
+ */
28
+ export function registerBuiltinPlugin(
29
+ definition: BuiltinPluginDefinition,
30
+ ): void {
31
+ BUILTIN_PLUGINS.set(definition.name, definition)
32
+ }
33
+
34
+ /**
35
+ * Check if a plugin ID represents a built-in plugin (ends with @builtin).
36
+ */
37
+ export function isBuiltinPluginId(pluginId: string): boolean {
38
+ return pluginId.endsWith(`@${BUILTIN_MARKETPLACE_NAME}`)
39
+ }
40
+
41
+ /**
42
+ * Get a specific built-in plugin definition by name.
43
+ * Useful for the /plugin UI to show the skills/hooks/MCP list without
44
+ * a marketplace lookup.
45
+ */
46
+ export function getBuiltinPluginDefinition(
47
+ name: string,
48
+ ): BuiltinPluginDefinition | undefined {
49
+ return BUILTIN_PLUGINS.get(name)
50
+ }
51
+
52
+ /**
53
+ * Get all registered built-in plugins as LoadedPlugin objects, split into
54
+ * enabled/disabled based on user settings (with defaultEnabled as fallback).
55
+ * Plugins whose isAvailable() returns false are omitted entirely.
56
+ */
57
+ export function getBuiltinPlugins(): {
58
+ enabled: LoadedPlugin[]
59
+ disabled: LoadedPlugin[]
60
+ } {
61
+ const settings = getSettings_DEPRECATED()
62
+ const enabled: LoadedPlugin[] = []
63
+ const disabled: LoadedPlugin[] = []
64
+
65
+ for (const [name, definition] of BUILTIN_PLUGINS) {
66
+ if (definition.isAvailable && !definition.isAvailable()) {
67
+ continue
68
+ }
69
+
70
+ const pluginId = `${name}@${BUILTIN_MARKETPLACE_NAME}`
71
+ const userSetting = settings?.enabledPlugins?.[pluginId]
72
+ // Enabled state: user preference > plugin default > true
73
+ const isEnabled =
74
+ userSetting !== undefined
75
+ ? userSetting === true
76
+ : (definition.defaultEnabled ?? true)
77
+
78
+ const plugin: LoadedPlugin = {
79
+ name,
80
+ manifest: {
81
+ name,
82
+ description: definition.description,
83
+ version: definition.version,
84
+ },
85
+ path: BUILTIN_MARKETPLACE_NAME, // sentinel — no filesystem path
86
+ source: pluginId,
87
+ repository: pluginId,
88
+ enabled: isEnabled,
89
+ isBuiltin: true,
90
+ hooksConfig: definition.hooks,
91
+ mcpServers: definition.mcpServers,
92
+ }
93
+
94
+ if (isEnabled) {
95
+ enabled.push(plugin)
96
+ } else {
97
+ disabled.push(plugin)
98
+ }
99
+ }
100
+
101
+ return { enabled, disabled }
102
+ }
103
+
104
+ /**
105
+ * Get skills from enabled built-in plugins as Command objects.
106
+ * Skills from disabled plugins are not returned.
107
+ */
108
+ export function getBuiltinPluginSkillCommands(): Command[] {
109
+ const { enabled } = getBuiltinPlugins()
110
+ const commands: Command[] = []
111
+
112
+ for (const plugin of enabled) {
113
+ const definition = BUILTIN_PLUGINS.get(plugin.name)
114
+ if (!definition?.skills) continue
115
+ for (const skill of definition.skills) {
116
+ commands.push(skillDefinitionToCommand(skill))
117
+ }
118
+ }
119
+
120
+ return commands
121
+ }
122
+
123
+ /**
124
+ * Clear built-in plugins registry (for testing).
125
+ */
126
+ export function clearBuiltinPlugins(): void {
127
+ BUILTIN_PLUGINS.clear()
128
+ }
129
+
130
+ // --
131
+
132
+ function skillDefinitionToCommand(definition: BundledSkillDefinition): Command {
133
+ return {
134
+ type: 'prompt',
135
+ name: definition.name,
136
+ description: definition.description,
137
+ hasUserSpecifiedDescription: true,
138
+ allowedTools: definition.allowedTools ?? [],
139
+ argumentHint: definition.argumentHint,
140
+ whenToUse: definition.whenToUse,
141
+ model: definition.model,
142
+ disableModelInvocation: definition.disableModelInvocation ?? false,
143
+ userInvocable: definition.userInvocable ?? true,
144
+ contentLength: 0,
145
+ // 'bundled' not 'builtin' — 'builtin' in Command.source means hardcoded
146
+ // slash commands (/help, /clear). Using 'bundled' keeps these skills in
147
+ // the Skill tool's listing, analytics name logging, and prompt-truncation
148
+ // exemption. The user-toggleable aspect is tracked on LoadedPlugin.isBuiltin.
149
+ source: 'bundled',
150
+ loadedFrom: 'bundled',
151
+ hooks: definition.hooks,
152
+ context: definition.context,
153
+ agent: definition.agent,
154
+ isEnabled: definition.isEnabled ?? (() => true),
155
+ isHidden: !(definition.userInvocable ?? true),
156
+ progressMessage: 'running',
157
+ getPromptForCommand: definition.getPromptForCommand,
158
+ }
159
+ }
@@ -0,0 +1,23 @@
1
+ /**
2
+ * Built-in Plugin Initialization
3
+ *
4
+ * Initializes built-in plugins that ship with the CLI and appear in the
5
+ * /plugin UI for users to enable/disable.
6
+ *
7
+ * Not all bundled features should be built-in plugins — use this for
8
+ * features that users should be able to explicitly enable/disable. For
9
+ * features with complex setup or automatic-enabling logic (e.g.
10
+ * claude-in-chrome), use src/skills/bundled/ instead.
11
+ *
12
+ * To add a new built-in plugin:
13
+ * 1. Import registerBuiltinPlugin from '../builtinPlugins.js'
14
+ * 2. Call registerBuiltinPlugin() with the plugin definition here
15
+ */
16
+
17
+ /**
18
+ * Initialize built-in plugins. Called during CLI startup.
19
+ */
20
+ export function initBuiltinPlugins(): void {
21
+ // No built-in plugins registered yet — this is the scaffolding for
22
+ // migrating bundled skills that should be user-toggleable.
23
+ }
@@ -0,0 +1,222 @@
1
+ /**
2
+ * Hook Zod schemas extracted to break import cycles.
3
+ *
4
+ * This file contains hook-related schema definitions that were originally
5
+ * in src/utils/settings/types.ts. By extracting them here, we break the
6
+ * circular dependency between settings/types.ts and plugins/schemas.ts.
7
+ *
8
+ * Both files now import from this shared location instead of each other.
9
+ */
10
+
11
+ import { HOOK_EVENTS, type HookEvent } from 'src/entrypoints/agentSdkTypes.js'
12
+ import { z } from 'zod/v4'
13
+ import { lazySchema } from '../utils/lazySchema.js'
14
+ import { SHELL_TYPES } from '../utils/shell/shellProvider.js'
15
+
16
+ // Shared schema for the `if` condition field.
17
+ // Uses permission rule syntax (e.g., "Bash(git *)", "Read(*.ts)") to filter hooks
18
+ // before spawning. Evaluated against the hook input's tool_name and tool_input.
19
+ const IfConditionSchema = lazySchema(() =>
20
+ z
21
+ .string()
22
+ .optional()
23
+ .describe(
24
+ 'Permission rule syntax to filter when this hook runs (e.g., "Bash(git *)"). ' +
25
+ 'Only runs if the tool call matches the pattern. Avoids spawning hooks for non-matching commands.',
26
+ ),
27
+ )
28
+
29
+ // Internal factory for individual hook schemas (shared between exported
30
+ // discriminated union members and the HookCommandSchema factory)
31
+ function buildHookSchemas() {
32
+ const BashCommandHookSchema = z.object({
33
+ type: z.literal('command').describe('Shell command hook type'),
34
+ command: z.string().describe('Shell command to execute'),
35
+ if: IfConditionSchema(),
36
+ shell: z
37
+ .enum(SHELL_TYPES)
38
+ .optional()
39
+ .describe(
40
+ "Shell interpreter. 'bash' uses your $SHELL (bash/zsh/sh); 'powershell' uses pwsh. Defaults to bash.",
41
+ ),
42
+ timeout: z
43
+ .number()
44
+ .positive()
45
+ .optional()
46
+ .describe('Timeout in seconds for this specific command'),
47
+ statusMessage: z
48
+ .string()
49
+ .optional()
50
+ .describe('Custom status message to display in spinner while hook runs'),
51
+ once: z
52
+ .boolean()
53
+ .optional()
54
+ .describe('If true, hook runs once and is removed after execution'),
55
+ async: z
56
+ .boolean()
57
+ .optional()
58
+ .describe('If true, hook runs in background without blocking'),
59
+ asyncRewake: z
60
+ .boolean()
61
+ .optional()
62
+ .describe(
63
+ 'If true, hook runs in background and wakes the model on exit code 2 (blocking error). Implies async.',
64
+ ),
65
+ })
66
+
67
+ const PromptHookSchema = z.object({
68
+ type: z.literal('prompt').describe('LLM prompt hook type'),
69
+ prompt: z
70
+ .string()
71
+ .describe(
72
+ 'Prompt to evaluate with LLM. Use $ARGUMENTS placeholder for hook input JSON.',
73
+ ),
74
+ if: IfConditionSchema(),
75
+ timeout: z
76
+ .number()
77
+ .positive()
78
+ .optional()
79
+ .describe('Timeout in seconds for this specific prompt evaluation'),
80
+ // @[MODEL LAUNCH]: Update the example model ID in the .describe() strings below (prompt + agent hooks).
81
+ model: z
82
+ .string()
83
+ .optional()
84
+ .describe(
85
+ 'Model to use for this prompt hook (e.g., "claude-sonnet-4-6"). If not specified, uses the default small fast model.',
86
+ ),
87
+ statusMessage: z
88
+ .string()
89
+ .optional()
90
+ .describe('Custom status message to display in spinner while hook runs'),
91
+ once: z
92
+ .boolean()
93
+ .optional()
94
+ .describe('If true, hook runs once and is removed after execution'),
95
+ })
96
+
97
+ const HttpHookSchema = z.object({
98
+ type: z.literal('http').describe('HTTP hook type'),
99
+ url: z.string().url().describe('URL to POST the hook input JSON to'),
100
+ if: IfConditionSchema(),
101
+ timeout: z
102
+ .number()
103
+ .positive()
104
+ .optional()
105
+ .describe('Timeout in seconds for this specific request'),
106
+ headers: z
107
+ .record(z.string(), z.string())
108
+ .optional()
109
+ .describe(
110
+ 'Additional headers to include in the request. Values may reference environment variables using $VAR_NAME or ${VAR_NAME} syntax (e.g., "Authorization": "Bearer $MY_TOKEN"). Only variables listed in allowedEnvVars will be interpolated.',
111
+ ),
112
+ allowedEnvVars: z
113
+ .array(z.string())
114
+ .optional()
115
+ .describe(
116
+ 'Explicit list of environment variable names that may be interpolated in header values. Only variables listed here will be resolved; all other $VAR references are left as empty strings. Required for env var interpolation to work.',
117
+ ),
118
+ statusMessage: z
119
+ .string()
120
+ .optional()
121
+ .describe('Custom status message to display in spinner while hook runs'),
122
+ once: z
123
+ .boolean()
124
+ .optional()
125
+ .describe('If true, hook runs once and is removed after execution'),
126
+ })
127
+
128
+ const AgentHookSchema = z.object({
129
+ type: z.literal('agent').describe('Agentic verifier hook type'),
130
+ // DO NOT add .transform() here. This schema is used by parseSettingsFile,
131
+ // and updateSettingsForSource round-trips the parsed result through
132
+ // JSON.stringify — a transformed function value is silently dropped,
133
+ // deleting the user's prompt from settings.json (gh-24920, CC-79). The
134
+ // transform (from #10594) wrapped the string in `(_msgs) => prompt`
135
+ // for a programmatic-construction use case in ExitPlanModeV2Tool that
136
+ // has since been refactored into VerifyPlanExecutionTool, which no
137
+ // longer constructs AgentHook objects at all.
138
+ prompt: z
139
+ .string()
140
+ .describe(
141
+ 'Prompt describing what to verify (e.g. "Verify that unit tests ran and passed."). Use $ARGUMENTS placeholder for hook input JSON.',
142
+ ),
143
+ if: IfConditionSchema(),
144
+ timeout: z
145
+ .number()
146
+ .positive()
147
+ .optional()
148
+ .describe('Timeout in seconds for agent execution (default 60)'),
149
+ model: z
150
+ .string()
151
+ .optional()
152
+ .describe(
153
+ 'Model to use for this agent hook (e.g., "claude-sonnet-4-6"). If not specified, uses Haiku.',
154
+ ),
155
+ statusMessage: z
156
+ .string()
157
+ .optional()
158
+ .describe('Custom status message to display in spinner while hook runs'),
159
+ once: z
160
+ .boolean()
161
+ .optional()
162
+ .describe('If true, hook runs once and is removed after execution'),
163
+ })
164
+
165
+ return {
166
+ BashCommandHookSchema,
167
+ PromptHookSchema,
168
+ HttpHookSchema,
169
+ AgentHookSchema,
170
+ }
171
+ }
172
+
173
+ /**
174
+ * Schema for hook command (excludes function hooks - they can't be persisted)
175
+ */
176
+ export const HookCommandSchema = lazySchema(() => {
177
+ const {
178
+ BashCommandHookSchema,
179
+ PromptHookSchema,
180
+ AgentHookSchema,
181
+ HttpHookSchema,
182
+ } = buildHookSchemas()
183
+ return z.discriminatedUnion('type', [
184
+ BashCommandHookSchema,
185
+ PromptHookSchema,
186
+ AgentHookSchema,
187
+ HttpHookSchema,
188
+ ])
189
+ })
190
+
191
+ /**
192
+ * Schema for matcher configuration with multiple hooks
193
+ */
194
+ export const HookMatcherSchema = lazySchema(() =>
195
+ z.object({
196
+ matcher: z
197
+ .string()
198
+ .optional()
199
+ .describe('String pattern to match (e.g. tool names like "Write")'), // String (e.g. Write) to match values related to the hook event, e.g. tool names
200
+ hooks: z
201
+ .array(HookCommandSchema())
202
+ .describe('List of hooks to execute when the matcher matches'),
203
+ }),
204
+ )
205
+
206
+ /**
207
+ * Schema for hooks configuration
208
+ * The key is the hook event. The value is an array of matcher configurations.
209
+ * Uses partialRecord since not all hook events need to be defined.
210
+ */
211
+ export const HooksSchema = lazySchema(() =>
212
+ z.partialRecord(z.enum(HOOK_EVENTS), z.array(HookMatcherSchema())),
213
+ )
214
+
215
+ // Inferred types from schemas
216
+ export type HookCommand = z.infer<ReturnType<typeof HookCommandSchema>>
217
+ export type BashCommandHook = Extract<HookCommand, { type: 'command' }>
218
+ export type PromptHook = Extract<HookCommand, { type: 'prompt' }>
219
+ export type AgentHook = Extract<HookCommand, { type: 'agent' }>
220
+ export type HttpHook = Extract<HookCommand, { type: 'http' }>
221
+ export type HookMatcher = z.infer<ReturnType<typeof HookMatcherSchema>>
222
+ export type HooksSettings = Partial<Record<HookEvent, HookMatcher[]>>