railcode 0.1.17 → 0.1.19

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/dist/index.js CHANGED
@@ -4382,8 +4382,9 @@ class CliError extends Error {
4382
4382
  // dev — local development server (`railcode dev`)
4383
4383
  //
4384
4384
  // Serves the app on a single loopback origin and emulates the /_api/* data plane:
4385
- // identity is synthetic, KV + files live on local disk. llm/sql/connections proxy
4386
- // to the real instance (added in a later milestone). Mirrors railcode-core's dev.
4385
+ // identity is synthetic, KV + files live on local disk. llm/sql/queries/connectors/email
4386
+ // proxy to the real instance as the signed-in user (their token + org grants) via the
4387
+ // org/user-scoped routes — no deploy needed. Mirrors railcode-core's dev.
4387
4388
  // ---------------------------------------------------------------------------
4388
4389
  const DEFAULT_DEV_PORT = 7331;
4389
4390
  const DEFAULT_ASSET_PORT = 5173;
@@ -4922,8 +4923,10 @@ async function handleLocalApi(ctx, req, res, url) {
4922
4923
  path === "/llm/generate" ||
4923
4924
  path === "/llm/stream" ||
4924
4925
  path === "/llm/providers" ||
4926
+ path === "/email" ||
4927
+ path === "/email/send" ||
4925
4928
  path === "/service-connectors" ||
4926
- path === "/service-connectors/request") {
4929
+ path.startsWith("/service-connectors/")) {
4927
4930
  await forwardCompute(ctx, req, res, path, url);
4928
4931
  return;
4929
4932
  }
@@ -5113,27 +5116,29 @@ function devCreds(config) {
5113
5116
  return null;
5114
5117
  return { apiUrl: normalizeApiOrigin(apiUrl), token, orgUuid };
5115
5118
  }
5116
- // Resolve the server-side app for proxying lazily and memoized. Local dev must
5117
- // not create product apps: an app becomes real on first successful deploy, not on
5118
- // a dev-time SQL/LLM/connector call.
5119
- function resolveDevAppUuid(ctx, creds) {
5120
- const base = `${creds.apiUrl}/api/organizations/${creds.orgUuid}/apps`;
5121
- const authed = { Authorization: `Bearer ${creds.token}` };
5122
- if (!ctx.appUuidPromise) {
5123
- ctx.appUuidPromise = (async () => {
5124
- try {
5125
- const resp = await fetch(base, { headers: authed });
5126
- if (!resp.ok)
5127
- return null;
5128
- const apps = (await resp.json());
5129
- return apps.find((a) => a.app_slug === ctx.app && a.status === "active")?.uuid ?? null;
5130
- }
5131
- catch {
5132
- return null;
5133
- }
5134
- })();
5135
- }
5136
- return ctx.appUuidPromise;
5119
+ // Map a local-dev /_api compute path to the real org/user-scoped upstream URL. Local
5120
+ // dev invokes compute as the signed-in user (their personal token + org grants), NOT
5121
+ // as a deployed app — so no `railcode deploy` is needed first. The app slug still
5122
+ // namespaces local KV/files, but plays no part here. Deployed apps keep their own
5123
+ // bearer app-scoped routes (.../apps/{app}/...); this is deliberately the user plane.
5124
+ export function devComputeTarget(creds, path, search) {
5125
+ const org = `${creds.apiUrl}/api/organizations/${creds.orgUuid}`;
5126
+ if (path === "/connections")
5127
+ return `${org}/data/connections${search}`;
5128
+ if (path === "/sql")
5129
+ return `${org}/data/sql${search}`;
5130
+ if (path === "/queries" || path.startsWith("/queries/"))
5131
+ return `${org}/data${path}${search}`;
5132
+ if (path === "/service-connectors" || path.startsWith("/service-connectors/")) {
5133
+ return `${org}/data${path}${search}`;
5134
+ }
5135
+ if (path === "/llm/providers" || path === "/llm/generate" || path === "/llm/stream") {
5136
+ return `${org}${path}${search}`;
5137
+ }
5138
+ if (path === "/email" || path === "/email/send") {
5139
+ return `${org}${path}${search}`;
5140
+ }
5141
+ return null;
5137
5142
  }
5138
5143
  export function devUpstreamAuthFallback(status, degradeOk) {
5139
5144
  if (status !== 401)
@@ -5164,19 +5169,13 @@ async function forwardCompute(ctx, req, res, path, url) {
5164
5169
  return sendJson(res, 200, []);
5165
5170
  return sendJson(res, 503, {
5166
5171
  error: "not_logged_in",
5167
- message: "Run `railcode login` to use LLM and connectors in dev.",
5172
+ message: "Run `railcode login` to use LLM, SQL, saved queries, connectors, and email in dev.",
5168
5173
  });
5169
5174
  }
5170
- const appUuid = await resolveDevAppUuid(ctx, creds);
5171
- if (!appUuid) {
5172
- if (degradeOk)
5173
- return sendJson(res, 200, []);
5174
- return sendJson(res, 503, {
5175
- error: "app_unavailable",
5176
- message: "Deploy this app once before using LLM, SQL, saved queries, or connectors in dev.",
5177
- });
5175
+ const target = devComputeTarget(creds, path, url.search);
5176
+ if (!target) {
5177
+ return sendJson(res, 404, { detail: "not found" });
5178
5178
  }
5179
- const target = `${creds.apiUrl}/api/organizations/${creds.orgUuid}/apps/${appUuid}${path}${url.search}`;
5180
5179
  const body = req.method === "POST" ? await readBody(req) : undefined;
5181
5180
  const headers = {
5182
5181
  Authorization: `Bearer ${creds.token}`,
@@ -5358,11 +5357,16 @@ function serveStatic(root, pathname, res) {
5358
5357
  }
5359
5358
  serveFile(res, fp, inferContentType(fp));
5360
5359
  }
5361
- function listenLoopback(server, port, limit = 50) {
5360
+ export function listenLoopback(server, port, limit = 50) {
5362
5361
  return new Promise((resolvePromise, reject) => {
5363
5362
  let attempt = 0;
5364
5363
  const tryListen = (p) => {
5364
+ const onListening = () => {
5365
+ server.removeListener("error", onError);
5366
+ resolvePromise(p);
5367
+ };
5365
5368
  const onError = (err) => {
5369
+ server.removeListener("listening", onListening);
5366
5370
  if (err.code === "EADDRINUSE" && attempt < limit) {
5367
5371
  attempt += 1;
5368
5372
  tryListen(p + 1);
@@ -5372,10 +5376,8 @@ function listenLoopback(server, port, limit = 50) {
5372
5376
  }
5373
5377
  };
5374
5378
  server.once("error", onError);
5375
- server.listen(p, "127.0.0.1", () => {
5376
- server.removeListener("error", onError);
5377
- resolvePromise(p);
5378
- });
5379
+ server.once("listening", onListening);
5380
+ server.listen(p, "127.0.0.1");
5379
5381
  };
5380
5382
  tryListen(port);
5381
5383
  });
@@ -5406,10 +5408,11 @@ function printDevBanner(ctx, config) {
5406
5408
  const creds = devCreds(config);
5407
5409
  if (creds) {
5408
5410
  console.log(` account: ${config.email ?? "?"} @ ${creds.apiUrl}`);
5409
- console.log(` proxied: llm/sql/connectors → real instance (REAL spend + data)`);
5411
+ console.log(` compute: llm/sql/queries/connectors/email → real instance (REAL spend + data)`);
5412
+ console.log(` runs as: you — your saved token + org permissions (no deploy needed)`);
5410
5413
  }
5411
5414
  else {
5412
- console.log(` account: not logged in — llm/sql return 503, connectors empty`);
5415
+ console.log(` account: not logged in — llm/sql/email return 503, connectors empty`);
5413
5416
  }
5414
5417
  console.log("");
5415
5418
  console.log(" Ctrl-C to stop.");
package/dist/manifest.js CHANGED
@@ -13,6 +13,7 @@
13
13
  // - GET /v1/balance
14
14
  // - GET /v1/customers/*
15
15
  // llm: true
16
+ // email: true
16
17
  // adhoc_sql:
17
18
  // - warehouse
18
19
  //
@@ -30,7 +31,7 @@
30
31
  // test/manifest-fixtures.json (also run by
31
32
  // backend/tests/test_manifest_parse_fixtures.py) keeps the two parsers pinned.
32
33
  export const APP_MANIFEST_NAME = "manifest.yaml";
33
- const ALLOWED_KEYS = ["run_as", "saved_queries", "connectors", "llm", "adhoc_sql"];
34
+ const ALLOWED_KEYS = ["run_as", "saved_queries", "connectors", "llm", "email", "adhoc_sql"];
34
35
  const HTTP_METHODS = ["GET", "POST", "PUT", "PATCH", "DELETE", "HEAD", "OPTIONS"];
35
36
  const SAVED_QUERY_NAME = /^[a-z0-9_]{1,80}$/;
36
37
  // PyYAML's YAML 1.1 plain-scalar resolver: a bare (unquoted) scalar matching one
@@ -322,7 +323,7 @@ function listString(s, key, num) {
322
323
  throw new ManifestParseError(`Empty list item under "${key}"`, num);
323
324
  return value;
324
325
  }
325
- function parseBool(s, num) {
326
+ function parseBool(s, key, num) {
326
327
  // A quoted scalar is a STRING even if it spells a boolean — as on the server.
327
328
  if (!s.quoted) {
328
329
  if (YAML_TRUE.test(s.value))
@@ -330,7 +331,7 @@ function parseBool(s, num) {
330
331
  if (YAML_FALSE.test(s.value))
331
332
  return false;
332
333
  }
333
- throw new ManifestParseError(`"llm" must be true or false (got "${s.value}")`, num);
334
+ throw new ManifestParseError(`"${key}" must be true or false (got "${s.value}")`, num);
334
335
  }
335
336
  // Strip the optional document markers: one leading "---" and one trailing "..."
336
337
  // are no-ops. Anything more is a multi-document stream, which the server
@@ -382,6 +383,7 @@ export function parseManifestYaml(source) {
382
383
  saved_queries: [],
383
384
  connectors: {},
384
385
  llm: false,
386
+ email: false,
385
387
  adhoc_sql: [],
386
388
  };
387
389
  for (const [key, entry] of entries) {
@@ -393,7 +395,10 @@ export function parseManifestYaml(source) {
393
395
  doc.run_as = value;
394
396
  }
395
397
  else if (key === "llm") {
396
- doc.llm = parseBool(inlineScalar(entry, key), entry.keyLine.num);
398
+ doc.llm = parseBool(inlineScalar(entry, key), key, entry.keyLine.num);
399
+ }
400
+ else if (key === "email") {
401
+ doc.email = parseBool(inlineScalar(entry, key), key, entry.keyLine.num);
397
402
  }
398
403
  else if (key === "connectors") {
399
404
  doc.connectors = parseConnectors(entry);
@@ -644,6 +649,8 @@ export function documentOperations(doc) {
644
649
  }
645
650
  if (doc.llm)
646
651
  ops.push({ type: "llm", id: "*", label: "LLM access" });
652
+ if (doc.email)
653
+ ops.push({ type: "email", id: "*", label: "email sending" });
647
654
  for (const name of doc.adhoc_sql) {
648
655
  ops.push({ type: "connector", id: name, label: `ad-hoc SQL on "${name}"` });
649
656
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "railcode",
3
- "version": "0.1.17",
3
+ "version": "0.1.19",
4
4
  "description": "Developer CLI for the multi-tenant Railcode platform: log in, scaffold, and deploy static apps.",
5
5
  "license": "MIT",
6
6
  "type": "module",
package/static/sdk.js CHANGED
@@ -175,8 +175,8 @@
175
175
  }
176
176
  return shorten(String(res), 48);
177
177
  }
178
- function track(kind, label2, thunk) {
179
- const entry = { kind, label: label2, status: "pending", t0: performance.now() };
178
+ function track(kind, label3, thunk) {
179
+ const entry = { kind, label: label3, status: "pending", t0: performance.now() };
180
180
  Inspector.add(entry);
181
181
  return thunk().then(
182
182
  (res) => {
@@ -195,8 +195,8 @@
195
195
  }
196
196
  );
197
197
  }
198
- function trackStream(kind, label2, streamFactory) {
199
- const entry = { kind, label: label2, status: "pending", t0: performance.now() };
198
+ function trackStream(kind, label3, streamFactory) {
199
+ const entry = { kind, label: label3, status: "pending", t0: performance.now() };
200
200
  Inspector.add(entry);
201
201
  return async function* () {
202
202
  let textLen = 0;
@@ -234,10 +234,10 @@
234
234
  };
235
235
  var runSQL = (engine, connection, query2, params) => {
236
236
  const trimmed = shorten(query2.replace(/\s+/g, " ").trim(), QUERY_LABEL_MAX);
237
- const label2 = `${engine ?? "data"}(${q(connection)}).runSQL(${q(trimmed)}${params && params.length ? `, ${shorten(JSON.stringify(params), PARAMS_LABEL_MAX)}` : ""})`;
237
+ const label3 = `${engine ?? "data"}(${q(connection)}).runSQL(${q(trimmed)}${params && params.length ? `, ${shorten(JSON.stringify(params), PARAMS_LABEL_MAX)}` : ""})`;
238
238
  return track(
239
239
  "sql",
240
- label2,
240
+ label3,
241
241
  () => call("POST", "/sql", {
242
242
  // engine is advisory; omit it for the generic data() namespace so the backend
243
243
  // dispatches on the connection's stored kind. postgres() pins engine="postgres".
@@ -387,6 +387,32 @@
387
387
  return resp.markdown;
388
388
  });
389
389
 
390
+ // ../sdk/src/email.ts
391
+ function label(opts) {
392
+ const to = Array.isArray(opts.to) ? `${opts.to.length} recipients` : opts.to;
393
+ return `email.send(${q(shorten(`${to}: ${opts.subject}`, 70))})`;
394
+ }
395
+ function body(opts) {
396
+ const out = {
397
+ to: opts.to,
398
+ subject: opts.subject
399
+ };
400
+ if (opts.html !== void 0) out.html = opts.html;
401
+ if (opts.text !== void 0) out.text = opts.text;
402
+ if (opts.cc !== void 0) out.cc = opts.cc;
403
+ if (opts.bcc !== void 0) out.bcc = opts.bcc;
404
+ if (opts.replyTo !== void 0) out.replyTo = opts.replyTo;
405
+ return out;
406
+ }
407
+ function send2(opts) {
408
+ return track(
409
+ "email",
410
+ label(opts),
411
+ () => call("POST", "/email/send", { body: body(opts) })
412
+ );
413
+ }
414
+ var email = { send: send2 };
415
+
390
416
  // ../sdk/src/files.ts
391
417
  function contentTypeOf(data2, explicit) {
392
418
  if (explicit) return explicit;
@@ -431,12 +457,12 @@
431
457
  var appUsers = () => track("app-users", "appUsers()", () => call("GET", "/app-users"));
432
458
 
433
459
  // ../sdk/src/llm.ts
434
- function label(method, input, opts = {}) {
460
+ function label2(method, input, opts = {}) {
435
461
  const preview = typeof input === "string" ? shorten(input.replace(/\s+/g, " ").trim(), 70) : `${input.length} message${input.length === 1 ? "" : "s"}`;
436
462
  const output = opts.output?.type === "json" ? " \u2192 json" : "";
437
463
  return `llm.${method}(${q(preview)})${output}`;
438
464
  }
439
- function body(input, opts = {}) {
465
+ function body2(input, opts = {}) {
440
466
  const out = Array.isArray(input) ? { messages: input } : { input };
441
467
  if (opts.model !== void 0) out.model = opts.model;
442
468
  if (opts.provider !== void 0) out.provider = opts.provider;
@@ -452,7 +478,7 @@
452
478
  method: "POST",
453
479
  credentials: "include",
454
480
  headers: { "Content-Type": "application/json" },
455
- body: JSON.stringify(body(input, opts))
481
+ body: JSON.stringify(body2(input, opts))
456
482
  });
457
483
  if (!resp.ok) throw new ApiError(resp.status, await resp.text());
458
484
  return await resp.json();
@@ -465,7 +491,7 @@
465
491
  method: "POST",
466
492
  credentials: "include",
467
493
  headers: { "Content-Type": "application/json" },
468
- body: JSON.stringify(body(input, opts))
494
+ body: JSON.stringify(body2(input, opts))
469
495
  });
470
496
  if (!resp.ok) throw new ApiError(resp.status, await resp.text());
471
497
  if (!resp.body) throw new Error("LLM stream response has no body.");
@@ -493,10 +519,10 @@
493
519
  }
494
520
  }
495
521
  function generate(input, opts = {}) {
496
- return track("llm", label("generate", input, opts), () => doGenerate(input, opts));
522
+ return track("llm", label2("generate", input, opts), () => doGenerate(input, opts));
497
523
  }
498
524
  function stream(input, opts = {}) {
499
- return trackStream("llm", label("stream", input, opts), () => doStream(input, opts));
525
+ return trackStream("llm", label2("stream", input, opts), () => doStream(input, opts));
500
526
  }
501
527
  var llm = { generate, stream };
502
528
  var llmProviders = () => track("llm", "llmProviders()", () => call("GET", "/llm/providers"));
@@ -504,10 +530,10 @@
504
530
  // ../sdk/src/queries.ts
505
531
  var PARAMS_LABEL_MAX2 = 40;
506
532
  var query = (name, params) => {
507
- const label2 = `query(${q(name)}${params && Object.keys(params).length ? `, ${shorten(JSON.stringify(params), PARAMS_LABEL_MAX2)}` : ""})`;
533
+ const label3 = `query(${q(name)}${params && Object.keys(params).length ? `, ${shorten(JSON.stringify(params), PARAMS_LABEL_MAX2)}` : ""})`;
508
534
  return track(
509
535
  "sql",
510
- label2,
536
+ label3,
511
537
  () => call("POST", `/queries/${encPath(name)}`, {
512
538
  body: { params: params || {} }
513
539
  }).then(toSqlRows)
@@ -518,22 +544,22 @@
518
544
  // ../sdk/src/service-connectors.ts
519
545
  var PATH_LABEL_MAX = 60;
520
546
  var toResponse = (env) => {
521
- const body2 = env.body ?? "";
547
+ const body3 = env.body ?? "";
522
548
  return {
523
549
  status: env.status,
524
550
  ok: env.ok,
525
551
  headers: env.headers || {},
526
552
  truncated: Boolean(env.truncated),
527
- text: () => Promise.resolve(body2),
528
- json: () => Promise.resolve(JSON.parse(body2))
553
+ text: () => Promise.resolve(body3),
554
+ json: () => Promise.resolve(JSON.parse(body3))
529
555
  };
530
556
  };
531
557
  var request = (name, path, opts) => {
532
558
  const method = (opts?.method || "GET").toUpperCase();
533
- const label2 = `connector(${q(name)}).fetch(${q(method)} ${q(shorten(path, PATH_LABEL_MAX))})`;
559
+ const label3 = `connector(${q(name)}).fetch(${q(method)} ${q(shorten(path, PATH_LABEL_MAX))})`;
534
560
  return track(
535
561
  "connector",
536
- label2,
562
+ label3,
537
563
  () => call("POST", "/service-connectors/request", {
538
564
  body: { connector: name, method, path, body: opts?.body }
539
565
  }).then(toResponse)
@@ -555,6 +581,7 @@
555
581
  target.me = me;
556
582
  target.appUsers = appUsers;
557
583
  target.designSystem = designSystem;
584
+ target.email = email;
558
585
  target.llm = llm;
559
586
  target.llmProviders = llmProviders;
560
587
  target.data = data;