yzcode-cli 1.0.2 → 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 +19 -2
  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,34 @@
1
+ import { BROWSER_TOOLS } from '@ant/claude-for-chrome-mcp'
2
+ import { BASE_CHROME_PROMPT } from '../../utils/claudeInChrome/prompt.js'
3
+ import { shouldAutoEnableClaudeInChrome } from '../../utils/claudeInChrome/setup.js'
4
+ import { registerBundledSkill } from '../bundledSkills.js'
5
+
6
+ const CLAUDE_IN_CHROME_MCP_TOOLS = BROWSER_TOOLS.map(
7
+ tool => `mcp__claude-in-chrome__${tool.name}`,
8
+ )
9
+
10
+ const SKILL_ACTIVATION_MESSAGE = `
11
+ Now that this skill is invoked, you have access to Chrome browser automation tools. You can now use the mcp__claude-in-chrome__* tools to interact with web pages.
12
+
13
+ IMPORTANT: Start by calling mcp__claude-in-chrome__tabs_context_mcp to get information about the user's current browser tabs.
14
+ `
15
+
16
+ export function registerClaudeInChromeSkill(): void {
17
+ registerBundledSkill({
18
+ name: 'claude-in-chrome',
19
+ description:
20
+ 'Automates your Chrome browser to interact with web pages - clicking elements, filling forms, capturing screenshots, reading console logs, and navigating sites. Opens pages in new tabs within your existing Chrome session. Requires site-level permissions before executing (configured in the extension).',
21
+ whenToUse:
22
+ 'When the user wants to interact with web pages, automate browser tasks, capture screenshots, read console logs, or perform any browser-based actions. Always invoke BEFORE attempting to use any mcp__claude-in-chrome__* tools.',
23
+ allowedTools: CLAUDE_IN_CHROME_MCP_TOOLS,
24
+ userInvocable: true,
25
+ isEnabled: () => shouldAutoEnableClaudeInChrome(),
26
+ async getPromptForCommand(args) {
27
+ let prompt = `${BASE_CHROME_PROMPT}\n${SKILL_ACTIVATION_MESSAGE}`
28
+ if (args) {
29
+ prompt += `\n## Task\n\n${args}`
30
+ }
31
+ return [{ type: 'text', text: prompt }]
32
+ },
33
+ })
34
+ }
@@ -0,0 +1,103 @@
1
+ import { open, stat } from 'fs/promises'
2
+ import { CLAUDE_CODE_GUIDE_AGENT_TYPE } from 'src/tools/AgentTool/built-in/claudeCodeGuideAgent.js'
3
+ import { getSettingsFilePathForSource } from 'src/utils/settings/settings.js'
4
+ import { enableDebugLogging, getDebugLogPath } from '../../utils/debug.js'
5
+ import { errorMessage, isENOENT } from '../../utils/errors.js'
6
+ import { formatFileSize } from '../../utils/format.js'
7
+ import { registerBundledSkill } from '../bundledSkills.js'
8
+
9
+ const DEFAULT_DEBUG_LINES_READ = 20
10
+ const TAIL_READ_BYTES = 64 * 1024
11
+
12
+ export function registerDebugSkill(): void {
13
+ registerBundledSkill({
14
+ name: 'debug',
15
+ description:
16
+ process.env.USER_TYPE === 'ant'
17
+ ? 'Debug your current Claude Code session by reading the session debug log. Includes all event logging'
18
+ : 'Enable debug logging for this session and help diagnose issues',
19
+ allowedTools: ['Read', 'Grep', 'Glob'],
20
+ argumentHint: '[issue description]',
21
+ // disableModelInvocation so that the user has to explicitly request it in
22
+ // interactive mode and so the description does not take up context.
23
+ disableModelInvocation: true,
24
+ userInvocable: true,
25
+ async getPromptForCommand(args) {
26
+ // Non-ants don't write debug logs by default — turn logging on now so
27
+ // subsequent activity in this session is captured.
28
+ const wasAlreadyLogging = enableDebugLogging()
29
+ const debugLogPath = getDebugLogPath()
30
+
31
+ let logInfo: string
32
+ try {
33
+ // Tail the log without reading the whole thing - debug logs grow
34
+ // unbounded in long sessions and reading them in full spikes RSS.
35
+ const stats = await stat(debugLogPath)
36
+ const readSize = Math.min(stats.size, TAIL_READ_BYTES)
37
+ const startOffset = stats.size - readSize
38
+ const fd = await open(debugLogPath, 'r')
39
+ try {
40
+ const { buffer, bytesRead } = await fd.read({
41
+ buffer: Buffer.alloc(readSize),
42
+ position: startOffset,
43
+ })
44
+ const tail = buffer
45
+ .toString('utf-8', 0, bytesRead)
46
+ .split('\n')
47
+ .slice(-DEFAULT_DEBUG_LINES_READ)
48
+ .join('\n')
49
+ logInfo = `Log size: ${formatFileSize(stats.size)}\n\n### Last ${DEFAULT_DEBUG_LINES_READ} lines\n\n\`\`\`\n${tail}\n\`\`\``
50
+ } finally {
51
+ await fd.close()
52
+ }
53
+ } catch (e) {
54
+ logInfo = isENOENT(e)
55
+ ? 'No debug log exists yet — logging was just enabled.'
56
+ : `Failed to read last ${DEFAULT_DEBUG_LINES_READ} lines of debug log: ${errorMessage(e)}`
57
+ }
58
+
59
+ const justEnabledSection = wasAlreadyLogging
60
+ ? ''
61
+ : `
62
+ ## Debug Logging Just Enabled
63
+
64
+ Debug logging was OFF for this session until now. Nothing prior to this /debug invocation was captured.
65
+
66
+ Tell the user that debug logging is now active at \`${debugLogPath}\`, ask them to reproduce the issue, then re-read the log. If they can't reproduce, they can also restart with \`claude --debug\` to capture logs from startup.
67
+ `
68
+
69
+ const prompt = `# Debug Skill
70
+
71
+ Help the user debug an issue they're encountering in this current Claude Code session.
72
+ ${justEnabledSection}
73
+ ## Session Debug Log
74
+
75
+ The debug log for the current session is at: \`${debugLogPath}\`
76
+
77
+ ${logInfo}
78
+
79
+ For additional context, grep for [ERROR] and [WARN] lines across the full file.
80
+
81
+ ## Issue Description
82
+
83
+ ${args || 'The user did not describe a specific issue. Read the debug log and summarize any errors, warnings, or notable issues.'}
84
+
85
+ ## Settings
86
+
87
+ Remember that settings are in:
88
+ * user - ${getSettingsFilePathForSource('userSettings')}
89
+ * project - ${getSettingsFilePathForSource('projectSettings')}
90
+ * local - ${getSettingsFilePathForSource('localSettings')}
91
+
92
+ ## Instructions
93
+
94
+ 1. Review the user's issue description
95
+ 2. The last ${DEFAULT_DEBUG_LINES_READ} lines show the debug file format. Look for [ERROR] and [WARN] entries, stack traces, and failure patterns across the file
96
+ 3. Consider launching the ${CLAUDE_CODE_GUIDE_AGENT_TYPE} subagent to understand the relevant Claude Code features
97
+ 4. Explain what you found in plain language
98
+ 5. Suggest concrete fixes or next steps
99
+ `
100
+ return [{ type: 'text', text: prompt }]
101
+ },
102
+ })
103
+ }
@@ -0,0 +1,79 @@
1
+ import { feature } from 'bun:bundle'
2
+ import { shouldAutoEnableClaudeInChrome } from 'src/utils/claudeInChrome/setup.js'
3
+ import { registerBatchSkill } from './batch.js'
4
+ import { registerClaudeInChromeSkill } from './claudeInChrome.js'
5
+ import { registerDebugSkill } from './debug.js'
6
+ import { registerKeybindingsSkill } from './keybindings.js'
7
+ import { registerLoremIpsumSkill } from './loremIpsum.js'
8
+ import { registerRememberSkill } from './remember.js'
9
+ import { registerSimplifySkill } from './simplify.js'
10
+ import { registerSkillifySkill } from './skillify.js'
11
+ import { registerStuckSkill } from './stuck.js'
12
+ import { registerUpdateConfigSkill } from './updateConfig.js'
13
+ import { registerVerifySkill } from './verify.js'
14
+
15
+ /**
16
+ * Initialize all bundled skills.
17
+ * Called at startup to register skills that ship with the CLI.
18
+ *
19
+ * To add a new bundled skill:
20
+ * 1. Create a new file in src/skills/bundled/ (e.g., myskill.ts)
21
+ * 2. Export a register function that calls registerBundledSkill()
22
+ * 3. Import and call that function here
23
+ */
24
+ export function initBundledSkills(): void {
25
+ registerUpdateConfigSkill()
26
+ registerKeybindingsSkill()
27
+ registerVerifySkill()
28
+ registerDebugSkill()
29
+ registerLoremIpsumSkill()
30
+ registerSkillifySkill()
31
+ registerRememberSkill()
32
+ registerSimplifySkill()
33
+ registerBatchSkill()
34
+ registerStuckSkill()
35
+ if (feature('KAIROS') || feature('KAIROS_DREAM')) {
36
+ /* eslint-disable @typescript-eslint/no-require-imports */
37
+ const { registerDreamSkill } = require('./dream.js')
38
+ /* eslint-enable @typescript-eslint/no-require-imports */
39
+ registerDreamSkill()
40
+ }
41
+ if (feature('REVIEW_ARTIFACT')) {
42
+ /* eslint-disable @typescript-eslint/no-require-imports */
43
+ const { registerHunterSkill } = require('./hunter.js')
44
+ /* eslint-enable @typescript-eslint/no-require-imports */
45
+ registerHunterSkill()
46
+ }
47
+ if (feature('AGENT_TRIGGERS')) {
48
+ /* eslint-disable @typescript-eslint/no-require-imports */
49
+ const { registerLoopSkill } = require('./loop.js')
50
+ /* eslint-enable @typescript-eslint/no-require-imports */
51
+ // /loop's isEnabled delegates to isKairosCronEnabled() — same lazy
52
+ // per-invocation pattern as the cron tools. Registered unconditionally;
53
+ // the skill's own isEnabled callback decides visibility.
54
+ registerLoopSkill()
55
+ }
56
+ if (feature('AGENT_TRIGGERS_REMOTE')) {
57
+ /* eslint-disable @typescript-eslint/no-require-imports */
58
+ const {
59
+ registerScheduleRemoteAgentsSkill,
60
+ } = require('./scheduleRemoteAgents.js')
61
+ /* eslint-enable @typescript-eslint/no-require-imports */
62
+ registerScheduleRemoteAgentsSkill()
63
+ }
64
+ if (feature('BUILDING_CLAUDE_APPS')) {
65
+ /* eslint-disable @typescript-eslint/no-require-imports */
66
+ const { registerClaudeApiSkill } = require('./claudeApi.js')
67
+ /* eslint-enable @typescript-eslint/no-require-imports */
68
+ registerClaudeApiSkill()
69
+ }
70
+ if (shouldAutoEnableClaudeInChrome()) {
71
+ registerClaudeInChromeSkill()
72
+ }
73
+ if (feature('RUN_SKILL_GENERATOR')) {
74
+ /* eslint-disable @typescript-eslint/no-require-imports */
75
+ const { registerRunSkillGeneratorSkill } = require('./runSkillGenerator.js')
76
+ /* eslint-enable @typescript-eslint/no-require-imports */
77
+ registerRunSkillGeneratorSkill()
78
+ }
79
+ }
@@ -0,0 +1,339 @@
1
+ import { DEFAULT_BINDINGS } from '../../keybindings/defaultBindings.js'
2
+ import { isKeybindingCustomizationEnabled } from '../../keybindings/loadUserBindings.js'
3
+ import {
4
+ MACOS_RESERVED,
5
+ NON_REBINDABLE,
6
+ TERMINAL_RESERVED,
7
+ } from '../../keybindings/reservedShortcuts.js'
8
+ import type { KeybindingsSchemaType } from '../../keybindings/schema.js'
9
+ import {
10
+ KEYBINDING_ACTIONS,
11
+ KEYBINDING_CONTEXT_DESCRIPTIONS,
12
+ KEYBINDING_CONTEXTS,
13
+ } from '../../keybindings/schema.js'
14
+ import { jsonStringify } from '../../utils/slowOperations.js'
15
+ import { registerBundledSkill } from '../bundledSkills.js'
16
+
17
+ /**
18
+ * Build a markdown table of all contexts.
19
+ */
20
+ function generateContextsTable(): string {
21
+ return markdownTable(
22
+ ['Context', 'Description'],
23
+ KEYBINDING_CONTEXTS.map(ctx => [
24
+ `\`${ctx}\``,
25
+ KEYBINDING_CONTEXT_DESCRIPTIONS[ctx],
26
+ ]),
27
+ )
28
+ }
29
+
30
+ /**
31
+ * Build a markdown table of all actions with their default bindings and context.
32
+ */
33
+ function generateActionsTable(): string {
34
+ // Build a lookup: action -> { keys, context }
35
+ const actionInfo: Record<string, { keys: string[]; context: string }> = {}
36
+ for (const block of DEFAULT_BINDINGS) {
37
+ for (const [key, action] of Object.entries(block.bindings)) {
38
+ if (action) {
39
+ if (!actionInfo[action]) {
40
+ actionInfo[action] = { keys: [], context: block.context }
41
+ }
42
+ actionInfo[action].keys.push(key)
43
+ }
44
+ }
45
+ }
46
+
47
+ return markdownTable(
48
+ ['Action', 'Default Key(s)', 'Context'],
49
+ KEYBINDING_ACTIONS.map(action => {
50
+ const info = actionInfo[action]
51
+ const keys = info ? info.keys.map(k => `\`${k}\``).join(', ') : '(none)'
52
+ const context = info ? info.context : inferContextFromAction(action)
53
+ return [`\`${action}\``, keys, context]
54
+ }),
55
+ )
56
+ }
57
+
58
+ /**
59
+ * Infer context from action prefix when not in DEFAULT_BINDINGS.
60
+ */
61
+ function inferContextFromAction(action: string): string {
62
+ const prefix = action.split(':')[0]
63
+ const prefixToContext: Record<string, string> = {
64
+ app: 'Global',
65
+ history: 'Global or Chat',
66
+ chat: 'Chat',
67
+ autocomplete: 'Autocomplete',
68
+ confirm: 'Confirmation',
69
+ tabs: 'Tabs',
70
+ transcript: 'Transcript',
71
+ historySearch: 'HistorySearch',
72
+ task: 'Task',
73
+ theme: 'ThemePicker',
74
+ help: 'Help',
75
+ attachments: 'Attachments',
76
+ footer: 'Footer',
77
+ messageSelector: 'MessageSelector',
78
+ diff: 'DiffDialog',
79
+ modelPicker: 'ModelPicker',
80
+ select: 'Select',
81
+ permission: 'Confirmation',
82
+ }
83
+ return prefixToContext[prefix ?? ''] ?? 'Unknown'
84
+ }
85
+
86
+ /**
87
+ * Build a list of reserved shortcuts.
88
+ */
89
+ function generateReservedShortcuts(): string {
90
+ const lines: string[] = []
91
+
92
+ lines.push('### Non-rebindable (errors)')
93
+ for (const s of NON_REBINDABLE) {
94
+ lines.push(`- \`${s.key}\` — ${s.reason}`)
95
+ }
96
+
97
+ lines.push('')
98
+ lines.push('### Terminal reserved (errors/warnings)')
99
+ for (const s of TERMINAL_RESERVED) {
100
+ lines.push(
101
+ `- \`${s.key}\` — ${s.reason} (${s.severity === 'error' ? 'will not work' : 'may conflict'})`,
102
+ )
103
+ }
104
+
105
+ lines.push('')
106
+ lines.push('### macOS reserved (errors)')
107
+ for (const s of MACOS_RESERVED) {
108
+ lines.push(`- \`${s.key}\` — ${s.reason}`)
109
+ }
110
+
111
+ return lines.join('\n')
112
+ }
113
+
114
+ const FILE_FORMAT_EXAMPLE: KeybindingsSchemaType = {
115
+ $schema: 'https://www.schemastore.org/claude-code-keybindings.json',
116
+ $docs: 'https://code.claude.com/docs/en/keybindings',
117
+ bindings: [
118
+ {
119
+ context: 'Chat',
120
+ bindings: {
121
+ 'ctrl+e': 'chat:externalEditor',
122
+ },
123
+ },
124
+ ],
125
+ }
126
+
127
+ const UNBIND_EXAMPLE: KeybindingsSchemaType['bindings'][number] = {
128
+ context: 'Chat',
129
+ bindings: {
130
+ 'ctrl+s': null,
131
+ },
132
+ }
133
+
134
+ const REBIND_EXAMPLE: KeybindingsSchemaType['bindings'][number] = {
135
+ context: 'Chat',
136
+ bindings: {
137
+ 'ctrl+g': null,
138
+ 'ctrl+e': 'chat:externalEditor',
139
+ },
140
+ }
141
+
142
+ const CHORD_EXAMPLE: KeybindingsSchemaType['bindings'][number] = {
143
+ context: 'Global',
144
+ bindings: {
145
+ 'ctrl+k ctrl+t': 'app:toggleTodos',
146
+ },
147
+ }
148
+
149
+ const SECTION_INTRO = [
150
+ '# Keybindings Skill',
151
+ '',
152
+ 'Create or modify `~/.claude/keybindings.json` to customize keyboard shortcuts.',
153
+ '',
154
+ '## CRITICAL: Read Before Write',
155
+ '',
156
+ '**Always read `~/.claude/keybindings.json` first** (it may not exist yet). Merge changes with existing bindings — never replace the entire file.',
157
+ '',
158
+ '- Use **Edit** tool for modifications to existing files',
159
+ '- Use **Write** tool only if the file does not exist yet',
160
+ ].join('\n')
161
+
162
+ const SECTION_FILE_FORMAT = [
163
+ '## File Format',
164
+ '',
165
+ '```json',
166
+ jsonStringify(FILE_FORMAT_EXAMPLE, null, 2),
167
+ '```',
168
+ '',
169
+ 'Always include the `$schema` and `$docs` fields.',
170
+ ].join('\n')
171
+
172
+ const SECTION_KEYSTROKE_SYNTAX = [
173
+ '## Keystroke Syntax',
174
+ '',
175
+ '**Modifiers** (combine with `+`):',
176
+ '- `ctrl` (alias: `control`)',
177
+ '- `alt` (aliases: `opt`, `option`) — note: `alt` and `meta` are identical in terminals',
178
+ '- `shift`',
179
+ '- `meta` (aliases: `cmd`, `command`)',
180
+ '',
181
+ '**Special keys**: `escape`/`esc`, `enter`/`return`, `tab`, `space`, `backspace`, `delete`, `up`, `down`, `left`, `right`',
182
+ '',
183
+ '**Chords**: Space-separated keystrokes, e.g. `ctrl+k ctrl+s` (1-second timeout between keystrokes)',
184
+ '',
185
+ '**Examples**: `ctrl+shift+p`, `alt+enter`, `ctrl+k ctrl+n`',
186
+ ].join('\n')
187
+
188
+ const SECTION_UNBINDING = [
189
+ '## Unbinding Default Shortcuts',
190
+ '',
191
+ 'Set a key to `null` to remove its default binding:',
192
+ '',
193
+ '```json',
194
+ jsonStringify(UNBIND_EXAMPLE, null, 2),
195
+ '```',
196
+ ].join('\n')
197
+
198
+ const SECTION_INTERACTION = [
199
+ '## How User Bindings Interact with Defaults',
200
+ '',
201
+ '- User bindings are **additive** — they are appended after the default bindings',
202
+ '- To **move** a binding to a different key: unbind the old key (`null`) AND add the new binding',
203
+ "- A context only needs to appear in the user's file if they want to change something in that context",
204
+ ].join('\n')
205
+
206
+ const SECTION_COMMON_PATTERNS = [
207
+ '## Common Patterns',
208
+ '',
209
+ '### Rebind a key',
210
+ 'To change the external editor shortcut from `ctrl+g` to `ctrl+e`:',
211
+ '```json',
212
+ jsonStringify(REBIND_EXAMPLE, null, 2),
213
+ '```',
214
+ '',
215
+ '### Add a chord binding',
216
+ '```json',
217
+ jsonStringify(CHORD_EXAMPLE, null, 2),
218
+ '```',
219
+ ].join('\n')
220
+
221
+ const SECTION_BEHAVIORAL_RULES = [
222
+ '## Behavioral Rules',
223
+ '',
224
+ '1. Only include contexts the user wants to change (minimal overrides)',
225
+ '2. Validate that actions and contexts are from the known lists below',
226
+ '3. Warn the user proactively if they choose a key that conflicts with reserved shortcuts or common tools like tmux (`ctrl+b`) and screen (`ctrl+a`)',
227
+ '4. When adding a new binding for an existing action, the new binding is additive (existing default still works unless explicitly unbound)',
228
+ '5. To fully replace a default binding, unbind the old key AND add the new one',
229
+ ].join('\n')
230
+
231
+ const SECTION_DOCTOR = [
232
+ '## Validation with /doctor',
233
+ '',
234
+ 'The `/doctor` command includes a "Keybinding Configuration Issues" section that validates `~/.claude/keybindings.json`.',
235
+ '',
236
+ '### Common Issues and Fixes',
237
+ '',
238
+ markdownTable(
239
+ ['Issue', 'Cause', 'Fix'],
240
+ [
241
+ [
242
+ '`keybindings.json must have a "bindings" array`',
243
+ 'Missing wrapper object',
244
+ 'Wrap bindings in `{ "bindings": [...] }`',
245
+ ],
246
+ [
247
+ '`"bindings" must be an array`',
248
+ '`bindings` is not an array',
249
+ 'Set `"bindings"` to an array: `[{ context: ..., bindings: ... }]`',
250
+ ],
251
+ [
252
+ '`Unknown context "X"`',
253
+ 'Typo or invalid context name',
254
+ 'Use exact context names from the Available Contexts table',
255
+ ],
256
+ [
257
+ '`Duplicate key "X" in Y bindings`',
258
+ 'Same key defined twice in one context',
259
+ 'Remove the duplicate; JSON uses only the last value',
260
+ ],
261
+ [
262
+ '`"X" may not work: ...`',
263
+ 'Key conflicts with terminal/OS reserved shortcut',
264
+ 'Choose a different key (see Reserved Shortcuts section)',
265
+ ],
266
+ [
267
+ '`Could not parse keystroke "X"`',
268
+ 'Invalid key syntax',
269
+ 'Check syntax: use `+` between modifiers, valid key names',
270
+ ],
271
+ [
272
+ '`Invalid action for "X"`',
273
+ 'Action value is not a string or null',
274
+ 'Actions must be strings like `"app:help"` or `null` to unbind',
275
+ ],
276
+ ],
277
+ ),
278
+ '',
279
+ '### Example /doctor Output',
280
+ '',
281
+ '```',
282
+ 'Keybinding Configuration Issues',
283
+ 'Location: ~/.claude/keybindings.json',
284
+ ' └ [Error] Unknown context "chat"',
285
+ ' → Valid contexts: Global, Chat, Autocomplete, ...',
286
+ ' └ [Warning] "ctrl+c" may not work: Terminal interrupt (SIGINT)',
287
+ '```',
288
+ '',
289
+ '**Errors** prevent bindings from working and must be fixed. **Warnings** indicate potential conflicts but the binding may still work.',
290
+ ].join('\n')
291
+
292
+ export function registerKeybindingsSkill(): void {
293
+ registerBundledSkill({
294
+ name: 'keybindings-help',
295
+ description:
296
+ 'Use when the user wants to customize keyboard shortcuts, rebind keys, add chord bindings, or modify ~/.claude/keybindings.json. Examples: "rebind ctrl+s", "add a chord shortcut", "change the submit key", "customize keybindings".',
297
+ allowedTools: ['Read'],
298
+ userInvocable: false,
299
+ isEnabled: isKeybindingCustomizationEnabled,
300
+ async getPromptForCommand(args) {
301
+ // Generate reference tables dynamically from source-of-truth arrays
302
+ const contextsTable = generateContextsTable()
303
+ const actionsTable = generateActionsTable()
304
+ const reservedShortcuts = generateReservedShortcuts()
305
+
306
+ const sections = [
307
+ SECTION_INTRO,
308
+ SECTION_FILE_FORMAT,
309
+ SECTION_KEYSTROKE_SYNTAX,
310
+ SECTION_UNBINDING,
311
+ SECTION_INTERACTION,
312
+ SECTION_COMMON_PATTERNS,
313
+ SECTION_BEHAVIORAL_RULES,
314
+ SECTION_DOCTOR,
315
+ `## Reserved Shortcuts\n\n${reservedShortcuts}`,
316
+ `## Available Contexts\n\n${contextsTable}`,
317
+ `## Available Actions\n\n${actionsTable}`,
318
+ ]
319
+
320
+ if (args) {
321
+ sections.push(`## User Request\n\n${args}`)
322
+ }
323
+
324
+ return [{ type: 'text', text: sections.join('\n\n') }]
325
+ },
326
+ })
327
+ }
328
+
329
+ /**
330
+ * Build a markdown table from headers and rows.
331
+ */
332
+ function markdownTable(headers: string[], rows: string[][]): string {
333
+ const separator = headers.map(() => '---')
334
+ return [
335
+ `| ${headers.join(' | ')} |`,
336
+ `| ${separator.join(' | ')} |`,
337
+ ...rows.map(row => `| ${row.join(' | ')} |`),
338
+ ].join('\n')
339
+ }
@@ -0,0 +1,92 @@
1
+ import {
2
+ CRON_CREATE_TOOL_NAME,
3
+ CRON_DELETE_TOOL_NAME,
4
+ DEFAULT_MAX_AGE_DAYS,
5
+ isKairosCronEnabled,
6
+ } from '../../tools/ScheduleCronTool/prompt.js'
7
+ import { registerBundledSkill } from '../bundledSkills.js'
8
+
9
+ const DEFAULT_INTERVAL = '10m'
10
+
11
+ const USAGE_MESSAGE = `Usage: /loop [interval] <prompt>
12
+
13
+ Run a prompt or slash command on a recurring interval.
14
+
15
+ Intervals: Ns, Nm, Nh, Nd (e.g. 5m, 30m, 2h, 1d). Minimum granularity is 1 minute.
16
+ If no interval is specified, defaults to ${DEFAULT_INTERVAL}.
17
+
18
+ Examples:
19
+ /loop 5m /babysit-prs
20
+ /loop 30m check the deploy
21
+ /loop 1h /standup 1
22
+ /loop check the deploy (defaults to ${DEFAULT_INTERVAL})
23
+ /loop check the deploy every 20m`
24
+
25
+ function buildPrompt(args: string): string {
26
+ return `# /loop — schedule a recurring prompt
27
+
28
+ Parse the input below into \`[interval] <prompt…>\` and schedule it with ${CRON_CREATE_TOOL_NAME}.
29
+
30
+ ## Parsing (in priority order)
31
+
32
+ 1. **Leading token**: if the first whitespace-delimited token matches \`^\\d+[smhd]$\` (e.g. \`5m\`, \`2h\`), that's the interval; the rest is the prompt.
33
+ 2. **Trailing "every" clause**: otherwise, if the input ends with \`every <N><unit>\` or \`every <N> <unit-word>\` (e.g. \`every 20m\`, \`every 5 minutes\`, \`every 2 hours\`), extract that as the interval and strip it from the prompt. Only match when what follows "every" is a time expression — \`check every PR\` has no interval.
34
+ 3. **Default**: otherwise, interval is \`${DEFAULT_INTERVAL}\` and the entire input is the prompt.
35
+
36
+ If the resulting prompt is empty, show usage \`/loop [interval] <prompt>\` and stop — do not call ${CRON_CREATE_TOOL_NAME}.
37
+
38
+ Examples:
39
+ - \`5m /babysit-prs\` → interval \`5m\`, prompt \`/babysit-prs\` (rule 1)
40
+ - \`check the deploy every 20m\` → interval \`20m\`, prompt \`check the deploy\` (rule 2)
41
+ - \`run tests every 5 minutes\` → interval \`5m\`, prompt \`run tests\` (rule 2)
42
+ - \`check the deploy\` → interval \`${DEFAULT_INTERVAL}\`, prompt \`check the deploy\` (rule 3)
43
+ - \`check every PR\` → interval \`${DEFAULT_INTERVAL}\`, prompt \`check every PR\` (rule 3 — "every" not followed by time)
44
+ - \`5m\` → empty prompt → show usage
45
+
46
+ ## Interval → cron
47
+
48
+ Supported suffixes: \`s\` (seconds, rounded up to nearest minute, min 1), \`m\` (minutes), \`h\` (hours), \`d\` (days). Convert:
49
+
50
+ | Interval pattern | Cron expression | Notes |
51
+ |-----------------------|---------------------|------------------------------------------|
52
+ | \`Nm\` where N ≤ 59 | \`*/N * * * *\` | every N minutes |
53
+ | \`Nm\` where N ≥ 60 | \`0 */H * * *\` | round to hours (H = N/60, must divide 24)|
54
+ | \`Nh\` where N ≤ 23 | \`0 */N * * *\` | every N hours |
55
+ | \`Nd\` | \`0 0 */N * *\` | every N days at midnight local |
56
+ | \`Ns\` | treat as \`ceil(N/60)m\` | cron minimum granularity is 1 minute |
57
+
58
+ **If the interval doesn't cleanly divide its unit** (e.g. \`7m\` → \`*/7 * * * *\` gives uneven gaps at :56→:00; \`90m\` → 1.5h which cron can't express), pick the nearest clean interval and tell the user what you rounded to before scheduling.
59
+
60
+ ## Action
61
+
62
+ 1. Call ${CRON_CREATE_TOOL_NAME} with:
63
+ - \`cron\`: the expression from the table above
64
+ - \`prompt\`: the parsed prompt from above, verbatim (slash commands are passed through unchanged)
65
+ - \`recurring\`: \`true\`
66
+ 2. Briefly confirm: what's scheduled, the cron expression, the human-readable cadence, that recurring tasks auto-expire after ${DEFAULT_MAX_AGE_DAYS} days, and that they can cancel sooner with ${CRON_DELETE_TOOL_NAME} (include the job ID).
67
+ 3. **Then immediately execute the parsed prompt now** — don't wait for the first cron fire. If it's a slash command, invoke it via the Skill tool; otherwise act on it directly.
68
+
69
+ ## Input
70
+
71
+ ${args}`
72
+ }
73
+
74
+ export function registerLoopSkill(): void {
75
+ registerBundledSkill({
76
+ name: 'loop',
77
+ description:
78
+ 'Run a prompt or slash command on a recurring interval (e.g. /loop 5m /foo, defaults to 10m)',
79
+ whenToUse:
80
+ 'When the user wants to set up a recurring task, poll for status, or run something repeatedly on an interval (e.g. "check the deploy every 5 minutes", "keep running /babysit-prs"). Do NOT invoke for one-off tasks.',
81
+ argumentHint: '[interval] <prompt>',
82
+ userInvocable: true,
83
+ isEnabled: isKairosCronEnabled,
84
+ async getPromptForCommand(args) {
85
+ const trimmed = args.trim()
86
+ if (!trimmed) {
87
+ return [{ type: 'text', text: USAGE_MESSAGE }]
88
+ }
89
+ return [{ type: 'text', text: buildPrompt(trimmed) }]
90
+ },
91
+ })
92
+ }