railcode 0.1.26 → 0.1.28

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)
@@ -32,6 +34,10 @@ railcode roles <list|create|update|delete|add-member|remove-member|grants|grant|
32
34
  Org roles + the granular grants table (admin)
33
35
  railcode apps <list|show|access|set-access|transfer|delete> ...
34
36
  Apps + access policy (owner/admin)
37
+ railcode app kv <collections|list|get|set|delete|drop> ...
38
+ Manage an app's KV store (owner)
39
+ railcode app files <list|download|upload|delete> ...
40
+ Manage an app's files (owner)
35
41
  railcode analytics <app> [--range 1d|7d|30d|90d] Per-app pageview analytics
36
42
  railcode logs <connector|service-connector|llm|email|agent> [filters]
37
43
  Org observability logs (admin)
@@ -54,7 +60,11 @@ your saved org and are gated server-side by the same capabilities as the console
54
60
  a personal API token: the CLI exchanges it once for a personal API token and
55
61
  writes the same config as a browser login. If it's expired or already used,
56
62
  generate a fresh prompt from the dashboard.
57
- - **`init <app>`** scaffolds a directory. By default it creates a React + Vite +
63
+ - **`init <app> [dir]`** scaffolds a directory. `dir` is optional and defaults to
64
+ `./<app>`; pass `.` to scaffold into the current directory, or any path to
65
+ scaffold into it. The target need not be empty (same-named files are
66
+ overwritten), but an existing `railcode.json` is preserved unless you pass
67
+ `--force`. By default it creates a React + Vite +
58
68
  Zustand starter — a personalized "Welcome to Railcode, `<name>`" home wired to
59
69
  every SDK capability: identity/team, a live to-dos list and file uploads (work
60
70
  with zero setup), and SQL, saved queries, service connectors, and LLM cards that
@@ -80,15 +90,15 @@ your saved org and are gated server-side by the same capabilities as the console
80
90
  (SQL is never interpolated), `--file <path>` reads SQL from a file, and
81
91
  `--json` prints the raw `{ columns, rows, rowcount, truncated }` envelope.
82
92
  - **`agent`** manages org-scoped managed agents when your permissions allow it.
83
- Manifests are JSON files in the same shape the API stores under
93
+ Manifests are JSON or YAML files in the same shape the API stores under
84
94
  `agent.manifest`.
85
95
  - `railcode agent list` lists agents; `--json` prints raw `AgentOut[]`.
86
96
  - `railcode agent show <name|uuid>` prints one agent; `--manifest` prints only
87
97
  its manifest JSON.
88
98
  - `railcode agent pull <name|uuid> --output agent.json` writes the existing
89
99
  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`
100
+ - `railcode agent create --file agent.yaml` creates an agent; `update <agent>
101
+ --file agent.yaml` replaces an existing agent manifest; `delete <agent> --yes`
92
102
  archives it. `--visibility <org|personal>` on `create`/`update`/`test` sets or
93
103
  changes who the agent belongs to (default `org` on create; omitted on `update`
94
104
  leaves it alone). A `personal` agent is invokable and manageable by its
@@ -96,7 +106,7 @@ your saved org and are gated server-side by the same capabilities as the console
96
106
  once created. Requires `agent:create` (personal) or `agent:create_org` (org).
97
107
  `agent show`/`list` print `visibility`, and `show` prints `Owner:` for a
98
108
  personal agent.
99
- - `railcode agent test --file agent.json --input '{"k":"v"}'` runs a draft
109
+ - `railcode agent test --file agent.yaml --input '{"k":"v"}'` runs a draft
100
110
  manifest without saving; `railcode agent run <agent> --input '{"k":"v"}'`
101
111
  invokes a saved agent and prints the result. Use `--trace` for the step trace
102
112
  and `--json` for the raw run detail.
@@ -104,7 +114,7 @@ your saved org and are gated server-side by the same capabilities as the console
104
114
  creates or updates the agent's one schedule. `schedule show`, `pause`,
105
115
  `resume`, `delete --yes`, and `run-now` operate on that single schedule.
106
116
  - **`personal-connectors`** (aliases `personal-connector`, `pc`) manages **your
107
- own** connected third-party accounts (Gmail, Slack, …), brokered by Composio
117
+ own** connected third-party accounts (Gmail, Slack, …), brokered by railcode (in-house)
108
118
  distinct from org-admin **service connectors** and from an agent's
109
119
  `tools.personal_connectors`. `list` shows toolkits this deployment brokers and
110
120
  your connection status; `tools <toolkit>` prints that toolkit's callable tools
@@ -125,6 +135,33 @@ API URL resolution: `--api-url` > `RAILCODE_API_URL` > saved config > prompt.
125
135
  Set `RAILCODE_API_TOKEN` to override the saved token in CI. On a `401`, the saved
126
136
  token is cleared and you're prompted to `railcode login` again.
127
137
 
138
+ ## Staying up to date
139
+
140
+ The CLI keeps itself current. When you run a command in an interactive terminal it
141
+ checks the npm registry — at most once every 6 hours — for a newer `railcode`
142
+ release **within your current major** and, if there is one, installs it globally
143
+ (with whichever package manager it detects: npm/pnpm/yarn/bun) and tells you:
144
+
145
+ ```
146
+ railcode: a new version is available — 0.1.26 → 0.1.27. Updating…
147
+ railcode: updated to 0.1.27. It takes effect on your next command.
148
+ ```
149
+
150
+ It never jumps across a major version (a breaking `2.0.0` is left for you to
151
+ install deliberately), the check is best-effort (a registry/network hiccup is
152
+ silently ignored — it never blocks your command), and it's skipped in
153
+ non-interactive/CI runs so automation never self-mutates a global install. The
154
+ `last_checked_for_updates` timestamp lives in `~/.railcode/update-check.json`, and
155
+ all of the above is printed to **stderr** so `--json` output stays clean.
156
+
157
+ Environment overrides:
158
+
159
+ - `RAILCODE_NO_UPDATE=1` — turn auto-update off entirely.
160
+ - `RAILCODE_UPDATE_DRY_RUN=1` — check and print the command it *would* run, without
161
+ installing (also forces the check even off-TTY; handy for scripts and testing).
162
+ - `RAILCODE_REGISTRY_URL` (falls back to `npm_config_registry`) — point the check
163
+ at a different npm-compatible registry.
164
+
128
165
  ## `railcode.json`
129
166
 
130
167
  ```json
@@ -0,0 +1,154 @@
1
+ // Pure helpers behind `railcode app kv ...` / `railcode app files ...`. Kept free
2
+ // of I/O and process state so they unit-test directly (see test/app-storage.test.mjs);
3
+ // the command layer in index.ts does the HTTP, file reads, and printing.
4
+ export class StorageError extends Error {
5
+ }
6
+ const UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
7
+ // Parse + validate the shared scope flags. `allowAll` gates the cross-scope `all`
8
+ // selector to the listing commands (a record/file mutation must name one namespace).
9
+ export function parseScopeSelector(scope, user, role, opts) {
10
+ const kind = (scope ?? "shared").toLowerCase();
11
+ const allowed = opts.allowAll ? "shared, user, role, all" : "shared, user, role";
12
+ if (!["shared", "user", "role", "all"].includes(kind)) {
13
+ throw new StorageError(`Unknown --scope "${scope}". Use one of: ${allowed}.`);
14
+ }
15
+ if (kind === "all") {
16
+ if (!opts.allowAll) {
17
+ throw new StorageError("--scope all only lists (kv collections, kv list, files list); a record/file " +
18
+ "operation must target one scope.");
19
+ }
20
+ if (user || role)
21
+ throw new StorageError("--scope all takes no --user or --role.");
22
+ return { scope: "all" };
23
+ }
24
+ if (kind === "shared") {
25
+ if (user || role)
26
+ throw new StorageError("--scope shared takes no --user or --role.");
27
+ return { scope: "shared" };
28
+ }
29
+ if (kind === "user") {
30
+ if (role)
31
+ throw new StorageError("--scope user takes no --role.");
32
+ if (!user)
33
+ throw new StorageError("--scope user requires --user <member-uuid>.");
34
+ if (!UUID_RE.test(user))
35
+ throw new StorageError("--user must be a member UUID.");
36
+ return { scope: "user", user };
37
+ }
38
+ // role
39
+ if (user)
40
+ throw new StorageError("--scope role takes no --user.");
41
+ if (!role)
42
+ throw new StorageError("--scope role requires --role <role-uuid>.");
43
+ if (!UUID_RE.test(role))
44
+ throw new StorageError("--role must be a role UUID.");
45
+ return { scope: "role", role };
46
+ }
47
+ // The scope selectors as a query string, echoed onto every storage request.
48
+ export function scopeQuery(sel) {
49
+ const params = new URLSearchParams();
50
+ params.set("scope", sel.scope);
51
+ if (sel.user)
52
+ params.set("user", sel.user);
53
+ if (sel.role)
54
+ params.set("role", sel.role);
55
+ return params.toString();
56
+ }
57
+ // The value for `kv set` — inline JSON or from --file, never both, never neither.
58
+ // A KV value is JSON of any shape: an object, array, string, number, or bool.
59
+ export function parseKvValue(positional, fileText) {
60
+ const hasPos = typeof positional === "string" && positional.length > 0;
61
+ const hasFile = typeof fileText === "string";
62
+ if (hasPos && hasFile) {
63
+ throw new StorageError("Pass the value inline OR with --file, not both.");
64
+ }
65
+ if (!hasPos && !hasFile) {
66
+ throw new StorageError('Provide a JSON value: railcode app kv set <collection> <key> \'{"n":1}\' (or --file v.json).');
67
+ }
68
+ const raw = hasFile ? fileText : positional;
69
+ try {
70
+ return JSON.parse(raw);
71
+ }
72
+ catch {
73
+ throw new StorageError("Value must be valid JSON — an object, array, string, number, or bool " +
74
+ "(e.g. '{\"n\":1}', '\"hello\"', 42, true).");
75
+ }
76
+ }
77
+ // --query is a KV `where` clause: a JSON array of [field, op, value] triples, passed
78
+ // through to the same query engine the data plane exposes. Validated here for a clean
79
+ // local error; the server re-validates authoritatively.
80
+ export function parseWhere(raw) {
81
+ if (raw === undefined)
82
+ return undefined;
83
+ let parsed;
84
+ try {
85
+ parsed = JSON.parse(raw);
86
+ }
87
+ catch {
88
+ throw new StorageError('--query must be JSON, e.g. --query \'[["n","gt",1]]\'.');
89
+ }
90
+ if (!Array.isArray(parsed)) {
91
+ throw new StorageError('--query must be a JSON array of [field, op, value] clauses.');
92
+ }
93
+ return raw;
94
+ }
95
+ // --limit/--offset → the API's page/size. offset must be a whole number of pages so
96
+ // the translation is exact (the API pages, it doesn't offset).
97
+ export function pageParams(limit, offset) {
98
+ const out = {};
99
+ if (limit !== undefined) {
100
+ if (!Number.isInteger(limit) || limit <= 0) {
101
+ throw new StorageError("--limit must be a positive integer.");
102
+ }
103
+ out.size = limit;
104
+ }
105
+ if (offset !== undefined) {
106
+ if (!Number.isInteger(offset) || offset < 0) {
107
+ throw new StorageError("--offset must be a non-negative integer.");
108
+ }
109
+ if (limit === undefined)
110
+ throw new StorageError("--offset requires --limit.");
111
+ if (offset % limit !== 0) {
112
+ throw new StorageError("--offset must be a whole multiple of --limit.");
113
+ }
114
+ out.page = Math.floor(offset / limit) + 1;
115
+ }
116
+ return out;
117
+ }
118
+ // A best-effort content type from a filename extension (upload default:
119
+ // application/octet-stream). The backend forces download on serve regardless.
120
+ export function guessContentType(name) {
121
+ const ext = name.toLowerCase().split(".").pop() ?? "";
122
+ const map = {
123
+ txt: "text/plain",
124
+ md: "text/markdown",
125
+ csv: "text/csv",
126
+ json: "application/json",
127
+ html: "text/html",
128
+ css: "text/css",
129
+ js: "text/javascript",
130
+ svg: "image/svg+xml",
131
+ png: "image/png",
132
+ jpg: "image/jpeg",
133
+ jpeg: "image/jpeg",
134
+ gif: "image/gif",
135
+ webp: "image/webp",
136
+ pdf: "application/pdf",
137
+ zip: "application/zip",
138
+ };
139
+ return map[ext] ?? "application/octet-stream";
140
+ }
141
+ // Percent-encode each segment of a file name while keeping the `/` separators the
142
+ // scope-aware `{name:path}` route expects.
143
+ export function encodeFilePath(name) {
144
+ return name
145
+ .split("/")
146
+ .map((segment) => encodeURIComponent(segment))
147
+ .join("/");
148
+ }
149
+ // The owner label (member email / role name) for a listing row, or a dash.
150
+ export function ownerCell(scope, ownerLabel) {
151
+ if (scope === "shared")
152
+ return "-";
153
+ return ownerLabel ?? "-";
154
+ }