rubric-chat 0.2.3 → 0.3.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.
- package/dist/commands/login.js +37 -84
- package/dist/commands/login.js.map +1 -1
- package/dist/lib/api.js +7 -4
- package/dist/lib/api.js.map +1 -1
- package/package.json +1 -1
- package/dist/lib/local-server.js +0 -141
- package/dist/lib/local-server.js.map +0 -1
package/dist/commands/login.js
CHANGED
|
@@ -1,115 +1,68 @@
|
|
|
1
1
|
import kleur from "kleur";
|
|
2
|
-
import openBrowser from "open";
|
|
3
2
|
import prompts from "prompts";
|
|
4
|
-
import { fetchMe, requestMagicLink,
|
|
5
|
-
import {
|
|
6
|
-
import { startLocalCallback } from "../lib/local-server.js";
|
|
3
|
+
import { fetchMe, requestMagicLink, verifyOtp } from "../lib/api.js";
|
|
4
|
+
import { storeToken } from "../lib/config.js";
|
|
7
5
|
/**
|
|
8
|
-
*
|
|
9
|
-
*
|
|
6
|
+
* OTP login. Pure terminal — no browser, no local server, no port races.
|
|
7
|
+
*
|
|
8
|
+
* 1. Ask for email (or use --email).
|
|
9
|
+
* 2. Backend sends an email with a 6-digit code.
|
|
10
|
+
* 3. User types the code into the terminal.
|
|
11
|
+
* 4. CLI exchanges it for a JWT and stores it in the keychain.
|
|
10
12
|
*/
|
|
11
13
|
export async function runLogin(options = {}) {
|
|
12
14
|
const email = await ensureEmail(options.email);
|
|
13
15
|
if (!email) {
|
|
14
|
-
// User hit Escape or Ctrl-C at the prompt — exit cleanly, no error.
|
|
15
16
|
console.log(kleur.dim("Cancelled. Run `rubric-chat login` when you're ready."));
|
|
16
17
|
return false;
|
|
17
18
|
}
|
|
18
|
-
if (!options.noBrowser) {
|
|
19
|
-
try {
|
|
20
|
-
const ok = await loginViaBrowserCallback(email);
|
|
21
|
-
if (ok)
|
|
22
|
-
return true;
|
|
23
|
-
}
|
|
24
|
-
catch (err) {
|
|
25
|
-
console.error(kleur.yellow(`Browser flow couldn't complete: ${err.message}`));
|
|
26
|
-
console.error(kleur.dim("Falling back to manual token entry…"));
|
|
27
|
-
}
|
|
28
|
-
}
|
|
29
|
-
return loginViaManualToken(email);
|
|
30
|
-
}
|
|
31
|
-
async function ensureEmail(provided) {
|
|
32
|
-
if (provided)
|
|
33
|
-
return provided;
|
|
34
|
-
const response = await prompts({
|
|
35
|
-
type: "text",
|
|
36
|
-
name: "email",
|
|
37
|
-
message: "Email"
|
|
38
|
-
});
|
|
39
|
-
return response.email;
|
|
40
|
-
}
|
|
41
|
-
async function loginViaBrowserCallback(email) {
|
|
42
|
-
const callback = await startLocalCallback();
|
|
43
|
-
try {
|
|
44
|
-
await requestMagicLink(email, {
|
|
45
|
-
callbackUrl: callback.callbackUrl,
|
|
46
|
-
callbackState: callback.state
|
|
47
|
-
});
|
|
48
|
-
}
|
|
49
|
-
catch (err) {
|
|
50
|
-
callback.close();
|
|
51
|
-
throw err;
|
|
52
|
-
}
|
|
53
|
-
console.log(kleur.cyan("\nOpening browser to ") +
|
|
54
|
-
kleur.bold(email) +
|
|
55
|
-
kleur.cyan("'s magic link…"));
|
|
56
|
-
console.log(kleur.dim(`Listening on ${callback.callbackUrl} (state ${callback.state.slice(0, 8)}…)`));
|
|
57
|
-
// The magic-link email is the actual entry point; we open the inbox-agnostic
|
|
58
|
-
// login page so the user knows what to expect while they wait for the email.
|
|
59
|
-
void openBrowser(`${getWebBaseUrl()}/auth/login`).catch(() => undefined);
|
|
60
|
-
try {
|
|
61
|
-
const result = await callback.wait();
|
|
62
|
-
await storeToken(result.jwt);
|
|
63
|
-
const me = await fetchMe();
|
|
64
|
-
console.log(kleur.green(`\nSigned in as ${me.email}.`));
|
|
65
|
-
return true;
|
|
66
|
-
}
|
|
67
|
-
catch (err) {
|
|
68
|
-
if (err.message.includes("Timed out")) {
|
|
69
|
-
console.error(kleur.yellow("\nBrowser sign-in timed out."));
|
|
70
|
-
return false;
|
|
71
|
-
}
|
|
72
|
-
throw err;
|
|
73
|
-
}
|
|
74
|
-
}
|
|
75
|
-
async function loginViaManualToken(email) {
|
|
76
|
-
console.log(kleur.cyan("\nWe'll send a magic link to ") + kleur.bold(email) + kleur.cyan("."));
|
|
77
19
|
try {
|
|
78
|
-
await requestMagicLink(email);
|
|
20
|
+
await requestMagicLink(email, { authType: "otp" });
|
|
79
21
|
}
|
|
80
22
|
catch (err) {
|
|
81
|
-
console.error(kleur.red(`
|
|
23
|
+
console.error(kleur.red(`Couldn't send the sign-in code: ${err.message}`));
|
|
82
24
|
process.exitCode = 1;
|
|
83
25
|
return false;
|
|
84
26
|
}
|
|
85
|
-
console.log(
|
|
86
|
-
console.log(kleur.dim("
|
|
87
|
-
|
|
88
|
-
console.log(kleur.dim(" 3. The link looks like ") + kleur.cyan("https://www.rubric.chat/auth/verify?token=") +
|
|
89
|
-
kleur.bold("<TOKEN>") + kleur.dim("."));
|
|
90
|
-
console.log(kleur.dim(" 4. Paste just the ") + kleur.bold("<TOKEN>") + kleur.dim(" part below (everything after `token=`).") + "\n");
|
|
91
|
-
const tokenAnswer = await prompts({
|
|
27
|
+
console.log("\n " + kleur.cyan("Sent a 6-digit code to ") + kleur.bold(email) + kleur.cyan("."));
|
|
28
|
+
console.log(kleur.dim(" The code expires in 30 minutes.\n"));
|
|
29
|
+
const codeAnswer = await prompts({
|
|
92
30
|
type: "text",
|
|
93
|
-
name: "
|
|
94
|
-
message: "
|
|
31
|
+
name: "code",
|
|
32
|
+
message: "Sign-in code",
|
|
33
|
+
validate: (value) => {
|
|
34
|
+
const cleaned = (value ?? "").replace(/\D/g, "");
|
|
35
|
+
return cleaned.length === 6 || "Code must be 6 digits.";
|
|
36
|
+
}
|
|
95
37
|
});
|
|
96
|
-
const
|
|
97
|
-
if (!
|
|
98
|
-
console.
|
|
99
|
-
process.exitCode = 1;
|
|
38
|
+
const code = codeAnswer.code?.replace(/\D/g, "");
|
|
39
|
+
if (!code) {
|
|
40
|
+
console.log(kleur.dim("Cancelled. Run `rubric-chat login` to request a new code."));
|
|
100
41
|
return false;
|
|
101
42
|
}
|
|
102
43
|
try {
|
|
103
|
-
const session = await
|
|
44
|
+
const session = await verifyOtp(email, code);
|
|
104
45
|
await storeToken(session.jwt);
|
|
105
46
|
const me = await fetchMe();
|
|
106
|
-
console.log(kleur.green(
|
|
47
|
+
console.log(kleur.green(`\nSigned in as ${me.email}.\n`));
|
|
107
48
|
return true;
|
|
108
49
|
}
|
|
109
50
|
catch (err) {
|
|
110
|
-
console.error(kleur.red(
|
|
51
|
+
console.error(kleur.red(`\nVerification failed: ${err.message}`));
|
|
52
|
+
console.error(kleur.dim("Double-check the code, or run `rubric-chat login` again for a fresh one."));
|
|
111
53
|
process.exitCode = 1;
|
|
112
54
|
return false;
|
|
113
55
|
}
|
|
114
56
|
}
|
|
57
|
+
async function ensureEmail(provided) {
|
|
58
|
+
if (provided)
|
|
59
|
+
return provided;
|
|
60
|
+
const response = await prompts({
|
|
61
|
+
type: "text",
|
|
62
|
+
name: "email",
|
|
63
|
+
message: "Email",
|
|
64
|
+
validate: (value) => /^[^@\s]+@[^@\s]+\.[^@\s]+$/.test((value ?? "").trim()) || "That doesn't look like an email."
|
|
65
|
+
});
|
|
66
|
+
return response.email?.trim();
|
|
67
|
+
}
|
|
115
68
|
//# sourceMappingURL=login.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"login.js","sourceRoot":"","sources":["../../src/commands/login.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,MAAM,OAAO,CAAC;AAC1B,OAAO,
|
|
1
|
+
{"version":3,"file":"login.js","sourceRoot":"","sources":["../../src/commands/login.ts"],"names":[],"mappings":"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"}
|
package/dist/lib/api.js
CHANGED
|
@@ -16,14 +16,17 @@ export async function requestMagicLink(email, opts = {}) {
|
|
|
16
16
|
headers: { "content-type": "application/json" },
|
|
17
17
|
body: JSON.stringify({
|
|
18
18
|
email,
|
|
19
|
-
|
|
20
|
-
callback_state: opts.callbackState ?? null
|
|
19
|
+
auth_type: opts.authType ?? "link"
|
|
21
20
|
})
|
|
22
21
|
});
|
|
23
22
|
await unwrap(response);
|
|
24
23
|
}
|
|
25
|
-
export async function
|
|
26
|
-
const response = await fetch(`${getApiBaseUrl()}/auth/verify
|
|
24
|
+
export async function verifyOtp(email, code) {
|
|
25
|
+
const response = await fetch(`${getApiBaseUrl()}/auth/verify-otp`, {
|
|
26
|
+
method: "POST",
|
|
27
|
+
headers: { "content-type": "application/json" },
|
|
28
|
+
body: JSON.stringify({ email, code })
|
|
29
|
+
});
|
|
27
30
|
return unwrap(response);
|
|
28
31
|
}
|
|
29
32
|
export async function fetchMe() {
|
package/dist/lib/api.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"api.js","sourceRoot":"","sources":["../../src/lib/api.ts"],"names":[],"mappings":"AAOA,OAAO,EAAE,aAAa,EAAE,SAAS,EAAE,MAAM,aAAa,CAAC;AAEvD,KAAK,UAAU,WAAW;IACxB,MAAM,GAAG,GAAG,MAAM,SAAS,EAAE,CAAC;IAC9B,OAAO,GAAG,CAAC,CAAC,CAAC,EAAE,aAAa,EAAE,UAAU,GAAG,EAAE,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;AACvD,CAAC;AAED,KAAK,UAAU,MAAM,CAAI,QAAkB;IACzC,IAAI,CAAC,QAAQ,CAAC,EAAE,EAAE,CAAC;QACjB,MAAM,MAAM,GAAG,MAAM,QAAQ,CAAC,IAAI,EAAE,CAAC,KAAK,CAAC,GAAG,EAAE,CAAC,EAAE,CAAC,CAAC;QACrD,MAAM,IAAI,KAAK,CAAC,MAAM,IAAI,mBAAmB,QAAQ,CAAC,MAAM,EAAE,CAAC,CAAC;IAClE,CAAC;IACD,OAAO,CAAC,MAAM,QAAQ,CAAC,IAAI,EAAE,CAAM,CAAC;AACtC,CAAC;
|
|
1
|
+
{"version":3,"file":"api.js","sourceRoot":"","sources":["../../src/lib/api.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "rubric-chat",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"description": "A strict 0–100 score for AI conversations. Auto-discovers Claude Code, Codex CLI, and Cursor sessions; also accepts ChatGPT and Claude.ai exports. Six dimensions, eight archetypes, shareable score card.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
package/dist/lib/local-server.js
DELETED
|
@@ -1,141 +0,0 @@
|
|
|
1
|
-
import { createServer } from "node:http";
|
|
2
|
-
import { randomBytes } from "node:crypto";
|
|
3
|
-
const SUCCESS_HTML = `<!doctype html><html><head><meta charset="utf-8"><title>Rubric — Signed in</title>
|
|
4
|
-
<style>
|
|
5
|
-
:root { color-scheme: dark; }
|
|
6
|
-
html, body { margin: 0; height: 100%; font-family: ui-sans-serif, system-ui, -apple-system, sans-serif; background: #0C0B0A; color: #F2EEE3; }
|
|
7
|
-
main { min-height: 100%; display: grid; place-items: center; padding: 24px; }
|
|
8
|
-
.card { max-width: 420px; padding: 28px 30px; border: 1px solid #2A271F; border-radius: 16px; background: #17150F; box-shadow: 0 12px 28px -16px rgba(0,0,0,.65); }
|
|
9
|
-
.eyebrow { font-size: 11px; letter-spacing: .14em; text-transform: uppercase; color: #857E73; margin: 0 0 8px; }
|
|
10
|
-
h1 { font-family: ui-serif, Georgia, serif; font-weight: 500; font-size: 26px; margin: 0 0 14px; }
|
|
11
|
-
p { color: #B7B1A1; line-height: 1.55; margin: 0 0 12px; font-size: 15px; }
|
|
12
|
-
code { background: #1F1C16; border: 1px solid #2A271F; border-radius: 6px; padding: 2px 6px; font-size: 12px; color: #F2EEE3; }
|
|
13
|
-
</style></head><body><main><div class="card">
|
|
14
|
-
<p class="eyebrow">Rubric CLI</p>
|
|
15
|
-
<h1>You can close this tab.</h1>
|
|
16
|
-
<p>Your terminal session has picked up the credentials. Head back to <code>rubric-chat</code>.</p>
|
|
17
|
-
</div></main></body></html>`;
|
|
18
|
-
const FAILURE_HTML = `<!doctype html><html><head><meta charset="utf-8"><title>Rubric — Bad request</title></head>
|
|
19
|
-
<body style="margin:0;font-family:system-ui;background:#0C0B0A;color:#F2EEE3;display:grid;place-items:center;height:100vh">
|
|
20
|
-
<div style="max-width:420px;padding:24px;border:1px solid #9B2C2C;border-radius:12px;background:#17150F">
|
|
21
|
-
<h1 style="font-size:18px;margin:0 0 10px">Something looked off.</h1>
|
|
22
|
-
<p style="color:#B7B1A1;font-size:14px;line-height:1.55;margin:0">The CLI couldn't verify this response. Try running <code>rubric-chat login</code> again.</p>
|
|
23
|
-
</div></body></html>`;
|
|
24
|
-
function setCors(res) {
|
|
25
|
-
res.setHeader("Access-Control-Allow-Origin", "*");
|
|
26
|
-
res.setHeader("Access-Control-Allow-Methods", "POST, OPTIONS");
|
|
27
|
-
res.setHeader("Access-Control-Allow-Headers", "Content-Type");
|
|
28
|
-
}
|
|
29
|
-
async function readJson(req) {
|
|
30
|
-
return new Promise((resolve, reject) => {
|
|
31
|
-
const chunks = [];
|
|
32
|
-
req.on("data", (chunk) => chunks.push(chunk));
|
|
33
|
-
req.on("end", () => {
|
|
34
|
-
const raw = Buffer.concat(chunks).toString("utf8") || "{}";
|
|
35
|
-
try {
|
|
36
|
-
resolve(JSON.parse(raw));
|
|
37
|
-
}
|
|
38
|
-
catch (err) {
|
|
39
|
-
reject(err);
|
|
40
|
-
}
|
|
41
|
-
});
|
|
42
|
-
req.on("error", reject);
|
|
43
|
-
});
|
|
44
|
-
}
|
|
45
|
-
/**
|
|
46
|
-
* Spawn a one-shot HTTP server on a random localhost port. The CLI passes its
|
|
47
|
-
* URL + state to the backend with the magic-link request; the verify page in
|
|
48
|
-
* the browser POSTs `{jwt, state, user}` here after a successful sign-in.
|
|
49
|
-
*
|
|
50
|
-
* Async because server.address() returns null until the `listening` event
|
|
51
|
-
* fires — reading the port synchronously was a real bug (returned undefined
|
|
52
|
-
* → "Cannot read properties of null (reading 'port')" later).
|
|
53
|
-
*/
|
|
54
|
-
export async function startLocalCallback(timeoutMs = 5 * 60 * 1000) {
|
|
55
|
-
const state = randomBytes(16).toString("hex");
|
|
56
|
-
let resolveResult = null;
|
|
57
|
-
let rejectResult = null;
|
|
58
|
-
const resultPromise = new Promise((resolve, reject) => {
|
|
59
|
-
resolveResult = resolve;
|
|
60
|
-
rejectResult = reject;
|
|
61
|
-
});
|
|
62
|
-
const server = createServer((req, res) => {
|
|
63
|
-
if (req.method === "OPTIONS") {
|
|
64
|
-
setCors(res);
|
|
65
|
-
res.writeHead(204);
|
|
66
|
-
res.end();
|
|
67
|
-
return;
|
|
68
|
-
}
|
|
69
|
-
if (req.method !== "POST" || !req.url?.startsWith("/auth")) {
|
|
70
|
-
res.writeHead(404, { "content-type": "text/plain" });
|
|
71
|
-
res.end("not found");
|
|
72
|
-
return;
|
|
73
|
-
}
|
|
74
|
-
setCors(res);
|
|
75
|
-
readJson(req)
|
|
76
|
-
.then((payload) => {
|
|
77
|
-
if (!payload || typeof payload !== "object") {
|
|
78
|
-
res.writeHead(400, { "content-type": "text/html" });
|
|
79
|
-
res.end(FAILURE_HTML);
|
|
80
|
-
return;
|
|
81
|
-
}
|
|
82
|
-
const body = payload;
|
|
83
|
-
if (body.state !== state) {
|
|
84
|
-
res.writeHead(400, { "content-type": "text/html" });
|
|
85
|
-
res.end(FAILURE_HTML);
|
|
86
|
-
rejectResult?.(new Error("CSRF state mismatch"));
|
|
87
|
-
return;
|
|
88
|
-
}
|
|
89
|
-
if (typeof body.jwt !== "string" || !body.jwt) {
|
|
90
|
-
res.writeHead(400, { "content-type": "text/html" });
|
|
91
|
-
res.end(FAILURE_HTML);
|
|
92
|
-
rejectResult?.(new Error("Missing jwt in callback"));
|
|
93
|
-
return;
|
|
94
|
-
}
|
|
95
|
-
res.writeHead(200, { "content-type": "text/html" });
|
|
96
|
-
res.end(SUCCESS_HTML);
|
|
97
|
-
resolveResult?.({
|
|
98
|
-
jwt: body.jwt,
|
|
99
|
-
user: body.user
|
|
100
|
-
});
|
|
101
|
-
})
|
|
102
|
-
.catch((err) => {
|
|
103
|
-
res.writeHead(400, { "content-type": "text/html" });
|
|
104
|
-
res.end(FAILURE_HTML);
|
|
105
|
-
rejectResult?.(err instanceof Error ? err : new Error(String(err)));
|
|
106
|
-
});
|
|
107
|
-
});
|
|
108
|
-
await new Promise((resolve, reject) => {
|
|
109
|
-
server.once("error", reject);
|
|
110
|
-
server.listen(0, "127.0.0.1", () => {
|
|
111
|
-
server.removeListener("error", reject);
|
|
112
|
-
resolve();
|
|
113
|
-
});
|
|
114
|
-
});
|
|
115
|
-
const addr = server.address();
|
|
116
|
-
if (!addr || typeof addr === "string") {
|
|
117
|
-
server.close();
|
|
118
|
-
throw new Error("Local callback server failed to bind a port");
|
|
119
|
-
}
|
|
120
|
-
const port = addr.port;
|
|
121
|
-
const callbackUrl = `http://127.0.0.1:${port}/auth`;
|
|
122
|
-
const timer = setTimeout(() => {
|
|
123
|
-
rejectResult?.(new Error("Timed out waiting for browser callback"));
|
|
124
|
-
server.close();
|
|
125
|
-
}, timeoutMs);
|
|
126
|
-
const close = () => {
|
|
127
|
-
clearTimeout(timer);
|
|
128
|
-
server.close();
|
|
129
|
-
};
|
|
130
|
-
const wait = async () => {
|
|
131
|
-
try {
|
|
132
|
-
const result = await resultPromise;
|
|
133
|
-
return result;
|
|
134
|
-
}
|
|
135
|
-
finally {
|
|
136
|
-
close();
|
|
137
|
-
}
|
|
138
|
-
};
|
|
139
|
-
return { port, callbackUrl, state, wait, close };
|
|
140
|
-
}
|
|
141
|
-
//# sourceMappingURL=local-server.js.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"local-server.js","sourceRoot":"","sources":["../../src/lib/local-server.ts"],"names":[],"mappings":"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"}
|