@axplusb/kepler 0.0.1 → 1.0.1

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 (218) hide show
  1. package/README.md +82 -0
  2. package/package.json +36 -4
  3. package/pulse/app/activity/page.tsx +190 -0
  4. package/pulse/app/api/activity/route.ts +138 -0
  5. package/pulse/app/api/costs/route.ts +88 -0
  6. package/pulse/app/api/export/route.ts +77 -0
  7. package/pulse/app/api/history/route.ts +11 -0
  8. package/pulse/app/api/import/route.ts +31 -0
  9. package/pulse/app/api/memory/route.ts +52 -0
  10. package/pulse/app/api/plans/route.ts +9 -0
  11. package/pulse/app/api/projects/[slug]/route.ts +96 -0
  12. package/pulse/app/api/projects/route.ts +121 -0
  13. package/pulse/app/api/sessions/[id]/replay/route.ts +20 -0
  14. package/pulse/app/api/sessions/[id]/route.ts +31 -0
  15. package/pulse/app/api/sessions/route.ts +112 -0
  16. package/pulse/app/api/settings/route.ts +14 -0
  17. package/pulse/app/api/stats/route.ts +143 -0
  18. package/pulse/app/api/todos/route.ts +9 -0
  19. package/pulse/app/api/tools/route.ts +160 -0
  20. package/pulse/app/costs/page.tsx +179 -0
  21. package/pulse/app/export/page.tsx +465 -0
  22. package/pulse/app/favicon.ico +0 -0
  23. package/pulse/app/globals.css +263 -0
  24. package/pulse/app/help/page.tsx +142 -0
  25. package/pulse/app/history/page.tsx +157 -0
  26. package/pulse/app/layout.tsx +46 -0
  27. package/pulse/app/memory/page.tsx +365 -0
  28. package/pulse/app/overview-client.tsx +393 -0
  29. package/pulse/app/page.tsx +14 -0
  30. package/pulse/app/plans/page.tsx +308 -0
  31. package/pulse/app/projects/[slug]/page.tsx +390 -0
  32. package/pulse/app/projects/page.tsx +110 -0
  33. package/pulse/app/sessions/[id]/page.tsx +243 -0
  34. package/pulse/app/sessions/page.tsx +39 -0
  35. package/pulse/app/settings/page.tsx +188 -0
  36. package/pulse/app/todos/page.tsx +211 -0
  37. package/pulse/app/tools/page.tsx +249 -0
  38. package/pulse/cli.js +159 -0
  39. package/pulse/components/activity/day-of-week-chart.tsx +35 -0
  40. package/pulse/components/activity/streak-card.tsx +36 -0
  41. package/pulse/components/costs/cache-efficiency-panel.tsx +76 -0
  42. package/pulse/components/costs/cost-by-project-chart.tsx +48 -0
  43. package/pulse/components/costs/cost-over-time-chart.tsx +95 -0
  44. package/pulse/components/costs/model-token-table.tsx +60 -0
  45. package/pulse/components/global-search.tsx +193 -0
  46. package/pulse/components/keyboard-nav-provider.tsx +23 -0
  47. package/pulse/components/layout/bottom-nav.tsx +52 -0
  48. package/pulse/components/layout/client-layout.tsx +31 -0
  49. package/pulse/components/layout/sidebar-context.tsx +50 -0
  50. package/pulse/components/layout/sidebar.tsx +182 -0
  51. package/pulse/components/layout/top-bar.tsx +121 -0
  52. package/pulse/components/overview/activity-heatmap.tsx +107 -0
  53. package/pulse/components/overview/conversation-table.tsx +148 -0
  54. package/pulse/components/overview/model-breakdown-donut.tsx +95 -0
  55. package/pulse/components/overview/peak-hours-chart.tsx +87 -0
  56. package/pulse/components/overview/project-activity-donut.tsx +96 -0
  57. package/pulse/components/overview/stat-card.tsx +102 -0
  58. package/pulse/components/overview/usage-over-time-chart.tsx +166 -0
  59. package/pulse/components/projects/project-card.tsx +175 -0
  60. package/pulse/components/sessions/replay/assistant-markdown.tsx +94 -0
  61. package/pulse/components/sessions/replay/compaction-card.tsx +25 -0
  62. package/pulse/components/sessions/replay/session-sidebar.tsx +231 -0
  63. package/pulse/components/sessions/replay/token-accumulation-chart.tsx +98 -0
  64. package/pulse/components/sessions/replay/tool-call-badge.tsx +127 -0
  65. package/pulse/components/sessions/replay/turn-cards.tsx +220 -0
  66. package/pulse/components/sessions/replay/user-tool-result.tsx +158 -0
  67. package/pulse/components/sessions/session-badges.tsx +49 -0
  68. package/pulse/components/sessions/session-table.tsx +299 -0
  69. package/pulse/components/theme-provider.tsx +44 -0
  70. package/pulse/components/tools/feature-adoption-table.tsx +58 -0
  71. package/pulse/components/tools/mcp-server-panel.tsx +45 -0
  72. package/pulse/components/tools/tool-ranking-chart.tsx +57 -0
  73. package/pulse/components/tools/version-history-table.tsx +32 -0
  74. package/pulse/components/ui/alert.tsx +66 -0
  75. package/pulse/components/ui/badge.tsx +48 -0
  76. package/pulse/components/ui/breadcrumb.tsx +109 -0
  77. package/pulse/components/ui/button.tsx +64 -0
  78. package/pulse/components/ui/calendar.tsx +220 -0
  79. package/pulse/components/ui/card.tsx +92 -0
  80. package/pulse/components/ui/command.tsx +158 -0
  81. package/pulse/components/ui/dialog.tsx +158 -0
  82. package/pulse/components/ui/input.tsx +21 -0
  83. package/pulse/components/ui/popover.tsx +89 -0
  84. package/pulse/components/ui/progress.tsx +31 -0
  85. package/pulse/components/ui/select.tsx +190 -0
  86. package/pulse/components/ui/separator.tsx +28 -0
  87. package/pulse/components/ui/sheet.tsx +143 -0
  88. package/pulse/components/ui/skeleton.tsx +13 -0
  89. package/pulse/components/ui/table.tsx +116 -0
  90. package/pulse/components/ui/tabs.tsx +91 -0
  91. package/pulse/components/ui/tooltip.tsx +57 -0
  92. package/pulse/components/use-global-keyboard-nav.ts +79 -0
  93. package/pulse/components.json +23 -0
  94. package/pulse/eslint.config.mjs +18 -0
  95. package/pulse/lib/claude-reader.ts +594 -0
  96. package/pulse/lib/decode.ts +129 -0
  97. package/pulse/lib/pricing.ts +102 -0
  98. package/pulse/lib/replay-parser.ts +165 -0
  99. package/pulse/lib/tool-categories.ts +127 -0
  100. package/pulse/lib/utils.ts +6 -0
  101. package/pulse/next-env.d.ts +6 -0
  102. package/pulse/next.config.ts +16 -0
  103. package/pulse/package.json +45 -0
  104. package/pulse/postcss.config.mjs +7 -0
  105. package/pulse/public/activity.png +0 -0
  106. package/pulse/public/cc-lens.png +0 -0
  107. package/pulse/public/command-k.png +0 -0
  108. package/pulse/public/costs.png +0 -0
  109. package/pulse/public/dashboard-dark.png +0 -0
  110. package/pulse/public/dashboard-white.png +0 -0
  111. package/pulse/public/export.png +0 -0
  112. package/pulse/public/file.svg +1 -0
  113. package/pulse/public/globe.svg +1 -0
  114. package/pulse/public/next.svg +1 -0
  115. package/pulse/public/projects.png +0 -0
  116. package/pulse/public/session-chat.png +0 -0
  117. package/pulse/public/todos.png +0 -0
  118. package/pulse/public/tools.png +0 -0
  119. package/pulse/public/vercel.svg +1 -0
  120. package/pulse/public/window.svg +1 -0
  121. package/pulse/tsconfig.json +34 -0
  122. package/pulse/types/claude.ts +294 -0
  123. package/src/agents/loader.mjs +89 -0
  124. package/src/agents/parser.mjs +98 -0
  125. package/src/agents/teams.mjs +123 -0
  126. package/src/auth/oauth.mjs +220 -0
  127. package/src/auth/tarang-auth.mjs +277 -0
  128. package/src/config/cli-args.mjs +173 -0
  129. package/src/config/env.mjs +263 -0
  130. package/src/config/settings.mjs +132 -0
  131. package/src/context/ast-parser.mjs +298 -0
  132. package/src/context/bm25.mjs +85 -0
  133. package/src/context/retriever.mjs +270 -0
  134. package/src/context/skeleton.mjs +134 -0
  135. package/src/core/agent-loop.mjs +480 -0
  136. package/src/core/approval.mjs +273 -0
  137. package/src/core/backend-url.mjs +57 -0
  138. package/src/core/cache.mjs +105 -0
  139. package/src/core/callback-client.mjs +149 -0
  140. package/src/core/checkpoints.mjs +142 -0
  141. package/src/core/context-manager.mjs +198 -0
  142. package/src/core/headless.mjs +168 -0
  143. package/src/core/hooks-manager.mjs +87 -0
  144. package/src/core/jsonl-writer.mjs +351 -0
  145. package/src/core/local-agent.mjs +429 -0
  146. package/src/core/local-store.mjs +325 -0
  147. package/src/core/mode-selector.mjs +51 -0
  148. package/src/core/output-filter.mjs +177 -0
  149. package/src/core/paths.mjs +101 -0
  150. package/src/core/pricing.mjs +314 -0
  151. package/src/core/providers.mjs +219 -0
  152. package/src/core/rate-limiter.mjs +119 -0
  153. package/src/core/safety.mjs +200 -0
  154. package/src/core/scheduler.mjs +173 -0
  155. package/src/core/session-manager.mjs +317 -0
  156. package/src/core/session.mjs +143 -0
  157. package/src/core/settings-sync.mjs +85 -0
  158. package/src/core/stagnation.mjs +57 -0
  159. package/src/core/stream-client.mjs +367 -0
  160. package/src/core/streaming.mjs +182 -0
  161. package/src/core/system-prompt.mjs +135 -0
  162. package/src/core/tool-executor.mjs +725 -0
  163. package/src/hooks/engine.mjs +162 -0
  164. package/src/index.mjs +370 -0
  165. package/src/mcp/client.mjs +253 -0
  166. package/src/mcp/transport-shttp.mjs +130 -0
  167. package/src/mcp/transport-sse.mjs +131 -0
  168. package/src/mcp/transport-ws.mjs +134 -0
  169. package/src/permissions/checker.mjs +57 -0
  170. package/src/permissions/command-classifier.mjs +573 -0
  171. package/src/permissions/injection-check.mjs +60 -0
  172. package/src/permissions/path-check.mjs +102 -0
  173. package/src/permissions/prompt.mjs +73 -0
  174. package/src/permissions/sandbox.mjs +112 -0
  175. package/src/plugins/loader.mjs +138 -0
  176. package/src/skills/loader.mjs +147 -0
  177. package/src/skills/runner.mjs +55 -0
  178. package/src/telemetry/index.mjs +96 -0
  179. package/src/terminal/agents.mjs +177 -0
  180. package/src/terminal/analytics.mjs +292 -0
  181. package/src/terminal/ansi.mjs +421 -0
  182. package/src/terminal/main.mjs +150 -0
  183. package/src/terminal/repl.mjs +1484 -0
  184. package/src/terminal/tool-display.mjs +58 -0
  185. package/src/tools/agent.mjs +137 -0
  186. package/src/tools/ask-user.mjs +61 -0
  187. package/src/tools/bash.mjs +148 -0
  188. package/src/tools/cron-create.mjs +120 -0
  189. package/src/tools/cron-delete.mjs +49 -0
  190. package/src/tools/cron-list.mjs +37 -0
  191. package/src/tools/edit.mjs +82 -0
  192. package/src/tools/enter-worktree.mjs +69 -0
  193. package/src/tools/exit-worktree.mjs +57 -0
  194. package/src/tools/glob.mjs +117 -0
  195. package/src/tools/grep.mjs +129 -0
  196. package/src/tools/lint.mjs +71 -0
  197. package/src/tools/ls.mjs +58 -0
  198. package/src/tools/lsp.mjs +115 -0
  199. package/src/tools/multi-edit.mjs +94 -0
  200. package/src/tools/notebook-edit.mjs +96 -0
  201. package/src/tools/read-mcp-resource.mjs +57 -0
  202. package/src/tools/read.mjs +138 -0
  203. package/src/tools/registry.mjs +132 -0
  204. package/src/tools/remote-trigger.mjs +84 -0
  205. package/src/tools/send-message.mjs +64 -0
  206. package/src/tools/skill.mjs +52 -0
  207. package/src/tools/test-runner.mjs +49 -0
  208. package/src/tools/todo-write.mjs +68 -0
  209. package/src/tools/tool-search.mjs +77 -0
  210. package/src/tools/web-fetch.mjs +65 -0
  211. package/src/tools/web-search.mjs +89 -0
  212. package/src/tools/write.mjs +55 -0
  213. package/src/ui/banner.mjs +237 -0
  214. package/src/ui/commands.mjs +499 -0
  215. package/src/ui/formatter.mjs +379 -0
  216. package/src/ui/markdown.mjs +278 -0
  217. package/src/ui/slash-commands.mjs +258 -0
  218. package/index.js +0 -1
@@ -0,0 +1,58 @@
1
+ const TOOL_LABELS = Object.freeze({
2
+ shell: 'Run command',
3
+ read_file: 'Read file',
4
+ read_files: 'Read files',
5
+ write_file: 'Create file',
6
+ write_project: 'Create project files',
7
+ edit_file: 'Edit file',
8
+ delete_file: 'Delete file',
9
+ list_files: 'List files',
10
+ search_code: 'Search code',
11
+ search_files: 'Search files',
12
+ grep: 'Search text',
13
+ get_file_info: 'Inspect file',
14
+ validate_file: 'Validate file',
15
+ validate_build: 'Validate build',
16
+ validate_structure: 'Check project structure',
17
+ lint_check: 'Check code quality',
18
+ run_tests: 'Run tests',
19
+ git_diff: 'Review changes',
20
+ git_status: 'Check repository status',
21
+ analyze_code: 'Analyze code',
22
+ explore: 'Explore codebase',
23
+ plan: 'Create implementation plan',
24
+ });
25
+
26
+ export function toolDisplayLabel(tool) {
27
+ if (!tool) return 'Use tool';
28
+ if (TOOL_LABELS[tool]) return TOOL_LABELS[tool];
29
+ return tool
30
+ .replace(/^mcp[_-]?/i, '')
31
+ .split(/[_-]+/)
32
+ .filter(Boolean)
33
+ .map((part, index) => index === 0
34
+ ? part.charAt(0).toUpperCase() + part.slice(1)
35
+ : part.toLowerCase())
36
+ .join(' ');
37
+ }
38
+
39
+ export function formatShellCommand(command, colors) {
40
+ const tokens = String(command || '').match(/"(?:\\.|[^"\\])*"|'(?:\\.|[^'\\])*'|&&|\|\||[|;<>]|[^\s]+|\s+/g) || [];
41
+ let expectsCommand = true;
42
+
43
+ return tokens.map(token => {
44
+ if (/^\s+$/.test(token)) return token;
45
+ if (/^(?:&&|\|\||[|;<>])$/.test(token)) {
46
+ expectsCommand = true;
47
+ return colors.red(token);
48
+ }
49
+ if (expectsCommand) {
50
+ expectsCommand = false;
51
+ return colors.blue(token);
52
+ }
53
+ if (/^-{1,2}[\w-]+(?:=.*)?$/.test(token) || /^["']/.test(token)) {
54
+ return colors.yellow(token);
55
+ }
56
+ return colors.white(token);
57
+ }).join('');
58
+ }
@@ -0,0 +1,137 @@
1
+ /**
2
+ * Agent Tool — spawn a subagent with its own agent loop.
3
+ *
4
+ * Features:
5
+ * - subagent_type parameter
6
+ * - isolation: "worktree" option
7
+ * - run_in_background option
8
+ * - model override
9
+ */
10
+
11
+ import { createAgentLoop } from '../core/agent-loop.mjs';
12
+ import { createToolRegistry } from './registry.mjs';
13
+ import { createPermissionChecker } from '../permissions/checker.mjs';
14
+
15
+ export const AgentTool = {
16
+ name: 'Agent',
17
+ description: 'Spawn a subagent to handle a task. The subagent has its own context and tools.',
18
+ inputSchema: {
19
+ type: 'object',
20
+ properties: {
21
+ prompt: {
22
+ type: 'string',
23
+ description: 'The task for the subagent to perform',
24
+ },
25
+ allowed_tools: {
26
+ type: 'array',
27
+ items: { type: 'string' },
28
+ description: 'List of tool names the subagent can use (default: all)',
29
+ },
30
+ subagent_type: {
31
+ type: 'string',
32
+ description: 'Type of subagent (e.g. coder, reviewer, researcher)',
33
+ },
34
+ isolation: {
35
+ type: 'string',
36
+ enum: ['default', 'worktree'],
37
+ description: 'Isolation mode. "worktree" uses a git worktree.',
38
+ },
39
+ run_in_background: {
40
+ type: 'boolean',
41
+ description: 'Run in background and return immediately',
42
+ },
43
+ model: {
44
+ type: 'string',
45
+ description: 'Override model for this subagent',
46
+ },
47
+ },
48
+ required: ['prompt'],
49
+ },
50
+
51
+ validateInput(input) {
52
+ const errors = [];
53
+ if (!input.prompt) errors.push('prompt is required');
54
+ return errors;
55
+ },
56
+
57
+ // Track background subagents
58
+ _backgroundAgents: new Map(),
59
+ _nextBgId: 0,
60
+
61
+ async call(input) {
62
+ const model = input.model || process.env.SUBAGENT_MODEL || 'claude-sonnet-4-6';
63
+ const tools = createToolRegistry();
64
+ const permissions = createPermissionChecker({ defaultMode: 'bypassPermissions' });
65
+
66
+ // Build type-specific system prompt prefix
67
+ let systemPrefix = '';
68
+ if (input.subagent_type) {
69
+ const typePrompts = {
70
+ coder: 'You are a coding agent. Write clean, tested code.',
71
+ reviewer: 'You are a code reviewer. Analyze code for bugs and improvements.',
72
+ researcher: 'You are a research agent. Find and summarize information.',
73
+ tester: 'You are a testing agent. Write and run tests.',
74
+ planner: 'You are a planning agent. Break down tasks into steps.',
75
+ };
76
+ systemPrefix = typePrompts[input.subagent_type] || `You are a ${input.subagent_type} agent.`;
77
+ }
78
+
79
+ const fullPrompt = systemPrefix
80
+ ? `${systemPrefix}\n\nTask: ${input.prompt}`
81
+ : input.prompt;
82
+
83
+ const loop = createAgentLoop({
84
+ model,
85
+ tools,
86
+ permissions,
87
+ settings: {
88
+ stream: false,
89
+ stagnationDetection: !['0', 'false', 'no', 'off'].includes(
90
+ (process.env.KEPLER_STAGNATION_DETECTION ?? '0').toLowerCase(),
91
+ ),
92
+ stagnationThreshold: Number.parseInt(
93
+ process.env.KEPLER_STAGNATION_THRESHOLD
94
+ ?? '3',
95
+ 10,
96
+ ),
97
+ },
98
+ });
99
+
100
+ if (input.run_in_background) {
101
+ const bgId = ++AgentTool._nextBgId;
102
+ const entry = { id: bgId, status: 'running', result: null, prompt: input.prompt };
103
+ AgentTool._backgroundAgents.set(bgId, entry);
104
+
105
+ // Run in background
106
+ runSubagent(loop, fullPrompt).then(result => {
107
+ entry.status = 'completed';
108
+ entry.result = result;
109
+ }).catch(err => {
110
+ entry.status = 'error';
111
+ entry.result = err.message;
112
+ });
113
+
114
+ return `Subagent started in background: id=${bgId}`;
115
+ }
116
+
117
+ return runSubagent(loop, fullPrompt);
118
+ },
119
+ };
120
+
121
+ async function runSubagent(loop, prompt) {
122
+ const results = [];
123
+ try {
124
+ for await (const event of loop.run(prompt)) {
125
+ if (event.type === 'assistant' && event.content) {
126
+ results.push(event.content);
127
+ }
128
+ if (event.type === 'result') {
129
+ results.push(`[tool:${event.tool}] ${String(event.result).slice(0, 500)}`);
130
+ }
131
+ }
132
+ } catch (err) {
133
+ return `Subagent error: ${err.message}`;
134
+ }
135
+
136
+ return results.join('\n') || 'Subagent completed with no output.';
137
+ }
@@ -0,0 +1,61 @@
1
+ /**
2
+ * AskUser Tool — prompt the user with a question and return their response.
3
+ *
4
+ * Used when the agent needs clarification or confirmation from the user.
5
+ * In non-interactive mode, returns a default or times out.
6
+ */
7
+
8
+ import readline from 'readline';
9
+
10
+ export const AskUserTool = {
11
+ name: 'AskUser',
12
+ description: 'Ask the user a question and wait for their response.',
13
+ inputSchema: {
14
+ type: 'object',
15
+ properties: {
16
+ question: {
17
+ type: 'string',
18
+ description: 'The question to ask the user',
19
+ },
20
+ default_value: {
21
+ type: 'string',
22
+ description: 'Default value if user provides no input',
23
+ },
24
+ timeout: {
25
+ type: 'number',
26
+ description: 'Timeout in milliseconds (default: 60000)',
27
+ },
28
+ },
29
+ required: ['question'],
30
+ },
31
+
32
+ validateInput(input) {
33
+ return input.question ? [] : ['question is required'];
34
+ },
35
+
36
+ async call(input) {
37
+ // In non-interactive mode, return default
38
+ if (!process.stdin.isTTY) {
39
+ return input.default_value || '[non-interactive: no user input available]';
40
+ }
41
+
42
+ return new Promise((resolve) => {
43
+ const rl = readline.createInterface({
44
+ input: process.stdin,
45
+ output: process.stderr,
46
+ });
47
+
48
+ const timeout = setTimeout(() => {
49
+ rl.close();
50
+ resolve(input.default_value || '[timeout: no response]');
51
+ }, input.timeout || 60000);
52
+
53
+ process.stderr.write(`\n\x1b[36m? ${input.question}\x1b[0m\n> `);
54
+ rl.question('', (answer) => {
55
+ clearTimeout(timeout);
56
+ rl.close();
57
+ resolve(answer.trim() || input.default_value || '');
58
+ });
59
+ });
60
+ },
61
+ };
@@ -0,0 +1,148 @@
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
+
21
+ export const BashTool = {
22
+ name: 'Bash',
23
+ description: 'Execute a bash command and return its output.',
24
+ inputSchema: {
25
+ type: 'object',
26
+ properties: {
27
+ command: { type: 'string', description: 'The command to execute' },
28
+ timeout: { type: 'number', description: 'Timeout in ms (max 600000)', default: 120000 },
29
+ description: { type: 'string', description: 'Description of what this command does' },
30
+ run_in_background: { type: 'boolean', description: 'Run in background', default: false },
31
+ },
32
+ required: ['command'],
33
+ },
34
+ validateInput(input) {
35
+ const errors = [];
36
+ if (!input.command) errors.push('command is required');
37
+ return errors;
38
+ },
39
+ async call(input) {
40
+ const timeout = Math.min(input.timeout || 120000, 600000);
41
+
42
+ if (input.run_in_background) {
43
+ return runBackground(input.command);
44
+ }
45
+
46
+ return new Promise((resolve) => {
47
+ let stdout = '';
48
+ let stderr = '';
49
+ let killed = false;
50
+ let exitCode = null;
51
+
52
+ const proc = spawn('bash', ['-c', input.command], {
53
+ env: { ...process.env },
54
+ stdio: ['pipe', 'pipe', 'pipe'],
55
+ timeout: 0, // we handle timeout ourselves
56
+ });
57
+
58
+ proc.stdout.on('data', (chunk) => {
59
+ if (stdout.length < MAX_OUTPUT_BYTES) {
60
+ stdout += chunk.toString();
61
+ }
62
+ });
63
+
64
+ proc.stderr.on('data', (chunk) => {
65
+ if (stderr.length < MAX_OUTPUT_BYTES) {
66
+ stderr += chunk.toString();
67
+ }
68
+ });
69
+
70
+ // Timeout: SIGTERM first, then SIGKILL after 5s
71
+ const timer = setTimeout(() => {
72
+ killed = true;
73
+ proc.kill('SIGTERM');
74
+ setTimeout(() => {
75
+ try { proc.kill('SIGKILL'); } catch { /* already dead */ }
76
+ }, 5000);
77
+ }, timeout);
78
+
79
+ proc.on('close', (code) => {
80
+ clearTimeout(timer);
81
+ exitCode = code;
82
+
83
+ // Truncate if over limit
84
+ if (stdout.length > MAX_OUTPUT_BYTES) {
85
+ stdout = stdout.slice(0, MAX_OUTPUT_BYTES) + '\n[output truncated at 1MB]';
86
+ }
87
+ if (stderr.length > MAX_OUTPUT_BYTES) {
88
+ stderr = stderr.slice(0, MAX_OUTPUT_BYTES) + '\n[output truncated at 1MB]';
89
+ }
90
+
91
+ // Strip ANSI by default
92
+ stdout = stripAnsi(stdout);
93
+ stderr = stripAnsi(stderr);
94
+
95
+ if (killed) {
96
+ resolve(`Error: Command timed out after ${timeout}ms\n${stdout}\n${stderr}`.trim());
97
+ return;
98
+ }
99
+
100
+ const output = (stdout + (stderr ? '\n' + stderr : '')).trim();
101
+ if (code !== 0) {
102
+ resolve(`Exit code: ${code}\n${output}`.trim());
103
+ } else {
104
+ resolve(output || '(no output)');
105
+ }
106
+ });
107
+
108
+ proc.on('error', (err) => {
109
+ clearTimeout(timer);
110
+ resolve(`Error: ${err.message}`);
111
+ });
112
+
113
+ // Close stdin
114
+ proc.stdin.end();
115
+ });
116
+ },
117
+ };
118
+
119
+ // Background jobs store
120
+ const backgroundJobs = new Map();
121
+ let bgJobId = 0;
122
+
123
+ function runBackground(command) {
124
+ const id = ++bgJobId;
125
+ const proc = spawn('bash', ['-c', command], {
126
+ detached: true,
127
+ stdio: ['ignore', 'pipe', 'pipe'],
128
+ });
129
+
130
+ let stdout = '';
131
+ let stderr = '';
132
+ proc.stdout.on('data', (d) => { stdout += d.toString(); });
133
+ proc.stderr.on('data', (d) => { stderr += d.toString(); });
134
+
135
+ const job = { id, pid: proc.pid, command, status: 'running', stdout: '', stderr: '' };
136
+ backgroundJobs.set(id, job);
137
+
138
+ proc.on('close', (code) => {
139
+ job.status = code === 0 ? 'completed' : `exited(${code})`;
140
+ job.stdout = stripAnsi(stdout.slice(0, MAX_OUTPUT_BYTES));
141
+ job.stderr = stripAnsi(stderr.slice(0, MAX_OUTPUT_BYTES));
142
+ });
143
+
144
+ proc.unref();
145
+ return `Background job started: id=${id}, pid=${proc.pid}`;
146
+ }
147
+
148
+ 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
+ };