@rankigi/cli 1.0.0

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.
Files changed (2) hide show
  1. package/dist/index.js +275 -0
  2. package/package.json +26 -0
package/dist/index.js ADDED
@@ -0,0 +1,275 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * @rankigi/cli
4
+ *
5
+ * One command: `npx @rankigi/cli init`
6
+ *
7
+ * Prompts for an API key and an agent name, calls /api/agents/birth,
8
+ * and writes RANKIGI_* credentials to .env in the current directory.
9
+ *
10
+ * The signing private key is returned exactly once by the birth endpoint.
11
+ * The CLI writes it to .env on the developer's machine. RANKIGI never
12
+ * stores it server side.
13
+ */
14
+ import { createInterface } from "node:readline/promises";
15
+ import { stdin, stdout } from "node:process";
16
+ import { readFileSync, writeFileSync, existsSync } from "node:fs";
17
+ import { dirname, join, resolve } from "node:path";
18
+ import { spawn } from "node:child_process";
19
+ import { fileURLToPath } from "node:url";
20
+ const DEFAULT_BASE_URL = process.env.RANKIGI_BASE_URL || "https://rankigi.com";
21
+ async function resolveCanonByFlag(baseUrl, apiKey, slugOrName) {
22
+ try {
23
+ const res = await fetch(baseUrl + "/api/v1/canons?status=active", {
24
+ headers: { Authorization: "Bearer " + apiKey },
25
+ });
26
+ if (!res.ok)
27
+ return null;
28
+ const body = (await res.json());
29
+ if (!body.ok || !Array.isArray(body.canons))
30
+ return null;
31
+ const needle = slugOrName.toLowerCase();
32
+ return (body.canons.find((c) => c.name.toLowerCase() === needle) ??
33
+ body.canons.find((c) => c.name.toLowerCase().includes(needle)) ??
34
+ body.canons.find((c) => c.id === slugOrName) ??
35
+ null);
36
+ }
37
+ catch {
38
+ return null;
39
+ }
40
+ }
41
+ async function ask(question, opts = {}) {
42
+ if (opts.mask && stdin.isTTY && typeof stdin.setRawMode === "function") {
43
+ stdout.write(question);
44
+ const answer = await readMasked();
45
+ return answer.trim();
46
+ }
47
+ const rl = createInterface({ input: stdin, output: stdout, terminal: true });
48
+ try {
49
+ const answer = await rl.question(question);
50
+ return answer.trim();
51
+ }
52
+ finally {
53
+ rl.close();
54
+ }
55
+ }
56
+ function readMasked() {
57
+ return new Promise((resolve) => {
58
+ let buf = "";
59
+ const onData = (chunk) => {
60
+ const s = chunk.toString("utf8");
61
+ for (const ch of s) {
62
+ if (ch === "\n" || ch === "\r") {
63
+ stdin.removeListener("data", onData);
64
+ stdin.setRawMode(false);
65
+ stdin.pause();
66
+ stdout.write("\n");
67
+ resolve(buf);
68
+ return;
69
+ }
70
+ if (ch === "\u0003") {
71
+ process.exit(130);
72
+ }
73
+ if (ch === "\u007f" || ch === "\b") {
74
+ if (buf.length > 0) {
75
+ buf = buf.slice(0, -1);
76
+ stdout.write("\b \b");
77
+ }
78
+ continue;
79
+ }
80
+ buf += ch;
81
+ stdout.write("*");
82
+ }
83
+ };
84
+ stdin.setRawMode(true);
85
+ stdin.resume();
86
+ stdin.on("data", onData);
87
+ });
88
+ }
89
+ function mergeEnv(existing, updates) {
90
+ const lines = existing.length > 0 ? existing.split(/\r?\n/) : [];
91
+ const seen = new Set();
92
+ const out = [];
93
+ for (const line of lines) {
94
+ const match = /^([A-Z0-9_]+)=/.exec(line);
95
+ if (match && Object.prototype.hasOwnProperty.call(updates, match[1])) {
96
+ out.push(`${match[1]}=${updates[match[1]]}`);
97
+ seen.add(match[1]);
98
+ }
99
+ else {
100
+ out.push(line);
101
+ }
102
+ }
103
+ if (out.length > 0 && out[out.length - 1] !== "")
104
+ out.push("");
105
+ for (const [key, value] of Object.entries(updates)) {
106
+ if (!seen.has(key))
107
+ out.push(`${key}=${value}`);
108
+ }
109
+ if (out[out.length - 1] !== "")
110
+ out.push("");
111
+ return out.join("\n");
112
+ }
113
+ async function init(canonSlug) {
114
+ console.log("RANKIGI agent enrollment");
115
+ console.log("");
116
+ console.log("Get your API key from https://app.rankigi.com/dashboard/keys");
117
+ console.log("");
118
+ const apiKey = await ask("RANKIGI_API_KEY: ", { mask: true });
119
+ if (!apiKey) {
120
+ console.error("Error: API key is required.");
121
+ process.exit(1);
122
+ }
123
+ const agentName = await ask("Agent name: ");
124
+ if (!agentName) {
125
+ console.error("Error: agent name is required.");
126
+ process.exit(1);
127
+ }
128
+ const baseUrl = DEFAULT_BASE_URL.replace(/\/$/, "");
129
+ const url = `${baseUrl}/api/agents/birth`;
130
+ // Optional canon resolution. If --canon was supplied, look the slug up
131
+ // against the public registry before enrollment so we can include
132
+ // default_canon_id in the birth body and surface the canon name in the
133
+ // success message. Resolution failures are non-fatal; org default applies.
134
+ let canonResolved = null;
135
+ if (canonSlug) {
136
+ canonResolved = await resolveCanonByFlag(baseUrl, apiKey, canonSlug);
137
+ if (!canonResolved) {
138
+ console.error(`Warning: canon "${canonSlug}" not found in registry. Falling back to org default.`);
139
+ }
140
+ }
141
+ const birthBody = { name: agentName };
142
+ if (canonResolved)
143
+ birthBody.default_canon_id = canonResolved.id;
144
+ let res;
145
+ try {
146
+ res = await fetch(url, {
147
+ method: "POST",
148
+ headers: {
149
+ "Content-Type": "application/json",
150
+ Authorization: `Bearer ${apiKey}`,
151
+ },
152
+ body: JSON.stringify(birthBody),
153
+ });
154
+ }
155
+ catch (e) {
156
+ const msg = e instanceof Error ? e.message : String(e);
157
+ console.error(`Error: request to ${url} failed: ${msg}`);
158
+ process.exit(1);
159
+ return;
160
+ }
161
+ const text = await res.text();
162
+ let body;
163
+ try {
164
+ body = JSON.parse(text);
165
+ }
166
+ catch {
167
+ console.error(`Error: non JSON response from server (HTTP ${res.status}).`);
168
+ console.error(text.slice(0, 500));
169
+ process.exit(1);
170
+ return;
171
+ }
172
+ if (!res.ok || !body.ok || !body.agent || !body.passport || !body.signing_private_key_b64) {
173
+ const reason = body.error || `HTTP ${res.status}`;
174
+ console.error(`Error: agent birth failed: ${reason}`);
175
+ process.exit(1);
176
+ return;
177
+ }
178
+ const credentialJson = JSON.stringify({
179
+ v: 1,
180
+ typ: "rankigi_agent_credential",
181
+ env: "live",
182
+ apiKey,
183
+ agentId: body.agent.id,
184
+ passportId: body.passport.id,
185
+ signingPrivateKey: body.signing_private_key_b64,
186
+ alg: "Ed25519",
187
+ issuedAt: new Date().toISOString(),
188
+ });
189
+ const credentialToken = "rnk_live_cred_v1." +
190
+ Buffer.from(credentialJson, "utf8")
191
+ .toString("base64")
192
+ .replace(/\+/g, "-")
193
+ .replace(/\//g, "_")
194
+ .replace(/=+$/, "");
195
+ const envPath = join(process.cwd(), ".env");
196
+ const existing = existsSync(envPath) ? readFileSync(envPath, "utf8") : "";
197
+ const updated = mergeEnv(existing, {
198
+ RANKIGI_CREDENTIAL: credentialToken,
199
+ ANTHROPIC_API_KEY: "",
200
+ });
201
+ writeFileSync(envPath, updated, { mode: 0o600 });
202
+ console.log("");
203
+ console.log("Agent enrolled successfully.");
204
+ console.log("RANKIGI_CREDENTIAL written to .env");
205
+ console.log("");
206
+ console.log(`Your agent: ${body.agent.name}`);
207
+ console.log(`Agent ID: ${body.agent.id}`);
208
+ if (canonResolved) {
209
+ console.log(`Governing standard: ${canonResolved.name} v${canonResolved.version}`);
210
+ }
211
+ console.log("");
212
+ console.log("Add ANTHROPIC_API_KEY to your .env and run:");
213
+ console.log("npx ts-node examples/anthropic_demo.ts");
214
+ }
215
+ function resolveDemoAgentPath() {
216
+ // Try repo-relative paths so `rankigi run` works whether invoked from the
217
+ // monorepo root or from the installed CLI's dist directory.
218
+ const here = dirname(fileURLToPath(import.meta.url));
219
+ const candidates = [
220
+ join(process.cwd(), "scripts", "demo-agent.ts"),
221
+ resolve(here, "..", "..", "..", "..", "scripts", "demo-agent.ts"),
222
+ resolve(here, "..", "..", "..", "scripts", "demo-agent.ts"),
223
+ ];
224
+ for (const p of candidates)
225
+ if (existsSync(p))
226
+ return p;
227
+ return null;
228
+ }
229
+ async function run(model) {
230
+ if (model !== "claude" && model !== undefined) {
231
+ console.error(`Unsupported model: ${model}. Supported: claude`);
232
+ process.exit(1);
233
+ }
234
+ const scriptPath = resolveDemoAgentPath();
235
+ if (!scriptPath) {
236
+ console.error("Could not locate scripts/demo-agent.ts. Run from the rankigi repo.");
237
+ process.exit(1);
238
+ }
239
+ const child = spawn("npx", ["tsx", scriptPath], {
240
+ stdio: "inherit",
241
+ env: process.env,
242
+ });
243
+ child.on("exit", (code) => process.exit(code ?? 0));
244
+ }
245
+ function parseFlag(argv, flag) {
246
+ const eqPrefix = flag + "=";
247
+ for (let i = 0; i < argv.length; i++) {
248
+ const a = argv[i];
249
+ if (a === flag)
250
+ return argv[i + 1];
251
+ if (a.startsWith(eqPrefix))
252
+ return a.slice(eqPrefix.length);
253
+ }
254
+ return undefined;
255
+ }
256
+ async function main() {
257
+ const cmd = process.argv[2];
258
+ if (cmd === "init" || cmd === undefined) {
259
+ const canon = parseFlag(process.argv.slice(2), "--canon");
260
+ await init(canon);
261
+ return;
262
+ }
263
+ if (cmd === "run") {
264
+ await run(process.argv[3]);
265
+ return;
266
+ }
267
+ console.error(`Unknown command: ${cmd}`);
268
+ console.error("Usage: rankigi init [--canon <slug>] | rankigi run claude");
269
+ process.exit(1);
270
+ }
271
+ main().catch((e) => {
272
+ const msg = e instanceof Error ? e.message : String(e);
273
+ console.error(`Error: ${msg}`);
274
+ process.exit(1);
275
+ });
package/package.json ADDED
@@ -0,0 +1,26 @@
1
+ {
2
+ "name": "@rankigi/cli",
3
+ "version": "1.0.0",
4
+ "description": "RANKIGI CLI. One command to enroll an agent and write credentials to .env.",
5
+ "license": "MIT",
6
+ "author": "Rankigi <hello@rankigi.com>",
7
+ "type": "module",
8
+ "main": "./dist/index.js",
9
+ "bin": {
10
+ "rankigi": "./dist/index.js"
11
+ },
12
+ "files": ["dist", "README.md"],
13
+ "publishConfig": {
14
+ "access": "public"
15
+ },
16
+ "scripts": {
17
+ "build": "tsc"
18
+ },
19
+ "devDependencies": {
20
+ "@types/node": "^20",
21
+ "typescript": "^5.4"
22
+ },
23
+ "engines": {
24
+ "node": ">=18"
25
+ }
26
+ }