@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,249 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import crypto from 'node:crypto';
4
+ import { loadMultiWorkflowFromFile } from './multi_workflow_loader.mjs';
5
+
6
+ export const WORKFLOW_SYNC_ENDPOINT = '/api/workflows';
7
+
8
+ export function slugifyWorkflowName(value) {
9
+ return String(value || 'workflow')
10
+ .trim()
11
+ .toLowerCase()
12
+ .replace(/[^a-z0-9]+/g, '-')
13
+ .replace(/^-+|-+$/g, '')
14
+ || 'workflow';
15
+ }
16
+
17
+ function yamlQuote(value) {
18
+ const text = String(value ?? '');
19
+ if (!text) return "''";
20
+ if (/[:#\n\r\t]/.test(text) || /^\s|\s$/.test(text) || text.includes('"') || text.includes("'")) {
21
+ return JSON.stringify(text);
22
+ }
23
+ return text;
24
+ }
25
+
26
+ function indentBlock(text, spaces) {
27
+ const pad = ' '.repeat(spaces);
28
+ return String(text || '')
29
+ .replace(/\r\n?/g, '\n')
30
+ .split('\n')
31
+ .map(line => `${pad}${line}`)
32
+ .join('\n');
33
+ }
34
+
35
+ function renderScalar(value) {
36
+ if (value === null || value === undefined) return 'null';
37
+ if (typeof value === 'number' || typeof value === 'boolean') return String(value);
38
+ return yamlQuote(value);
39
+ }
40
+
41
+ function renderYamlValue(value, indent = 0) {
42
+ const pad = ' '.repeat(indent);
43
+ if (Array.isArray(value)) {
44
+ if (!value.length) return `${pad}[]`;
45
+ return value.map(item => {
46
+ if (item && typeof item === 'object' && !Array.isArray(item)) {
47
+ const nested = renderYamlValue(item, indent + 2).split('\n');
48
+ return `${pad}- ${nested[0].trim()}\n${nested.slice(1).join('\n')}`;
49
+ }
50
+ return `${pad}- ${renderScalar(item)}`;
51
+ }).join('\n');
52
+ }
53
+
54
+ if (value && typeof value === 'object') {
55
+ const entries = Object.entries(value).filter(([, v]) => v !== undefined);
56
+ if (!entries.length) return `${pad}{}`;
57
+ return entries.map(([key, item]) => {
58
+ if (Array.isArray(item)) {
59
+ if (!item.length) return `${pad}${key}: []`;
60
+ const rendered = renderYamlValue(item, indent + 2);
61
+ return `${pad}${key}:\n${rendered}`;
62
+ }
63
+ if (item && typeof item === 'object') {
64
+ const rendered = renderYamlValue(item, indent + 2);
65
+ return `${pad}${key}:\n${rendered}`;
66
+ }
67
+ return `${pad}${key}: ${renderScalar(item)}`;
68
+ }).join('\n');
69
+ }
70
+
71
+ return `${pad}${renderScalar(value)}`;
72
+ }
73
+
74
+ function normalizeWorkflowAgent(agent, index) {
75
+ if (typeof agent === 'string') {
76
+ const slug = slugifyWorkflowName(agent);
77
+ return {
78
+ slug,
79
+ label: agent,
80
+ model: 'auto',
81
+ tools: [],
82
+ config: {},
83
+ };
84
+ }
85
+ const slug = slugifyWorkflowName(agent?.slug || agent?.name || `agent-${index + 1}`);
86
+ const label = agent?.label || agent?.name || slug;
87
+ return {
88
+ slug,
89
+ label,
90
+ model: agent?.model || 'auto',
91
+ tools: Array.isArray(agent?.tools) ? agent.tools : String(agent?.tools || '')
92
+ .split(',')
93
+ .map(item => item.trim())
94
+ .filter(Boolean),
95
+ config: agent?.config && typeof agent.config === 'object' ? agent.config : {},
96
+ };
97
+ }
98
+
99
+ function normalizeWorkflowEdges(edges, agentSlugs) {
100
+ if (Array.isArray(edges) && edges.length > 0) {
101
+ return edges
102
+ .filter(edge => edge && edge.source && edge.target)
103
+ .map(edge => ({ source: edge.source, target: edge.target }));
104
+ }
105
+ const normalized = [];
106
+ if (!agentSlugs.length) return normalized;
107
+ normalized.push({ source: 'trigger', target: agentSlugs[0] });
108
+ for (let i = 0; i < agentSlugs.length - 1; i++) {
109
+ normalized.push({ source: agentSlugs[i], target: agentSlugs[i + 1] });
110
+ }
111
+ normalized.push({ source: agentSlugs[agentSlugs.length - 1], target: 'output' });
112
+ return normalized;
113
+ }
114
+
115
+ export function createWorkflowFile({
116
+ cwd = process.cwd(),
117
+ name,
118
+ description = '',
119
+ pattern = 'sequential',
120
+ agents = [],
121
+ edges = [],
122
+ globalParams = {},
123
+ force = false,
124
+ } = {}) {
125
+ if (!name || !String(name).trim()) {
126
+ throw new Error('name is required');
127
+ }
128
+ const slug = slugifyWorkflowName(name);
129
+ const dir = path.join(cwd, '.bahulam', 'workflows');
130
+ const filePath = path.join(dir, `${slug}.yaml`);
131
+ if (fs.existsSync(filePath) && !force) {
132
+ throw new Error(`Workflow already exists: ${filePath}`);
133
+ }
134
+ const normalizedAgents = (Array.isArray(agents) ? agents : [agents])
135
+ .filter(Boolean)
136
+ .map((agent, index) => normalizeWorkflowAgent(agent, index));
137
+ if (!normalizedAgents.length) {
138
+ throw new Error('agents is required for a workflow');
139
+ }
140
+ const agentSlugs = normalizedAgents.map(agent => agent.slug);
141
+ const normalizedEdges = normalizeWorkflowEdges(edges, agentSlugs);
142
+
143
+ const lines = [
144
+ 'apiVersion: kepler.workflow/v1',
145
+ 'kind: MultiWorkflow',
146
+ 'metadata:',
147
+ ` name: ${yamlQuote(name)}`,
148
+ ];
149
+ if (description) {
150
+ lines.push(` description: ${yamlQuote(description)}`);
151
+ }
152
+ lines.push(
153
+ 'orchestration:',
154
+ ` pattern: ${yamlQuote(pattern)}`,
155
+ 'agents:'
156
+ );
157
+
158
+ for (const agent of normalizedAgents) {
159
+ lines.push(` - slug: ${yamlQuote(agent.slug)}`);
160
+ lines.push(` label: ${yamlQuote(agent.label)}`);
161
+ lines.push(` model: ${yamlQuote(agent.model || 'auto')}`);
162
+ lines.push(' tools:');
163
+ if (agent.tools.length === 0) {
164
+ lines.push(' []');
165
+ } else {
166
+ for (const tool of agent.tools) {
167
+ lines.push(` - ${yamlQuote(tool)}`);
168
+ }
169
+ }
170
+ const configKeys = Object.keys(agent.config || {});
171
+ if (configKeys.length > 0) {
172
+ lines.push(' config:');
173
+ for (const key of configKeys.sort()) {
174
+ const value = agent.config[key];
175
+ if (Array.isArray(value) || (value && typeof value === 'object')) {
176
+ lines.push(indentBlock(`${key}:`, 6));
177
+ lines.push(renderYamlValue(value, 8));
178
+ } else {
179
+ lines.push(` ${key}: ${renderScalar(value)}`);
180
+ }
181
+ }
182
+ }
183
+ }
184
+
185
+ lines.push('edges:');
186
+ for (const edge of normalizedEdges) {
187
+ lines.push(' - source: ' + yamlQuote(edge.source));
188
+ lines.push(' target: ' + yamlQuote(edge.target));
189
+ }
190
+
191
+ lines.push('global_params:');
192
+ const globalKeys = Object.keys(globalParams || {});
193
+ if (globalKeys.length === 0) {
194
+ lines.push(' {}');
195
+ } else {
196
+ for (const key of globalKeys.sort()) {
197
+ const value = globalParams[key];
198
+ if (Array.isArray(value) || (value && typeof value === 'object')) {
199
+ lines.push(indentBlock(`${key}:`, 2));
200
+ lines.push(renderYamlValue(value, 4));
201
+ } else {
202
+ lines.push(` ${key}: ${renderScalar(value)}`);
203
+ }
204
+ }
205
+ }
206
+
207
+ fs.mkdirSync(dir, { recursive: true });
208
+ fs.writeFileSync(filePath, `${lines.join('\n')}\n`, { mode: 0o600 });
209
+
210
+ return {
211
+ slug,
212
+ filePath,
213
+ pattern,
214
+ agent_count: normalizedAgents.length,
215
+ edge_count: normalizedEdges.length,
216
+ };
217
+ }
218
+
219
+ export function listLocalWorkflows(cwd = process.cwd()) {
220
+ const dir = path.join(cwd, '.bahulam', 'workflows');
221
+ const results = [];
222
+ try {
223
+ const entries = fs.readdirSync(dir, { withFileTypes: true });
224
+ for (const entry of entries) {
225
+ if (!entry.isFile()) continue;
226
+ if (!['.yaml', '.yml'].includes(path.extname(entry.name).toLowerCase())) continue;
227
+ const filePath = path.join(dir, entry.name);
228
+ try {
229
+ const payload = loadMultiWorkflowFromFile(filePath);
230
+ results.push({
231
+ filePath,
232
+ slug: slugifyWorkflowName(payload.name || entry.name),
233
+ ...payload,
234
+ agent_count: (payload.graph?.nodes || []).filter(node => node.type === 'agent').length,
235
+ edge_count: (payload.graph?.edges || []).length,
236
+ content_hash: crypto.createHash('sha256').update(JSON.stringify(payload)).digest('hex'),
237
+ });
238
+ } catch (err) {
239
+ if (process.env.DEBUG) {
240
+ console.error(`[workflow-scaffold] Failed to load ${filePath}: ${err.message}`);
241
+ }
242
+ }
243
+ }
244
+ } catch {
245
+ // Directory missing
246
+ }
247
+ return results;
248
+ }
249
+
@@ -0,0 +1,220 @@
1
+ /**
2
+ * OAuth Client — PKCE OAuth flow for Anthropic and other providers.
3
+ *
4
+ * Supports:
5
+ * - Device code flow (for headless environments)
6
+ * - Authorization code + PKCE
7
+ * - Token refresh
8
+ * - Credential storage in ~/.claude/credentials
9
+ */
10
+
11
+ import crypto from 'crypto';
12
+ import fs from 'fs';
13
+ import path from 'path';
14
+ import os from 'os';
15
+
16
+ export class OAuthClient {
17
+ /**
18
+ * @param {string} clientId - OAuth client ID
19
+ * @param {object} [options]
20
+ * @param {string} [options.authUrl] - authorization endpoint
21
+ * @param {string} [options.tokenUrl] - token endpoint
22
+ * @param {string} [options.deviceUrl] - device authorization endpoint
23
+ * @param {string} [options.credentialsPath] - path to store credentials
24
+ */
25
+ constructor(clientId, options = {}) {
26
+ this.clientId = clientId;
27
+ this.authUrl = options.authUrl || 'https://console.anthropic.com/oauth/authorize';
28
+ this.tokenUrl = options.tokenUrl || 'https://console.anthropic.com/oauth/token';
29
+ this.deviceUrl = options.deviceUrl || 'https://console.anthropic.com/oauth/device';
30
+ this.credentialsPath = options.credentialsPath ||
31
+ path.join(os.homedir(), '.claude', 'credentials');
32
+ }
33
+
34
+ /**
35
+ * Generate a PKCE code verifier and challenge.
36
+ * @returns {{ verifier: string, challenge: string }}
37
+ */
38
+ generatePKCE() {
39
+ const verifier = crypto.randomBytes(32)
40
+ .toString('base64url')
41
+ .replace(/[^a-zA-Z0-9]/g, '')
42
+ .substring(0, 128);
43
+
44
+ const challenge = crypto
45
+ .createHash('sha256')
46
+ .update(verifier)
47
+ .digest('base64url');
48
+
49
+ return { verifier, challenge };
50
+ }
51
+
52
+ /**
53
+ * Get the authorization URL for the PKCE flow.
54
+ * @param {object} [options]
55
+ * @param {string} [options.redirectUri] - redirect URI
56
+ * @param {string} [options.scope] - requested scope
57
+ * @returns {{ url: string, verifier: string, state: string }}
58
+ */
59
+ getAuthorizationUrl(options = {}) {
60
+ const { verifier, challenge } = this.generatePKCE();
61
+ const state = crypto.randomBytes(16).toString('hex');
62
+
63
+ const params = new URLSearchParams({
64
+ client_id: this.clientId,
65
+ response_type: 'code',
66
+ code_challenge: challenge,
67
+ code_challenge_method: 'S256',
68
+ state,
69
+ redirect_uri: options.redirectUri || 'http://localhost:9876/callback',
70
+ ...(options.scope && { scope: options.scope }),
71
+ });
72
+
73
+ return {
74
+ url: `${this.authUrl}?${params.toString()}`,
75
+ verifier,
76
+ state,
77
+ };
78
+ }
79
+
80
+ /**
81
+ * Start a device code flow (for headless environments).
82
+ * @returns {Promise<{ device_code: string, user_code: string, verification_uri: string, interval: number, expires_in: number }>}
83
+ */
84
+ async startDeviceFlow() {
85
+ const body = new URLSearchParams({
86
+ client_id: this.clientId,
87
+ });
88
+
89
+ const res = await fetch(this.deviceUrl, {
90
+ method: 'POST',
91
+ headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
92
+ body: body.toString(),
93
+ });
94
+
95
+ if (!res.ok) {
96
+ const text = await res.text();
97
+ throw new Error(`Device flow failed (${res.status}): ${text}`);
98
+ }
99
+
100
+ return res.json();
101
+ }
102
+
103
+ /**
104
+ * Exchange an authorization code for tokens (PKCE flow).
105
+ * @param {string} code - authorization code
106
+ * @param {string} verifier - PKCE code verifier
107
+ * @param {string} [redirectUri]
108
+ * @returns {Promise<{ access_token: string, refresh_token?: string, expires_in: number }>}
109
+ */
110
+ async exchangeCode(code, verifier, redirectUri) {
111
+ const body = new URLSearchParams({
112
+ grant_type: 'authorization_code',
113
+ client_id: this.clientId,
114
+ code,
115
+ code_verifier: verifier,
116
+ redirect_uri: redirectUri || 'http://localhost:9876/callback',
117
+ });
118
+
119
+ const res = await fetch(this.tokenUrl, {
120
+ method: 'POST',
121
+ headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
122
+ body: body.toString(),
123
+ });
124
+
125
+ if (!res.ok) {
126
+ const text = await res.text();
127
+ throw new Error(`Token exchange failed (${res.status}): ${text}`);
128
+ }
129
+
130
+ const token = await res.json();
131
+ this.saveToken(token);
132
+ return token;
133
+ }
134
+
135
+ /**
136
+ * Refresh an access token using a refresh token.
137
+ * @param {string} refreshToken
138
+ * @returns {Promise<{ access_token: string, refresh_token?: string, expires_in: number }>}
139
+ */
140
+ async refreshToken(refreshToken) {
141
+ const body = new URLSearchParams({
142
+ grant_type: 'refresh_token',
143
+ client_id: this.clientId,
144
+ refresh_token: refreshToken,
145
+ });
146
+
147
+ const res = await fetch(this.tokenUrl, {
148
+ method: 'POST',
149
+ headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
150
+ body: body.toString(),
151
+ });
152
+
153
+ if (!res.ok) {
154
+ const text = await res.text();
155
+ throw new Error(`Token refresh failed (${res.status}): ${text}`);
156
+ }
157
+
158
+ const token = await res.json();
159
+ this.saveToken(token);
160
+ return token;
161
+ }
162
+
163
+ /**
164
+ * Get stored token from credentials file.
165
+ * @returns {object|null}
166
+ */
167
+ getStoredToken() {
168
+ try {
169
+ const raw = fs.readFileSync(this.credentialsPath, 'utf-8');
170
+ return JSON.parse(raw);
171
+ } catch {
172
+ return null;
173
+ }
174
+ }
175
+
176
+ /**
177
+ * Save a token to the credentials file.
178
+ * @param {object} token
179
+ */
180
+ saveToken(token) {
181
+ try {
182
+ const dir = path.dirname(this.credentialsPath);
183
+ fs.mkdirSync(dir, { recursive: true });
184
+
185
+ const data = {
186
+ ...token,
187
+ saved_at: new Date().toISOString(),
188
+ };
189
+
190
+ fs.writeFileSync(this.credentialsPath, JSON.stringify(data, null, 2), { mode: 0o600 });
191
+ } catch {
192
+ // Best effort
193
+ }
194
+ }
195
+
196
+ /**
197
+ * Delete stored credentials.
198
+ */
199
+ clearToken() {
200
+ try {
201
+ fs.unlinkSync(this.credentialsPath);
202
+ return true;
203
+ } catch {
204
+ return false;
205
+ }
206
+ }
207
+
208
+ /**
209
+ * Check if the stored token is expired.
210
+ * @returns {boolean}
211
+ */
212
+ isTokenExpired() {
213
+ const token = this.getStoredToken();
214
+ if (!token || !token.saved_at || !token.expires_in) return true;
215
+
216
+ const savedAt = new Date(token.saved_at).getTime();
217
+ const expiresAt = savedAt + (token.expires_in * 1000);
218
+ return Date.now() >= expiresAt;
219
+ }
220
+ }