@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,158 @@
|
|
|
1
|
+
import * as crypto from 'node:crypto';
|
|
2
|
+
import * as fs from 'node:fs';
|
|
3
|
+
import * as path from 'node:path';
|
|
4
|
+
import { TIERS } from './risk-tier.mjs';
|
|
5
|
+
|
|
6
|
+
function trustPath(cwd) {
|
|
7
|
+
return path.join(cwd, '.bahulam', 'trust.json');
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
function nowIso() {
|
|
11
|
+
return new Date().toISOString();
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function commandShape(command) {
|
|
15
|
+
const parts = String(command || '').trim().split(/\s+/).filter(Boolean);
|
|
16
|
+
return parts.slice(0, 2).join(' ');
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function shellPattern(args) {
|
|
20
|
+
const shape = commandShape(args?.command);
|
|
21
|
+
return shape ? `${shape}*` : '*';
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function pathPattern(args) {
|
|
25
|
+
const p = String(args?.file_path || args?.path || '').trim();
|
|
26
|
+
if (!p) return '*';
|
|
27
|
+
const dir = path.dirname(p);
|
|
28
|
+
return dir === '.' ? p : path.join(dir, '**');
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function patternFor(tool, args = {}) {
|
|
32
|
+
if (tool === 'shell') return shellPattern(args);
|
|
33
|
+
return pathPattern(args);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function matches(pattern, value) {
|
|
37
|
+
if (!pattern || pattern === '*') return true;
|
|
38
|
+
if (pattern.endsWith('*')) return String(value || '').startsWith(pattern.slice(0, -1));
|
|
39
|
+
if (pattern.endsWith('/**')) return String(value || '').startsWith(pattern.slice(0, -3));
|
|
40
|
+
return pattern === value;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function valueFor(tool, args = {}) {
|
|
44
|
+
if (tool === 'shell') return String(args.command || '');
|
|
45
|
+
return String(args.file_path || args.path || '');
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export class TrustStore {
|
|
49
|
+
constructor({ cwd = process.cwd(), policy = {} } = {}) {
|
|
50
|
+
this.cwd = cwd;
|
|
51
|
+
this.policy = policy;
|
|
52
|
+
this.sessionRules = [];
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
loadProjectRules() {
|
|
56
|
+
try {
|
|
57
|
+
const file = trustPath(this.cwd);
|
|
58
|
+
if (!fs.existsSync(file)) return [];
|
|
59
|
+
const data = JSON.parse(fs.readFileSync(file, 'utf-8'));
|
|
60
|
+
return Array.isArray(data.rules) ? data.rules : [];
|
|
61
|
+
} catch {
|
|
62
|
+
return [];
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
saveProjectRules(rules) {
|
|
67
|
+
const file = trustPath(this.cwd);
|
|
68
|
+
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
69
|
+
fs.writeFileSync(file, JSON.stringify({ version: 1, rules }, null, 2) + '\n');
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
allRules() {
|
|
73
|
+
return [...this.sessionRules, ...this.loadProjectRules()];
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
find(tool, args = {}, tier = '') {
|
|
77
|
+
const value = valueFor(tool, args);
|
|
78
|
+
const rules = this.allRules().filter(rule => rule.tool === tool && matches(rule.pattern, value));
|
|
79
|
+
const deny = rules.find(rule => rule.decision === 'deny');
|
|
80
|
+
if (deny) return { decision: 'deny', rule: deny };
|
|
81
|
+
const allow = rules.find(rule => rule.decision === 'allow');
|
|
82
|
+
if (!allow) return null;
|
|
83
|
+
|
|
84
|
+
const reask = this.shouldReask(allow, args, tier);
|
|
85
|
+
if (reask) return { decision: 'reask', rule: allow, reason: reask };
|
|
86
|
+
return { decision: 'allow', rule: allow };
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
shouldReask(rule, args = {}, tier = '') {
|
|
90
|
+
const reask = rule.reask || {};
|
|
91
|
+
const after = reask.after_minutes ?? this.policy.hitl?.reaskAfterMinutes;
|
|
92
|
+
if (after && rule.created_at) {
|
|
93
|
+
const ageMs = Date.now() - Date.parse(rule.created_at);
|
|
94
|
+
if (Number.isFinite(ageMs) && ageMs > after * 60 * 1000) {
|
|
95
|
+
return `approval expired after ${after}m`;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
if ((reask.on_risk_increase ?? this.policy.hitl?.reaskOnRiskIncrease) && rule.tier && tier) {
|
|
99
|
+
const order = [TIERS.READ, TIERS.SHELL_SAFE, TIERS.LOCAL_EDIT, TIERS.SHELL_MEDIUM, TIERS.NETWORK, TIERS.SHELL_DANGEROUS, TIERS.DESTRUCTIVE];
|
|
100
|
+
if (order.indexOf(tier) > order.indexOf(rule.tier)) return 'risk tier increased';
|
|
101
|
+
}
|
|
102
|
+
if ((this.policy.hitl?.alwaysAskForDangerous ?? true) && (tier === TIERS.SHELL_DANGEROUS || tier === TIERS.DESTRUCTIVE)) {
|
|
103
|
+
return 'dangerous tier requires fresh approval';
|
|
104
|
+
}
|
|
105
|
+
if ((reask.on_command_shape_change ?? this.policy.hitl?.reaskOnCommandShapeChange) && rule.command_shape && args.command) {
|
|
106
|
+
if (rule.command_shape !== commandShape(args.command)) return 'command shape changed';
|
|
107
|
+
}
|
|
108
|
+
return '';
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
add({ tool, args = {}, tier, scope = 'SESSION', decision = 'allow' }) {
|
|
112
|
+
const normalizedScope = String(scope || 'SESSION').toUpperCase();
|
|
113
|
+
const pattern = patternFor(tool, args);
|
|
114
|
+
const id = `${tool}-${crypto.createHash('sha1').update(pattern + normalizedScope).digest('hex').slice(0, 10)}`;
|
|
115
|
+
const ttl = this.policy.hitl?.reaskAfterMinutes ?? 30;
|
|
116
|
+
const rule = {
|
|
117
|
+
id,
|
|
118
|
+
tool,
|
|
119
|
+
pattern,
|
|
120
|
+
scope: normalizedScope,
|
|
121
|
+
decision,
|
|
122
|
+
tier,
|
|
123
|
+
command_shape: tool === 'shell' ? commandShape(args.command) : undefined,
|
|
124
|
+
created_at: nowIso(),
|
|
125
|
+
expires_at: normalizedScope === 'SESSION'
|
|
126
|
+
? new Date(Date.now() + ttl * 60 * 1000).toISOString()
|
|
127
|
+
: undefined,
|
|
128
|
+
reask: {
|
|
129
|
+
after_minutes: ttl,
|
|
130
|
+
on_command_shape_change: this.policy.hitl?.reaskOnCommandShapeChange ?? true,
|
|
131
|
+
on_risk_increase: this.policy.hitl?.reaskOnRiskIncrease ?? true,
|
|
132
|
+
on_path_boundary_change: this.policy.hitl?.reaskOnPathBoundaryChange ?? true,
|
|
133
|
+
},
|
|
134
|
+
};
|
|
135
|
+
if (normalizedScope === 'PROJECT') {
|
|
136
|
+
const rules = this.loadProjectRules().filter(r => r.id !== id);
|
|
137
|
+
rules.push(rule);
|
|
138
|
+
this.saveProjectRules(rules);
|
|
139
|
+
} else {
|
|
140
|
+
this.sessionRules = this.sessionRules.filter(r => r.id !== id);
|
|
141
|
+
this.sessionRules.push(rule);
|
|
142
|
+
}
|
|
143
|
+
return rule;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
revoke() {
|
|
147
|
+
const active = this.sessionRules.length > 0;
|
|
148
|
+
this.sessionRules = [];
|
|
149
|
+
return active;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
summary() {
|
|
153
|
+
return {
|
|
154
|
+
sessionRules: this.sessionRules.length,
|
|
155
|
+
projectRules: this.loadProjectRules().length,
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
}
|
|
@@ -0,0 +1,248 @@
|
|
|
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
|
+
|
|
6
|
+
const SCHEMA = 'kepler.work_scope/1';
|
|
7
|
+
const ROOT_MARKERS = [
|
|
8
|
+
'.bahulam',
|
|
9
|
+
'.git',
|
|
10
|
+
'package.json',
|
|
11
|
+
'pyproject.toml',
|
|
12
|
+
'setup.py',
|
|
13
|
+
'go.mod',
|
|
14
|
+
'Cargo.toml',
|
|
15
|
+
];
|
|
16
|
+
|
|
17
|
+
function stable(value) {
|
|
18
|
+
if (Array.isArray(value)) return value.map(stable);
|
|
19
|
+
if (value && typeof value === 'object') {
|
|
20
|
+
return Object.fromEntries(
|
|
21
|
+
Object.entries(value)
|
|
22
|
+
.filter(([, v]) => v !== undefined)
|
|
23
|
+
.sort(([a], [b]) => a.localeCompare(b))
|
|
24
|
+
.map(([k, v]) => [k, stable(v)]),
|
|
25
|
+
);
|
|
26
|
+
}
|
|
27
|
+
return value;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function sha(payload) {
|
|
31
|
+
return crypto
|
|
32
|
+
.createHash('sha256')
|
|
33
|
+
.update(JSON.stringify(stable(payload)))
|
|
34
|
+
.digest('hex')
|
|
35
|
+
.slice(0, 16);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function normalizePathInput(value) {
|
|
39
|
+
let s = String(value || '').trim();
|
|
40
|
+
if ((s.startsWith('"') && s.endsWith('"')) || (s.startsWith("'") && s.endsWith("'"))) {
|
|
41
|
+
s = s.slice(1, -1);
|
|
42
|
+
}
|
|
43
|
+
if (s === '~' || s.startsWith('~/')) {
|
|
44
|
+
s = path.join(os.homedir(), s.slice(1));
|
|
45
|
+
}
|
|
46
|
+
return s.replace(/\\([ \t()&$;'"])/g, '$1');
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function nearestProjectRoot(candidate) {
|
|
50
|
+
let resolved = normalizePathInput(candidate);
|
|
51
|
+
if (!resolved || !path.isAbsolute(resolved)) return null;
|
|
52
|
+
|
|
53
|
+
try {
|
|
54
|
+
resolved = fs.realpathSync(resolved);
|
|
55
|
+
} catch {
|
|
56
|
+
return null;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
let dir = resolved;
|
|
60
|
+
try {
|
|
61
|
+
if (fs.statSync(resolved).isFile()) dir = path.dirname(resolved);
|
|
62
|
+
} catch {
|
|
63
|
+
return null;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const root = path.parse(dir).root;
|
|
67
|
+
let current = dir;
|
|
68
|
+
while (current && current !== root) {
|
|
69
|
+
if (ROOT_MARKERS.some(marker => fs.existsSync(path.join(current, marker)))) {
|
|
70
|
+
return fs.realpathSync(current);
|
|
71
|
+
}
|
|
72
|
+
current = path.dirname(current);
|
|
73
|
+
}
|
|
74
|
+
return fs.realpathSync(dir);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function extractQuotedPaths(text) {
|
|
78
|
+
const paths = [];
|
|
79
|
+
const re = /(['"])(\/[^'"\n]+)\1/g;
|
|
80
|
+
let match;
|
|
81
|
+
while ((match = re.exec(String(text || ''))) !== null) {
|
|
82
|
+
paths.push(match[2]);
|
|
83
|
+
}
|
|
84
|
+
return paths;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function isInsideQuotedSpan(source, index) {
|
|
88
|
+
let quote = null;
|
|
89
|
+
let escaped = false;
|
|
90
|
+
for (let i = 0; i < index; i++) {
|
|
91
|
+
const ch = source[i];
|
|
92
|
+
if (escaped) {
|
|
93
|
+
escaped = false;
|
|
94
|
+
continue;
|
|
95
|
+
}
|
|
96
|
+
if (ch === '\\') {
|
|
97
|
+
escaped = true;
|
|
98
|
+
continue;
|
|
99
|
+
}
|
|
100
|
+
if ((ch === '"' || ch === "'")) {
|
|
101
|
+
quote = quote === ch ? null : (quote || ch);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
return Boolean(quote);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function extractPastedPaths(text) {
|
|
108
|
+
const source = String(text || '');
|
|
109
|
+
const paths = [];
|
|
110
|
+
for (let i = 0; i < source.length; i++) {
|
|
111
|
+
if (source[i] !== '/') continue;
|
|
112
|
+
if (isInsideQuotedSpan(source, i)) continue;
|
|
113
|
+
const line = source.slice(i).split(/\r?\n/, 1)[0].replace(/[),.;:]+$/g, '');
|
|
114
|
+
const parts = line.split(/\s+/).filter(Boolean);
|
|
115
|
+
let candidate = '';
|
|
116
|
+
let lastRoot = null;
|
|
117
|
+
for (const part of parts.slice(0, 12)) {
|
|
118
|
+
candidate = candidate ? `${candidate} ${part}` : part;
|
|
119
|
+
const root = nearestProjectRoot(candidate.replace(/[),.;:]+$/g, ''));
|
|
120
|
+
if (root) lastRoot = root;
|
|
121
|
+
}
|
|
122
|
+
if (lastRoot) paths.push(lastRoot);
|
|
123
|
+
}
|
|
124
|
+
return paths;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function uniqueRoots(entries) {
|
|
128
|
+
const seen = new Set();
|
|
129
|
+
const result = [];
|
|
130
|
+
for (const entry of entries) {
|
|
131
|
+
if (!entry?.path || seen.has(entry.path)) continue;
|
|
132
|
+
seen.add(entry.path);
|
|
133
|
+
result.push(entry);
|
|
134
|
+
}
|
|
135
|
+
return result;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
export function promptProjectRoots(instruction = '') {
|
|
139
|
+
const roots = [];
|
|
140
|
+
const seen = new Set();
|
|
141
|
+
for (const raw of [...extractQuotedPaths(instruction), ...extractPastedPaths(instruction)]) {
|
|
142
|
+
const root = nearestProjectRoot(raw);
|
|
143
|
+
if (!root || seen.has(root)) continue;
|
|
144
|
+
seen.add(root);
|
|
145
|
+
roots.push(root);
|
|
146
|
+
}
|
|
147
|
+
return roots;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
function roleForRoot(root, cwd) {
|
|
151
|
+
const base = path.basename(root).toLowerCase();
|
|
152
|
+
if (base.includes('backend')) return 'backend';
|
|
153
|
+
if (base.includes('frontend') || base.includes('web')) return 'frontend';
|
|
154
|
+
if (base.includes('deploy')) return 'deploy';
|
|
155
|
+
if (base.includes('docs') || base.includes('prd')) return 'docs';
|
|
156
|
+
if (base.includes('npm') || base.includes('cli')) return 'cli';
|
|
157
|
+
if (root === cwd) return 'primary';
|
|
158
|
+
return 'workspace';
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
function truncate(value, max = 280) {
|
|
162
|
+
const s = String(value || '').replace(/\s+/g, ' ').trim();
|
|
163
|
+
return s.length > max ? `${s.slice(0, max - 1)}…` : s;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
function sortedResources(resources) {
|
|
167
|
+
return (Array.isArray(resources) ? resources : [])
|
|
168
|
+
.filter(resource => resource && resource.root)
|
|
169
|
+
.map(resource => ({
|
|
170
|
+
project_id: String(resource.project_id || ''),
|
|
171
|
+
root: String(resource.root || ''),
|
|
172
|
+
name: String(resource.name || path.basename(String(resource.root || ''))),
|
|
173
|
+
index_version: String(resource.index_version || ''),
|
|
174
|
+
}))
|
|
175
|
+
.sort((a, b) => a.root.localeCompare(b.root));
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
export function buildWorkScope({
|
|
179
|
+
instruction = '',
|
|
180
|
+
cwd = process.cwd(),
|
|
181
|
+
projectResources = [],
|
|
182
|
+
} = {}) {
|
|
183
|
+
const cwdRoot = nearestProjectRoot(cwd) || path.resolve(cwd);
|
|
184
|
+
const roots = [{
|
|
185
|
+
path: cwdRoot,
|
|
186
|
+
role: roleForRoot(cwdRoot, cwdRoot),
|
|
187
|
+
source: 'cwd',
|
|
188
|
+
status: 'active',
|
|
189
|
+
}];
|
|
190
|
+
|
|
191
|
+
for (const root of promptProjectRoots(instruction)) {
|
|
192
|
+
roots.push({
|
|
193
|
+
path: root,
|
|
194
|
+
role: roleForRoot(root, cwdRoot),
|
|
195
|
+
source: 'prompt',
|
|
196
|
+
status: 'active',
|
|
197
|
+
});
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
for (const resource of sortedResources(projectResources)) {
|
|
201
|
+
roots.push({
|
|
202
|
+
path: resource.root,
|
|
203
|
+
role: roleForRoot(resource.root, cwdRoot),
|
|
204
|
+
source: 'registered',
|
|
205
|
+
status: 'active',
|
|
206
|
+
project_id: resource.project_id || undefined,
|
|
207
|
+
});
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
const activeRoots = uniqueRoots(roots);
|
|
211
|
+
const resources = sortedResources(projectResources);
|
|
212
|
+
const scope = {
|
|
213
|
+
schema: SCHEMA,
|
|
214
|
+
primary_root: cwdRoot,
|
|
215
|
+
intent: truncate(instruction),
|
|
216
|
+
active_roots: activeRoots,
|
|
217
|
+
candidate_roots: [],
|
|
218
|
+
workspace_resources: resources,
|
|
219
|
+
cache_policy: {
|
|
220
|
+
stable_system: false,
|
|
221
|
+
placement: 'pinned_context',
|
|
222
|
+
reason: 'scope changes with user intent and discovered roots',
|
|
223
|
+
},
|
|
224
|
+
};
|
|
225
|
+
scope.version = sha({
|
|
226
|
+
schema: scope.schema,
|
|
227
|
+
primary_root: scope.primary_root,
|
|
228
|
+
intent: scope.intent,
|
|
229
|
+
active_roots: scope.active_roots,
|
|
230
|
+
workspace_resources: scope.workspace_resources,
|
|
231
|
+
});
|
|
232
|
+
return scope;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
export function summarizeWorkScope(scope) {
|
|
236
|
+
if (!scope || typeof scope !== 'object') return '';
|
|
237
|
+
const roots = Array.isArray(scope.active_roots) ? scope.active_roots : [];
|
|
238
|
+
const lines = [
|
|
239
|
+
`Work scope ${scope.version || 'unknown'}`,
|
|
240
|
+
`Primary: ${scope.primary_root || '(unknown)'}`,
|
|
241
|
+
];
|
|
242
|
+
if (scope.intent) lines.push(`Intent: ${scope.intent}`);
|
|
243
|
+
for (const root of roots) {
|
|
244
|
+
if (!root?.path) continue;
|
|
245
|
+
lines.push(`- ${root.path} [${root.role || 'workspace'}; ${root.source || 'unknown'}]`);
|
|
246
|
+
}
|
|
247
|
+
return lines.join('\n');
|
|
248
|
+
}
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Hook Engine — pre/post tool use and stop hooks.
|
|
3
|
+
*
|
|
4
|
+
* Based on Claude Code's hooks system (6 event types):
|
|
5
|
+
* - PreToolUse: can block tool execution
|
|
6
|
+
* - PostToolUse: can modify results
|
|
7
|
+
* - Stop: can prevent the agent from stopping
|
|
8
|
+
* - Notification: inform external systems
|
|
9
|
+
* - PrePrompt: modify user input
|
|
10
|
+
* - PostResponse: modify assistant output
|
|
11
|
+
*
|
|
12
|
+
* Hooks are defined in settings.json under the "hooks" key.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import { execSync } from 'child_process';
|
|
16
|
+
|
|
17
|
+
export class HookEngine {
|
|
18
|
+
/**
|
|
19
|
+
* @param {object} hooksConfig - hooks configuration from settings
|
|
20
|
+
*/
|
|
21
|
+
constructor(hooksConfig = {}) {
|
|
22
|
+
this.hooks = hooksConfig;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Run pre-tool-use hooks. Returns { allow, message }.
|
|
27
|
+
* If any hook returns deny, the tool call is blocked.
|
|
28
|
+
*
|
|
29
|
+
* @param {string} toolName - name of the tool being called
|
|
30
|
+
* @param {object} input - tool input arguments
|
|
31
|
+
* @returns {Promise<{allow: boolean, message?: string}>}
|
|
32
|
+
*/
|
|
33
|
+
async runPreToolUse(toolName, input) {
|
|
34
|
+
const hooks = this._getHooks('PreToolUse');
|
|
35
|
+
for (const hook of hooks) {
|
|
36
|
+
// Check if hook applies to this tool
|
|
37
|
+
if (hook.toolName && hook.toolName !== toolName) continue;
|
|
38
|
+
|
|
39
|
+
const result = await this._executeHook(hook, {
|
|
40
|
+
event: 'PreToolUse',
|
|
41
|
+
toolName,
|
|
42
|
+
input,
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
if (result?.decision === 'deny' || result?.decision === 'block') {
|
|
46
|
+
return { allow: false, message: result.message || `Blocked by hook: ${hook.name || 'unnamed'}` };
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
return { allow: true };
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Run post-tool-use hooks. Can modify the result.
|
|
54
|
+
*
|
|
55
|
+
* @param {string} toolName - name of the tool that was called
|
|
56
|
+
* @param {*} result - tool execution result
|
|
57
|
+
* @returns {Promise<*>} possibly modified result
|
|
58
|
+
*/
|
|
59
|
+
async runPostToolUse(toolName, result) {
|
|
60
|
+
const hooks = this._getHooks('PostToolUse');
|
|
61
|
+
let current = result;
|
|
62
|
+
for (const hook of hooks) {
|
|
63
|
+
if (hook.toolName && hook.toolName !== toolName) continue;
|
|
64
|
+
|
|
65
|
+
const hookResult = await this._executeHook(hook, {
|
|
66
|
+
event: 'PostToolUse',
|
|
67
|
+
toolName,
|
|
68
|
+
result: current,
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
if (hookResult?.modifiedResult !== undefined) {
|
|
72
|
+
current = hookResult.modifiedResult;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
return current;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Run stop hooks. Returns true if stop should proceed, false to continue.
|
|
80
|
+
*
|
|
81
|
+
* @returns {Promise<boolean>} whether to allow stopping
|
|
82
|
+
*/
|
|
83
|
+
async runStop() {
|
|
84
|
+
const hooks = this._getHooks('Stop');
|
|
85
|
+
for (const hook of hooks) {
|
|
86
|
+
const result = await this._executeHook(hook, { event: 'Stop' });
|
|
87
|
+
if (result?.preventStop) {
|
|
88
|
+
return false; // do not stop
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
return true; // allow stop
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Run notification hooks (fire-and-forget).
|
|
96
|
+
* @param {string} event - notification event name
|
|
97
|
+
* @param {object} data - event data
|
|
98
|
+
*/
|
|
99
|
+
async runNotification(event, data) {
|
|
100
|
+
const hooks = this._getHooks('Notification');
|
|
101
|
+
for (const hook of hooks) {
|
|
102
|
+
try {
|
|
103
|
+
await this._executeHook(hook, { event, ...data });
|
|
104
|
+
} catch {
|
|
105
|
+
// Notifications are best-effort
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Get hooks for a given event type.
|
|
112
|
+
* @param {string} eventType
|
|
113
|
+
* @returns {Array}
|
|
114
|
+
*/
|
|
115
|
+
_getHooks(eventType) {
|
|
116
|
+
if (!this.hooks || !this.hooks[eventType]) return [];
|
|
117
|
+
const hooks = this.hooks[eventType];
|
|
118
|
+
return Array.isArray(hooks) ? hooks : [hooks];
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Execute a single hook. Supports command (shell) and function hooks.
|
|
123
|
+
*
|
|
124
|
+
* @param {object} hook - hook definition
|
|
125
|
+
* @param {object} context - execution context
|
|
126
|
+
* @returns {Promise<object|null>}
|
|
127
|
+
*/
|
|
128
|
+
async _executeHook(hook, context) {
|
|
129
|
+
try {
|
|
130
|
+
if (hook.command) {
|
|
131
|
+
const env = {
|
|
132
|
+
...process.env,
|
|
133
|
+
HOOK_EVENT: context.event,
|
|
134
|
+
HOOK_TOOL: context.toolName || '',
|
|
135
|
+
HOOK_INPUT: JSON.stringify(context.input || {}),
|
|
136
|
+
};
|
|
137
|
+
const output = execSync(hook.command, {
|
|
138
|
+
encoding: 'utf-8',
|
|
139
|
+
timeout: hook.timeout || 10000,
|
|
140
|
+
env,
|
|
141
|
+
});
|
|
142
|
+
try {
|
|
143
|
+
return JSON.parse(output.trim());
|
|
144
|
+
} catch {
|
|
145
|
+
return { output: output.trim() };
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
if (typeof hook.handler === 'function') {
|
|
150
|
+
return await hook.handler(context);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
return null;
|
|
154
|
+
} catch (err) {
|
|
155
|
+
if (hook.failOpen !== false) {
|
|
156
|
+
// Default: fail open (allow)
|
|
157
|
+
return null;
|
|
158
|
+
}
|
|
159
|
+
return { decision: 'deny', message: `Hook error: ${err.message}` };
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
}
|