@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,252 @@
1
+ /**
2
+ * Portable Agent Skills loader.
3
+ *
4
+ * Discovers standard <root>/<skill>/SKILL.md bundles from Bahulam and Claude
5
+ * locations. Only metadata is exposed eagerly; instructions and resources are
6
+ * loaded on demand through view().
7
+ */
8
+
9
+ import crypto from 'node:crypto';
10
+ import fs from 'node:fs';
11
+ import os from 'node:os';
12
+ import path from 'node:path';
13
+
14
+ const DEFAULT_MAX_CHARS = 12_000;
15
+
16
+ function isWithin(root, candidate) {
17
+ const relative = path.relative(root, candidate);
18
+ return relative === '' || (!relative.startsWith('..') && !path.isAbsolute(relative));
19
+ }
20
+
21
+ function parseScalar(value) {
22
+ const trimmed = value.trim();
23
+ if (
24
+ (trimmed.startsWith('"') && trimmed.endsWith('"'))
25
+ || (trimmed.startsWith("'") && trimmed.endsWith("'"))
26
+ ) {
27
+ return trimmed.slice(1, -1);
28
+ }
29
+ if (trimmed.startsWith('[') && trimmed.endsWith(']')) {
30
+ return trimmed.slice(1, -1).split(',').map(item => parseScalar(item)).filter(Boolean);
31
+ }
32
+ if (trimmed === 'true') return true;
33
+ if (trimmed === 'false') return false;
34
+ return trimmed;
35
+ }
36
+
37
+ export function parseSkill(content, fallbackName) {
38
+ const match = content.match(/^---[ \t]*\r?\n([\s\S]*?)\r?\n---[ \t]*\r?\n?/);
39
+ if (!match) throw new Error('SKILL.md requires YAML frontmatter');
40
+
41
+ const metadata = {};
42
+ let activeList = null;
43
+ for (const rawLine of match[1].split(/\r?\n/)) {
44
+ if (!rawLine.trim() || rawLine.trimStart().startsWith('#')) continue;
45
+ const listMatch = rawLine.match(/^\s*-\s+(.+)$/);
46
+ if (listMatch && activeList) {
47
+ metadata[activeList].push(parseScalar(listMatch[1]));
48
+ continue;
49
+ }
50
+ const fieldMatch = rawLine.match(/^([A-Za-z0-9_-]+):\s*(.*)$/);
51
+ if (!fieldMatch) continue;
52
+ const [, key, rawValue] = fieldMatch;
53
+ if (!rawValue.trim()) {
54
+ metadata[key] = [];
55
+ activeList = key;
56
+ } else {
57
+ metadata[key] = parseScalar(rawValue);
58
+ activeList = null;
59
+ }
60
+ }
61
+
62
+ const name = String(metadata.name || fallbackName || '').trim();
63
+ const description = String(metadata.description || '').trim();
64
+ const instructions = content.slice(match[0].length).trim();
65
+ if (!name) throw new Error("SKILL.md requires non-empty 'name'");
66
+ if (!description) throw new Error("SKILL.md requires non-empty 'description'");
67
+ if (!instructions) throw new Error('SKILL.md requires instruction content');
68
+
69
+ return {
70
+ name,
71
+ description,
72
+ aliases: Array.isArray(metadata.aliases) ? metadata.aliases : [],
73
+ compatibility: Array.isArray(metadata.compatibility) ? metadata.compatibility : [],
74
+ metadata,
75
+ instructions,
76
+ prompt: instructions,
77
+ };
78
+ }
79
+
80
+ export function discoverSkillDirectories(root) {
81
+ if (!root || !fs.existsSync(root)) return [];
82
+ const resolved = fs.realpathSync(root);
83
+ if (fs.existsSync(path.join(resolved, 'SKILL.md'))) return [resolved];
84
+ return fs.readdirSync(resolved, { withFileTypes: true })
85
+ .filter(entry => entry.isDirectory() && !entry.isSymbolicLink())
86
+ .map(entry => path.join(resolved, entry.name))
87
+ .filter(dir => fs.existsSync(path.join(dir, 'SKILL.md')))
88
+ .sort();
89
+ }
90
+
91
+ function walkFiles(root) {
92
+ const files = [];
93
+ const queue = [root];
94
+ while (queue.length) {
95
+ const current = queue.shift();
96
+ for (const entry of fs.readdirSync(current, { withFileTypes: true })) {
97
+ const fullPath = path.join(current, entry.name);
98
+ if (entry.isSymbolicLink()) continue;
99
+ if (entry.isDirectory()) queue.push(fullPath);
100
+ else if (entry.isFile()) files.push(fullPath);
101
+ }
102
+ }
103
+ return files.sort((a, b) => a.localeCompare(b));
104
+ }
105
+
106
+ function contentHash(root) {
107
+ const digest = crypto.createHash('sha256');
108
+ for (const filePath of walkFiles(root)) {
109
+ digest.update(path.relative(root, filePath).split(path.sep).join('/'));
110
+ digest.update('\0');
111
+ digest.update(fs.readFileSync(filePath));
112
+ digest.update('\0');
113
+ }
114
+ return `sha256:${digest.digest('hex')}`;
115
+ }
116
+
117
+ function bounded(content, maxChars) {
118
+ if (content.length <= maxChars) return content;
119
+ const head = Math.floor(maxChars * 0.7);
120
+ const tail = maxChars - head;
121
+ return `${content.slice(0, head)}\n\n[... skill content truncated ...]\n\n${content.slice(-tail)}`;
122
+ }
123
+
124
+ export class SkillsLoader {
125
+ constructor({ homeDir = os.homedir() } = {}) {
126
+ this.homeDir = homeDir;
127
+ this.skills = new Map();
128
+ this.versions = new Map();
129
+ this.searchPaths = [];
130
+ }
131
+
132
+ load(cwd = process.cwd()) {
133
+ this.skills.clear();
134
+ this.versions.clear();
135
+ const project = path.resolve(cwd);
136
+ this.searchPaths = [
137
+ { dir: path.join(project, '.bahulam', 'skills'), source: 'bahulam-project', scope: 'project', priority: 500 },
138
+ { dir: path.join(project, '.claude', 'skills'), source: 'claude-project', scope: 'project', priority: 400 },
139
+ { dir: path.join(this.homeDir, '.bahulam', 'skills'), source: 'bahulam-global', scope: 'global', priority: 300 },
140
+ { dir: path.join(this.homeDir, '.claude', 'skills'), source: 'claude-global', scope: 'global', priority: 200 },
141
+ ];
142
+
143
+ for (const config of this.searchPaths) this._loadFromDir(config);
144
+ return this;
145
+ }
146
+
147
+ _loadFromDir(config) {
148
+ for (const skillDir of discoverSkillDirectories(config.dir)) {
149
+ try {
150
+ const skillFile = path.join(skillDir, 'SKILL.md');
151
+ if (fs.lstatSync(skillFile).isSymbolicLink()) continue;
152
+ const parsed = parseSkill(fs.readFileSync(skillFile, 'utf-8'), path.basename(skillDir));
153
+ const skill = {
154
+ ...parsed,
155
+ root: skillDir,
156
+ source: config.source,
157
+ source_id: `${config.source}:${parsed.name}`,
158
+ scope: config.scope,
159
+ priority: config.priority,
160
+ content_hash: contentHash(skillDir),
161
+ };
162
+ const versions = this.versions.get(skill.name) || [];
163
+ versions.push(skill);
164
+ versions.sort((a, b) => b.priority - a.priority || a.source.localeCompare(b.source));
165
+ this.versions.set(skill.name, versions);
166
+ this.skills.set(skill.name, versions[0]);
167
+ } catch {
168
+ // Invalid third-party bundles are ignored during discovery.
169
+ }
170
+ }
171
+ }
172
+
173
+ get(name, sourceId = null) {
174
+ if (sourceId) {
175
+ return (this.versions.get(name) || []).find(skill => skill.source_id === sourceId) || null;
176
+ }
177
+ if (this.skills.has(name)) return this.skills.get(name);
178
+ for (const skill of this.skills.values()) {
179
+ if (skill.name.startsWith(name) || skill.aliases.includes(name)) return skill;
180
+ }
181
+ return null;
182
+ }
183
+
184
+ list({ query = '', source = '', scope = '' } = {}) {
185
+ const needle = query.toLowerCase();
186
+ return [...this.skills.values()]
187
+ .filter(skill => !source || skill.source === source)
188
+ .filter(skill => !scope || skill.scope === scope)
189
+ .filter(skill => !needle || `${skill.name} ${skill.description}`.toLowerCase().includes(needle))
190
+ .sort((a, b) => a.name.localeCompare(b.name))
191
+ .map(skill => ({
192
+ name: skill.name,
193
+ description: skill.description,
194
+ source: skill.source,
195
+ source_id: skill.source_id,
196
+ scope: skill.scope,
197
+ content_hash: skill.content_hash,
198
+ compatibility: skill.compatibility,
199
+ shadowed_sources: (this.versions.get(skill.name) || []).slice(1).map(item => item.source_id),
200
+ }));
201
+ }
202
+
203
+ view(name, resourcePath = null, { sourceId = null, maxChars = DEFAULT_MAX_CHARS } = {}) {
204
+ const skill = this.get(name, sourceId);
205
+ if (!skill) throw new Error(`Unknown skill: ${name}`);
206
+ if (!resourcePath) {
207
+ return {
208
+ name: skill.name,
209
+ description: skill.description,
210
+ source: skill.source,
211
+ source_id: skill.source_id,
212
+ scope: skill.scope,
213
+ content_hash: skill.content_hash,
214
+ compatibility: skill.compatibility,
215
+ shadowed_sources: (this.versions.get(skill.name) || [])
216
+ .filter(item => item.source_id !== skill.source_id)
217
+ .map(item => item.source_id),
218
+ instructions: bounded(skill.instructions, maxChars),
219
+ resources: walkFiles(skill.root)
220
+ .map(file => path.relative(skill.root, file).split(path.sep).join('/'))
221
+ .filter(file => file !== 'SKILL.md'),
222
+ metadata: skill.metadata,
223
+ };
224
+ }
225
+
226
+ if (path.isAbsolute(resourcePath)) throw new Error('Skill resource path must be relative');
227
+ const candidate = path.resolve(skill.root, resourcePath);
228
+ if (!isWithin(skill.root, candidate)) throw new Error('Skill resource escapes its bundle');
229
+ const relativeParts = path.relative(skill.root, candidate).split(path.sep);
230
+ let cursor = skill.root;
231
+ for (const part of relativeParts) {
232
+ cursor = path.join(cursor, part);
233
+ if (!fs.existsSync(cursor)) throw new Error(`Skill resource not found: ${resourcePath}`);
234
+ if (fs.lstatSync(cursor).isSymbolicLink()) throw new Error('Symlinked skill resources are not allowed');
235
+ }
236
+ if (!fs.statSync(candidate).isFile()) throw new Error(`Skill resource not found: ${resourcePath}`);
237
+ return {
238
+ name: skill.name,
239
+ source_id: skill.source_id,
240
+ path: resourcePath,
241
+ content: bounded(fs.readFileSync(candidate, 'utf-8'), maxChars),
242
+ };
243
+ }
244
+
245
+ async run(name, args) {
246
+ const skill = this.get(name);
247
+ if (!skill) throw new Error(`Unknown skill: ${name}`);
248
+ let prompt = skill.instructions;
249
+ if (args) prompt = prompt.replace(/\$ARGUMENTS/g, args);
250
+ return `[Skill: ${skill.name}]\n${prompt}${args ? `\n\nArguments: ${args}` : ''}`;
251
+ }
252
+ }
@@ -0,0 +1,55 @@
1
+ /**
2
+ * Skills Runner — executes a skill by injecting its prompt.
3
+ *
4
+ * When a skill is invoked, its prompt is injected as a system message
5
+ * into the conversation context, guiding the agent's behavior.
6
+ */
7
+
8
+ export class SkillRunner {
9
+ /**
10
+ * @param {object} loader - SkillsLoader instance
11
+ * @param {object} agentLoop - agent loop instance
12
+ */
13
+ constructor(loader, agentLoop) {
14
+ this.loader = loader;
15
+ this.loop = agentLoop;
16
+ }
17
+
18
+ /**
19
+ * Execute a skill.
20
+ * @param {string} name - skill name
21
+ * @param {string} [args] - optional arguments
22
+ * @returns {AsyncGenerator} event stream from agent loop
23
+ */
24
+ async *execute(name, args) {
25
+ const skill = this.loader.get(name);
26
+ if (!skill) {
27
+ yield { type: 'error', message: `Unknown skill: ${name}` };
28
+ return;
29
+ }
30
+
31
+ // Build the skill prompt
32
+ let prompt = skill.prompt;
33
+ if (args) {
34
+ prompt = prompt.replace(/\$ARGUMENTS/g, args);
35
+ }
36
+
37
+ // Inject skill context as a user message
38
+ const message = `[Invoking skill: ${skill.name}]\n\n${prompt}${args ? `\n\nArguments: ${args}` : ''}`;
39
+
40
+ // Run through agent loop
41
+ yield* this.loop.run(message);
42
+ }
43
+
44
+ /**
45
+ * List available skills for display.
46
+ * @returns {Array<{name: string, description: string}>}
47
+ */
48
+ listAvailable() {
49
+ return this.loader.list().map(s => ({
50
+ name: s.name,
51
+ description: s.description,
52
+ aliases: s.aliases || [],
53
+ }));
54
+ }
55
+ }
@@ -0,0 +1,263 @@
1
+ /**
2
+ * Orbit state machine — Mission Control (PRD-055 §5.2).
3
+ *
4
+ * The "orbit" is the current phase of the session. The status bar reads
5
+ * from this module; the REPL pushes events into it. It is intentionally a
6
+ * pure state machine — no I/O, no side effects, no globals. Each REPL
7
+ * creates one instance.
8
+ *
9
+ * const orbit = createOrbit();
10
+ * orbit.on('change', state => statusBar.render(state));
11
+ * orbit.onEvent({ type: 'tool_call', data: { tool: 'edit_file' } });
12
+ *
13
+ * States (PRD §5.2):
14
+ * IDLE — waiting for user input
15
+ * DISCOVERY — first message until first plan or edit
16
+ * PLANNING — preflight plan running OR plan() sub-agent active
17
+ * EXECUTION — write/edit/shell tools firing
18
+ * ALIGNMENT — tests / validators running
19
+ * AWAITING — approval required
20
+ * PAUSED — user pressed `p`
21
+ *
22
+ * Transitions are derived from existing backend SSE events. We never
23
+ * teach the backend about orbits; the CLI infers them from tool activity.
24
+ */
25
+
26
+ // Tool families used for orbit inference. Mirrors src/ui/icons.mjs but
27
+ // scoped to the few orbits actually need.
28
+ const PLANNING_TOOLS = new Set(['plan']);
29
+ const EXECUTION_TOOLS = new Set(['edit_file', 'write_file', 'write_project', 'shell', 'delete_file']);
30
+ const ALIGNMENT_TOOLS = new Set(['run_tests', 'validate_build', 'lint_check', 'validate_file', 'validate_structure', 'git_diff', 'git_status']);
31
+ const RESEARCH_TOOLS = new Set(['search_code', 'search_files', 'grep', 'read_file', 'read_files', 'list_files', 'analyze_code', 'get_project_overview', 'explore']);
32
+
33
+ export const ORBITS = Object.freeze({
34
+ IDLE: 'IDLE',
35
+ DISCOVERY: 'DISCOVERY',
36
+ PLANNING: 'PLANNING',
37
+ EXECUTION: 'EXECUTION',
38
+ ALIGNMENT: 'ALIGNMENT',
39
+ AWAITING: 'AWAITING',
40
+ PAUSED: 'PAUSED',
41
+ });
42
+
43
+ /**
44
+ * @returns the snapshot consumed by the status bar.
45
+ */
46
+ function snapshot(s) {
47
+ return {
48
+ orbit: s.orbit,
49
+ task: s.task || '',
50
+ turn: s.turn,
51
+ maxTurn: s.maxTurn,
52
+ cost: s.cost,
53
+ activeTool: s.activeTool || '',
54
+ subAgents: s.subAgents,
55
+ paused: s.paused,
56
+ awaitingTier: s.awaitingTier || null,
57
+ awaitingTool: s.awaitingTool || '',
58
+ };
59
+ }
60
+
61
+ export function createOrbit() {
62
+ const state = {
63
+ orbit: ORBITS.IDLE,
64
+ task: '',
65
+ turn: 0,
66
+ maxTurn: 0,
67
+ cost: 0,
68
+ activeTool: '',
69
+ subAgents: 0, // count of currently-active sub-agents
70
+ paused: false,
71
+ awaitingTool: '',
72
+ awaitingTier: null,
73
+ _hasEdited: false, // for DISCOVERY → EXECUTION transition
74
+ _resumeOrbit: null, // remembered orbit when paused
75
+ };
76
+
77
+ const listeners = new Set();
78
+
79
+ function emit() {
80
+ const snap = snapshot(state);
81
+ for (const fn of listeners) {
82
+ try { fn(snap); } catch {}
83
+ }
84
+ }
85
+
86
+ function setOrbit(next) {
87
+ if (state.paused && next !== ORBITS.PAUSED && next !== ORBITS.IDLE) {
88
+ // While paused, remember the orbit that would have applied but stay paused.
89
+ state._resumeOrbit = next;
90
+ return;
91
+ }
92
+ if (state.orbit === next) return;
93
+ state.orbit = next;
94
+ emit();
95
+ }
96
+
97
+ function inferOrbitFromTool(toolName) {
98
+ if (state.paused) return null;
99
+ if (state.subAgents > 0 && PLANNING_TOOLS.has(toolName)) return ORBITS.PLANNING;
100
+ if (PLANNING_TOOLS.has(toolName)) return ORBITS.PLANNING;
101
+ if (EXECUTION_TOOLS.has(toolName)) {
102
+ state._hasEdited = true;
103
+ return ORBITS.EXECUTION;
104
+ }
105
+ if (ALIGNMENT_TOOLS.has(toolName)) return ORBITS.ALIGNMENT;
106
+ if (RESEARCH_TOOLS.has(toolName)) {
107
+ // Stay in DISCOVERY until first edit; afterwards research stays in
108
+ // current orbit (EXECUTION) so the status doesn't flicker back.
109
+ return state._hasEdited ? null : ORBITS.DISCOVERY;
110
+ }
111
+ return null;
112
+ }
113
+
114
+ return {
115
+ state: () => snapshot(state),
116
+
117
+ on(event, fn) {
118
+ if (event !== 'change') return () => {};
119
+ listeners.add(fn);
120
+ return () => listeners.delete(fn);
121
+ },
122
+
123
+ // ── Inbound events from the REPL ──────────────────────────────────
124
+
125
+ onUserInput(text) {
126
+ // First user message of the session OR a new turn opens DISCOVERY.
127
+ state.turn++;
128
+ state.task = (text || '').replace(/\s+/g, ' ').trim().slice(0, 80);
129
+ state._hasEdited = false;
130
+ state.activeTool = '';
131
+ setOrbit(ORBITS.DISCOVERY);
132
+ },
133
+
134
+ onMaxTurn(n) {
135
+ if (typeof n === 'number' && n > 0) {
136
+ state.maxTurn = n;
137
+ emit();
138
+ }
139
+ },
140
+
141
+ onTask(text) {
142
+ if (!text) return;
143
+ state.task = String(text).replace(/\s+/g, ' ').trim().slice(0, 80);
144
+ emit();
145
+ },
146
+
147
+ onCost(value) {
148
+ if (typeof value === 'number' && Number.isFinite(value)) {
149
+ state.cost = value;
150
+ emit();
151
+ }
152
+ },
153
+
154
+ onToolCall(toolName) {
155
+ state.activeTool = toolName || '';
156
+ const next = inferOrbitFromTool(toolName);
157
+ if (next) setOrbit(next);
158
+ else emit();
159
+ },
160
+
161
+ onToolResult() {
162
+ state.activeTool = '';
163
+ emit();
164
+ },
165
+
166
+ onSubAgentStart() {
167
+ state.subAgents = Math.max(0, state.subAgents) + 1;
168
+ setOrbit(ORBITS.PLANNING);
169
+ },
170
+
171
+ onSubAgentEnd() {
172
+ state.subAgents = Math.max(0, state.subAgents - 1);
173
+ // Fall back to whatever the parent was doing — we don't track that
174
+ // precisely, so go to EXECUTION if an edit has happened, else
175
+ // DISCOVERY. The next tool_call event will refine.
176
+ if (state.subAgents === 0) {
177
+ setOrbit(state._hasEdited ? ORBITS.EXECUTION : ORBITS.DISCOVERY);
178
+ } else {
179
+ emit();
180
+ }
181
+ },
182
+
183
+ onApprovalRequired({ tool, tier } = {}) {
184
+ state.awaitingTool = tool || '';
185
+ state.awaitingTier = tier || null;
186
+ setOrbit(ORBITS.AWAITING);
187
+ },
188
+
189
+ onApprovalResolved() {
190
+ state.awaitingTool = '';
191
+ state.awaitingTier = null;
192
+ // Drop back to the inferred orbit for the active tool, or EXECUTION
193
+ // if we have an active tool but can't classify, or IDLE.
194
+ const inferred = inferOrbitFromTool(state.activeTool) || (state._hasEdited ? ORBITS.EXECUTION : ORBITS.DISCOVERY);
195
+ setOrbit(inferred);
196
+ },
197
+
198
+ onComplete({ cost } = {}) {
199
+ if (typeof cost === 'number') state.cost = cost;
200
+ state.activeTool = '';
201
+ setOrbit(ORBITS.IDLE);
202
+ },
203
+
204
+ onPause() {
205
+ if (state.paused) return;
206
+ state.paused = true;
207
+ state._resumeOrbit = state.orbit;
208
+ state.orbit = ORBITS.PAUSED;
209
+ emit();
210
+ },
211
+
212
+ onResume() {
213
+ if (!state.paused) return;
214
+ state.paused = false;
215
+ const resume = state._resumeOrbit || ORBITS.IDLE;
216
+ state._resumeOrbit = null;
217
+ state.orbit = resume;
218
+ emit();
219
+ },
220
+
221
+ /**
222
+ * Generic event router so the REPL can feed raw SSE events without a
223
+ * giant switch in this module. Returns true if the event was handled.
224
+ */
225
+ onEvent(event) {
226
+ if (!event || !event.type) return false;
227
+ const { type, data } = event;
228
+ switch (type) {
229
+ case 'tool_call':
230
+ case 'tool_request':
231
+ this.onToolCall(data?.tool || '');
232
+ return true;
233
+ case 'tool_result':
234
+ case 'tool_done':
235
+ this.onToolResult();
236
+ return true;
237
+ case 'sub_agent_start':
238
+ this.onSubAgentStart();
239
+ return true;
240
+ case 'sub_agent_complete':
241
+ this.onSubAgentEnd();
242
+ return true;
243
+ case 'approval_required':
244
+ this.onApprovalRequired({ tool: data?.tool, tier: data?.tier });
245
+ return true;
246
+ case 'approval_granted':
247
+ case 'approval_denied':
248
+ this.onApprovalResolved();
249
+ return true;
250
+ case 'complete': {
251
+ const usage = data?.usage || {};
252
+ this.onComplete({ cost: usage.total_cost_usd ?? usage.cost_usd });
253
+ return true;
254
+ }
255
+ case 'plan_created':
256
+ this.onTask(data?.title || data?.task || '');
257
+ return true;
258
+ default:
259
+ return false;
260
+ }
261
+ },
262
+ };
263
+ }
@@ -0,0 +1,99 @@
1
+ /**
2
+ * Verbosity modes — Mission Control (PRD-055 §12).
3
+ *
4
+ * quiet Folded summary only. Sub-agent inner tools hidden.
5
+ * default Folded summary. Sub-agent header shown, inner tools folded.
6
+ * verbose Folded summary. Sub-agent inner tools shown.
7
+ * surgical Expanded tool details + raw model reasoning.
8
+ *
9
+ * Persisted to `~/.bahulam/config.json` under the `verbosity` key so the
10
+ * choice survives across sessions.
11
+ *
12
+ * import { getVerbosity, setVerbosity, showSubAgentTools, showReasoning } from './verbosity.mjs';
13
+ *
14
+ * No imports from the REPL — this module is pure state + filesystem.
15
+ */
16
+
17
+ import fs from 'node:fs';
18
+ import path from 'node:path';
19
+ import { bahulamHome } from '../core/paths.mjs';
20
+
21
+ export const MODES = Object.freeze({
22
+ QUIET: 'quiet',
23
+ DEFAULT: 'default',
24
+ VERBOSE: 'verbose',
25
+ SURGICAL: 'surgical',
26
+ });
27
+
28
+ const VALID = new Set(Object.values(MODES));
29
+
30
+ const CONFIG_DIR = bahulamHome();
31
+ const CONFIG_PATH = path.join(CONFIG_DIR, 'config.json');
32
+
33
+ let _cached = null;
34
+
35
+ function readConfig() {
36
+ try { return JSON.parse(fs.readFileSync(CONFIG_PATH, 'utf-8')); }
37
+ catch { return {}; }
38
+ }
39
+
40
+ function writeConfig(obj) {
41
+ try {
42
+ fs.mkdirSync(CONFIG_DIR, { recursive: true, mode: 0o700 });
43
+ fs.writeFileSync(CONFIG_PATH, JSON.stringify(obj, null, 2));
44
+ } catch { /* best effort */ }
45
+ }
46
+
47
+ /** Read the current mode (falls back to default). */
48
+ export function getVerbosity() {
49
+ if (_cached) return _cached;
50
+ const v = readConfig().verbosity;
51
+ _cached = VALID.has(v) ? v : MODES.DEFAULT;
52
+ return _cached;
53
+ }
54
+
55
+ /** Update the persisted mode. Returns the new mode. */
56
+ export function setVerbosity(mode) {
57
+ if (!VALID.has(mode)) throw new Error(`Unknown verbosity mode: ${mode}`);
58
+ const cfg = readConfig();
59
+ cfg.verbosity = mode;
60
+ writeConfig(cfg);
61
+ _cached = mode;
62
+ return mode;
63
+ }
64
+
65
+ /** Force-reload from disk (used by tests). */
66
+ export function _resetCache() { _cached = null; }
67
+
68
+ // ── Predicates — let other modules ask "should I render X?" ─────────────
69
+
70
+ /** Should sub-agent inner tool cards be printed? */
71
+ export function showSubAgentTools(mode = getVerbosity()) {
72
+ return mode === MODES.VERBOSE || mode === MODES.SURGICAL;
73
+ }
74
+
75
+ /** Should raw model reasoning be printed? */
76
+ export function showReasoning(mode = getVerbosity()) {
77
+ return mode === MODES.SURGICAL;
78
+ }
79
+
80
+ /** Should tool cards default to expanded instead of folded? */
81
+ export function defaultExpanded(mode = getVerbosity()) {
82
+ return mode === MODES.SURGICAL;
83
+ }
84
+
85
+ /** Should markdown be rendered? (only `surgical` shows raw, others render) */
86
+ export function renderMarkdown(mode = getVerbosity()) {
87
+ return mode !== MODES.SURGICAL;
88
+ }
89
+
90
+ /** Per-mode label for /help and status display. */
91
+ export function label(mode = getVerbosity()) {
92
+ switch (mode) {
93
+ case MODES.QUIET: return 'quiet (compact)';
94
+ case MODES.DEFAULT: return 'default';
95
+ case MODES.VERBOSE: return 'verbose (sub-agent tools visible)';
96
+ case MODES.SURGICAL: return 'surgical (everything shown)';
97
+ default: return String(mode || 'default');
98
+ }
99
+ }