@eidentic/studio 0.1.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.
@@ -0,0 +1,113 @@
1
+ import * as _eidentic_server from '@eidentic/server';
2
+ export { ApiKeyAuth, NoAuth, ServeNodeHandle, serveNode } from '@eidentic/server';
3
+ import { Hono } from 'hono';
4
+ import { Agent } from '@eidentic/core';
5
+ import { AuthPort, PriceTable } from '@eidentic/types';
6
+ import { WorkflowResult } from '@eidentic/workflow';
7
+
8
+ /** A SkillBank-shaped interface: only what the studio needs. */
9
+ interface SkillBankLike {
10
+ list(): unknown[];
11
+ approve(name: string): boolean;
12
+ }
13
+ /** Summary shape returned by GET /api/workflows (list). */
14
+ interface WorkflowRunSummary {
15
+ id: string;
16
+ name: string;
17
+ status: "ok" | "error";
18
+ startedAt: number;
19
+ durationMs: number;
20
+ stepCount: number;
21
+ }
22
+ /**
23
+ * The Hono app returned by createStudioApi / createStudio, extended with
24
+ * workflow recording. It IS a Hono instance (all `app.request(...)` etc. work)
25
+ * and additionally exposes `recordWorkflow`.
26
+ */
27
+ type StudioHandle = Hono & {
28
+ /**
29
+ * Record a completed workflow run into the in-memory registry.
30
+ *
31
+ * @param name - Human-readable workflow name (e.g. "triage").
32
+ * @param result - The WorkflowResult returned by `workflow.run()`.
33
+ *
34
+ * @example
35
+ * const r = await wf.run(input);
36
+ * studio.recordWorkflow("triage", r);
37
+ */
38
+ recordWorkflow<O>(name: string, result: WorkflowResult<O>): void;
39
+ };
40
+ interface StudioOptions {
41
+ /** The agents to inspect/manage, keyed by their id. */
42
+ agents: Record<string, Agent>;
43
+ /**
44
+ * Optional skill banks, keyed by agentId. Each value must be a SkillBank
45
+ * (from @eidentic/skills) or any object with a `list()` and `approve()` method.
46
+ */
47
+ skillBanks?: Record<string, SkillBankLike>;
48
+ /**
49
+ * Authentication adapter. Defaults to NoAuth.
50
+ *
51
+ * IMPORTANT: The studio exposes agent memory read/write, session traces,
52
+ * and fact graph data. In production or any networked environment, always
53
+ * configure ApiKeyAuth (or a custom AuthPort) — never run with NoAuth
54
+ * on an internet-reachable address.
55
+ */
56
+ auth?: AuthPort;
57
+ /** Base path for the management API. Default "/api". */
58
+ basePath?: string;
59
+ /**
60
+ * USD price table used for cost estimation. Defaults to `defaultPrices` from
61
+ * @eidentic/model. Pass a custom table to override (e.g. for private deployments
62
+ * with negotiated pricing). Token counts are always exact; USD is an estimate.
63
+ */
64
+ prices?: PriceTable;
65
+ }
66
+ /**
67
+ * Build a Hono app exposing the Eidentic Studio management API.
68
+ *
69
+ * Routes (all JSON, auth-gated except health):
70
+ * GET /api/health
71
+ * GET /api/agents
72
+ * GET /api/agents/:id/sessions?limit=
73
+ * GET /api/agents/:id/sessions/:sid/events
74
+ * GET /api/agents/:id/blocks?userId=
75
+ * PUT /api/agents/:id/blocks/:label
76
+ * GET /api/agents/:id/facts?userId=&subject=
77
+ * GET /api/agents/:id/memories?userId=&q=
78
+ * GET /api/agents/:id/skills
79
+ * POST /api/agents/:id/skills/:skillId/approve
80
+ * GET /api/workflows
81
+ * GET /api/workflows/:id
82
+ * POST /api/workflows (HTTP ingestion, auth-gated)
83
+ *
84
+ * Returns a StudioHandle with `app` (the Hono instance) and `recordWorkflow`.
85
+ */
86
+ declare function createStudioApi(opts: StudioOptions): StudioHandle;
87
+ /**
88
+ * Build a Hono app combining the agent query/resume/events run API (from
89
+ * `@eidentic/server`) with the Studio management API.
90
+ *
91
+ * Run routes: POST /v1/agents/:id/query (SSE)
92
+ * POST /v1/agents/:id/resume (SSE)
93
+ * Studio: GET /api/health
94
+ * GET /api/agents
95
+ * ...all management routes
96
+ *
97
+ * Returns a StudioHandle (includes `recordWorkflow` and `app`).
98
+ */
99
+ declare function createStudio(opts: StudioOptions): StudioHandle;
100
+ interface StudioServeOptions extends StudioOptions {
101
+ }
102
+ /**
103
+ * Build the combined Hono app (static SPA + /api + /v1 routes) and serve it
104
+ * via @hono/node-server on the given port.
105
+ *
106
+ * The built React SPA (ui/dist) is resolved relative to this source file at
107
+ * runtime so it works whether installed from npm or used from the monorepo.
108
+ */
109
+ declare function serveStudio(opts: StudioServeOptions, { port }?: {
110
+ port?: number;
111
+ }): Promise<_eidentic_server.ServeNodeHandle>;
112
+
113
+ export { type StudioHandle, type StudioOptions, type StudioServeOptions, type WorkflowRunSummary, createStudio, createStudioApi, serveStudio };
@@ -0,0 +1,113 @@
1
+ import * as _eidentic_server from '@eidentic/server';
2
+ export { ApiKeyAuth, NoAuth, ServeNodeHandle, serveNode } from '@eidentic/server';
3
+ import { Hono } from 'hono';
4
+ import { Agent } from '@eidentic/core';
5
+ import { AuthPort, PriceTable } from '@eidentic/types';
6
+ import { WorkflowResult } from '@eidentic/workflow';
7
+
8
+ /** A SkillBank-shaped interface: only what the studio needs. */
9
+ interface SkillBankLike {
10
+ list(): unknown[];
11
+ approve(name: string): boolean;
12
+ }
13
+ /** Summary shape returned by GET /api/workflows (list). */
14
+ interface WorkflowRunSummary {
15
+ id: string;
16
+ name: string;
17
+ status: "ok" | "error";
18
+ startedAt: number;
19
+ durationMs: number;
20
+ stepCount: number;
21
+ }
22
+ /**
23
+ * The Hono app returned by createStudioApi / createStudio, extended with
24
+ * workflow recording. It IS a Hono instance (all `app.request(...)` etc. work)
25
+ * and additionally exposes `recordWorkflow`.
26
+ */
27
+ type StudioHandle = Hono & {
28
+ /**
29
+ * Record a completed workflow run into the in-memory registry.
30
+ *
31
+ * @param name - Human-readable workflow name (e.g. "triage").
32
+ * @param result - The WorkflowResult returned by `workflow.run()`.
33
+ *
34
+ * @example
35
+ * const r = await wf.run(input);
36
+ * studio.recordWorkflow("triage", r);
37
+ */
38
+ recordWorkflow<O>(name: string, result: WorkflowResult<O>): void;
39
+ };
40
+ interface StudioOptions {
41
+ /** The agents to inspect/manage, keyed by their id. */
42
+ agents: Record<string, Agent>;
43
+ /**
44
+ * Optional skill banks, keyed by agentId. Each value must be a SkillBank
45
+ * (from @eidentic/skills) or any object with a `list()` and `approve()` method.
46
+ */
47
+ skillBanks?: Record<string, SkillBankLike>;
48
+ /**
49
+ * Authentication adapter. Defaults to NoAuth.
50
+ *
51
+ * IMPORTANT: The studio exposes agent memory read/write, session traces,
52
+ * and fact graph data. In production or any networked environment, always
53
+ * configure ApiKeyAuth (or a custom AuthPort) — never run with NoAuth
54
+ * on an internet-reachable address.
55
+ */
56
+ auth?: AuthPort;
57
+ /** Base path for the management API. Default "/api". */
58
+ basePath?: string;
59
+ /**
60
+ * USD price table used for cost estimation. Defaults to `defaultPrices` from
61
+ * @eidentic/model. Pass a custom table to override (e.g. for private deployments
62
+ * with negotiated pricing). Token counts are always exact; USD is an estimate.
63
+ */
64
+ prices?: PriceTable;
65
+ }
66
+ /**
67
+ * Build a Hono app exposing the Eidentic Studio management API.
68
+ *
69
+ * Routes (all JSON, auth-gated except health):
70
+ * GET /api/health
71
+ * GET /api/agents
72
+ * GET /api/agents/:id/sessions?limit=
73
+ * GET /api/agents/:id/sessions/:sid/events
74
+ * GET /api/agents/:id/blocks?userId=
75
+ * PUT /api/agents/:id/blocks/:label
76
+ * GET /api/agents/:id/facts?userId=&subject=
77
+ * GET /api/agents/:id/memories?userId=&q=
78
+ * GET /api/agents/:id/skills
79
+ * POST /api/agents/:id/skills/:skillId/approve
80
+ * GET /api/workflows
81
+ * GET /api/workflows/:id
82
+ * POST /api/workflows (HTTP ingestion, auth-gated)
83
+ *
84
+ * Returns a StudioHandle with `app` (the Hono instance) and `recordWorkflow`.
85
+ */
86
+ declare function createStudioApi(opts: StudioOptions): StudioHandle;
87
+ /**
88
+ * Build a Hono app combining the agent query/resume/events run API (from
89
+ * `@eidentic/server`) with the Studio management API.
90
+ *
91
+ * Run routes: POST /v1/agents/:id/query (SSE)
92
+ * POST /v1/agents/:id/resume (SSE)
93
+ * Studio: GET /api/health
94
+ * GET /api/agents
95
+ * ...all management routes
96
+ *
97
+ * Returns a StudioHandle (includes `recordWorkflow` and `app`).
98
+ */
99
+ declare function createStudio(opts: StudioOptions): StudioHandle;
100
+ interface StudioServeOptions extends StudioOptions {
101
+ }
102
+ /**
103
+ * Build the combined Hono app (static SPA + /api + /v1 routes) and serve it
104
+ * via @hono/node-server on the given port.
105
+ *
106
+ * The built React SPA (ui/dist) is resolved relative to this source file at
107
+ * runtime so it works whether installed from npm or used from the monorepo.
108
+ */
109
+ declare function serveStudio(opts: StudioServeOptions, { port }?: {
110
+ port?: number;
111
+ }): Promise<_eidentic_server.ServeNodeHandle>;
112
+
113
+ export { type StudioHandle, type StudioOptions, type StudioServeOptions, type WorkflowRunSummary, createStudio, createStudioApi, serveStudio };
package/dist/index.js ADDED
@@ -0,0 +1,372 @@
1
+ // src/index.ts
2
+ import { Hono } from "hono";
3
+ import { serveStatic } from "@hono/node-server/serve-static";
4
+ import { fileURLToPath } from "node:url";
5
+ import { dirname, join } from "node:path";
6
+ import { createWorkflowRunRegistry } from "@eidentic/workflow";
7
+ import { createServer, NoAuth, ApiKeyAuth, serveNode } from "@eidentic/server";
8
+ import { defaultPrices, pricesUpdatedAt } from "@eidentic/model";
9
+ async function runAuth(auth, req) {
10
+ const headers = {};
11
+ req.headers.forEach((value, key) => {
12
+ headers[key.toLowerCase()] = value;
13
+ });
14
+ const url = new URL(req.url);
15
+ const authReq = {
16
+ method: req.method,
17
+ path: url.pathname,
18
+ headers
19
+ };
20
+ const principal = await auth.authenticate(authReq);
21
+ return principal !== null;
22
+ }
23
+ function hasGraph(store) {
24
+ return typeof store["queryFacts"] === "function";
25
+ }
26
+ function parseLimit(raw) {
27
+ if (raw === void 0) return void 0;
28
+ const n = Number(raw);
29
+ return Number.isFinite(n) && n > 0 ? Math.floor(n) : void 0;
30
+ }
31
+ function sessionUsage(events) {
32
+ let inputTokens = 0;
33
+ let outputTokens = 0;
34
+ let cachedInputTokens = 0;
35
+ let messages = 0;
36
+ for (const e of events) {
37
+ if (e.kind !== "assistant") continue;
38
+ messages++;
39
+ const usage = e.meta?.["usage"];
40
+ if (!usage) continue;
41
+ inputTokens += usage.inputTokens ?? 0;
42
+ outputTokens += usage.outputTokens ?? 0;
43
+ cachedInputTokens += usage.cachedInputTokens ?? 0;
44
+ }
45
+ return { inputTokens, outputTokens, cachedInputTokens, messages };
46
+ }
47
+ function usdForUsage(usage, prices, modelId) {
48
+ if (!modelId) return void 0;
49
+ const p = prices[modelId];
50
+ if (!p) return void 0;
51
+ const cachedToks = Math.min(usage.cachedInputTokens, usage.inputTokens);
52
+ const uncachedToks = usage.inputTokens - cachedToks;
53
+ const cacheRate = p.cachedInputPerMTok ?? p.inputPerMTok;
54
+ return (uncachedToks * p.inputPerMTok + cachedToks * cacheRate + usage.outputTokens * p.outputPerMTok) / 1e6;
55
+ }
56
+ function createStudioApi(opts) {
57
+ const auth = opts.auth ?? NoAuth;
58
+ const base = (opts.basePath ?? "/api").replace(/\/$/, "");
59
+ const app = new Hono({ strict: false });
60
+ const wfRegistry = createWorkflowRunRegistry({ limit: 100 });
61
+ app.get(`${base}/health`, (c) => c.json({ ok: true }));
62
+ app.use(`${base}/*`, async (c, next) => {
63
+ if (c.req.path === `${base}/health`) {
64
+ return next();
65
+ }
66
+ const ok = await runAuth(auth, c.req.raw);
67
+ if (!ok) return c.json({ error: "Unauthorized" }, 401);
68
+ return next();
69
+ });
70
+ app.get(`${base}/agents`, (c) => {
71
+ const agents = Object.entries(opts.agents).map(([id, agent]) => {
72
+ const store = agent.store;
73
+ const hasMemory = typeof store["getBlocks"] === "function";
74
+ const hasSkills = opts.skillBanks?.[id] !== void 0 || agent.skillCatalog().length > 0;
75
+ const hasGraphFeature = hasGraph(store);
76
+ return {
77
+ id,
78
+ hasMemory,
79
+ hasSkills,
80
+ hasGraph: hasGraphFeature,
81
+ toolCount: agent.toolSchemas().length
82
+ };
83
+ });
84
+ return c.json(agents);
85
+ });
86
+ app.get(`${base}/agents/:id`, (c) => {
87
+ const id = c.req.param("id");
88
+ const agent = opts.agents[id];
89
+ if (!agent) return c.json({ error: `Unknown agent: ${id}` }, 404);
90
+ const store = agent.store;
91
+ const hasMemory = typeof store["getBlocks"] === "function";
92
+ const hasSkillsFlag = opts.skillBanks?.[id] !== void 0 || agent.skillCatalog().length > 0;
93
+ const hasGraphFeature = hasGraph(store);
94
+ return c.json({
95
+ id,
96
+ instructions: agent.instructions,
97
+ model: agent.modelId,
98
+ tools: agent.toolSchemas(),
99
+ hasMemory,
100
+ hasSkills: hasSkillsFlag,
101
+ hasGraph: hasGraphFeature
102
+ });
103
+ });
104
+ app.get(`${base}/agents/:id/sessions`, async (c) => {
105
+ const id = c.req.param("id");
106
+ const agent = opts.agents[id];
107
+ if (!agent) return c.json({ error: `Unknown agent: ${id}` }, 404);
108
+ const limit = parseLimit(c.req.query("limit"));
109
+ const sessions = await agent.store.listSessions({ agentId: id, limit });
110
+ const priceTable = opts.prices ?? defaultPrices;
111
+ const modelId = agent.modelId;
112
+ const enriched = await Promise.all(
113
+ sessions.map(async (s) => {
114
+ try {
115
+ const events = await agent.store.readEvents(s.id);
116
+ const usage = sessionUsage(events);
117
+ const usd = usdForUsage(usage, priceTable, modelId);
118
+ return { ...s, usage, usd };
119
+ } catch {
120
+ return s;
121
+ }
122
+ })
123
+ );
124
+ return c.json(enriched);
125
+ });
126
+ app.get(`${base}/agents/:id/sessions/:sid/events`, async (c) => {
127
+ const id = c.req.param("id");
128
+ const agent = opts.agents[id];
129
+ if (!agent) return c.json({ error: `Unknown agent: ${id}` }, 404);
130
+ const sid = c.req.param("sid");
131
+ const events = await agent.store.readEvents(sid);
132
+ return c.json({ events });
133
+ });
134
+ app.get(`${base}/agents/:id/cost`, async (c) => {
135
+ const id = c.req.param("id");
136
+ const agent = opts.agents[id];
137
+ if (!agent) return c.json({ error: `Unknown agent: ${id}` }, 404);
138
+ const priceTable = opts.prices ?? defaultPrices;
139
+ const pricedFrom = opts.prices ? "configured" : "default";
140
+ const modelId = agent.modelId;
141
+ const sessions = await agent.store.listSessions({ agentId: id });
142
+ let totalInput = 0;
143
+ let totalOutput = 0;
144
+ let totalCached = 0;
145
+ let totalMessages = 0;
146
+ let totalUsd = 0;
147
+ let hasUsd = false;
148
+ for (const s of sessions) {
149
+ try {
150
+ const events = await agent.store.readEvents(s.id);
151
+ const usage = sessionUsage(events);
152
+ totalInput += usage.inputTokens;
153
+ totalOutput += usage.outputTokens;
154
+ totalCached += usage.cachedInputTokens;
155
+ totalMessages += usage.messages;
156
+ const usd = usdForUsage(usage, priceTable, modelId);
157
+ if (usd !== void 0) {
158
+ totalUsd += usd;
159
+ hasUsd = true;
160
+ }
161
+ } catch {
162
+ }
163
+ }
164
+ return c.json({
165
+ inputTokens: totalInput,
166
+ outputTokens: totalOutput,
167
+ cachedInputTokens: totalCached,
168
+ usd: hasUsd ? totalUsd : void 0,
169
+ messages: totalMessages,
170
+ sessions: sessions.length,
171
+ modelId: modelId ?? null,
172
+ pricedFrom,
173
+ pricesUpdatedAt
174
+ });
175
+ });
176
+ app.get(`${base}/agents/:id/blocks`, async (c) => {
177
+ const id = c.req.param("id");
178
+ const agent = opts.agents[id];
179
+ if (!agent) return c.json({ error: `Unknown agent: ${id}` }, 404);
180
+ const userId = c.req.query("userId");
181
+ const scope = userId ? { kind: "user", agentId: id, userId } : { kind: "agent", agentId: id };
182
+ const blocks = await agent.store.listBlocks(scope);
183
+ return c.json(blocks);
184
+ });
185
+ app.put(`${base}/agents/:id/blocks/:label`, async (c) => {
186
+ const id = c.req.param("id");
187
+ const agent = opts.agents[id];
188
+ if (!agent) return c.json({ error: `Unknown agent: ${id}` }, 404);
189
+ const label = c.req.param("label");
190
+ const userId = c.req.query("userId");
191
+ const scope = userId ? { kind: "user", agentId: id, userId } : { kind: "agent", agentId: id };
192
+ let body;
193
+ try {
194
+ body = await c.req.json();
195
+ } catch {
196
+ return c.json({ error: "Invalid JSON body" }, 400);
197
+ }
198
+ if (typeof body !== "object" || body === null) {
199
+ return c.json({ error: "Body must be a JSON object" }, 400);
200
+ }
201
+ const { value, expectVersion } = body;
202
+ if (typeof value !== "string") {
203
+ return c.json({ error: "Missing or invalid 'value' field (string required)" }, 400);
204
+ }
205
+ if (expectVersion !== void 0 && typeof expectVersion !== "number") {
206
+ return c.json({ error: "'expectVersion' must be a number when provided" }, 400);
207
+ }
208
+ try {
209
+ const block = await agent.store.upsertBlock(
210
+ scope,
211
+ { label, value },
212
+ typeof expectVersion === "number" ? expectVersion : void 0
213
+ );
214
+ return c.json(block);
215
+ } catch (err) {
216
+ if (err instanceof Error && /conflict/i.test(err.message)) {
217
+ return c.json({ error: "conflict", message: err.message }, 409);
218
+ }
219
+ throw err;
220
+ }
221
+ });
222
+ app.get(`${base}/agents/:id/facts`, async (c) => {
223
+ const id = c.req.param("id");
224
+ const agent = opts.agents[id];
225
+ if (!agent) return c.json({ error: `Unknown agent: ${id}` }, 404);
226
+ const userId = c.req.query("userId");
227
+ const subject = c.req.query("subject");
228
+ const scope = userId ? { kind: "user", agentId: id, userId } : { kind: "agent", agentId: id };
229
+ if (!hasGraph(agent.store)) {
230
+ return c.json([]);
231
+ }
232
+ const facts = await agent.store.queryFacts({ scope, ...subject ? { subject } : {} });
233
+ return c.json(facts);
234
+ });
235
+ app.get(`${base}/agents/:id/memories`, async (c) => {
236
+ const id = c.req.param("id");
237
+ const agent = opts.agents[id];
238
+ if (!agent) return c.json({ error: `Unknown agent: ${id}` }, 404);
239
+ const userId = c.req.query("userId");
240
+ const q = c.req.query("q") ?? "";
241
+ const scope = userId ? { kind: "user", agentId: id, userId } : { kind: "agent", agentId: id };
242
+ const snippets = await agent.store.searchMemory(scope, q, 20);
243
+ return c.json(snippets);
244
+ });
245
+ app.get(`${base}/agents/:id/skills`, (c) => {
246
+ const id = c.req.param("id");
247
+ const agent = opts.agents[id];
248
+ if (!agent) return c.json({ error: `Unknown agent: ${id}` }, 404);
249
+ const promptSkills = agent.skillCatalog().map((s) => ({
250
+ name: s.name,
251
+ description: s.description,
252
+ type: "prompt"
253
+ }));
254
+ const bank = opts.skillBanks?.[id];
255
+ const bankSkills = (bank ? bank.list() : []).map((rawLock) => {
256
+ const lock = rawLock;
257
+ return {
258
+ name: lock["name"] ?? lock["skillId"] ?? "?",
259
+ description: lock["description"] ?? "",
260
+ type: "executable",
261
+ quarantined: !!lock["quarantined"],
262
+ version: lock["version"],
263
+ author: lock["author"]
264
+ };
265
+ });
266
+ return c.json([...promptSkills, ...bankSkills]);
267
+ });
268
+ app.post(`${base}/agents/:id/skills/:skillId/approve`, (c) => {
269
+ const id = c.req.param("id");
270
+ if (!opts.agents[id]) return c.json({ error: `Unknown agent: ${id}` }, 404);
271
+ const bank = opts.skillBanks?.[id];
272
+ if (!bank) return c.json({ error: "No skill bank configured for this agent" }, 404);
273
+ const skillId = c.req.param("skillId");
274
+ const ok = bank.approve(skillId);
275
+ if (!ok) return c.json({ error: `Skill not found: ${skillId}` }, 404);
276
+ return c.json({ approved: true, name: skillId });
277
+ });
278
+ app.get(`${base}/workflows`, (c) => {
279
+ const runs = wfRegistry.list();
280
+ const summaries = runs.map((r) => ({
281
+ id: r.id,
282
+ name: r.name,
283
+ status: r.status,
284
+ startedAt: r.startedAt,
285
+ durationMs: r.durationMs,
286
+ stepCount: r.stepCount
287
+ }));
288
+ return c.json(summaries);
289
+ });
290
+ app.get(`${base}/workflows/:wid`, (c) => {
291
+ const wid = c.req.param("wid");
292
+ const run = wfRegistry.get(wid);
293
+ if (!run) return c.json({ error: `Unknown workflow run: ${wid}` }, 404);
294
+ return c.json(run);
295
+ });
296
+ app.post(`${base}/workflows`, async (c) => {
297
+ let body;
298
+ try {
299
+ body = await c.req.json();
300
+ } catch {
301
+ return c.json({ error: "Invalid JSON body" }, 400);
302
+ }
303
+ if (typeof body !== "object" || body === null) {
304
+ return c.json({ error: "Body must be a JSON object" }, 400);
305
+ }
306
+ const b = body;
307
+ const name = b["name"];
308
+ if (typeof name !== "string" || !name) {
309
+ return c.json({ error: "Missing or invalid 'name' field (non-empty string required)" }, 400);
310
+ }
311
+ const trace = b["trace"];
312
+ if (!Array.isArray(trace)) {
313
+ return c.json({ error: "Missing or invalid 'trace' field (array required)" }, 400);
314
+ }
315
+ const result = {
316
+ output: b["output"],
317
+ trace
318
+ };
319
+ const record = wfRegistry.record(name, result);
320
+ return c.json({ id: record.id, name: record.name, status: record.status }, 201);
321
+ });
322
+ const handle = app;
323
+ handle.recordWorkflow = function recordWorkflow(name, result) {
324
+ wfRegistry.record(name, result);
325
+ };
326
+ return handle;
327
+ }
328
+ function createStudio(opts) {
329
+ const run = createServer({
330
+ agents: opts.agents,
331
+ auth: opts.auth,
332
+ exposeEvents: true
333
+ });
334
+ const studioHandle = createStudioApi(opts);
335
+ const combined = new Hono({ strict: false });
336
+ combined.route("/", run);
337
+ combined.route("/", studioHandle);
338
+ const handle = combined;
339
+ handle.recordWorkflow = studioHandle.recordWorkflow.bind(studioHandle);
340
+ return handle;
341
+ }
342
+ async function serveStudio(opts, { port = 3535 } = {}) {
343
+ let uiDist;
344
+ const metaUrl = import.meta.url;
345
+ if (metaUrl) {
346
+ const _dirname = dirname(fileURLToPath(metaUrl));
347
+ uiDist = join(_dirname, "..", "ui", "dist");
348
+ } else {
349
+ const cjsDirname = typeof __dirname !== "undefined" ? __dirname : process.cwd();
350
+ uiDist = join(cjsDirname, "..", "ui", "dist");
351
+ }
352
+ const handle = createStudio(opts);
353
+ handle.use(
354
+ "/*",
355
+ serveStatic({
356
+ root: uiDist,
357
+ rewriteRequestPath: (path) => {
358
+ return path === "/" ? "/index.html" : path;
359
+ }
360
+ })
361
+ );
362
+ handle.get("/*", serveStatic({ root: uiDist, path: "/index.html" }));
363
+ return serveNode(handle, { port });
364
+ }
365
+ export {
366
+ ApiKeyAuth,
367
+ NoAuth,
368
+ createStudio,
369
+ createStudioApi,
370
+ serveNode,
371
+ serveStudio
372
+ };
package/package.json ADDED
@@ -0,0 +1,71 @@
1
+ {
2
+ "name": "@eidentic/studio",
3
+ "version": "0.1.0",
4
+ "type": "module",
5
+ "license": "Apache-2.0",
6
+ "publishConfig": {
7
+ "access": "public"
8
+ },
9
+ "repository": {
10
+ "type": "git",
11
+ "url": "git+https://github.com/eidentic/eidentic.git",
12
+ "directory": "packages/studio"
13
+ },
14
+ "main": "./dist/index.cjs",
15
+ "module": "./dist/index.js",
16
+ "types": "./dist/index.d.ts",
17
+ "exports": {
18
+ ".": {
19
+ "types": "./dist/index.d.ts",
20
+ "import": "./dist/index.js",
21
+ "require": "./dist/index.cjs"
22
+ }
23
+ },
24
+ "files": [
25
+ "dist",
26
+ "ui/dist",
27
+ "LICENSE",
28
+ "README.md"
29
+ ],
30
+ "sideEffects": false,
31
+ "dependencies": {
32
+ "@hono/node-server": "^2.0.0",
33
+ "hono": "^4.12.0",
34
+ "@eidentic/core": "0.1.0",
35
+ "@eidentic/model": "0.1.0",
36
+ "@eidentic/types": "0.1.0",
37
+ "@eidentic/server": "0.1.0",
38
+ "@eidentic/workflow": "0.1.0"
39
+ },
40
+ "devDependencies": {
41
+ "@vitejs/plugin-react": "^4.5.2",
42
+ "@types/react": "^19.1.0",
43
+ "@types/react-dom": "^19.1.0",
44
+ "react": "^19.1.0",
45
+ "react-dom": "^19.1.0",
46
+ "vite": "^6.3.5"
47
+ },
48
+ "description": "Local dev studio for Eidentic — inspect agent sessions, memory, skills, and workflow runs via a web dashboard.",
49
+ "keywords": [
50
+ "ai",
51
+ "agents",
52
+ "typescript",
53
+ "eidentic",
54
+ "studio",
55
+ "dashboard",
56
+ "devtools",
57
+ "debug"
58
+ ],
59
+ "homepage": "https://github.com/eidentic/eidentic#readme",
60
+ "bugs": {
61
+ "url": "https://github.com/eidentic/eidentic/issues"
62
+ },
63
+ "engines": {
64
+ "node": ">=22"
65
+ },
66
+ "scripts": {
67
+ "build": "cd ui && vite build && cd .. && tsup src/index.ts --format esm,cjs --dts --clean",
68
+ "build:ts": "tsup src/index.ts --format esm,cjs --dts --clean",
69
+ "typecheck": "tsc --noEmit"
70
+ }
71
+ }