@boardwalk-labs/workflow 0.1.2 → 0.1.4
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 +2 -2
- package/dist/extract.js +36 -0
- package/dist/host.d.ts +1 -1
- package/dist/index.d.ts +17 -10
- package/dist/index.js +4 -9
- package/dist/manifest.d.ts +0 -8
- package/dist/manifest.js +7 -11
- package/dist/meta.d.ts +11 -15
- package/package.json +3 -2
package/README.md
CHANGED
|
@@ -9,7 +9,7 @@ export const meta = {
|
|
|
9
9
|
name: "morning-digest",
|
|
10
10
|
description: "Summarize my open issues every weekday at 9am",
|
|
11
11
|
triggers: [{ kind: "cron", expr: "0 9 * * 1-5" }],
|
|
12
|
-
secrets: [{ name: "GITHUB_TOKEN" }],
|
|
12
|
+
permissions: { secrets: [{ name: "GITHUB_TOKEN" }] },
|
|
13
13
|
} satisfies WorkflowMeta;
|
|
14
14
|
|
|
15
15
|
const token = await secrets.get("GITHUB_TOKEN");
|
|
@@ -36,7 +36,7 @@ A workflow is **a script**: the `meta` export is a **pure literal** (engines der
|
|
|
36
36
|
- **`agent(prompt, opts?)`** — run an agent loop and get its final text (or `schema`-validated JSON). `model` is optional: name one explicitly, or let the engine resolve it. Loops can use **tools** (built-in or program-defined), **MCP servers**, **skills**, and **memory** — each brought **per call** on `agent()`; the manifest declares none of them.
|
|
37
37
|
- **`sleep(ms | { until })`** — durable wait; the run holds, locals survive.
|
|
38
38
|
- **`workflows.call(name, input)`** — durably invoke another workflow and await its result; idempotent across restarts. `workflows.run` is the fire-and-forget sibling.
|
|
39
|
-
- **`secrets.get(name)`** — read a secret declared in `
|
|
39
|
+
- **`secrets.get(name)`** — read a secret declared in `permissions.secrets`. Resolved from your `.env` locally, from the encrypted vault on hosted Boardwalk. Secret values never reach model context — the SDK contract requires engines to redact them.
|
|
40
40
|
- **`output(value)`** — declare the run's result.
|
|
41
41
|
- **Memory = a persistent directory, per agent.** `agent(prompt, { memory: "memory/triager" })` names any workspace-relative directory; the engine auto-persists it across runs — no declaration needed. The loop gets read/write file tools scoped to it, and your code can read and write the same files. (`workspace.persist` is the separate knob for non-memory state your program manages directly.)
|
|
42
42
|
|
package/dist/extract.js
CHANGED
|
@@ -37,6 +37,13 @@ const DEFAULT_FILE_NAME = "index.ts";
|
|
|
37
37
|
export function extractMetaLiteral(source, options = {}) {
|
|
38
38
|
const fileName = options.fileName ?? DEFAULT_FILE_NAME;
|
|
39
39
|
const sf = ts.createSourceFile(fileName, source, ts.ScriptTarget.Latest, /*setParentNodes*/ true);
|
|
40
|
+
// A syntax error otherwise falls through to "No `meta` declaration found" (the parser produced no
|
|
41
|
+
// usable `meta` node) — misleading. Report the real syntax error, with position, first.
|
|
42
|
+
const syntaxError = firstSyntaxError(sf);
|
|
43
|
+
if (syntaxError !== undefined) {
|
|
44
|
+
const text = ts.flattenDiagnosticMessageText(syntaxError.messageText, "\n");
|
|
45
|
+
throw failAt(`syntax error: ${text}`, sf, syntaxError.start);
|
|
46
|
+
}
|
|
40
47
|
const initializer = findMetaInitializer(sf);
|
|
41
48
|
if (initializer === null) {
|
|
42
49
|
throw fail("No `meta` declaration found — a workflow program must export a pure-literal " +
|
|
@@ -191,3 +198,32 @@ function fail(message, node, sf) {
|
|
|
191
198
|
}
|
|
192
199
|
return new MetaExtractionError(message);
|
|
193
200
|
}
|
|
201
|
+
/** Like {@link fail}, but positioned at a raw source offset (a diagnostic's `start`). */
|
|
202
|
+
function failAt(message, sf, start) {
|
|
203
|
+
if (start === undefined)
|
|
204
|
+
return new MetaExtractionError(message);
|
|
205
|
+
const { line, character } = sf.getLineAndCharacterOfPosition(start);
|
|
206
|
+
return new MetaExtractionError(`${sf.fileName}:${String(line + 1)}:${String(character + 1)} — ${message}`);
|
|
207
|
+
}
|
|
208
|
+
/**
|
|
209
|
+
* The first syntax error TS recorded while parsing, or undefined. The parser stores these on the
|
|
210
|
+
* source file's `parseDiagnostics` (an internal field), so read it through `Reflect` + a runtime
|
|
211
|
+
* guard rather than a cast — a future TS that drops/renames it simply yields "no error".
|
|
212
|
+
*/
|
|
213
|
+
function firstSyntaxError(sf) {
|
|
214
|
+
const raw = Reflect.get(sf, "parseDiagnostics");
|
|
215
|
+
if (!Array.isArray(raw))
|
|
216
|
+
return undefined;
|
|
217
|
+
for (const d of raw) {
|
|
218
|
+
if (isErrorDiagnostic(d))
|
|
219
|
+
return d;
|
|
220
|
+
}
|
|
221
|
+
return undefined;
|
|
222
|
+
}
|
|
223
|
+
function isErrorDiagnostic(d) {
|
|
224
|
+
return (typeof d === "object" &&
|
|
225
|
+
d !== null &&
|
|
226
|
+
"messageText" in d &&
|
|
227
|
+
"category" in d &&
|
|
228
|
+
d.category === ts.DiagnosticCategory.Error);
|
|
229
|
+
}
|
package/dist/host.d.ts
CHANGED
|
@@ -16,7 +16,7 @@ export interface WorkflowHost {
|
|
|
16
16
|
callWorkflow(slug: string, input: unknown, opts: CallOptions | undefined): Promise<unknown>;
|
|
17
17
|
/** Hold the run for the requested duration (the run stays held while it waits; locals survive). */
|
|
18
18
|
sleep(arg: SleepArg): Promise<void>;
|
|
19
|
-
/** Resolve a granted secret to its plaintext value (fail-closed against `
|
|
19
|
+
/** Resolve a granted secret to its plaintext value (fail-closed against `permissions.secrets`). */
|
|
20
20
|
getSecret(name: string): Promise<string>;
|
|
21
21
|
/**
|
|
22
22
|
* Fire-and-forget trigger of another workflow; resolve to the new run's id WITHOUT holding for
|
package/dist/index.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { AgentOptions, ArtifactBody, ArtifactRef, CallOptions, JsonValue, PhaseOptions, SleepArg } from "./types.js";
|
|
1
|
+
import type { AgentOptions, ArtifactBody, ArtifactRef, CallOptions, JsonSchema, JsonValue, PhaseOptions, SleepArg } from "./types.js";
|
|
2
2
|
/**
|
|
3
3
|
* Mark the current run phase for live-tail and run-log grouping. Everything after this call
|
|
4
4
|
* belongs to the named phase until the next `phase(...)` marker or the run ends. This is
|
|
@@ -6,14 +6,21 @@ import type { AgentOptions, ArtifactBody, ArtifactRef, CallOptions, JsonValue, P
|
|
|
6
6
|
*/
|
|
7
7
|
export declare function phase(name: string, opts?: PhaseOptions): void;
|
|
8
8
|
/**
|
|
9
|
-
* Run an agent leaf to completion.
|
|
10
|
-
* (`
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
9
|
+
* Run an agent leaf to completion. Two typed forms, by whether you pass a `schema`:
|
|
10
|
+
* - `agent(prompt, opts?)` (no `schema`) → the leaf's final text (`Promise<string>`).
|
|
11
|
+
* - `agent<Shape>(prompt, { schema })` → the schema-validated object (`Promise<Shape>`); name the
|
|
12
|
+
* expected type. The run fails if the model's output doesn't validate.
|
|
13
|
+
*
|
|
14
|
+
* Asking for a typed result WITHOUT a schema (`agent<Shape>(prompt)`) is a type error: there would
|
|
15
|
+
* be nothing to validate against, so the value would really be a string. Omit `opts.model` to let
|
|
16
|
+
* the provider route automatically (the default `boardwalk` provider on every engine; your own keys
|
|
17
|
+
* only via an explicit provider). Capabilities (`tools`, `mcp`, `skills`, `memory`) are PER-AGENT —
|
|
18
|
+
* each call brings its own; the manifest declares none of them.
|
|
15
19
|
*/
|
|
16
|
-
export declare function agent<T
|
|
20
|
+
export declare function agent<T>(prompt: string, opts: AgentOptions & {
|
|
21
|
+
schema: JsonSchema;
|
|
22
|
+
}): Promise<T>;
|
|
23
|
+
export declare function agent(prompt: string, opts?: AgentOptions): Promise<string>;
|
|
17
24
|
/** Cross-workflow composition: `call` (await the result) and `run` (fire-and-forget). */
|
|
18
25
|
export declare const workflows: {
|
|
19
26
|
/**
|
|
@@ -31,7 +38,7 @@ export declare const workflows: {
|
|
|
31
38
|
};
|
|
32
39
|
/** Hold the run for a duration or until a timestamp (the run stays held while it waits; locals survive). */
|
|
33
40
|
export declare function sleep(arg: SleepArg): Promise<void>;
|
|
34
|
-
/** Granted secrets, resolved lazily and fail-closed against `
|
|
41
|
+
/** Granted secrets, resolved lazily and fail-closed against `permissions.secrets`. */
|
|
35
42
|
export declare const secrets: {
|
|
36
43
|
/** Resolve a granted secret to its plaintext value. */
|
|
37
44
|
readonly get: (name: string) => Promise<string>;
|
|
@@ -60,7 +67,7 @@ export declare function parallel<T>(thunks: readonly (() => Promise<T>)[]): Prom
|
|
|
60
67
|
*/
|
|
61
68
|
export declare function output(value: JsonValue): void;
|
|
62
69
|
export { input, config } from "./host.js";
|
|
63
|
-
export type { WorkflowMeta, Trigger, CronTrigger, WebhookTrigger, ManualTrigger,
|
|
70
|
+
export type { WorkflowMeta, Trigger, CronTrigger, WebhookTrigger, ManualTrigger, McpServerRef, Concurrency, CallableBy, OrgRole, RunsOn, HostedRunsOn, HostedRunsOnObject, HostedRunnerSize, SelfHostedRunsOn, Container, SecretRef, EnvVars, EgressPolicy, RunPermissions, RunPermissionAccess, Budget, Notification, Workspace, } from "./meta.js";
|
|
64
71
|
export type { AgentOptions, ToolDef, ArtifactBody, ArtifactRef, CallOptions, PhaseOptions, SleepArg, JsonSchema, JsonValue, } from "./types.js";
|
|
65
72
|
export { workflowManifestSchema, validateMeta, MetaValidationError, type WorkflowManifest, } from "./manifest.js";
|
|
66
73
|
export { type RunEvent, type RunEventKind, type RunStatus, type Channel, type EventEnvelope, type TokenUsage, type ToolReturn, runEventSchema, CHANNELS, DEFAULT_CHANNELS, channelOf, matchesChannels, makeCursor, TURN_CURSOR_STRIDE, } from "./events.js";
|
package/dist/index.js
CHANGED
|
@@ -24,15 +24,10 @@ export function phase(name, opts) {
|
|
|
24
24
|
}
|
|
25
25
|
host.setPhase(name, opts);
|
|
26
26
|
}
|
|
27
|
-
/**
|
|
28
|
-
* Run an agent leaf to completion. Without `opts.schema`, resolves to the leaf's final text
|
|
29
|
-
* (`T` defaults to `string`); with a schema, resolves to the validated object — pass the
|
|
30
|
-
* expected type, e.g. `await agent<Groups>(prompt, { schema })`. Omit `opts.model` to let the
|
|
31
|
-
* provider route automatically (the default `boardwalk` provider on every engine; your own
|
|
32
|
-
* keys only via an explicit provider). Capabilities (`tools`, `mcp`, `skills`, `memory`) are
|
|
33
|
-
* PER-AGENT — each call brings its own; the manifest declares none of them.
|
|
34
|
-
*/
|
|
35
27
|
export async function agent(prompt, opts) {
|
|
28
|
+
// The host returns `unknown`; the overloads above are the public contract. With a `schema` the
|
|
29
|
+
// host validated the value (best-effort; the run fails on mismatch) → `T`; without one it is the
|
|
30
|
+
// leaf's final text → `string` (the `T = string` default). The cast is confined to this boundary.
|
|
36
31
|
return (await requireHost().agent(prompt, opts));
|
|
37
32
|
}
|
|
38
33
|
/** Cross-workflow composition: `call` (await the result) and `run` (fire-and-forget). */
|
|
@@ -62,7 +57,7 @@ export const workflows = {
|
|
|
62
57
|
export async function sleep(arg) {
|
|
63
58
|
await requireHost().sleep(arg);
|
|
64
59
|
}
|
|
65
|
-
/** Granted secrets, resolved lazily and fail-closed against `
|
|
60
|
+
/** Granted secrets, resolved lazily and fail-closed against `permissions.secrets`. */
|
|
66
61
|
export const secrets = {
|
|
67
62
|
/** Resolve a granted secret to its plaintext value. */
|
|
68
63
|
async get(name) {
|
package/dist/manifest.d.ts
CHANGED
|
@@ -15,9 +15,6 @@ export declare const workflowManifestSchema: z.ZodObject<{
|
|
|
15
15
|
}, z.core.$strict>, z.ZodObject<{
|
|
16
16
|
kind: z.ZodLiteral<"manual">;
|
|
17
17
|
}, z.core.$strict>], "kind">>;
|
|
18
|
-
secrets: z.ZodOptional<z.ZodArray<z.ZodObject<{
|
|
19
|
-
name: z.ZodString;
|
|
20
|
-
}, z.core.$strict>>>;
|
|
21
18
|
env: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodString>>;
|
|
22
19
|
input_schema: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
|
|
23
20
|
output_schema: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
|
|
@@ -81,11 +78,6 @@ export declare const workflowManifestSchema: z.ZodObject<{
|
|
|
81
78
|
secrets: z.ZodOptional<z.ZodArray<z.ZodObject<{
|
|
82
79
|
name: z.ZodString;
|
|
83
80
|
}, z.core.$strict>>>;
|
|
84
|
-
tools: z.ZodOptional<z.ZodArray<z.ZodObject<{
|
|
85
|
-
name: z.ZodString;
|
|
86
|
-
config: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
|
|
87
|
-
scope: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
88
|
-
}, z.core.$strict>>>;
|
|
89
81
|
}, z.core.$strict>>;
|
|
90
82
|
callable_by: z.ZodDefault<z.ZodUnion<readonly [z.ZodObject<{
|
|
91
83
|
roles: z.ZodArray<z.ZodEnum<{
|
package/dist/manifest.js
CHANGED
|
@@ -115,15 +115,6 @@ const concurrencySchema = z.union([
|
|
|
115
115
|
z.strictObject({ mode: z.literal("unlimited") }),
|
|
116
116
|
]);
|
|
117
117
|
// ============================================================================
|
|
118
|
-
// Agent capabilities: NONE on the manifest — tools/mcp/skills/memory are all per-agent
|
|
119
|
-
// ============================================================================
|
|
120
|
-
// Used only by the platform-extension permissions.tools (hosted run-permission scoping).
|
|
121
|
-
const toolGrantSchema = z.strictObject({
|
|
122
|
-
name: shortName,
|
|
123
|
-
config: z.record(z.string(), z.unknown()).optional(),
|
|
124
|
-
scope: z.array(z.string().min(1).max(200)).optional(),
|
|
125
|
-
});
|
|
126
|
-
// ============================================================================
|
|
127
118
|
// Runner selection
|
|
128
119
|
// ============================================================================
|
|
129
120
|
const hostedRunsOnLabel = z.enum([
|
|
@@ -149,12 +140,16 @@ const runsOnSchema = z.union([
|
|
|
149
140
|
// ============================================================================
|
|
150
141
|
const containerSchema = z.strictObject({ image: z.string().min(1).max(512) });
|
|
151
142
|
const permissionAccess = z.enum(["none", "read", "write"]);
|
|
143
|
+
// `permissions` is the run's access-grant surface: what the workflow is ALLOWED to access or do.
|
|
144
|
+
// Access-level knobs (id_token/artifacts/contents) plus the SECRET allowlist — a secret a program
|
|
145
|
+
// may read is a grant, so it lives here, not as a top-level field (a top-level `secrets` next to
|
|
146
|
+
// `env` reads like injection; it isn't). There is NO `tools` grant: tool selection is per-agent
|
|
147
|
+
// (AgentOptions.tools), declared on the `agent()` call that uses it — one place, no run-level ceiling.
|
|
152
148
|
const permissionsSchema = z.strictObject({
|
|
153
149
|
id_token: z.enum(["none", "write"]).optional(),
|
|
154
150
|
artifacts: permissionAccess.optional(),
|
|
155
151
|
contents: permissionAccess.optional(),
|
|
156
152
|
secrets: z.array(secretRefSchema).optional(),
|
|
157
|
-
tools: z.array(toolGrantSchema).optional(),
|
|
158
153
|
});
|
|
159
154
|
const callableBySchema = z.union([
|
|
160
155
|
z.strictObject({ roles: z.array(z.enum(["owner", "admin", "member", "viewer"])).min(1) }),
|
|
@@ -189,7 +184,8 @@ export const workflowManifestSchema = z.strictObject({
|
|
|
189
184
|
name: workflowName,
|
|
190
185
|
description: z.string().max(1000).optional(),
|
|
191
186
|
triggers: z.array(triggerSchema).min(1),
|
|
192
|
-
secrets
|
|
187
|
+
// NO top-level `secrets` — the secret allowlist is `permissions.secrets` (a secret you may read
|
|
188
|
+
// is an access grant). `env` is for value injection (incl. `${{ secrets.NAME }}` of a permitted secret).
|
|
193
189
|
env: envVarsSchema.optional(),
|
|
194
190
|
input_schema: jsonSchemaObject.optional(),
|
|
195
191
|
output_schema: jsonSchemaObject.optional(),
|
package/dist/meta.d.ts
CHANGED
|
@@ -14,15 +14,6 @@ export interface ManualTrigger {
|
|
|
14
14
|
kind: "manual";
|
|
15
15
|
}
|
|
16
16
|
export type Trigger = CronTrigger | WebhookTrigger | ManualTrigger;
|
|
17
|
-
/**
|
|
18
|
-
* A built-in tool grant, with optional configuration. Used only by the platform-extension
|
|
19
|
-
* `permissions.tools` (hosted run-permission scoping) — agent tool selection is per-call.
|
|
20
|
-
*/
|
|
21
|
-
export interface ToolGrant {
|
|
22
|
-
name: string;
|
|
23
|
-
config?: Record<string, unknown>;
|
|
24
|
-
scope?: readonly string[];
|
|
25
|
-
}
|
|
26
17
|
/**
|
|
27
18
|
* An MCP server an `agent()` call connects to (inline in `AgentOptions.mcp` — per-agent, no
|
|
28
19
|
* meta declaration). The program is the trusted layer: put credentials in `env`/`headers`
|
|
@@ -66,8 +57,9 @@ export interface Container {
|
|
|
66
57
|
}
|
|
67
58
|
/**
|
|
68
59
|
* A secret the program may read with `secrets.get(name)` — an allowlist entry, never a value.
|
|
69
|
-
*
|
|
70
|
-
*
|
|
60
|
+
* Declared in `permissions.secrets` (a readable secret is an access grant). Resolution is
|
|
61
|
+
* engine-dependent: environment/`.env` on local engines, the encrypted vault on the Boardwalk
|
|
62
|
+
* platform. Secrets + env vars are the entire credential story.
|
|
71
63
|
*/
|
|
72
64
|
export interface SecretRef {
|
|
73
65
|
name: string;
|
|
@@ -75,8 +67,8 @@ export interface SecretRef {
|
|
|
75
67
|
/**
|
|
76
68
|
* Environment variables for the run. A value is either non-secret plaintext, or a whole-value
|
|
77
69
|
* secret reference `"${{ secrets.NAME }}"` resolved at run time (never stored in the manifest).
|
|
78
|
-
*
|
|
79
|
-
* keys are not allowed.
|
|
70
|
+
* A referenced secret must also be declared in `permissions.secrets` (env injection is the
|
|
71
|
+
* delivery, the permission is the grant). Reserved `BOARDWALK_*` / `AWS_*` keys are not allowed.
|
|
80
72
|
*/
|
|
81
73
|
export type EnvVars = Record<string, string>;
|
|
82
74
|
export type EgressPolicy = {
|
|
@@ -91,12 +83,17 @@ export type EgressPolicy = {
|
|
|
91
83
|
include_defaults?: boolean;
|
|
92
84
|
};
|
|
93
85
|
export type RunPermissionAccess = "none" | "read" | "write";
|
|
86
|
+
/**
|
|
87
|
+
* The run's access-grant surface — what the workflow is allowed to access or do. Access-level
|
|
88
|
+
* knobs (`id_token`/`artifacts`/`contents`) plus the secret allowlist (`secrets`). No `tools`
|
|
89
|
+
* grant: tool selection is per-agent (`AgentOptions.tools`), never a manifest-level ceiling.
|
|
90
|
+
*/
|
|
94
91
|
export interface RunPermissions {
|
|
95
92
|
id_token?: "none" | "write";
|
|
96
93
|
artifacts?: RunPermissionAccess;
|
|
97
94
|
contents?: RunPermissionAccess;
|
|
95
|
+
/** Names of secrets the program may read with `secrets.get` — an allowlist, not values. */
|
|
98
96
|
secrets?: readonly SecretRef[];
|
|
99
|
-
tools?: readonly ToolGrant[];
|
|
100
97
|
}
|
|
101
98
|
export type OrgRole = "owner" | "admin" | "member" | "viewer";
|
|
102
99
|
export type CallableBy = "anyone_in_org" | "users_only" | "workflows_only" | {
|
|
@@ -148,7 +145,6 @@ export interface WorkflowMeta {
|
|
|
148
145
|
description?: string;
|
|
149
146
|
/** At least one trigger is required. */
|
|
150
147
|
triggers: readonly Trigger[];
|
|
151
|
-
secrets?: readonly SecretRef[];
|
|
152
148
|
env?: EnvVars;
|
|
153
149
|
input_schema?: Record<string, unknown>;
|
|
154
150
|
output_schema?: Record<string, unknown>;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@boardwalk-labs/workflow",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.4",
|
|
4
4
|
"description": "Author Boardwalk workflows in TypeScript: agent(), sleep(), workflows.call(), secrets, the manifest schema, and the run-event wire format.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -37,7 +37,7 @@
|
|
|
37
37
|
"lint": "eslint . --max-warnings 0",
|
|
38
38
|
"format": "prettier --write .",
|
|
39
39
|
"format:check": "prettier --check .",
|
|
40
|
-
"test": "vitest run",
|
|
40
|
+
"test": "vitest run --coverage",
|
|
41
41
|
"test:watch": "vitest"
|
|
42
42
|
},
|
|
43
43
|
"dependencies": {
|
|
@@ -47,6 +47,7 @@
|
|
|
47
47
|
"devDependencies": {
|
|
48
48
|
"@eslint/js": "^9.18.0",
|
|
49
49
|
"@types/node": "^24.0.0",
|
|
50
|
+
"@vitest/coverage-v8": "^3.2.6",
|
|
50
51
|
"eslint": "^9.18.0",
|
|
51
52
|
"prettier": "^3.4.0",
|
|
52
53
|
"typescript-eslint": "^8.20.0",
|