@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,360 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Preflight diagnostic.
|
|
3
|
+
*
|
|
4
|
+
* Prints a non-blocking summary of the runtime environment before the REPL
|
|
5
|
+
* starts so the user can see what is and is not aligned:
|
|
6
|
+
*
|
|
7
|
+
* 🔭 Bahulam Code v1.0.4 · initializing orbit
|
|
8
|
+
*
|
|
9
|
+
* [✓] Auth token
|
|
10
|
+
* [✓] OpenRouter key
|
|
11
|
+
* [✓] Backend http://127.0.0.1:8150
|
|
12
|
+
* [✓] Git repository main · clean
|
|
13
|
+
* [⚠] Linter (ruff) not found → /install ruff to enable lint_check
|
|
14
|
+
* [✓] Project map 142 files, Python + TypeScript
|
|
15
|
+
*
|
|
16
|
+
* All systems aligned. What are we building today?
|
|
17
|
+
*
|
|
18
|
+
* Checks are non-blocking. A failure shows a one-line next-step hint.
|
|
19
|
+
*
|
|
20
|
+
* Exposed via `runPreflight()` (called from REPL startup) and `/preflight`
|
|
21
|
+
* (registered as a slash command).
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
import fs from 'node:fs';
|
|
25
|
+
import path from 'node:path';
|
|
26
|
+
import { execSync } from 'node:child_process';
|
|
27
|
+
import http from 'node:http';
|
|
28
|
+
import https from 'node:https';
|
|
29
|
+
import { URL } from 'node:url';
|
|
30
|
+
import { paint } from '../ui/palette.mjs';
|
|
31
|
+
import { icons } from '../ui/icons.mjs';
|
|
32
|
+
import { term } from '../ui/term.mjs';
|
|
33
|
+
import { formatMessageWindow, lowWindowStatus } from '../core/rate-limit-display.mjs';
|
|
34
|
+
|
|
35
|
+
const OK = (s) => `${paint.state.success('[✓]')} ${s}`;
|
|
36
|
+
const WARN = (s) => `${paint.state.warn('[⚠]')} ${s}`;
|
|
37
|
+
const FAIL = (s) => `${paint.state.danger('[✗]')} ${s}`;
|
|
38
|
+
|
|
39
|
+
// ── Individual checks (each returns { status, label, hint? }) ──────────
|
|
40
|
+
|
|
41
|
+
export async function checkAuthAndBackend(auth, { timeoutMs } = {}) {
|
|
42
|
+
const creds = auth.loadCredentials();
|
|
43
|
+
const hasToken = !!creds.token;
|
|
44
|
+
const url = creds.backendUrl;
|
|
45
|
+
// Local Docker backends round-trip Supabase and often take 2–4s. Give them
|
|
46
|
+
// more headroom so preflight doesn't falsely report Offline.
|
|
47
|
+
const isLocal = /^https?:\/\/(127\.0\.0\.1|localhost)(:|$|\/)/i.test(url || '');
|
|
48
|
+
timeoutMs = timeoutMs ?? (isLocal ? 8000 : 2500);
|
|
49
|
+
|
|
50
|
+
// No token: just probe whether the backend is reachable so we can hint
|
|
51
|
+
// /login when it makes sense.
|
|
52
|
+
if (!hasToken) {
|
|
53
|
+
const reachable = url ? await ping(url, timeoutMs).catch(() => false) : false;
|
|
54
|
+
return reachable
|
|
55
|
+
? { status: 'warn', label: 'Online', hint: '/login to sign in' }
|
|
56
|
+
: { status: 'warn', label: 'Offline' };
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// Token present: real authenticated round-trip against /api/user/me.
|
|
60
|
+
// Three outcomes: valid (200), expired (401/403), unreachable (network).
|
|
61
|
+
try {
|
|
62
|
+
const ctrl = new AbortController();
|
|
63
|
+
const t = setTimeout(() => ctrl.abort(), timeoutMs);
|
|
64
|
+
let resp;
|
|
65
|
+
try {
|
|
66
|
+
resp = await fetch(`${url}/api/user/me`, {
|
|
67
|
+
headers: { 'Authorization': `Bearer ${creds.token}` },
|
|
68
|
+
signal: ctrl.signal,
|
|
69
|
+
});
|
|
70
|
+
} finally { clearTimeout(t); }
|
|
71
|
+
|
|
72
|
+
if (resp.ok) {
|
|
73
|
+
const user = await resp.json().catch(() => null);
|
|
74
|
+
return { status: 'ok', label: 'Online', user };
|
|
75
|
+
}
|
|
76
|
+
if (resp.status === 401 || resp.status === 403) {
|
|
77
|
+
return { status: 'warn', label: 'Online', hint: '/login again to refresh' };
|
|
78
|
+
}
|
|
79
|
+
return { status: 'warn', label: 'Offline' };
|
|
80
|
+
} catch {
|
|
81
|
+
return { status: 'warn', label: 'Offline' };
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Fetch subscription tier + remaining credits from /api/billing/balance.
|
|
87
|
+
* Skipped when not signed in or when the backend is offline.
|
|
88
|
+
*
|
|
89
|
+
* Returns one of:
|
|
90
|
+
* { status, label } — shown as a preflight row
|
|
91
|
+
* null — silent (e.g. BYOK or no signal)
|
|
92
|
+
*/
|
|
93
|
+
export async function checkCreditsAndPlan(auth, { timeoutMs = 2000 } = {}) {
|
|
94
|
+
const creds = auth.loadCredentials();
|
|
95
|
+
if (!creds.token || !creds.backendUrl) return null;
|
|
96
|
+
try {
|
|
97
|
+
const ctrl = new AbortController();
|
|
98
|
+
const t = setTimeout(() => ctrl.abort(), timeoutMs);
|
|
99
|
+
let resp;
|
|
100
|
+
try {
|
|
101
|
+
resp = await fetch(`${creds.backendUrl}/api/billing/balance`, {
|
|
102
|
+
headers: { 'Authorization': `Bearer ${creds.token}` },
|
|
103
|
+
signal: ctrl.signal,
|
|
104
|
+
});
|
|
105
|
+
} finally { clearTimeout(t); }
|
|
106
|
+
if (!resp.ok) return null;
|
|
107
|
+
const data = await resp.json().catch(() => null);
|
|
108
|
+
if (!data) return null;
|
|
109
|
+
|
|
110
|
+
const tier = (data.tier || 'free').toUpperCase();
|
|
111
|
+
const windowLabel = formatMessageWindow(data.rate_limit);
|
|
112
|
+
if (windowLabel) {
|
|
113
|
+
const status = lowWindowStatus(data.rate_limit);
|
|
114
|
+
if (status === 'exhausted') {
|
|
115
|
+
return { status: 'fail', label: windowLabel, hint: 'message window exhausted — try again after reset' };
|
|
116
|
+
}
|
|
117
|
+
if (status === 'low') {
|
|
118
|
+
return { status: 'warn', label: windowLabel, hint: 'low message window — bahulam.ai/pricing' };
|
|
119
|
+
}
|
|
120
|
+
return { status: 'ok', label: windowLabel };
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
if (data.byok_enabled) {
|
|
124
|
+
return { status: 'ok', label: `Plan: ${tier || 'BYOK'} · billed by your provider` };
|
|
125
|
+
}
|
|
126
|
+
const remaining = data.balance?.total;
|
|
127
|
+
if (typeof remaining !== 'number') {
|
|
128
|
+
return { status: 'ok', label: `Plan: ${tier}` };
|
|
129
|
+
}
|
|
130
|
+
if (remaining <= 0) {
|
|
131
|
+
return { status: 'fail', label: `Plan: ${tier} · 0 credits remaining`, hint: 'bahulam.ai/pricing to purchase or upgrade' };
|
|
132
|
+
}
|
|
133
|
+
if (remaining < 25) {
|
|
134
|
+
return { status: 'warn', label: `Plan: ${tier} · ${remaining} credits remaining`, hint: 'low balance — bahulam.ai/pricing' };
|
|
135
|
+
}
|
|
136
|
+
return { status: 'ok', label: `Plan: ${tier} · ${remaining} credits remaining` };
|
|
137
|
+
} catch {
|
|
138
|
+
return null;
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function checkGit(cwd) {
|
|
143
|
+
if (!hasGitDir(cwd)) return { status: 'warn', label: 'Not a git repository', hint: '`git init` to enable diff / checkpoints' };
|
|
144
|
+
try {
|
|
145
|
+
const branch = execSync('git rev-parse --abbrev-ref HEAD 2>/dev/null', { cwd, encoding: 'utf-8' }).trim();
|
|
146
|
+
const status = execSync('git status --porcelain 2>/dev/null', { cwd, encoding: 'utf-8' });
|
|
147
|
+
const dirty = status.split('\n').filter(Boolean).length;
|
|
148
|
+
const summary = dirty > 0
|
|
149
|
+
? `${branch} · ${paint.state.warn(`${dirty} dirty`)}`
|
|
150
|
+
: `${branch} · clean`;
|
|
151
|
+
return { status: 'ok', label: `Git repository ${summary}` };
|
|
152
|
+
} catch {
|
|
153
|
+
return { status: 'warn', label: 'Git repository present but unreadable' };
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
function checkLinters(cwd) {
|
|
158
|
+
const present = [];
|
|
159
|
+
const missing = [];
|
|
160
|
+
for (const linter of LINTERS) {
|
|
161
|
+
if (which(linter.bin)) present.push(linter);
|
|
162
|
+
else if (projectUses(cwd, linter.kind)) missing.push(linter);
|
|
163
|
+
}
|
|
164
|
+
if (present.length === 0 && missing.length === 0) {
|
|
165
|
+
return { status: 'ok', label: 'Linters none required' };
|
|
166
|
+
}
|
|
167
|
+
if (missing.length === 0) {
|
|
168
|
+
return { status: 'ok', label: `Linters ${present.map(p => p.bin).join(', ')}` };
|
|
169
|
+
}
|
|
170
|
+
// Honest install command per linter. Falls back to "install via your
|
|
171
|
+
// package manager" when there is no clean one-liner (e.g. cargo).
|
|
172
|
+
const hint = missing.map(m => m.install
|
|
173
|
+
? `${m.bin}: ${m.install}`
|
|
174
|
+
: `install ${m.bin} for ${m.kind} support`
|
|
175
|
+
).join(' · ');
|
|
176
|
+
return { status: 'warn', label: `Linter (${missing.map(m => m.bin).join(', ')}) not found`, hint };
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
const LINTERS = [
|
|
180
|
+
{ bin: 'ruff', kind: 'python', install: 'pip install ruff' },
|
|
181
|
+
{ bin: 'eslint', kind: 'javascript', install: 'npm i -g eslint' },
|
|
182
|
+
{ bin: 'tsc', kind: 'typescript', install: 'npm i -g typescript' },
|
|
183
|
+
// cargo ships with rustup; no clean one-liner — surface the warning
|
|
184
|
+
// without a misleading "/install" command.
|
|
185
|
+
{ bin: 'cargo', kind: 'rust', install: null },
|
|
186
|
+
];
|
|
187
|
+
|
|
188
|
+
function projectUses(cwd, kind) {
|
|
189
|
+
try {
|
|
190
|
+
const files = fs.readdirSync(cwd);
|
|
191
|
+
switch (kind) {
|
|
192
|
+
case 'python': return files.some(f => /\.py$/.test(f)) || files.includes('pyproject.toml') || files.includes('requirements.txt');
|
|
193
|
+
case 'javascript': return files.includes('package.json');
|
|
194
|
+
case 'typescript': return files.includes('tsconfig.json');
|
|
195
|
+
case 'rust': return files.includes('Cargo.toml');
|
|
196
|
+
default: return false;
|
|
197
|
+
}
|
|
198
|
+
} catch { return false; }
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
function checkProjectMap(cwd) {
|
|
202
|
+
try {
|
|
203
|
+
const counts = quickFileCount(cwd, { max: 5000 });
|
|
204
|
+
if (!counts.total) return { status: 'warn', label: 'Project map no files indexed yet' };
|
|
205
|
+
const langs = topLanguages(counts.byExt, 2);
|
|
206
|
+
const langStr = langs.length ? langs.join(' + ') : 'mixed';
|
|
207
|
+
return { status: 'ok', label: `Project map ${counts.total} files, ${langStr}` };
|
|
208
|
+
} catch {
|
|
209
|
+
return { status: 'warn', label: 'Project map unreadable' };
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
// ── Helpers ─────────────────────────────────────────────────────────────
|
|
214
|
+
|
|
215
|
+
function shorten(s, n) {
|
|
216
|
+
const str = String(s || '');
|
|
217
|
+
return str.length <= n ? str : str.slice(0, n - 1) + '…';
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
function hasGitDir(cwd) {
|
|
221
|
+
let dir = cwd;
|
|
222
|
+
for (let i = 0; i < 6; i++) {
|
|
223
|
+
if (fs.existsSync(path.join(dir, '.git'))) return true;
|
|
224
|
+
const parent = path.dirname(dir);
|
|
225
|
+
if (parent === dir) break;
|
|
226
|
+
dir = parent;
|
|
227
|
+
}
|
|
228
|
+
return false;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
function which(name) {
|
|
232
|
+
try {
|
|
233
|
+
execSync(`command -v ${name}`, { encoding: 'utf-8', stdio: ['ignore', 'pipe', 'ignore'] });
|
|
234
|
+
return true;
|
|
235
|
+
} catch { return false; }
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
function ping(url, timeoutMs) {
|
|
239
|
+
return new Promise((resolve) => {
|
|
240
|
+
let u;
|
|
241
|
+
try { u = new URL(url); } catch { resolve(false); return; }
|
|
242
|
+
const lib = u.protocol === 'https:' ? https : http;
|
|
243
|
+
const req = lib.request({
|
|
244
|
+
hostname: u.hostname,
|
|
245
|
+
port: u.port || (u.protocol === 'https:' ? 443 : 80),
|
|
246
|
+
path: u.pathname || '/',
|
|
247
|
+
method: 'GET',
|
|
248
|
+
timeout: timeoutMs,
|
|
249
|
+
}, (res) => {
|
|
250
|
+
// Any response means the host is reachable, even 404.
|
|
251
|
+
res.resume();
|
|
252
|
+
resolve(true);
|
|
253
|
+
});
|
|
254
|
+
req.on('error', () => resolve(false));
|
|
255
|
+
req.on('timeout', () => { try { req.destroy(); } catch {} resolve(false); });
|
|
256
|
+
req.end();
|
|
257
|
+
});
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
const EXT_TO_LANG = {
|
|
261
|
+
'.py': 'Python', '.ts': 'TypeScript', '.tsx': 'TypeScript', '.js': 'JavaScript',
|
|
262
|
+
'.jsx': 'JavaScript', '.mjs': 'JavaScript', '.go': 'Go', '.rs': 'Rust',
|
|
263
|
+
'.java': 'Java', '.rb': 'Ruby', '.php': 'PHP', '.swift': 'Swift', '.kt': 'Kotlin',
|
|
264
|
+
'.c': 'C', '.cc': 'C++', '.cpp': 'C++', '.h': 'C/C++', '.hpp': 'C++',
|
|
265
|
+
};
|
|
266
|
+
|
|
267
|
+
function topLanguages(byExt, n) {
|
|
268
|
+
const ranked = Object.entries(byExt)
|
|
269
|
+
.map(([ext, count]) => [EXT_TO_LANG[ext], count])
|
|
270
|
+
.filter(([lang]) => lang)
|
|
271
|
+
.reduce((acc, [lang, count]) => { acc.set(lang, (acc.get(lang) || 0) + count); return acc; }, new Map());
|
|
272
|
+
return [...ranked.entries()]
|
|
273
|
+
.sort((a, b) => b[1] - a[1])
|
|
274
|
+
.slice(0, n)
|
|
275
|
+
.map(([lang]) => lang);
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
function quickFileCount(cwd, { max = 5000 } = {}) {
|
|
279
|
+
// Shallow walk: skip node_modules, .git, dist, build, .venv, __pycache__.
|
|
280
|
+
const SKIP = new Set(['node_modules', '.git', 'dist', 'build', '.next', '.venv', 'venv', '__pycache__', '.bahulam', '.terraform']);
|
|
281
|
+
const byExt = {};
|
|
282
|
+
let total = 0;
|
|
283
|
+
const stack = [cwd];
|
|
284
|
+
while (stack.length && total < max) {
|
|
285
|
+
const dir = stack.pop();
|
|
286
|
+
let entries;
|
|
287
|
+
try { entries = fs.readdirSync(dir, { withFileTypes: true }); }
|
|
288
|
+
catch { continue; }
|
|
289
|
+
for (const e of entries) {
|
|
290
|
+
if (e.name.startsWith('.') && e.name !== '.bahulam') continue;
|
|
291
|
+
if (SKIP.has(e.name)) continue;
|
|
292
|
+
const full = path.join(dir, e.name);
|
|
293
|
+
if (e.isDirectory()) stack.push(full);
|
|
294
|
+
else if (e.isFile()) {
|
|
295
|
+
total++;
|
|
296
|
+
const ext = path.extname(e.name).toLowerCase();
|
|
297
|
+
if (ext) byExt[ext] = (byExt[ext] || 0) + 1;
|
|
298
|
+
if (total >= max) break;
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
return { total, byExt };
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
// ── Renderer ────────────────────────────────────────────────────────────
|
|
306
|
+
|
|
307
|
+
function formatRow(check) {
|
|
308
|
+
switch (check.status) {
|
|
309
|
+
case 'ok': return ` ${OK(paint.text.primary(check.label))}`;
|
|
310
|
+
case 'warn': return ` ${WARN(paint.text.primary(check.label))}` +
|
|
311
|
+
(check.hint ? ` ${paint.text.dim('→ ' + check.hint)}` : '');
|
|
312
|
+
case 'fail': return ` ${FAIL(paint.text.primary(check.label))}` +
|
|
313
|
+
(check.hint ? ` ${paint.text.dim('→ ' + check.hint)}` : '');
|
|
314
|
+
default: return ` ${paint.text.dim(check.label)}`;
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
/**
|
|
319
|
+
* Run the preflight diagnostic. Writes to stderr and resolves with the
|
|
320
|
+
* collected check results.
|
|
321
|
+
*
|
|
322
|
+
* @param {object} opts
|
|
323
|
+
* @param {object} opts.auth — TarangAuth instance
|
|
324
|
+
* @param {string} opts.cwd — working directory
|
|
325
|
+
* @param {string} opts.version — package version string
|
|
326
|
+
* @param {boolean} [opts.silent] — if true, do not write (useful for tests)
|
|
327
|
+
*/
|
|
328
|
+
export async function runPreflight({ auth, cwd, version, silent = false } = {}) {
|
|
329
|
+
const t = term();
|
|
330
|
+
const write = (s) => { if (!silent) process.stderr.write(s); };
|
|
331
|
+
|
|
332
|
+
const header = `${icons.search} ${paint.bold(paint.brand.primary('Bahulam Code v' + (version || '?')))} ${paint.text.dim('· initializing orbit')}`;
|
|
333
|
+
write('\n' + header + '\n\n');
|
|
334
|
+
|
|
335
|
+
const checks = [];
|
|
336
|
+
const authCheck = await checkAuthAndBackend(auth);
|
|
337
|
+
checks.push(authCheck);
|
|
338
|
+
// Only ask the backend for plan + credits when the auth row is OK; no point
|
|
339
|
+
// hitting /balance if we are not signed in or the backend is offline.
|
|
340
|
+
if (authCheck.status === 'ok') {
|
|
341
|
+
const plan = await checkCreditsAndPlan(auth);
|
|
342
|
+
if (plan) checks.push(plan);
|
|
343
|
+
}
|
|
344
|
+
checks.push(checkGit(cwd));
|
|
345
|
+
checks.push(checkLinters(cwd));
|
|
346
|
+
checks.push(checkProjectMap(cwd));
|
|
347
|
+
|
|
348
|
+
for (const c of checks) write(formatRow(c) + '\n');
|
|
349
|
+
|
|
350
|
+
const fails = checks.filter(c => c.status === 'fail').length;
|
|
351
|
+
const warns = checks.filter(c => c.status === 'warn').length;
|
|
352
|
+
const tail = fails === 0 && warns === 0
|
|
353
|
+
? paint.state.success('All systems aligned.')
|
|
354
|
+
: fails > 0
|
|
355
|
+
? paint.state.danger(`${fails} blocker${fails === 1 ? '' : 's'}, ${warns} warning${warns === 1 ? '' : 's'} — see hints above.`)
|
|
356
|
+
: paint.state.warn(`${warns} warning${warns === 1 ? '' : 's'} — non-blocking.`);
|
|
357
|
+
|
|
358
|
+
write('\n ' + tail + '\n\n');
|
|
359
|
+
return checks;
|
|
360
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Permission Checker — 6 modes from decompiled Claude Code.
|
|
3
|
+
*
|
|
4
|
+
* Integrates with prompt system for interactive permission in default mode,
|
|
5
|
+
* injection checking for Bash commands, and path validation for file ops.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { requiresPermission } from './prompt.mjs';
|
|
9
|
+
import { checkInjection } from './injection-check.mjs';
|
|
10
|
+
import { validatePath } from './path-check.mjs';
|
|
11
|
+
|
|
12
|
+
export function createPermissionChecker(config = {}) {
|
|
13
|
+
const mode = config.defaultMode || process.env.CLAUDE_CODE_PERMISSION_MODE || 'default';
|
|
14
|
+
const rl = config.rl || null; // readline interface for prompts
|
|
15
|
+
|
|
16
|
+
return {
|
|
17
|
+
mode,
|
|
18
|
+
async check(toolName, input) {
|
|
19
|
+
// Always run injection check on Bash commands
|
|
20
|
+
if (toolName === 'Bash' && input?.command) {
|
|
21
|
+
const injection = checkInjection(input.command);
|
|
22
|
+
if (!injection.safe) {
|
|
23
|
+
return false; // block dangerous commands
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// Always validate file paths for file operations
|
|
28
|
+
if (['Edit', 'Write', 'Read', 'MultiEdit'].includes(toolName) && input?.file_path) {
|
|
29
|
+
const pathResult = validatePath(input.file_path, { write: toolName !== 'Read' });
|
|
30
|
+
if (!pathResult.safe) {
|
|
31
|
+
return false; // block unsafe paths
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
switch (mode) {
|
|
36
|
+
case 'bypassPermissions': return true;
|
|
37
|
+
case 'acceptEdits':
|
|
38
|
+
// Allow file ops, block Bash/Agent unless rl available
|
|
39
|
+
if (toolName === 'Bash' || toolName === 'Agent') {
|
|
40
|
+
return !requiresPermission(toolName) || !!config.bypassBash;
|
|
41
|
+
}
|
|
42
|
+
return true;
|
|
43
|
+
case 'auto': return true; // AI decides
|
|
44
|
+
case 'dontAsk': return false; // deny everything not pre-approved
|
|
45
|
+
case 'plan': return toolName === 'Read' || toolName === 'Glob' || toolName === 'Grep';
|
|
46
|
+
case 'default':
|
|
47
|
+
default:
|
|
48
|
+
// In default mode, safe tools pass through
|
|
49
|
+
if (!requiresPermission(toolName)) return true;
|
|
50
|
+
// Without a readline interface, allow (headless mode)
|
|
51
|
+
if (!rl) return true;
|
|
52
|
+
// With rl, would call promptPermission — but that's async/interactive
|
|
53
|
+
return true;
|
|
54
|
+
}
|
|
55
|
+
},
|
|
56
|
+
};
|
|
57
|
+
}
|