@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,641 @@
|
|
|
1
|
+
import * as crypto from 'node:crypto';
|
|
2
|
+
import * as fs from 'node:fs';
|
|
3
|
+
import * as os from 'node:os';
|
|
4
|
+
import * as path from 'node:path';
|
|
5
|
+
import { spawnSync } from 'node:child_process';
|
|
6
|
+
import { ContextRetriever } from '../context/retriever.mjs';
|
|
7
|
+
import { buildProjectSkeleton } from '../context/skeleton.mjs';
|
|
8
|
+
import { indexDir as getIndexDir, projectConfigDir, bahulamHome } from '../core/paths.mjs';
|
|
9
|
+
|
|
10
|
+
const RESOURCE_FILE = 'project-resource.json';
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Expand "~" and trim surrounding quotes/whitespace. Does NOT unescape shell
|
|
14
|
+
* meta characters — that is a separate, last-resort step done only if the
|
|
15
|
+
* literal path does not resolve.
|
|
16
|
+
*/
|
|
17
|
+
function normalizePathInput(p) {
|
|
18
|
+
let s = String(p || '').trim();
|
|
19
|
+
// Trim balanced surrounding quotes.
|
|
20
|
+
if ((s.startsWith('"') && s.endsWith('"')) ||
|
|
21
|
+
(s.startsWith("'") && s.endsWith("'"))) {
|
|
22
|
+
s = s.slice(1, -1);
|
|
23
|
+
}
|
|
24
|
+
// Tilde expansion (~ or ~/...).
|
|
25
|
+
if (s === '~' || s.startsWith('~/')) {
|
|
26
|
+
s = path.join(os.homedir(), s.slice(1));
|
|
27
|
+
}
|
|
28
|
+
return s;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Replace common shell escape sequences with their literal characters. Used
|
|
33
|
+
* as a fallback when the literal path does not resolve — the agent may have
|
|
34
|
+
* pasted a copy of what they would type at a shell prompt.
|
|
35
|
+
*/
|
|
36
|
+
function unescapeShellPath(p) {
|
|
37
|
+
return String(p || '').replace(/\\([ \t()&$;'"])/g, '$1');
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const LANGUAGE_EXTENSIONS = new Map([
|
|
41
|
+
['.py', 'Python'],
|
|
42
|
+
['.js', 'JavaScript'],
|
|
43
|
+
['.mjs', 'JavaScript'],
|
|
44
|
+
['.ts', 'TypeScript'],
|
|
45
|
+
['.tsx', 'TypeScript'],
|
|
46
|
+
['.go', 'Go'],
|
|
47
|
+
['.rs', 'Rust'],
|
|
48
|
+
['.java', 'Java'],
|
|
49
|
+
['.rb', 'Ruby'],
|
|
50
|
+
['.c', 'C'],
|
|
51
|
+
['.cpp', 'C++'],
|
|
52
|
+
]);
|
|
53
|
+
const IGNORED_DIRS = new Set([
|
|
54
|
+
'.git', '.bahulam', '.next', '.venv', '__pycache__',
|
|
55
|
+
'build', 'dist', 'node_modules', 'venv',
|
|
56
|
+
]);
|
|
57
|
+
|
|
58
|
+
function projectId(canonicalPath) {
|
|
59
|
+
return crypto.createHash('sha256').update(canonicalPath).digest('hex').slice(0, 12);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function isWithin(root, candidate) {
|
|
63
|
+
const relative = path.relative(root, candidate);
|
|
64
|
+
return relative === '' || (!relative.startsWith('..') && !path.isAbsolute(relative));
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function uniqueValues(values) {
|
|
68
|
+
return [...new Set(values.filter(Boolean))];
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function canonicalRoot(rootPath) {
|
|
72
|
+
const resolved = path.resolve(normalizePathInput(rootPath));
|
|
73
|
+
try {
|
|
74
|
+
return fs.realpathSync(resolved);
|
|
75
|
+
} catch {
|
|
76
|
+
return resolved;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function canonicalizeCandidate(candidate) {
|
|
81
|
+
if (fs.existsSync(candidate)) return fs.realpathSync(candidate);
|
|
82
|
+
|
|
83
|
+
const missing = [];
|
|
84
|
+
let parent = candidate;
|
|
85
|
+
while (!fs.existsSync(parent)) {
|
|
86
|
+
const next = path.dirname(parent);
|
|
87
|
+
if (next === parent) break;
|
|
88
|
+
missing.unshift(path.basename(parent));
|
|
89
|
+
parent = next;
|
|
90
|
+
}
|
|
91
|
+
return path.join(fs.realpathSync(parent), ...missing);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function projectFingerprint(projectDir) {
|
|
95
|
+
const hash = crypto.createHash('sha256');
|
|
96
|
+
const queue = [projectDir];
|
|
97
|
+
|
|
98
|
+
while (queue.length > 0) {
|
|
99
|
+
const dir = queue.shift();
|
|
100
|
+
let entries;
|
|
101
|
+
try {
|
|
102
|
+
entries = fs.readdirSync(dir, { withFileTypes: true })
|
|
103
|
+
.sort((a, b) => a.name.localeCompare(b.name));
|
|
104
|
+
} catch {
|
|
105
|
+
continue;
|
|
106
|
+
}
|
|
107
|
+
for (const entry of entries) {
|
|
108
|
+
if (entry.name.startsWith('.') || IGNORED_DIRS.has(entry.name)) continue;
|
|
109
|
+
const fullPath = path.join(dir, entry.name);
|
|
110
|
+
if (entry.isDirectory()) {
|
|
111
|
+
queue.push(fullPath);
|
|
112
|
+
continue;
|
|
113
|
+
}
|
|
114
|
+
if (!entry.isFile()) continue;
|
|
115
|
+
try {
|
|
116
|
+
const stat = fs.statSync(fullPath);
|
|
117
|
+
hash.update(
|
|
118
|
+
`${path.relative(projectDir, fullPath)}:${stat.size}:${Math.trunc(stat.mtimeMs)}\n`
|
|
119
|
+
);
|
|
120
|
+
} catch { /* file changed during scan */ }
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
return hash.digest('hex').slice(0, 16);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function detectLanguages(projectDir) {
|
|
127
|
+
const counts = new Map();
|
|
128
|
+
const queue = [projectDir];
|
|
129
|
+
let scanned = 0;
|
|
130
|
+
|
|
131
|
+
while (queue.length > 0 && scanned < 500) {
|
|
132
|
+
const dir = queue.shift();
|
|
133
|
+
let entries;
|
|
134
|
+
try {
|
|
135
|
+
entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
136
|
+
} catch {
|
|
137
|
+
continue;
|
|
138
|
+
}
|
|
139
|
+
for (const entry of entries) {
|
|
140
|
+
if (scanned >= 500) break;
|
|
141
|
+
if (entry.name.startsWith('.') || IGNORED_DIRS.has(entry.name)) continue;
|
|
142
|
+
const fullPath = path.join(dir, entry.name);
|
|
143
|
+
if (entry.isDirectory()) {
|
|
144
|
+
queue.push(fullPath);
|
|
145
|
+
} else if (entry.isFile()) {
|
|
146
|
+
scanned++;
|
|
147
|
+
const language = LANGUAGE_EXTENSIONS.get(path.extname(entry.name));
|
|
148
|
+
if (language) counts.set(language, (counts.get(language) || 0) + 1);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
return [...counts.entries()]
|
|
154
|
+
.sort((a, b) => b[1] - a[1])
|
|
155
|
+
.slice(0, 4)
|
|
156
|
+
.map(([language]) => language);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
function detectCommands(projectDir) {
|
|
160
|
+
const commands = {};
|
|
161
|
+
try {
|
|
162
|
+
const pkg = JSON.parse(fs.readFileSync(path.join(projectDir, 'package.json'), 'utf-8'));
|
|
163
|
+
if (pkg.scripts?.test) commands.test = 'npm test';
|
|
164
|
+
if (pkg.scripts?.build) commands.build = 'npm run build';
|
|
165
|
+
if (pkg.scripts?.lint) commands.lint = 'npm run lint';
|
|
166
|
+
} catch { /* no package.json */ }
|
|
167
|
+
|
|
168
|
+
if (
|
|
169
|
+
fs.existsSync(path.join(projectDir, 'pyproject.toml')) ||
|
|
170
|
+
fs.existsSync(path.join(projectDir, 'setup.py'))
|
|
171
|
+
) {
|
|
172
|
+
if (!commands.test) commands.test = 'python -m pytest';
|
|
173
|
+
}
|
|
174
|
+
if (fs.existsSync(path.join(projectDir, 'Makefile')) && !commands.build) {
|
|
175
|
+
commands.build = 'make';
|
|
176
|
+
}
|
|
177
|
+
return commands;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
function commandVersion(command, args = ['--version']) {
|
|
181
|
+
try {
|
|
182
|
+
const result = spawnSync(command, args, {
|
|
183
|
+
encoding: 'utf-8',
|
|
184
|
+
timeout: 2000,
|
|
185
|
+
windowsHide: true,
|
|
186
|
+
});
|
|
187
|
+
if (result.error || result.status !== 0) return '';
|
|
188
|
+
return `${result.stdout || result.stderr || ''}`.trim().split('\n')[0].slice(0, 120);
|
|
189
|
+
} catch {
|
|
190
|
+
return '';
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
function detectEnvironment() {
|
|
195
|
+
const candidates = [
|
|
196
|
+
['python', 'python3'],
|
|
197
|
+
['node', 'node'],
|
|
198
|
+
['git', 'git'],
|
|
199
|
+
['npm', 'npm'],
|
|
200
|
+
['uv', 'uv'],
|
|
201
|
+
['pytest', 'pytest'],
|
|
202
|
+
['docker', 'docker'],
|
|
203
|
+
];
|
|
204
|
+
const tools = {};
|
|
205
|
+
for (const [name, command] of candidates) {
|
|
206
|
+
const version = commandVersion(command);
|
|
207
|
+
if (version) tools[name] = version;
|
|
208
|
+
}
|
|
209
|
+
return {
|
|
210
|
+
platform: os.platform(),
|
|
211
|
+
release: os.release(),
|
|
212
|
+
architecture: os.arch(),
|
|
213
|
+
shell: process.env.SHELL || process.env.ComSpec || '',
|
|
214
|
+
node: process.version,
|
|
215
|
+
tools,
|
|
216
|
+
};
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
function formatResource(resource) {
|
|
220
|
+
const lines = [
|
|
221
|
+
`Project registered: ${resource.name} (project_id=${resource.project_id})`,
|
|
222
|
+
`Root: ${resource.root}`,
|
|
223
|
+
`Languages: ${resource.languages.join(', ') || 'unknown'}`,
|
|
224
|
+
`Index: ${resource.index_status} (${resource.index_version})`,
|
|
225
|
+
];
|
|
226
|
+
if (resource.environment) {
|
|
227
|
+
const env = resource.environment;
|
|
228
|
+
lines.push(
|
|
229
|
+
`Environment: ${env.platform || 'unknown'} ${env.release || ''} ` +
|
|
230
|
+
`(${env.architecture || 'unknown'}), shell=${env.shell || 'unknown'}, node=${env.node || 'unknown'}`
|
|
231
|
+
);
|
|
232
|
+
const toolVersions = Object.entries(env.tools || {});
|
|
233
|
+
if (toolVersions.length > 0) {
|
|
234
|
+
lines.push(`Available tools: ${toolVersions.map(([name, version]) =>
|
|
235
|
+
`${name}=${version}`).join(', ')}`);
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
if (Object.keys(resource.commands).length > 0) {
|
|
239
|
+
lines.push(`Commands: ${Object.entries(resource.commands)
|
|
240
|
+
.map(([name, command]) => `${name}="${command}"`).join(', ')}`);
|
|
241
|
+
}
|
|
242
|
+
if (resource.skills_index && resource.skills_index.length > 0) {
|
|
243
|
+
lines.push(`Skills: ${resource.skills_index.map(s => s.name).join(', ')}`);
|
|
244
|
+
}
|
|
245
|
+
lines.push('', resource.overview);
|
|
246
|
+
if (resource.project_context) {
|
|
247
|
+
lines.push('', '--- Project Context ---', resource.project_context);
|
|
248
|
+
}
|
|
249
|
+
if (resource.style) {
|
|
250
|
+
lines.push('', '--- Project Style ---', resource.style);
|
|
251
|
+
}
|
|
252
|
+
if (resource.goal) {
|
|
253
|
+
lines.push('', '--- Current Goal ---', resource.goal);
|
|
254
|
+
}
|
|
255
|
+
if (resource.plan) {
|
|
256
|
+
lines.push('', '--- Current Plan ---', resource.plan);
|
|
257
|
+
}
|
|
258
|
+
return lines.join('\n');
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
function _readIfExists(dir, filename, maxChars = 8000) {
|
|
262
|
+
try {
|
|
263
|
+
const filePath = path.join(dir, filename);
|
|
264
|
+
if (!fs.existsSync(filePath)) return '';
|
|
265
|
+
const content = fs.readFileSync(filePath, 'utf-8');
|
|
266
|
+
if (content.length > maxChars) {
|
|
267
|
+
// 70/20 head/tail truncation
|
|
268
|
+
const head = Math.floor(maxChars * 0.7);
|
|
269
|
+
const tail = Math.floor(maxChars * 0.2);
|
|
270
|
+
return content.slice(0, head) + '\n\n[...truncated...]\n\n' + content.slice(-tail);
|
|
271
|
+
}
|
|
272
|
+
return content;
|
|
273
|
+
} catch { return ''; }
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
function _scanSkills(keplerDir) {
|
|
277
|
+
const skillsDir = path.join(keplerDir, 'skills');
|
|
278
|
+
if (!fs.existsSync(skillsDir)) return [];
|
|
279
|
+
try {
|
|
280
|
+
return fs.readdirSync(skillsDir, { withFileTypes: true })
|
|
281
|
+
.map(entry => {
|
|
282
|
+
const file = entry.isDirectory()
|
|
283
|
+
? path.join(skillsDir, entry.name, 'SKILL.md')
|
|
284
|
+
: path.join(skillsDir, entry.name);
|
|
285
|
+
if (!fs.existsSync(file) || !file.endsWith('.md')) return null;
|
|
286
|
+
const content = fs.readFileSync(file, 'utf-8');
|
|
287
|
+
const descMatch = content.match(/^#\s+.*\n+(.+)/);
|
|
288
|
+
return {
|
|
289
|
+
name: entry.isDirectory() ? entry.name : entry.name.replace('.md', ''),
|
|
290
|
+
description: content.match(/^description:\s*(.+)$/mi)?.[1]?.trim()
|
|
291
|
+
|| (descMatch ? descMatch[1].slice(0, 100) : entry.name.replace('.md', '')),
|
|
292
|
+
};
|
|
293
|
+
})
|
|
294
|
+
.filter(Boolean);
|
|
295
|
+
} catch { return []; }
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
function defaultScratchRoots() {
|
|
299
|
+
return uniqueValues([
|
|
300
|
+
'/tmp',
|
|
301
|
+
'/private/tmp',
|
|
302
|
+
os.tmpdir(),
|
|
303
|
+
process.env.TMPDIR,
|
|
304
|
+
...(process.env.KEPLER_SCRATCH_ROOTS || '')
|
|
305
|
+
.split(path.delimiter)
|
|
306
|
+
.map(s => s.trim())
|
|
307
|
+
.filter(Boolean),
|
|
308
|
+
]).map(canonicalRoot);
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
export class ProjectRegistry {
|
|
312
|
+
constructor() {
|
|
313
|
+
this.projects = new Map();
|
|
314
|
+
this.scratchRoots = new Set(defaultScratchRoots());
|
|
315
|
+
this._globalIdentity = null;
|
|
316
|
+
this._globalPreferences = null;
|
|
317
|
+
this._globalSkills = null;
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
addScratchRoot(rawPath) {
|
|
321
|
+
if (!rawPath) return null;
|
|
322
|
+
const root = canonicalRoot(rawPath);
|
|
323
|
+
this.scratchRoots.add(root);
|
|
324
|
+
return root;
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
/**
|
|
328
|
+
* Load global context from ~/.bahulam/ (once per session).
|
|
329
|
+
*/
|
|
330
|
+
loadGlobalContext() {
|
|
331
|
+
if (this._globalIdentity !== null) return;
|
|
332
|
+
// Resolver — prefers ~/.bahulam, falls back to ~/.kepler for legacy installs.
|
|
333
|
+
const globalDir = bahulamHome();
|
|
334
|
+
this._globalIdentity = _readIfExists(globalDir, 'identity.md', 4000);
|
|
335
|
+
this._globalPreferences = _readIfExists(globalDir, 'preferences.md', 2000);
|
|
336
|
+
this._globalSkills = _scanSkills(globalDir);
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
/**
|
|
340
|
+
* Get the global agent context (identity, preferences, skills).
|
|
341
|
+
*/
|
|
342
|
+
getGlobalContext() {
|
|
343
|
+
this.loadGlobalContext();
|
|
344
|
+
return {
|
|
345
|
+
identity: this._globalIdentity || '',
|
|
346
|
+
preferences: this._globalPreferences || '',
|
|
347
|
+
skills: this._globalSkills || [],
|
|
348
|
+
};
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
// PRD-69 project context is live metadata, not index cache. Re-read it on
|
|
352
|
+
// every registration attempt so repeated get_project_overview calls pick up
|
|
353
|
+
// .bahulam/KEPLER.md, goal/plan/style, skills, AGENTS.md, etc. changes.
|
|
354
|
+
_attachLiveContext(resource, root) {
|
|
355
|
+
// Resolver — prefers .bahulam/, falls back to .kepler/ for legacy projects.
|
|
356
|
+
const keplerDir = projectConfigDir(root);
|
|
357
|
+
resource.environment = detectEnvironment();
|
|
358
|
+
resource.project_context = _readIfExists(keplerDir, 'KEPLER.md', 10000) ||
|
|
359
|
+
_readIfExists(root, 'KEPLER.md', 10000) ||
|
|
360
|
+
_readIfExists(keplerDir, 'project.md', 8000);
|
|
361
|
+
resource.style = _readIfExists(keplerDir, 'style.md', 4000);
|
|
362
|
+
resource.goal = _readIfExists(keplerDir, 'goal.md', 2000);
|
|
363
|
+
resource.plan = _readIfExists(keplerDir, 'plan.md', 6000);
|
|
364
|
+
resource.skills_index = _scanSkills(keplerDir);
|
|
365
|
+
|
|
366
|
+
if (!resource.project_context) {
|
|
367
|
+
for (const name of ['.bahulam.md', 'AGENTS.md', 'CLAUDE.md']) {
|
|
368
|
+
const content = _readIfExists(root, name, 8000);
|
|
369
|
+
if (content) { resource.project_context = content; break; }
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
return resource;
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
async register(rawPath, { forceRefresh = false, force_refresh = false } = {}) {
|
|
376
|
+
if (!rawPath) {
|
|
377
|
+
throw new Error('get_project_overview requires a project path');
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
// LLM sometimes passes shell-escaped paths ("Tarang\ Orca") or paths
|
|
381
|
+
// beginning with "~". Normalize defensively so the tool does not bounce
|
|
382
|
+
// back a "not found" error on a path that's correct apart from quoting.
|
|
383
|
+
rawPath = normalizePathInput(rawPath);
|
|
384
|
+
|
|
385
|
+
if (!path.isAbsolute(rawPath)) {
|
|
386
|
+
rawPath = path.resolve(process.cwd(), rawPath);
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
let root;
|
|
390
|
+
try {
|
|
391
|
+
root = fs.realpathSync(rawPath);
|
|
392
|
+
} catch {
|
|
393
|
+
// Try the unescaped variant explicitly so the error message can
|
|
394
|
+
// tell the agent what it actually attempted.
|
|
395
|
+
const unescaped = unescapeShellPath(rawPath);
|
|
396
|
+
if (unescaped !== rawPath) {
|
|
397
|
+
try { root = fs.realpathSync(unescaped); }
|
|
398
|
+
catch { throw new Error(`Project path not found: ${rawPath} (also tried ${unescaped})`); }
|
|
399
|
+
} else {
|
|
400
|
+
throw new Error(`Project path not found: ${rawPath}`);
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
if (!fs.statSync(root).isDirectory()) {
|
|
404
|
+
throw new Error(`Project path is not a directory: ${root}`);
|
|
405
|
+
}
|
|
406
|
+
if (root === path.parse(root).root || root === os.homedir()) {
|
|
407
|
+
throw new Error(
|
|
408
|
+
`Refusing to index ${root} — too broad. Pass the project directory itself.`
|
|
409
|
+
);
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
const id = projectId(root);
|
|
413
|
+
const fingerprint = projectFingerprint(root);
|
|
414
|
+
const existing = this.projects.get(id);
|
|
415
|
+
const shouldForceRefresh = Boolean(forceRefresh || force_refresh);
|
|
416
|
+
if (existing && !shouldForceRefresh && existing.resource.index_version === fingerprint) {
|
|
417
|
+
this._attachLiveContext(existing.resource, root);
|
|
418
|
+
return {
|
|
419
|
+
already_registered: true,
|
|
420
|
+
refreshed: false,
|
|
421
|
+
resource: existing.resource,
|
|
422
|
+
output:
|
|
423
|
+
`Project already registered as project_id=${id}. ` +
|
|
424
|
+
`Use project_id=${id} with search_code and use absolute paths for file tools.`,
|
|
425
|
+
};
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
const retriever = new ContextRetriever(root);
|
|
429
|
+
const resourcePath = path.join(getIndexDir(root), RESOURCE_FILE);
|
|
430
|
+
let resource = null;
|
|
431
|
+
|
|
432
|
+
try {
|
|
433
|
+
const persisted = JSON.parse(fs.readFileSync(resourcePath, 'utf-8'));
|
|
434
|
+
if (!shouldForceRefresh && persisted.index_version === fingerprint && retriever.loadIndex()) {
|
|
435
|
+
resource = persisted;
|
|
436
|
+
}
|
|
437
|
+
} catch { /* missing or stale index */ }
|
|
438
|
+
|
|
439
|
+
if (!resource) {
|
|
440
|
+
await retriever.buildIndex();
|
|
441
|
+
resource = {
|
|
442
|
+
project_id: id,
|
|
443
|
+
root,
|
|
444
|
+
name: path.basename(root),
|
|
445
|
+
languages: detectLanguages(root),
|
|
446
|
+
commands: detectCommands(root),
|
|
447
|
+
overview: buildProjectSkeleton(root, { maxFiles: 150, maxChars: 2500 }) ||
|
|
448
|
+
`Project at ${root}`,
|
|
449
|
+
index_status: 'ready',
|
|
450
|
+
index_version: fingerprint,
|
|
451
|
+
};
|
|
452
|
+
fs.writeFileSync(resourcePath, JSON.stringify(resource));
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
this._attachLiveContext(resource, root);
|
|
456
|
+
this.projects.set(id, { resource, retriever });
|
|
457
|
+
const refreshed = Boolean(existing);
|
|
458
|
+
return {
|
|
459
|
+
already_registered: refreshed,
|
|
460
|
+
refreshed,
|
|
461
|
+
resource,
|
|
462
|
+
output: refreshed
|
|
463
|
+
? `Project refreshed: ${resource.name} (project_id=${id})\nRoot: ${resource.root}`
|
|
464
|
+
: formatResource(resource),
|
|
465
|
+
};
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
resources() {
|
|
469
|
+
return [...this.projects.values()].map(({ resource }) => resource);
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
get(projectIdValue) {
|
|
473
|
+
return this.projects.get(projectIdValue) || null;
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
projectScratchRoots() {
|
|
477
|
+
return this.resources().map(resource => path.join(projectConfigDir(resource.root), 'tmp'));
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
allowedScratchRoots() {
|
|
481
|
+
return uniqueValues([
|
|
482
|
+
...this.scratchRoots,
|
|
483
|
+
...this.projectScratchRoots(),
|
|
484
|
+
]).map(canonicalRoot);
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
isAllowedScratchPath(filePath) {
|
|
488
|
+
const normalized = normalizePathInput(filePath);
|
|
489
|
+
const candidate = canonicalizeCandidate(path.resolve(normalized));
|
|
490
|
+
return this.allowedScratchRoots().some(root => isWithin(root, candidate));
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
async registerFileRead(candidate) {
|
|
494
|
+
if (!candidate || !fs.existsSync(candidate)) return null;
|
|
495
|
+
let stat;
|
|
496
|
+
try {
|
|
497
|
+
stat = fs.statSync(candidate);
|
|
498
|
+
} catch {
|
|
499
|
+
return null;
|
|
500
|
+
}
|
|
501
|
+
if (!stat.isFile()) return null;
|
|
502
|
+
|
|
503
|
+
const filePath = fs.realpathSync(candidate);
|
|
504
|
+
const dir = path.dirname(filePath);
|
|
505
|
+
if (dir === path.parse(dir).root || dir === os.homedir()) return null;
|
|
506
|
+
|
|
507
|
+
const registered = await this.register(dir);
|
|
508
|
+
const owner = this.projects.get(registered.resource.project_id);
|
|
509
|
+
if (!owner) return null;
|
|
510
|
+
|
|
511
|
+
const files = Array.isArray(owner.resource.files_read)
|
|
512
|
+
? owner.resource.files_read
|
|
513
|
+
: [];
|
|
514
|
+
if (!files.includes(filePath)) {
|
|
515
|
+
owner.resource.files_read = [...files, filePath];
|
|
516
|
+
}
|
|
517
|
+
return { filePath, project: owner, registered };
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
async resolvePath(rawPath, projectIdValue, { allowMissing = false, allowExternalFileRead = false } = {}) {
|
|
521
|
+
let root = null;
|
|
522
|
+
if (projectIdValue) {
|
|
523
|
+
root = this.get(projectIdValue)?.resource.root || null;
|
|
524
|
+
if (!root) throw new Error(`Unknown project_id: ${projectIdValue}`);
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
if (!rawPath) {
|
|
528
|
+
if (root) return root;
|
|
529
|
+
if (this.projects.size === 1) return this.resources()[0].root;
|
|
530
|
+
// Fall back to the first registered project when the model omits
|
|
531
|
+
// both path and project_id. Beats throwing on an inferable case.
|
|
532
|
+
const first = this.resources()[0];
|
|
533
|
+
if (first) return first.root;
|
|
534
|
+
throw new Error('No projects registered. Call get_project_overview first.');
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
// LLM frequently passes shell-quoted paths copied from a terminal,
|
|
538
|
+
// e.g. "Tarang\ Orca/src/app/\(kepler\)/page.tsx". Normalize here so
|
|
539
|
+
// every tool benefits, not just get_project_overview.
|
|
540
|
+
rawPath = normalizePathInput(rawPath);
|
|
541
|
+
|
|
542
|
+
const buildCandidate = (input) => {
|
|
543
|
+
if (path.isAbsolute(input)) {
|
|
544
|
+
return canonicalizeCandidate(path.resolve(input));
|
|
545
|
+
}
|
|
546
|
+
if (!root) {
|
|
547
|
+
if (this.projects.size === 1) {
|
|
548
|
+
return canonicalizeCandidate(path.resolve(this.resources()[0].root, input));
|
|
549
|
+
}
|
|
550
|
+
if (this.projects.size > 1) {
|
|
551
|
+
throw new Error('Relative path requires project_id when multiple projects are registered. Pass project_id or use an absolute path.');
|
|
552
|
+
}
|
|
553
|
+
throw new Error('No projects registered. Call get_project_overview first.');
|
|
554
|
+
}
|
|
555
|
+
return canonicalizeCandidate(path.resolve(root, input));
|
|
556
|
+
};
|
|
557
|
+
|
|
558
|
+
let candidate = buildCandidate(rawPath);
|
|
559
|
+
|
|
560
|
+
const findContaining = (cand) => [...this.projects.values()].find(({ resource }) =>
|
|
561
|
+
isWithin(resource.root, cand)
|
|
562
|
+
);
|
|
563
|
+
const findScratchRoot = (cand) => this.allowedScratchRoots().find(scratchRoot =>
|
|
564
|
+
isWithin(scratchRoot, cand)
|
|
565
|
+
);
|
|
566
|
+
|
|
567
|
+
let containingProject = findContaining(candidate);
|
|
568
|
+
let containingScratchRoot = containingProject ? null : findScratchRoot(candidate);
|
|
569
|
+
|
|
570
|
+
// Two reasons to try the unescaped variant:
|
|
571
|
+
// (1) candidate is outside every project root (literal "Tarang\ Orca"
|
|
572
|
+
// does not contain a real project), or
|
|
573
|
+
// (2) candidate is inside a root but does not exist on disk because
|
|
574
|
+
// a path segment like "\(kepler\)" only resolves once unescaped.
|
|
575
|
+
// We retry once on the unescaped form before raising.
|
|
576
|
+
const needsRetry = !containingProject ||
|
|
577
|
+
(!allowMissing && !fs.existsSync(candidate));
|
|
578
|
+
if (needsRetry) {
|
|
579
|
+
const unescaped = unescapeShellPath(rawPath);
|
|
580
|
+
if (unescaped !== rawPath) {
|
|
581
|
+
try {
|
|
582
|
+
const altCandidate = buildCandidate(unescaped);
|
|
583
|
+
const altProject = findContaining(altCandidate);
|
|
584
|
+
if (altProject && (allowMissing || fs.existsSync(altCandidate))) {
|
|
585
|
+
candidate = altCandidate;
|
|
586
|
+
containingProject = altProject;
|
|
587
|
+
containingScratchRoot = null;
|
|
588
|
+
} else {
|
|
589
|
+
const altScratchRoot = findScratchRoot(altCandidate);
|
|
590
|
+
if (altScratchRoot && (allowMissing || fs.existsSync(altCandidate))) {
|
|
591
|
+
candidate = altCandidate;
|
|
592
|
+
containingScratchRoot = altScratchRoot;
|
|
593
|
+
}
|
|
594
|
+
}
|
|
595
|
+
} catch { /* fall through to the original error */ }
|
|
596
|
+
}
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
if (!containingProject && !containingScratchRoot) {
|
|
600
|
+
if (allowExternalFileRead) {
|
|
601
|
+
let external = await this.registerFileRead(candidate);
|
|
602
|
+
if (!external) {
|
|
603
|
+
const unescaped = unescapeShellPath(rawPath);
|
|
604
|
+
if (unescaped !== rawPath) {
|
|
605
|
+
try {
|
|
606
|
+
external = await this.registerFileRead(buildCandidate(unescaped));
|
|
607
|
+
} catch { /* keep original outside-root error */ }
|
|
608
|
+
}
|
|
609
|
+
}
|
|
610
|
+
if (external) return external.filePath;
|
|
611
|
+
}
|
|
612
|
+
throw new Error(`Path is outside registered project roots: ${rawPath}`);
|
|
613
|
+
}
|
|
614
|
+
if (!allowMissing && !fs.existsSync(candidate)) {
|
|
615
|
+
throw new Error(`Path not found: ${rawPath}`);
|
|
616
|
+
}
|
|
617
|
+
return candidate;
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
projectForPath(filePath) {
|
|
621
|
+
const normalized = normalizePathInput(filePath);
|
|
622
|
+
const candidate = canonicalizeCandidate(path.resolve(normalized));
|
|
623
|
+
const direct = [...this.projects.values()].find(({ resource }) =>
|
|
624
|
+
isWithin(resource.root, candidate)
|
|
625
|
+
);
|
|
626
|
+
if (direct) return direct;
|
|
627
|
+
// Same unescape fallback used in resolvePath.
|
|
628
|
+
const unescaped = unescapeShellPath(normalized);
|
|
629
|
+
if (unescaped !== normalized) {
|
|
630
|
+
const altCandidate = canonicalizeCandidate(path.resolve(unescaped));
|
|
631
|
+
return [...this.projects.values()].find(({ resource }) =>
|
|
632
|
+
isWithin(resource.root, altCandidate)
|
|
633
|
+
) || null;
|
|
634
|
+
}
|
|
635
|
+
return null;
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
reset() {
|
|
639
|
+
this.projects.clear();
|
|
640
|
+
}
|
|
641
|
+
}
|
|
@@ -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
|
+
};
|