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 +44 -7
- package/dist/app-storage.js +154 -0
- package/dist/index.js +866 -67
- package/dist/manifest.js +2 -2
- package/dist/orgadmin.js +20 -8
- package/dist/update.js +112 -0
- package/package.json +4 -1
- package/static/sdk.js +752 -51
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
|
|
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.
|
|
91
|
-
--file agent.
|
|
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.
|
|
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
|
|
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
|
+
}
|