robodev 0.34.0 → 0.35.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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "robodev",
3
- "version": "0.34.0",
3
+ "version": "0.35.0",
4
4
  "description": "CLI for Robodev Starbase — create, auth, link, deploy, and run a project locally with robodev dev",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -22,14 +22,16 @@
22
22
  },
23
23
  "dependencies": {
24
24
  "@fastify/cors": "^11.3.0",
25
+ "@fastify/websocket": "^11.2.0",
25
26
  "embedded-postgres": "17.10.0-beta.17",
26
27
  "fastify": "^5.12.1",
27
28
  "tsx": "^4.20.5",
28
- "@robodev-ai/runtime": "0.3.0",
29
+ "@robodev-ai/runtime": "0.4.0",
29
30
  "@robodev-ai/sdk": "0.11.0"
30
31
  },
31
32
  "devDependencies": {
32
33
  "@types/node": "^24.3.0",
34
+ "@types/ws": "^8.18.1",
33
35
  "typescript": "^5.9.2"
34
36
  },
35
37
  "scripts": {
@@ -1,18 +1,20 @@
1
+ import {
2
+ createLocalStorageClient,
3
+ createProjectSocketsClient,
4
+ type StorageQueryable,
5
+ } from "@robodev-ai/runtime";
1
6
  import type { RouteClients } from "@robodev-ai/runtime";
2
7
  import type { EmailClient, JobEnqueueInput, LlmClient } from "@robodev-ai/sdk";
3
8
  import type { DevInspectStore } from "./dev-inspect.js";
4
9
 
5
10
  /**
6
11
  * The `ctx.*` clients `robodev dev` can offer offline. `email` prints to the terminal;
7
- * `jobs` enqueues onto the local engine; everything else that needs Starbase
8
- * infrastructure fails with a message that says so rather than silently doing nothing.
12
+ * `jobs`, `storage`, and `sockets` run locally. llm / agent / push throw hosted-style
13
+ * not-configured codes rather than silently doing nothing.
9
14
  */
10
15
 
11
- function unavailable(feature: string): never {
12
- throw Object.assign(
13
- new Error(`ctx.${feature} needs hosted Starbase. Run robodev deploy to use it.`),
14
- { statusCode: 501 },
15
- );
16
+ function notConfigured(code: string): never {
17
+ throw Object.assign(new Error(code), { statusCode: 400, code });
16
18
  }
17
19
 
18
20
  function errorMessage(error: unknown): string {
@@ -37,8 +39,8 @@ export function consoleEmailClient(
37
39
 
38
40
  function offlineLlmClient(): LlmClient {
39
41
  return {
40
- complete: () => unavailable("llm"),
41
- stream: () => unavailable("llm"),
42
+ complete: () => notConfigured("llm_not_configured"),
43
+ stream: () => notConfigured("llm_not_configured"),
42
44
  };
43
45
  }
44
46
 
@@ -47,25 +49,30 @@ export function devRouteClients(input: {
47
49
  log: (line: string) => void;
48
50
  enqueue: (job: JobEnqueueInput) => Promise<{ id: string }>;
49
51
  inspect: DevInspectStore;
52
+ query: StorageQueryable["query"];
53
+ objectsDir: string;
54
+ publicBaseUrl: string;
55
+ projectId: string;
56
+ jwtSecret: string;
50
57
  }): RouteClients {
51
58
  return {
52
59
  email: consoleEmailClient(input.log, input.inspect),
53
60
  llm: offlineLlmClient(),
54
61
  agent: {
55
- createSession: () => unavailable("agent"),
56
- start: () => unavailable("agent"),
57
- events: () => unavailable("agent"),
58
- subscribe: () => unavailable("agent"),
59
- destroy: () => unavailable("agent"),
62
+ createSession: () => notConfigured("llm_not_configured"),
63
+ start: () => notConfigured("llm_not_configured"),
64
+ events: () => notConfigured("llm_not_configured"),
65
+ subscribe: () => notConfigured("llm_not_configured"),
66
+ destroy: () => notConfigured("llm_not_configured"),
60
67
  },
61
- storage: {
62
- upload: () => unavailable("storage"),
63
- get: () => unavailable("storage"),
64
- getUrl: () => unavailable("storage"),
65
- delete: () => unavailable("storage"),
66
- list: () => unavailable("storage"),
67
- },
68
- push: { send: () => unavailable("push") },
68
+ storage: createLocalStorageClient({
69
+ query: input.query,
70
+ objectsDir: input.objectsDir,
71
+ publicBaseUrl: input.publicBaseUrl,
72
+ projectId: input.projectId,
73
+ jwtSecret: input.jwtSecret,
74
+ }),
75
+ push: { send: () => notConfigured("push_not_configured") },
69
76
  jobs: {
70
77
  enqueue: async (job) => {
71
78
  try {
@@ -88,10 +95,7 @@ export function devRouteClients(input: {
88
95
  }
89
96
  },
90
97
  },
91
- sockets: {
92
- send: () => unavailable("sockets"),
93
- broadcast: () => unavailable("sockets"),
94
- },
98
+ sockets: createProjectSocketsClient(input.projectId),
95
99
  env: input.env,
96
100
  };
97
101
  }
package/src/dev-server.ts CHANGED
@@ -1,19 +1,35 @@
1
1
  import { Readable } from "node:stream";
2
2
  import cors from "@fastify/cors";
3
- import Fastify, { type FastifyInstance, type FastifyRequest } from "fastify";
3
+ import websocket from "@fastify/websocket";
4
+ import Fastify, { type FastifyInstance, type FastifyReply, type FastifyRequest } from "fastify";
5
+ import type { WebSocket } from "ws";
4
6
  import {
7
+ SOCKET_CLOSE_POLICY,
8
+ SOCKET_IDLE_PING_MS,
9
+ bindSocketSession,
5
10
  buildOpenApi,
6
11
  handleLocalAuth,
7
12
  isLocalAuthPath,
13
+ isReservedStoragePath,
8
14
  matchProjectRoute,
15
+ matchSocketRoute,
16
+ parseLogicalKey,
17
+ resolveLocalObject,
9
18
  resolveLocalUser,
10
19
  runRoute,
11
20
  scalarHtml,
12
21
  swaggerHtml,
22
+ verifyStorageServeToken,
13
23
  type AuthQueryable,
14
24
  type RuntimeRoute,
25
+ type RuntimeSocket,
15
26
  } from "@robodev-ai/runtime";
16
- import type { JobEnqueueInput, RobodevDb } from "@robodev-ai/sdk";
27
+ import type {
28
+ JobEnqueueInput,
29
+ RobodevDb,
30
+ SocketDefinition,
31
+ SocketHandlerContext,
32
+ } from "@robodev-ai/sdk";
17
33
  import type { RuntimeJob } from "@robodev-ai/runtime";
18
34
  import { devRouteClients } from "./dev-clients.js";
19
35
  import { isInspectPath, type DevInspectStore } from "./dev-inspect.js";
@@ -24,6 +40,7 @@ export type DevRuntime = {
24
40
  pool: AuthQueryable & { end: () => Promise<void> };
25
41
  routes: RuntimeRoute[];
26
42
  jobs: RuntimeJob[];
43
+ sockets: RuntimeSocket[];
27
44
  openapi: Record<string, unknown>;
28
45
  };
29
46
 
@@ -39,6 +56,7 @@ export type DevServerOptions = {
39
56
  brokerBaseUrl: string;
40
57
  googleClientId?: string;
41
58
  googleClientSecret?: string;
59
+ storageDir: string;
42
60
  };
43
61
 
44
62
  const SWAGGER_PATHS = new Set(["/_robodev/docs", "/_robodev/swagger", "/docs", "/swagger"]);
@@ -57,6 +75,238 @@ function rawBodyOf(request: FastifyRequest): Buffer | undefined {
57
75
  return (request as FastifyRequest & { rawBody?: Buffer }).rawBody;
58
76
  }
59
77
 
78
+ function queryRecord(query: unknown): Record<string, string> {
79
+ if (!query || typeof query !== "object") return {};
80
+ const out: Record<string, string> = {};
81
+ for (const [key, value] of Object.entries(query as Record<string, unknown>)) {
82
+ if (typeof value === "string") out[key] = value;
83
+ else if (Array.isArray(value) && typeof value[0] === "string") out[key] = value[0];
84
+ }
85
+ return out;
86
+ }
87
+
88
+ function socketAuthorization(request: FastifyRequest): string | undefined {
89
+ const header = request.headers.authorization;
90
+ if (typeof header === "string" && header.length > 0) return header;
91
+ const token = queryRecord(request.query).accessToken;
92
+ if (token) return `Bearer ${token}`;
93
+ return undefined;
94
+ }
95
+
96
+ function codedErrorBody(error: unknown): { status: number; body: Record<string, unknown> } | null {
97
+ if (!error || typeof error !== "object" || !("statusCode" in error)) return null;
98
+ const statusCode = (error as { statusCode: unknown }).statusCode;
99
+ if (typeof statusCode !== "number") return null;
100
+ const code = (error as { code?: unknown }).code;
101
+ const message = error instanceof Error ? error.message : String(error);
102
+ return {
103
+ status: statusCode,
104
+ body: {
105
+ error: typeof code === "string" ? code : message,
106
+ message,
107
+ },
108
+ };
109
+ }
110
+
111
+ function routeClients(options: DevServerOptions, getRuntime: () => DevRuntime | null) {
112
+ return devRouteClients({
113
+ env: options.env,
114
+ log: options.log,
115
+ enqueue: options.enqueue,
116
+ inspect: options.inspect,
117
+ query: (sql, values) => {
118
+ const runtime = getRuntime();
119
+ if (!runtime) throw new Error("dev runtime not ready");
120
+ return runtime.pool.query(sql, values);
121
+ },
122
+ objectsDir: options.storageDir,
123
+ publicBaseUrl: options.listenOrigin,
124
+ projectId: options.projectId,
125
+ jwtSecret: options.jwtSecret,
126
+ });
127
+ }
128
+
129
+ async function resolveSocketUser(
130
+ options: DevServerOptions,
131
+ def: SocketDefinition,
132
+ request: FastifyRequest,
133
+ ): Promise<{ ok: true; user: SocketHandlerContext["user"] } | { ok: false }> {
134
+ const auth = def.auth ?? false;
135
+ if (auth === false) return { ok: true, user: null };
136
+ if (typeof auth === "function") {
137
+ try {
138
+ const user = await auth({ headers: request.headers });
139
+ if (!user) return { ok: false };
140
+ return { ok: true, user };
141
+ } catch {
142
+ return { ok: false };
143
+ }
144
+ }
145
+ if (auth !== "required" && auth !== "optional") return { ok: false };
146
+ const user = await resolveLocalUser(
147
+ { projectId: options.projectId, jwtSecret: options.jwtSecret },
148
+ socketAuthorization(request),
149
+ );
150
+ if (auth === "required" && !user) return { ok: false };
151
+ return { ok: true, user };
152
+ }
153
+
154
+ async function handleLocalSocket(
155
+ socket: WebSocket,
156
+ request: FastifyRequest,
157
+ options: DevServerOptions,
158
+ getRuntime: () => DevRuntime | null,
159
+ ): Promise<void> {
160
+ const runtime = getRuntime();
161
+ if (!runtime) {
162
+ socket.close(SOCKET_CLOSE_POLICY, "not_found");
163
+ return;
164
+ }
165
+ const pathname = pathnameOf(request.url);
166
+ const matched = matchSocketRoute(pathname, runtime.sockets);
167
+ if (!matched) {
168
+ socket.close(SOCKET_CLOSE_POLICY, "not_found");
169
+ return;
170
+ }
171
+
172
+ const auth = await resolveSocketUser(options, matched.socket.def, request);
173
+ if (!auth.ok) {
174
+ socket.close(SOCKET_CLOSE_POLICY, "unauthorized");
175
+ return;
176
+ }
177
+
178
+ const clients = routeClients(options, getRuntime);
179
+ const session = await bindSocketSession({
180
+ projectId: options.projectId,
181
+ name: matched.socket.name,
182
+ socket: {
183
+ send: (data) => socket.send(data),
184
+ close: (code, reason) => socket.close(code, reason),
185
+ get readyState() {
186
+ return socket.readyState;
187
+ },
188
+ },
189
+ timeoutMs: matched.socket.def.timeoutMs,
190
+ createContext: ({ send, close, room }) => ({
191
+ db: runtime.db,
192
+ email: clients.email,
193
+ llm: clients.llm,
194
+ agent: clients.agent,
195
+ storage: clients.storage,
196
+ env: clients.env,
197
+ params: matched.params,
198
+ query: queryRecord(request.query),
199
+ headers: request.headers,
200
+ user: auth.user,
201
+ push: clients.push,
202
+ jobs: clients.jobs,
203
+ sockets: clients.sockets,
204
+ send,
205
+ close,
206
+ room,
207
+ }),
208
+ onConnect: matched.socket.def.onConnect,
209
+ onMessage: matched.socket.def.onMessage,
210
+ onClose: matched.socket.def.onClose,
211
+ });
212
+ if (!session.ok) {
213
+ if (socket.readyState === socket.OPEN) {
214
+ socket.close(session.code, session.reason);
215
+ }
216
+ return;
217
+ }
218
+
219
+ let alive = true;
220
+ const pingTimer = setInterval(() => {
221
+ if (socket.readyState !== socket.OPEN) {
222
+ clearInterval(pingTimer);
223
+ return;
224
+ }
225
+ if (!alive) {
226
+ socket.close(SOCKET_CLOSE_POLICY, "idle");
227
+ return;
228
+ }
229
+ alive = false;
230
+ try {
231
+ socket.ping();
232
+ } catch {
233
+ socket.close(SOCKET_CLOSE_POLICY, "idle");
234
+ }
235
+ }, SOCKET_IDLE_PING_MS);
236
+ pingTimer.unref?.();
237
+
238
+ socket.on("pong", () => {
239
+ alive = true;
240
+ });
241
+ socket.on("message", (data, isBinary) => {
242
+ if (isBinary) {
243
+ session.onBinary();
244
+ return;
245
+ }
246
+ const text = typeof data === "string" ? data : Buffer.from(data as Uint8Array).toString("utf8");
247
+ session.onText(text);
248
+ });
249
+ socket.on("close", () => {
250
+ clearInterval(pingTimer);
251
+ session.onClose();
252
+ });
253
+ }
254
+
255
+ async function serveLocalStorage(
256
+ request: FastifyRequest,
257
+ reply: FastifyReply,
258
+ options: DevServerOptions,
259
+ getRuntime: () => DevRuntime | null,
260
+ pathname: string,
261
+ ): Promise<void> {
262
+ const runtime = getRuntime();
263
+ if (!runtime) {
264
+ await reply.code(503).send({ error: "not_ready" });
265
+ return;
266
+ }
267
+ const encodedKey = pathname.slice("/storage/objects/".length);
268
+ let key: string;
269
+ try {
270
+ key = decodeURIComponent(encodedKey);
271
+ parseLogicalKey(key);
272
+ } catch (error) {
273
+ const mapped = codedErrorBody(error);
274
+ if (mapped) {
275
+ await reply.code(mapped.status).send(mapped.body);
276
+ return;
277
+ }
278
+ await reply.code(400).send({ error: "invalid_key" });
279
+ return;
280
+ }
281
+
282
+ const resolved = await resolveLocalObject({
283
+ query: (sql, values) => runtime.pool.query(sql, values),
284
+ objectsDir: options.storageDir,
285
+ key,
286
+ });
287
+ if (!resolved) {
288
+ await reply.code(404).send({ error: "not_found" });
289
+ return;
290
+ }
291
+
292
+ const user = await resolveLocalUser(
293
+ { projectId: options.projectId, jwtSecret: options.jwtSecret },
294
+ request.headers.authorization,
295
+ );
296
+ const signed = verifyStorageServeToken(
297
+ options.jwtSecret,
298
+ options.projectId,
299
+ key,
300
+ request.query as { exp?: unknown; sig?: unknown },
301
+ );
302
+ if (!resolved.row.public && !user && !signed) {
303
+ await reply.code(404).send({ error: "not_found" });
304
+ return;
305
+ }
306
+
307
+ await reply.type(resolved.row.content_type).send(resolved.body);
308
+ }
309
+
60
310
  /**
61
311
  * A single-project HTTP host. Unlike the hosted project host there is no `Host` lookup
62
312
  * and no static SPA serving (local Vite does that). Fastify stays `logger: false`;
@@ -74,6 +324,10 @@ export async function startDevServer(
74
324
  credentials: true,
75
325
  methods: ["GET", "HEAD", "POST", "PUT", "PATCH", "DELETE", "OPTIONS"],
76
326
  });
327
+ await app.register(websocket);
328
+ app.get("/sockets/*", { websocket: true }, (socket, request) => {
329
+ void handleLocalSocket(socket, request, options, getRuntime);
330
+ });
77
331
 
78
332
  app.addContentTypeParser("application/json", { parseAs: "string" }, (_request, body, done) => {
79
333
  if (!body) {
@@ -103,13 +357,16 @@ export async function startDevServer(
103
357
  );
104
358
 
105
359
  // `rawBody` handlers and multipart parsing need the untouched bytes.
106
- app.addHook("preParsing", async (_request, _reply, payload) => {
360
+ app.addHook("preParsing", async (request, _reply, payload) => {
361
+ if (String(request.headers.upgrade ?? "").toLowerCase() === "websocket") {
362
+ return payload;
363
+ }
107
364
  const chunks: Buffer[] = [];
108
365
  for await (const chunk of payload) {
109
366
  chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
110
367
  }
111
368
  const raw = Buffer.concat(chunks);
112
- (_request as FastifyRequest & { rawBody?: Buffer }).rawBody = raw;
369
+ (request as FastifyRequest & { rawBody?: Buffer }).rawBody = raw;
113
370
  return Readable.from(raw);
114
371
  });
115
372
 
@@ -214,6 +471,11 @@ export async function startDevServer(
214
471
  return;
215
472
  }
216
473
 
474
+ if (request.method.toUpperCase() === "GET" && isReservedStoragePath(pathname)) {
475
+ await serveLocalStorage(request, reply, options, getRuntime, pathname);
476
+ return;
477
+ }
478
+
217
479
  if (!matchProjectRoute(pathname, method, runtime.routes)) {
218
480
  await reply.code(404).send({
219
481
  error: "not_found",
@@ -238,13 +500,8 @@ export async function startDevServer(
238
500
  { projectId: options.projectId, jwtSecret: options.jwtSecret },
239
501
  authorization,
240
502
  ),
241
- clients: () =>
242
- devRouteClients({
243
- env: options.env,
244
- log: options.log,
245
- enqueue: options.enqueue,
246
- inspect: options.inspect,
247
- }),
503
+ clients: () => routeClients(options, getRuntime),
504
+ mapError: (error) => codedErrorBody(error),
248
505
  });
249
506
 
250
507
  if (outcome.kind === "error") {
package/src/dev.ts CHANGED
@@ -9,9 +9,13 @@ import {
9
9
  assertJobCrons,
10
10
  buildPlan,
11
11
  compileApiTree,
12
+ closeProjectSockets,
12
13
  createLocalJobsEngine,
13
14
  ensureAuthSchema,
14
15
  ensureJobsSchema,
16
+ ensureStorageSchema,
17
+ setProjectSocketNames,
18
+ SOCKET_CLOSE_RESTART,
15
19
  isEsbuildFailure,
16
20
  loadModules,
17
21
  mapEsbuildErrors,
@@ -154,6 +158,8 @@ export async function main(args: string[]): Promise<void> {
154
158
  const postgres: DevPostgres = parsed.databaseUrl
155
159
  ? externalDevPostgres(parsed.databaseUrl)
156
160
  : await startDevPostgres({ dataDir: join(devDir, "pg"), port: parsed.dbPort, quiet: true });
161
+ const storageDir = join(devDir, "storage");
162
+ await mkdir(storageDir, { recursive: true });
157
163
 
158
164
  let current: DevRuntime | null = null;
159
165
  let lastSchemaSource = "";
@@ -172,7 +178,20 @@ export async function main(args: string[]): Promise<void> {
172
178
  getGeneration: () => current,
173
179
  log,
174
180
  clients: () =>
175
- devRouteClients({ env, log, inspect, enqueue: (input) => engine.enqueue(input) }),
181
+ devRouteClients({
182
+ env,
183
+ log,
184
+ inspect,
185
+ enqueue: (input) => engine.enqueue(input),
186
+ query: (sql, values) => {
187
+ if (!current) throw new Error("dev runtime not ready");
188
+ return current.pool.query(sql, values);
189
+ },
190
+ objectsDir: storageDir,
191
+ publicBaseUrl: `http://localhost:${parsed.port}`,
192
+ projectId: DEV_PROJECT_ID,
193
+ jwtSecret: DEV_JWT_SECRET,
194
+ }),
176
195
  });
177
196
 
178
197
  async function compileAndActivate(input: { syncSchema: boolean }): Promise<void> {
@@ -222,15 +241,22 @@ export async function main(args: string[]): Promise<void> {
222
241
  await pool.query("SELECT 1");
223
242
  await ensureAuthSchema(pool);
224
243
  await ensureJobsSchema(pool);
244
+ await ensureStorageSchema(pool);
225
245
 
246
+ closeProjectSockets(DEV_PROJECT_ID, SOCKET_CLOSE_RESTART, "deployed");
226
247
  const previous = current;
227
248
  current = {
228
249
  db,
229
250
  pool,
230
251
  routes: loaded.routes,
231
252
  jobs: loaded.jobs,
253
+ sockets: loaded.sockets,
232
254
  openapi: buildDevOpenApi(loaded.database.name, loaded.routes),
233
255
  };
256
+ setProjectSocketNames(
257
+ DEV_PROJECT_ID,
258
+ loaded.sockets.map((socket) => socket.name),
259
+ );
234
260
  await engine.reconcile(loaded.jobs);
235
261
 
236
262
  for (const route of loaded.routes) {
@@ -243,10 +269,8 @@ export async function main(args: string[]): Promise<void> {
243
269
  log(` job ${job.name}`);
244
270
  }
245
271
  }
246
- if (loaded.sockets.length > 0) {
247
- log(
248
- ` ${loaded.sockets.length} socket(s) compiled but not served — sockets need hosted Starbase.`,
249
- );
272
+ for (const socket of loaded.sockets) {
273
+ log(` WS ${socket.path}`);
250
274
  }
251
275
  if (previous) {
252
276
  await previous.pool.end().catch(() => undefined);
@@ -275,6 +299,7 @@ export async function main(args: string[]): Promise<void> {
275
299
  brokerBaseUrl: starbaseUrl(),
276
300
  googleClientId: env.GOOGLE_CLIENT_ID,
277
301
  googleClientSecret: env.GOOGLE_CLIENT_SECRET,
302
+ storageDir,
278
303
  },
279
304
  () => current,
280
305
  );
@@ -295,6 +320,7 @@ export async function main(args: string[]): Promise<void> {
295
320
  `Postgres ${postgres.host}:${postgres.port}/${postgres.database} (--database-url)`,
296
321
  );
297
322
  }
323
+ console.log(`Storage ${relative(project.root, storageDir) || storageDir}`);
298
324
  console.log(`API ${apiUrl}`);
299
325
  console.log(`Swagger ${apiUrl}/_robodev/docs`);
300
326
  console.log(`Inspect ${apiUrl}/_robodev/logs`);
@@ -352,6 +378,7 @@ export async function main(args: string[]): Promise<void> {
352
378
  async function shutdown(code: number): Promise<void> {
353
379
  if (closing) return;
354
380
  closing = true;
381
+ closeProjectSockets(DEV_PROJECT_ID);
355
382
  await engine.stop();
356
383
  for (const watcher of watchers) watcher.close();
357
384
  fe?.kill("SIGTERM");
@@ -12,7 +12,7 @@
12
12
  "@robodev-ai/sdk": "^0.11.0"
13
13
  },
14
14
  "devDependencies": {
15
- "robodev": "^0.34.0",
15
+ "robodev": "^0.35.0",
16
16
  "rulesync": "^8.18.0",
17
17
  "typescript": "^5.9.2"
18
18
  }
@@ -11,7 +11,7 @@
11
11
  "@robodev-ai/sdk": "^0.11.0"
12
12
  },
13
13
  "devDependencies": {
14
- "robodev": "^0.34.0",
14
+ "robodev": "^0.35.0",
15
15
  "typescript": "^5.9.2"
16
16
  }
17
17
  }
@@ -11,7 +11,7 @@
11
11
  "@robodev-ai/sdk": "^0.11.0"
12
12
  },
13
13
  "devDependencies": {
14
- "robodev": "^0.34.0",
14
+ "robodev": "^0.35.0",
15
15
  "typescript": "^5.9.2"
16
16
  }
17
17
  }
@@ -12,7 +12,7 @@
12
12
  "@robodev-ai/sdk": "^0.11.0"
13
13
  },
14
14
  "devDependencies": {
15
- "robodev": "^0.34.0",
15
+ "robodev": "^0.35.0",
16
16
  "rulesync": "^8.18.0",
17
17
  "typescript": "^5.9.2"
18
18
  }
@@ -12,7 +12,7 @@
12
12
  "@robodev-ai/sdk": "^0.11.0"
13
13
  },
14
14
  "devDependencies": {
15
- "robodev": "^0.34.0",
15
+ "robodev": "^0.35.0",
16
16
  "rulesync": "^8.18.0",
17
17
  "typescript": "^5.9.2"
18
18
  }