@xelauvas/xela-cli 0.3.0 → 0.4.0

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 (136) hide show
  1. package/local/README.md +112 -0
  2. package/local/start-qwen.sh +47 -0
  3. package/local/start-tunnel.sh +36 -0
  4. package/local/xela-local.sh +139 -0
  5. package/package.json +19 -5
  6. package/run.sh +33 -0
  7. package/src/Tool.ts +1 -1
  8. package/src/_shims/_generated_stubs/_universal_stub.mjs +168 -0
  9. package/src/_shims/_generated_stubs/stub_346f96741420f62caece15b196e233c6.mjs +19 -0
  10. package/src/_shims/_generated_stubs/stub_4b79af2a6ac1af5a53a55fd666b2bbb4.mjs +3 -0
  11. package/src/_shims/_generated_stubs/stub_94345ddeadacc184ff895597ec5abf71.mjs +6 -0
  12. package/src/_shims/_generated_stubs/stub_99fa0bc272643c549d8a2ec6d578436f.mjs +3 -0
  13. package/src/_shims/_generated_stubs/stub_e3451b131fcaf272a2a75270b9a427a3.mjs +9 -0
  14. package/src/_shims/_generated_stubs/text_stub.mjs +1 -0
  15. package/src/_shims/bun_bundle.js +79 -4
  16. package/src/_shims/cjs_stub.cjs +32 -10
  17. package/src/_shims/loader.js +104 -5
  18. package/src/_shims/openai_adapter.ts +446 -230
  19. package/src/_shims/register.js +16 -3
  20. package/src/assistant/gate.ts +15 -0
  21. package/src/assistant/index.ts +89 -0
  22. package/src/bootstrap/state.ts +6 -1
  23. package/src/bridge/peerSessions.ts +18 -0
  24. package/src/cli/print.ts +5 -0
  25. package/src/commands/assistant/assistant.ts +17 -0
  26. package/src/commands/assistant/index.ts +11 -0
  27. package/src/commands/brief.ts +3 -1
  28. package/src/commands/buddy/buddy.ts +88 -0
  29. package/src/commands/buddy/index.ts +12 -0
  30. package/src/commands/model/model.tsx +8 -23
  31. package/src/commands/peers/index.ts +11 -0
  32. package/src/commands/peers/peers.ts +24 -0
  33. package/src/commands/proactive/proactive.ts +64 -0
  34. package/src/commands/proactive.ts +12 -0
  35. package/src/commands/workflows/index.ts +12 -0
  36. package/src/commands/workflows/workflows.ts +36 -0
  37. package/src/commands.ts +4 -8
  38. package/src/components/Messages.tsx +1 -1
  39. package/src/components/ModelPicker.tsx +4 -34
  40. package/src/components/Spinner.tsx +54 -1
  41. package/src/components/StructuredDiff/colorDiff.ts +3 -1
  42. package/src/components/messages/SnipBoundaryMessage.tsx +15 -0
  43. package/src/components/messages/UserCrossSessionMessage.tsx +26 -0
  44. package/src/components/permissions/PermissionRequest.tsx +4 -2
  45. package/src/components/permissions/ReviewArtifactPermissionRequest/ReviewArtifactPermissionRequest.tsx +96 -0
  46. package/src/constants/prompts.ts +26 -0
  47. package/src/ink/terminal.ts +9 -1
  48. package/src/jobs/classifier.ts +103 -0
  49. package/src/memdir/paths.ts +13 -1
  50. package/src/proactive/index.ts +158 -0
  51. package/src/proactive/useProactive.ts +78 -0
  52. package/src/screens/REPL.tsx +28 -8
  53. package/src/services/api/claude.ts +6 -1
  54. package/src/services/compact/autoCompact.ts +18 -4
  55. package/src/services/compact/cachedMCConfig.ts +1 -0
  56. package/src/services/compact/cachedMicrocompact.ts +113 -0
  57. package/src/services/compact/compact.ts +22 -2
  58. package/src/services/compact/reactiveCompact.ts +251 -0
  59. package/src/services/compact/snipCompact.ts +282 -0
  60. package/src/services/compact/snipProjection.ts +50 -0
  61. package/src/services/contextCollapse/index.ts +322 -0
  62. package/src/services/contextCollapse/operations.ts +187 -0
  63. package/src/services/contextCollapse/persist.ts +40 -0
  64. package/src/services/extractMemories/extractMemories.ts +11 -2
  65. package/src/services/skillSearch/featureCheck.ts +12 -0
  66. package/src/services/skillSearch/localSearch.ts +98 -0
  67. package/src/services/skillSearch/prefetch.ts +125 -0
  68. package/src/services/skillSearch/remoteSkillLoader.ts +9 -0
  69. package/src/services/skillSearch/remoteSkillState.ts +21 -0
  70. package/src/services/skillSearch/signals.ts +7 -0
  71. package/src/services/skillSearch/telemetry.ts +9 -0
  72. package/src/setup.ts +10 -5
  73. package/src/skills/bundled/dream.ts +31 -0
  74. package/src/skills/bundled/hunter.ts +45 -0
  75. package/src/skills/bundled/index.ts +10 -23
  76. package/src/skills/bundled/runSkillGenerator.ts +61 -0
  77. package/src/skills/mcpSkills.ts +101 -0
  78. package/src/tasks/LocalWorkflowTask/LocalWorkflowTask.ts +301 -0
  79. package/src/tasks/MonitorMcpTask/MonitorMcpTask.ts +273 -0
  80. package/src/tasks.ts +22 -8
  81. package/src/tools/AgentTool/runAgent.ts +11 -0
  82. package/src/tools/BriefTool/BriefTool.ts +2 -0
  83. package/src/tools/CtxInspectTool/CtxInspectTool.ts +102 -0
  84. package/src/tools/DiscoverSkillsTool/DiscoverSkillsTool.ts +78 -0
  85. package/src/tools/DiscoverSkillsTool/prompt.ts +3 -0
  86. package/src/tools/ListPeersTool/ListPeersTool.ts +89 -0
  87. package/src/tools/MCPTool/MCPTool.ts +6 -0
  88. package/src/tools/MonitorTool/MonitorTool.ts +273 -0
  89. package/src/tools/MonitorTool/UI.tsx +25 -0
  90. package/src/tools/MonitorTool/events.ts +84 -0
  91. package/src/tools/MonitorTool/prompt.ts +45 -0
  92. package/src/tools/OverflowTestTool/OverflowTestTool.ts +73 -0
  93. package/src/tools/PushNotificationTool/PushNotificationTool.ts +119 -0
  94. package/src/tools/ReviewArtifactTool/ReviewArtifactTool.ts +93 -0
  95. package/src/tools/ReviewArtifactTool/prompt.ts +5 -0
  96. package/src/tools/SendUserFileTool/SendUserFileTool.ts +87 -0
  97. package/src/tools/SendUserFileTool/prompt.ts +7 -0
  98. package/src/tools/SleepTool/SleepTool.ts +126 -0
  99. package/src/tools/SnipTool/SnipTool.ts +90 -0
  100. package/src/tools/SnipTool/prompt.ts +5 -0
  101. package/src/tools/SubscribePRTool/SubscribePRTool.ts +115 -0
  102. package/src/tools/TerminalCaptureTool/TerminalCaptureTool.ts +129 -0
  103. package/src/tools/TerminalCaptureTool/prompt.ts +8 -0
  104. package/src/tools/ToolSearchTool/prompt.ts +1 -1
  105. package/src/tools/VerifyPlanExecutionTool/VerifyPlanExecutionTool.ts +99 -0
  106. package/src/tools/WebBrowserTool/WebBrowserPanel.tsx +10 -0
  107. package/src/tools/WebBrowserTool/WebBrowserTool.ts +190 -0
  108. package/src/tools/WebBrowserTool/prompt.ts +16 -0
  109. package/src/tools/WorkflowTool/UI.tsx +27 -0
  110. package/src/tools/WorkflowTool/WorkflowTool.ts +401 -0
  111. package/src/tools/WorkflowTool/bundled/index.ts +88 -0
  112. package/src/tools/WorkflowTool/constants.ts +7 -0
  113. package/src/tools/WorkflowTool/createWorkflowCommand.ts +84 -0
  114. package/src/tools/WorkflowTool/prompt.ts +38 -0
  115. package/src/tools/WorkflowTool/runtime.ts +331 -0
  116. package/src/tools.ts +36 -31
  117. package/src/utils/betas.ts +5 -0
  118. package/src/utils/context.ts +9 -0
  119. package/src/utils/conversationRecovery.ts +1 -1
  120. package/src/utils/forkedAgent.ts +4 -0
  121. package/src/utils/logoV2Utils.ts +7 -3
  122. package/src/utils/messageQueueManager.ts +23 -9
  123. package/src/utils/model/model.ts +10 -9
  124. package/src/utils/model/modelOptions.ts +0 -21
  125. package/src/utils/model/validateModel.ts +0 -19
  126. package/src/utils/permissions/permissionSetup.ts +5 -0
  127. package/src/utils/permissions/permissions.ts +25 -0
  128. package/src/utils/permissions/yolo-classifier-prompts/auto_mode_system_prompt.txt +38 -0
  129. package/src/utils/permissions/yolo-classifier-prompts/permissions_external.txt +25 -0
  130. package/src/utils/permissions/yoloClassifier.ts +28 -6
  131. package/src/utils/plugins/officialMarketplaceStartupCheck.ts +9 -0
  132. package/src/utils/systemThemeWatcher.ts +62 -0
  133. package/src/utils/udsClient.ts +115 -0
  134. package/src/utils/udsMessaging.ts +157 -0
  135. package/src/utils/yaml.ts +251 -5
  136. package/src/utils/model/providerModels.ts +0 -168
@@ -0,0 +1,45 @@
1
+ export const MONITOR_TOOL_NAME = 'Monitor'
2
+
3
+ export const DESCRIPTION = `Start a background monitor that streams events from a long-running script. Each stdout line is an event — you keep working and notifications arrive in the chat. Events arrive on their own schedule and are not replies from the user.
4
+
5
+ Pick by how many notifications you need:
6
+ - **One** ("tell me when the server is ready / the build finishes") → use **Bash with \`run_in_background\`** and a command that exits when the condition is true, e.g. \`until grep -q "Ready in" dev.log; do sleep 0.5; done\`. You get a single completion notification when it exits.
7
+ - **One per occurrence, until the monitor expires** ("tell me every time an ERROR line appears") → Monitor with an unbounded command (\`tail -f\`, \`inotifywait -m\`, \`while true\`).
8
+ - **One per occurrence, until a known end** ("emit each CI step result, stop when the run completes") → Monitor with a command that emits lines and then exits.
9
+
10
+ Your script's stdout is the event stream. Each line becomes a notification. Exit ends the watch.
11
+
12
+ # Each matching log line is an event
13
+ tail -f /var/log/app.log | grep --line-buffered "ERROR"
14
+
15
+ # Each file change is an event
16
+ inotifywait -m --format '%e %f' /watched/dir
17
+
18
+ # Poll and emit one line per new item
19
+ last=$(date -u +%Y-%m-%dT%H:%M:%SZ)
20
+ while true; do
21
+ now=$(date -u +%Y-%m-%dT%H:%M:%SZ)
22
+ gh api "repos/owner/repo/issues/123/comments?since=$last" --jq '.[] | "\\(.user.login): \\(.body)"'
23
+ last=$now; sleep 30
24
+ done
25
+
26
+ **Don't use an unbounded command for a single notification.** \`tail -f\`, \`inotifywait -m\` and \`while true\` never exit on their own, so the monitor stays armed until timeout even after the event has fired.
27
+
28
+ **Script quality:**
29
+ - Every pipe stage must flush per line: \`grep\` needs \`--line-buffered\`, \`awk\` needs \`fflush()\`. \`head\` cannot flush at all.
30
+ - In poll loops, handle transient failures (\`curl ... || true\`) — one failed request shouldn't kill the monitor.
31
+ - Poll intervals: 30s+ for remote APIs, 0.5-1s for local checks.
32
+ - Write a specific \`description\` — it appears in every notification ("errors in deploy.log", not "watching logs").
33
+ - Only stdout is the event stream. Merge stderr with \`2>&1\` when its failures matter.
34
+
35
+ **Coverage — silence is not success.** Your filter must match every terminal state, not just the happy path: a monitor that greps only for the success marker stays silent through a crash. Widen the alternation: \`grep -E --line-buffered "elapsed_steps=|Traceback|Error|FAILED|Killed|OOM"\`.
36
+
37
+ **Output volume**: every stdout line is a conversation message, so filter to the lines you would act on. Monitors that produce too many events are stopped automatically; restart with a tighter filter.
38
+
39
+ Lines within 200ms are batched into a single notification. Every monitor expires after \`timeout_ms\` (default 5 minutes, at most 30 minutes); you get one notice at expiry and can re-arm. Use TaskStop to cancel early.
40
+
41
+ **ws source** — open a WebSocket and stream each incoming text frame as an event, no shell and no polling:
42
+
43
+ Monitor({ ws: { url: 'wss://events.example.com/stream' }, description: 'deploy events' })
44
+
45
+ Each text frame becomes one notification. Socket close ends the watch with the close code surfaced.`
@@ -0,0 +1,73 @@
1
+ import { z } from 'zod/v4'
2
+ import { buildTool } from '../../Tool.js'
3
+ import { lazySchema } from '../../utils/lazySchema.js'
4
+
5
+ export const OVERFLOW_TEST_TOOL_NAME = 'OverflowTest'
6
+
7
+ const MAX_KB = 4096
8
+
9
+ const inputSchema = lazySchema(() =>
10
+ z.strictObject({
11
+ size_kb: z.number().int().positive().max(MAX_KB).default(256).describe('How many kilobytes of text to return'),
12
+ marker: z.string().max(40).default('OVERFLOW').describe('Token repeated in the output, to check what survives truncation or compaction'),
13
+ }),
14
+ )
15
+ type InputSchema = ReturnType<typeof inputSchema>
16
+
17
+ const outputSchema = lazySchema(() => z.object({ text: z.string(), bytes: z.number() }))
18
+ type OutputSchema = ReturnType<typeof outputSchema>
19
+ export type Output = z.infer<OutputSchema>
20
+
21
+ /**
22
+ * Developer tool: returns a large, numbered payload so context-overflow
23
+ * handling (truncation, microcompact, reactive compact) can be exercised
24
+ * without a real workload. Gated behind OVERFLOW_TEST_TOOL.
25
+ */
26
+ export const OverflowTestTool = buildTool({
27
+ name: OVERFLOW_TEST_TOOL_NAME,
28
+ searchHint: 'generate a large tool result for overflow testing',
29
+ maxResultSizeChars: MAX_KB * 1024 + 100,
30
+ userFacingName: () => 'Overflow test',
31
+ get inputSchema(): InputSchema {
32
+ return inputSchema()
33
+ },
34
+ get outputSchema(): OutputSchema {
35
+ return outputSchema()
36
+ },
37
+ isConcurrencySafe() {
38
+ return true
39
+ },
40
+ isReadOnly() {
41
+ return true
42
+ },
43
+ async checkPermissions(input) {
44
+ return { behavior: 'allow', updatedInput: input }
45
+ },
46
+ async description({ size_kb }) {
47
+ return `Generate ${size_kb} KB of test output`
48
+ },
49
+ async prompt() {
50
+ return 'Testing tool: returns the requested number of kilobytes of numbered filler lines. Only use it when explicitly asked to test context overflow behaviour.'
51
+ },
52
+ mapToolResultToToolResultBlockParam(output, toolUseID) {
53
+ return { tool_use_id: toolUseID, type: 'tool_result', content: output.text }
54
+ },
55
+ renderToolUseMessage(input: { size_kb?: number }) {
56
+ return `${input.size_kb ?? 256} KB`
57
+ },
58
+ renderToolResultMessage(output: Output) {
59
+ return `${Math.round(output.bytes / 1024)} KB generated`
60
+ },
61
+ async call({ size_kb, marker }) {
62
+ const target = size_kb * 1024
63
+ const lines: string[] = []
64
+ let bytes = 0
65
+ for (let i = 1; bytes < target; i++) {
66
+ const line = `${marker} line ${i.toString().padStart(7, '0')} ${'x'.repeat(48)}\n`
67
+ lines.push(line)
68
+ bytes += line.length
69
+ }
70
+ const text = lines.join('')
71
+ return { data: { text, bytes: text.length } }
72
+ },
73
+ })
@@ -0,0 +1,119 @@
1
+ import { spawn } from 'child_process'
2
+ import { z } from 'zod/v4'
3
+ import { buildTool } from '../../Tool.js'
4
+ import { lazySchema } from '../../utils/lazySchema.js'
5
+
6
+ export const PUSH_NOTIFICATION_TOOL_NAME = 'PushNotification'
7
+
8
+ const PROMPT = `Send a desktop notification to the user, for moments they would want to be interrupted for: a long task finished, a decision is needed, something broke. Keep the title under 50 characters and the body to one or two sentences. Do not use it for routine progress; the transcript already shows that.`
9
+
10
+ const inputSchema = lazySchema(() =>
11
+ z.strictObject({
12
+ title: z.string().max(120).describe('Notification title'),
13
+ body: z.string().max(1000).optional().describe('Notification body'),
14
+ }),
15
+ )
16
+ type InputSchema = ReturnType<typeof inputSchema>
17
+
18
+ const outputSchema = lazySchema(() =>
19
+ z.object({ channels: z.array(z.string()), message: z.string() }),
20
+ )
21
+ type OutputSchema = ReturnType<typeof outputSchema>
22
+ export type Output = z.infer<OutputSchema>
23
+
24
+ function run(cmd: string, args: string[], timeoutMs = 4000): Promise<boolean> {
25
+ return new Promise(resolve => {
26
+ try {
27
+ const child = spawn(cmd, args, { stdio: 'ignore' })
28
+ const t = setTimeout(() => {
29
+ child.kill()
30
+ resolve(false)
31
+ }, timeoutMs)
32
+ child.on('error', () => {
33
+ clearTimeout(t)
34
+ resolve(false)
35
+ })
36
+ child.on('exit', code => {
37
+ clearTimeout(t)
38
+ resolve(code === 0)
39
+ })
40
+ } catch {
41
+ resolve(false)
42
+ }
43
+ })
44
+ }
45
+
46
+ function isWsl(): boolean {
47
+ return !!process.env.WSL_DISTRO_NAME || !!process.env.WSL_INTEROP
48
+ }
49
+
50
+ /**
51
+ * Best effort across environments: terminal notification escape (OSC 9 /
52
+ * OSC 777, understood by Windows Terminal, iTerm2, kitty, WezTerm...), then a
53
+ * native notifier when one exists. Returns the channels that accepted it.
54
+ */
55
+ export async function sendDesktopNotification(title: string, body: string): Promise<string[]> {
56
+ const channels: string[] = []
57
+ if (process.stdout.isTTY) {
58
+ const text = body ? `${title}: ${body}` : title
59
+ process.stdout.write(`\u001b]9;${text}\u0007`)
60
+ process.stdout.write(`\u001b]777;notify;${title};${body}\u0007`)
61
+ process.stdout.write('\u0007')
62
+ channels.push('terminal')
63
+ }
64
+ if (process.platform === 'darwin') {
65
+ const script = `display notification ${JSON.stringify(body)} with title ${JSON.stringify(title)}`
66
+ if (await run('osascript', ['-e', script])) channels.push('macos')
67
+ } else if (process.platform === 'linux') {
68
+ if (isWsl()) {
69
+ const ps = `[Windows.UI.Notifications.ToastNotificationManager, Windows.UI.Notifications, ContentType = WindowsRuntime] | Out-Null; $t=[Windows.UI.Notifications.ToastNotificationManager]::GetTemplateContent([Windows.UI.Notifications.ToastTemplateType]::ToastText02); $n=$t.GetElementsByTagName('text'); $n.Item(0).AppendChild($t.CreateTextNode(${JSON.stringify(title)})) | Out-Null; $n.Item(1).AppendChild($t.CreateTextNode(${JSON.stringify(body)})) | Out-Null; [Windows.UI.Notifications.ToastNotificationManager]::CreateToastNotifier('Xela').Show([Windows.UI.Notifications.ToastNotification]::new($t))`
70
+ if (await run('powershell.exe', ['-NoProfile', '-Command', ps], 8000)) channels.push('windows-toast')
71
+ }
72
+ if (await run('notify-send', [title, body])) channels.push('notify-send')
73
+ }
74
+ return channels
75
+ }
76
+
77
+ export const PushNotificationTool = buildTool({
78
+ name: PUSH_NOTIFICATION_TOOL_NAME,
79
+ searchHint: 'desktop notification to the user',
80
+ maxResultSizeChars: 500,
81
+ userFacingName: () => 'Notify',
82
+ get inputSchema(): InputSchema {
83
+ return inputSchema()
84
+ },
85
+ get outputSchema(): OutputSchema {
86
+ return outputSchema()
87
+ },
88
+ isConcurrencySafe() {
89
+ return true
90
+ },
91
+ isReadOnly() {
92
+ return true
93
+ },
94
+ async checkPermissions(input) {
95
+ return { behavior: 'allow', updatedInput: input }
96
+ },
97
+ async description({ title }) {
98
+ return `Notify: ${title}`
99
+ },
100
+ async prompt() {
101
+ return PROMPT
102
+ },
103
+ mapToolResultToToolResultBlockParam(output, toolUseID) {
104
+ return { tool_use_id: toolUseID, type: 'tool_result', content: output.message }
105
+ },
106
+ renderToolUseMessage(input: { title?: string }) {
107
+ return input.title ?? ''
108
+ },
109
+ renderToolResultMessage(output: Output) {
110
+ return output.channels.length ? `sent via ${output.channels.join(', ')}` : 'no notification channel available'
111
+ },
112
+ async call({ title, body }) {
113
+ const channels = await sendDesktopNotification(title, body ?? '')
114
+ const message = channels.length
115
+ ? `Notification sent (${channels.join(', ')}).`
116
+ : 'No notification channel was available; the user will only see this transcript.'
117
+ return { data: { channels, message } }
118
+ },
119
+ })
@@ -0,0 +1,93 @@
1
+ import { mkdirSync, writeFileSync } from 'fs'
2
+ import { dirname, isAbsolute, join, relative, resolve } from 'path'
3
+ import { z } from 'zod/v4'
4
+ import { buildTool } from '../../Tool.js'
5
+ import { getCwd } from '../../utils/cwd.js'
6
+ import { lazySchema } from '../../utils/lazySchema.js'
7
+ import { REVIEW_ARTIFACT_PROMPT, REVIEW_ARTIFACT_TOOL_NAME } from './prompt.js'
8
+
9
+ const inputSchema = lazySchema(() =>
10
+ z.strictObject({
11
+ title: z.string().min(1).max(200).describe('Report title'),
12
+ content: z.string().min(1).describe('Full Markdown body of the report'),
13
+ path: z.string().optional().describe('Where to write it; default .claude/reviews/<date>-<slug>.md'),
14
+ findings: z.number().int().nonnegative().optional().describe('Number of confirmed findings (for the summary line)'),
15
+ severity: z.enum(['none', 'low', 'medium', 'high', 'blocker']).optional().describe('Highest severity found'),
16
+ }),
17
+ )
18
+ type InputSchema = ReturnType<typeof inputSchema>
19
+
20
+ const outputSchema = lazySchema(() =>
21
+ z.object({ path: z.string(), bytes: z.number(), message: z.string() }),
22
+ )
23
+ type OutputSchema = ReturnType<typeof outputSchema>
24
+ export type Output = z.infer<OutputSchema>
25
+
26
+ export function slugify(s: string): string {
27
+ return s
28
+ .toLowerCase()
29
+ .replace(/[^a-z0-9]+/g, '-')
30
+ .replace(/^-+|-+$/g, '')
31
+ .slice(0, 60) || 'review'
32
+ }
33
+
34
+ export function defaultArtifactPath(title: string): string {
35
+ const date = new Date().toISOString().slice(0, 10)
36
+ return join('.claude', 'reviews', `${date}-${slugify(title)}.md`)
37
+ }
38
+
39
+ export function resolveArtifactPath(input: { title: string; path?: string }): string {
40
+ const p = input.path?.trim() || defaultArtifactPath(input.title)
41
+ return isAbsolute(p) ? p : resolve(getCwd(), p)
42
+ }
43
+
44
+ export const ReviewArtifactTool = buildTool({
45
+ name: REVIEW_ARTIFACT_TOOL_NAME,
46
+ searchHint: 'publish a code review report as a Markdown artifact',
47
+ maxResultSizeChars: 2000,
48
+ userFacingName: () => 'Review artifact',
49
+ get inputSchema(): InputSchema {
50
+ return inputSchema()
51
+ },
52
+ get outputSchema(): OutputSchema {
53
+ return outputSchema()
54
+ },
55
+ isConcurrencySafe() {
56
+ return false
57
+ },
58
+ isReadOnly() {
59
+ return false
60
+ },
61
+ async checkPermissions(input) {
62
+ // Always confirmed by the user through ReviewArtifactPermissionRequest.
63
+ return {
64
+ behavior: 'ask',
65
+ message: `Publish review artifact "${input.title}" to ${relative(getCwd(), resolveArtifactPath(input))}`,
66
+ }
67
+ },
68
+ async description({ title }) {
69
+ return `Publish review: ${title}`
70
+ },
71
+ async prompt() {
72
+ return REVIEW_ARTIFACT_PROMPT
73
+ },
74
+ mapToolResultToToolResultBlockParam(output, toolUseID) {
75
+ return { tool_use_id: toolUseID, type: 'tool_result', content: output.message }
76
+ },
77
+ renderToolUseMessage(input: { title?: string; path?: string }) {
78
+ return input.title ?? input.path ?? ''
79
+ },
80
+ renderToolResultMessage(output: Output) {
81
+ return `written to ${output.path}`
82
+ },
83
+ async call(input) {
84
+ const target = resolveArtifactPath(input)
85
+ mkdirSync(dirname(target), { recursive: true })
86
+ const header = `# ${input.title}\n\n_Generated ${new Date().toISOString()}${input.findings !== undefined ? ` · ${input.findings} finding${input.findings === 1 ? '' : 's'}` : ''}${input.severity && input.severity !== 'none' ? ` · highest severity: ${input.severity}` : ''}_\n\n`
87
+ const body = input.content.startsWith('# ') ? input.content : header + input.content
88
+ writeFileSync(target, body.endsWith('\n') ? body : body + '\n')
89
+ const rel = relative(getCwd(), target)
90
+ const message = `Review artifact written to ${rel}${input.findings !== undefined ? ` (${input.findings} finding${input.findings === 1 ? '' : 's'})` : ''}. Tell the user the path and summarize the findings.`
91
+ return { data: { path: rel, bytes: body.length, message } }
92
+ },
93
+ })
@@ -0,0 +1,5 @@
1
+ export const REVIEW_ARTIFACT_TOOL_NAME = 'ReviewArtifact'
2
+
3
+ export const REVIEW_ARTIFACT_PROMPT = `Publish a review artifact: a Markdown report of a code review, bug hunt or audit that the team can read and act on without the transcript.
4
+
5
+ The user is shown the title and a preview and approves before anything is written. Reports go under \`.claude/reviews/\` in the project unless a path is given. Include: scope, findings ordered by severity with file:line references and a concrete failure scenario each, what was checked and found fine, and open questions. Call it once per review, at the end.`
@@ -0,0 +1,87 @@
1
+ import { copyFileSync, existsSync, mkdirSync, statSync } from 'fs'
2
+ import { basename, isAbsolute, join, resolve } from 'path'
3
+ import { z } from 'zod/v4'
4
+ import { buildTool } from '../../Tool.js'
5
+ import { getCwd } from '../../utils/cwd.js'
6
+ import { getClaudeConfigHomeDir } from '../../utils/envUtils.js'
7
+ import { lazySchema } from '../../utils/lazySchema.js'
8
+ import { DESCRIPTION, SEND_USER_FILE_PROMPT, SEND_USER_FILE_TOOL_NAME } from './prompt.js'
9
+
10
+ const inputSchema = lazySchema(() =>
11
+ z.strictObject({
12
+ file_path: z.string().describe('Path of the file to deliver (absolute, or relative to the working directory)'),
13
+ description: z.string().optional().describe('What the file is, in a few words (shown to the user)'),
14
+ }),
15
+ )
16
+ type InputSchema = ReturnType<typeof inputSchema>
17
+
18
+ const outputSchema = lazySchema(() =>
19
+ z.object({
20
+ deliveredPath: z.string(),
21
+ sizeBytes: z.number(),
22
+ message: z.string(),
23
+ }),
24
+ )
25
+ type OutputSchema = ReturnType<typeof outputSchema>
26
+ export type Output = z.infer<OutputSchema>
27
+
28
+ export function getOutboxDir(): string {
29
+ return process.env.CLAUDE_CODE_OUTBOX_DIR || join(getClaudeConfigHomeDir(), 'outbox')
30
+ }
31
+
32
+ function formatSize(n: number): string {
33
+ if (n < 1024) return `${n} B`
34
+ if (n < 1024 * 1024) return `${(n / 1024).toFixed(1)} KB`
35
+ return `${(n / (1024 * 1024)).toFixed(1)} MB`
36
+ }
37
+
38
+ export const SendUserFileTool = buildTool({
39
+ name: SEND_USER_FILE_TOOL_NAME,
40
+ searchHint: 'deliver a produced file to the user',
41
+ maxResultSizeChars: 2000,
42
+ userFacingName: () => 'Send file',
43
+ get inputSchema(): InputSchema {
44
+ return inputSchema()
45
+ },
46
+ get outputSchema(): OutputSchema {
47
+ return outputSchema()
48
+ },
49
+ isConcurrencySafe() {
50
+ return true
51
+ },
52
+ isReadOnly() {
53
+ return false
54
+ },
55
+ async checkPermissions(input) {
56
+ return { behavior: 'allow', updatedInput: input }
57
+ },
58
+ async description({ file_path }) {
59
+ return `${DESCRIPTION}: ${basename(file_path)}`
60
+ },
61
+ async prompt() {
62
+ return SEND_USER_FILE_PROMPT
63
+ },
64
+ mapToolResultToToolResultBlockParam(output, toolUseID) {
65
+ return { tool_use_id: toolUseID, type: 'tool_result', content: output.message }
66
+ },
67
+ renderToolUseMessage(input: { file_path?: string; description?: string }) {
68
+ return `${input.file_path ?? ''}${input.description ? ` · ${input.description}` : ''}`
69
+ },
70
+ renderToolResultMessage(output: Output) {
71
+ return `delivered to ${output.deliveredPath} (${formatSize(output.sizeBytes)})`
72
+ },
73
+ async call({ file_path, description }) {
74
+ const source = isAbsolute(file_path) ? file_path : resolve(getCwd(), file_path)
75
+ if (!existsSync(source) || !statSync(source).isFile()) {
76
+ throw new Error(`File not found: ${source}`)
77
+ }
78
+ const dir = getOutboxDir()
79
+ mkdirSync(dir, { recursive: true })
80
+ const stamp = new Date().toISOString().replace(/[:.]/g, '-').slice(0, 19)
81
+ const target = join(dir, `${stamp}-${basename(source)}`)
82
+ copyFileSync(source, target)
83
+ const sizeBytes = statSync(target).size
84
+ const message = `File delivered to the user: ${target} (${formatSize(sizeBytes)})${description ? ` — ${description}` : ''}. The original stays at ${source}.`
85
+ return { data: { deliveredPath: target, sizeBytes, message } }
86
+ },
87
+ })
@@ -0,0 +1,7 @@
1
+ export const SEND_USER_FILE_TOOL_NAME = 'SendUserFile'
2
+
3
+ export const DESCRIPTION = 'Deliver a file to the user'
4
+
5
+ export const SEND_USER_FILE_PROMPT = `Deliver a file you produced (report, export, screenshot, archive) to the user.
6
+
7
+ The file is copied into the session outbox directory and the user is told where it is, so it survives temporary directories being cleaned up. Use it for deliverables the user asked for, not for every file you touch. Pass a short description so the notification is meaningful.`
@@ -0,0 +1,126 @@
1
+ import { z } from 'zod/v4'
2
+ import { isProactiveActive, scheduleNextTick } from '../../proactive/index.js'
3
+ import { buildTool } from '../../Tool.js'
4
+ import { lazySchema } from '../../utils/lazySchema.js'
5
+ import { DESCRIPTION, SLEEP_TOOL_NAME, SLEEP_TOOL_PROMPT } from './prompt.js'
6
+
7
+ const MAX_SECONDS = 60 * 60
8
+ // Proactive sleeps are capped lower so a user prompt never waits long behind one.
9
+ const PROACTIVE_MAX_SECONDS = 15 * 60
10
+
11
+ const inputSchema = lazySchema(() =>
12
+ z.strictObject({
13
+ seconds: z
14
+ .number()
15
+ .positive()
16
+ .max(MAX_SECONDS)
17
+ .describe(`How long to wait, in seconds (at most ${MAX_SECONDS})`),
18
+ reason: z.string().optional().describe('Why you are waiting (shown to the user)'),
19
+ }),
20
+ )
21
+ type InputSchema = ReturnType<typeof inputSchema>
22
+
23
+ const outputSchema = lazySchema(() =>
24
+ z.object({
25
+ sleptMs: z.number(),
26
+ interrupted: z.boolean(),
27
+ message: z.string(),
28
+ }),
29
+ )
30
+ type OutputSchema = ReturnType<typeof outputSchema>
31
+ export type Output = z.infer<OutputSchema>
32
+
33
+ function waitFor(ms: number, signal: AbortSignal): Promise<boolean> {
34
+ return new Promise<boolean>(resolve => {
35
+ if (signal.aborted) return resolve(true)
36
+ const timer = setTimeout(() => {
37
+ signal.removeEventListener('abort', onAbort)
38
+ resolve(false)
39
+ }, ms)
40
+ const onAbort = (): void => {
41
+ clearTimeout(timer)
42
+ resolve(true)
43
+ }
44
+ signal.addEventListener('abort', onAbort, { once: true })
45
+ })
46
+ }
47
+
48
+ export const SleepTool = buildTool({
49
+ name: SLEEP_TOOL_NAME,
50
+ searchHint: 'wait without holding a shell',
51
+ maxResultSizeChars: 1000,
52
+ userFacingName: () => 'Sleep',
53
+ get inputSchema(): InputSchema {
54
+ return inputSchema()
55
+ },
56
+ get outputSchema(): OutputSchema {
57
+ return outputSchema()
58
+ },
59
+ isConcurrencySafe() {
60
+ return true
61
+ },
62
+ isReadOnly() {
63
+ return true
64
+ },
65
+ // Only offered while proactive mode is running; otherwise the model has no
66
+ // reason to wait and would only burn a turn.
67
+ isEnabled() {
68
+ return isProactiveActive()
69
+ },
70
+ async checkPermissions(input) {
71
+ return { behavior: 'allow', updatedInput: input }
72
+ },
73
+ async description({ seconds, reason }) {
74
+ return `${DESCRIPTION}: ${seconds}s${reason ? ` (${reason})` : ''}`
75
+ },
76
+ async prompt() {
77
+ return SLEEP_TOOL_PROMPT
78
+ },
79
+ mapToolResultToToolResultBlockParam(output, toolUseID) {
80
+ return { tool_use_id: toolUseID, type: 'tool_result', content: output.message }
81
+ },
82
+ renderToolUseMessage(input: { seconds?: number; reason?: string }) {
83
+ return `${input.seconds ?? '?'}s${input.reason ? ` · ${input.reason}` : ''}`
84
+ },
85
+ renderToolResultMessage(output: Output) {
86
+ return output.interrupted ? `interrupted after ${Math.round(output.sleptMs / 1000)}s` : `slept ${Math.round(output.sleptMs / 1000)}s`
87
+ },
88
+ async call({ seconds }, { abortController }) {
89
+ // In proactive mode the wait is the gap until the next <tick>: schedule
90
+ // it and end the turn instead of holding the loop open.
91
+ if (isProactiveActive()) {
92
+ // Proactive mode: really wait (abortable). Returning immediately let a
93
+ // small model keep working in a tight loop instead of ending its turn;
94
+ // blocking makes "do the task, Sleep 60" pace itself at one cycle per
95
+ // minute whatever the model does afterwards. The footer countdown
96
+ // shows the wake-up time; a tick that fires meanwhile is queued.
97
+ const ms = Math.min(seconds, PROACTIVE_MAX_SECONDS) * 1000
98
+ scheduleNextTick(ms)
99
+ const start = Date.now()
100
+ const interrupted = await waitFor(ms, abortController.signal)
101
+ const sleptMs = Date.now() - start
102
+ return {
103
+ data: {
104
+ sleptMs,
105
+ interrupted,
106
+ message: interrupted
107
+ ? `Sleep interrupted after ${Math.round(sleptMs / 1000)}s.`
108
+ : `Slept ${Math.round(sleptMs / 1000)}s. If there is still nothing to do, end your turn now; the next tick will wake you.`,
109
+ },
110
+ }
111
+ }
112
+ const start = Date.now()
113
+ const ms = Math.min(seconds, MAX_SECONDS) * 1000
114
+ const interrupted = await waitFor(ms, abortController.signal)
115
+ const sleptMs = Date.now() - start
116
+ return {
117
+ data: {
118
+ sleptMs,
119
+ interrupted,
120
+ message: interrupted
121
+ ? `Sleep interrupted after ${Math.round(sleptMs / 1000)}s.`
122
+ : `Slept for ${Math.round(sleptMs / 1000)}s.`,
123
+ },
124
+ }
125
+ },
126
+ })
@@ -0,0 +1,90 @@
1
+ import { z } from 'zod/v4'
2
+ import { buildTool } from '../../Tool.js'
3
+ import { lazySchema } from '../../utils/lazySchema.js'
4
+ import { jsonStringify } from '../../utils/slowOperations.js'
5
+ import { DESCRIPTION, SNIP_TOOL_NAME } from './prompt.js'
6
+
7
+ const inputSchema = lazySchema(() =>
8
+ z.strictObject({
9
+ start_id: z
10
+ .string()
11
+ .describe('The [id:...] tag of the first message to remove (just the id, e.g. "3kd92a")'),
12
+ end_id: z
13
+ .string()
14
+ .describe('The [id:...] tag of the last message to remove (inclusive)'),
15
+ reason: z
16
+ .string()
17
+ .optional()
18
+ .describe('One short phrase on why this range is no longer needed'),
19
+ }),
20
+ )
21
+ type InputSchema = ReturnType<typeof inputSchema>
22
+
23
+ const outputSchema = lazySchema(() =>
24
+ z.object({
25
+ scheduled: z.literal(true),
26
+ start_id: z.string(),
27
+ end_id: z.string(),
28
+ message: z.string(),
29
+ }),
30
+ )
31
+ type OutputSchema = ReturnType<typeof outputSchema>
32
+ export type Output = z.infer<OutputSchema>
33
+
34
+ /**
35
+ * The tool call itself is the snip marker: the next query pass
36
+ * (snipCompactIfNeeded) finds Snip tool_use blocks without a matching
37
+ * boundary, removes the tagged range and leaves a boundary behind.
38
+ */
39
+ export const SnipTool = buildTool({
40
+ name: SNIP_TOOL_NAME,
41
+ searchHint: 'drop stale earlier messages from the context window',
42
+ maxResultSizeChars: 10_000,
43
+ userFacingName: () => 'Snip',
44
+ get inputSchema(): InputSchema {
45
+ return inputSchema()
46
+ },
47
+ get outputSchema(): OutputSchema {
48
+ return outputSchema()
49
+ },
50
+ isConcurrencySafe() {
51
+ return true
52
+ },
53
+ isReadOnly() {
54
+ return true
55
+ },
56
+ async checkPermissions(input) {
57
+ return { behavior: 'allow', updatedInput: input }
58
+ },
59
+ async description({ start_id, end_id }) {
60
+ return `Snip messages ${start_id}..${end_id}`
61
+ },
62
+ async prompt() {
63
+ return DESCRIPTION
64
+ },
65
+ mapToolResultToToolResultBlockParam(output, toolUseID) {
66
+ return {
67
+ tool_use_id: toolUseID,
68
+ type: 'tool_result',
69
+ content: jsonStringify(output),
70
+ }
71
+ },
72
+ renderToolUseMessage(input: { start_id?: string; end_id?: string; reason?: string }) {
73
+ return `${input.start_id ?? '?'} → ${input.end_id ?? '?'}${input.reason ? ` · ${input.reason}` : ''}`
74
+ },
75
+ renderToolResultMessage() {
76
+ return null
77
+ },
78
+ async call({ start_id, end_id }) {
79
+ const clean = (s: string): string => s.trim().replace(/^\[?id:/, '').replace(/\]$/, '')
80
+ return {
81
+ data: {
82
+ scheduled: true as const,
83
+ start_id: clean(start_id),
84
+ end_id: clean(end_id),
85
+ message:
86
+ 'Scheduled. The range is removed from your context starting with the next turn; a boundary note will mark where it was.',
87
+ },
88
+ }
89
+ },
90
+ })
@@ -0,0 +1,5 @@
1
+ export const SNIP_TOOL_NAME = 'Snip'
2
+
3
+ export const DESCRIPTION = `Remove a range of earlier messages from your context window when they are no longer needed (stale tool output, exploration that led nowhere, resolved sub-tasks). User messages carry an \`[id:xxxxxx]\` tag; pass the first and last tag of the range you want gone. Everything from start_id through end_id (inclusive) stops being sent to the model from the next turn on, and a short boundary note takes its place. The user still sees the original messages in their scrollback.
4
+
5
+ Use it when the conversation has grown and a chunk of it is clearly done: large file reads you already acted on, failed attempts you have moved past, verbose command output whose conclusion you have noted. Keep anything you may still need to quote or re-check. The most recent turns cannot be snipped. Tool calls and their results are removed together so the transcript stays consistent.`