@stackbone/cli 0.1.0-alpha.12 → 0.1.0-alpha.14
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/CHANGELOG.md +69 -0
- package/main.js +18389 -16138
- package/migrations/0017_runs_workflow_chat_triggers.sql +25 -0
- package/migrations/0018_run_steps_v2.sql +167 -0
- package/migrations/0019_sessions.sql +106 -0
- package/migrations/0020_runs_entity_name.sql +23 -0
- package/package.json +2 -2
- package/runtime-deps/@stackbone/agent-server/index.js +8 -16
- package/runtime-deps/@stackbone/workflow-world/index.js +8913 -0
- package/stackbone-cli-0.1.0-alpha.14.tgz +0 -0
- package/stackbone-cli-0.1.0-alpha.12.tgz +0 -0
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
-- 0017_runs_workflow_chat_triggers.sql — widen the runs/run_steps CHECKs for
|
|
2
|
+
-- the eve durable runtime (feature 67).
|
|
3
|
+
--
|
|
4
|
+
-- The Workflow World → runs projection (F5) materialises each durable workflow
|
|
5
|
+
-- as a first-class run so it shows up in the Studio timeline; it tags those rows
|
|
6
|
+
-- with `trigger='workflow'` and records each workflow step as a generic
|
|
7
|
+
-- `run_steps.type='workflow_step'`. The chat recorder (F6) persists every eve
|
|
8
|
+
-- chat turn as a run tagged `trigger='chat'` (the chat session id rides the
|
|
9
|
+
-- free-form `runs.metadata` column — no new column is added). The earlier
|
|
10
|
+
-- CHECKs (0001 listed `manual/webhook/cron/event`, 0016 added `job`) predate all
|
|
11
|
+
-- three values and would reject the inserts. Widen `runs_trigger_check` to match
|
|
12
|
+
-- `runTriggerSchema` and `run_steps_type_check` to match the SDK `RunStepType`.
|
|
13
|
+
ALTER TABLE stackbone_platform.runs DROP CONSTRAINT IF EXISTS runs_trigger_check;
|
|
14
|
+
ALTER TABLE stackbone_platform.runs
|
|
15
|
+
ADD CONSTRAINT runs_trigger_check
|
|
16
|
+
CHECK (trigger IN ('manual', 'webhook', 'cron', 'event', 'job', 'workflow', 'chat'));
|
|
17
|
+
|
|
18
|
+
ALTER TABLE stackbone_platform.run_steps DROP CONSTRAINT IF EXISTS run_steps_type_check;
|
|
19
|
+
ALTER TABLE stackbone_platform.run_steps
|
|
20
|
+
ADD CONSTRAINT run_steps_type_check
|
|
21
|
+
CHECK (type IN (
|
|
22
|
+
'agent', 'llm_call', 'db_query', 'http_fetch',
|
|
23
|
+
'queue_publish', 'hitl_pause', 'tool_call',
|
|
24
|
+
'rag_query', 'storage_op', 'workflow_step'
|
|
25
|
+
));
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
-- 0018_run_steps_v2.sql — demolish the legacy run_steps surface and rebuild it
|
|
2
|
+
-- around the eve + Workflow World step telemetry (feature 68, Fase 2 / A4).
|
|
3
|
+
--
|
|
4
|
+
-- The old run_steps table (0001) modelled hand-written SDK steps (`ctx.step`
|
|
5
|
+
-- with types like `llm_call`/`db_query`/…), carried no token columns and was
|
|
6
|
+
-- driven by the now-removed span processor / `ctx.step` writers. Feature 67 only
|
|
7
|
+
-- widened its type CHECK to admit `workflow_step`; feature 68 drops the whole
|
|
8
|
+
-- table and recreates it v2.
|
|
9
|
+
--
|
|
10
|
+
-- v2 projects the durable Workflow World step timeline (one row per World step,
|
|
11
|
+
-- `type='workflow_step'`) plus, for the fast-follow, per-model-call rows
|
|
12
|
+
-- (`type='model_call'`). Each row carries the four eve token counters
|
|
13
|
+
-- (input/output/cache-read/cache-write); the total is derived at read time, not
|
|
14
|
+
-- stored. `world_step_id` is the World correlation/idempotency key the projector
|
|
15
|
+
-- upserts on. No cost column (D5 — tokens only, no $).
|
|
16
|
+
--
|
|
17
|
+
-- This migration also drops `runs.cost_estimated_usd` (the demolished cost
|
|
18
|
+
-- accumulator) and recreates the `approvals.requested_by_step_id` FK against
|
|
19
|
+
-- run_steps v2 (A6), plus the `notify_run_step_event` pg_notify trigger that the
|
|
20
|
+
-- table drop took down with it.
|
|
21
|
+
|
|
22
|
+
-- --------------------------------------------------------------------------
|
|
23
|
+
-- Demolition (A3): drop the legacy table, the cost column and the dependent FK.
|
|
24
|
+
-- --------------------------------------------------------------------------
|
|
25
|
+
|
|
26
|
+
-- The FK lives on `approvals` and references the old run_steps; drop it before
|
|
27
|
+
-- the table so the table drop does not have to CASCADE through it. Recreated
|
|
28
|
+
-- against v2 at the end of this migration.
|
|
29
|
+
ALTER TABLE stackbone_platform.approvals
|
|
30
|
+
DROP CONSTRAINT IF EXISTS approvals_requested_by_step_id_fkey;
|
|
31
|
+
ALTER TABLE stackbone_platform.approvals
|
|
32
|
+
DROP COLUMN IF EXISTS requested_by_step_id;
|
|
33
|
+
|
|
34
|
+
-- The AFTER INSERT/UPDATE trigger and its function are owned by the old table;
|
|
35
|
+
-- drop both so the recreate below is unambiguous.
|
|
36
|
+
DROP TRIGGER IF EXISTS run_steps_notify_aiu ON stackbone_platform.run_steps;
|
|
37
|
+
DROP FUNCTION IF EXISTS stackbone_platform.notify_run_step_event();
|
|
38
|
+
|
|
39
|
+
-- Self-referential parent FK and run FK go away with the table. CASCADE clears
|
|
40
|
+
-- the dependent self-FK in one shot.
|
|
41
|
+
DROP TABLE IF EXISTS stackbone_platform.run_steps CASCADE;
|
|
42
|
+
|
|
43
|
+
-- The per-run cost accumulator is demolished (no $ cost anywhere in this
|
|
44
|
+
-- feature).
|
|
45
|
+
ALTER TABLE stackbone_platform.runs
|
|
46
|
+
DROP COLUMN IF EXISTS cost_estimated_usd;
|
|
47
|
+
|
|
48
|
+
-- Per-turn token counters on `runs` (v1, A5). An eve agent CHAT turn = one
|
|
49
|
+
-- `runs` row (trigger='chat'); the chat recorder stamps the four eve
|
|
50
|
+
-- `data.usage` counters onto that row (the per-model-call granularity stays in
|
|
51
|
+
-- run_steps, used by workflows). All nullable; the total is `input + output`,
|
|
52
|
+
-- derived at read, never stored (D4). The runs timeline and feature 69's
|
|
53
|
+
-- session aggregate read these directly off `runs`.
|
|
54
|
+
ALTER TABLE stackbone_platform.runs
|
|
55
|
+
ADD COLUMN IF NOT EXISTS input_tokens integer,
|
|
56
|
+
ADD COLUMN IF NOT EXISTS output_tokens integer,
|
|
57
|
+
ADD COLUMN IF NOT EXISTS cache_read_tokens integer,
|
|
58
|
+
ADD COLUMN IF NOT EXISTS cache_write_tokens integer;
|
|
59
|
+
|
|
60
|
+
-- --------------------------------------------------------------------------
|
|
61
|
+
-- Recreate run_steps v2.
|
|
62
|
+
-- --------------------------------------------------------------------------
|
|
63
|
+
|
|
64
|
+
CREATE TABLE stackbone_platform.run_steps (
|
|
65
|
+
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
66
|
+
run_id uuid NOT NULL
|
|
67
|
+
REFERENCES stackbone_platform.runs(id) ON DELETE CASCADE,
|
|
68
|
+
-- Nullable self-FK. In practice null for flat "use step" workflows (the
|
|
69
|
+
-- World fork does not populate step-parent provenance for them), but kept
|
|
70
|
+
-- for the nested-step tree the projector derives from the event-log writer.
|
|
71
|
+
parent_step_id uuid REFERENCES stackbone_platform.run_steps(id) ON DELETE CASCADE,
|
|
72
|
+
type text NOT NULL
|
|
73
|
+
CHECK (type IN ('workflow_step', 'model_call')),
|
|
74
|
+
name text,
|
|
75
|
+
status text NOT NULL DEFAULT 'running'
|
|
76
|
+
CHECK (status IN (
|
|
77
|
+
'pending', 'running', 'completed',
|
|
78
|
+
'failed', 'cancelled'
|
|
79
|
+
)),
|
|
80
|
+
started_at timestamptz,
|
|
81
|
+
completed_at timestamptz,
|
|
82
|
+
duration_ms integer,
|
|
83
|
+
input jsonb,
|
|
84
|
+
output jsonb,
|
|
85
|
+
error jsonb,
|
|
86
|
+
-- The four eve token counters (data.usage on `step.completed`). All
|
|
87
|
+
-- nullable: workflow steps carry none (the World has no tokens) and any
|
|
88
|
+
-- counter may be absent. The total is `input + output`, derived at read
|
|
89
|
+
-- time — never persisted (D4).
|
|
90
|
+
input_tokens integer,
|
|
91
|
+
output_tokens integer,
|
|
92
|
+
cache_read_tokens integer,
|
|
93
|
+
cache_write_tokens integer,
|
|
94
|
+
-- World correlation / idempotency key (= the World `stepId`). The
|
|
95
|
+
-- event-driven projector upserts on this so a replayed/retried step updates
|
|
96
|
+
-- the same row instead of inserting a duplicate.
|
|
97
|
+
world_step_id text,
|
|
98
|
+
created_at timestamptz NOT NULL DEFAULT now()
|
|
99
|
+
);
|
|
100
|
+
|
|
101
|
+
CREATE INDEX IF NOT EXISTS run_steps_run_started_idx
|
|
102
|
+
ON stackbone_platform.run_steps (run_id, started_at);
|
|
103
|
+
CREATE INDEX IF NOT EXISTS run_steps_parent_idx
|
|
104
|
+
ON stackbone_platform.run_steps (parent_step_id);
|
|
105
|
+
|
|
106
|
+
-- Idempotent upsert target for the projector: one row per (run, World step).
|
|
107
|
+
-- Partial so rows without a World correlation (e.g. future synthetic rows)
|
|
108
|
+
-- never block one another on a NULL key.
|
|
109
|
+
CREATE UNIQUE INDEX IF NOT EXISTS run_steps_world_step_uq
|
|
110
|
+
ON stackbone_platform.run_steps (run_id, world_step_id)
|
|
111
|
+
WHERE world_step_id IS NOT NULL;
|
|
112
|
+
|
|
113
|
+
-- --------------------------------------------------------------------------
|
|
114
|
+
-- Recreate the pg_notify trigger on the same `stackbone_platform_events`
|
|
115
|
+
-- channel the SSE handler / emulator bus already LISTEN on. `created` fires on
|
|
116
|
+
-- INSERT; `finished` fires when completed_at transitions from null to a value.
|
|
117
|
+
-- --------------------------------------------------------------------------
|
|
118
|
+
|
|
119
|
+
CREATE OR REPLACE FUNCTION stackbone_platform.notify_run_step_event() RETURNS trigger
|
|
120
|
+
LANGUAGE plpgsql AS $fn$
|
|
121
|
+
DECLARE
|
|
122
|
+
payload jsonb;
|
|
123
|
+
BEGIN
|
|
124
|
+
IF TG_OP = 'INSERT' THEN
|
|
125
|
+
payload := jsonb_build_object(
|
|
126
|
+
'type', 'run.step.created',
|
|
127
|
+
'run_id', NEW.run_id,
|
|
128
|
+
'step_id', NEW.id,
|
|
129
|
+
'parent_step_id', NEW.parent_step_id,
|
|
130
|
+
'step_type', NEW.type,
|
|
131
|
+
'started_at', NEW.started_at
|
|
132
|
+
);
|
|
133
|
+
ELSIF TG_OP = 'UPDATE' THEN
|
|
134
|
+
IF (OLD.completed_at IS NULL) AND (NEW.completed_at IS NOT NULL) THEN
|
|
135
|
+
payload := jsonb_build_object(
|
|
136
|
+
'type', 'run.step.finished',
|
|
137
|
+
'run_id', NEW.run_id,
|
|
138
|
+
'step_id', NEW.id,
|
|
139
|
+
'status', NEW.status,
|
|
140
|
+
'duration_ms', NEW.duration_ms
|
|
141
|
+
);
|
|
142
|
+
ELSE
|
|
143
|
+
RETURN NULL;
|
|
144
|
+
END IF;
|
|
145
|
+
ELSE
|
|
146
|
+
RETURN NULL;
|
|
147
|
+
END IF;
|
|
148
|
+
|
|
149
|
+
PERFORM pg_notify('stackbone_platform_events', payload::text);
|
|
150
|
+
RETURN NULL;
|
|
151
|
+
END;
|
|
152
|
+
$fn$;
|
|
153
|
+
|
|
154
|
+
DROP TRIGGER IF EXISTS run_steps_notify_aiu ON stackbone_platform.run_steps;
|
|
155
|
+
CREATE TRIGGER run_steps_notify_aiu
|
|
156
|
+
AFTER INSERT OR UPDATE ON stackbone_platform.run_steps
|
|
157
|
+
FOR EACH ROW EXECUTE FUNCTION stackbone_platform.notify_run_step_event();
|
|
158
|
+
|
|
159
|
+
-- --------------------------------------------------------------------------
|
|
160
|
+
-- Recreate the approvals → run_steps FK against v2 (A6). `requestApproval`
|
|
161
|
+
-- stamps the id of the workflow step that asked for the approval so the inbox
|
|
162
|
+
-- can anchor it to the exact step.
|
|
163
|
+
-- --------------------------------------------------------------------------
|
|
164
|
+
|
|
165
|
+
ALTER TABLE stackbone_platform.approvals
|
|
166
|
+
ADD COLUMN IF NOT EXISTS requested_by_step_id uuid
|
|
167
|
+
REFERENCES stackbone_platform.run_steps(id) ON DELETE SET NULL;
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
-- 0019_sessions.sql — project eve agent conversations as a first-class entity
|
|
2
|
+
-- (feature 69, Fase 1).
|
|
3
|
+
--
|
|
4
|
+
-- An eve agent conversation is durable in the agent's Workflow World (one
|
|
5
|
+
-- long-lived `workflowEntry` run per session). The control plane never read the
|
|
6
|
+
-- World back, so a conversation had no row/endpoint/screen — only the loose
|
|
7
|
+
-- `runs.metadata.sessionId` jsonb the chat proxy stamped on each turn. This
|
|
8
|
+
-- migration adds the SESSION as a projected entity: a lean `sessions` table fed
|
|
9
|
+
-- by the World→sessions projector (Fase 2), plus a real `runs.session_id` column
|
|
10
|
+
-- so a session's turns join by an indexed key instead of the jsonb.
|
|
11
|
+
--
|
|
12
|
+
-- Lean by design (D6): `sessions` stores ONLY lifecycle (status + timestamps).
|
|
13
|
+
-- The read-time aggregates (turn_count, last_activity_at, preview, tokens) are a
|
|
14
|
+
-- GROUP BY over `runs` keyed on `session_id`; nothing here couples to feature
|
|
15
|
+
-- 68's token columns.
|
|
16
|
+
|
|
17
|
+
-- --------------------------------------------------------------------------
|
|
18
|
+
-- The sessions table. `id` is the deterministic UUIDv5 the projector derives
|
|
19
|
+
-- from the eve sessionId (= the `workflowEntry` World runId) under its own
|
|
20
|
+
-- namespace — always caller-provided, so no gen_random_uuid default. The
|
|
21
|
+
-- `status` CHECK is intentionally EXTENSIBLE (D5): v1 models only the three
|
|
22
|
+
-- lifecycle states; the later HITL-in-chat feature widens it with
|
|
23
|
+
-- `awaiting_input` via a non-breaking ALTER (the 0017 pattern), not a rewrite.
|
|
24
|
+
-- --------------------------------------------------------------------------
|
|
25
|
+
CREATE TABLE IF NOT EXISTS stackbone_platform.sessions (
|
|
26
|
+
id uuid PRIMARY KEY,
|
|
27
|
+
-- The eve agent this conversation targets. Nullable in local (the emulator
|
|
28
|
+
-- is single-tenant), required in cloud — mirrors `runs.agent_id`.
|
|
29
|
+
agent_id text,
|
|
30
|
+
status text NOT NULL DEFAULT 'open'
|
|
31
|
+
CHECK (status IN ('open', 'completed', 'failed')),
|
|
32
|
+
started_at timestamptz,
|
|
33
|
+
ended_at timestamptz,
|
|
34
|
+
created_at timestamptz NOT NULL DEFAULT now()
|
|
35
|
+
);
|
|
36
|
+
|
|
37
|
+
-- Cursor pagination over (created_at, id), the same keyset shape the runs list
|
|
38
|
+
-- uses, so the Studio Sessions list reuses one decoder.
|
|
39
|
+
CREATE INDEX IF NOT EXISTS sessions_created_at_id_idx
|
|
40
|
+
ON stackbone_platform.sessions (created_at DESC, id DESC);
|
|
41
|
+
|
|
42
|
+
CREATE INDEX IF NOT EXISTS sessions_agent_status_idx
|
|
43
|
+
ON stackbone_platform.sessions (agent_id, status, created_at DESC);
|
|
44
|
+
|
|
45
|
+
-- --------------------------------------------------------------------------
|
|
46
|
+
-- `runs.session_id` — the real join key (D4). Stamped by the chat recorder and
|
|
47
|
+
-- the projector with the SAME deterministic UUIDv5 as `sessions.id`. A LOGICAL
|
|
48
|
+
-- FK only (NOT enforced): a turn can persist before the session row exists
|
|
49
|
+
-- (eventual consistency; the projector and recorder race), so a real FK would
|
|
50
|
+
-- reject the turn. `runs.metadata.sessionId` stays for back-compat but the join
|
|
51
|
+
-- moves to this column. Indexed for the `WHERE session_id = :id` turn lookup.
|
|
52
|
+
-- --------------------------------------------------------------------------
|
|
53
|
+
ALTER TABLE stackbone_platform.runs
|
|
54
|
+
ADD COLUMN IF NOT EXISTS session_id uuid;
|
|
55
|
+
|
|
56
|
+
CREATE INDEX IF NOT EXISTS runs_session_id_idx
|
|
57
|
+
ON stackbone_platform.runs (session_id, created_at)
|
|
58
|
+
WHERE session_id IS NOT NULL;
|
|
59
|
+
|
|
60
|
+
-- --------------------------------------------------------------------------
|
|
61
|
+
-- pg_notify trigger on the SAME `stackbone_platform_events` channel the
|
|
62
|
+
-- runs/run_steps/approvals triggers use, so the cloud `StudioStreamService` (one
|
|
63
|
+
-- LISTEN) and the emulator's run-steps-bridge forward `session.*` with no new
|
|
64
|
+
-- infra. `created` fires on INSERT; `updated` fires when the lifecycle actually
|
|
65
|
+
-- moved (status or ended_at changed) so an idempotent re-upsert of the same
|
|
66
|
+
-- state does not spam the stream.
|
|
67
|
+
-- --------------------------------------------------------------------------
|
|
68
|
+
CREATE OR REPLACE FUNCTION stackbone_platform.notify_session_event() RETURNS trigger
|
|
69
|
+
LANGUAGE plpgsql AS $fn$
|
|
70
|
+
DECLARE
|
|
71
|
+
payload jsonb;
|
|
72
|
+
BEGIN
|
|
73
|
+
IF TG_OP = 'INSERT' THEN
|
|
74
|
+
payload := jsonb_build_object(
|
|
75
|
+
'type', 'session.created',
|
|
76
|
+
'session_id', NEW.id,
|
|
77
|
+
'agent_id', NEW.agent_id,
|
|
78
|
+
'status', NEW.status,
|
|
79
|
+
'started_at', NEW.started_at,
|
|
80
|
+
'created_at', NEW.created_at
|
|
81
|
+
);
|
|
82
|
+
ELSIF TG_OP = 'UPDATE' THEN
|
|
83
|
+
IF (OLD.status IS DISTINCT FROM NEW.status)
|
|
84
|
+
OR (OLD.ended_at IS DISTINCT FROM NEW.ended_at) THEN
|
|
85
|
+
payload := jsonb_build_object(
|
|
86
|
+
'type', 'session.updated',
|
|
87
|
+
'session_id', NEW.id,
|
|
88
|
+
'status', NEW.status,
|
|
89
|
+
'ended_at', NEW.ended_at
|
|
90
|
+
);
|
|
91
|
+
ELSE
|
|
92
|
+
RETURN NULL;
|
|
93
|
+
END IF;
|
|
94
|
+
ELSE
|
|
95
|
+
RETURN NULL;
|
|
96
|
+
END IF;
|
|
97
|
+
|
|
98
|
+
PERFORM pg_notify('stackbone_platform_events', payload::text);
|
|
99
|
+
RETURN NULL;
|
|
100
|
+
END;
|
|
101
|
+
$fn$;
|
|
102
|
+
|
|
103
|
+
DROP TRIGGER IF EXISTS sessions_notify_aiu ON stackbone_platform.sessions;
|
|
104
|
+
CREATE TRIGGER sessions_notify_aiu
|
|
105
|
+
AFTER INSERT OR UPDATE ON stackbone_platform.sessions
|
|
106
|
+
FOR EACH ROW EXECUTE FUNCTION stackbone_platform.notify_session_event();
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
-- 0020_runs_entity_name.sql — record which agent/workflow a run belongs to.
|
|
2
|
+
--
|
|
3
|
+
-- The Studio Runs screen lists a run's id, trigger and status but never the
|
|
4
|
+
-- NAME of the agent or workflow that produced it, so a workspace running several
|
|
5
|
+
-- eve agents + workflows could not tell whose run a row was, nor filter by it.
|
|
6
|
+
-- The name is known at run-creation time (the chat proxy has the agent `:name`;
|
|
7
|
+
-- the workflow start has the workflow `:name`) but was dropped.
|
|
8
|
+
--
|
|
9
|
+
-- `entity_name` denormalises that name onto the run so the list renders it as a
|
|
10
|
+
-- column and filters by it without a join. It is the agent name for `chat` runs
|
|
11
|
+
-- and the workflow name for `workflow` runs; combined with the existing
|
|
12
|
+
-- `trigger` the UI knows the kind. Nullable: classic/manual cloud runs (eve is
|
|
13
|
+
-- dev-only) leave it NULL. Distinct from `agent_id`, which in cloud is the agent
|
|
14
|
+
-- TEMPLATE id (a stable identifier, not a display name) and is null for workflows.
|
|
15
|
+
|
|
16
|
+
ALTER TABLE stackbone_platform.runs
|
|
17
|
+
ADD COLUMN IF NOT EXISTS entity_name text;
|
|
18
|
+
|
|
19
|
+
-- Backs both the exact-match filter (`WHERE entity_name = $1`) and the distinct
|
|
20
|
+
-- list that populates the Runs screen's name dropdown.
|
|
21
|
+
CREATE INDEX IF NOT EXISTS runs_entity_name_idx
|
|
22
|
+
ON stackbone_platform.runs (entity_name)
|
|
23
|
+
WHERE entity_name IS NOT NULL;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@stackbone/cli",
|
|
3
|
-
"version": "0.1.0-alpha.
|
|
3
|
+
"version": "0.1.0-alpha.14",
|
|
4
4
|
"license": "UNLICENSED",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -34,7 +34,7 @@
|
|
|
34
34
|
"pino": "^10.3.1",
|
|
35
35
|
"pino-pretty": "^13.1.3",
|
|
36
36
|
"postgres": "^3.4.9",
|
|
37
|
-
"
|
|
37
|
+
"tar": "^7.5.13",
|
|
38
38
|
"yaml": "^2.8.3",
|
|
39
39
|
"zod": "^4.3.6"
|
|
40
40
|
}
|