railcode 0.1.26 → 0.1.27

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
@@ -19,7 +19,9 @@ on your `PATH`.
19
19
  ```
20
20
  railcode login [--api-url <url>] Sign in and mint a personal API token
21
21
  railcode login --setup-token <token> Sign in with a one-time onboarding setup token
22
- railcode init <app> [--template react|static]
22
+ railcode init <app> [dir] [--template react|static] [--force]
23
+ Scaffold an app. dir defaults to ./<app>;
24
+ pass "." to scaffold into the current dir
23
25
  railcode deploy Build (if configured) and deploy the app here
24
26
  railcode db <list|query> ... List data connectors / run read-only SQL
25
27
  railcode connections <list|create|delete> ... Manage org data connectors (admin)
@@ -54,7 +56,11 @@ your saved org and are gated server-side by the same capabilities as the console
54
56
  a personal API token: the CLI exchanges it once for a personal API token and
55
57
  writes the same config as a browser login. If it's expired or already used,
56
58
  generate a fresh prompt from the dashboard.
57
- - **`init <app>`** scaffolds a directory. By default it creates a React + Vite +
59
+ - **`init <app> [dir]`** scaffolds a directory. `dir` is optional and defaults to
60
+ `./<app>`; pass `.` to scaffold into the current directory, or any path to
61
+ scaffold into it. The target need not be empty (same-named files are
62
+ overwritten), but an existing `railcode.json` is preserved unless you pass
63
+ `--force`. By default it creates a React + Vite +
58
64
  Zustand starter — a personalized "Welcome to Railcode, `<name>`" home wired to
59
65
  every SDK capability: identity/team, a live to-dos list and file uploads (work
60
66
  with zero setup), and SQL, saved queries, service connectors, and LLM cards that
@@ -80,15 +86,15 @@ your saved org and are gated server-side by the same capabilities as the console
80
86
  (SQL is never interpolated), `--file <path>` reads SQL from a file, and
81
87
  `--json` prints the raw `{ columns, rows, rowcount, truncated }` envelope.
82
88
  - **`agent`** manages org-scoped managed agents when your permissions allow it.
83
- Manifests are JSON files in the same shape the API stores under
89
+ Manifests are JSON or YAML files in the same shape the API stores under
84
90
  `agent.manifest`.
85
91
  - `railcode agent list` lists agents; `--json` prints raw `AgentOut[]`.
86
92
  - `railcode agent show <name|uuid>` prints one agent; `--manifest` prints only
87
93
  its manifest JSON.
88
94
  - `railcode agent pull <name|uuid> --output agent.json` writes the existing
89
95
  manifest for editing.
90
- - `railcode agent create --file agent.json` creates an agent; `update <agent>
91
- --file agent.json` replaces an existing agent manifest; `delete <agent> --yes`
96
+ - `railcode agent create --file agent.yaml` creates an agent; `update <agent>
97
+ --file agent.yaml` replaces an existing agent manifest; `delete <agent> --yes`
92
98
  archives it. `--visibility <org|personal>` on `create`/`update`/`test` sets or
93
99
  changes who the agent belongs to (default `org` on create; omitted on `update`
94
100
  leaves it alone). A `personal` agent is invokable and manageable by its
@@ -96,7 +102,7 @@ your saved org and are gated server-side by the same capabilities as the console
96
102
  once created. Requires `agent:create` (personal) or `agent:create_org` (org).
97
103
  `agent show`/`list` print `visibility`, and `show` prints `Owner:` for a
98
104
  personal agent.
99
- - `railcode agent test --file agent.json --input '{"k":"v"}'` runs a draft
105
+ - `railcode agent test --file agent.yaml --input '{"k":"v"}'` runs a draft
100
106
  manifest without saving; `railcode agent run <agent> --input '{"k":"v"}'`
101
107
  invokes a saved agent and prints the result. Use `--trace` for the step trace
102
108
  and `--json` for the raw run detail.
@@ -104,7 +110,7 @@ your saved org and are gated server-side by the same capabilities as the console
104
110
  creates or updates the agent's one schedule. `schedule show`, `pause`,
105
111
  `resume`, `delete --yes`, and `run-now` operate on that single schedule.
106
112
  - **`personal-connectors`** (aliases `personal-connector`, `pc`) manages **your
107
- own** connected third-party accounts (Gmail, Slack, …), brokered by Composio
113
+ own** connected third-party accounts (Gmail, Slack, …), brokered by railcode (in-house)
108
114
  distinct from org-admin **service connectors** and from an agent's
109
115
  `tools.personal_connectors`. `list` shows toolkits this deployment brokers and
110
116
  your connection status; `tools <toolkit>` prints that toolkit's callable tools
package/dist/index.js CHANGED
@@ -10,6 +10,7 @@ import { dirname, join, relative, resolve, sep } from "node:path";
10
10
  import process from "node:process";
11
11
  import { createInterface } from "node:readline/promises";
12
12
  import { fileURLToPath } from "node:url";
13
+ import { parse as parseYaml } from "yaml";
13
14
  import { KvQueryError, kvCount, kvQuery, } from "./dev/kv-query.js";
14
15
  import { DbError, formatTable, inferEngine, parseEngine, parseSqlParams, pickSqlSource, } from "./db.js";
15
16
  import { QueryCmdError, paramSignature, parseParamDecls, parseRunParams, pickAuthoringSql, } from "./query.js";
@@ -35,11 +36,13 @@ const HELP = `railcode ${VERSION}
35
36
  Developer CLI for the multi-tenant Railcode platform.
36
37
 
37
38
  Usage:
38
- railcode login [--api-url <url>] Sign in and mint a personal API token
39
+ railcode login [--api-url <url>] [--paste] Sign in and mint a personal API token
40
+ (--paste / --no-browser: skip the localhost
41
+ callback, paste a code instead — for SSH/headless)
39
42
  railcode login --setup-token <token> Sign in with a one-time onboarding setup token
40
43
  railcode deploy [--private] Build (if configured) and deploy the app here
41
44
  railcode dev [--port <n>] [--reset] Run the app locally against an emulated /_api
42
- railcode init <app> [--template react|static]
45
+ railcode init <app> [dir] [--template react|static] [--force]
43
46
  railcode design-system Print your org's design-system guidance (markdown)
44
47
  railcode db <list|query> ... List data connectors / run read-only SQL
45
48
  railcode query <list|run|create|update|delete> ... Saved queries: invoke by name / author (admin)
@@ -63,7 +66,8 @@ Usage:
63
66
  railcode --help
64
67
 
65
68
  Config: ~/.railcode/config.json
66
- API URL resolves from --api-url, then RAILCODE_API_URL, then saved config.
69
+ API URL resolves from --api-url, then RAILCODE_API_URL, then saved config, then
70
+ the default (${DEFAULT_API_URL}) — never prompted.
67
71
  `;
68
72
  // ---------------------------------------------------------------------------
69
73
  // Entry point
@@ -212,9 +216,12 @@ async function commandLogin(args) {
212
216
  await loginWithSetupToken(args, saved, setupToken);
213
217
  return;
214
218
  }
215
- const apiUrl = await resolveLoginApiUrl(args, saved);
219
+ const apiUrl = resolveLoginApiUrl(args, saved);
220
+ // --paste / --no-browser: skip the localhost callback entirely and go straight
221
+ // to "open this URL, paste the code" — the right default for headless/SSH.
222
+ const pasteMode = args.options.paste === true || args.options.noBrowser === true;
216
223
  const serverConfig = (await readJsonOrThrow(await apiFetch(apiUrl, "/api/config", {}), "Server config"));
217
- const minted = await authorizeCliWithBrowser(apiUrl);
224
+ const minted = await authorizeCliWithBrowser(apiUrl, pasteMode);
218
225
  // Resolve the caller's identity + organization.
219
226
  const me = (await readJsonOrThrow(await apiFetch(apiUrl, "/api/auth/me", {
220
227
  headers: { Authorization: `Bearer ${minted.token}` },
@@ -282,10 +289,16 @@ async function loginWithSetupToken(args, saved, setupToken) {
282
289
  }
283
290
  console.log(`Token ${redeemed.token_prefix}... saved to ${CONFIG_PATH}.`);
284
291
  }
285
- async function authorizeCliWithBrowser(apiUrl) {
292
+ async function authorizeCliWithBrowser(apiUrl, pasteMode) {
286
293
  if (!process.stdin.isTTY) {
287
294
  throw new CliError("Browser login requires a TTY. Set RAILCODE_API_TOKEN in non-interactive environments.", 2);
288
295
  }
296
+ if (pasteMode) {
297
+ return authorizeCliByPaste(apiUrl);
298
+ }
299
+ // Happy path: a localhost callback server catches the code automatically. We
300
+ // also print a code-mode URL so login still completes when the browser is on a
301
+ // different machine and can't reach this box's localhost (the common SSH case).
289
302
  const state = randomBytes(16).toString("hex");
290
303
  const callback = await startCliAuthCallback(state);
291
304
  try {
@@ -293,23 +306,80 @@ async function authorizeCliWithBrowser(apiUrl) {
293
306
  redirect_uri: callback.redirectUri,
294
307
  state,
295
308
  })}`;
296
- console.log("");
297
- printUrlBox("Railcode login", authorizeUrl);
309
+ const manualUrl = `${apiUrl}/api/auth/cli/authorize?${new URLSearchParams({ mode: "code" })}`;
298
310
  console.log("");
299
311
  if (openBrowser(authorizeUrl)) {
300
- console.log("Opening your browser. If it does not open, paste the URL above.");
312
+ console.log("Opening your browser to authorize Railcode.");
301
313
  }
302
314
  else {
303
- console.log("Paste the URL above into your browser to continue.");
315
+ console.log("Open this URL in your browser to authorize Railcode:");
316
+ console.log(` ${authorizeUrl}`);
304
317
  }
305
- console.log("Waiting for browser authorization...");
306
- const code = await callback.code;
318
+ console.log("");
319
+ console.log("On a remote/SSH box? If the browser can't reach this machine, open the");
320
+ console.log("URL below to get a code, then paste it here:");
321
+ console.log("");
322
+ printUrlBox("Railcode login", manualUrl);
323
+ console.log("");
324
+ const code = await waitForCallbackOrPastedCode(callback.code);
307
325
  return await exchangeCliAuthCode(apiUrl, code);
308
326
  }
309
327
  finally {
310
328
  await callback.close();
311
329
  }
312
330
  }
331
+ // Paste-only flow (--paste / --no-browser): no localhost server, no waiting on a
332
+ // callback. Show the code-mode URL and read the pasted code back.
333
+ async function authorizeCliByPaste(apiUrl) {
334
+ const manualUrl = `${apiUrl}/api/auth/cli/authorize?${new URLSearchParams({ mode: "code" })}`;
335
+ console.log("");
336
+ printUrlBox("Railcode login", manualUrl);
337
+ console.log("");
338
+ if (openBrowser(manualUrl)) {
339
+ console.log("Opening your browser. If it does not open, paste the URL above.");
340
+ }
341
+ else {
342
+ console.log("Paste the URL above into your browser to continue.");
343
+ }
344
+ console.log("");
345
+ const code = (await prompt("Paste the code from your browser: ")).trim();
346
+ if (!code) {
347
+ throw new CliError("No code entered.", 1);
348
+ }
349
+ return exchangeCliAuthCode(apiUrl, code);
350
+ }
351
+ // Race the localhost callback against a pasted line — whichever yields a code
352
+ // first wins. An empty line means "keep waiting", so we re-prompt. When the
353
+ // callback wins, the abort signal cancels the pending readline question.
354
+ async function waitForCallbackOrPastedCode(callbackCode) {
355
+ const controller = new AbortController();
356
+ const rl = createInterface({ input: process.stdin, output: process.stdout });
357
+ const fromCallback = callbackCode.then((code) => {
358
+ controller.abort();
359
+ return code;
360
+ }, (error) => {
361
+ controller.abort();
362
+ throw error;
363
+ });
364
+ const fromPaste = (async () => {
365
+ for (;;) {
366
+ const answer = (await rl.question("Paste the code from your browser (or press Enter to keep waiting): ", {
367
+ signal: controller.signal,
368
+ })).trim();
369
+ if (answer)
370
+ return answer;
371
+ }
372
+ })();
373
+ try {
374
+ return await Promise.race([fromCallback, fromPaste]);
375
+ }
376
+ finally {
377
+ controller.abort();
378
+ rl.close();
379
+ fromCallback.catch(() => { });
380
+ fromPaste.catch(() => { });
381
+ }
382
+ }
313
383
  async function startCliAuthCallback(expectedState) {
314
384
  let resolveCode = () => { };
315
385
  let rejectCode = () => { };
@@ -443,13 +513,12 @@ function escapeHtml(value) {
443
513
  .replaceAll('"', "&quot;")
444
514
  .replaceAll("'", "&#39;");
445
515
  }
446
- async function resolveLoginApiUrl(args, saved) {
447
- const explicit = optionApiUrl(args) || process.env.RAILCODE_API_URL;
448
- if (explicit)
449
- return normalizeApiOrigin(explicit);
450
- const fallback = saved.apiUrl || DEFAULT_API_URL;
451
- const answer = await prompt(`Server URL [${fallback}]: `);
452
- return normalizeApiOrigin(answer.trim() || fallback);
516
+ // Resolve the API URL non-interactively — no prompt. Precedence:
517
+ // --api-url flag RAILCODE_API_URL env → saved config → DEFAULT_API_URL.
518
+ // The default "just works"; override it only with --api-url (env/saved kept for
519
+ // continuity). Mirrors the setup-token path's resolution in loginWithSetupToken.
520
+ function resolveLoginApiUrl(args, saved) {
521
+ return normalizeApiOrigin(optionApiUrl(args) || process.env.RAILCODE_API_URL || saved.apiUrl || DEFAULT_API_URL);
453
522
  }
454
523
  // ---------------------------------------------------------------------------
455
524
  // deploy
@@ -700,12 +769,19 @@ function packageBuildCommand(root) {
700
769
  async function commandInit(args) {
701
770
  const app = args.rest[0];
702
771
  if (!app)
703
- throw new CliError("Usage: railcode init <app>", 2);
772
+ throw new CliError("Usage: railcode init <app> [dir]", 2);
704
773
  assertAppName(app);
705
774
  const template = initTemplate(args);
706
- const target = resolve(process.cwd(), app);
707
- if (existsSync(target) && readdirSync(target).length > 0) {
708
- throw new CliError(`Refusing to scaffold into a non-empty directory: ${target}`, 1);
775
+ // Optional second positional: the directory to scaffold into. Defaults to a
776
+ // new "<app>/" subdir. Passing "." scaffolds into the current directory. The
777
+ // target need not be empty same-named files are overwritten.
778
+ const dir = args.rest[1];
779
+ const target = resolve(process.cwd(), dir ?? app);
780
+ // Light-touch guard: never clobber an existing app manifest without --force.
781
+ const manifestPath = join(target, MANIFEST_NAME);
782
+ if (existsSync(manifestPath) && !args.options.force) {
783
+ throw new CliError(`A ${MANIFEST_NAME} already exists in ${relativeLabel(process.cwd(), target)}/. ` +
784
+ "Re-run with --force to overwrite it.", 1);
709
785
  }
710
786
  mkdirSync(target, { recursive: true });
711
787
  if (template === "static") {
@@ -716,7 +792,10 @@ async function commandInit(args) {
716
792
  }
717
793
  console.log(`Created ${template} Railcode app "${app}" in ${relativeLabel(process.cwd(), target)}/`);
718
794
  console.log("Next:");
719
- console.log(` cd ${app}`);
795
+ // Only prompt a `cd` when the files landed somewhere other than the cwd.
796
+ if (resolve(target) !== resolve(process.cwd())) {
797
+ console.log(` cd ${relativeLabel(process.cwd(), target)}`);
798
+ }
720
799
  if (template === "react") {
721
800
  console.log(" npm install");
722
801
  }
@@ -970,7 +1049,6 @@ type LlmOptions = {
970
1049
  provider?: string;
971
1050
  system?: string;
972
1051
  output?: LlmOutputSpec;
973
- temperature?: number;
974
1052
  maxOutputTokens?: number;
975
1053
  metadata?: Record<string, unknown>;
976
1054
  };
@@ -3794,8 +3872,8 @@ Usage:
3794
3872
  railcode agent show <agent> Show one agent
3795
3873
  railcode agent pull <agent> [--output <path>]
3796
3874
  Write the agent manifest as JSON
3797
- railcode agent create --file <path> Create an agent from a JSON manifest
3798
- railcode agent update <agent> --file <path> Update an agent from a JSON manifest
3875
+ railcode agent create --file <path> Create an agent from a JSON or YAML manifest
3876
+ railcode agent update <agent> --file <path> Update an agent from a JSON or YAML manifest
3799
3877
  railcode agent delete <agent> [--yes] Delete/archive an agent
3800
3878
  railcode agent test --file <path> [opts] Run a draft manifest without saving
3801
3879
  railcode agent run <agent> [opts] Trigger a saved agent and print results
@@ -3803,7 +3881,7 @@ Usage:
3803
3881
 
3804
3882
  Agent options:
3805
3883
  --json Print the raw API response
3806
- --file <path> JSON manifest file
3884
+ --file <path> JSON or YAML manifest file
3807
3885
  --output <path> Output path for pull (default stdout)
3808
3886
  --input '<json>' Invoke/test input JSON (default null)
3809
3887
  --input-file <path> Read invoke/test input JSON from a file
@@ -3823,7 +3901,7 @@ Schedule options:
3823
3901
  --timezone <iana> IANA timezone (default: UTC on create)
3824
3902
  --enabled / --disabled Initial or updated enabled state
3825
3903
 
3826
- Manifests are JSON files in the same shape the API stores under agent.manifest.
3904
+ Manifests are JSON or YAML files in the same shape the API stores under agent.manifest.
3827
3905
  Schedules are currently one per agent, so "set" creates the schedule if missing
3828
3906
  or updates the existing one.
3829
3907
  `;
@@ -4185,8 +4263,9 @@ async function requireSingleSchedule(config, ref) {
4185
4263
  function readAgentManifest(args) {
4186
4264
  const path = optionString(args, "file");
4187
4265
  if (!path)
4188
- throw new CliError("Pass --file <agent-manifest.json>.", 2);
4189
- return readJsonObjectFile(path, "Agent manifest");
4266
+ throw new CliError("Pass --file <agent-manifest.json|yaml|yml>.", 2);
4267
+ const format = /\.ya?ml$/i.test(path) ? "YAML" : "JSON";
4268
+ return readObjectFile(path, "Agent manifest", format);
4190
4269
  }
4191
4270
  // Omitted means "use the server default" on create/test (org) and "leave alone"
4192
4271
  // on update — never send the key unless the caller passed it explicitly.
@@ -4200,18 +4279,22 @@ function agentVisibilityOption(args) {
4200
4279
  return value;
4201
4280
  }
4202
4281
  function readJsonObjectFile(path, label) {
4282
+ return readObjectFile(path, label, "JSON");
4283
+ }
4284
+ function readObjectFile(path, label, format) {
4203
4285
  const resolved = resolve(process.cwd(), path);
4204
4286
  if (!existsSync(resolved))
4205
4287
  throw new CliError(`${label} file not found: ${path}`, 2);
4206
4288
  let parsed;
4207
4289
  try {
4208
- parsed = JSON.parse(readFileSync(resolved, "utf8"));
4290
+ const raw = readFileSync(resolved, "utf8");
4291
+ parsed = format === "YAML" ? parseYaml(raw) : JSON.parse(raw);
4209
4292
  }
4210
4293
  catch (error) {
4211
- throw new CliError(`${label} file is not valid JSON: ${error instanceof Error ? error.message : String(error)}`, 2);
4294
+ throw new CliError(`${label} file is not valid ${format}: ${error instanceof Error ? error.message : String(error)}`, 2);
4212
4295
  }
4213
4296
  if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
4214
- throw new CliError(`${label} file must contain a JSON object.`, 2);
4297
+ throw new CliError(`${label} file must contain a ${format} object.`, 2);
4215
4298
  }
4216
4299
  return parsed;
4217
4300
  }
@@ -5148,7 +5231,9 @@ const PC_HELP = `railcode personal-connectors — your own connected accounts (G
5148
5231
  Usage:
5149
5232
  railcode personal-connectors list Toolkits this deployment brokers + your status
5150
5233
  railcode personal-connectors tools <toolkit> The callable tools of a toolkit (and their inputs)
5151
- railcode personal-connectors connect <toolkit> Print the OAuth URL to connect it (open in a browser)
5234
+ railcode personal-connectors connect <toolkit> Start connecting it: prints the OAuth URL, or
5235
+ reports if it needs a token
5236
+ railcode personal-connectors connect <toolkit> --token '<value>' Link a token-auth connector (e.g. a GitHub PAT)
5152
5237
  railcode personal-connectors call <toolkit> <tool> [--args '<json>'] Run one tool on your own account
5153
5238
 
5154
5239
  Aliases: personal-connector, pc
@@ -5159,6 +5244,7 @@ input schema, so you know what to put in an app's \`personal_connectors:\` and w
5159
5244
 
5160
5245
  Options:
5161
5246
  --json Print raw JSON instead of a table
5247
+ --token '<value>' Token/key for \`connect\` on a token-auth connector
5162
5248
  --args '<json>' Arguments object for \`call\` (default {})
5163
5249
  `;
5164
5250
  async function commandPersonalConnectors(args) {
@@ -5237,13 +5323,31 @@ async function commandPcTools(args) {
5237
5323
  console.log(`\n${tools.length} tool(s). Add \`--json\` to see each tool's input schema.`);
5238
5324
  }
5239
5325
  async function commandPcConnect(args) {
5240
- rejectUnknownOptions(args, ["json", "apiUrl"], "personal-connectors connect", PC_HELP);
5326
+ rejectUnknownOptions(args, ["json", "token", "apiUrl"], "personal-connectors connect", PC_HELP);
5241
5327
  const toolkit = args.rest[1];
5242
5328
  if (!toolkit)
5243
5329
  throw new CliError("Usage: railcode personal-connectors connect <toolkit>", 2);
5244
5330
  const config = requireLogin(args);
5245
- // Connecting is browser-bound OAuth; the CLI can't complete it. It prints the
5246
- // URL and leaves opening it to you — the same handoff `railcode login` uses.
5331
+ // A token-auth connector (e.g. a GitHub PAT): `--token <value>` submits it
5332
+ // directly, otherwise we tell the caller a token is needed.
5333
+ if (typeof args.options.token === "string") {
5334
+ const tokResp = await authedFetch(config, `/api/personal-connections/${encodeURIComponent(toolkit)}/token`, {
5335
+ method: "POST",
5336
+ headers: { "Content-Type": "application/json" },
5337
+ body: JSON.stringify({ token: args.options.token }),
5338
+ });
5339
+ if (tokResp.status === 401)
5340
+ await reLoginOrThrow();
5341
+ if (pcUnavailable(tokResp.status)) {
5342
+ console.log("Personal connectors are not enabled on this deployment.");
5343
+ return;
5344
+ }
5345
+ await readJsonOrThrow(tokResp, `Connect ${toolkit}`);
5346
+ console.log(`Connected ${toolkit}.`);
5347
+ return;
5348
+ }
5349
+ // Otherwise start the flow. OAuth is browser-bound (the CLI can't complete it),
5350
+ // so it prints the URL; a token connector reports that a token is required.
5247
5351
  const resp = await authedFetch(config, `/api/personal-connections/${encodeURIComponent(toolkit)}/connect`, { method: "POST" });
5248
5352
  if (resp.status === 401)
5249
5353
  await reLoginOrThrow();
@@ -5256,6 +5360,11 @@ async function commandPcConnect(args) {
5256
5360
  console.log(JSON.stringify(body, null, 2));
5257
5361
  return;
5258
5362
  }
5363
+ if (body.mode === "token") {
5364
+ console.log(`${toolkit} connects with a ${body.token_label ?? "token"}. ` +
5365
+ `Re-run with --token '<value>' to link it:\n\n railcode personal-connectors connect ${toolkit} --token '<value>'\n`);
5366
+ return;
5367
+ }
5259
5368
  console.log(`Open this URL to connect ${toolkit}, then re-run your command:\n\n ${body.redirect_url}\n`);
5260
5369
  }
5261
5370
  async function commandPcCall(args) {
package/dist/manifest.js CHANGED
@@ -48,7 +48,7 @@ const SAVED_QUERY_NAME = /^[a-z0-9_]{1,80}$/;
48
48
  // Mirrors the server's _PERSONAL_CONNECTOR_RE exactly. The server is
49
49
  // authoritative; this exists so `railcode deploy` can reject a bad manifest
50
50
  // locally instead of after a round trip.
51
- const PERSONAL_CONNECTOR = /^[a-z0-9][a-z0-9_]{0,63}(?::(?:\*|[A-Z0-9_]{1,120}))?$/;
51
+ const PERSONAL_CONNECTOR = /^[a-z0-9][a-z0-9_]{0,63}(?::(?:\*|[A-Za-z0-9_]{1,120}))?$/;
52
52
  // PyYAML's YAML 1.1 plain-scalar resolver: a bare (unquoted) scalar matching one
53
53
  // of these resolves to a non-string (bool/null/int/float/timestamp), and the
54
54
  // server rejects a non-string wherever the manifest wants a name. The quirks are
@@ -653,7 +653,7 @@ function validateDoc(doc, lines) {
653
653
  doc.agents = [...new Set(doc.agents)].sort();
654
654
  for (const entry of doc.personal_connectors) {
655
655
  if (!PERSONAL_CONNECTOR.test(entry)) {
656
- throw new ManifestParseError(`"${entry}" is not a valid personal_connectors entry — use "gmail", "gmail:*", or "gmail:GMAIL_SEND_EMAIL"`, lineOf(entry));
656
+ throw new ManifestParseError(`"${entry}" is not a valid personal_connectors entry — use "gmail", "gmail:*", or "gmail:send_email"`, lineOf(entry));
657
657
  }
658
658
  }
659
659
  doc.personal_connectors = [...new Set(doc.personal_connectors)].sort();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "railcode",
3
- "version": "0.1.26",
3
+ "version": "0.1.27",
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",
@@ -27,5 +27,8 @@
27
27
  "devDependencies": {
28
28
  "@types/node": "^24.0.0",
29
29
  "typescript": "^5.9.0"
30
+ },
31
+ "dependencies": {
32
+ "yaml": "^2.9.0"
30
33
  }
31
34
  }