@bahulam/code 2.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +80 -0
- package/package.json +49 -0
- package/pulse/app/activity/page.tsx +190 -0
- package/pulse/app/api/activity/route.ts +138 -0
- package/pulse/app/api/benchmark/route.ts +113 -0
- package/pulse/app/api/benchmarks/route.ts +195 -0
- package/pulse/app/api/costs/route.ts +88 -0
- package/pulse/app/api/export/route.ts +77 -0
- package/pulse/app/api/history/route.ts +11 -0
- package/pulse/app/api/import/route.ts +31 -0
- package/pulse/app/api/memory/route.ts +50 -0
- package/pulse/app/api/plans/route.ts +9 -0
- package/pulse/app/api/projects/[slug]/route.ts +96 -0
- package/pulse/app/api/projects/route.ts +121 -0
- package/pulse/app/api/sessions/[id]/replay/route.ts +20 -0
- package/pulse/app/api/sessions/[id]/route.ts +31 -0
- package/pulse/app/api/sessions/route.ts +112 -0
- package/pulse/app/api/settings/route.ts +14 -0
- package/pulse/app/api/stats/route.ts +143 -0
- package/pulse/app/api/todos/route.ts +9 -0
- package/pulse/app/api/tools/route.ts +160 -0
- package/pulse/app/benchmarks/page.tsx +224 -0
- package/pulse/app/costs/page.tsx +179 -0
- package/pulse/app/export/page.tsx +465 -0
- package/pulse/app/favicon.ico +0 -0
- package/pulse/app/globals.css +263 -0
- package/pulse/app/help/page.tsx +143 -0
- package/pulse/app/history/page.tsx +157 -0
- package/pulse/app/layout.tsx +46 -0
- package/pulse/app/memory/page.tsx +365 -0
- package/pulse/app/overview-client.tsx +393 -0
- package/pulse/app/page.tsx +14 -0
- package/pulse/app/plans/page.tsx +308 -0
- package/pulse/app/projects/[slug]/page.tsx +390 -0
- package/pulse/app/projects/page.tsx +110 -0
- package/pulse/app/sessions/[id]/page.tsx +243 -0
- package/pulse/app/sessions/page.tsx +39 -0
- package/pulse/app/settings/page.tsx +188 -0
- package/pulse/app/todos/page.tsx +211 -0
- package/pulse/app/tools/page.tsx +249 -0
- package/pulse/cli.js +164 -0
- package/pulse/components/activity/day-of-week-chart.tsx +35 -0
- package/pulse/components/activity/streak-card.tsx +36 -0
- package/pulse/components/costs/cache-efficiency-panel.tsx +76 -0
- package/pulse/components/costs/cost-by-project-chart.tsx +48 -0
- package/pulse/components/costs/cost-over-time-chart.tsx +95 -0
- package/pulse/components/costs/model-token-table.tsx +60 -0
- package/pulse/components/global-search.tsx +193 -0
- package/pulse/components/keyboard-nav-provider.tsx +23 -0
- package/pulse/components/layout/bottom-nav.tsx +53 -0
- package/pulse/components/layout/client-layout.tsx +31 -0
- package/pulse/components/layout/sidebar-context.tsx +50 -0
- package/pulse/components/layout/sidebar.tsx +183 -0
- package/pulse/components/layout/top-bar.tsx +121 -0
- package/pulse/components/overview/activity-heatmap.tsx +107 -0
- package/pulse/components/overview/conversation-table.tsx +148 -0
- package/pulse/components/overview/model-breakdown-donut.tsx +95 -0
- package/pulse/components/overview/peak-hours-chart.tsx +87 -0
- package/pulse/components/overview/project-activity-donut.tsx +96 -0
- package/pulse/components/overview/stat-card.tsx +102 -0
- package/pulse/components/overview/usage-over-time-chart.tsx +166 -0
- package/pulse/components/projects/project-card.tsx +175 -0
- package/pulse/components/sessions/replay/assistant-markdown.tsx +94 -0
- package/pulse/components/sessions/replay/compaction-card.tsx +25 -0
- package/pulse/components/sessions/replay/session-sidebar.tsx +231 -0
- package/pulse/components/sessions/replay/token-accumulation-chart.tsx +98 -0
- package/pulse/components/sessions/replay/tool-call-badge.tsx +127 -0
- package/pulse/components/sessions/replay/turn-cards.tsx +220 -0
- package/pulse/components/sessions/replay/user-tool-result.tsx +158 -0
- package/pulse/components/sessions/session-badges.tsx +49 -0
- package/pulse/components/sessions/session-table.tsx +299 -0
- package/pulse/components/theme-provider.tsx +44 -0
- package/pulse/components/tools/feature-adoption-table.tsx +58 -0
- package/pulse/components/tools/mcp-server-panel.tsx +45 -0
- package/pulse/components/tools/tool-ranking-chart.tsx +57 -0
- package/pulse/components/tools/version-history-table.tsx +32 -0
- package/pulse/components/ui/alert.tsx +66 -0
- package/pulse/components/ui/badge.tsx +48 -0
- package/pulse/components/ui/breadcrumb.tsx +109 -0
- package/pulse/components/ui/button.tsx +64 -0
- package/pulse/components/ui/calendar.tsx +220 -0
- package/pulse/components/ui/card.tsx +92 -0
- package/pulse/components/ui/command.tsx +158 -0
- package/pulse/components/ui/dialog.tsx +158 -0
- package/pulse/components/ui/input.tsx +21 -0
- package/pulse/components/ui/popover.tsx +89 -0
- package/pulse/components/ui/progress.tsx +31 -0
- package/pulse/components/ui/select.tsx +190 -0
- package/pulse/components/ui/separator.tsx +28 -0
- package/pulse/components/ui/sheet.tsx +143 -0
- package/pulse/components/ui/skeleton.tsx +13 -0
- package/pulse/components/ui/table.tsx +116 -0
- package/pulse/components/ui/tabs.tsx +91 -0
- package/pulse/components/ui/tooltip.tsx +57 -0
- package/pulse/components/use-global-keyboard-nav.ts +79 -0
- package/pulse/components.json +23 -0
- package/pulse/eslint.config.mjs +18 -0
- package/pulse/lib/bahulam-paths.ts +23 -0
- package/pulse/lib/claude-reader.ts +592 -0
- package/pulse/lib/decode.ts +129 -0
- package/pulse/lib/pricing.ts +102 -0
- package/pulse/lib/replay-parser.ts +165 -0
- package/pulse/lib/tool-categories.ts +127 -0
- package/pulse/lib/utils.ts +6 -0
- package/pulse/next-env.d.ts +6 -0
- package/pulse/next.config.ts +16 -0
- package/pulse/package.json +45 -0
- package/pulse/postcss.config.mjs +7 -0
- package/pulse/public/activity.png +0 -0
- package/pulse/public/cc-lens.png +0 -0
- package/pulse/public/command-k.png +0 -0
- package/pulse/public/costs.png +0 -0
- package/pulse/public/dashboard-dark.png +0 -0
- package/pulse/public/dashboard-white.png +0 -0
- package/pulse/public/export.png +0 -0
- package/pulse/public/file.svg +1 -0
- package/pulse/public/globe.svg +1 -0
- package/pulse/public/next.svg +1 -0
- package/pulse/public/projects.png +0 -0
- package/pulse/public/session-chat.png +0 -0
- package/pulse/public/todos.png +0 -0
- package/pulse/public/tools.png +0 -0
- package/pulse/public/vercel.svg +1 -0
- package/pulse/public/window.svg +1 -0
- package/pulse/tsconfig.json +34 -0
- package/pulse/types/claude.ts +294 -0
- package/src/agents/loader.mjs +94 -0
- package/src/agents/multi_workflow_loader.mjs +330 -0
- package/src/agents/parser.mjs +205 -0
- package/src/agents/scaffold.mjs +222 -0
- package/src/agents/teams.mjs +123 -0
- package/src/agents/workflow_loader.mjs +122 -0
- package/src/agents/workflow_scaffold.mjs +249 -0
- package/src/auth/oauth.mjs +220 -0
- package/src/auth/tarang-auth.mjs +306 -0
- package/src/commands/agent.mjs +220 -0
- package/src/commands/workflow.mjs +581 -0
- package/src/config/cli-args.mjs +200 -0
- package/src/config/env.mjs +263 -0
- package/src/config/hook-runner.mjs +100 -0
- package/src/config/memory-loader.mjs +32 -0
- package/src/config/settings-loader.mjs +45 -0
- package/src/config/settings.mjs +132 -0
- package/src/context/ast-parser.mjs +298 -0
- package/src/context/bm25.mjs +85 -0
- package/src/context/retriever.mjs +308 -0
- package/src/context/skeleton.mjs +134 -0
- package/src/context/symbol-indexer.mjs +375 -0
- package/src/core/agent-history.mjs +111 -0
- package/src/core/agent-loop.mjs +486 -0
- package/src/core/approval-log.mjs +104 -0
- package/src/core/approval.mjs +476 -0
- package/src/core/attachments.mjs +380 -0
- package/src/core/backend-url.mjs +55 -0
- package/src/core/cache-control.mjs +92 -0
- package/src/core/cache.mjs +105 -0
- package/src/core/callback-client.mjs +180 -0
- package/src/core/checkpoints.mjs +142 -0
- package/src/core/compact-history.mjs +127 -0
- package/src/core/context-envelope.mjs +54 -0
- package/src/core/context-manager.mjs +198 -0
- package/src/core/error-guidance.mjs +311 -0
- package/src/core/file-diff.mjs +217 -0
- package/src/core/headless.mjs +448 -0
- package/src/core/hooks-manager.mjs +87 -0
- package/src/core/jsonl-writer.mjs +449 -0
- package/src/core/local-agent.mjs +537 -0
- package/src/core/local-store.mjs +836 -0
- package/src/core/mode-selector.mjs +51 -0
- package/src/core/output-filter.mjs +177 -0
- package/src/core/paths.mjs +190 -0
- package/src/core/policy-resolver.mjs +156 -0
- package/src/core/pricing.mjs +336 -0
- package/src/core/project-artifacts.mjs +39 -0
- package/src/core/project-context-loader.mjs +139 -0
- package/src/core/providers.mjs +219 -0
- package/src/core/rate-limit-display.mjs +121 -0
- package/src/core/rate-limiter.mjs +119 -0
- package/src/core/resume-mode.mjs +192 -0
- package/src/core/risk-tier.mjs +337 -0
- package/src/core/safety.mjs +203 -0
- package/src/core/scheduler.mjs +173 -0
- package/src/core/session-manager.mjs +360 -0
- package/src/core/session.mjs +143 -0
- package/src/core/settings-sync.mjs +85 -0
- package/src/core/stagnation.mjs +57 -0
- package/src/core/stream-client.mjs +829 -0
- package/src/core/streaming.mjs +182 -0
- package/src/core/system-prompt.mjs +140 -0
- package/src/core/tasks.mjs +196 -0
- package/src/core/tool-executor.mjs +1950 -0
- package/src/core/trust.mjs +158 -0
- package/src/core/work-scope.mjs +248 -0
- package/src/hooks/engine.mjs +162 -0
- package/src/index.mjs +426 -0
- package/src/mcp/client.mjs +253 -0
- package/src/mcp/transport-shttp.mjs +130 -0
- package/src/mcp/transport-sse.mjs +131 -0
- package/src/mcp/transport-ws.mjs +134 -0
- package/src/onboarding/preflight.mjs +360 -0
- package/src/permissions/checker.mjs +57 -0
- package/src/permissions/command-classifier.mjs +652 -0
- package/src/permissions/injection-check.mjs +60 -0
- package/src/permissions/path-check.mjs +102 -0
- package/src/permissions/prompt.mjs +73 -0
- package/src/permissions/sandbox.mjs +112 -0
- package/src/plugins/loader.mjs +138 -0
- package/src/skills/installer.mjs +188 -0
- package/src/skills/loader.mjs +252 -0
- package/src/skills/runner.mjs +55 -0
- package/src/state/orbit.mjs +263 -0
- package/src/state/verbosity.mjs +99 -0
- package/src/telemetry/index.mjs +96 -0
- package/src/terminal/agents.mjs +177 -0
- package/src/terminal/analytics.mjs +292 -0
- package/src/terminal/ansi.mjs +695 -0
- package/src/terminal/init.mjs +145 -0
- package/src/terminal/main.mjs +269 -0
- package/src/terminal/repl-explore.mjs +35 -0
- package/src/terminal/repl-format.mjs +257 -0
- package/src/terminal/repl-render.mjs +561 -0
- package/src/terminal/repl-resume.mjs +625 -0
- package/src/terminal/repl-state.mjs +103 -0
- package/src/terminal/repl-utils.mjs +34 -0
- package/src/terminal/repl.mjs +3832 -0
- package/src/terminal/skills.mjs +54 -0
- package/src/terminal/tool-display.mjs +240 -0
- package/src/tools/agent.mjs +137 -0
- package/src/tools/ask-user.mjs +61 -0
- package/src/tools/bash.mjs +231 -0
- package/src/tools/cron-create.mjs +120 -0
- package/src/tools/cron-delete.mjs +49 -0
- package/src/tools/cron-list.mjs +37 -0
- package/src/tools/edit.mjs +82 -0
- package/src/tools/enter-worktree.mjs +69 -0
- package/src/tools/exit-worktree.mjs +57 -0
- package/src/tools/glob.mjs +117 -0
- package/src/tools/grep.mjs +129 -0
- package/src/tools/lint.mjs +71 -0
- package/src/tools/ls.mjs +58 -0
- package/src/tools/lsp.mjs +115 -0
- package/src/tools/multi-edit.mjs +94 -0
- package/src/tools/notebook-edit.mjs +96 -0
- package/src/tools/project-overview.mjs +641 -0
- package/src/tools/read-mcp-resource.mjs +57 -0
- package/src/tools/read.mjs +138 -0
- package/src/tools/registry.mjs +116 -0
- package/src/tools/remote-trigger.mjs +84 -0
- package/src/tools/send-message.mjs +64 -0
- package/src/tools/skill.mjs +52 -0
- package/src/tools/test-runner.mjs +49 -0
- package/src/tools/todo-write.mjs +68 -0
- package/src/tools/tool-search.mjs +77 -0
- package/src/tools/web-fetch.mjs +65 -0
- package/src/tools/web-search.mjs +89 -0
- package/src/tools/write.mjs +55 -0
- package/src/ui/approval.mjs +263 -0
- package/src/ui/banner.mjs +235 -0
- package/src/ui/commands.mjs +537 -0
- package/src/ui/formatter.mjs +409 -0
- package/src/ui/icons.mjs +164 -0
- package/src/ui/input-dock.mjs +444 -0
- package/src/ui/markdown.mjs +278 -0
- package/src/ui/mission-report.mjs +296 -0
- package/src/ui/palette.mjs +189 -0
- package/src/ui/slash-commands.mjs +245 -0
- package/src/ui/spinner.mjs +116 -0
- package/src/ui/sub-agent.mjs +152 -0
- package/src/ui/term.mjs +159 -0
- package/src/ui/text-layout.mjs +127 -0
- package/src/ui/tool-card.mjs +463 -0
- package/src/ui/tool-details.mjs +312 -0
- package/src/ui/transcript-block.mjs +21 -0
|
@@ -0,0 +1,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
|
+
}
|