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 +13 -7
- package/dist/index.js +147 -38
- package/dist/manifest.js +2 -2
- 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)
|
|
@@ -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,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.
|
|
91
|
-
--file agent.
|
|
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.
|
|
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
|
|
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 =
|
|
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,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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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>
|
|
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
|
-
//
|
|
5246
|
-
//
|
|
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-
|
|
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
|
}
|