@bahulam/code 2.6.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.
- package/README.md +80 -0
- package/package.json +49 -0
- package/pulse/app/activity/page.tsx +190 -0
- package/pulse/app/api/activity/route.ts +138 -0
- package/pulse/app/api/benchmark/route.ts +113 -0
- package/pulse/app/api/benchmarks/route.ts +195 -0
- package/pulse/app/api/costs/route.ts +88 -0
- package/pulse/app/api/export/route.ts +77 -0
- package/pulse/app/api/history/route.ts +11 -0
- package/pulse/app/api/import/route.ts +31 -0
- package/pulse/app/api/memory/route.ts +50 -0
- package/pulse/app/api/plans/route.ts +9 -0
- package/pulse/app/api/projects/[slug]/route.ts +96 -0
- package/pulse/app/api/projects/route.ts +121 -0
- package/pulse/app/api/sessions/[id]/replay/route.ts +20 -0
- package/pulse/app/api/sessions/[id]/route.ts +31 -0
- package/pulse/app/api/sessions/route.ts +112 -0
- package/pulse/app/api/settings/route.ts +14 -0
- package/pulse/app/api/stats/route.ts +143 -0
- package/pulse/app/api/todos/route.ts +9 -0
- package/pulse/app/api/tools/route.ts +160 -0
- package/pulse/app/benchmarks/page.tsx +224 -0
- package/pulse/app/costs/page.tsx +179 -0
- package/pulse/app/export/page.tsx +465 -0
- package/pulse/app/favicon.ico +0 -0
- package/pulse/app/globals.css +263 -0
- package/pulse/app/help/page.tsx +143 -0
- package/pulse/app/history/page.tsx +157 -0
- package/pulse/app/layout.tsx +46 -0
- package/pulse/app/memory/page.tsx +365 -0
- package/pulse/app/overview-client.tsx +393 -0
- package/pulse/app/page.tsx +14 -0
- package/pulse/app/plans/page.tsx +308 -0
- package/pulse/app/projects/[slug]/page.tsx +390 -0
- package/pulse/app/projects/page.tsx +110 -0
- package/pulse/app/sessions/[id]/page.tsx +243 -0
- package/pulse/app/sessions/page.tsx +39 -0
- package/pulse/app/settings/page.tsx +188 -0
- package/pulse/app/todos/page.tsx +211 -0
- package/pulse/app/tools/page.tsx +249 -0
- package/pulse/cli.js +164 -0
- package/pulse/components/activity/day-of-week-chart.tsx +35 -0
- package/pulse/components/activity/streak-card.tsx +36 -0
- package/pulse/components/costs/cache-efficiency-panel.tsx +76 -0
- package/pulse/components/costs/cost-by-project-chart.tsx +48 -0
- package/pulse/components/costs/cost-over-time-chart.tsx +95 -0
- package/pulse/components/costs/model-token-table.tsx +60 -0
- package/pulse/components/global-search.tsx +193 -0
- package/pulse/components/keyboard-nav-provider.tsx +23 -0
- package/pulse/components/layout/bottom-nav.tsx +53 -0
- package/pulse/components/layout/client-layout.tsx +31 -0
- package/pulse/components/layout/sidebar-context.tsx +50 -0
- package/pulse/components/layout/sidebar.tsx +183 -0
- package/pulse/components/layout/top-bar.tsx +121 -0
- package/pulse/components/overview/activity-heatmap.tsx +107 -0
- package/pulse/components/overview/conversation-table.tsx +148 -0
- package/pulse/components/overview/model-breakdown-donut.tsx +95 -0
- package/pulse/components/overview/peak-hours-chart.tsx +87 -0
- package/pulse/components/overview/project-activity-donut.tsx +96 -0
- package/pulse/components/overview/stat-card.tsx +102 -0
- package/pulse/components/overview/usage-over-time-chart.tsx +166 -0
- package/pulse/components/projects/project-card.tsx +175 -0
- package/pulse/components/sessions/replay/assistant-markdown.tsx +94 -0
- package/pulse/components/sessions/replay/compaction-card.tsx +25 -0
- package/pulse/components/sessions/replay/session-sidebar.tsx +231 -0
- package/pulse/components/sessions/replay/token-accumulation-chart.tsx +98 -0
- package/pulse/components/sessions/replay/tool-call-badge.tsx +127 -0
- package/pulse/components/sessions/replay/turn-cards.tsx +220 -0
- package/pulse/components/sessions/replay/user-tool-result.tsx +158 -0
- package/pulse/components/sessions/session-badges.tsx +49 -0
- package/pulse/components/sessions/session-table.tsx +299 -0
- package/pulse/components/theme-provider.tsx +44 -0
- package/pulse/components/tools/feature-adoption-table.tsx +58 -0
- package/pulse/components/tools/mcp-server-panel.tsx +45 -0
- package/pulse/components/tools/tool-ranking-chart.tsx +57 -0
- package/pulse/components/tools/version-history-table.tsx +32 -0
- package/pulse/components/ui/alert.tsx +66 -0
- package/pulse/components/ui/badge.tsx +48 -0
- package/pulse/components/ui/breadcrumb.tsx +109 -0
- package/pulse/components/ui/button.tsx +64 -0
- package/pulse/components/ui/calendar.tsx +220 -0
- package/pulse/components/ui/card.tsx +92 -0
- package/pulse/components/ui/command.tsx +158 -0
- package/pulse/components/ui/dialog.tsx +158 -0
- package/pulse/components/ui/input.tsx +21 -0
- package/pulse/components/ui/popover.tsx +89 -0
- package/pulse/components/ui/progress.tsx +31 -0
- package/pulse/components/ui/select.tsx +190 -0
- package/pulse/components/ui/separator.tsx +28 -0
- package/pulse/components/ui/sheet.tsx +143 -0
- package/pulse/components/ui/skeleton.tsx +13 -0
- package/pulse/components/ui/table.tsx +116 -0
- package/pulse/components/ui/tabs.tsx +91 -0
- package/pulse/components/ui/tooltip.tsx +57 -0
- package/pulse/components/use-global-keyboard-nav.ts +79 -0
- package/pulse/components.json +23 -0
- package/pulse/eslint.config.mjs +18 -0
- package/pulse/lib/bahulam-paths.ts +23 -0
- package/pulse/lib/claude-reader.ts +592 -0
- package/pulse/lib/decode.ts +129 -0
- package/pulse/lib/pricing.ts +102 -0
- package/pulse/lib/replay-parser.ts +165 -0
- package/pulse/lib/tool-categories.ts +127 -0
- package/pulse/lib/utils.ts +6 -0
- package/pulse/next-env.d.ts +6 -0
- package/pulse/next.config.ts +16 -0
- package/pulse/package.json +45 -0
- package/pulse/postcss.config.mjs +7 -0
- package/pulse/public/activity.png +0 -0
- package/pulse/public/cc-lens.png +0 -0
- package/pulse/public/command-k.png +0 -0
- package/pulse/public/costs.png +0 -0
- package/pulse/public/dashboard-dark.png +0 -0
- package/pulse/public/dashboard-white.png +0 -0
- package/pulse/public/export.png +0 -0
- package/pulse/public/file.svg +1 -0
- package/pulse/public/globe.svg +1 -0
- package/pulse/public/next.svg +1 -0
- package/pulse/public/projects.png +0 -0
- package/pulse/public/session-chat.png +0 -0
- package/pulse/public/todos.png +0 -0
- package/pulse/public/tools.png +0 -0
- package/pulse/public/vercel.svg +1 -0
- package/pulse/public/window.svg +1 -0
- package/pulse/tsconfig.json +34 -0
- package/pulse/types/claude.ts +294 -0
- package/src/agents/loader.mjs +94 -0
- package/src/agents/multi_workflow_loader.mjs +330 -0
- package/src/agents/parser.mjs +205 -0
- package/src/agents/scaffold.mjs +222 -0
- package/src/agents/teams.mjs +123 -0
- package/src/agents/workflow_loader.mjs +122 -0
- package/src/agents/workflow_scaffold.mjs +249 -0
- package/src/auth/oauth.mjs +220 -0
- package/src/auth/tarang-auth.mjs +306 -0
- package/src/commands/agent.mjs +220 -0
- package/src/commands/workflow.mjs +581 -0
- package/src/config/cli-args.mjs +200 -0
- package/src/config/env.mjs +263 -0
- package/src/config/hook-runner.mjs +100 -0
- package/src/config/memory-loader.mjs +32 -0
- package/src/config/settings-loader.mjs +45 -0
- package/src/config/settings.mjs +132 -0
- package/src/context/ast-parser.mjs +298 -0
- package/src/context/bm25.mjs +85 -0
- package/src/context/retriever.mjs +308 -0
- package/src/context/skeleton.mjs +134 -0
- package/src/context/symbol-indexer.mjs +375 -0
- package/src/core/agent-history.mjs +111 -0
- package/src/core/agent-loop.mjs +486 -0
- package/src/core/approval-log.mjs +104 -0
- package/src/core/approval.mjs +476 -0
- package/src/core/attachments.mjs +380 -0
- package/src/core/backend-url.mjs +55 -0
- package/src/core/cache-control.mjs +92 -0
- package/src/core/cache.mjs +105 -0
- package/src/core/callback-client.mjs +180 -0
- package/src/core/checkpoints.mjs +142 -0
- package/src/core/compact-history.mjs +127 -0
- package/src/core/context-envelope.mjs +54 -0
- package/src/core/context-manager.mjs +198 -0
- package/src/core/error-guidance.mjs +311 -0
- package/src/core/file-diff.mjs +217 -0
- package/src/core/headless.mjs +448 -0
- package/src/core/hooks-manager.mjs +87 -0
- package/src/core/jsonl-writer.mjs +449 -0
- package/src/core/local-agent.mjs +537 -0
- package/src/core/local-store.mjs +836 -0
- package/src/core/mode-selector.mjs +51 -0
- package/src/core/output-filter.mjs +177 -0
- package/src/core/paths.mjs +190 -0
- package/src/core/policy-resolver.mjs +156 -0
- package/src/core/pricing.mjs +336 -0
- package/src/core/project-artifacts.mjs +39 -0
- package/src/core/project-context-loader.mjs +139 -0
- package/src/core/providers.mjs +219 -0
- package/src/core/rate-limit-display.mjs +121 -0
- package/src/core/rate-limiter.mjs +119 -0
- package/src/core/resume-mode.mjs +192 -0
- package/src/core/risk-tier.mjs +337 -0
- package/src/core/safety.mjs +203 -0
- package/src/core/scheduler.mjs +173 -0
- package/src/core/session-manager.mjs +360 -0
- package/src/core/session.mjs +143 -0
- package/src/core/settings-sync.mjs +85 -0
- package/src/core/stagnation.mjs +57 -0
- package/src/core/stream-client.mjs +829 -0
- package/src/core/streaming.mjs +182 -0
- package/src/core/system-prompt.mjs +140 -0
- package/src/core/tasks.mjs +196 -0
- package/src/core/tool-executor.mjs +1950 -0
- package/src/core/trust.mjs +158 -0
- package/src/core/work-scope.mjs +248 -0
- package/src/hooks/engine.mjs +162 -0
- package/src/index.mjs +426 -0
- package/src/mcp/client.mjs +253 -0
- package/src/mcp/transport-shttp.mjs +130 -0
- package/src/mcp/transport-sse.mjs +131 -0
- package/src/mcp/transport-ws.mjs +134 -0
- package/src/onboarding/preflight.mjs +360 -0
- package/src/permissions/checker.mjs +57 -0
- package/src/permissions/command-classifier.mjs +652 -0
- package/src/permissions/injection-check.mjs +60 -0
- package/src/permissions/path-check.mjs +102 -0
- package/src/permissions/prompt.mjs +73 -0
- package/src/permissions/sandbox.mjs +112 -0
- package/src/plugins/loader.mjs +138 -0
- package/src/skills/installer.mjs +188 -0
- package/src/skills/loader.mjs +252 -0
- package/src/skills/runner.mjs +55 -0
- package/src/state/orbit.mjs +263 -0
- package/src/state/verbosity.mjs +99 -0
- package/src/telemetry/index.mjs +96 -0
- package/src/terminal/agents.mjs +177 -0
- package/src/terminal/analytics.mjs +292 -0
- package/src/terminal/ansi.mjs +695 -0
- package/src/terminal/init.mjs +145 -0
- package/src/terminal/main.mjs +269 -0
- package/src/terminal/repl-explore.mjs +35 -0
- package/src/terminal/repl-format.mjs +257 -0
- package/src/terminal/repl-render.mjs +561 -0
- package/src/terminal/repl-resume.mjs +625 -0
- package/src/terminal/repl-state.mjs +103 -0
- package/src/terminal/repl-utils.mjs +34 -0
- package/src/terminal/repl.mjs +3832 -0
- package/src/terminal/skills.mjs +54 -0
- package/src/terminal/tool-display.mjs +240 -0
- package/src/tools/agent.mjs +137 -0
- package/src/tools/ask-user.mjs +61 -0
- package/src/tools/bash.mjs +231 -0
- package/src/tools/cron-create.mjs +120 -0
- package/src/tools/cron-delete.mjs +49 -0
- package/src/tools/cron-list.mjs +37 -0
- package/src/tools/edit.mjs +82 -0
- package/src/tools/enter-worktree.mjs +69 -0
- package/src/tools/exit-worktree.mjs +57 -0
- package/src/tools/glob.mjs +117 -0
- package/src/tools/grep.mjs +129 -0
- package/src/tools/lint.mjs +71 -0
- package/src/tools/ls.mjs +58 -0
- package/src/tools/lsp.mjs +115 -0
- package/src/tools/multi-edit.mjs +94 -0
- package/src/tools/notebook-edit.mjs +96 -0
- package/src/tools/project-overview.mjs +641 -0
- package/src/tools/read-mcp-resource.mjs +57 -0
- package/src/tools/read.mjs +138 -0
- package/src/tools/registry.mjs +116 -0
- package/src/tools/remote-trigger.mjs +84 -0
- package/src/tools/send-message.mjs +64 -0
- package/src/tools/skill.mjs +52 -0
- package/src/tools/test-runner.mjs +49 -0
- package/src/tools/todo-write.mjs +68 -0
- package/src/tools/tool-search.mjs +77 -0
- package/src/tools/web-fetch.mjs +65 -0
- package/src/tools/web-search.mjs +89 -0
- package/src/tools/write.mjs +55 -0
- package/src/ui/approval.mjs +263 -0
- package/src/ui/banner.mjs +235 -0
- package/src/ui/commands.mjs +537 -0
- package/src/ui/formatter.mjs +409 -0
- package/src/ui/icons.mjs +164 -0
- package/src/ui/input-dock.mjs +444 -0
- package/src/ui/markdown.mjs +278 -0
- package/src/ui/mission-report.mjs +296 -0
- package/src/ui/palette.mjs +189 -0
- package/src/ui/slash-commands.mjs +245 -0
- package/src/ui/spinner.mjs +116 -0
- package/src/ui/sub-agent.mjs +152 -0
- package/src/ui/term.mjs +159 -0
- package/src/ui/text-layout.mjs +127 -0
- package/src/ui/tool-card.mjs +463 -0
- package/src/ui/tool-details.mjs +312 -0
- package/src/ui/transcript-block.mjs +21 -0
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Bash Tool — matches Claude Code's exact behavior.
|
|
3
|
+
*
|
|
4
|
+
* Features:
|
|
5
|
+
* - Timeout with SIGTERM -> SIGKILL escalation
|
|
6
|
+
* - run_in_background option
|
|
7
|
+
* - description parameter
|
|
8
|
+
* - 1MB output limit
|
|
9
|
+
* - ANSI code stripping by default
|
|
10
|
+
*/
|
|
11
|
+
import { spawn } from 'child_process';
|
|
12
|
+
|
|
13
|
+
// Strip ANSI escape sequences
|
|
14
|
+
function stripAnsi(str) {
|
|
15
|
+
// eslint-disable-next-line no-control-regex
|
|
16
|
+
return str.replace(/\x1b\[[0-9;]*[a-zA-Z]/g, '');
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const MAX_OUTPUT_BYTES = 1024 * 1024; // 1MB
|
|
20
|
+
const TIMEOUT_TAIL_BYTES = 64 * 1024;
|
|
21
|
+
|
|
22
|
+
export const BashTool = {
|
|
23
|
+
name: 'Bash',
|
|
24
|
+
description: 'Execute a bash command and return its output.',
|
|
25
|
+
inputSchema: {
|
|
26
|
+
type: 'object',
|
|
27
|
+
properties: {
|
|
28
|
+
command: { type: 'string', description: 'The command to execute' },
|
|
29
|
+
timeout: { type: 'number', description: 'Timeout in ms (max 600000)', default: 120000 },
|
|
30
|
+
description: { type: 'string', description: 'Description of what this command does' },
|
|
31
|
+
cwd: { type: 'string', description: 'Working directory for the command' },
|
|
32
|
+
run_in_background: { type: 'boolean', description: 'Run in background', default: false },
|
|
33
|
+
},
|
|
34
|
+
required: ['command'],
|
|
35
|
+
},
|
|
36
|
+
validateInput(input) {
|
|
37
|
+
const errors = [];
|
|
38
|
+
if (!input.command) errors.push('command is required');
|
|
39
|
+
return errors;
|
|
40
|
+
},
|
|
41
|
+
async call(input) {
|
|
42
|
+
const timeout = Math.min(input.timeout || 120000, 600000);
|
|
43
|
+
const abortSignal = input.signal || input._signal || null;
|
|
44
|
+
|
|
45
|
+
if (input.run_in_background) {
|
|
46
|
+
return runBackground(input.command, input.cwd);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
if (abortSignal?.aborted) {
|
|
50
|
+
return 'Error: Command cancelled by user';
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
return new Promise((resolve) => {
|
|
54
|
+
let stdout = '';
|
|
55
|
+
let stderr = '';
|
|
56
|
+
let stdoutTail = '';
|
|
57
|
+
let stderrTail = '';
|
|
58
|
+
let killed = false;
|
|
59
|
+
let cancelled = false;
|
|
60
|
+
let exitCode = null;
|
|
61
|
+
let killTimer = null;
|
|
62
|
+
let settled = false;
|
|
63
|
+
|
|
64
|
+
const proc = spawn('bash', ['-c', input.command], {
|
|
65
|
+
cwd: input.cwd,
|
|
66
|
+
env: { ...process.env },
|
|
67
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
68
|
+
detached: process.platform !== 'win32',
|
|
69
|
+
timeout: 0, // we handle timeout ourselves
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
function finish(value) {
|
|
73
|
+
if (settled) return;
|
|
74
|
+
settled = true;
|
|
75
|
+
clearTimeout(timer);
|
|
76
|
+
if (killTimer) clearTimeout(killTimer);
|
|
77
|
+
abortSignal?.removeEventListener?.('abort', onAbort);
|
|
78
|
+
resolve(value);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function terminate(reason) {
|
|
82
|
+
killed = true;
|
|
83
|
+
cancelled = reason === 'cancelled';
|
|
84
|
+
killProcess(proc, 'SIGTERM');
|
|
85
|
+
killTimer = setTimeout(() => {
|
|
86
|
+
killProcess(proc, 'SIGKILL');
|
|
87
|
+
}, 5000);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function onAbort() {
|
|
91
|
+
terminate('cancelled');
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
abortSignal?.addEventListener?.('abort', onAbort, { once: true });
|
|
95
|
+
if (abortSignal?.aborted) {
|
|
96
|
+
terminate('cancelled');
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
proc.stdout.on('data', (chunk) => {
|
|
100
|
+
const text = chunk.toString();
|
|
101
|
+
stdout = appendHead(stdout, text, MAX_OUTPUT_BYTES);
|
|
102
|
+
stdoutTail = appendTail(stdoutTail, text, TIMEOUT_TAIL_BYTES);
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
proc.stderr.on('data', (chunk) => {
|
|
106
|
+
const text = chunk.toString();
|
|
107
|
+
stderr = appendHead(stderr, text, MAX_OUTPUT_BYTES);
|
|
108
|
+
stderrTail = appendTail(stderrTail, text, TIMEOUT_TAIL_BYTES);
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
// Timeout: SIGTERM first, then SIGKILL after 5s
|
|
112
|
+
const timer = setTimeout(() => {
|
|
113
|
+
terminate('timeout');
|
|
114
|
+
}, timeout);
|
|
115
|
+
|
|
116
|
+
proc.on('close', (code) => {
|
|
117
|
+
exitCode = code;
|
|
118
|
+
|
|
119
|
+
// Truncate if over limit
|
|
120
|
+
if (stdout.length > MAX_OUTPUT_BYTES) {
|
|
121
|
+
stdout = stdout.slice(0, MAX_OUTPUT_BYTES) + '\n[output truncated at 1MB]';
|
|
122
|
+
}
|
|
123
|
+
if (stderr.length > MAX_OUTPUT_BYTES) {
|
|
124
|
+
stderr = stderr.slice(0, MAX_OUTPUT_BYTES) + '\n[output truncated at 1MB]';
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// Strip ANSI by default
|
|
128
|
+
stdout = stripAnsi(stdout);
|
|
129
|
+
stderr = stripAnsi(stderr);
|
|
130
|
+
|
|
131
|
+
if (killed) {
|
|
132
|
+
const tail = formatTimeoutTail(stdoutTail, stderrTail);
|
|
133
|
+
const message = cancelled
|
|
134
|
+
? `Error: Command cancelled by user\n${tail}`.trim()
|
|
135
|
+
: `Error: Command timed out after ${timeout}ms\n${tail}`.trim();
|
|
136
|
+
finish(message);
|
|
137
|
+
return;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
const output = (stdout + (stderr ? '\n' + stderr : '')).trim();
|
|
141
|
+
if (code !== 0) {
|
|
142
|
+
finish(`Exit code: ${code}\n${output}`.trim());
|
|
143
|
+
} else {
|
|
144
|
+
finish(output || '(no output)');
|
|
145
|
+
}
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
proc.on('error', (err) => {
|
|
149
|
+
finish(`Error: ${err.message}`);
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
// Close stdin
|
|
153
|
+
proc.stdin.end();
|
|
154
|
+
});
|
|
155
|
+
},
|
|
156
|
+
};
|
|
157
|
+
|
|
158
|
+
function appendHead(current, chunk, maxBytes) {
|
|
159
|
+
if (current.length >= maxBytes) return current;
|
|
160
|
+
const next = current + chunk;
|
|
161
|
+
return next.length > maxBytes ? next.slice(0, maxBytes) : next;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
function appendTail(current, chunk, maxBytes) {
|
|
165
|
+
const next = current + chunk;
|
|
166
|
+
return next.length > maxBytes ? next.slice(next.length - maxBytes) : next;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
function formatTimeoutTail(stdoutTail, stderrTail) {
|
|
170
|
+
const out = stripAnsi(stdoutTail || '').trim();
|
|
171
|
+
const err = stripAnsi(stderrTail || '').trim();
|
|
172
|
+
const lines = [];
|
|
173
|
+
if (out) {
|
|
174
|
+
lines.push('[stdout tail]');
|
|
175
|
+
lines.push(tailLines(out, 80));
|
|
176
|
+
}
|
|
177
|
+
if (err) {
|
|
178
|
+
if (lines.length) lines.push('');
|
|
179
|
+
lines.push('[stderr tail]');
|
|
180
|
+
lines.push(tailLines(err, 80));
|
|
181
|
+
}
|
|
182
|
+
return lines.length ? lines.join('\n') : '(no output captured before timeout)';
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
function tailLines(text, maxLines) {
|
|
186
|
+
const lines = String(text || '').split('\n');
|
|
187
|
+
return lines.slice(-maxLines).join('\n');
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
function killProcess(proc, signal) {
|
|
191
|
+
if (!proc?.pid) return;
|
|
192
|
+
try {
|
|
193
|
+
if (process.platform !== 'win32') {
|
|
194
|
+
process.kill(-proc.pid, signal);
|
|
195
|
+
return;
|
|
196
|
+
}
|
|
197
|
+
} catch { /* fall through to direct process kill */ }
|
|
198
|
+
try { proc.kill(signal); } catch { /* already exited */ }
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
// Background jobs store
|
|
202
|
+
const backgroundJobs = new Map();
|
|
203
|
+
let bgJobId = 0;
|
|
204
|
+
|
|
205
|
+
function runBackground(command, cwd) {
|
|
206
|
+
const id = ++bgJobId;
|
|
207
|
+
const proc = spawn('bash', ['-c', command], {
|
|
208
|
+
cwd,
|
|
209
|
+
detached: true,
|
|
210
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
let stdout = '';
|
|
214
|
+
let stderr = '';
|
|
215
|
+
proc.stdout.on('data', (d) => { stdout += d.toString(); });
|
|
216
|
+
proc.stderr.on('data', (d) => { stderr += d.toString(); });
|
|
217
|
+
|
|
218
|
+
const job = { id, pid: proc.pid, command, status: 'running', stdout: '', stderr: '' };
|
|
219
|
+
backgroundJobs.set(id, job);
|
|
220
|
+
|
|
221
|
+
proc.on('close', (code) => {
|
|
222
|
+
job.status = code === 0 ? 'completed' : `exited(${code})`;
|
|
223
|
+
job.stdout = stripAnsi(stdout.slice(0, MAX_OUTPUT_BYTES));
|
|
224
|
+
job.stderr = stripAnsi(stderr.slice(0, MAX_OUTPUT_BYTES));
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
proc.unref();
|
|
228
|
+
return `Background job started: id=${id}, pid=${proc.pid}`;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
export { backgroundJobs };
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CronCreate Tool — create a scheduled task.
|
|
3
|
+
*
|
|
4
|
+
* Stores cron definitions in memory and optionally persists them
|
|
5
|
+
* to ~/.claude/cron.json. Uses setTimeout-based scheduling for
|
|
6
|
+
* the duration of the session.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import fs from 'fs';
|
|
10
|
+
import path from 'path';
|
|
11
|
+
import os from 'os';
|
|
12
|
+
|
|
13
|
+
// Shared cron store
|
|
14
|
+
export const cronStore = new Map();
|
|
15
|
+
let cronIdCounter = 1;
|
|
16
|
+
|
|
17
|
+
export const CronCreateTool = {
|
|
18
|
+
name: 'CronCreate',
|
|
19
|
+
description: 'Create a scheduled task that runs on a cron schedule.',
|
|
20
|
+
inputSchema: {
|
|
21
|
+
type: 'object',
|
|
22
|
+
properties: {
|
|
23
|
+
name: { type: 'string', description: 'Name for the scheduled task' },
|
|
24
|
+
schedule: {
|
|
25
|
+
type: 'string',
|
|
26
|
+
description: 'Cron expression (e.g., "*/5 * * * *") or interval (e.g., "5m", "1h")',
|
|
27
|
+
},
|
|
28
|
+
command: {
|
|
29
|
+
type: 'string',
|
|
30
|
+
description: 'Command or prompt to execute on schedule',
|
|
31
|
+
},
|
|
32
|
+
type: {
|
|
33
|
+
type: 'string',
|
|
34
|
+
enum: ['command', 'prompt'],
|
|
35
|
+
description: 'Whether to run as shell command or agent prompt',
|
|
36
|
+
},
|
|
37
|
+
},
|
|
38
|
+
required: ['name', 'schedule', 'command'],
|
|
39
|
+
},
|
|
40
|
+
|
|
41
|
+
validateInput(input) {
|
|
42
|
+
const errors = [];
|
|
43
|
+
if (!input.name) errors.push('name is required');
|
|
44
|
+
if (!input.schedule) errors.push('schedule is required');
|
|
45
|
+
if (!input.command) errors.push('command is required');
|
|
46
|
+
return errors;
|
|
47
|
+
},
|
|
48
|
+
|
|
49
|
+
async call(input) {
|
|
50
|
+
if (process.env.CLAUDE_CODE_DISABLE_CRON === '1') {
|
|
51
|
+
return 'Cron tasks are disabled (CLAUDE_CODE_DISABLE_CRON=1)';
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const id = `cron_${cronIdCounter++}`;
|
|
55
|
+
const intervalMs = parseSchedule(input.schedule);
|
|
56
|
+
|
|
57
|
+
const job = {
|
|
58
|
+
id,
|
|
59
|
+
name: input.name,
|
|
60
|
+
schedule: input.schedule,
|
|
61
|
+
command: input.command,
|
|
62
|
+
type: input.type || 'command',
|
|
63
|
+
intervalMs,
|
|
64
|
+
createdAt: new Date().toISOString(),
|
|
65
|
+
lastRun: null,
|
|
66
|
+
runCount: 0,
|
|
67
|
+
timer: null,
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
// Set up interval timer
|
|
71
|
+
if (intervalMs > 0) {
|
|
72
|
+
job.timer = setInterval(() => {
|
|
73
|
+
job.lastRun = new Date().toISOString();
|
|
74
|
+
job.runCount++;
|
|
75
|
+
// Execution is handled by the cron runner in the main loop
|
|
76
|
+
}, intervalMs);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
cronStore.set(id, job);
|
|
80
|
+
persistCronJobs();
|
|
81
|
+
|
|
82
|
+
return `Created scheduled task:\n ID: ${id}\n Name: ${input.name}\n Schedule: ${input.schedule}\n Interval: ${intervalMs}ms\n Type: ${job.type}`;
|
|
83
|
+
},
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Parse a schedule string into milliseconds.
|
|
88
|
+
* Supports cron shorthand: "5m", "1h", "30s", "1d"
|
|
89
|
+
*/
|
|
90
|
+
function parseSchedule(schedule) {
|
|
91
|
+
const match = schedule.match(/^(\d+)(s|m|h|d)$/);
|
|
92
|
+
if (match) {
|
|
93
|
+
const value = parseInt(match[1], 10);
|
|
94
|
+
const units = { s: 1000, m: 60000, h: 3600000, d: 86400000 };
|
|
95
|
+
return value * (units[match[2]] || 60000);
|
|
96
|
+
}
|
|
97
|
+
// Default: treat as minutes for simple numbers
|
|
98
|
+
const num = parseInt(schedule, 10);
|
|
99
|
+
if (!isNaN(num)) return num * 60000;
|
|
100
|
+
// For full cron expressions, default to 5 minutes
|
|
101
|
+
return 300000;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function persistCronJobs() {
|
|
105
|
+
try {
|
|
106
|
+
const cronDir = path.join(os.homedir(), '.claude');
|
|
107
|
+
fs.mkdirSync(cronDir, { recursive: true });
|
|
108
|
+
const jobs = [];
|
|
109
|
+
for (const [, job] of cronStore) {
|
|
110
|
+
const { timer, ...rest } = job;
|
|
111
|
+
jobs.push(rest);
|
|
112
|
+
}
|
|
113
|
+
fs.writeFileSync(
|
|
114
|
+
path.join(cronDir, 'cron.json'),
|
|
115
|
+
JSON.stringify(jobs, null, 2)
|
|
116
|
+
);
|
|
117
|
+
} catch {
|
|
118
|
+
// Best effort
|
|
119
|
+
}
|
|
120
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CronDelete Tool — delete a scheduled task.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { cronStore } from './cron-create.mjs';
|
|
6
|
+
|
|
7
|
+
export const CronDeleteTool = {
|
|
8
|
+
name: 'CronDelete',
|
|
9
|
+
description: 'Delete a scheduled task by ID or name.',
|
|
10
|
+
inputSchema: {
|
|
11
|
+
type: 'object',
|
|
12
|
+
properties: {
|
|
13
|
+
id: { type: 'string', description: 'Cron job ID to delete' },
|
|
14
|
+
name: { type: 'string', description: 'Cron job name to delete (alternative to id)' },
|
|
15
|
+
},
|
|
16
|
+
required: [],
|
|
17
|
+
},
|
|
18
|
+
|
|
19
|
+
validateInput(input) {
|
|
20
|
+
if (!input.id && !input.name) return ['Either id or name is required'];
|
|
21
|
+
return [];
|
|
22
|
+
},
|
|
23
|
+
|
|
24
|
+
async call(input) {
|
|
25
|
+
let target = null;
|
|
26
|
+
|
|
27
|
+
if (input.id) {
|
|
28
|
+
target = cronStore.get(input.id);
|
|
29
|
+
} else if (input.name) {
|
|
30
|
+
for (const [, job] of cronStore) {
|
|
31
|
+
if (job.name === input.name) {
|
|
32
|
+
target = job;
|
|
33
|
+
break;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
if (!target) {
|
|
39
|
+
return `No cron job found matching ${input.id || input.name}`;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
if (target.timer) {
|
|
43
|
+
clearInterval(target.timer);
|
|
44
|
+
}
|
|
45
|
+
cronStore.delete(target.id);
|
|
46
|
+
|
|
47
|
+
return `Deleted cron job: ${target.id} (${target.name})`;
|
|
48
|
+
},
|
|
49
|
+
};
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CronList Tool — list all scheduled tasks.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { cronStore } from './cron-create.mjs';
|
|
6
|
+
|
|
7
|
+
export const CronListTool = {
|
|
8
|
+
name: 'CronList',
|
|
9
|
+
description: 'List all scheduled tasks.',
|
|
10
|
+
inputSchema: {
|
|
11
|
+
type: 'object',
|
|
12
|
+
properties: {},
|
|
13
|
+
required: [],
|
|
14
|
+
},
|
|
15
|
+
|
|
16
|
+
validateInput() { return []; },
|
|
17
|
+
|
|
18
|
+
async call() {
|
|
19
|
+
if (cronStore.size === 0) {
|
|
20
|
+
return 'No scheduled tasks.';
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const lines = [];
|
|
24
|
+
for (const [, job] of cronStore) {
|
|
25
|
+
lines.push(
|
|
26
|
+
` ${job.id}: ${job.name}\n` +
|
|
27
|
+
` Schedule: ${job.schedule} (${job.intervalMs}ms)\n` +
|
|
28
|
+
` Type: ${job.type}\n` +
|
|
29
|
+
` Runs: ${job.runCount}\n` +
|
|
30
|
+
` Last: ${job.lastRun || 'never'}\n` +
|
|
31
|
+
` Created: ${job.createdAt}`
|
|
32
|
+
);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
return `Scheduled tasks (${cronStore.size}):\n${lines.join('\n\n')}`;
|
|
36
|
+
},
|
|
37
|
+
};
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Edit Tool — matches Claude Code's exact behavior.
|
|
3
|
+
*
|
|
4
|
+
* Features:
|
|
5
|
+
* - replace_all parameter for global replacement
|
|
6
|
+
* - Verify old_string is unique (error if not)
|
|
7
|
+
* - Require file was Read first (track read files)
|
|
8
|
+
* - Preserve exact indentation
|
|
9
|
+
*/
|
|
10
|
+
import fs from 'fs';
|
|
11
|
+
import path from 'path';
|
|
12
|
+
import { hasBeenRead, markRead } from './read.mjs';
|
|
13
|
+
|
|
14
|
+
export const EditTool = {
|
|
15
|
+
name: 'Edit',
|
|
16
|
+
description: 'Performs exact string replacements in files.',
|
|
17
|
+
inputSchema: {
|
|
18
|
+
type: 'object',
|
|
19
|
+
properties: {
|
|
20
|
+
file_path: { type: 'string', description: 'Absolute path to the file' },
|
|
21
|
+
old_string: { type: 'string', description: 'The text to replace' },
|
|
22
|
+
new_string: { type: 'string', description: 'The replacement text' },
|
|
23
|
+
replace_all: { type: 'boolean', description: 'Replace all occurrences', default: false },
|
|
24
|
+
},
|
|
25
|
+
required: ['file_path', 'old_string', 'new_string'],
|
|
26
|
+
},
|
|
27
|
+
validateInput(input) {
|
|
28
|
+
const errors = [];
|
|
29
|
+
if (!input.file_path) errors.push('file_path required');
|
|
30
|
+
if (!input.old_string && input.old_string !== '') errors.push('old_string required');
|
|
31
|
+
if (input.old_string === input.new_string) errors.push('old_string must differ from new_string');
|
|
32
|
+
return errors;
|
|
33
|
+
},
|
|
34
|
+
async call(input) {
|
|
35
|
+
const filePath = path.resolve(input.file_path);
|
|
36
|
+
|
|
37
|
+
// Check file exists
|
|
38
|
+
if (!fs.existsSync(filePath)) {
|
|
39
|
+
return `Error: File not found: ${filePath}`;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// Require file was read first
|
|
43
|
+
if (!hasBeenRead(filePath)) {
|
|
44
|
+
return `Error: You must Read ${filePath} before editing it. Use the Read tool first.`;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
let content;
|
|
48
|
+
try {
|
|
49
|
+
content = fs.readFileSync(filePath, 'utf-8');
|
|
50
|
+
} catch (e) {
|
|
51
|
+
return `Error: ${e.message}`;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
if (!content.includes(input.old_string)) {
|
|
55
|
+
return 'Error: old_string not found in file. Make sure the string matches exactly, including whitespace and indentation.';
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
if (input.replace_all) {
|
|
59
|
+
// Replace all occurrences
|
|
60
|
+
const escaped = input.old_string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
61
|
+
content = content.replace(new RegExp(escaped, 'g'), input.new_string);
|
|
62
|
+
} else {
|
|
63
|
+
// Check uniqueness: old_string must appear exactly once
|
|
64
|
+
const firstIdx = content.indexOf(input.old_string);
|
|
65
|
+
const secondIdx = content.indexOf(input.old_string, firstIdx + 1);
|
|
66
|
+
if (secondIdx !== -1) {
|
|
67
|
+
const count = content.split(input.old_string).length - 1;
|
|
68
|
+
return `Error: old_string is not unique in the file (found ${count} occurrences). Provide more context to make it unique, or use replace_all to replace all occurrences.`;
|
|
69
|
+
}
|
|
70
|
+
content = content.replace(input.old_string, input.new_string);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
try {
|
|
74
|
+
fs.writeFileSync(filePath, content);
|
|
75
|
+
// Keep it marked as read
|
|
76
|
+
markRead(filePath);
|
|
77
|
+
return `File updated: ${filePath}`;
|
|
78
|
+
} catch (e) {
|
|
79
|
+
return `Error writing file: ${e.message}`;
|
|
80
|
+
}
|
|
81
|
+
},
|
|
82
|
+
};
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* EnterWorktree Tool — create and enter a git worktree for isolation.
|
|
3
|
+
*
|
|
4
|
+
* Creates a temporary worktree branch so edits do not affect the main branch.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { execSync } from 'child_process';
|
|
8
|
+
import path from 'path';
|
|
9
|
+
import os from 'os';
|
|
10
|
+
|
|
11
|
+
export const EnterWorktreeTool = {
|
|
12
|
+
name: 'EnterWorktree',
|
|
13
|
+
description: 'Create a git worktree for isolated file editing.',
|
|
14
|
+
inputSchema: {
|
|
15
|
+
type: 'object',
|
|
16
|
+
properties: {
|
|
17
|
+
branch: {
|
|
18
|
+
type: 'string',
|
|
19
|
+
description: 'Branch name for the worktree (auto-generated if omitted)',
|
|
20
|
+
},
|
|
21
|
+
path: {
|
|
22
|
+
type: 'string',
|
|
23
|
+
description: 'Directory for the worktree (temp dir if omitted)',
|
|
24
|
+
},
|
|
25
|
+
},
|
|
26
|
+
required: [],
|
|
27
|
+
},
|
|
28
|
+
|
|
29
|
+
// Shared state for active worktree
|
|
30
|
+
_activeWorktree: null,
|
|
31
|
+
|
|
32
|
+
validateInput() { return []; },
|
|
33
|
+
|
|
34
|
+
async call(input) {
|
|
35
|
+
if (this._activeWorktree) {
|
|
36
|
+
return `Already in worktree at ${this._activeWorktree.path}. Use ExitWorktree first.`;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
try {
|
|
40
|
+
// Verify we are in a git repo
|
|
41
|
+
execSync('git rev-parse --is-inside-work-tree', { encoding: 'utf-8' });
|
|
42
|
+
} catch {
|
|
43
|
+
return 'Error: not inside a git repository';
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const branch = input.branch || `occ-worktree-${Date.now()}`;
|
|
47
|
+
const worktreePath = input.path || path.join(os.tmpdir(), `occ-wt-${Date.now()}`);
|
|
48
|
+
const originalCwd = process.cwd();
|
|
49
|
+
|
|
50
|
+
try {
|
|
51
|
+
execSync(`git worktree add -b "${branch}" "${worktreePath}"`, {
|
|
52
|
+
encoding: 'utf-8',
|
|
53
|
+
stdio: 'pipe',
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
this._activeWorktree = {
|
|
57
|
+
path: worktreePath,
|
|
58
|
+
branch,
|
|
59
|
+
originalCwd,
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
process.chdir(worktreePath);
|
|
63
|
+
|
|
64
|
+
return `Entered worktree:\n Branch: ${branch}\n Path: ${worktreePath}\n Original: ${originalCwd}`;
|
|
65
|
+
} catch (err) {
|
|
66
|
+
return `Error creating worktree: ${err.message}`;
|
|
67
|
+
}
|
|
68
|
+
},
|
|
69
|
+
};
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ExitWorktree Tool — exit and clean up a git worktree.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { execSync } from 'child_process';
|
|
6
|
+
|
|
7
|
+
export const ExitWorktreeTool = {
|
|
8
|
+
name: 'ExitWorktree',
|
|
9
|
+
description: 'Exit the current git worktree and return to the original directory.',
|
|
10
|
+
inputSchema: {
|
|
11
|
+
type: 'object',
|
|
12
|
+
properties: {
|
|
13
|
+
cleanup: {
|
|
14
|
+
type: 'boolean',
|
|
15
|
+
description: 'Remove the worktree branch after exiting (default: true)',
|
|
16
|
+
},
|
|
17
|
+
},
|
|
18
|
+
required: [],
|
|
19
|
+
},
|
|
20
|
+
|
|
21
|
+
validateInput() { return []; },
|
|
22
|
+
|
|
23
|
+
async call(input) {
|
|
24
|
+
const { EnterWorktreeTool } = await import('./enter-worktree.mjs');
|
|
25
|
+
const wt = EnterWorktreeTool._activeWorktree;
|
|
26
|
+
|
|
27
|
+
if (!wt) {
|
|
28
|
+
return 'Not currently in a worktree.';
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const cleanup = input.cleanup !== false;
|
|
32
|
+
|
|
33
|
+
try {
|
|
34
|
+
process.chdir(wt.originalCwd);
|
|
35
|
+
|
|
36
|
+
if (cleanup) {
|
|
37
|
+
try {
|
|
38
|
+
execSync(`git worktree remove "${wt.path}" --force`, {
|
|
39
|
+
encoding: 'utf-8',
|
|
40
|
+
stdio: 'pipe',
|
|
41
|
+
});
|
|
42
|
+
execSync(`git branch -D "${wt.branch}"`, {
|
|
43
|
+
encoding: 'utf-8',
|
|
44
|
+
stdio: 'pipe',
|
|
45
|
+
});
|
|
46
|
+
} catch {
|
|
47
|
+
// Best effort cleanup
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
EnterWorktreeTool._activeWorktree = null;
|
|
52
|
+
return `Exited worktree. Returned to ${wt.originalCwd}${cleanup ? ' (cleaned up)' : ''}`;
|
|
53
|
+
} catch (err) {
|
|
54
|
+
return `Error exiting worktree: ${err.message}`;
|
|
55
|
+
}
|
|
56
|
+
},
|
|
57
|
+
};
|