railcode 0.1.25 → 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 +28 -7
- package/dist/index.js +201 -75
- package/dist/manifest.js +2 -2
- package/package.json +4 -1
- package/static/sdk.js +755 -54
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
|
|
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,23 +86,38 @@ 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.
|
|
91
|
-
--file agent.
|
|
92
|
-
archives it.
|
|
93
|
-
|
|
96
|
+
- `railcode agent create --file agent.yaml` creates an agent; `update <agent>
|
|
97
|
+
--file agent.yaml` replaces an existing agent manifest; `delete <agent> --yes`
|
|
98
|
+
archives it. `--visibility <org|personal>` on `create`/`update`/`test` sets or
|
|
99
|
+
changes who the agent belongs to (default `org` on create; omitted on `update`
|
|
100
|
+
leaves it alone). A `personal` agent is invokable and manageable by its
|
|
101
|
+
creator alone — no grant makes it shared — and `personal -> org` is blocked
|
|
102
|
+
once created. Requires `agent:create` (personal) or `agent:create_org` (org).
|
|
103
|
+
`agent show`/`list` print `visibility`, and `show` prints `Owner:` for a
|
|
104
|
+
personal agent.
|
|
105
|
+
- `railcode agent test --file agent.yaml --input '{"k":"v"}'` runs a draft
|
|
94
106
|
manifest without saving; `railcode agent run <agent> --input '{"k":"v"}'`
|
|
95
107
|
invokes a saved agent and prints the result. Use `--trace` for the step trace
|
|
96
108
|
and `--json` for the raw run detail.
|
|
97
109
|
- `railcode agent schedule set <agent> --cron "0 9 * * *" --timezone UTC`
|
|
98
110
|
creates or updates the agent's one schedule. `schedule show`, `pause`,
|
|
99
111
|
`resume`, `delete --yes`, and `run-now` operate on that single schedule.
|
|
112
|
+
- **`personal-connectors`** (aliases `personal-connector`, `pc`) manages **your
|
|
113
|
+
own** connected third-party accounts (Gmail, Slack, …), brokered by railcode (in-house) —
|
|
114
|
+
distinct from org-admin **service connectors** and from an agent's
|
|
115
|
+
`tools.personal_connectors`. `list` shows toolkits this deployment brokers and
|
|
116
|
+
your connection status; `tools <toolkit>` prints that toolkit's callable tools
|
|
117
|
+
and input schemas; `connect <toolkit>` prints an OAuth URL to open in a
|
|
118
|
+
browser; `call <toolkit> <tool> [--args '<json>']` runs one tool on your own
|
|
119
|
+
connected account. Hits the non-org-scoped `/api/personal-connections/*` plane
|
|
120
|
+
— a personal connection belongs to the human, not the org.
|
|
100
121
|
|
|
101
122
|
## Configuration
|
|
102
123
|
|
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 =
|
|
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
|
-
|
|
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
|
|
312
|
+
console.log("Opening your browser to authorize Railcode.");
|
|
301
313
|
}
|
|
302
314
|
else {
|
|
303
|
-
console.log("
|
|
315
|
+
console.log("Open this URL in your browser to authorize Railcode:");
|
|
316
|
+
console.log(` ${authorizeUrl}`);
|
|
304
317
|
}
|
|
305
|
-
console.log("
|
|
306
|
-
|
|
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('"', """)
|
|
444
514
|
.replaceAll("'", "'");
|
|
445
515
|
}
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
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
|
-
|
|
707
|
-
|
|
708
|
-
|
|
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
|
-
|
|
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,12 +3881,18 @@ 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
|
|
3810
3888
|
--trace Include the run step trace in human output
|
|
3811
3889
|
--yes Confirm delete without a prompt
|
|
3890
|
+
--visibility <org|personal> create/update/test only (default: org). A personal
|
|
3891
|
+
agent is owned/invoked by its creator alone — no
|
|
3892
|
+
grant makes it shared, and "personal -> org" is
|
|
3893
|
+
blocked once created. Requires the matching
|
|
3894
|
+
capability: agent:create for personal,
|
|
3895
|
+
agent:create_org for org.
|
|
3812
3896
|
|
|
3813
3897
|
Schedule options:
|
|
3814
3898
|
--name <name> Schedule name (default: default on create)
|
|
@@ -3817,7 +3901,7 @@ Schedule options:
|
|
|
3817
3901
|
--timezone <iana> IANA timezone (default: UTC on create)
|
|
3818
3902
|
--enabled / --disabled Initial or updated enabled state
|
|
3819
3903
|
|
|
3820
|
-
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.
|
|
3821
3905
|
Schedules are currently one per agent, so "set" creates the schedule if missing
|
|
3822
3906
|
or updates the existing one.
|
|
3823
3907
|
`;
|
|
@@ -3881,9 +3965,10 @@ async function commandAgentList(args) {
|
|
|
3881
3965
|
console.log("No managed agents in this organization.");
|
|
3882
3966
|
return;
|
|
3883
3967
|
}
|
|
3884
|
-
console.log(formatTable(["name", "model", "uuid", "updated"], agents.map((agent) => [
|
|
3968
|
+
console.log(formatTable(["name", "model", "visibility", "uuid", "updated"], agents.map((agent) => [
|
|
3885
3969
|
agent.name,
|
|
3886
3970
|
typeof agent.manifest.model === "string" ? agent.manifest.model : "-",
|
|
3971
|
+
agent.visibility,
|
|
3887
3972
|
agent.uuid,
|
|
3888
3973
|
shortDate(agent.updated_at),
|
|
3889
3974
|
])));
|
|
@@ -3918,13 +4003,14 @@ async function commandAgentPull(args) {
|
|
|
3918
4003
|
process.stdout.write(manifest);
|
|
3919
4004
|
}
|
|
3920
4005
|
async function commandAgentCreate(args) {
|
|
3921
|
-
assertKnownAgentOptions(args, ["file", "json", "apiUrl"]);
|
|
4006
|
+
assertKnownAgentOptions(args, ["file", "json", "visibility", "apiUrl"]);
|
|
3922
4007
|
const manifest = readAgentManifest(args);
|
|
4008
|
+
const visibility = agentVisibilityOption(args);
|
|
3923
4009
|
const config = requireLogin(args);
|
|
3924
4010
|
const resp = await authedFetch(config, `/api/organizations/${config.orgUuid}/agents`, {
|
|
3925
4011
|
method: "POST",
|
|
3926
4012
|
headers: { "Content-Type": "application/json" },
|
|
3927
|
-
body: JSON.stringify({ manifest }),
|
|
4013
|
+
body: JSON.stringify({ manifest, ...(visibility !== undefined ? { visibility } : {}) }),
|
|
3928
4014
|
});
|
|
3929
4015
|
if (resp.status === 401)
|
|
3930
4016
|
await reLoginOrThrow();
|
|
@@ -3932,14 +4018,15 @@ async function commandAgentCreate(args) {
|
|
|
3932
4018
|
printAgentMutation(result, args.options.json === true, "created");
|
|
3933
4019
|
}
|
|
3934
4020
|
async function commandAgentUpdate(args) {
|
|
3935
|
-
assertKnownAgentOptions(args, ["file", "json", "apiUrl"]);
|
|
4021
|
+
assertKnownAgentOptions(args, ["file", "json", "visibility", "apiUrl"]);
|
|
3936
4022
|
const ref = agentArg(args, "update");
|
|
3937
4023
|
const manifest = readAgentManifest(args);
|
|
4024
|
+
const visibility = agentVisibilityOption(args);
|
|
3938
4025
|
const config = requireLogin(args);
|
|
3939
4026
|
const resp = await authedFetch(config, `/api/organizations/${config.orgUuid}/agents/${encodeURIComponent(ref)}`, {
|
|
3940
4027
|
method: "PUT",
|
|
3941
4028
|
headers: { "Content-Type": "application/json" },
|
|
3942
|
-
body: JSON.stringify({ manifest }),
|
|
4029
|
+
body: JSON.stringify({ manifest, ...(visibility !== undefined ? { visibility } : {}) }),
|
|
3943
4030
|
});
|
|
3944
4031
|
if (resp.status === 401)
|
|
3945
4032
|
await reLoginOrThrow();
|
|
@@ -3968,14 +4055,15 @@ async function commandAgentDelete(args) {
|
|
|
3968
4055
|
console.log(`Agent "${ref}" deleted.`);
|
|
3969
4056
|
}
|
|
3970
4057
|
async function commandAgentTest(args) {
|
|
3971
|
-
assertKnownAgentOptions(args, ["file", "input", "inputFile", "json", "trace", "apiUrl"]);
|
|
4058
|
+
assertKnownAgentOptions(args, ["file", "input", "inputFile", "json", "trace", "visibility", "apiUrl"]);
|
|
3972
4059
|
const manifest = readAgentManifest(args);
|
|
3973
4060
|
const input = readJsonInput(args);
|
|
4061
|
+
const visibility = agentVisibilityOption(args);
|
|
3974
4062
|
const config = requireLogin(args);
|
|
3975
4063
|
const resp = await authedFetch(config, `/api/organizations/${config.orgUuid}/agents/test`, {
|
|
3976
4064
|
method: "POST",
|
|
3977
4065
|
headers: { "Content-Type": "application/json" },
|
|
3978
|
-
body: JSON.stringify({ manifest, input }),
|
|
4066
|
+
body: JSON.stringify({ manifest, input, ...(visibility !== undefined ? { visibility } : {}) }),
|
|
3979
4067
|
});
|
|
3980
4068
|
if (resp.status === 401)
|
|
3981
4069
|
await reLoginOrThrow();
|
|
@@ -4175,22 +4263,38 @@ async function requireSingleSchedule(config, ref) {
|
|
|
4175
4263
|
function readAgentManifest(args) {
|
|
4176
4264
|
const path = optionString(args, "file");
|
|
4177
4265
|
if (!path)
|
|
4178
|
-
throw new CliError("Pass --file <agent-manifest.json>.", 2);
|
|
4179
|
-
|
|
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);
|
|
4269
|
+
}
|
|
4270
|
+
// Omitted means "use the server default" on create/test (org) and "leave alone"
|
|
4271
|
+
// on update — never send the key unless the caller passed it explicitly.
|
|
4272
|
+
function agentVisibilityOption(args) {
|
|
4273
|
+
const value = optionString(args, "visibility");
|
|
4274
|
+
if (value === undefined)
|
|
4275
|
+
return undefined;
|
|
4276
|
+
if (value !== "org" && value !== "personal") {
|
|
4277
|
+
throw new CliError(`Invalid --visibility: ${value} (expected "org" or "personal").`, 2);
|
|
4278
|
+
}
|
|
4279
|
+
return value;
|
|
4180
4280
|
}
|
|
4181
4281
|
function readJsonObjectFile(path, label) {
|
|
4282
|
+
return readObjectFile(path, label, "JSON");
|
|
4283
|
+
}
|
|
4284
|
+
function readObjectFile(path, label, format) {
|
|
4182
4285
|
const resolved = resolve(process.cwd(), path);
|
|
4183
4286
|
if (!existsSync(resolved))
|
|
4184
4287
|
throw new CliError(`${label} file not found: ${path}`, 2);
|
|
4185
4288
|
let parsed;
|
|
4186
4289
|
try {
|
|
4187
|
-
|
|
4290
|
+
const raw = readFileSync(resolved, "utf8");
|
|
4291
|
+
parsed = format === "YAML" ? parseYaml(raw) : JSON.parse(raw);
|
|
4188
4292
|
}
|
|
4189
4293
|
catch (error) {
|
|
4190
|
-
throw new CliError(`${label} file is not valid
|
|
4294
|
+
throw new CliError(`${label} file is not valid ${format}: ${error instanceof Error ? error.message : String(error)}`, 2);
|
|
4191
4295
|
}
|
|
4192
4296
|
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
4193
|
-
throw new CliError(`${label} file must contain a
|
|
4297
|
+
throw new CliError(`${label} file must contain a ${format} object.`, 2);
|
|
4194
4298
|
}
|
|
4195
4299
|
return parsed;
|
|
4196
4300
|
}
|
|
@@ -4264,6 +4368,7 @@ function printAgentMutation(result, json, verb) {
|
|
|
4264
4368
|
}
|
|
4265
4369
|
console.log(`Agent "${result.agent.name}" ${verb}.`);
|
|
4266
4370
|
console.log(`UUID: ${result.agent.uuid}`);
|
|
4371
|
+
console.log(`Visibility: ${result.agent.visibility}`);
|
|
4267
4372
|
if (result.warnings.length > 0) {
|
|
4268
4373
|
console.log("");
|
|
4269
4374
|
console.log("Warnings:");
|
|
@@ -4276,6 +4381,9 @@ function printAgent(agent) {
|
|
|
4276
4381
|
console.log(`UUID: ${agent.uuid}`);
|
|
4277
4382
|
console.log(`Description: ${agent.description || "-"}`);
|
|
4278
4383
|
console.log(`Model: ${typeof agent.manifest.model === "string" ? agent.manifest.model : "-"}`);
|
|
4384
|
+
console.log(`Visibility: ${agent.visibility}`);
|
|
4385
|
+
if (agent.visibility === "personal")
|
|
4386
|
+
console.log(`Owner: ${agent.owner_uuid ?? "-"}`);
|
|
4279
4387
|
console.log(`Updated: ${agent.updated_at}`);
|
|
4280
4388
|
console.log(`Content hash: ${agent.content_hash}`);
|
|
4281
4389
|
}
|
|
@@ -5123,17 +5231,20 @@ const PC_HELP = `railcode personal-connectors — your own connected accounts (G
|
|
|
5123
5231
|
Usage:
|
|
5124
5232
|
railcode personal-connectors list Toolkits this deployment brokers + your status
|
|
5125
5233
|
railcode personal-connectors tools <toolkit> The callable tools of a toolkit (and their inputs)
|
|
5126
|
-
railcode personal-connectors connect <toolkit>
|
|
5127
|
-
|
|
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)
|
|
5237
|
+
railcode personal-connectors call <toolkit> <tool> [--args '<json>'] Run one tool on your own account
|
|
5128
5238
|
|
|
5129
5239
|
Aliases: personal-connector, pc
|
|
5130
5240
|
|
|
5131
5241
|
Discovery is what you build against: \`tools <toolkit> --json\` shows each tool's
|
|
5132
5242
|
input schema, so you know what to put in an app's \`personal_connectors:\` and what
|
|
5133
|
-
\`personalConnections.call('<tool>', {...})\` expects.
|
|
5243
|
+
\`personalConnections.call('<toolkit>', '<tool>', {...})\` expects.
|
|
5134
5244
|
|
|
5135
5245
|
Options:
|
|
5136
5246
|
--json Print raw JSON instead of a table
|
|
5247
|
+
--token '<value>' Token/key for \`connect\` on a token-auth connector
|
|
5137
5248
|
--args '<json>' Arguments object for \`call\` (default {})
|
|
5138
5249
|
`;
|
|
5139
5250
|
async function commandPersonalConnectors(args) {
|
|
@@ -5212,13 +5323,31 @@ async function commandPcTools(args) {
|
|
|
5212
5323
|
console.log(`\n${tools.length} tool(s). Add \`--json\` to see each tool's input schema.`);
|
|
5213
5324
|
}
|
|
5214
5325
|
async function commandPcConnect(args) {
|
|
5215
|
-
rejectUnknownOptions(args, ["json", "apiUrl"], "personal-connectors connect", PC_HELP);
|
|
5326
|
+
rejectUnknownOptions(args, ["json", "token", "apiUrl"], "personal-connectors connect", PC_HELP);
|
|
5216
5327
|
const toolkit = args.rest[1];
|
|
5217
5328
|
if (!toolkit)
|
|
5218
5329
|
throw new CliError("Usage: railcode personal-connectors connect <toolkit>", 2);
|
|
5219
5330
|
const config = requireLogin(args);
|
|
5220
|
-
//
|
|
5221
|
-
//
|
|
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.
|
|
5222
5351
|
const resp = await authedFetch(config, `/api/personal-connections/${encodeURIComponent(toolkit)}/connect`, { method: "POST" });
|
|
5223
5352
|
if (resp.status === 401)
|
|
5224
5353
|
await reLoginOrThrow();
|
|
@@ -5231,13 +5360,21 @@ async function commandPcConnect(args) {
|
|
|
5231
5360
|
console.log(JSON.stringify(body, null, 2));
|
|
5232
5361
|
return;
|
|
5233
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
|
+
}
|
|
5234
5368
|
console.log(`Open this URL to connect ${toolkit}, then re-run your command:\n\n ${body.redirect_url}\n`);
|
|
5235
5369
|
}
|
|
5236
5370
|
async function commandPcCall(args) {
|
|
5237
5371
|
rejectUnknownOptions(args, ["json", "args", "apiUrl"], "personal-connectors call", PC_HELP);
|
|
5238
|
-
|
|
5239
|
-
|
|
5240
|
-
|
|
5372
|
+
// Scoped by connector: name the toolkit AND the tool.
|
|
5373
|
+
const toolkit = args.rest[1];
|
|
5374
|
+
const tool = args.rest[2];
|
|
5375
|
+
if (!toolkit || !tool) {
|
|
5376
|
+
throw new CliError("Usage: railcode personal-connectors call <toolkit> <tool> [--args '<json>']", 2);
|
|
5377
|
+
}
|
|
5241
5378
|
let toolArgs = {};
|
|
5242
5379
|
if (typeof args.options.args === "string") {
|
|
5243
5380
|
try {
|
|
@@ -5251,7 +5388,7 @@ async function commandPcCall(args) {
|
|
|
5251
5388
|
const resp = await authedFetch(config, `/api/personal-connections/call`, {
|
|
5252
5389
|
method: "POST",
|
|
5253
5390
|
headers: { "Content-Type": "application/json" },
|
|
5254
|
-
body: JSON.stringify({ tool, arguments: toolArgs }),
|
|
5391
|
+
body: JSON.stringify({ toolkit, tool, arguments: toolArgs }),
|
|
5255
5392
|
});
|
|
5256
5393
|
if (resp.status === 401)
|
|
5257
5394
|
await reLoginOrThrow();
|
|
@@ -5264,7 +5401,7 @@ async function commandPcCall(args) {
|
|
|
5264
5401
|
if (resp.status === 409) {
|
|
5265
5402
|
throw new CliError(await errorDetail(resp), 1);
|
|
5266
5403
|
}
|
|
5267
|
-
const body = await readJsonOrThrow(resp, `Call ${tool}`);
|
|
5404
|
+
const body = await readJsonOrThrow(resp, `Call ${toolkit}/${tool}`);
|
|
5268
5405
|
console.log(JSON.stringify(body, null, 2));
|
|
5269
5406
|
}
|
|
5270
5407
|
// ---------------------------------------------------------------------------
|
|
@@ -7286,17 +7423,6 @@ async function pcForward(ctx, method, apiPath, body) {
|
|
|
7286
7423
|
}
|
|
7287
7424
|
return { status: upstream.status, body: parsed };
|
|
7288
7425
|
}
|
|
7289
|
-
// Which declared toolkit owns `tool` — asked of the user-plane catalog, scanning
|
|
7290
|
-
// only the app's declared toolkits (as the server's app-plane resolver does).
|
|
7291
|
-
async function pcResolveToolkit(ctx, entries, tool) {
|
|
7292
|
-
for (const toolkit of pcDeclaredToolkits(entries)) {
|
|
7293
|
-
const { status, body } = await pcForward(ctx, "GET", `/personal-connections/toolkits/${encodeURIComponent(toolkit)}/tools`);
|
|
7294
|
-
if (status === 200 && Array.isArray(body) && body.some((t) => t.slug === tool)) {
|
|
7295
|
-
return toolkit;
|
|
7296
|
-
}
|
|
7297
|
-
}
|
|
7298
|
-
return null;
|
|
7299
|
-
}
|
|
7300
7426
|
const PC_NOT_IN_MANIFEST = (label) => ({
|
|
7301
7427
|
detail: `personal connector "${label}" was not in the manifest`,
|
|
7302
7428
|
});
|
|
@@ -7336,10 +7462,11 @@ async function handlePersonalConnections(ctx, req, res, path, url) {
|
|
|
7336
7462
|
const { status, body } = await pcForward(ctx, "POST", `/personal-connections/${encodeURIComponent(toolkit)}/connect`);
|
|
7337
7463
|
return sendJson(res, status, body);
|
|
7338
7464
|
}
|
|
7339
|
-
// call(tool, args) — the load-bearing gate
|
|
7340
|
-
//
|
|
7341
|
-
//
|
|
7342
|
-
// prod chokepoint's "no manifest =
|
|
7465
|
+
// call(toolkit, tool, args) — the load-bearing gate. Scoped by connector, so
|
|
7466
|
+
// the bound is a direct match against the local manifest: the toolkit must be
|
|
7467
|
+
// declared AND the specific tool permitted, else 403 (never forwarded). No
|
|
7468
|
+
// declaration at all ⇒ refused, matching the prod chokepoint's "no manifest =
|
|
7469
|
+
// refuse". The user plane then verifies the (toolkit, tool) pair is real.
|
|
7343
7470
|
if (rest === "/call" && req.method === "POST") {
|
|
7344
7471
|
const raw = await readBody(req);
|
|
7345
7472
|
let payload;
|
|
@@ -7349,16 +7476,15 @@ async function handlePersonalConnections(ctx, req, res, path, url) {
|
|
|
7349
7476
|
catch {
|
|
7350
7477
|
return sendJson(res, 400, { detail: "body must be JSON" });
|
|
7351
7478
|
}
|
|
7352
|
-
const tool = payload
|
|
7353
|
-
if (!tool)
|
|
7354
|
-
return sendJson(res, 400, { detail: '"tool"
|
|
7355
|
-
|
|
7356
|
-
|
|
7357
|
-
|
|
7358
|
-
if (!toolkit || !pcAllows(entries, toolkit, tool)) {
|
|
7359
|
-
return sendJson(res, 403, PC_NOT_IN_MANIFEST(tool));
|
|
7479
|
+
const { toolkit, tool } = payload;
|
|
7480
|
+
if (!toolkit || !tool) {
|
|
7481
|
+
return sendJson(res, 400, { detail: '"toolkit" and "tool" are required' });
|
|
7482
|
+
}
|
|
7483
|
+
if (!pcAllows(entries, toolkit, tool)) {
|
|
7484
|
+
return sendJson(res, 403, PC_NOT_IN_MANIFEST(`${toolkit}:${tool}`));
|
|
7360
7485
|
}
|
|
7361
7486
|
const { status, body } = await pcForward(ctx, "POST", "/personal-connections/call", {
|
|
7487
|
+
toolkit,
|
|
7362
7488
|
tool,
|
|
7363
7489
|
arguments: payload.arguments ?? {},
|
|
7364
7490
|
});
|
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-
|
|
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:
|
|
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.
|
|
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
|
}
|