@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.
Files changed (273) hide show
  1. package/README.md +80 -0
  2. package/package.json +49 -0
  3. package/pulse/app/activity/page.tsx +190 -0
  4. package/pulse/app/api/activity/route.ts +138 -0
  5. package/pulse/app/api/benchmark/route.ts +113 -0
  6. package/pulse/app/api/benchmarks/route.ts +195 -0
  7. package/pulse/app/api/costs/route.ts +88 -0
  8. package/pulse/app/api/export/route.ts +77 -0
  9. package/pulse/app/api/history/route.ts +11 -0
  10. package/pulse/app/api/import/route.ts +31 -0
  11. package/pulse/app/api/memory/route.ts +50 -0
  12. package/pulse/app/api/plans/route.ts +9 -0
  13. package/pulse/app/api/projects/[slug]/route.ts +96 -0
  14. package/pulse/app/api/projects/route.ts +121 -0
  15. package/pulse/app/api/sessions/[id]/replay/route.ts +20 -0
  16. package/pulse/app/api/sessions/[id]/route.ts +31 -0
  17. package/pulse/app/api/sessions/route.ts +112 -0
  18. package/pulse/app/api/settings/route.ts +14 -0
  19. package/pulse/app/api/stats/route.ts +143 -0
  20. package/pulse/app/api/todos/route.ts +9 -0
  21. package/pulse/app/api/tools/route.ts +160 -0
  22. package/pulse/app/benchmarks/page.tsx +224 -0
  23. package/pulse/app/costs/page.tsx +179 -0
  24. package/pulse/app/export/page.tsx +465 -0
  25. package/pulse/app/favicon.ico +0 -0
  26. package/pulse/app/globals.css +263 -0
  27. package/pulse/app/help/page.tsx +143 -0
  28. package/pulse/app/history/page.tsx +157 -0
  29. package/pulse/app/layout.tsx +46 -0
  30. package/pulse/app/memory/page.tsx +365 -0
  31. package/pulse/app/overview-client.tsx +393 -0
  32. package/pulse/app/page.tsx +14 -0
  33. package/pulse/app/plans/page.tsx +308 -0
  34. package/pulse/app/projects/[slug]/page.tsx +390 -0
  35. package/pulse/app/projects/page.tsx +110 -0
  36. package/pulse/app/sessions/[id]/page.tsx +243 -0
  37. package/pulse/app/sessions/page.tsx +39 -0
  38. package/pulse/app/settings/page.tsx +188 -0
  39. package/pulse/app/todos/page.tsx +211 -0
  40. package/pulse/app/tools/page.tsx +249 -0
  41. package/pulse/cli.js +164 -0
  42. package/pulse/components/activity/day-of-week-chart.tsx +35 -0
  43. package/pulse/components/activity/streak-card.tsx +36 -0
  44. package/pulse/components/costs/cache-efficiency-panel.tsx +76 -0
  45. package/pulse/components/costs/cost-by-project-chart.tsx +48 -0
  46. package/pulse/components/costs/cost-over-time-chart.tsx +95 -0
  47. package/pulse/components/costs/model-token-table.tsx +60 -0
  48. package/pulse/components/global-search.tsx +193 -0
  49. package/pulse/components/keyboard-nav-provider.tsx +23 -0
  50. package/pulse/components/layout/bottom-nav.tsx +53 -0
  51. package/pulse/components/layout/client-layout.tsx +31 -0
  52. package/pulse/components/layout/sidebar-context.tsx +50 -0
  53. package/pulse/components/layout/sidebar.tsx +183 -0
  54. package/pulse/components/layout/top-bar.tsx +121 -0
  55. package/pulse/components/overview/activity-heatmap.tsx +107 -0
  56. package/pulse/components/overview/conversation-table.tsx +148 -0
  57. package/pulse/components/overview/model-breakdown-donut.tsx +95 -0
  58. package/pulse/components/overview/peak-hours-chart.tsx +87 -0
  59. package/pulse/components/overview/project-activity-donut.tsx +96 -0
  60. package/pulse/components/overview/stat-card.tsx +102 -0
  61. package/pulse/components/overview/usage-over-time-chart.tsx +166 -0
  62. package/pulse/components/projects/project-card.tsx +175 -0
  63. package/pulse/components/sessions/replay/assistant-markdown.tsx +94 -0
  64. package/pulse/components/sessions/replay/compaction-card.tsx +25 -0
  65. package/pulse/components/sessions/replay/session-sidebar.tsx +231 -0
  66. package/pulse/components/sessions/replay/token-accumulation-chart.tsx +98 -0
  67. package/pulse/components/sessions/replay/tool-call-badge.tsx +127 -0
  68. package/pulse/components/sessions/replay/turn-cards.tsx +220 -0
  69. package/pulse/components/sessions/replay/user-tool-result.tsx +158 -0
  70. package/pulse/components/sessions/session-badges.tsx +49 -0
  71. package/pulse/components/sessions/session-table.tsx +299 -0
  72. package/pulse/components/theme-provider.tsx +44 -0
  73. package/pulse/components/tools/feature-adoption-table.tsx +58 -0
  74. package/pulse/components/tools/mcp-server-panel.tsx +45 -0
  75. package/pulse/components/tools/tool-ranking-chart.tsx +57 -0
  76. package/pulse/components/tools/version-history-table.tsx +32 -0
  77. package/pulse/components/ui/alert.tsx +66 -0
  78. package/pulse/components/ui/badge.tsx +48 -0
  79. package/pulse/components/ui/breadcrumb.tsx +109 -0
  80. package/pulse/components/ui/button.tsx +64 -0
  81. package/pulse/components/ui/calendar.tsx +220 -0
  82. package/pulse/components/ui/card.tsx +92 -0
  83. package/pulse/components/ui/command.tsx +158 -0
  84. package/pulse/components/ui/dialog.tsx +158 -0
  85. package/pulse/components/ui/input.tsx +21 -0
  86. package/pulse/components/ui/popover.tsx +89 -0
  87. package/pulse/components/ui/progress.tsx +31 -0
  88. package/pulse/components/ui/select.tsx +190 -0
  89. package/pulse/components/ui/separator.tsx +28 -0
  90. package/pulse/components/ui/sheet.tsx +143 -0
  91. package/pulse/components/ui/skeleton.tsx +13 -0
  92. package/pulse/components/ui/table.tsx +116 -0
  93. package/pulse/components/ui/tabs.tsx +91 -0
  94. package/pulse/components/ui/tooltip.tsx +57 -0
  95. package/pulse/components/use-global-keyboard-nav.ts +79 -0
  96. package/pulse/components.json +23 -0
  97. package/pulse/eslint.config.mjs +18 -0
  98. package/pulse/lib/bahulam-paths.ts +23 -0
  99. package/pulse/lib/claude-reader.ts +592 -0
  100. package/pulse/lib/decode.ts +129 -0
  101. package/pulse/lib/pricing.ts +102 -0
  102. package/pulse/lib/replay-parser.ts +165 -0
  103. package/pulse/lib/tool-categories.ts +127 -0
  104. package/pulse/lib/utils.ts +6 -0
  105. package/pulse/next-env.d.ts +6 -0
  106. package/pulse/next.config.ts +16 -0
  107. package/pulse/package.json +45 -0
  108. package/pulse/postcss.config.mjs +7 -0
  109. package/pulse/public/activity.png +0 -0
  110. package/pulse/public/cc-lens.png +0 -0
  111. package/pulse/public/command-k.png +0 -0
  112. package/pulse/public/costs.png +0 -0
  113. package/pulse/public/dashboard-dark.png +0 -0
  114. package/pulse/public/dashboard-white.png +0 -0
  115. package/pulse/public/export.png +0 -0
  116. package/pulse/public/file.svg +1 -0
  117. package/pulse/public/globe.svg +1 -0
  118. package/pulse/public/next.svg +1 -0
  119. package/pulse/public/projects.png +0 -0
  120. package/pulse/public/session-chat.png +0 -0
  121. package/pulse/public/todos.png +0 -0
  122. package/pulse/public/tools.png +0 -0
  123. package/pulse/public/vercel.svg +1 -0
  124. package/pulse/public/window.svg +1 -0
  125. package/pulse/tsconfig.json +34 -0
  126. package/pulse/types/claude.ts +294 -0
  127. package/src/agents/loader.mjs +94 -0
  128. package/src/agents/multi_workflow_loader.mjs +330 -0
  129. package/src/agents/parser.mjs +205 -0
  130. package/src/agents/scaffold.mjs +222 -0
  131. package/src/agents/teams.mjs +123 -0
  132. package/src/agents/workflow_loader.mjs +122 -0
  133. package/src/agents/workflow_scaffold.mjs +249 -0
  134. package/src/auth/oauth.mjs +220 -0
  135. package/src/auth/tarang-auth.mjs +306 -0
  136. package/src/commands/agent.mjs +220 -0
  137. package/src/commands/workflow.mjs +581 -0
  138. package/src/config/cli-args.mjs +200 -0
  139. package/src/config/env.mjs +263 -0
  140. package/src/config/hook-runner.mjs +100 -0
  141. package/src/config/memory-loader.mjs +32 -0
  142. package/src/config/settings-loader.mjs +45 -0
  143. package/src/config/settings.mjs +132 -0
  144. package/src/context/ast-parser.mjs +298 -0
  145. package/src/context/bm25.mjs +85 -0
  146. package/src/context/retriever.mjs +308 -0
  147. package/src/context/skeleton.mjs +134 -0
  148. package/src/context/symbol-indexer.mjs +375 -0
  149. package/src/core/agent-history.mjs +111 -0
  150. package/src/core/agent-loop.mjs +486 -0
  151. package/src/core/approval-log.mjs +104 -0
  152. package/src/core/approval.mjs +476 -0
  153. package/src/core/attachments.mjs +380 -0
  154. package/src/core/backend-url.mjs +55 -0
  155. package/src/core/cache-control.mjs +92 -0
  156. package/src/core/cache.mjs +105 -0
  157. package/src/core/callback-client.mjs +180 -0
  158. package/src/core/checkpoints.mjs +142 -0
  159. package/src/core/compact-history.mjs +127 -0
  160. package/src/core/context-envelope.mjs +54 -0
  161. package/src/core/context-manager.mjs +198 -0
  162. package/src/core/error-guidance.mjs +311 -0
  163. package/src/core/file-diff.mjs +217 -0
  164. package/src/core/headless.mjs +448 -0
  165. package/src/core/hooks-manager.mjs +87 -0
  166. package/src/core/jsonl-writer.mjs +449 -0
  167. package/src/core/local-agent.mjs +537 -0
  168. package/src/core/local-store.mjs +836 -0
  169. package/src/core/mode-selector.mjs +51 -0
  170. package/src/core/output-filter.mjs +177 -0
  171. package/src/core/paths.mjs +190 -0
  172. package/src/core/policy-resolver.mjs +156 -0
  173. package/src/core/pricing.mjs +336 -0
  174. package/src/core/project-artifacts.mjs +39 -0
  175. package/src/core/project-context-loader.mjs +139 -0
  176. package/src/core/providers.mjs +219 -0
  177. package/src/core/rate-limit-display.mjs +121 -0
  178. package/src/core/rate-limiter.mjs +119 -0
  179. package/src/core/resume-mode.mjs +192 -0
  180. package/src/core/risk-tier.mjs +337 -0
  181. package/src/core/safety.mjs +203 -0
  182. package/src/core/scheduler.mjs +173 -0
  183. package/src/core/session-manager.mjs +360 -0
  184. package/src/core/session.mjs +143 -0
  185. package/src/core/settings-sync.mjs +85 -0
  186. package/src/core/stagnation.mjs +57 -0
  187. package/src/core/stream-client.mjs +829 -0
  188. package/src/core/streaming.mjs +182 -0
  189. package/src/core/system-prompt.mjs +140 -0
  190. package/src/core/tasks.mjs +196 -0
  191. package/src/core/tool-executor.mjs +1950 -0
  192. package/src/core/trust.mjs +158 -0
  193. package/src/core/work-scope.mjs +248 -0
  194. package/src/hooks/engine.mjs +162 -0
  195. package/src/index.mjs +426 -0
  196. package/src/mcp/client.mjs +253 -0
  197. package/src/mcp/transport-shttp.mjs +130 -0
  198. package/src/mcp/transport-sse.mjs +131 -0
  199. package/src/mcp/transport-ws.mjs +134 -0
  200. package/src/onboarding/preflight.mjs +360 -0
  201. package/src/permissions/checker.mjs +57 -0
  202. package/src/permissions/command-classifier.mjs +652 -0
  203. package/src/permissions/injection-check.mjs +60 -0
  204. package/src/permissions/path-check.mjs +102 -0
  205. package/src/permissions/prompt.mjs +73 -0
  206. package/src/permissions/sandbox.mjs +112 -0
  207. package/src/plugins/loader.mjs +138 -0
  208. package/src/skills/installer.mjs +188 -0
  209. package/src/skills/loader.mjs +252 -0
  210. package/src/skills/runner.mjs +55 -0
  211. package/src/state/orbit.mjs +263 -0
  212. package/src/state/verbosity.mjs +99 -0
  213. package/src/telemetry/index.mjs +96 -0
  214. package/src/terminal/agents.mjs +177 -0
  215. package/src/terminal/analytics.mjs +292 -0
  216. package/src/terminal/ansi.mjs +695 -0
  217. package/src/terminal/init.mjs +145 -0
  218. package/src/terminal/main.mjs +269 -0
  219. package/src/terminal/repl-explore.mjs +35 -0
  220. package/src/terminal/repl-format.mjs +257 -0
  221. package/src/terminal/repl-render.mjs +561 -0
  222. package/src/terminal/repl-resume.mjs +625 -0
  223. package/src/terminal/repl-state.mjs +103 -0
  224. package/src/terminal/repl-utils.mjs +34 -0
  225. package/src/terminal/repl.mjs +3832 -0
  226. package/src/terminal/skills.mjs +54 -0
  227. package/src/terminal/tool-display.mjs +240 -0
  228. package/src/tools/agent.mjs +137 -0
  229. package/src/tools/ask-user.mjs +61 -0
  230. package/src/tools/bash.mjs +231 -0
  231. package/src/tools/cron-create.mjs +120 -0
  232. package/src/tools/cron-delete.mjs +49 -0
  233. package/src/tools/cron-list.mjs +37 -0
  234. package/src/tools/edit.mjs +82 -0
  235. package/src/tools/enter-worktree.mjs +69 -0
  236. package/src/tools/exit-worktree.mjs +57 -0
  237. package/src/tools/glob.mjs +117 -0
  238. package/src/tools/grep.mjs +129 -0
  239. package/src/tools/lint.mjs +71 -0
  240. package/src/tools/ls.mjs +58 -0
  241. package/src/tools/lsp.mjs +115 -0
  242. package/src/tools/multi-edit.mjs +94 -0
  243. package/src/tools/notebook-edit.mjs +96 -0
  244. package/src/tools/project-overview.mjs +641 -0
  245. package/src/tools/read-mcp-resource.mjs +57 -0
  246. package/src/tools/read.mjs +138 -0
  247. package/src/tools/registry.mjs +116 -0
  248. package/src/tools/remote-trigger.mjs +84 -0
  249. package/src/tools/send-message.mjs +64 -0
  250. package/src/tools/skill.mjs +52 -0
  251. package/src/tools/test-runner.mjs +49 -0
  252. package/src/tools/todo-write.mjs +68 -0
  253. package/src/tools/tool-search.mjs +77 -0
  254. package/src/tools/web-fetch.mjs +65 -0
  255. package/src/tools/web-search.mjs +89 -0
  256. package/src/tools/write.mjs +55 -0
  257. package/src/ui/approval.mjs +263 -0
  258. package/src/ui/banner.mjs +235 -0
  259. package/src/ui/commands.mjs +537 -0
  260. package/src/ui/formatter.mjs +409 -0
  261. package/src/ui/icons.mjs +164 -0
  262. package/src/ui/input-dock.mjs +444 -0
  263. package/src/ui/markdown.mjs +278 -0
  264. package/src/ui/mission-report.mjs +296 -0
  265. package/src/ui/palette.mjs +189 -0
  266. package/src/ui/slash-commands.mjs +245 -0
  267. package/src/ui/spinner.mjs +116 -0
  268. package/src/ui/sub-agent.mjs +152 -0
  269. package/src/ui/term.mjs +159 -0
  270. package/src/ui/text-layout.mjs +127 -0
  271. package/src/ui/tool-card.mjs +463 -0
  272. package/src/ui/tool-details.mjs +312 -0
  273. 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
+ }