@multiclawcli/core 0.3.1 → 0.4.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.
@@ -1,61 +0,0 @@
1
- import type { AgentDefinition } from "../types/agent"
2
-
3
- type Overrides = Partial<AgentDefinition>
4
-
5
- const SYSTEM = `You are a UI/UX designer. Your job is to translate architecture and requirements into a detailed, developer-ready UI design specification.
6
-
7
- Responsibilities:
8
- - Define the component inventory: every page, section, and reusable component
9
- - Specify layouts, spacing principles, and responsive breakpoints
10
- - Define interaction patterns: loading states, empty states, error states, transitions
11
- - Specify design tokens: color palette, typography scale, spacing scale, shadow levels
12
- - Describe accessibility requirements: ARIA roles, keyboard navigation, focus management
13
- - Do not write implementation code — produce specification documents only
14
-
15
- Deliverables (save to the working directory):
16
- - ui-design.md — full UI design specification: component inventory, page layouts, interaction flows, design tokens, accessibility requirements
17
-
18
- Principles: Be specific enough that a developer can implement without guessing. Every component gets a name, purpose, props/variants, and states. Flag anything that depends on backend data.`
19
-
20
- const TASK = `## Task Scope
21
- {{file:task-plan.json}}
22
-
23
- Find the entry where id == "ui-designer" and treat its scope as the primary guide for your work.
24
- If task-plan.json is not found, determine your scope from the architecture design and API specification below.
25
-
26
- ---
27
-
28
- ## Project
29
- {{projectName}}
30
-
31
- ## Requirement
32
- {{requirement}}
33
-
34
- ## Architecture Design
35
- {{file:architecture.md}}
36
-
37
- ## API Specification
38
- {{file:api-spec.md}}
39
-
40
- ## Task
41
- Create ui-design.md covering:
42
- 1. **Component Inventory** — list every page and reusable component; for each: name, purpose, variants/states, props
43
- 2. **Page Layouts** — describe the visual structure of each page (header, main, sidebar, footer zones); include responsive behaviour
44
- 3. **Interaction Flows** — key user flows with step-by-step state transitions (loading → success → error)
45
- 4. **Design Tokens** — color palette (primary, neutral, semantic), typography scale, spacing scale
46
- 5. **Accessibility** — ARIA roles/labels for interactive components, keyboard nav, focus order
47
-
48
- Be concrete. Reference API endpoints where components bind to data. Flag any UX decision that needs product clarification.`
49
-
50
- export function uiDesigner(overrides?: Overrides): AgentDefinition {
51
- return {
52
- id: "ui-designer",
53
- name: "UI Designer",
54
- icon: "▣",
55
- models: { claude: "sonnet" },
56
- systemPrompt: SYSTEM,
57
- taskPrompt: TASK,
58
- tools: ["Read", "Write"],
59
- ...overrides,
60
- }
61
- }
@@ -1,4 +0,0 @@
1
- import type { MultiClawConfig } from "../types/config"
2
- export function defineConfig(config: MultiClawConfig): MultiClawConfig {
3
- return config
4
- }
@@ -1,19 +0,0 @@
1
- import type { MultiClawEvent, EventListener } from "../types/event"
2
-
3
- export class EventBus {
4
- private listeners = new Set<EventListener>()
5
-
6
- subscribe(listener: EventListener): () => void {
7
- this.listeners.add(listener)
8
- return () => this.listeners.delete(listener)
9
- }
10
-
11
- emit(event: MultiClawEvent): void {
12
- for (const listener of this.listeners) {
13
- try { listener(event) }
14
- catch (err) { console.error("EventBus listener error:", err) }
15
- }
16
- }
17
-
18
- clear(): void { this.listeners.clear() }
19
- }
package/src/index.ts DELETED
@@ -1,8 +0,0 @@
1
- export * from "./types"
2
- export { EventBus } from "./event/EventBus"
3
- export { Orchestrator } from "./orchestrator/Orchestrator"
4
- export { buildStages } from "./orchestrator/StageBuilder"
5
- export { runtimeRegistry, ClaudeRuntime } from "./runtime"
6
- export { RuntimeBase } from "./runtime/RuntimeBase"
7
- export { defineConfig } from "./config/defineConfig"
8
- export { agents } from "./agents"
@@ -1,104 +0,0 @@
1
- import * as fs from "fs"
2
- import * as path from "path"
3
- import type { AgentDefinition, AgentResult } from "../types/agent"
4
- import { runtimeRegistry } from "../runtime"
5
- import { EventBus } from "../event/EventBus"
6
- import { ContextManager } from "./ContextManager"
7
- import { withRetry } from "../utils/retry"
8
- import { withTimeout } from "../utils/timeout"
9
- import { renderPrompt } from "../utils/prompt"
10
-
11
- export class AgentRunner {
12
- constructor(
13
- private eventBus: EventBus,
14
- private contextManager: ContextManager,
15
- private globalWorkDir: string
16
- ) {}
17
-
18
- async run(agent: AgentDefinition): Promise<AgentResult> {
19
- const startTime = new Date()
20
- const workDir = agent.workDir
21
- ? path.resolve(this.globalWorkDir, agent.workDir)
22
- : this.globalWorkDir
23
- fs.mkdirSync(workDir, { recursive: true })
24
-
25
- const runtime = runtimeRegistry.get(agent.runtime ?? "claude")
26
- const retries = agent.retries ?? 0
27
- const timeout = agent.timeout ?? 600_000
28
- const tools = agent.tools ?? ["Read", "Write", "Bash"]
29
- const model = agent.model ?? agent.models?.[agent.runtime ?? "claude"]
30
-
31
- this.eventBus.emit({
32
- type: "agent:start",
33
- timestamp: new Date().toISOString(),
34
- payload: { agentId: agent.id, agentName: agent.name, icon: agent.icon, model, taskTitle: agent.taskTitle },
35
- })
36
-
37
- let attempts = 0
38
- let output = ""
39
-
40
- try {
41
- output = await withTimeout(
42
- (signal) => withRetry(async (attempt) => {
43
- attempts = attempt
44
- const prompt = await renderPrompt(
45
- agent.taskPrompt,
46
- this.contextManager.snapshot(),
47
- this.globalWorkDir
48
- )
49
- return (await runtime.execute({
50
- agentId: agent.id,
51
- systemPrompt: agent.systemPrompt,
52
- prompt, tools, workDir, timeout, signal,
53
- model,
54
- onOutput: (chunk) => {
55
- this.eventBus.emit({
56
- type: "agent:output",
57
- timestamp: new Date().toISOString(),
58
- payload: { agentId: agent.id, chunk },
59
- })
60
- },
61
- })).output
62
- }, retries, (attempt, err) => {
63
- this.eventBus.emit({
64
- type: "agent:retrying",
65
- timestamp: new Date().toISOString(),
66
- payload: {
67
- agentId: agent.id,
68
- agentName: agent.name,
69
- attempt,
70
- maxAttempts: retries + 1,
71
- error: (err as Error).message,
72
- },
73
- })
74
- }),
75
- timeout,
76
- agent.name
77
- )
78
-
79
- const endTime = new Date()
80
- const result: AgentResult = {
81
- agentId: agent.id, agentName: agent.name, status: "success",
82
- output, duration: endTime.getTime() - startTime.getTime(),
83
- attempts, startTime: startTime.toISOString(), endTime: endTime.toISOString(),
84
- }
85
- this.eventBus.emit({
86
- type: "agent:complete", timestamp: new Date().toISOString(), payload: result,
87
- })
88
- return result
89
- } catch (err) {
90
- const endTime = new Date()
91
- const result: AgentResult = {
92
- agentId: agent.id, agentName: agent.name, status: "failed",
93
- output, duration: endTime.getTime() - startTime.getTime(),
94
- attempts, error: (err as Error).message,
95
- startTime: startTime.toISOString(), endTime: endTime.toISOString(),
96
- }
97
- this.eventBus.emit({
98
- type: "agent:failed", timestamp: new Date().toISOString(), payload: result,
99
- })
100
- return result
101
- }
102
- }
103
- }
104
-
@@ -1,11 +0,0 @@
1
- export class ContextManager {
2
- private store = new Map<string, string>()
3
- constructor(initial?: Record<string, string>) {
4
- if (initial) for (const [k, v] of Object.entries(initial)) this.store.set(k, v)
5
- }
6
- set(key: string, value: string) { this.store.set(key, value) }
7
- get(key: string) { return this.store.get(key) }
8
- snapshot(): Record<string, string> {
9
- return Object.fromEntries(this.store.entries())
10
- }
11
- }
@@ -1,180 +0,0 @@
1
- import * as fs from "fs"
2
- import * as path from "path"
3
- import type { MultiClawConfig, OrchestratorResult } from "../types/config"
4
- import type { AgentDefinition, AgentResult, TaskPlan } from "../types/agent"
5
- import type { StageInfo } from "../types/event"
6
- import { EventBus } from "../event/EventBus"
7
- import { ContextManager } from "./ContextManager"
8
- import { AgentRunner } from "./AgentRunner"
9
- import { buildStages } from "./StageBuilder"
10
-
11
- export class Orchestrator {
12
- readonly eventBus = new EventBus()
13
- private contextManager: ContextManager
14
- private runner: AgentRunner
15
- private workDir: string
16
-
17
- constructor(private config: MultiClawConfig) {
18
- this.workDir = path.resolve(config.workDir)
19
- fs.mkdirSync(this.workDir, { recursive: true })
20
- this.contextManager = new ContextManager(config.context)
21
- this.runner = new AgentRunner(this.eventBus, this.contextManager, this.workDir)
22
- }
23
-
24
- async run(): Promise<OrchestratorResult> {
25
- const startTime = new Date()
26
- const agents = await this.resolveAgents()
27
- this.contextManager.set(
28
- "pipelineAgents",
29
- agents.map((a) => `${a.id}: ${a.name}`).join("\n")
30
- )
31
-
32
- let stages = buildStages(agents)
33
-
34
- const getModel = (agent: AgentDefinition): string | undefined => {
35
- return agent.model ?? agent.models?.[agent.runtime ?? "claude"]
36
- }
37
-
38
- const stageInfos: StageInfo[] = stages.map((stage, i) => ({
39
- stageIndex: i,
40
- agents: stage.map((a) => ({ id: a.id, name: a.name, icon: a.icon, model: getModel(a), taskTitle: a.taskTitle })),
41
- }))
42
-
43
- this.eventBus.emit({
44
- type: "orchestration:start",
45
- timestamp: new Date().toISOString(),
46
- payload: { name: this.config.name, requirement: this.config.context?.requirement, totalAgents: agents.length, stages: stageInfos },
47
- })
48
-
49
- const results: AgentResult[] = []
50
- let allSuccess = true
51
-
52
- for (let i = 0; i < stages.length; i++) {
53
- const stage = stages[i]
54
-
55
- this.eventBus.emit({
56
- type: "stage:start", timestamp: new Date().toISOString(),
57
- payload: { stageIndex: i, agentIds: stage.map((a) => a.id) },
58
- })
59
-
60
- const stageResults = await this.runStage(stage)
61
- results.push(...stageResults)
62
-
63
- // After architect stage completes, apply task plan and rebuild stages
64
- if (i === 0 && stage.some((a) => a.id === "architect") && stageResults.some((r) => r.status === "success")) {
65
- await this.applyTaskPlan(agents)
66
- stages = buildStages(agents)
67
- }
68
-
69
- for (const r of stageResults) {
70
- if (r.status !== "success") {
71
- allSuccess = false
72
- if (!this.config.continueOnError) break
73
- }
74
- }
75
-
76
- this.eventBus.emit({
77
- type: "stage:complete", timestamp: new Date().toISOString(),
78
- payload: { stageIndex: i },
79
- })
80
-
81
- if (!allSuccess && !this.config.continueOnError) {
82
- for (const remainingStage of stages.slice(i + 1)) {
83
- for (const agent of remainingStage) {
84
- this.eventBus.emit({
85
- type: "agent:skipped",
86
- timestamp: new Date().toISOString(),
87
- payload: { agentId: agent.id, agentName: agent.name, icon: agent.icon },
88
- })
89
- }
90
- }
91
- break
92
- }
93
- }
94
-
95
- const endTime = new Date()
96
- const result: OrchestratorResult = {
97
- name: this.config.name, success: allSuccess,
98
- totalDuration: endTime.getTime() - startTime.getTime(),
99
- agentResults: results,
100
- startTime: startTime.toISOString(), endTime: endTime.toISOString(),
101
- }
102
-
103
- this.eventBus.emit({
104
- type: "orchestration:complete", timestamp: new Date().toISOString(), payload: result,
105
- })
106
- return result
107
- }
108
-
109
- private async resolveAgents(): Promise<AgentDefinition[]> {
110
- if (!this.config.leader || this.config.useLeader === false) return this.config.agents
111
-
112
- this.contextManager.set(
113
- "availableAgents",
114
- this.config.agents.map((a) => `${a.id}: ${a.name}`).join("\n")
115
- )
116
-
117
- const result = await this.runner.run(this.config.leader)
118
- if (result.status !== "success") return this.config.agents
119
-
120
- const match = result.output.match(/\{[\s\S]*?"run"\s*:\s*\[[\s\S]*?\]\s*\}/)
121
- if (!match) return this.config.agents
122
-
123
- let plan: { run: string[] }
124
- try { plan = JSON.parse(match[0]) } catch { return this.config.agents }
125
-
126
- const runSet = new Set(plan.run)
127
- const skippedIds = new Set(
128
- this.config.agents.filter((a) => !runSet.has(a.id)).map((a) => a.id)
129
- )
130
-
131
- for (const agent of this.config.agents.filter((a) => skippedIds.has(a.id))) {
132
- this.eventBus.emit({
133
- type: "agent:skipped",
134
- timestamp: new Date().toISOString(),
135
- payload: { agentId: agent.id, agentName: agent.name, icon: agent.icon },
136
- })
137
- }
138
-
139
- return this.config.agents
140
- .filter((a) => runSet.has(a.id))
141
- .map((a) => ({
142
- ...a,
143
- dependsOn: a.dependsOn?.filter((dep) => !skippedIds.has(dep)),
144
- }))
145
- }
146
-
147
- private async runStage(agents: AgentDefinition[]): Promise<AgentResult[]> {
148
- const { maxConcurrency } = this.config
149
- if (!maxConcurrency || agents.length <= maxConcurrency) {
150
- return Promise.all(agents.map((a) => this.runner.run(a)))
151
- }
152
- const results: AgentResult[] = []
153
- for (let i = 0; i < agents.length; i += maxConcurrency) {
154
- const batch = agents.slice(i, i + maxConcurrency)
155
- results.push(...(await Promise.all(batch.map((a) => this.runner.run(a)))))
156
- }
157
- return results
158
- }
159
-
160
- private async applyTaskPlan(agents: AgentDefinition[]): Promise<void> {
161
- try {
162
- const taskPlanPath = path.join(this.workDir, "task-plan.json")
163
- if (!fs.existsSync(taskPlanPath)) return
164
-
165
- const content = fs.readFileSync(taskPlanPath, "utf-8")
166
- const taskPlan: TaskPlan = JSON.parse(content)
167
-
168
- const taskMap = new Map(taskPlan.tasks.map((t) => [t.id, t]))
169
- for (const agent of agents) {
170
- const task = taskMap.get(agent.id)
171
- if (task) {
172
- agent.taskTitle = task.title
173
- agent.dependsOn = task.dependsOn
174
- }
175
- }
176
- } catch {
177
- // Silently ignore parsing errors
178
- }
179
- }
180
- }
@@ -1,30 +0,0 @@
1
- import type { AgentDefinition } from "../types/agent"
2
-
3
- export function buildStages(agents: AgentDefinition[]): AgentDefinition[][] {
4
- const stages: AgentDefinition[][] = []
5
- const completed = new Set<string>()
6
- let remaining = [...agents]
7
-
8
- const allIds = new Set(agents.map((a) => a.id))
9
- for (const a of agents) {
10
- for (const dep of a.dependsOn ?? []) {
11
- if (!allIds.has(dep)) {
12
- throw new Error(`Agent "${a.id}" depends on "${dep}" which does not exist`)
13
- }
14
- }
15
- }
16
-
17
- while (remaining.length > 0) {
18
- const stage = remaining.filter((a) =>
19
- (a.dependsOn ?? []).every((dep) => completed.has(dep))
20
- )
21
- if (stage.length === 0) {
22
- const ids = remaining.map((a) => a.id).join(", ")
23
- throw new Error(`Circular or unsatisfiable dependency detected: ${ids}`)
24
- }
25
- stages.push(stage)
26
- stage.forEach((a) => completed.add(a.id))
27
- remaining = remaining.filter((a) => !stage.includes(a))
28
- }
29
- return stages
30
- }
@@ -1,41 +0,0 @@
1
- import { spawn, execSync } from "child_process"
2
- import { RuntimeBase } from "./RuntimeBase"
3
- import type { AgentTask, AgentOutput } from "../types/runtime"
4
-
5
- export class ClaudeRuntime extends RuntimeBase {
6
- readonly name = "claude"
7
-
8
- async checkAvailable(): Promise<boolean> {
9
- try { execSync("claude --version", { stdio: "ignore" }); return true }
10
- catch { return false }
11
- }
12
-
13
- async execute(task: AgentTask): Promise<AgentOutput> {
14
- const args = [
15
- "-p", task.prompt,
16
- "--allowedTools", task.tools.join(","),
17
- ]
18
- if (task.systemPrompt) args.push("--system-prompt", task.systemPrompt)
19
- if (task.model) args.push("--model", task.model)
20
-
21
- return new Promise((resolve, reject) => {
22
- const child = spawn("claude", args, { cwd: task.workDir, env: process.env })
23
- task.signal?.addEventListener("abort", () => child.kill())
24
-
25
- let output = ""
26
- let errorOutput = ""
27
- child.stdout.on("data", (d: Buffer) => {
28
- const chunk = d.toString(); output += chunk; task.onOutput?.(chunk)
29
- })
30
- child.stderr.on("data", (d: Buffer) => { errorOutput += d.toString() })
31
- child.on("close", (code) => {
32
- if (task.signal?.aborted) return
33
- code === 0
34
- ? resolve({ output, exitCode: code })
35
- : reject(new Error(`claude exited with code ${code}: ${errorOutput}`))
36
- })
37
- child.on("error", (err) => reject(new Error(`Failed to start claude: ${err.message}`)))
38
- })
39
- }
40
- }
41
-
@@ -1,7 +0,0 @@
1
- import type { AgentRuntime, AgentTask, AgentOutput } from "../types/runtime"
2
-
3
- export abstract class RuntimeBase implements AgentRuntime {
4
- abstract readonly name: string
5
- abstract checkAvailable(): Promise<boolean>
6
- abstract execute(task: AgentTask): Promise<AgentOutput>
7
- }
@@ -1,20 +0,0 @@
1
- import type { AgentRuntime } from "../types/runtime"
2
- import { ClaudeRuntime } from "./ClaudeRuntime"
3
-
4
- class RuntimeRegistry {
5
- private runtimes = new Map<string, AgentRuntime>()
6
- constructor() {
7
- this.register(new ClaudeRuntime())
8
- }
9
- register(r: AgentRuntime) { this.runtimes.set(r.name, r) }
10
- get(name: string): AgentRuntime {
11
- const r = this.runtimes.get(name)
12
- if (!r) throw new Error(`Runtime not found: "${name}". Available: ${[...this.runtimes.keys()].join(", ")}`)
13
- return r
14
- }
15
- has(name: string) { return this.runtimes.has(name) }
16
- list(): string[] { return [...this.runtimes.keys()] }
17
- }
18
-
19
- export const runtimeRegistry = new RuntimeRegistry()
20
- export { ClaudeRuntime }
@@ -1,48 +0,0 @@
1
- export type ToolName =
2
- | "Read" | "Write" | "Edit" | "MultiEdit"
3
- | "Bash" | "Glob" | "Grep" | "WebFetch"
4
-
5
- export interface AgentDefinition {
6
- id: string
7
- name: string
8
- icon?: string
9
- runtime?: string
10
- model?: string
11
- models?: Partial<Record<string, string>>
12
- systemPrompt: string
13
- taskPrompt: string
14
- taskTitle?: string
15
- tools?: ToolName[]
16
- dependsOn?: string[]
17
- timeout?: number
18
- retries?: number
19
- workDir?: string
20
- }
21
-
22
- export type AgentStatus =
23
- | "pending" | "running" | "retrying" | "success" | "failed" | "skipped"
24
-
25
- export interface AgentResult {
26
- agentId: string
27
- agentName: string
28
- status: AgentStatus
29
- output: string
30
- duration: number
31
- attempts: number
32
- error?: string
33
- startTime: string
34
- endTime: string
35
- }
36
-
37
- export interface SubTask {
38
- id: string // matches AgentDefinition.id
39
- title: string
40
- scope: string
41
- dependsOn?: string[]
42
- }
43
-
44
- export interface TaskPlan {
45
- projectName: string
46
- requirement: string
47
- tasks: SubTask[]
48
- }
@@ -1,26 +0,0 @@
1
- import type { AgentDefinition } from "./agent"
2
-
3
- export interface MultiClawConfig {
4
- name: string
5
- workDir: string
6
- leader?: AgentDefinition
7
- agents: AgentDefinition[]
8
- context?: Record<string, string>
9
- logDir?: string
10
- continueOnError?: boolean
11
- maxConcurrency?: number
12
- useLeader?: boolean
13
- dashboard?: {
14
- port?: number
15
- autoOpen?: boolean
16
- }
17
- }
18
-
19
- export interface OrchestratorResult {
20
- name: string
21
- success: boolean
22
- totalDuration: number
23
- agentResults: import("./agent").AgentResult[]
24
- startTime: string
25
- endTime: string
26
- }
@@ -1,21 +0,0 @@
1
- import type { AgentResult, OrchestratorResult } from "./index"
2
-
3
- export interface StageInfo {
4
- stageIndex: number
5
- agents: Array<{ id: string; name: string; icon?: string; model?: string; taskTitle?: string }>
6
- }
7
-
8
- export type MultiClawEvent =
9
- | { type: "orchestration:start"; timestamp: string; payload: { name: string; requirement?: string; totalAgents: number; stages: StageInfo[] } }
10
- | { type: "orchestration:complete"; timestamp: string; payload: OrchestratorResult }
11
- | { type: "stage:start"; timestamp: string; payload: { stageIndex: number; agentIds: string[] } }
12
- | { type: "stage:complete"; timestamp: string; payload: { stageIndex: number } }
13
- | { type: "agent:start"; timestamp: string; payload: { agentId: string; agentName: string; icon?: string; model?: string; taskTitle?: string } }
14
- | { type: "agent:output"; timestamp: string; payload: { agentId: string; chunk: string } }
15
- | { type: "agent:complete"; timestamp: string; payload: AgentResult }
16
- | { type: "agent:failed"; timestamp: string; payload: AgentResult }
17
- | { type: "agent:skipped"; timestamp: string; payload: { agentId: string; agentName: string; icon?: string } }
18
- | { type: "agent:retrying"; timestamp: string; payload: { agentId: string; agentName: string; attempt: number; maxAttempts: number; error: string } }
19
-
20
- export type EventListener = (event: MultiClawEvent) => void
21
-
@@ -1,4 +0,0 @@
1
- export * from "./agent"
2
- export * from "./runtime"
3
- export * from "./config"
4
- export * from "./event"
@@ -1,23 +0,0 @@
1
- export interface AgentTask {
2
- agentId: string
3
- systemPrompt: string
4
- prompt: string
5
- tools: string[]
6
- workDir: string
7
- timeout: number
8
- signal?: AbortSignal
9
- onOutput?: (chunk: string) => void
10
- model?: string
11
- }
12
-
13
- export interface AgentOutput {
14
- output: string
15
- exitCode: number
16
- }
17
-
18
- export interface AgentRuntime {
19
- readonly name: string
20
- checkAvailable(): Promise<boolean>
21
- execute(task: AgentTask): Promise<AgentOutput>
22
- }
23
-
@@ -1,24 +0,0 @@
1
- import * as fs from "fs/promises"
2
- import * as path from "path"
3
-
4
- export async function renderPrompt(
5
- template: string,
6
- context: Record<string, string>,
7
- workDir: string
8
- ): Promise<string> {
9
- let result = template
10
- const fileMatches = [...template.matchAll(/\{\{file:([^}]+)\}\}/g)]
11
- for (const match of fileMatches) {
12
- const filePath = path.resolve(workDir, match[1].trim())
13
- try {
14
- const content = await fs.readFile(filePath, "utf-8")
15
- result = result.replace(match[0], content)
16
- } catch {
17
- result = result.replace(match[0], `[file not found: ${match[1]}]`)
18
- }
19
- }
20
- for (const [key, value] of Object.entries(context)) {
21
- result = result.replaceAll(`{{${key}}}`, value)
22
- }
23
- return result
24
- }
@@ -1,24 +0,0 @@
1
- export function sleep(ms: number): Promise<void> {
2
- return new Promise((r) => setTimeout(r, ms))
3
- }
4
-
5
- export async function withRetry<T>(
6
- fn: (attempt: number) => Promise<T>,
7
- retries: number,
8
- onRetry?: (failedAttempt: number, error: unknown) => void,
9
- delayMs = 2000
10
- ): Promise<T> {
11
- let lastError: Error = new Error("Unknown error")
12
- for (let attempt = 1; attempt <= retries + 1; attempt++) {
13
- try { return await fn(attempt) }
14
- catch (err) {
15
- lastError = err as Error
16
- if (attempt <= retries) {
17
- onRetry?.(attempt, err)
18
- await sleep(delayMs * attempt)
19
- }
20
- }
21
- }
22
- throw lastError
23
- }
24
-