@axplusb/kepler 0.0.1 → 1.0.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 +82 -0
- package/package.json +36 -4
- package/pulse/app/activity/page.tsx +190 -0
- package/pulse/app/api/activity/route.ts +138 -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 +52 -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/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 +142 -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 +159 -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 +52 -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 +182 -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/claude-reader.ts +594 -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 +89 -0
- package/src/agents/parser.mjs +98 -0
- package/src/agents/teams.mjs +123 -0
- package/src/auth/oauth.mjs +220 -0
- package/src/auth/tarang-auth.mjs +277 -0
- package/src/config/cli-args.mjs +173 -0
- package/src/config/env.mjs +263 -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 +270 -0
- package/src/context/skeleton.mjs +134 -0
- package/src/core/agent-loop.mjs +480 -0
- package/src/core/approval.mjs +273 -0
- package/src/core/backend-url.mjs +57 -0
- package/src/core/cache.mjs +105 -0
- package/src/core/callback-client.mjs +149 -0
- package/src/core/checkpoints.mjs +142 -0
- package/src/core/context-manager.mjs +198 -0
- package/src/core/headless.mjs +168 -0
- package/src/core/hooks-manager.mjs +87 -0
- package/src/core/jsonl-writer.mjs +351 -0
- package/src/core/local-agent.mjs +429 -0
- package/src/core/local-store.mjs +325 -0
- package/src/core/mode-selector.mjs +51 -0
- package/src/core/output-filter.mjs +177 -0
- package/src/core/paths.mjs +98 -0
- package/src/core/pricing.mjs +314 -0
- package/src/core/providers.mjs +219 -0
- package/src/core/rate-limiter.mjs +119 -0
- package/src/core/safety.mjs +200 -0
- package/src/core/scheduler.mjs +173 -0
- package/src/core/session-manager.mjs +317 -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 +367 -0
- package/src/core/streaming.mjs +182 -0
- package/src/core/system-prompt.mjs +135 -0
- package/src/core/tool-executor.mjs +725 -0
- package/src/hooks/engine.mjs +162 -0
- package/src/index.mjs +370 -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/permissions/checker.mjs +57 -0
- package/src/permissions/command-classifier.mjs +573 -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/loader.mjs +147 -0
- package/src/skills/runner.mjs +55 -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 +421 -0
- package/src/terminal/main.mjs +150 -0
- package/src/terminal/repl.mjs +1484 -0
- package/src/terminal/tool-display.mjs +58 -0
- package/src/tools/agent.mjs +137 -0
- package/src/tools/ask-user.mjs +61 -0
- package/src/tools/bash.mjs +148 -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/read-mcp-resource.mjs +57 -0
- package/src/tools/read.mjs +138 -0
- package/src/tools/registry.mjs +132 -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/banner.mjs +237 -0
- package/src/ui/commands.mjs +499 -0
- package/src/ui/formatter.mjs +379 -0
- package/src/ui/markdown.mjs +278 -0
- package/src/ui/slash-commands.mjs +258 -0
- package/index.js +0 -1
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MultiEdit Tool — apply multiple edits to one or more files atomically.
|
|
3
|
+
*
|
|
4
|
+
* Each edit is an { file_path, old_string, new_string } triple.
|
|
5
|
+
* All edits are validated before any are applied.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import fs from 'fs';
|
|
9
|
+
import path from 'path';
|
|
10
|
+
|
|
11
|
+
export const MultiEditTool = {
|
|
12
|
+
name: 'MultiEdit',
|
|
13
|
+
description: 'Apply multiple file edits in a single operation. All edits are validated first.',
|
|
14
|
+
inputSchema: {
|
|
15
|
+
type: 'object',
|
|
16
|
+
properties: {
|
|
17
|
+
edits: {
|
|
18
|
+
type: 'array',
|
|
19
|
+
items: {
|
|
20
|
+
type: 'object',
|
|
21
|
+
properties: {
|
|
22
|
+
file_path: { type: 'string' },
|
|
23
|
+
old_string: { type: 'string' },
|
|
24
|
+
new_string: { type: 'string' },
|
|
25
|
+
},
|
|
26
|
+
required: ['file_path', 'old_string', 'new_string'],
|
|
27
|
+
},
|
|
28
|
+
description: 'Array of edits to apply',
|
|
29
|
+
},
|
|
30
|
+
},
|
|
31
|
+
required: ['edits'],
|
|
32
|
+
},
|
|
33
|
+
|
|
34
|
+
validateInput(input) {
|
|
35
|
+
const errors = [];
|
|
36
|
+
if (!input.edits || !Array.isArray(input.edits)) {
|
|
37
|
+
errors.push('edits must be an array');
|
|
38
|
+
return errors;
|
|
39
|
+
}
|
|
40
|
+
for (let i = 0; i < input.edits.length; i++) {
|
|
41
|
+
const e = input.edits[i];
|
|
42
|
+
if (!e.file_path) errors.push(`edit[${i}]: file_path required`);
|
|
43
|
+
if (!e.old_string) errors.push(`edit[${i}]: old_string required`);
|
|
44
|
+
if (e.old_string === e.new_string) errors.push(`edit[${i}]: old_string must differ from new_string`);
|
|
45
|
+
}
|
|
46
|
+
return errors;
|
|
47
|
+
},
|
|
48
|
+
|
|
49
|
+
async call(input) {
|
|
50
|
+
// Phase 1: Validate all edits
|
|
51
|
+
const fileContents = new Map();
|
|
52
|
+
const errors = [];
|
|
53
|
+
|
|
54
|
+
for (let i = 0; i < input.edits.length; i++) {
|
|
55
|
+
const edit = input.edits[i];
|
|
56
|
+
const filePath = path.resolve(edit.file_path);
|
|
57
|
+
|
|
58
|
+
if (!fileContents.has(filePath)) {
|
|
59
|
+
try {
|
|
60
|
+
fileContents.set(filePath, fs.readFileSync(filePath, 'utf-8'));
|
|
61
|
+
} catch (err) {
|
|
62
|
+
errors.push(`edit[${i}]: cannot read ${filePath}: ${err.message}`);
|
|
63
|
+
continue;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
let content = fileContents.get(filePath);
|
|
68
|
+
if (!content.includes(edit.old_string)) {
|
|
69
|
+
errors.push(`edit[${i}]: old_string not found in ${edit.file_path}`);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
if (errors.length > 0) {
|
|
74
|
+
return `Validation failed:\n${errors.join('\n')}`;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// Phase 2: Apply all edits
|
|
78
|
+
const applied = [];
|
|
79
|
+
for (const edit of input.edits) {
|
|
80
|
+
const filePath = path.resolve(edit.file_path);
|
|
81
|
+
let content = fileContents.get(filePath);
|
|
82
|
+
content = content.replace(edit.old_string, edit.new_string);
|
|
83
|
+
fileContents.set(filePath, content);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// Phase 3: Write all files
|
|
87
|
+
for (const [filePath, content] of fileContents) {
|
|
88
|
+
fs.writeFileSync(filePath, content);
|
|
89
|
+
applied.push(filePath);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
return `Applied ${input.edits.length} edits to ${applied.length} file(s):\n${applied.join('\n')}`;
|
|
93
|
+
},
|
|
94
|
+
};
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* NotebookEdit Tool — edit Jupyter notebook (.ipynb) cells.
|
|
3
|
+
*
|
|
4
|
+
* Supports operations:
|
|
5
|
+
* - insert: add a new cell at a position
|
|
6
|
+
* - replace: replace content of an existing cell
|
|
7
|
+
* - delete: remove a cell
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import fs from 'fs';
|
|
11
|
+
import path from 'path';
|
|
12
|
+
|
|
13
|
+
export const NotebookEditTool = {
|
|
14
|
+
name: 'NotebookEdit',
|
|
15
|
+
description: 'Edit Jupyter notebook cells. Supports insert, replace, and delete operations.',
|
|
16
|
+
inputSchema: {
|
|
17
|
+
type: 'object',
|
|
18
|
+
properties: {
|
|
19
|
+
notebook_path: { type: 'string', description: 'Path to the .ipynb file' },
|
|
20
|
+
operation: {
|
|
21
|
+
type: 'string',
|
|
22
|
+
enum: ['insert', 'replace', 'delete'],
|
|
23
|
+
description: 'Operation to perform',
|
|
24
|
+
},
|
|
25
|
+
cell_index: { type: 'number', description: 'Cell index (0-based)' },
|
|
26
|
+
cell_type: {
|
|
27
|
+
type: 'string',
|
|
28
|
+
enum: ['code', 'markdown', 'raw'],
|
|
29
|
+
description: 'Cell type (for insert/replace)',
|
|
30
|
+
},
|
|
31
|
+
source: { type: 'string', description: 'Cell content (for insert/replace)' },
|
|
32
|
+
},
|
|
33
|
+
required: ['notebook_path', 'operation', 'cell_index'],
|
|
34
|
+
},
|
|
35
|
+
|
|
36
|
+
validateInput(input) {
|
|
37
|
+
const errors = [];
|
|
38
|
+
if (!input.notebook_path) errors.push('notebook_path required');
|
|
39
|
+
if (!['insert', 'replace', 'delete'].includes(input.operation)) {
|
|
40
|
+
errors.push('operation must be insert, replace, or delete');
|
|
41
|
+
}
|
|
42
|
+
if (typeof input.cell_index !== 'number') errors.push('cell_index must be a number');
|
|
43
|
+
if (input.operation !== 'delete' && !input.source) errors.push('source required for insert/replace');
|
|
44
|
+
return errors;
|
|
45
|
+
},
|
|
46
|
+
|
|
47
|
+
async call(input) {
|
|
48
|
+
const filePath = path.resolve(input.notebook_path);
|
|
49
|
+
|
|
50
|
+
try {
|
|
51
|
+
const raw = fs.readFileSync(filePath, 'utf-8');
|
|
52
|
+
const notebook = JSON.parse(raw);
|
|
53
|
+
|
|
54
|
+
if (!notebook.cells || !Array.isArray(notebook.cells)) {
|
|
55
|
+
return 'Error: invalid notebook format (no cells array)';
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const cellType = input.cell_type || 'code';
|
|
59
|
+
const sourceLines = (input.source || '').split('\n').map(l => l + '\n');
|
|
60
|
+
|
|
61
|
+
switch (input.operation) {
|
|
62
|
+
case 'insert': {
|
|
63
|
+
const newCell = {
|
|
64
|
+
cell_type: cellType,
|
|
65
|
+
metadata: {},
|
|
66
|
+
source: sourceLines,
|
|
67
|
+
...(cellType === 'code' ? { outputs: [], execution_count: null } : {}),
|
|
68
|
+
};
|
|
69
|
+
const idx = Math.min(input.cell_index, notebook.cells.length);
|
|
70
|
+
notebook.cells.splice(idx, 0, newCell);
|
|
71
|
+
break;
|
|
72
|
+
}
|
|
73
|
+
case 'replace': {
|
|
74
|
+
if (input.cell_index >= notebook.cells.length) {
|
|
75
|
+
return `Error: cell_index ${input.cell_index} out of range (${notebook.cells.length} cells)`;
|
|
76
|
+
}
|
|
77
|
+
notebook.cells[input.cell_index].source = sourceLines;
|
|
78
|
+
if (cellType) notebook.cells[input.cell_index].cell_type = cellType;
|
|
79
|
+
break;
|
|
80
|
+
}
|
|
81
|
+
case 'delete': {
|
|
82
|
+
if (input.cell_index >= notebook.cells.length) {
|
|
83
|
+
return `Error: cell_index ${input.cell_index} out of range (${notebook.cells.length} cells)`;
|
|
84
|
+
}
|
|
85
|
+
notebook.cells.splice(input.cell_index, 1);
|
|
86
|
+
break;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
fs.writeFileSync(filePath, JSON.stringify(notebook, null, 1));
|
|
91
|
+
return `Notebook updated: ${input.operation} at cell ${input.cell_index} (${notebook.cells.length} cells total)`;
|
|
92
|
+
} catch (err) {
|
|
93
|
+
return `Error: ${err.message}`;
|
|
94
|
+
}
|
|
95
|
+
},
|
|
96
|
+
};
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ReadMcpResource Tool — read a resource from an MCP server.
|
|
3
|
+
*
|
|
4
|
+
* MCP resources are identified by URI and can contain text, JSON,
|
|
5
|
+
* or binary data. This tool reads text/JSON resources.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
export const ReadMcpResourceTool = {
|
|
9
|
+
name: 'ReadMcpResource',
|
|
10
|
+
description: 'Read a resource from an MCP server by URI.',
|
|
11
|
+
inputSchema: {
|
|
12
|
+
type: 'object',
|
|
13
|
+
properties: {
|
|
14
|
+
uri: {
|
|
15
|
+
type: 'string',
|
|
16
|
+
description: 'Resource URI (e.g., "file:///path" or "mcp://server/resource")',
|
|
17
|
+
},
|
|
18
|
+
server: {
|
|
19
|
+
type: 'string',
|
|
20
|
+
description: 'MCP server name (if URI does not specify)',
|
|
21
|
+
},
|
|
22
|
+
},
|
|
23
|
+
required: ['uri'],
|
|
24
|
+
},
|
|
25
|
+
|
|
26
|
+
// Set by the MCP integration layer
|
|
27
|
+
_mcpClients: null,
|
|
28
|
+
|
|
29
|
+
validateInput(input) {
|
|
30
|
+
return input.uri ? [] : ['uri is required'];
|
|
31
|
+
},
|
|
32
|
+
|
|
33
|
+
async call(input) {
|
|
34
|
+
if (!this._mcpClients || this._mcpClients.length === 0) {
|
|
35
|
+
return 'No MCP servers connected. Configure MCP servers in settings.';
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
for (const client of this._mcpClients) {
|
|
39
|
+
try {
|
|
40
|
+
const result = await client.readResource(input.uri);
|
|
41
|
+
if (result) {
|
|
42
|
+
if (typeof result === 'string') return result;
|
|
43
|
+
if (result.contents && Array.isArray(result.contents)) {
|
|
44
|
+
return result.contents
|
|
45
|
+
.map(c => c.text || JSON.stringify(c))
|
|
46
|
+
.join('\n');
|
|
47
|
+
}
|
|
48
|
+
return JSON.stringify(result, null, 2);
|
|
49
|
+
}
|
|
50
|
+
} catch {
|
|
51
|
+
// Try next client
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
return `Resource not found: ${input.uri}`;
|
|
56
|
+
},
|
|
57
|
+
};
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Read Tool — matches Claude Code's exact behavior.
|
|
3
|
+
*
|
|
4
|
+
* Features:
|
|
5
|
+
* - pages parameter for PDF files
|
|
6
|
+
* - Binary file detection
|
|
7
|
+
* - Default 2000 line limit
|
|
8
|
+
* - Line number prefix (cat -n format)
|
|
9
|
+
* - Graceful file not found handling
|
|
10
|
+
* - Tracks read files for Edit/Write verification
|
|
11
|
+
*/
|
|
12
|
+
import fs from 'fs';
|
|
13
|
+
import path from 'path';
|
|
14
|
+
|
|
15
|
+
const DEFAULT_LIMIT = 2000;
|
|
16
|
+
|
|
17
|
+
// Track which files have been read (used by Edit and Write tools)
|
|
18
|
+
const readFiles = new Set();
|
|
19
|
+
|
|
20
|
+
export function hasBeenRead(filePath) {
|
|
21
|
+
return readFiles.has(path.resolve(filePath));
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function markRead(filePath) {
|
|
25
|
+
readFiles.add(path.resolve(filePath));
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// Binary detection: check for null bytes in first 8KB
|
|
29
|
+
function isBinary(buffer) {
|
|
30
|
+
const len = Math.min(buffer.length, 8192);
|
|
31
|
+
for (let i = 0; i < len; i++) {
|
|
32
|
+
if (buffer[i] === 0) return true;
|
|
33
|
+
}
|
|
34
|
+
return false;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export const ReadTool = {
|
|
38
|
+
name: 'Read',
|
|
39
|
+
description: 'Read a file from the local filesystem.',
|
|
40
|
+
inputSchema: {
|
|
41
|
+
type: 'object',
|
|
42
|
+
properties: {
|
|
43
|
+
file_path: { type: 'string', description: 'Absolute path to the file' },
|
|
44
|
+
offset: { type: 'number', description: 'Line number to start reading from (0-based)' },
|
|
45
|
+
limit: { type: 'number', description: 'Number of lines to read (default 2000)' },
|
|
46
|
+
pages: { type: 'string', description: 'Page range for PDF files (e.g. "1-5")' },
|
|
47
|
+
},
|
|
48
|
+
required: ['file_path'],
|
|
49
|
+
},
|
|
50
|
+
validateInput(input) {
|
|
51
|
+
const errors = [];
|
|
52
|
+
if (!input.file_path) errors.push('file_path is required');
|
|
53
|
+
return errors;
|
|
54
|
+
},
|
|
55
|
+
async call(input) {
|
|
56
|
+
const filePath = path.resolve(input.file_path);
|
|
57
|
+
|
|
58
|
+
// Check existence
|
|
59
|
+
if (!fs.existsSync(filePath)) {
|
|
60
|
+
return `Error: File not found: ${filePath}`;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// Check if directory
|
|
64
|
+
const stat = fs.statSync(filePath);
|
|
65
|
+
if (stat.isDirectory()) {
|
|
66
|
+
return `Error: ${filePath} is a directory, not a file. Use Bash with ls to list directory contents.`;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// PDF handling
|
|
70
|
+
if (filePath.endsWith('.pdf')) {
|
|
71
|
+
return readPdf(filePath, input.pages);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// Binary detection
|
|
75
|
+
try {
|
|
76
|
+
const fd = fs.openSync(filePath, 'r');
|
|
77
|
+
const buf = Buffer.alloc(8192);
|
|
78
|
+
const bytesRead = fs.readSync(fd, buf, 0, 8192, 0);
|
|
79
|
+
fs.closeSync(fd);
|
|
80
|
+
if (isBinary(buf.subarray(0, bytesRead))) {
|
|
81
|
+
return `Error: ${filePath} appears to be a binary file. Cannot display binary content.`;
|
|
82
|
+
}
|
|
83
|
+
} catch (e) {
|
|
84
|
+
return `Error: ${e.message}`;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
try {
|
|
88
|
+
const content = fs.readFileSync(filePath, 'utf-8');
|
|
89
|
+
const lines = content.split('\n');
|
|
90
|
+
const start = input.offset || 0;
|
|
91
|
+
const limit = input.limit || DEFAULT_LIMIT;
|
|
92
|
+
const end = Math.min(start + limit, lines.length);
|
|
93
|
+
|
|
94
|
+
// Track as read
|
|
95
|
+
readFiles.add(filePath);
|
|
96
|
+
|
|
97
|
+
// Handle empty files
|
|
98
|
+
if (content === '' || (content.length === 0)) {
|
|
99
|
+
return '[File exists but is empty]';
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
const output = lines
|
|
103
|
+
.slice(start, end)
|
|
104
|
+
.map((l, i) => `${start + i + 1}\t${l}`)
|
|
105
|
+
.join('\n');
|
|
106
|
+
|
|
107
|
+
if (end < lines.length) {
|
|
108
|
+
return output + `\n\n[File has ${lines.length} lines total. Showing lines ${start + 1}-${end}. Use offset/limit for more.]`;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
return output;
|
|
112
|
+
} catch (e) {
|
|
113
|
+
return `Error: ${e.message}`;
|
|
114
|
+
}
|
|
115
|
+
},
|
|
116
|
+
};
|
|
117
|
+
|
|
118
|
+
function readPdf(filePath, pages) {
|
|
119
|
+
// PDF reading requires external tools; provide a best-effort
|
|
120
|
+
// text extraction using a simple approach
|
|
121
|
+
try {
|
|
122
|
+
const { execSync } = await_import_child_process();
|
|
123
|
+
const pageArg = pages ? `-f ${pages.split('-')[0]} -l ${pages.split('-').pop()}` : '-f 1 -l 20';
|
|
124
|
+
const text = execSync(`pdftotext ${pageArg} "${filePath}" - 2>/dev/null`, {
|
|
125
|
+
encoding: 'utf-8',
|
|
126
|
+
timeout: 30000,
|
|
127
|
+
maxBuffer: 1024 * 1024,
|
|
128
|
+
});
|
|
129
|
+
return text || `[PDF file at ${filePath} — could not extract text. Use a PDF viewer.]`;
|
|
130
|
+
} catch {
|
|
131
|
+
return `[PDF file at ${filePath} — pdftotext not available. Install poppler-utils for PDF support.]`;
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// Lazy import helper
|
|
136
|
+
function await_import_child_process() {
|
|
137
|
+
return require('child_process');
|
|
138
|
+
}
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tool Registry — validateInput/call interface.
|
|
3
|
+
* Mirrors Claude Code's tool dispatch system.
|
|
4
|
+
* Registers all 25+ built-in tools.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { BashTool } from './bash.mjs';
|
|
8
|
+
import { ReadTool } from './read.mjs';
|
|
9
|
+
import { EditTool } from './edit.mjs';
|
|
10
|
+
import { WriteTool } from './write.mjs';
|
|
11
|
+
import { GlobTool } from './glob.mjs';
|
|
12
|
+
import { GrepTool } from './grep.mjs';
|
|
13
|
+
import { AgentTool } from './agent.mjs';
|
|
14
|
+
import { WebFetchTool } from './web-fetch.mjs';
|
|
15
|
+
import { WebSearchTool } from './web-search.mjs';
|
|
16
|
+
import { TodoWriteTool } from './todo-write.mjs';
|
|
17
|
+
import { NotebookEditTool } from './notebook-edit.mjs';
|
|
18
|
+
import { MultiEditTool } from './multi-edit.mjs';
|
|
19
|
+
import { LsTool } from './ls.mjs';
|
|
20
|
+
import { ToolSearchTool } from './tool-search.mjs';
|
|
21
|
+
import { AskUserTool } from './ask-user.mjs';
|
|
22
|
+
import { EnterWorktreeTool } from './enter-worktree.mjs';
|
|
23
|
+
import { ExitWorktreeTool } from './exit-worktree.mjs';
|
|
24
|
+
import { SkillTool } from './skill.mjs';
|
|
25
|
+
import { SendMessageTool } from './send-message.mjs';
|
|
26
|
+
import { RemoteTriggerTool } from './remote-trigger.mjs';
|
|
27
|
+
import { CronCreateTool } from './cron-create.mjs';
|
|
28
|
+
import { CronDeleteTool } from './cron-delete.mjs';
|
|
29
|
+
import { CronListTool } from './cron-list.mjs';
|
|
30
|
+
import { LspTool } from './lsp.mjs';
|
|
31
|
+
import { ReadMcpResourceTool } from './read-mcp-resource.mjs';
|
|
32
|
+
import { ContextRetriever } from '../context/retriever.mjs';
|
|
33
|
+
|
|
34
|
+
// Tools that modify files — trigger index update after success
|
|
35
|
+
const WRITE_TOOLS = new Set(['Edit', 'Write', 'MultiEdit']);
|
|
36
|
+
|
|
37
|
+
const BUILTIN_TOOLS = [
|
|
38
|
+
BashTool,
|
|
39
|
+
ReadTool,
|
|
40
|
+
EditTool,
|
|
41
|
+
WriteTool,
|
|
42
|
+
GlobTool,
|
|
43
|
+
GrepTool,
|
|
44
|
+
AgentTool,
|
|
45
|
+
WebFetchTool,
|
|
46
|
+
WebSearchTool,
|
|
47
|
+
TodoWriteTool,
|
|
48
|
+
NotebookEditTool,
|
|
49
|
+
MultiEditTool,
|
|
50
|
+
LsTool,
|
|
51
|
+
ToolSearchTool,
|
|
52
|
+
AskUserTool,
|
|
53
|
+
EnterWorktreeTool,
|
|
54
|
+
ExitWorktreeTool,
|
|
55
|
+
SkillTool,
|
|
56
|
+
SendMessageTool,
|
|
57
|
+
RemoteTriggerTool,
|
|
58
|
+
CronCreateTool,
|
|
59
|
+
CronDeleteTool,
|
|
60
|
+
CronListTool,
|
|
61
|
+
LspTool,
|
|
62
|
+
ReadMcpResourceTool,
|
|
63
|
+
];
|
|
64
|
+
|
|
65
|
+
export function createToolRegistry() {
|
|
66
|
+
const tools = new Map();
|
|
67
|
+
for (const Tool of BUILTIN_TOOLS) {
|
|
68
|
+
tools.set(Tool.name, Tool);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const registry = {
|
|
72
|
+
list() {
|
|
73
|
+
return [...tools.values()].map(t => ({
|
|
74
|
+
name: t.name,
|
|
75
|
+
description: t.description,
|
|
76
|
+
input_schema: t.inputSchema,
|
|
77
|
+
}));
|
|
78
|
+
},
|
|
79
|
+
|
|
80
|
+
async call(name, input) {
|
|
81
|
+
const tool = tools.get(name);
|
|
82
|
+
if (!tool) throw new Error(`Unknown tool: ${name}`);
|
|
83
|
+
const errors = tool.validateInput?.(input) || [];
|
|
84
|
+
if (errors.length > 0) return `Validation error: ${errors.join(', ')}`;
|
|
85
|
+
const result = await tool.call(input);
|
|
86
|
+
|
|
87
|
+
// After successful file writes, update BM25 index incrementally
|
|
88
|
+
// so search stays fresh within the session (~5-50ms, non-blocking)
|
|
89
|
+
if (WRITE_TOOLS.has(name) && typeof result === 'string' && !result.startsWith('Error')) {
|
|
90
|
+
const filePath = input.file_path;
|
|
91
|
+
if (filePath) {
|
|
92
|
+
try {
|
|
93
|
+
const retriever = new ContextRetriever();
|
|
94
|
+
retriever.updateFile(filePath);
|
|
95
|
+
} catch { /* index update is best-effort */ }
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
return result;
|
|
100
|
+
},
|
|
101
|
+
|
|
102
|
+
register(tool) {
|
|
103
|
+
tools.set(tool.name, tool);
|
|
104
|
+
},
|
|
105
|
+
|
|
106
|
+
get(name) {
|
|
107
|
+
return tools.get(name);
|
|
108
|
+
},
|
|
109
|
+
|
|
110
|
+
has(name) {
|
|
111
|
+
return tools.has(name);
|
|
112
|
+
},
|
|
113
|
+
|
|
114
|
+
registerMcpTools(mcpTools, callFn) {
|
|
115
|
+
ToolSearchTool._mcpTools = mcpTools;
|
|
116
|
+
|
|
117
|
+
for (const mcpTool of mcpTools) {
|
|
118
|
+
const wrapper = {
|
|
119
|
+
name: mcpTool.name,
|
|
120
|
+
description: mcpTool.description || '',
|
|
121
|
+
inputSchema: mcpTool.inputSchema || { type: 'object', properties: {} },
|
|
122
|
+
validateInput() { return []; },
|
|
123
|
+
async call(input) { return callFn(mcpTool.name, input); },
|
|
124
|
+
};
|
|
125
|
+
tools.set(mcpTool.name, wrapper);
|
|
126
|
+
}
|
|
127
|
+
},
|
|
128
|
+
};
|
|
129
|
+
|
|
130
|
+
ToolSearchTool._registry = registry;
|
|
131
|
+
return registry;
|
|
132
|
+
}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* RemoteTrigger Tool — trigger remote execution of a task.
|
|
3
|
+
*
|
|
4
|
+
* Sends a task to a remote agent endpoint for execution.
|
|
5
|
+
* Used for distributed agent workflows and scheduled tasks.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
export const RemoteTriggerTool = {
|
|
9
|
+
name: 'RemoteTrigger',
|
|
10
|
+
description: 'Trigger remote execution of a task on a remote agent.',
|
|
11
|
+
inputSchema: {
|
|
12
|
+
type: 'object',
|
|
13
|
+
properties: {
|
|
14
|
+
endpoint: {
|
|
15
|
+
type: 'string',
|
|
16
|
+
description: 'Remote agent endpoint URL',
|
|
17
|
+
},
|
|
18
|
+
task: {
|
|
19
|
+
type: 'string',
|
|
20
|
+
description: 'Task description or prompt to execute remotely',
|
|
21
|
+
},
|
|
22
|
+
timeout: {
|
|
23
|
+
type: 'number',
|
|
24
|
+
description: 'Timeout in milliseconds (default: 300000)',
|
|
25
|
+
},
|
|
26
|
+
async: {
|
|
27
|
+
type: 'boolean',
|
|
28
|
+
description: 'Fire-and-forget mode (default: false)',
|
|
29
|
+
},
|
|
30
|
+
},
|
|
31
|
+
required: ['task'],
|
|
32
|
+
},
|
|
33
|
+
|
|
34
|
+
validateInput(input) {
|
|
35
|
+
return input.task ? [] : ['task is required'];
|
|
36
|
+
},
|
|
37
|
+
|
|
38
|
+
async call(input) {
|
|
39
|
+
const endpoint = input.endpoint || process.env.REMOTE_AGENT_URL;
|
|
40
|
+
if (!endpoint) {
|
|
41
|
+
return 'No remote endpoint configured. Set endpoint parameter or REMOTE_AGENT_URL env var.';
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
try {
|
|
45
|
+
const controller = new AbortController();
|
|
46
|
+
const timeout = setTimeout(
|
|
47
|
+
() => controller.abort(),
|
|
48
|
+
input.timeout || 300000
|
|
49
|
+
);
|
|
50
|
+
|
|
51
|
+
const res = await fetch(endpoint, {
|
|
52
|
+
method: 'POST',
|
|
53
|
+
headers: {
|
|
54
|
+
'Content-Type': 'application/json',
|
|
55
|
+
...(process.env.REMOTE_AGENT_TOKEN && {
|
|
56
|
+
Authorization: `Bearer ${process.env.REMOTE_AGENT_TOKEN}`,
|
|
57
|
+
}),
|
|
58
|
+
},
|
|
59
|
+
body: JSON.stringify({
|
|
60
|
+
task: input.task,
|
|
61
|
+
async: input.async || false,
|
|
62
|
+
}),
|
|
63
|
+
signal: controller.signal,
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
clearTimeout(timeout);
|
|
67
|
+
|
|
68
|
+
if (!res.ok) {
|
|
69
|
+
return `Remote error: HTTP ${res.status} ${res.statusText}`;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
if (input.async) {
|
|
73
|
+
return `Task submitted to ${endpoint} (async mode)`;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const data = await res.json();
|
|
77
|
+
return typeof data.result === 'string'
|
|
78
|
+
? data.result
|
|
79
|
+
: JSON.stringify(data, null, 2);
|
|
80
|
+
} catch (err) {
|
|
81
|
+
return `Remote trigger error: ${err.message}`;
|
|
82
|
+
}
|
|
83
|
+
},
|
|
84
|
+
};
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SendMessage Tool — send a message to a teammate agent.
|
|
3
|
+
*
|
|
4
|
+
* Used in multi-agent (teams) mode to communicate between agents.
|
|
5
|
+
* Messages are stored in a shared message queue.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
const messageQueue = new Map(); // agentId -> messages[]
|
|
9
|
+
|
|
10
|
+
export const SendMessageTool = {
|
|
11
|
+
name: 'SendMessage',
|
|
12
|
+
description: 'Send a message to a teammate agent in multi-agent mode.',
|
|
13
|
+
inputSchema: {
|
|
14
|
+
type: 'object',
|
|
15
|
+
properties: {
|
|
16
|
+
to: {
|
|
17
|
+
type: 'string',
|
|
18
|
+
description: 'Target agent ID or name',
|
|
19
|
+
},
|
|
20
|
+
content: {
|
|
21
|
+
type: 'string',
|
|
22
|
+
description: 'Message content to send',
|
|
23
|
+
},
|
|
24
|
+
type: {
|
|
25
|
+
type: 'string',
|
|
26
|
+
enum: ['request', 'response', 'notification', 'handoff'],
|
|
27
|
+
description: 'Message type (default: notification)',
|
|
28
|
+
},
|
|
29
|
+
},
|
|
30
|
+
required: ['to', 'content'],
|
|
31
|
+
},
|
|
32
|
+
|
|
33
|
+
validateInput(input) {
|
|
34
|
+
const errors = [];
|
|
35
|
+
if (!input.to) errors.push('to is required');
|
|
36
|
+
if (!input.content) errors.push('content is required');
|
|
37
|
+
return errors;
|
|
38
|
+
},
|
|
39
|
+
|
|
40
|
+
async call(input) {
|
|
41
|
+
const msg = {
|
|
42
|
+
id: `msg_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`,
|
|
43
|
+
from: process.env.AGENT_ID || 'main',
|
|
44
|
+
to: input.to,
|
|
45
|
+
type: input.type || 'notification',
|
|
46
|
+
content: input.content,
|
|
47
|
+
timestamp: new Date().toISOString(),
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
if (!messageQueue.has(input.to)) {
|
|
51
|
+
messageQueue.set(input.to, []);
|
|
52
|
+
}
|
|
53
|
+
messageQueue.get(input.to).push(msg);
|
|
54
|
+
|
|
55
|
+
return `Message sent to "${input.to}" (id: ${msg.id}, type: ${msg.type})`;
|
|
56
|
+
},
|
|
57
|
+
|
|
58
|
+
// Helper: receive messages for an agent
|
|
59
|
+
receive(agentId) {
|
|
60
|
+
const messages = messageQueue.get(agentId) || [];
|
|
61
|
+
messageQueue.set(agentId, []);
|
|
62
|
+
return messages;
|
|
63
|
+
},
|
|
64
|
+
};
|