railcode 0.1.19 → 0.1.21

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 CHANGED
@@ -22,6 +22,8 @@ railcode login --setup-token <token> Sign in with a one-time onboarding setup t
22
22
  railcode init <app> [--template react|static]
23
23
  railcode deploy Build (if configured) and deploy the app here
24
24
  railcode db <list|query> ... List data connectors / run read-only SQL
25
+ railcode agent <list|show|create|update|delete|run|schedule> ...
26
+ Manage org-scoped managed agents
25
27
  ```
26
28
 
27
29
  - **`login`** prompts for the server URL (default `https://api.railcode.app`),
@@ -56,10 +58,28 @@ railcode db <list|query> ... List data connectors / run read-only SQL
56
58
  `railcode db list` (aliases `ls`, `connections`) prints each connector's
57
59
  `name` + `engine` (`--json` for the raw array). `railcode db query "<sql>"`
58
60
  (alias `sql`) runs SQL against `--connection` (default `default`) and prints a
59
- table + row count; `--engine <postgres|bigquery|snowflake>` is inferred from the connector
61
+ table + row count; `--engine <postgres|bigquery|turso>` is inferred from the connector
60
62
  list when omitted, `--params '<json-array>'` binds `$1, $2, …` placeholders
61
63
  (SQL is never interpolated), `--file <path>` reads SQL from a file, and
62
64
  `--json` prints the raw `{ columns, rows, rowcount, truncated }` envelope.
65
+ - **`agent`** manages org-scoped managed agents when your permissions allow it.
66
+ Manifests are JSON files in the same shape the API stores under
67
+ `agent.manifest`.
68
+ - `railcode agent list` lists agents; `--json` prints raw `AgentOut[]`.
69
+ - `railcode agent show <name|uuid>` prints one agent; `--manifest` prints only
70
+ its manifest JSON.
71
+ - `railcode agent pull <name|uuid> --output agent.json` writes the existing
72
+ manifest for editing.
73
+ - `railcode agent create --file agent.json` creates an agent; `update <agent>
74
+ --file agent.json` replaces an existing agent manifest; `delete <agent> --yes`
75
+ archives it.
76
+ - `railcode agent test --file agent.json --input '{"k":"v"}'` runs a draft
77
+ manifest without saving; `railcode agent run <agent> --input '{"k":"v"}'`
78
+ invokes a saved agent and prints the result. Use `--trace` for the step trace
79
+ and `--json` for the raw run detail.
80
+ - `railcode agent schedule set <agent> --cron "0 9 * * *" --timezone UTC`
81
+ creates or updates the agent's one schedule. `schedule show`, `pause`,
82
+ `resume`, `delete --yes`, and `run-now` operate on that single schedule.
63
83
 
64
84
  ## Configuration
65
85
 
package/dist/db.js CHANGED
@@ -3,7 +3,7 @@
3
3
  // the command layer in index.ts does the HTTP, file reads, and printing.
4
4
  export class DbError extends Error {
5
5
  }
6
- export const DB_ENGINES = ["postgres", "bigquery", "snowflake"];
6
+ export const DB_ENGINES = ["postgres", "bigquery", "turso"];
7
7
  // `--params '<json-array>'` → the positional $1,$2,… values. Empty when omitted.
8
8
  export function parseSqlParams(raw) {
9
9
  if (raw === undefined)
package/dist/index.js CHANGED
@@ -43,6 +43,8 @@ Usage:
43
43
  railcode connector <list|docs|fetch> ... List connectors / show API docs / proxy a call
44
44
  railcode llm <providers|models> List the LLM providers/models apps can call
45
45
  railcode manifest <validate|show> ... Validate ${APP_MANIFEST_NAME} locally / show an app's ratified manifest
46
+ railcode agent <list|show|create|update|delete|run|schedule> ...
47
+ Manage org-scoped managed agents
46
48
  railcode --version
47
49
  railcode --help
48
50
 
@@ -97,6 +99,10 @@ async function main() {
97
99
  case "manifest":
98
100
  await commandManifest(args);
99
101
  return;
102
+ case "agent":
103
+ case "agents":
104
+ await commandAgent(args);
105
+ return;
100
106
  default:
101
107
  throw new CliError(`Unknown command: ${args.command}\n\n${HELP}`, 2);
102
108
  }
@@ -820,7 +826,7 @@ export default defineConfig({
820
826
  }
821
827
  function reactRailcodeTypes() {
822
828
  return `type Me = {
823
- user: { uuid: string; name: string; email: string };
829
+ user: { uuid: string; name: string; email: string; is_admin: boolean; roles: RoleRef[] };
824
830
  app: { uuid: string; slug: string; name: string };
825
831
  org: { uuid: string; slug: string; name: string };
826
832
  };
@@ -829,9 +835,12 @@ type AppUser = {
829
835
  uuid: string;
830
836
  name: string;
831
837
  email: string;
832
- role: string | null;
838
+ is_admin: boolean;
833
839
  };
834
840
 
841
+ type RoleRef = { uuid: string; name: string };
842
+ type OrgRole = RoleRef & { description: string; is_member: boolean };
843
+
835
844
  type KvRecord<T = unknown> = {
836
845
  key: string;
837
846
  value: T;
@@ -875,7 +884,7 @@ type SqlRows = Array<Record<string, unknown>> & {
875
884
  };
876
885
 
877
886
  type DataConnectorInfo = {
878
- engine: "postgres" | "bigquery" | "snowflake" | string;
887
+ engine: "postgres" | "bigquery" | "turso" | string;
879
888
  name: string;
880
889
  };
881
890
 
@@ -954,8 +963,25 @@ type LlmModelInfo = { model: string; default: boolean };
954
963
 
955
964
  type LlmProviderInfo = { provider: string; default: boolean; models: LlmModelInfo[] };
956
965
 
966
+ type EmailSendOptions = {
967
+ to: string | string[];
968
+ subject: string;
969
+ html?: string;
970
+ text?: string;
971
+ cc?: string | string[];
972
+ bcc?: string | string[];
973
+ replyTo?: string;
974
+ };
975
+
976
+ type EmailSendResult = {
977
+ id: string;
978
+ status: string;
979
+ requestId: string;
980
+ };
981
+
957
982
  declare const me: () => Promise<Me>;
958
983
  declare const appUsers: () => Promise<AppUser[]>;
984
+ declare const roles: () => Promise<OrgRole[]>;
959
985
  declare const designSystem: () => Promise<string>;
960
986
  declare const db: { collection<T = unknown>(name: string): Collection<T> };
961
987
  declare const files: {
@@ -967,7 +993,6 @@ declare const files: {
967
993
  declare const data: DatabaseNamespace;
968
994
  declare const postgres: DatabaseNamespace;
969
995
  declare const bigquery: DatabaseNamespace;
970
- declare const snowflake: DatabaseNamespace;
971
996
  declare const dataConnectors: () => Promise<DataConnectorInfo[]>;
972
997
  declare const savedQueries: () => Promise<SavedQueryInfo[]>;
973
998
  declare const query: (name: string, params?: Record<string, unknown>) => Promise<SqlRows>;
@@ -980,6 +1005,7 @@ declare const llm: {
980
1005
  stream(input: string | LlmMessage[], opts?: LlmOptions): AsyncIterable<LlmStreamEvent>;
981
1006
  };
982
1007
  declare const llmProviders: () => Promise<LlmProviderInfo[]>;
1008
+ declare const email: { send(opts: EmailSendOptions): Promise<EmailSendResult> };
983
1009
  `;
984
1010
  }
985
1011
  function reactViteEnvTypes() {
@@ -1054,6 +1080,14 @@ function message(error: unknown): string {
1054
1080
  return error instanceof Error ? error.message : String(error);
1055
1081
  }
1056
1082
 
1083
+ function httpStatus(error: unknown): number | null {
1084
+ if (typeof error === "object" && error !== null && "status" in error) {
1085
+ const status = (error as { status?: unknown }).status;
1086
+ return typeof status === "number" ? status : null;
1087
+ }
1088
+ return null;
1089
+ }
1090
+
1057
1091
  function truncate(value: string, max: number): string {
1058
1092
  return value.length > max ? value.slice(0, max - 1) + "…" : value;
1059
1093
  }
@@ -1355,7 +1389,7 @@ function Hero() {
1355
1389
  </div>
1356
1390
  ) : null}
1357
1391
  {teammates.slice(0, 4).map((user) => (
1358
- <div className="avatar" key={user.uuid} title={user.name + (user.role ? " · " + user.role : "")}>
1392
+ <div className="avatar" key={user.uuid} title={user.name + (user.is_admin ? " · admin" : "")}>
1359
1393
  {initials(user.name)}
1360
1394
  </div>
1361
1395
  ))}
@@ -1861,9 +1895,124 @@ function ConnectorsCard() {
1861
1895
  );
1862
1896
  }
1863
1897
 
1898
+ function emailFailureMessage(error: unknown): string {
1899
+ const status = httpStatus(error);
1900
+ if (status === 403) return "Email is not granted for this app or user yet.";
1901
+ if (status === 429) return "The daily email recipient limit has been reached.";
1902
+ if (status === 503) return "Email is not configured for this Railcode instance.";
1903
+ return message(error);
1904
+ }
1905
+
1906
+ function EmailCard() {
1907
+ const { identity } = useAppStore();
1908
+ const [to, setTo] = useState("");
1909
+ const [subject, setSubject] = useState("Hello from " + APP_SLUG);
1910
+ const [body, setBody] = useState(
1911
+ "This message was sent by the Railcode starter app through the platform email gateway.",
1912
+ );
1913
+ const [busy, setBusy] = useState(false);
1914
+ const [status, setStatus] = useState<{ kind: "sent" | "error"; text: string } | null>(null);
1915
+
1916
+ useEffect(() => {
1917
+ if (!to && identity?.user.email) setTo(identity.user.email);
1918
+ }, [identity, to]);
1919
+
1920
+ async function sendMessage(event: FormEvent<HTMLFormElement>) {
1921
+ event.preventDefault();
1922
+ const recipient = to.trim();
1923
+ const trimmedSubject = subject.trim() || "Hello from " + APP_SLUG;
1924
+ const text = body.trim() || "Sent from a Railcode starter app.";
1925
+ if (!recipient) {
1926
+ setStatus({ kind: "error", text: "Add a recipient email address first." });
1927
+ return;
1928
+ }
1929
+
1930
+ setBusy(true);
1931
+ setStatus(null);
1932
+ try {
1933
+ const result = await email.send({ to: recipient, subject: trimmedSubject, text });
1934
+ setStatus({
1935
+ kind: "sent",
1936
+ text: "Sent with status " + result.status + " · " + result.requestId,
1937
+ });
1938
+ } catch (error) {
1939
+ setStatus({ kind: "error", text: emailFailureMessage(error) });
1940
+ } finally {
1941
+ setBusy(false);
1942
+ }
1943
+ }
1944
+
1945
+ return (
1946
+ <article className="card">
1947
+ <div className="card-head">
1948
+ <div>
1949
+ <div className="card-title-row">
1950
+ <Icon name="mail" />
1951
+ <span className="card-title">
1952
+ <Reveal code="await email.send(message)">Email</Reveal>
1953
+ </span>
1954
+ </div>
1955
+ </div>
1956
+ </div>
1957
+
1958
+ <form className="email-form" onSubmit={sendMessage}>
1959
+ <label className="control-field">
1960
+ <span>Recipient</span>
1961
+ <input
1962
+ className="field"
1963
+ type="email"
1964
+ autoComplete="email"
1965
+ value={to}
1966
+ onChange={(event) => setTo(event.target.value)}
1967
+ />
1968
+ </label>
1969
+ <label className="control-field">
1970
+ <span>Subject</span>
1971
+ <input
1972
+ className="field"
1973
+ type="text"
1974
+ value={subject}
1975
+ onChange={(event) => setSubject(event.target.value)}
1976
+ />
1977
+ </label>
1978
+ <label className="json-editor">
1979
+ <span>Message</span>
1980
+ <textarea
1981
+ className="field"
1982
+ rows={4}
1983
+ value={body}
1984
+ onChange={(event) => setBody(event.target.value)}
1985
+ />
1986
+ </label>
1987
+ <ActionTip code={\`await email.send({
1988
+ to,
1989
+ subject,
1990
+ text
1991
+ })\`} className="upload-tip">
1992
+ <button className="btn btn-primary" type="submit" disabled={busy}>
1993
+ {busy ? <span className="spinner" /> : <Icon name="mail" />}
1994
+ {busy ? "Sending..." : "Send test email"}
1995
+ </button>
1996
+ </ActionTip>
1997
+ </form>
1998
+
1999
+ <div className="email-state">
2000
+ <div className="result-status">
2001
+ <span>Delivery</span>
2002
+ <span>{status?.kind === "sent" ? "Sent" : status?.kind === "error" ? "Needs setup" : "Ready"}</span>
2003
+ </div>
2004
+ <p className={status?.kind === "error" ? "email-note is-error" : "email-note"}>
2005
+ {status?.text ||
2006
+ "Email uses the platform sender and requires an email grant or app manifest approval."}
2007
+ </p>
2008
+ </div>
2009
+ </article>
2010
+ );
2011
+ }
2012
+
1864
2013
  const AI_SAMPLE_PROMPT = "Summarize what this Railcode template demonstrates in one sentence.";
1865
2014
  const AI_SAMPLE_RESPONSE =
1866
- "This template shows a live Railcode app reading identity, storing notes in KV, uploading files, querying Postgres, invoking a saved query, calling Stripe through a service connector, and generating text — all without handling a single credential in the browser.";
2015
+ "This template shows a live Railcode app reading identity, storing notes in KV, uploading files, querying Postgres, invoking a saved query, calling Stripe through a service connector, sending email, and generating text — all without handling a single credential in the browser.";
1867
2016
 
1868
2017
  function AiCard() {
1869
2018
  const [prompt, setPrompt] = useState(AI_SAMPLE_PROMPT);
@@ -2004,6 +2153,10 @@ function IconSprite() {
2004
2153
  <symbol id="i-chat" viewBox="0 0 24 24">
2005
2154
  <path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z" />
2006
2155
  </symbol>
2156
+ <symbol id="i-mail" viewBox="0 0 24 24">
2157
+ <rect x="3" y="5" width="18" height="14" rx="2" />
2158
+ <path d="m3 7 9 6 9-6" />
2159
+ </symbol>
2007
2160
  <symbol id="i-code" viewBox="0 0 24 24">
2008
2161
  <polyline points="16 18 22 12 16 6" />
2009
2162
  <polyline points="8 6 2 12 8 18" />
@@ -2070,11 +2223,18 @@ export function App() {
2070
2223
 
2071
2224
  <GuideSection
2072
2225
  title="They can connect to external services"
2073
- body="Railcode apps can securely connect to your SaaS services so you can manage refunds, send emails, view data, and anything else you need."
2226
+ body="Railcode apps can securely connect to your SaaS services so you can manage refunds, sync tickets, enrich records, and anything else you need."
2074
2227
  >
2075
2228
  <ConnectorsCard />
2076
2229
  </GuideSection>
2077
2230
 
2231
+ <GuideSection
2232
+ title="They can send email"
2233
+ body="Send transactional email through the Railcode gateway without putting provider credentials in browser code."
2234
+ >
2235
+ <EmailCard />
2236
+ </GuideSection>
2237
+
2078
2238
  <GuideSection
2079
2239
  title="They can use AI"
2080
2240
  body="Build AI-native apps that use LLMs to enable powerful workflows."
@@ -3095,6 +3255,41 @@ th:first-child {
3095
3255
  }
3096
3256
  }
3097
3257
 
3258
+ /* ---------- email card ---------- */
3259
+
3260
+ .email-form {
3261
+ display: flex;
3262
+ flex-direction: column;
3263
+ gap: var(--space-3);
3264
+ }
3265
+
3266
+ .email-form .field {
3267
+ width: 100%;
3268
+ }
3269
+
3270
+ .email-state {
3271
+ margin-top: var(--space-4);
3272
+ padding: 12px 14px;
3273
+ border: 1px solid var(--line);
3274
+ border-radius: var(--radius-md);
3275
+ background: var(--bg-inset);
3276
+ }
3277
+
3278
+ .email-state .result-status {
3279
+ margin-bottom: 6px;
3280
+ }
3281
+
3282
+ .email-note {
3283
+ color: var(--ink-3);
3284
+ font-size: 12.5px;
3285
+ line-height: 1.55;
3286
+ overflow-wrap: anywhere;
3287
+ }
3288
+
3289
+ .email-note.is-error {
3290
+ color: #b42318;
3291
+ }
3292
+
3098
3293
  /* ---------- reveal + action tooltips ---------- */
3099
3294
 
3100
3295
  .reveal {
@@ -3534,6 +3729,550 @@ async function commandManifestShow(args) {
3534
3729
  }
3535
3730
  }
3536
3731
  // ---------------------------------------------------------------------------
3732
+ // agent — managed agents (`railcode agent ...`)
3733
+ // ---------------------------------------------------------------------------
3734
+ const AGENT_HELP = `railcode agent — managed agents
3735
+
3736
+ Usage:
3737
+ railcode agent list List managed agents in this org
3738
+ railcode agent show <agent> Show one agent
3739
+ railcode agent pull <agent> [--output <path>]
3740
+ Write the agent manifest as JSON
3741
+ railcode agent create --file <path> Create an agent from a JSON manifest
3742
+ railcode agent update <agent> --file <path> Update an agent from a JSON manifest
3743
+ railcode agent delete <agent> [--yes] Delete/archive an agent
3744
+ railcode agent test --file <path> [opts] Run a draft manifest without saving
3745
+ railcode agent run <agent> [opts] Trigger a saved agent and print results
3746
+ railcode agent schedule <show|set|add|update|pause|resume|delete|run-now> <agent>
3747
+
3748
+ Agent options:
3749
+ --json Print the raw API response
3750
+ --file <path> JSON manifest file
3751
+ --output <path> Output path for pull (default stdout)
3752
+ --input '<json>' Invoke/test input JSON (default null)
3753
+ --input-file <path> Read invoke/test input JSON from a file
3754
+ --trace Include the run step trace in human output
3755
+ --yes Confirm delete without a prompt
3756
+
3757
+ Schedule options:
3758
+ --name <name> Schedule name (default: default on create)
3759
+ --cron <expr> Five-field cron expression
3760
+ --schedule <expr> Alias for --cron
3761
+ --timezone <iana> IANA timezone (default: UTC on create)
3762
+ --enabled / --disabled Initial or updated enabled state
3763
+
3764
+ Manifests are JSON files in the same shape the API stores under agent.manifest.
3765
+ Schedules are currently one per agent, so "set" creates the schedule if missing
3766
+ or updates the existing one.
3767
+ `;
3768
+ async function commandAgent(args) {
3769
+ const sub = args.rest[0] ?? "";
3770
+ switch (sub) {
3771
+ case "list":
3772
+ case "ls":
3773
+ await commandAgentList(args);
3774
+ return;
3775
+ case "show":
3776
+ case "get":
3777
+ await commandAgentShow(args);
3778
+ return;
3779
+ case "pull":
3780
+ case "export":
3781
+ await commandAgentPull(args);
3782
+ return;
3783
+ case "create":
3784
+ await commandAgentCreate(args);
3785
+ return;
3786
+ case "update":
3787
+ case "edit":
3788
+ await commandAgentUpdate(args);
3789
+ return;
3790
+ case "delete":
3791
+ case "rm":
3792
+ await commandAgentDelete(args);
3793
+ return;
3794
+ case "test":
3795
+ await commandAgentTest(args);
3796
+ return;
3797
+ case "run":
3798
+ case "invoke":
3799
+ await commandAgentRun(args);
3800
+ return;
3801
+ case "schedule":
3802
+ case "schedules":
3803
+ await commandAgentSchedule(args);
3804
+ return;
3805
+ case "":
3806
+ case "help":
3807
+ console.log(AGENT_HELP);
3808
+ return;
3809
+ default:
3810
+ throw new CliError(`Unknown agent command: ${sub}\n\n${AGENT_HELP}`, 2);
3811
+ }
3812
+ }
3813
+ async function commandAgentList(args) {
3814
+ assertKnownAgentOptions(args, ["json", "apiUrl"]);
3815
+ if (args.rest.length > 1) {
3816
+ throw new CliError(`railcode agent list takes no arguments (got "${args.rest[1]}").`, 2);
3817
+ }
3818
+ const config = requireLogin(args);
3819
+ const agents = await fetchAgents(config);
3820
+ if (args.options.json) {
3821
+ console.log(JSON.stringify(agents, null, 2));
3822
+ return;
3823
+ }
3824
+ if (agents.length === 0) {
3825
+ console.log("No managed agents in this organization.");
3826
+ return;
3827
+ }
3828
+ console.log(formatTable(["name", "model", "uuid", "updated"], agents.map((agent) => [
3829
+ agent.name,
3830
+ typeof agent.manifest.model === "string" ? agent.manifest.model : "-",
3831
+ agent.uuid,
3832
+ shortDate(agent.updated_at),
3833
+ ])));
3834
+ }
3835
+ async function commandAgentShow(args) {
3836
+ assertKnownAgentOptions(args, ["json", "manifest", "apiUrl"]);
3837
+ const ref = agentArg(args, "show");
3838
+ const config = requireLogin(args);
3839
+ const agent = await fetchAgent(config, ref);
3840
+ if (args.options.json) {
3841
+ console.log(JSON.stringify(agent, null, 2));
3842
+ return;
3843
+ }
3844
+ if (args.options.manifest) {
3845
+ console.log(JSON.stringify(agent.manifest, null, 2));
3846
+ return;
3847
+ }
3848
+ printAgent(agent);
3849
+ }
3850
+ async function commandAgentPull(args) {
3851
+ assertKnownAgentOptions(args, ["output", "json", "apiUrl"]);
3852
+ const ref = agentArg(args, "pull");
3853
+ const output = optionString(args, "output");
3854
+ const config = requireLogin(args);
3855
+ const agent = await fetchAgent(config, ref);
3856
+ const manifest = JSON.stringify(agent.manifest, null, 2) + "\n";
3857
+ if (output) {
3858
+ writeFileSync(resolve(process.cwd(), output), manifest, { mode: 0o644 });
3859
+ console.log(`Wrote ${output}`);
3860
+ return;
3861
+ }
3862
+ process.stdout.write(manifest);
3863
+ }
3864
+ async function commandAgentCreate(args) {
3865
+ assertKnownAgentOptions(args, ["file", "json", "apiUrl"]);
3866
+ const manifest = readAgentManifest(args);
3867
+ const config = requireLogin(args);
3868
+ const resp = await authedFetch(config, `/api/organizations/${config.orgUuid}/agents`, {
3869
+ method: "POST",
3870
+ headers: { "Content-Type": "application/json" },
3871
+ body: JSON.stringify({ manifest }),
3872
+ });
3873
+ if (resp.status === 401)
3874
+ await reLoginOrThrow();
3875
+ const result = (await readJsonOrThrow(resp, "Create agent"));
3876
+ printAgentMutation(result, args.options.json === true, "created");
3877
+ }
3878
+ async function commandAgentUpdate(args) {
3879
+ assertKnownAgentOptions(args, ["file", "json", "apiUrl"]);
3880
+ const ref = agentArg(args, "update");
3881
+ const manifest = readAgentManifest(args);
3882
+ const config = requireLogin(args);
3883
+ const resp = await authedFetch(config, `/api/organizations/${config.orgUuid}/agents/${encodeURIComponent(ref)}`, {
3884
+ method: "PUT",
3885
+ headers: { "Content-Type": "application/json" },
3886
+ body: JSON.stringify({ manifest }),
3887
+ });
3888
+ if (resp.status === 401)
3889
+ await reLoginOrThrow();
3890
+ const result = (await readJsonOrThrow(resp, `Update agent "${ref}"`));
3891
+ printAgentMutation(result, args.options.json === true, "updated");
3892
+ }
3893
+ async function commandAgentDelete(args) {
3894
+ assertKnownAgentOptions(args, ["yes", "apiUrl"]);
3895
+ const ref = agentArg(args, "delete");
3896
+ if (args.options.yes !== true) {
3897
+ if (!process.stdin.isTTY) {
3898
+ throw new CliError("Pass --yes to delete an agent in a non-interactive session.", 2);
3899
+ }
3900
+ const answer = await prompt(`Delete agent "${ref}"? Its run history is kept. [y/N] `);
3901
+ if (!["y", "yes"].includes(answer.trim().toLowerCase())) {
3902
+ throw new CliError("Delete cancelled.", 1);
3903
+ }
3904
+ }
3905
+ const config = requireLogin(args);
3906
+ const resp = await authedFetch(config, `/api/organizations/${config.orgUuid}/agents/${encodeURIComponent(ref)}`, { method: "DELETE" });
3907
+ if (resp.status === 401)
3908
+ await reLoginOrThrow();
3909
+ if (!resp.ok) {
3910
+ throw new CliError(`Delete agent "${ref}" failed (${resp.status}): ${await errorDetail(resp)}`, 1);
3911
+ }
3912
+ console.log(`Agent "${ref}" deleted.`);
3913
+ }
3914
+ async function commandAgentTest(args) {
3915
+ assertKnownAgentOptions(args, ["file", "input", "inputFile", "json", "trace", "apiUrl"]);
3916
+ const manifest = readAgentManifest(args);
3917
+ const input = readJsonInput(args);
3918
+ const config = requireLogin(args);
3919
+ const resp = await authedFetch(config, `/api/organizations/${config.orgUuid}/agents/test`, {
3920
+ method: "POST",
3921
+ headers: { "Content-Type": "application/json" },
3922
+ body: JSON.stringify({ manifest, input }),
3923
+ });
3924
+ if (resp.status === 401)
3925
+ await reLoginOrThrow();
3926
+ const run = (await readJsonOrThrow(resp, "Test agent"));
3927
+ printAgentRun(run, { json: args.options.json === true, trace: args.options.trace === true });
3928
+ }
3929
+ async function commandAgentRun(args) {
3930
+ assertKnownAgentOptions(args, ["input", "inputFile", "json", "trace", "apiUrl"]);
3931
+ const ref = agentArg(args, "run");
3932
+ const input = readJsonInput(args);
3933
+ const config = requireLogin(args);
3934
+ const resp = await authedFetch(config, `/api/organizations/${config.orgUuid}/agents/${encodeURIComponent(ref)}/invoke`, {
3935
+ method: "POST",
3936
+ headers: { "Content-Type": "application/json" },
3937
+ body: JSON.stringify({ input }),
3938
+ });
3939
+ if (resp.status === 401)
3940
+ await reLoginOrThrow();
3941
+ const run = (await readJsonOrThrow(resp, `Run agent "${ref}"`));
3942
+ printAgentRun(run, { json: args.options.json === true, trace: args.options.trace === true });
3943
+ }
3944
+ async function commandAgentSchedule(args) {
3945
+ const sub = args.rest[1] ?? "";
3946
+ switch (sub) {
3947
+ case "show":
3948
+ case "list":
3949
+ case "ls":
3950
+ await commandAgentScheduleShow(args);
3951
+ return;
3952
+ case "set":
3953
+ await commandAgentScheduleSet(args, "upsert");
3954
+ return;
3955
+ case "add":
3956
+ case "create":
3957
+ await commandAgentScheduleSet(args, "create");
3958
+ return;
3959
+ case "update":
3960
+ await commandAgentScheduleSet(args, "update");
3961
+ return;
3962
+ case "pause":
3963
+ await commandAgentScheduleEnabled(args, false);
3964
+ return;
3965
+ case "resume":
3966
+ await commandAgentScheduleEnabled(args, true);
3967
+ return;
3968
+ case "delete":
3969
+ case "rm":
3970
+ await commandAgentScheduleDelete(args);
3971
+ return;
3972
+ case "run-now":
3973
+ case "run":
3974
+ await commandAgentScheduleRunNow(args);
3975
+ return;
3976
+ case "":
3977
+ case "help":
3978
+ console.log(AGENT_HELP);
3979
+ return;
3980
+ default:
3981
+ throw new CliError(`Unknown agent schedule command: ${sub}\n\n${AGENT_HELP}`, 2);
3982
+ }
3983
+ }
3984
+ async function commandAgentScheduleShow(args) {
3985
+ assertKnownAgentOptions(args, ["json", "apiUrl"]);
3986
+ const ref = agentScheduleAgentArg(args, "show");
3987
+ const config = requireLogin(args);
3988
+ const schedules = await fetchAgentSchedules(config, ref);
3989
+ if (args.options.json) {
3990
+ console.log(JSON.stringify(schedules, null, 2));
3991
+ return;
3992
+ }
3993
+ if (schedules.length === 0) {
3994
+ console.log(`Agent "${ref}" has no schedule.`);
3995
+ return;
3996
+ }
3997
+ printSchedule(schedules[0]);
3998
+ }
3999
+ async function commandAgentScheduleSet(args, mode) {
4000
+ assertKnownAgentOptions(args, [
4001
+ "name",
4002
+ "cron",
4003
+ "schedule",
4004
+ "timezone",
4005
+ "enabled",
4006
+ "disabled",
4007
+ "json",
4008
+ "apiUrl",
4009
+ ]);
4010
+ const ref = agentScheduleAgentArg(args, mode === "upsert" ? "set" : mode);
4011
+ const config = requireLogin(args);
4012
+ const existing = await fetchAgentSchedules(config, ref);
4013
+ if (mode === "create" && existing.length > 0) {
4014
+ throw new CliError(`Agent "${ref}" already has a schedule. Use \`railcode agent schedule set ${ref}\` to update it.`, 1);
4015
+ }
4016
+ if (mode === "update" && existing.length === 0) {
4017
+ throw new CliError(`Agent "${ref}" has no schedule. Use \`railcode agent schedule set ${ref} --cron "0 9 * * *"\` to create one.`, 1);
4018
+ }
4019
+ const body = schedulePayload(args, existing.length === 0);
4020
+ const path = existing.length === 0
4021
+ ? `/api/organizations/${config.orgUuid}/agents/${encodeURIComponent(ref)}/schedules`
4022
+ : `/api/organizations/${config.orgUuid}/agents/${encodeURIComponent(ref)}/schedules/${encodeURIComponent(existing[0].uuid)}`;
4023
+ const resp = await authedFetch(config, path, {
4024
+ method: existing.length === 0 ? "POST" : "PATCH",
4025
+ headers: { "Content-Type": "application/json" },
4026
+ body: JSON.stringify(body),
4027
+ });
4028
+ if (resp.status === 401)
4029
+ await reLoginOrThrow();
4030
+ const schedule = (await readJsonOrThrow(resp, `${existing.length === 0 ? "Create" : "Update"} agent schedule`));
4031
+ if (args.options.json) {
4032
+ console.log(JSON.stringify(schedule, null, 2));
4033
+ return;
4034
+ }
4035
+ console.log(existing.length === 0 ? "Schedule created." : "Schedule updated.");
4036
+ printSchedule(schedule);
4037
+ }
4038
+ async function commandAgentScheduleEnabled(args, enabled) {
4039
+ assertKnownAgentOptions(args, ["json", "apiUrl"]);
4040
+ const ref = agentScheduleAgentArg(args, enabled ? "resume" : "pause");
4041
+ const config = requireLogin(args);
4042
+ const schedule = await requireSingleSchedule(config, ref);
4043
+ const updated = await patchAgentSchedule(config, ref, schedule.uuid, { enabled });
4044
+ if (args.options.json) {
4045
+ console.log(JSON.stringify(updated, null, 2));
4046
+ return;
4047
+ }
4048
+ console.log(enabled ? "Schedule resumed." : "Schedule paused.");
4049
+ printSchedule(updated);
4050
+ }
4051
+ async function commandAgentScheduleDelete(args) {
4052
+ assertKnownAgentOptions(args, ["yes", "apiUrl"]);
4053
+ const ref = agentScheduleAgentArg(args, "delete");
4054
+ const config = requireLogin(args);
4055
+ const schedule = await requireSingleSchedule(config, ref);
4056
+ if (args.options.yes !== true) {
4057
+ if (!process.stdin.isTTY) {
4058
+ throw new CliError("Pass --yes to delete a schedule in a non-interactive session.", 2);
4059
+ }
4060
+ const answer = await prompt(`Delete schedule "${schedule.name}" for agent "${ref}"? [y/N] `);
4061
+ if (!["y", "yes"].includes(answer.trim().toLowerCase())) {
4062
+ throw new CliError("Delete cancelled.", 1);
4063
+ }
4064
+ }
4065
+ const resp = await authedFetch(config, `/api/organizations/${config.orgUuid}/agents/${encodeURIComponent(ref)}/schedules/${encodeURIComponent(schedule.uuid)}`, { method: "DELETE" });
4066
+ if (resp.status === 401)
4067
+ await reLoginOrThrow();
4068
+ if (!resp.ok) {
4069
+ throw new CliError(`Delete agent schedule failed (${resp.status}): ${await errorDetail(resp)}`, 1);
4070
+ }
4071
+ console.log(`Schedule "${schedule.name}" deleted.`);
4072
+ }
4073
+ async function commandAgentScheduleRunNow(args) {
4074
+ assertKnownAgentOptions(args, ["json", "trace", "apiUrl"]);
4075
+ const ref = agentScheduleAgentArg(args, "run-now");
4076
+ const config = requireLogin(args);
4077
+ const schedule = await requireSingleSchedule(config, ref);
4078
+ const resp = await authedFetch(config, `/api/organizations/${config.orgUuid}/agents/${encodeURIComponent(ref)}/schedules/${encodeURIComponent(schedule.uuid)}/run-now`, { method: "POST" });
4079
+ if (resp.status === 401)
4080
+ await reLoginOrThrow();
4081
+ const run = (await readJsonOrThrow(resp, `Run schedule "${schedule.name}"`));
4082
+ printAgentRun(run, { json: args.options.json === true, trace: args.options.trace === true });
4083
+ }
4084
+ async function fetchAgents(config) {
4085
+ const resp = await authedFetch(config, `/api/organizations/${config.orgUuid}/agents`);
4086
+ if (resp.status === 401)
4087
+ await reLoginOrThrow();
4088
+ return (await readJsonOrThrow(resp, "List agents"));
4089
+ }
4090
+ async function fetchAgent(config, ref) {
4091
+ const resp = await authedFetch(config, `/api/organizations/${config.orgUuid}/agents/${encodeURIComponent(ref)}`);
4092
+ if (resp.status === 401)
4093
+ await reLoginOrThrow();
4094
+ return (await readJsonOrThrow(resp, `Fetch agent "${ref}"`));
4095
+ }
4096
+ async function fetchAgentSchedules(config, ref) {
4097
+ const resp = await authedFetch(config, `/api/organizations/${config.orgUuid}/agents/${encodeURIComponent(ref)}/schedules`);
4098
+ if (resp.status === 401)
4099
+ await reLoginOrThrow();
4100
+ return (await readJsonOrThrow(resp, `List schedules for agent "${ref}"`));
4101
+ }
4102
+ async function patchAgentSchedule(config, ref, scheduleUuid, body) {
4103
+ const resp = await authedFetch(config, `/api/organizations/${config.orgUuid}/agents/${encodeURIComponent(ref)}/schedules/${encodeURIComponent(scheduleUuid)}`, {
4104
+ method: "PATCH",
4105
+ headers: { "Content-Type": "application/json" },
4106
+ body: JSON.stringify(body),
4107
+ });
4108
+ if (resp.status === 401)
4109
+ await reLoginOrThrow();
4110
+ return (await readJsonOrThrow(resp, "Update agent schedule"));
4111
+ }
4112
+ async function requireSingleSchedule(config, ref) {
4113
+ const schedules = await fetchAgentSchedules(config, ref);
4114
+ if (schedules.length === 0) {
4115
+ throw new CliError(`Agent "${ref}" has no schedule.`, 1);
4116
+ }
4117
+ return schedules[0];
4118
+ }
4119
+ function readAgentManifest(args) {
4120
+ const path = optionString(args, "file");
4121
+ if (!path)
4122
+ throw new CliError("Pass --file <agent-manifest.json>.", 2);
4123
+ return readJsonObjectFile(path, "Agent manifest");
4124
+ }
4125
+ function readJsonObjectFile(path, label) {
4126
+ const resolved = resolve(process.cwd(), path);
4127
+ if (!existsSync(resolved))
4128
+ throw new CliError(`${label} file not found: ${path}`, 2);
4129
+ let parsed;
4130
+ try {
4131
+ parsed = JSON.parse(readFileSync(resolved, "utf8"));
4132
+ }
4133
+ catch (error) {
4134
+ throw new CliError(`${label} file is not valid JSON: ${error instanceof Error ? error.message : String(error)}`, 2);
4135
+ }
4136
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
4137
+ throw new CliError(`${label} file must contain a JSON object.`, 2);
4138
+ }
4139
+ return parsed;
4140
+ }
4141
+ function readJsonInput(args) {
4142
+ const inline = optionString(args, "input");
4143
+ const file = optionString(args, "inputFile");
4144
+ if (inline !== undefined && file !== undefined) {
4145
+ throw new CliError("Pass either --input or --input-file, not both.", 2);
4146
+ }
4147
+ if (inline === undefined && file === undefined)
4148
+ return null;
4149
+ const raw = inline ?? readFileSync(resolve(process.cwd(), file), "utf8");
4150
+ try {
4151
+ return JSON.parse(raw);
4152
+ }
4153
+ catch (error) {
4154
+ throw new CliError(`Agent input is not valid JSON: ${error instanceof Error ? error.message : String(error)}`, 2);
4155
+ }
4156
+ }
4157
+ function schedulePayload(args, creating) {
4158
+ const body = {};
4159
+ const name = optionString(args, "name");
4160
+ const cron = optionString(args, "cron") ?? optionString(args, "schedule");
4161
+ const timezone = optionString(args, "timezone");
4162
+ if (args.options.enabled === true && args.options.disabled === true) {
4163
+ throw new CliError("Pass either --enabled or --disabled, not both.", 2);
4164
+ }
4165
+ if (name !== undefined)
4166
+ body.name = name;
4167
+ if (cron !== undefined)
4168
+ body.schedule = cron;
4169
+ if (timezone !== undefined)
4170
+ body.timezone = timezone;
4171
+ if (args.options.enabled === true)
4172
+ body.enabled = true;
4173
+ if (args.options.disabled === true)
4174
+ body.enabled = false;
4175
+ if (creating) {
4176
+ if (!body.name)
4177
+ body.name = "default";
4178
+ if (!body.schedule)
4179
+ throw new CliError("Creating an agent schedule requires --cron.", 2);
4180
+ if (!body.timezone)
4181
+ body.timezone = "UTC";
4182
+ if (body.enabled === undefined)
4183
+ body.enabled = true;
4184
+ }
4185
+ else if (Object.keys(body).length === 0) {
4186
+ throw new CliError("Nothing to update. Pass --name, --cron, --timezone, --enabled or --disabled.", 2);
4187
+ }
4188
+ return body;
4189
+ }
4190
+ function agentArg(args, command) {
4191
+ const ref = args.rest[1];
4192
+ if (!ref || args.rest.length > 2) {
4193
+ throw new CliError(`Usage: railcode agent ${command} <agent>`, 2);
4194
+ }
4195
+ return ref;
4196
+ }
4197
+ function agentScheduleAgentArg(args, command) {
4198
+ const ref = args.rest[2];
4199
+ if (!ref || args.rest.length > 3) {
4200
+ throw new CliError(`Usage: railcode agent schedule ${command} <agent>`, 2);
4201
+ }
4202
+ return ref;
4203
+ }
4204
+ function printAgentMutation(result, json, verb) {
4205
+ if (json) {
4206
+ console.log(JSON.stringify(result, null, 2));
4207
+ return;
4208
+ }
4209
+ console.log(`Agent "${result.agent.name}" ${verb}.`);
4210
+ console.log(`UUID: ${result.agent.uuid}`);
4211
+ if (result.warnings.length > 0) {
4212
+ console.log("");
4213
+ console.log("Warnings:");
4214
+ for (const warning of result.warnings)
4215
+ console.log(`- ${warning}`);
4216
+ }
4217
+ }
4218
+ function printAgent(agent) {
4219
+ console.log(agent.name);
4220
+ console.log(`UUID: ${agent.uuid}`);
4221
+ console.log(`Description: ${agent.description || "-"}`);
4222
+ console.log(`Model: ${typeof agent.manifest.model === "string" ? agent.manifest.model : "-"}`);
4223
+ console.log(`Updated: ${agent.updated_at}`);
4224
+ console.log(`Content hash: ${agent.content_hash}`);
4225
+ }
4226
+ function printAgentRun(run, options) {
4227
+ if (options.json) {
4228
+ console.log(JSON.stringify(run, null, 2));
4229
+ return;
4230
+ }
4231
+ console.log(`Status: ${run.status}`);
4232
+ console.log(`Request: ${run.request_id}`);
4233
+ if (run.error_code || run.error_message) {
4234
+ console.log(`Error: ${[run.error_code, run.error_message].filter(Boolean).join(" - ")}`);
4235
+ }
4236
+ console.log("Output:");
4237
+ console.log(JSON.stringify(run.output_json, null, 2));
4238
+ if (options.trace) {
4239
+ console.log("");
4240
+ if (run.steps.length === 0) {
4241
+ console.log("Trace: no steps");
4242
+ }
4243
+ else {
4244
+ console.log("Trace:");
4245
+ console.log(formatTable(["#", "kind", "status", "tool", "summary"], run.steps.map((step) => [
4246
+ String(step.step_index),
4247
+ step.kind,
4248
+ step.status,
4249
+ step.tool_name ?? "-",
4250
+ step.result_summary ?? step.args_summary ?? step.error_message ?? "-",
4251
+ ])));
4252
+ }
4253
+ }
4254
+ }
4255
+ function printSchedule(schedule) {
4256
+ console.log(schedule.name);
4257
+ console.log(`UUID: ${schedule.uuid}`);
4258
+ console.log(`Cron: ${schedule.schedule}`);
4259
+ console.log(`Timezone: ${schedule.timezone}`);
4260
+ console.log(`Enabled: ${schedule.enabled ? "yes" : "no"}`);
4261
+ console.log(`Next fire: ${schedule.next_fire_at}`);
4262
+ console.log(`Last fire: ${schedule.last_fire_at ?? "-"}`);
4263
+ console.log(`Last run: ${schedule.last_run_uuid ?? "-"}`);
4264
+ }
4265
+ function assertKnownAgentOptions(args, allowed) {
4266
+ for (const key of Object.keys(args.options)) {
4267
+ if (!allowed.includes(key)) {
4268
+ throw new CliError(`Unknown option --${camelToKebab(key)} for \`railcode agent\`.\n\n${AGENT_HELP}`, 2);
4269
+ }
4270
+ }
4271
+ }
4272
+ function shortDate(value) {
4273
+ return value.includes("T") ? value.slice(0, 10) : value;
4274
+ }
4275
+ // ---------------------------------------------------------------------------
3537
4276
  // db — list data connectors + run read-only SQL (`railcode db list|query`)
3538
4277
  //
3539
4278
  // Data connectors are org-wide, so these commands use the app-less org data
@@ -3552,7 +4291,7 @@ List options:
3552
4291
 
3553
4292
  Query options:
3554
4293
  --connection <name> Connector to query (default: default)
3555
- --engine <postgres|mysql> Engine override (inferred from --connection otherwise)
4294
+ --engine <postgres|bigquery|turso> Engine override (inferred from --connection otherwise)
3556
4295
  --params '<json-array>' Values for $1, $2, … placeholders
3557
4296
  --file <path> Read SQL from a file instead of the argument
3558
4297
  --json Print the raw { columns, rows, rowcount, truncated } envelope
@@ -4903,6 +5642,10 @@ async function handleLocalApi(ctx, req, res, url) {
4903
5642
  sendJson(res, 200, [appUserEnvelope()]);
4904
5643
  return;
4905
5644
  }
5645
+ if (path === "/roles") {
5646
+ sendJson(res, 200, []);
5647
+ return;
5648
+ }
4906
5649
  if (path === "/config/design-system") {
4907
5650
  await handleDevDesignSystem(ctx, res);
4908
5651
  return;
@@ -5253,13 +5996,19 @@ async function handleDevDesignSystem(ctx, res) {
5253
5996
  // ── envelopes + http helpers ──────────────────────────────────────────────────
5254
5997
  function meEnvelope(ctx) {
5255
5998
  return {
5256
- user: { uuid: DEV_USER_UUID, name: "Local Dev", email: "dev@localhost" },
5999
+ user: {
6000
+ uuid: DEV_USER_UUID,
6001
+ name: "Local Dev",
6002
+ email: "dev@localhost",
6003
+ is_admin: true,
6004
+ roles: [],
6005
+ },
5257
6006
  app: { uuid: DEV_APP_UUID, slug: ctx.app, name: titleCase(ctx.app) },
5258
6007
  org: { uuid: DEV_ORG_UUID, slug: "local", name: "Local" },
5259
6008
  };
5260
6009
  }
5261
6010
  function appUserEnvelope() {
5262
- return { uuid: DEV_USER_UUID, name: "Local Dev", email: "dev@localhost", role: "owner" };
6011
+ return { uuid: DEV_USER_UUID, name: "Local Dev", email: "dev@localhost", is_admin: true };
5263
6012
  }
5264
6013
  function isLoopbackHost(host) {
5265
6014
  if (!host)
package/dist/manifest.js CHANGED
@@ -16,6 +16,8 @@
16
16
  // email: true
17
17
  // adhoc_sql:
18
18
  // - warehouse
19
+ // agents:
20
+ // - sales-digest
19
21
  //
20
22
  // Also supported, matching PyYAML: flow lists (`saved_queries: [a, b]`), a flow
21
23
  // map for connectors (`connectors: {stripe: [GET /v1/balance]}`), a whole-
@@ -31,7 +33,15 @@
31
33
  // test/manifest-fixtures.json (also run by
32
34
  // backend/tests/test_manifest_parse_fixtures.py) keeps the two parsers pinned.
33
35
  export const APP_MANIFEST_NAME = "manifest.yaml";
34
- const ALLOWED_KEYS = ["run_as", "saved_queries", "connectors", "llm", "email", "adhoc_sql"];
36
+ const ALLOWED_KEYS = [
37
+ "run_as",
38
+ "saved_queries",
39
+ "connectors",
40
+ "llm",
41
+ "email",
42
+ "adhoc_sql",
43
+ "agents",
44
+ ];
35
45
  const HTTP_METHODS = ["GET", "POST", "PUT", "PATCH", "DELETE", "HEAD", "OPTIONS"];
36
46
  const SAVED_QUERY_NAME = /^[a-z0-9_]{1,80}$/;
37
47
  // PyYAML's YAML 1.1 plain-scalar resolver: a bare (unquoted) scalar matching one
@@ -385,6 +395,7 @@ export function parseManifestYaml(source) {
385
395
  llm: false,
386
396
  email: false,
387
397
  adhoc_sql: [],
398
+ agents: [],
388
399
  };
389
400
  for (const [key, entry] of entries) {
390
401
  if (key === "run_as") {
@@ -404,10 +415,12 @@ export function parseManifestYaml(source) {
404
415
  doc.connectors = parseConnectors(entry);
405
416
  }
406
417
  else {
407
- // saved_queries / adhoc_sql: a list of names.
418
+ // saved_queries / adhoc_sql / agents: a list of names.
408
419
  const items = parseStringList(entry, key);
409
420
  if (key === "saved_queries")
410
421
  doc.saved_queries = items;
422
+ else if (key === "agents")
423
+ doc.agents = items;
411
424
  else
412
425
  doc.adhoc_sql = items;
413
426
  }
@@ -629,6 +642,7 @@ function validateDoc(doc, lines) {
629
642
  }
630
643
  doc.saved_queries = [...new Set(doc.saved_queries)].sort();
631
644
  doc.adhoc_sql = [...new Set(doc.adhoc_sql)].sort();
645
+ doc.agents = [...new Set(doc.agents)].sort();
632
646
  for (const name of Object.keys(doc.connectors)) {
633
647
  doc.connectors[name] = [...new Set(doc.connectors[name])].sort();
634
648
  }
@@ -673,6 +687,9 @@ export function renderManifestSummary(doc) {
673
687
  if (doc.adhoc_sql.length > 0) {
674
688
  lines.push("warning: adhoc_sql grants ad-hoc SQL authority — scarce by design; prefer saved queries");
675
689
  }
690
+ if (doc.agents.length > 0) {
691
+ lines.push(`agents: ${doc.agents.join(", ")}`);
692
+ }
676
693
  return lines.join("\n");
677
694
  }
678
695
  export function renderManifestDeployOutcome(m) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "railcode",
3
- "version": "0.1.19",
3
+ "version": "0.1.21",
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
@@ -35,7 +35,7 @@
35
35
  padding:2px 6px; border-radius:4px; white-space:nowrap; align-self:start; margin-top:2px; }
36
36
  .k-sql { background:#1e3a5f; color:#7cc4ff; } .k-db { background:#3a2752; color:#c79bff; }
37
37
  .k-files { background:#13413c; color:#5fd6c4; } .k-llm { background:#4a2b16; color:#f6b26b; }
38
- .k-me, .k-connections, .k-app-users { background:#2a2f38; color:#9aa4b2; }
38
+ .k-me, .k-connections, .k-app-users, .k-roles { background:#2a2f38; color:#9aa4b2; }
39
39
  .body { min-width:0; }
40
40
  code { font-family: ui-monospace, "SF Mono", Menlo, monospace; font-size:11.5px; color:#e6e8eb;
41
41
  word-break:break-word; display:block; line-height:1.45; }
@@ -54,7 +54,7 @@
54
54
  <button id="collapse">Hide \u203A</button>
55
55
  </header>
56
56
  <div class="log" id="log"></div>
57
- <footer>Calls made via <code>postgres()</code>, <code>llm</code>, <code>db</code>, <code>files</code>, <code>me()</code>, <code>appUsers()</code></footer>
57
+ <footer>Calls made via <code>postgres()</code>, <code>llm</code>, <code>db</code>, <code>files</code>, <code>me()</code>, <code>appUsers()</code>, <code>roles()</code></footer>
58
58
  </div>`;
59
59
  function mount() {
60
60
  (document.body || document.documentElement).appendChild(host);
@@ -219,6 +219,15 @@
219
219
  }();
220
220
  }
221
221
 
222
+ // ../sdk/src/agents.ts
223
+ async function doInvoke(name, input) {
224
+ return call("POST", `/agents/${encodeURIComponent(name)}`, { body: { input } });
225
+ }
226
+ function invoke(name, input) {
227
+ return track("agents", `agents.invoke(${q(name)})`, () => doInvoke(name, input));
228
+ }
229
+ var agents = { invoke };
230
+
222
231
  // ../sdk/src/databases.ts
223
232
  var QUERY_LABEL_MAX = 80;
224
233
  var PARAMS_LABEL_MAX = 40;
@@ -256,7 +265,7 @@
256
265
  var data = namespace();
257
266
  var postgres = namespace("postgres");
258
267
  var bigquery = namespace("bigquery");
259
- var snowflake = namespace("snowflake");
268
+ var turso = namespace("turso");
260
269
  var dataConnectors = () => track("connections", "dataConnectors()", () => call("GET", "/connections"));
261
270
 
262
271
  // ../sdk/src/util.ts
@@ -268,9 +277,18 @@
268
277
  }
269
278
 
270
279
  // ../sdk/src/db.ts
280
+ function scopeParams(s) {
281
+ return { scope: s.scope, role: s.role };
282
+ }
283
+ function scopeLabel(s) {
284
+ if (s.scope === "user") return "db.user";
285
+ if (s.scope === "role") return `db.role(${q(s.role ?? "")})`;
286
+ return "db.shared";
287
+ }
271
288
  var Query = class {
272
- constructor(collection) {
289
+ constructor(collection, scope) {
273
290
  this.collection = collection;
291
+ this.scope = scope;
274
292
  this._where = [];
275
293
  }
276
294
  where(field, op, value) {
@@ -300,11 +318,12 @@
300
318
  prefix: this._prefix,
301
319
  updated_since: this._since,
302
320
  updated_before: this._before,
321
+ ...scopeParams(this.scope),
303
322
  ...extra
304
323
  };
305
324
  }
306
325
  label(method) {
307
- return `db.collection(${q(this.collection)}).${method}`;
326
+ return `${scopeLabel(this.scope)}.collection(${q(this.collection)}).${method}`;
308
327
  }
309
328
  async fetchPage(page, size) {
310
329
  const resp = await call("GET", `/kv/${encPath(this.collection)}`, {
@@ -331,16 +350,20 @@
331
350
  }
332
351
  };
333
352
  var Collection = class {
334
- constructor(name) {
353
+ constructor(name, scope) {
335
354
  this.name = name;
355
+ this.scope = scope;
336
356
  }
337
357
  label(method) {
338
- return `db.collection(${q(this.name)}).${method}`;
358
+ return `${scopeLabel(this.scope)}.collection(${q(this.name)}).${method}`;
339
359
  }
340
360
  /** The stored value for ``key``, or null if absent. */
341
361
  get(key) {
342
362
  return track("db", this.label(`get(${q(key)})`), async () => {
343
- const record = await getOrNull(`/kv/${encPath(this.name)}/${encPath(key)}`);
363
+ const record = await getOrNull(
364
+ `/kv/${encPath(this.name)}/${encPath(key)}`,
365
+ scopeParams(this.scope)
366
+ );
344
367
  return record ? record.value : null;
345
368
  });
346
369
  }
@@ -348,7 +371,8 @@
348
371
  put(key, value) {
349
372
  return track("db", this.label(`put(${q(key)}, ${shorten(JSON.stringify(value), 40)})`), async () => {
350
373
  const record = await call("PUT", `/kv/${encPath(this.name)}/${encPath(key)}`, {
351
- body: { value }
374
+ body: { value },
375
+ query: scopeParams(this.scope)
352
376
  });
353
377
  return record.value;
354
378
  });
@@ -357,18 +381,22 @@
357
381
  return track(
358
382
  "db",
359
383
  this.label(`delete(${q(key)})`),
360
- () => call("DELETE", `/kv/${encPath(this.name)}/${encPath(key)}`)
384
+ () => call("DELETE", `/kv/${encPath(this.name)}/${encPath(key)}`, {
385
+ query: scopeParams(this.scope)
386
+ })
361
387
  );
362
388
  }
363
389
  /** All records in the collection (first page). */
364
390
  list() {
365
391
  return track("db", this.label("list()"), async () => {
366
- const resp = await call("GET", `/kv/${encPath(this.name)}`);
392
+ const resp = await call("GET", `/kv/${encPath(this.name)}`, {
393
+ query: scopeParams(this.scope)
394
+ });
367
395
  return resp.items;
368
396
  });
369
397
  }
370
398
  query() {
371
- return new Query(this.name);
399
+ return new Query(this.name, this.scope);
372
400
  }
373
401
  where(field, op, value) {
374
402
  return this.query().where(field, op, value);
@@ -377,8 +405,15 @@
377
405
  return this.query().prefix(value);
378
406
  }
379
407
  };
408
+ function namespace2(scope) {
409
+ return { collection: (name) => new Collection(name, scope) };
410
+ }
411
+ var shared = namespace2({});
380
412
  var db = {
381
- collection: (name) => new Collection(name)
413
+ shared,
414
+ user: namespace2({ scope: "user" }),
415
+ role: (roleUuid) => namespace2({ scope: "role", role: roleUuid }),
416
+ collection: shared.collection
382
417
  };
383
418
 
384
419
  // ../sdk/src/design-system.ts
@@ -414,18 +449,41 @@
414
449
  var email = { send: send2 };
415
450
 
416
451
  // ../sdk/src/files.ts
452
+ var URL_EXPIRY_MARGIN_MS = 15e3;
453
+ var urlCache = /* @__PURE__ */ new Map();
454
+ function scopeParams2(s) {
455
+ return { scope: s.scope, role: s.role };
456
+ }
457
+ function scopeKey(s) {
458
+ if (s.scope === "user") return "user";
459
+ if (s.scope === "role") return `role:${s.role ?? ""}`;
460
+ return "shared";
461
+ }
462
+ function scopeLabel2(s) {
463
+ if (s.scope === "user") return "files.user";
464
+ if (s.scope === "role") return `files.role(${q(s.role ?? "")})`;
465
+ return "files.shared";
466
+ }
467
+ function cacheKey(s, name) {
468
+ return `${scopeKey(s)} ${name}`;
469
+ }
417
470
  function contentTypeOf(data2, explicit) {
418
471
  if (explicit) return explicit;
419
472
  if (data2 instanceof Blob && data2.type) return data2.type;
420
473
  return "application/octet-stream";
421
474
  }
422
- function upload(name, data2, contentType) {
423
- return track("files", `files.upload(${q(name)}, <blob>)`, () => doUpload(name, data2, contentType));
475
+ function upload(scope, name, data2, contentType) {
476
+ return track("files", `${scopeLabel2(scope)}.upload(${q(name)}, <blob>)`, async () => {
477
+ const result = await doUpload(scope, name, data2, contentType);
478
+ urlCache.delete(cacheKey(scope, name));
479
+ return result;
480
+ });
424
481
  }
425
- async function doUpload(name, data2, contentType) {
482
+ async function doUpload(scope, name, data2, contentType) {
426
483
  const ct = contentTypeOf(data2, contentType);
427
484
  const target2 = await call("POST", "/files", {
428
- body: { name, content_type: ct }
485
+ body: { name, content_type: ct },
486
+ query: scopeParams2(scope)
429
487
  });
430
488
  const headers = { "Content-Type": ct, ...target2.headers };
431
489
  if (target2.mode === "proxy") {
@@ -440,21 +498,79 @@
440
498
  }
441
499
  const resp = await fetch(target2.url, { method: target2.method, body: data2, headers });
442
500
  if (!resp.ok) throw new Error(`upload failed: ${resp.status}`);
443
- return call("POST", "/files/finalize", { body: { name } });
501
+ return call("POST", "/files/finalize", { body: { name }, query: scopeParams2(scope) });
444
502
  }
445
- function url(name) {
446
- return buildUrl(`/files/${encPath(name)}`);
503
+ function url(scope, name) {
504
+ return buildUrl(`/files/${encPath(name)}`, scopeParams2(scope));
447
505
  }
448
- var list = () => track("files", "files.list()", async () => {
449
- const resp = await call("GET", "/files");
450
- return resp.items;
451
- });
452
- var remove = (name) => track("files", `files.delete(${q(name)})`, () => call("DELETE", `/files/${encPath(name)}`));
453
- var files = { upload, url, list, delete: remove };
506
+ async function urls(scope, names) {
507
+ return track("files", `${scopeLabel2(scope)}.urls(<${names.length} names>)`, async () => {
508
+ const unique = [...new Set(names)];
509
+ const now = Date.now();
510
+ const items = /* @__PURE__ */ new Map();
511
+ const unresolved = [];
512
+ for (const name of unique) {
513
+ const cached = urlCache.get(cacheKey(scope, name));
514
+ if (cached && cached.expiresAt > now + URL_EXPIRY_MARGIN_MS) items.set(name, cached.item);
515
+ else {
516
+ urlCache.delete(cacheKey(scope, name));
517
+ unresolved.push(name);
518
+ }
519
+ }
520
+ let missing = [];
521
+ if (unresolved.length) {
522
+ const resolved = await call("POST", "/files/urls", {
523
+ body: { names: unresolved },
524
+ query: scopeParams2(scope)
525
+ });
526
+ missing = resolved.missing;
527
+ for (const item of resolved.items) {
528
+ const normalized = { ...item, url: new URL(item.url, location.origin).toString() };
529
+ const expiresAt = item.expires_in === null ? Number.POSITIVE_INFINITY : now + item.expires_in * 1e3;
530
+ urlCache.set(cacheKey(scope, item.name), { item: normalized, expiresAt });
531
+ items.set(item.name, normalized);
532
+ }
533
+ }
534
+ return { items: unique.flatMap((name) => items.has(name) ? [items.get(name)] : []), missing };
535
+ });
536
+ }
537
+ function list(scope) {
538
+ return track("files", `${scopeLabel2(scope)}.list()`, async () => {
539
+ const resp = await call("GET", "/files", { query: scopeParams2(scope) });
540
+ return resp.items;
541
+ });
542
+ }
543
+ function remove(scope, name) {
544
+ return track("files", `${scopeLabel2(scope)}.delete(${q(name)})`, async () => {
545
+ await call("DELETE", `/files/${encPath(name)}`, { query: scopeParams2(scope) });
546
+ urlCache.delete(cacheKey(scope, name));
547
+ });
548
+ }
549
+ function fileNamespace(scope) {
550
+ return {
551
+ upload: (name, data2, contentType) => upload(scope, name, data2, contentType),
552
+ url: (name) => url(scope, name),
553
+ urls: (names) => urls(scope, names),
554
+ list: () => list(scope),
555
+ delete: (name) => remove(scope, name)
556
+ };
557
+ }
558
+ var shared2 = fileNamespace({});
559
+ var files = {
560
+ shared: shared2,
561
+ user: fileNamespace({ scope: "user" }),
562
+ role: (roleUuid) => fileNamespace({ scope: "role", role: roleUuid }),
563
+ upload: shared2.upload,
564
+ url: shared2.url,
565
+ urls: shared2.urls,
566
+ list: shared2.list,
567
+ delete: shared2.delete
568
+ };
454
569
 
455
570
  // ../sdk/src/identity.ts
456
571
  var me = () => track("me", "me()", () => call("GET", "/me"));
457
572
  var appUsers = () => track("app-users", "appUsers()", () => call("GET", "/app-users"));
573
+ var roles = () => track("roles", "roles()", () => call("GET", "/roles"));
458
574
 
459
575
  // ../sdk/src/llm.ts
460
576
  function label2(method, input, opts = {}) {
@@ -580,6 +696,7 @@
580
696
  target.files = files;
581
697
  target.me = me;
582
698
  target.appUsers = appUsers;
699
+ target.roles = roles;
583
700
  target.designSystem = designSystem;
584
701
  target.email = email;
585
702
  target.llm = llm;
@@ -587,10 +704,11 @@
587
704
  target.data = data;
588
705
  target.postgres = postgres;
589
706
  target.bigquery = bigquery;
590
- target.snowflake = snowflake;
707
+ target.turso = turso;
591
708
  target.dataConnectors = dataConnectors;
592
709
  target.connector = connector;
593
710
  target.serviceConnectors = serviceConnectors;
594
711
  target.query = query;
595
712
  target.savedQueries = savedQueries;
713
+ target.agents = agents;
596
714
  })();