@john-ezra/openralph 0.1.1
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/.dockerignore +25 -0
- package/AGENTS.md +148 -0
- package/CHANGELOG.md +16 -0
- package/LICENSE +21 -0
- package/PROMPT_build.md +48 -0
- package/PROMPT_plan.md +29 -0
- package/README.md +247 -0
- package/bin/openralph +5 -0
- package/bun.lock +85 -0
- package/container/Dockerfile +58 -0
- package/container/bin/chrome-devtools-mcp-wrapper +57 -0
- package/package.json +50 -0
- package/src/args.ts +236 -0
- package/src/artifacts.ts +183 -0
- package/src/cli.ts +125 -0
- package/src/design.ts +24 -0
- package/src/docker.ts +403 -0
- package/src/exec.ts +103 -0
- package/src/git.ts +62 -0
- package/src/launcher.ts +235 -0
- package/src/loop.ts +339 -0
- package/src/plugin.ts +56 -0
- package/src/release-check.ts +143 -0
- package/src/sentinels.ts +23 -0
- package/src/tags.ts +22 -0
- package/src/trust.ts +116 -0
- package/src/tui.ts +436 -0
- package/tsconfig.json +14 -0
package/src/tui.ts
ADDED
|
@@ -0,0 +1,436 @@
|
|
|
1
|
+
import type { TuiDialogStack, TuiPlugin, TuiPluginModule } from "@opencode-ai/plugin/tui"
|
|
2
|
+
import { validateOptions } from "./args.ts"
|
|
3
|
+
import { buildDesignUserPrompt, DESIGN_SYSTEM_PROMPT } from "./design.ts"
|
|
4
|
+
import type { CommandOutputEvent } from "./exec.ts"
|
|
5
|
+
import { runOpenRalphLauncher, type RunLauncherInput } from "./launcher.ts"
|
|
6
|
+
|
|
7
|
+
const MAX_OUTPUT_CHARS = 40_000
|
|
8
|
+
const OUTPUT_DIALOG_LINES = 36
|
|
9
|
+
const OUTPUT_DIALOG_REFRESH_MS = 1000
|
|
10
|
+
const MAX_DIALOG_LINE_LENGTH = 220
|
|
11
|
+
|
|
12
|
+
interface TuiRunState {
|
|
13
|
+
controller: AbortController
|
|
14
|
+
phase: "plan" | "build"
|
|
15
|
+
rawArgs: string
|
|
16
|
+
startedAt: number
|
|
17
|
+
status: string
|
|
18
|
+
output: string
|
|
19
|
+
summary?: string
|
|
20
|
+
dialog?: TuiDialogStack
|
|
21
|
+
refreshTimer?: ReturnType<typeof setTimeout>
|
|
22
|
+
suppressDialogClose?: boolean
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
type RalphMode = "design" | "plan" | "build"
|
|
26
|
+
type RunLauncher = typeof runOpenRalphLauncher
|
|
27
|
+
|
|
28
|
+
export function createTuiModule(runLauncher: RunLauncher = runOpenRalphLauncher): TuiPluginModule {
|
|
29
|
+
const tui = (async (api, rawOptions, _meta) => {
|
|
30
|
+
const options = validateOptions(rawOptions)
|
|
31
|
+
let active: TuiRunState | undefined
|
|
32
|
+
let lastRun: TuiRunState | undefined
|
|
33
|
+
|
|
34
|
+
const unregister = api.keymap.registerLayer({
|
|
35
|
+
commands: [
|
|
36
|
+
{
|
|
37
|
+
namespace: "palette",
|
|
38
|
+
name: "openralph",
|
|
39
|
+
title: "OpenRalph: Choose Phase",
|
|
40
|
+
category: "OpenRalph",
|
|
41
|
+
slashName: "ralph",
|
|
42
|
+
run: () => showModeSelect(api, undefined, options, runLauncher, () => active, (next) => {
|
|
43
|
+
active = next
|
|
44
|
+
}, (next) => {
|
|
45
|
+
lastRun = next
|
|
46
|
+
}),
|
|
47
|
+
},
|
|
48
|
+
],
|
|
49
|
+
bindings: [],
|
|
50
|
+
})
|
|
51
|
+
|
|
52
|
+
api.lifecycle.onDispose(() => {
|
|
53
|
+
active?.controller.abort()
|
|
54
|
+
if (active) clearRefreshTimer(active)
|
|
55
|
+
if (lastRun && lastRun !== active) clearRefreshTimer(lastRun)
|
|
56
|
+
unregister()
|
|
57
|
+
})
|
|
58
|
+
}) satisfies TuiPlugin
|
|
59
|
+
|
|
60
|
+
return { id: "openralph", tui }
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export default createTuiModule()
|
|
64
|
+
|
|
65
|
+
function showModeSelect(
|
|
66
|
+
api: Parameters<TuiPlugin>[0],
|
|
67
|
+
dialog: TuiDialogStack | undefined,
|
|
68
|
+
options: ReturnType<typeof validateOptions>,
|
|
69
|
+
runLauncher: RunLauncher,
|
|
70
|
+
getActive: () => TuiRunState | undefined,
|
|
71
|
+
setActive: (run: TuiRunState | undefined) => void,
|
|
72
|
+
setLastRun: (run: TuiRunState) => void,
|
|
73
|
+
): void {
|
|
74
|
+
const stack = dialog ?? api.ui.dialog
|
|
75
|
+
stack.replace(() =>
|
|
76
|
+
api.ui.DialogSelect<RalphMode>({
|
|
77
|
+
title: "OpenRalph",
|
|
78
|
+
placeholder: "Select a Ralph phase",
|
|
79
|
+
options: [
|
|
80
|
+
{
|
|
81
|
+
title: "Design",
|
|
82
|
+
value: "design",
|
|
83
|
+
description: "Ideate and write planning-ready specs",
|
|
84
|
+
},
|
|
85
|
+
{
|
|
86
|
+
title: "Plan",
|
|
87
|
+
value: "plan",
|
|
88
|
+
description: "Create or refine IMPLEMENTATION_PLAN.md from specs",
|
|
89
|
+
},
|
|
90
|
+
{
|
|
91
|
+
title: "Build",
|
|
92
|
+
value: "build",
|
|
93
|
+
description: "Implement planned work one task and commit at a time",
|
|
94
|
+
},
|
|
95
|
+
],
|
|
96
|
+
onSelect: (option) => {
|
|
97
|
+
stack.clear()
|
|
98
|
+
if (option.value === "design") {
|
|
99
|
+
promptForDesign(api, stack, options, getActive)
|
|
100
|
+
return
|
|
101
|
+
}
|
|
102
|
+
promptForArgs(api, stack, option.value, options, runLauncher, getActive, setActive, setLastRun)
|
|
103
|
+
},
|
|
104
|
+
}),
|
|
105
|
+
)
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function promptForDesign(
|
|
109
|
+
api: Parameters<TuiPlugin>[0],
|
|
110
|
+
dialog: TuiDialogStack | undefined,
|
|
111
|
+
options: ReturnType<typeof validateOptions>,
|
|
112
|
+
getActive: () => TuiRunState | undefined,
|
|
113
|
+
): void {
|
|
114
|
+
if (getActive()) {
|
|
115
|
+
api.ui.toast({ variant: "warning", title: "OpenRalph", message: "An OpenRalph run is already active." })
|
|
116
|
+
return
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
const stack = dialog ?? api.ui.dialog
|
|
120
|
+
stack.replace(() =>
|
|
121
|
+
api.ui.DialogPrompt({
|
|
122
|
+
title: "OpenRalph: Design",
|
|
123
|
+
placeholder: "Feature, workflow, bug, or product change (optional)",
|
|
124
|
+
onConfirm: (value) => {
|
|
125
|
+
stack.clear()
|
|
126
|
+
void startDesignTurn(api, value, options)
|
|
127
|
+
},
|
|
128
|
+
onCancel: () => stack.clear(),
|
|
129
|
+
}),
|
|
130
|
+
)
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
async function startDesignTurn(
|
|
134
|
+
api: Parameters<TuiPlugin>[0],
|
|
135
|
+
initialIdea: string,
|
|
136
|
+
options: ReturnType<typeof validateOptions>,
|
|
137
|
+
): Promise<void> {
|
|
138
|
+
try {
|
|
139
|
+
const sessionID = await ensureDesignSession(api)
|
|
140
|
+
await api.client.session.prompt(
|
|
141
|
+
{
|
|
142
|
+
sessionID,
|
|
143
|
+
directory: projectDirectory(api),
|
|
144
|
+
system: DESIGN_SYSTEM_PROMPT,
|
|
145
|
+
parts: [{ type: "text", text: buildDesignUserPrompt(initialIdea) }],
|
|
146
|
+
...(options.defineModel ? { model: parseProviderModel(options.defineModel) } : {}),
|
|
147
|
+
},
|
|
148
|
+
{ throwOnError: true },
|
|
149
|
+
)
|
|
150
|
+
} catch (error) {
|
|
151
|
+
api.ui.toast({
|
|
152
|
+
variant: "error",
|
|
153
|
+
title: "OpenRalph Design failed",
|
|
154
|
+
message: trimMessage(formatError(error)),
|
|
155
|
+
duration: 12000,
|
|
156
|
+
})
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
async function ensureDesignSession(api: Parameters<TuiPlugin>[0]): Promise<string> {
|
|
161
|
+
const sessionID = currentSessionID(api)
|
|
162
|
+
if (sessionID) return sessionID
|
|
163
|
+
|
|
164
|
+
const result = await api.client.session.create(
|
|
165
|
+
{
|
|
166
|
+
directory: projectDirectory(api),
|
|
167
|
+
title: "Ralph Design",
|
|
168
|
+
metadata: { openralph: "design" },
|
|
169
|
+
},
|
|
170
|
+
{ throwOnError: true },
|
|
171
|
+
)
|
|
172
|
+
api.route.navigate("session", { sessionID: result.data.id })
|
|
173
|
+
return result.data.id
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
function currentSessionID(api: Parameters<TuiPlugin>[0]): string | undefined {
|
|
177
|
+
const route = api.route.current
|
|
178
|
+
if (route.name !== "session") return undefined
|
|
179
|
+
const sessionID = route.params?.sessionID
|
|
180
|
+
return typeof sessionID === "string" ? sessionID : undefined
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
function projectDirectory(api: Parameters<TuiPlugin>[0]): string {
|
|
184
|
+
return api.state.path.worktree || api.state.path.directory
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
function parseProviderModel(model: string): { providerID: string; modelID: string } {
|
|
188
|
+
const separator = model.indexOf("/")
|
|
189
|
+
if (separator <= 0 || separator === model.length - 1) {
|
|
190
|
+
throw new Error("defineModel must use provider/model format for OpenRalph Design")
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
return {
|
|
194
|
+
providerID: model.slice(0, separator),
|
|
195
|
+
modelID: model.slice(separator + 1),
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
function promptForArgs(
|
|
200
|
+
api: Parameters<TuiPlugin>[0],
|
|
201
|
+
dialog: TuiDialogStack | undefined,
|
|
202
|
+
phase: "plan" | "build",
|
|
203
|
+
options: ReturnType<typeof validateOptions>,
|
|
204
|
+
runLauncher: RunLauncher,
|
|
205
|
+
getActive: () => TuiRunState | undefined,
|
|
206
|
+
setActive: (run: TuiRunState | undefined) => void,
|
|
207
|
+
setLastRun: (run: TuiRunState) => void,
|
|
208
|
+
): void {
|
|
209
|
+
const stack = dialog ?? api.ui.dialog
|
|
210
|
+
stack.replace(() =>
|
|
211
|
+
api.ui.DialogPrompt({
|
|
212
|
+
title: phase === "plan" ? "OpenRalph: Plan" : "OpenRalph: Build",
|
|
213
|
+
placeholder: argsPromptPlaceholder(phase),
|
|
214
|
+
onConfirm: (value) => {
|
|
215
|
+
stack.clear()
|
|
216
|
+
setTimeout(() => {
|
|
217
|
+
void startTuiRun(api, phase, value, options, runLauncher, getActive, setActive, setLastRun)
|
|
218
|
+
}, 0)
|
|
219
|
+
},
|
|
220
|
+
onCancel: () => stack.clear(),
|
|
221
|
+
}),
|
|
222
|
+
)
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
function argsPromptPlaceholder(phase: "plan" | "build"): string {
|
|
226
|
+
return phase === "plan"
|
|
227
|
+
? "[number] [--model provider/model] [--no-docker]"
|
|
228
|
+
: "[number] [--model provider/model] [--push] [--no-docker]"
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
async function startTuiRun(
|
|
232
|
+
api: Parameters<TuiPlugin>[0],
|
|
233
|
+
phase: "plan" | "build",
|
|
234
|
+
rawArgs: string,
|
|
235
|
+
options: ReturnType<typeof validateOptions>,
|
|
236
|
+
runLauncher: RunLauncher,
|
|
237
|
+
getActive: () => TuiRunState | undefined,
|
|
238
|
+
setActive: (run: TuiRunState | undefined) => void,
|
|
239
|
+
setLastRun: (run: TuiRunState) => void,
|
|
240
|
+
): Promise<void> {
|
|
241
|
+
if (getActive()) {
|
|
242
|
+
api.ui.toast({ variant: "warning", title: "OpenRalph", message: "An OpenRalph run is already active." })
|
|
243
|
+
return
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
const controller = new AbortController()
|
|
247
|
+
const run: TuiRunState = {
|
|
248
|
+
controller,
|
|
249
|
+
phase,
|
|
250
|
+
rawArgs,
|
|
251
|
+
startedAt: Date.now(),
|
|
252
|
+
status: "starting",
|
|
253
|
+
output: "",
|
|
254
|
+
}
|
|
255
|
+
setActive(run)
|
|
256
|
+
setLastRun(run)
|
|
257
|
+
showOutputDialog(api, undefined, run)
|
|
258
|
+
|
|
259
|
+
try {
|
|
260
|
+
const input: RunLauncherInput = {
|
|
261
|
+
phase,
|
|
262
|
+
rawArgs,
|
|
263
|
+
cwd: api.state.path.worktree || api.state.path.directory,
|
|
264
|
+
options,
|
|
265
|
+
streamOutput: false,
|
|
266
|
+
captureOutput: true,
|
|
267
|
+
onOutput: (event) => recordOutput(api, run, event),
|
|
268
|
+
signal: controller.signal,
|
|
269
|
+
}
|
|
270
|
+
const result = await runLauncher(input)
|
|
271
|
+
run.status = result.status
|
|
272
|
+
run.summary = result.summary
|
|
273
|
+
refreshOutputDialog(api, run)
|
|
274
|
+
} catch (error) {
|
|
275
|
+
const stopped = controller.signal.aborted
|
|
276
|
+
run.status = stopped ? "stopped" : "failed"
|
|
277
|
+
run.summary = formatError(error)
|
|
278
|
+
refreshOutputDialog(api, run)
|
|
279
|
+
} finally {
|
|
280
|
+
clearRefreshTimer(run)
|
|
281
|
+
if (getActive() === run) setActive(undefined)
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
function recordOutput(api: Parameters<TuiPlugin>[0], run: TuiRunState, event: CommandOutputEvent): void {
|
|
286
|
+
if (run.status === "starting") run.status = "running"
|
|
287
|
+
run.output += event.chunk
|
|
288
|
+
if (run.output.length > MAX_OUTPUT_CHARS) run.output = run.output.slice(-MAX_OUTPUT_CHARS)
|
|
289
|
+
scheduleOutputDialogRefresh(api, run)
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
function showOutputDialog(api: Parameters<TuiPlugin>[0], dialog: TuiDialogStack | undefined, run: TuiRunState): void {
|
|
293
|
+
const stack = dialog ?? api.ui.dialog
|
|
294
|
+
run.dialog = stack
|
|
295
|
+
refreshOutputDialog(api, run)
|
|
296
|
+
scheduleOutputDialogRefresh(api, run)
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
function scheduleOutputDialogRefresh(api: Parameters<TuiPlugin>[0], run: TuiRunState): void {
|
|
300
|
+
if (!run.dialog || run.refreshTimer || !isActiveRunStatus(run.status)) return
|
|
301
|
+
run.refreshTimer = setTimeout(() => {
|
|
302
|
+
run.refreshTimer = undefined
|
|
303
|
+
refreshOutputDialog(api, run)
|
|
304
|
+
scheduleOutputDialogRefresh(api, run)
|
|
305
|
+
}, OUTPUT_DIALOG_REFRESH_MS)
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
function refreshOutputDialog(api: Parameters<TuiPlugin>[0], run: TuiRunState): void {
|
|
309
|
+
if (!run.dialog) return
|
|
310
|
+
const stack = run.dialog
|
|
311
|
+
const closeViewer = () => {
|
|
312
|
+
closeOutputDialog(run, stack)
|
|
313
|
+
stack.clear()
|
|
314
|
+
}
|
|
315
|
+
run.suppressDialogClose = true
|
|
316
|
+
const onClose = () => {
|
|
317
|
+
if (run.suppressDialogClose) return
|
|
318
|
+
closeOutputDialog(run, stack)
|
|
319
|
+
}
|
|
320
|
+
if (canStopRun(run)) {
|
|
321
|
+
stack.replace(
|
|
322
|
+
() =>
|
|
323
|
+
api.ui.DialogConfirm({
|
|
324
|
+
title: `OpenRalph ${run.phase} output`,
|
|
325
|
+
message: `${formatOutputDialogMessage(run)}\n\nConfirm closes this viewer. Cancel stops the active Ralph loop.`,
|
|
326
|
+
onConfirm: closeViewer,
|
|
327
|
+
onCancel: () => stopFromOutputDialog(api, run, stack),
|
|
328
|
+
}),
|
|
329
|
+
onClose,
|
|
330
|
+
)
|
|
331
|
+
} else {
|
|
332
|
+
stack.replace(
|
|
333
|
+
() =>
|
|
334
|
+
api.ui.DialogAlert({
|
|
335
|
+
title: `OpenRalph ${run.phase} output`,
|
|
336
|
+
message: formatOutputDialogMessage(run),
|
|
337
|
+
onConfirm: closeViewer,
|
|
338
|
+
}),
|
|
339
|
+
onClose,
|
|
340
|
+
)
|
|
341
|
+
}
|
|
342
|
+
queueMicrotask(() => {
|
|
343
|
+
if (run.dialog === stack) run.suppressDialogClose = false
|
|
344
|
+
})
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
function stopFromOutputDialog(api: Parameters<TuiPlugin>[0], run: TuiRunState, stack: TuiDialogStack): void {
|
|
348
|
+
requestStopRun(api, run, { refresh: false })
|
|
349
|
+
closeOutputDialog(run, stack)
|
|
350
|
+
stack.clear()
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
function requestStopRun(
|
|
354
|
+
api: Parameters<TuiPlugin>[0],
|
|
355
|
+
run: TuiRunState,
|
|
356
|
+
options: { refresh: boolean },
|
|
357
|
+
): void {
|
|
358
|
+
if (!run.controller.signal.aborted) {
|
|
359
|
+
run.status = "stop requested"
|
|
360
|
+
run.controller.abort()
|
|
361
|
+
}
|
|
362
|
+
if (options.refresh) refreshOutputDialog(api, run)
|
|
363
|
+
api.ui.toast({ variant: "warning", title: "OpenRalph", message: "Stop requested for the active run." })
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
function closeOutputDialog(run: TuiRunState, stack: TuiDialogStack): void {
|
|
367
|
+
if (run.dialog === stack) run.dialog = undefined
|
|
368
|
+
run.suppressDialogClose = false
|
|
369
|
+
clearRefreshTimer(run)
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
function clearRefreshTimer(run: TuiRunState): void {
|
|
373
|
+
if (!run.refreshTimer) return
|
|
374
|
+
clearTimeout(run.refreshTimer)
|
|
375
|
+
run.refreshTimer = undefined
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
function formatOutputDialogMessage(run: TuiRunState): string {
|
|
379
|
+
const lines = [
|
|
380
|
+
`Status: ${run.status}`,
|
|
381
|
+
`Args: ${run.rawArgs.trim() || "(default)"}`,
|
|
382
|
+
`Elapsed: ${formatElapsed(Date.now() - run.startedAt)}`,
|
|
383
|
+
"",
|
|
384
|
+
"Recent output:",
|
|
385
|
+
recentOutput(run),
|
|
386
|
+
]
|
|
387
|
+
|
|
388
|
+
if (run.summary) {
|
|
389
|
+
lines.push("", "Summary:", trimMessage(run.summary))
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
return lines.join("\n")
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
function recentOutput(run: TuiRunState): string {
|
|
396
|
+
const output = sanitizeOutput(run.output).trimEnd()
|
|
397
|
+
if (!output) return "Waiting for Docker/opencode output..."
|
|
398
|
+
|
|
399
|
+
return output
|
|
400
|
+
.split(/\n/)
|
|
401
|
+
.slice(-OUTPUT_DIALOG_LINES)
|
|
402
|
+
.map(trimDialogLine)
|
|
403
|
+
.join("\n")
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
function sanitizeOutput(value: string): string {
|
|
407
|
+
return value.replace(/\x1B(?:[@-Z\\-_]|\[[0-?]*[ -/]*[@-~])/g, "").replace(/\r/g, "\n")
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
function trimDialogLine(value: string): string {
|
|
411
|
+
if (value.length <= MAX_DIALOG_LINE_LENGTH) return value
|
|
412
|
+
return `${value.slice(0, MAX_DIALOG_LINE_LENGTH - 3)}...`
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
function formatElapsed(milliseconds: number): string {
|
|
416
|
+
const seconds = Math.max(0, Math.floor(milliseconds / 1000))
|
|
417
|
+
if (seconds < 60) return `${seconds}s`
|
|
418
|
+
return `${Math.floor(seconds / 60)}m ${seconds % 60}s`
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
function isActiveRunStatus(status: string): boolean {
|
|
422
|
+
return status === "starting" || status === "running" || status === "stop requested"
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
function canStopRun(run: TuiRunState): boolean {
|
|
426
|
+
return (run.status === "starting" || run.status === "running") && !run.controller.signal.aborted
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
function trimMessage(value: string): string {
|
|
430
|
+
const lines = value.trim().split(/\r?\n/)
|
|
431
|
+
return lines.slice(Math.max(0, lines.length - 8)).join("\n")
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
function formatError(error: unknown): string {
|
|
435
|
+
return error instanceof Error ? error.message : String(error)
|
|
436
|
+
}
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2022",
|
|
4
|
+
"lib": ["ES2022"],
|
|
5
|
+
"module": "ESNext",
|
|
6
|
+
"moduleResolution": "Bundler",
|
|
7
|
+
"types": ["bun"],
|
|
8
|
+
"strict": true,
|
|
9
|
+
"noEmit": true,
|
|
10
|
+
"skipLibCheck": true,
|
|
11
|
+
"allowImportingTsExtensions": true
|
|
12
|
+
},
|
|
13
|
+
"include": ["src/**/*.ts", "tests/**/*.ts"]
|
|
14
|
+
}
|