syndai 0.1.1 → 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/CHANGELOG.md +31 -0
- package/README.md +76 -52
- package/bin/syndai.js +13 -29
- package/package.json +6 -11
- package/src/auth.js +179 -0
- package/src/changes.js +307 -0
- package/src/errors.js +128 -0
- package/src/http.js +153 -0
- package/src/main.js +212 -0
- package/src/mcp.js +93 -0
- package/src/projects.js +168 -0
- package/src/routes.js +56 -0
- package/src/settings.js +160 -0
- package/src/store.js +134 -0
- package/src/ui.js +133 -0
- package/src/watch.js +292 -0
- package/LICENSE +0 -9
- package/src/api.js +0 -70
- package/src/claude.js +0 -145
- package/src/codex.js +0 -71
- package/src/connect.js +0 -122
- package/src/proc.js +0 -36
- package/src/prompt.js +0 -43
- package/src/tokens.js +0 -45
package/src/claude.js
DELETED
|
@@ -1,145 +0,0 @@
|
|
|
1
|
-
// Claude plan connect: mint a fresh setup-token, verify it with the official CLI,
|
|
2
|
-
// or fall back to a hidden paste when `claude` isn't installed. The token is never
|
|
3
|
-
// printed, never written to disk, never put on the clipboard.
|
|
4
|
-
|
|
5
|
-
import { spawnSync } from "node:child_process";
|
|
6
|
-
import fs from "node:fs";
|
|
7
|
-
import os from "node:os";
|
|
8
|
-
import path from "node:path";
|
|
9
|
-
import { extractOatToken, stripInternalWhitespace } from "./tokens.js";
|
|
10
|
-
import { onPath, isBsdScript } from "./proc.js";
|
|
11
|
-
import { readHidden } from "./prompt.js";
|
|
12
|
-
|
|
13
|
-
// `claude setup-token` draws a full-screen terminal UI, so it needs a PTY; we get
|
|
14
|
-
// one from script(1). BSD (macOS) and util-linux take different argument shapes —
|
|
15
|
-
// both handled. Injected in tests via `deps` so the pure extraction stays testable.
|
|
16
|
-
export function buildScriptArgv() {
|
|
17
|
-
if (isBsdScript()) {
|
|
18
|
-
return ["script", ["-q", "/dev/null", "claude", "setup-token"]];
|
|
19
|
-
}
|
|
20
|
-
return ["script", ["-qec", "claude setup-token", "/dev/null"]];
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
function runSetupToken(deps) {
|
|
24
|
-
const [cmd, args] = deps.buildScriptArgv();
|
|
25
|
-
const res = deps.spawnSync(cmd, args, { encoding: "utf8", stdio: ["inherit", "pipe", "inherit"] });
|
|
26
|
-
const out = (res.stdout || "") + (res.stderr || "");
|
|
27
|
-
return deps.extractOatToken(out);
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
/**
|
|
31
|
-
* Verify a Claude plan token by asking the official CLI to answer a one-word
|
|
32
|
-
* prompt. A rate-limited token still exits 0, so we DO NOT trust the exit code —
|
|
33
|
-
* success is a `result` frame with `is_error === false`. A throwaway
|
|
34
|
-
* CLAUDE_CONFIG_DIR and blanked ANTHROPIC_* keep the check hermetic and force the
|
|
35
|
-
* token (not a stray key) to authenticate.
|
|
36
|
-
*/
|
|
37
|
-
export function verifyClaudeToken(token, deps = {}) {
|
|
38
|
-
const _spawnSync = deps.spawnSync || spawnSync;
|
|
39
|
-
const _mkdtemp = deps.mkdtempSync || ((p) => fs.mkdtempSync(p));
|
|
40
|
-
const configDir = _mkdtemp(path.join(os.tmpdir(), "syndai-claude-"));
|
|
41
|
-
try {
|
|
42
|
-
const res = _spawnSync("claude", ["-p", "ping", "--output-format", "json"], {
|
|
43
|
-
encoding: "utf8",
|
|
44
|
-
stdio: ["ignore", "pipe", "pipe"],
|
|
45
|
-
env: {
|
|
46
|
-
...process.env,
|
|
47
|
-
CLAUDE_CODE_OAUTH_TOKEN: token,
|
|
48
|
-
ANTHROPIC_API_KEY: "",
|
|
49
|
-
ANTHROPIC_AUTH_TOKEN: "",
|
|
50
|
-
ANTHROPIC_BASE_URL: "",
|
|
51
|
-
CLAUDE_CONFIG_DIR: configDir,
|
|
52
|
-
},
|
|
53
|
-
});
|
|
54
|
-
return resultIsError(res.stdout) === false;
|
|
55
|
-
} finally {
|
|
56
|
-
try {
|
|
57
|
-
fs.rmSync(configDir, { recursive: true, force: true });
|
|
58
|
-
} catch {
|
|
59
|
-
/* ignore */
|
|
60
|
-
}
|
|
61
|
-
}
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
// Parse `claude -p --output-format json` stdout and report the result frame's
|
|
65
|
-
// is_error. Returns true/false when found, or null when no result frame parsed.
|
|
66
|
-
export function resultIsError(stdout) {
|
|
67
|
-
const text = String(stdout || "").trim();
|
|
68
|
-
if (!text) return null;
|
|
69
|
-
// Try a single JSON object first, then fall back to JSON-lines (stream-json).
|
|
70
|
-
const candidates = [];
|
|
71
|
-
try {
|
|
72
|
-
candidates.push(JSON.parse(text));
|
|
73
|
-
} catch {
|
|
74
|
-
for (const line of text.split(/\r?\n/)) {
|
|
75
|
-
const t = line.trim();
|
|
76
|
-
if (!t) continue;
|
|
77
|
-
try {
|
|
78
|
-
candidates.push(JSON.parse(t));
|
|
79
|
-
} catch {
|
|
80
|
-
/* skip noise */
|
|
81
|
-
}
|
|
82
|
-
}
|
|
83
|
-
}
|
|
84
|
-
for (const frame of candidates) {
|
|
85
|
-
if (frame && typeof frame === "object" && (frame.type === "result" || "is_error" in frame)) {
|
|
86
|
-
return Boolean(frame.is_error);
|
|
87
|
-
}
|
|
88
|
-
}
|
|
89
|
-
return null;
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
/**
|
|
93
|
-
* Obtain a verified Claude plan token, or null if the user declines / it fails.
|
|
94
|
-
* - `claude` on PATH: mint via setup-token (PTY), then verify.
|
|
95
|
-
* - not installed: print the paste-fallback instruction, read a HIDDEN paste in
|
|
96
|
-
* this same command, strip wrapping whitespace, then verify.
|
|
97
|
-
* The token is returned to the caller for the single upload; nothing else touches it.
|
|
98
|
-
*/
|
|
99
|
-
export async function connectClaude(deps = {}) {
|
|
100
|
-
const _onPath = deps.onPath || onPath;
|
|
101
|
-
const _readHidden = deps.readHidden || readHidden;
|
|
102
|
-
const log = deps.log || console.log;
|
|
103
|
-
|
|
104
|
-
const scriptOnPath = deps.scriptOnPath ?? _onPath("script");
|
|
105
|
-
|
|
106
|
-
if (_onPath("claude") && scriptOnPath) {
|
|
107
|
-
log("Claude: minting a fresh plan token (approve in your browser when Claude asks)…");
|
|
108
|
-
const token = runSetupToken({
|
|
109
|
-
buildScriptArgv: deps.buildScriptArgv || buildScriptArgv,
|
|
110
|
-
spawnSync: deps.spawnSync || spawnSync,
|
|
111
|
-
extractOatToken,
|
|
112
|
-
});
|
|
113
|
-
if (!token) {
|
|
114
|
-
log("Claude: could not read a token from setup-token. Skipping Claude.");
|
|
115
|
-
return null;
|
|
116
|
-
}
|
|
117
|
-
log("Claude: verifying the token with the official CLI…");
|
|
118
|
-
if (!verifyClaudeToken(token, deps)) {
|
|
119
|
-
log("Claude: the minted token did not verify. Skipping Claude.");
|
|
120
|
-
return null;
|
|
121
|
-
}
|
|
122
|
-
log("Claude: token verified ✓");
|
|
123
|
-
return token;
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
// Paste fallback (no `claude`, or no PTY via script(1) — e.g. Windows w/o WSL).
|
|
127
|
-
log("");
|
|
128
|
-
log("Claude CLI not found. To connect your Claude plan, in another terminal run:");
|
|
129
|
-
log(" claude setup-token");
|
|
130
|
-
log("then paste the sk-ant-oat01-… value below (it is hidden and not stored):");
|
|
131
|
-
const raw = await _readHidden("Claude plan token: ");
|
|
132
|
-
const token = stripInternalWhitespace(raw);
|
|
133
|
-
if (!token) {
|
|
134
|
-
log("Claude: nothing pasted. Skipping Claude.");
|
|
135
|
-
return null;
|
|
136
|
-
}
|
|
137
|
-
if (deps.onPath ? _onPath("claude") : onPath("claude")) {
|
|
138
|
-
// If claude showed up we can still verify; otherwise trust the server verify.
|
|
139
|
-
if (!verifyClaudeToken(token, deps)) {
|
|
140
|
-
log("Claude: that token did not verify. Skipping Claude.");
|
|
141
|
-
return null;
|
|
142
|
-
}
|
|
143
|
-
}
|
|
144
|
-
return token;
|
|
145
|
-
}
|
package/src/codex.js
DELETED
|
@@ -1,71 +0,0 @@
|
|
|
1
|
-
// ChatGPT plan connect: give Syndai its OWN Codex login lineage — `codex login`
|
|
2
|
-
// into a throwaway CODEX_HOME, confirm, read the resulting auth.json, then delete
|
|
3
|
-
// the directory. Copying the laptop's ~/.codex/auth.json is deliberately NOT done:
|
|
4
|
-
// two machines sharing one rotating refresh token log each other out (spec §Q3a).
|
|
5
|
-
|
|
6
|
-
import { spawnSync } from "node:child_process";
|
|
7
|
-
import fs from "node:fs";
|
|
8
|
-
import os from "node:os";
|
|
9
|
-
import path from "node:path";
|
|
10
|
-
import { onPath } from "./proc.js";
|
|
11
|
-
|
|
12
|
-
/**
|
|
13
|
-
* Run the Codex own-lineage login and return the parsed auth.json OBJECT, or null if the
|
|
14
|
-
* user declines / it fails. The temp CODEX_HOME is always removed. OPENAI_API_KEY
|
|
15
|
-
* is unset so a stray key can't shadow the plan login; stdin is closed for any
|
|
16
|
-
* non-interactive step.
|
|
17
|
-
*/
|
|
18
|
-
export function connectCodex(deps = {}) {
|
|
19
|
-
const _onPath = deps.onPath || onPath;
|
|
20
|
-
const _spawnSync = deps.spawnSync || spawnSync;
|
|
21
|
-
const log = deps.log || console.log;
|
|
22
|
-
|
|
23
|
-
if (!_onPath("codex")) {
|
|
24
|
-
return null; // no ChatGPT plan connect without the official CLI
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
const home = fs.mkdtempSync(path.join(os.tmpdir(), "syndai-codex-"));
|
|
28
|
-
const env = { ...process.env, CODEX_HOME: home };
|
|
29
|
-
delete env.OPENAI_API_KEY;
|
|
30
|
-
|
|
31
|
-
try {
|
|
32
|
-
log("ChatGPT: opening a Codex login for Syndai (approve in your browser)…");
|
|
33
|
-
const login = _spawnSync("codex", ["login"], { stdio: "inherit", env });
|
|
34
|
-
if (login.status !== 0) {
|
|
35
|
-
log("ChatGPT: codex login did not complete. Skipping ChatGPT.");
|
|
36
|
-
return null;
|
|
37
|
-
}
|
|
38
|
-
// Confirm the session landed; stdin closed so `status` can't block.
|
|
39
|
-
const status = _spawnSync("codex", ["login", "status"], {
|
|
40
|
-
stdio: ["ignore", "pipe", "pipe"],
|
|
41
|
-
encoding: "utf8",
|
|
42
|
-
env,
|
|
43
|
-
});
|
|
44
|
-
if (status.status !== 0) {
|
|
45
|
-
log("ChatGPT: login status check failed. Skipping ChatGPT.");
|
|
46
|
-
return null;
|
|
47
|
-
}
|
|
48
|
-
const authPath = path.join(home, "auth.json");
|
|
49
|
-
if (!fs.existsSync(authPath)) {
|
|
50
|
-
log("ChatGPT: no auth.json produced. Skipping ChatGPT.");
|
|
51
|
-
return null;
|
|
52
|
-
}
|
|
53
|
-
// Return the PARSED object: the server's upload DTO types codex_auth_json as an object,
|
|
54
|
-
// so sending the raw string would be rejected at decode (400) before the credential lands.
|
|
55
|
-
let parsed;
|
|
56
|
-
try {
|
|
57
|
-
parsed = JSON.parse(fs.readFileSync(authPath, "utf8"));
|
|
58
|
-
} catch {
|
|
59
|
-
log("ChatGPT: auth.json was not valid JSON. Skipping ChatGPT.");
|
|
60
|
-
return null;
|
|
61
|
-
}
|
|
62
|
-
log("ChatGPT: login captured ✓");
|
|
63
|
-
return parsed;
|
|
64
|
-
} finally {
|
|
65
|
-
try {
|
|
66
|
-
fs.rmSync(home, { recursive: true, force: true });
|
|
67
|
-
} catch {
|
|
68
|
-
/* ignore */
|
|
69
|
-
}
|
|
70
|
-
}
|
|
71
|
-
}
|
package/src/connect.js
DELETED
|
@@ -1,122 +0,0 @@
|
|
|
1
|
-
// Orchestrates `syndai connect <code>`: claim the pairing, confirm the workspace
|
|
2
|
-
// + phrase, wait for the owner's Approve click, mint/capture whatever plans are
|
|
3
|
-
// present locally, and upload them in exactly ONE call. No token is ever printed.
|
|
4
|
-
|
|
5
|
-
import os from "node:os";
|
|
6
|
-
import { post, ok, errorMessage } from "./api.js";
|
|
7
|
-
import { confirm } from "./prompt.js";
|
|
8
|
-
import { connectClaude } from "./claude.js";
|
|
9
|
-
import { connectCodex } from "./codex.js";
|
|
10
|
-
|
|
11
|
-
const BASE = "/api/v1/coding/auth/pairings";
|
|
12
|
-
const POLL_INTERVAL_MS = 3000;
|
|
13
|
-
const POLL_TIMEOUT_MS = 5 * 60 * 1000; // 5 min — the owner has to click Approve.
|
|
14
|
-
|
|
15
|
-
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
16
|
-
|
|
17
|
-
// The code-authed routes: possession of the code is the credential (spec §4.1).
|
|
18
|
-
// Poll re-uses /claim — the only code-authed status read the CLI can make; it
|
|
19
|
-
// returns the current pairing state each time.
|
|
20
|
-
// ponytail: if the backend adds a dedicated code-authed poll route, swap the path
|
|
21
|
-
// in pollState — the shape ({code, hostname} -> {state, ...}) is unchanged.
|
|
22
|
-
function claim(code) {
|
|
23
|
-
return post(`${BASE}/claim`, { code, hostname: os.hostname() });
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
async function pollForApproval(code, deps) {
|
|
27
|
-
const log = deps.log || console.log;
|
|
28
|
-
const started = deps.now ? deps.now() : Date.now();
|
|
29
|
-
log('Waiting for the workspace owner to click "Approve"…');
|
|
30
|
-
for (;;) {
|
|
31
|
-
const { status, body } = await claim(code);
|
|
32
|
-
if (!ok(status)) return { ok: false, message: errorMessage(body) };
|
|
33
|
-
const state = body && body.state;
|
|
34
|
-
if (state === "approved" || state === "uploaded" || state === "verified") {
|
|
35
|
-
return { ok: true };
|
|
36
|
-
}
|
|
37
|
-
if (state === "failed") return { ok: false, message: "the pairing was rejected or expired" };
|
|
38
|
-
const elapsed = (deps.now ? deps.now() : Date.now()) - started;
|
|
39
|
-
if (elapsed >= POLL_TIMEOUT_MS) {
|
|
40
|
-
return { ok: false, message: "timed out waiting for approval (15-min code TTL — mint a new one)" };
|
|
41
|
-
}
|
|
42
|
-
await sleep(deps.pollIntervalMs ?? POLL_INTERVAL_MS);
|
|
43
|
-
}
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
/**
|
|
47
|
-
* Run the full connect flow. `deps` lets tests inject the network/prompt seams;
|
|
48
|
-
* defaults use the real ones. Returns a process exit code.
|
|
49
|
-
*/
|
|
50
|
-
export async function runConnect(code, deps = {}) {
|
|
51
|
-
const log = deps.log || console.log;
|
|
52
|
-
const err = deps.error || console.error;
|
|
53
|
-
const askConfirm = deps.confirm || confirm;
|
|
54
|
-
|
|
55
|
-
if (!code || typeof code !== "string") {
|
|
56
|
-
err("usage: syndai connect <code>");
|
|
57
|
-
return 2;
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
// 1. Claim — learn the destination workspace + phrase before anything is minted.
|
|
61
|
-
const claimed = await (deps.claim ? deps.claim(code) : claim(code));
|
|
62
|
-
if (!ok(claimed.status)) {
|
|
63
|
-
err(`connect: ${errorMessage(claimed.body)}`);
|
|
64
|
-
return 1;
|
|
65
|
-
}
|
|
66
|
-
const info = claimed.body || {};
|
|
67
|
-
const name = info.workspace_name || "your workspace";
|
|
68
|
-
const email = info.owner_email_masked || "the owner";
|
|
69
|
-
const phrase = info.phrase || "(no phrase)";
|
|
70
|
-
|
|
71
|
-
// 2. Two-sided confirm (default No, no --yes flag).
|
|
72
|
-
const yes = await askConfirm(
|
|
73
|
-
`Connect to workspace "${name}" (${email})? Phrase: ${phrase} [y/N] `,
|
|
74
|
-
);
|
|
75
|
-
if (!yes) {
|
|
76
|
-
log("Aborted. Nothing was sent.");
|
|
77
|
-
return 1;
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
// 3. Wait for the owner's Approve click.
|
|
81
|
-
const approval = deps.pollForApproval
|
|
82
|
-
? await deps.pollForApproval(code, deps)
|
|
83
|
-
: await pollForApproval(code, deps);
|
|
84
|
-
if (!approval.ok) {
|
|
85
|
-
err(`connect: ${approval.message}`);
|
|
86
|
-
return 1;
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
// 4. Collect whatever plans are present on this machine.
|
|
90
|
-
const claudeToken = deps.connectClaude ? await deps.connectClaude(deps) : await connectClaude(deps);
|
|
91
|
-
const codexAuthJson = deps.connectCodex ? deps.connectCodex(deps) : connectCodex(deps);
|
|
92
|
-
|
|
93
|
-
if (!claudeToken && !codexAuthJson) {
|
|
94
|
-
err("connect: no Claude or ChatGPT plan could be connected. Nothing uploaded.");
|
|
95
|
-
return 1;
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
// 5. Exactly one upload, carrying whatever was collected.
|
|
99
|
-
const payload = { code };
|
|
100
|
-
if (claudeToken) payload.claude_oauth_token = claudeToken;
|
|
101
|
-
if (codexAuthJson) payload.codex_auth_json = codexAuthJson;
|
|
102
|
-
|
|
103
|
-
const uploaded = deps.upload ? await deps.upload(payload) : await post(`${BASE}/upload`, payload);
|
|
104
|
-
if (!ok(uploaded.status)) {
|
|
105
|
-
err(`connect: upload failed — ${errorMessage(uploaded.body)}`);
|
|
106
|
-
return 1;
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
// Report the SERVER's outcome, not what we sent: a token can be uploaded but fail server-side
|
|
110
|
-
// verification (a dead ChatGPT plan), and only the server knows which providers actually landed.
|
|
111
|
-
const body = uploaded.body || {};
|
|
112
|
-
const labels = { claude_max: "Claude plan", chatgpt: "ChatGPT plan" };
|
|
113
|
-
const connected = Array.isArray(body.connected) ? body.connected : [];
|
|
114
|
-
const failed = Array.isArray(body.failed) ? body.failed : [];
|
|
115
|
-
for (const p of connected) log(`Connected ✓ · ${labels[p] || p}`);
|
|
116
|
-
for (const p of failed) err(`Not connected · ${labels[p] || p} — the token could not be verified`);
|
|
117
|
-
if (connected.length === 0) {
|
|
118
|
-
err("connect: nothing was connected.");
|
|
119
|
-
return 1;
|
|
120
|
-
}
|
|
121
|
-
return 0;
|
|
122
|
-
}
|
package/src/proc.js
DELETED
|
@@ -1,36 +0,0 @@
|
|
|
1
|
-
// Small process helpers shared by the Claude and Codex flows. Node builtins only.
|
|
2
|
-
|
|
3
|
-
import fs from "node:fs";
|
|
4
|
-
import path from "node:path";
|
|
5
|
-
|
|
6
|
-
/**
|
|
7
|
-
* Is `name` an executable on PATH? A dependency-free `which`: scan PATH dirs for
|
|
8
|
-
* an executable file. Windows PATHEXT is honored for the .exe/.cmd case.
|
|
9
|
-
*/
|
|
10
|
-
export function onPath(name) {
|
|
11
|
-
const parts = (process.env.PATH || "").split(path.delimiter).filter(Boolean);
|
|
12
|
-
const exts = process.platform === "win32" ? (process.env.PATHEXT || ".EXE;.CMD;.BAT").split(";") : [""];
|
|
13
|
-
for (const dir of parts) {
|
|
14
|
-
for (const ext of exts) {
|
|
15
|
-
const candidate = path.join(dir, name + ext.toLowerCase());
|
|
16
|
-
const candidateUpper = path.join(dir, name + ext);
|
|
17
|
-
for (const c of new Set([candidate, candidateUpper])) {
|
|
18
|
-
try {
|
|
19
|
-
fs.accessSync(c, fs.constants.X_OK);
|
|
20
|
-
return true;
|
|
21
|
-
} catch {
|
|
22
|
-
/* keep scanning */
|
|
23
|
-
}
|
|
24
|
-
}
|
|
25
|
-
}
|
|
26
|
-
}
|
|
27
|
-
return false;
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
/**
|
|
31
|
-
* True on the BSD family (macOS included), where script(1) takes
|
|
32
|
-
* `script -q <file> <cmd...>`; util-linux takes `script -qec "<cmd>" <file>`.
|
|
33
|
-
*/
|
|
34
|
-
export function isBsdScript() {
|
|
35
|
-
return ["darwin", "freebsd", "openbsd", "netbsd"].includes(process.platform);
|
|
36
|
-
}
|
package/src/prompt.js
DELETED
|
@@ -1,43 +0,0 @@
|
|
|
1
|
-
// Terminal prompts: a default-No confirm and a no-echo paste. Both read one line
|
|
2
|
-
// from the TTY using node:readline. The paste path disables terminal echo with
|
|
3
|
-
// stty(1) so the token never reaches the scrollback — and never the clipboard,
|
|
4
|
-
// because it is typed/pasted straight into the process, not through a copy.
|
|
5
|
-
|
|
6
|
-
import readline from "node:readline";
|
|
7
|
-
import { spawnSync } from "node:child_process";
|
|
8
|
-
|
|
9
|
-
/** Ask a yes/no question; returns true ONLY for y/Y. Default (empty, EOF) is No. */
|
|
10
|
-
export function confirm(question) {
|
|
11
|
-
return new Promise((resolve) => {
|
|
12
|
-
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
13
|
-
rl.question(question, (answer) => {
|
|
14
|
-
rl.close();
|
|
15
|
-
resolve(/^y(es)?$/i.test(String(answer).trim()));
|
|
16
|
-
});
|
|
17
|
-
});
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
function setEcho(on) {
|
|
21
|
-
// Best effort; on a non-TTY (or Windows without stty) this is a no-op and the
|
|
22
|
-
// paste simply echoes, which is still correct — just not hidden.
|
|
23
|
-
try {
|
|
24
|
-
spawnSync("stty", [on ? "echo" : "-echo"], { stdio: "inherit" });
|
|
25
|
-
} catch {
|
|
26
|
-
/* ignore */
|
|
27
|
-
}
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
/** Read one line WITHOUT echoing it. Used for the Claude token paste fallback. */
|
|
31
|
-
export function readHidden(promptText) {
|
|
32
|
-
return new Promise((resolve) => {
|
|
33
|
-
process.stdout.write(promptText);
|
|
34
|
-
setEcho(false);
|
|
35
|
-
const rl = readline.createInterface({ input: process.stdin, output: process.stdout, terminal: false });
|
|
36
|
-
rl.question("", (answer) => {
|
|
37
|
-
rl.close();
|
|
38
|
-
setEcho(true);
|
|
39
|
-
process.stdout.write("\n");
|
|
40
|
-
resolve(String(answer));
|
|
41
|
-
});
|
|
42
|
-
});
|
|
43
|
-
}
|
package/src/tokens.js
DELETED
|
@@ -1,45 +0,0 @@
|
|
|
1
|
-
// Pure, dependency-free string helpers for the connect flow. Kept apart from the
|
|
2
|
-
// network/PTY code (api.js, claude.js, codex.js) so the parsing logic that turns
|
|
3
|
-
// terminal output and pasted values into credentials is unit-testable without a
|
|
4
|
-
// subprocess or a socket. Node builtins only; no state, no side effects.
|
|
5
|
-
|
|
6
|
-
// Matches the escape sequences `claude setup-token` draws its full-screen UI with:
|
|
7
|
-
// CSI (`ESC [ … final`), the two-char sequences (`ESC =`, `ESC >`), and OSC strings
|
|
8
|
-
// (`ESC ] … BEL` or `ESC ] … ST`). Broad on purpose — the token we extract afterward
|
|
9
|
-
// is validated by its own strict prefix regex, so over-stripping here is harmless.
|
|
10
|
-
const ANSI = new RegExp(
|
|
11
|
-
[
|
|
12
|
-
"[\\u001B\\u009B][[\\]()#;?]*(?:(?:(?:[a-zA-Z0-9]*;)*[a-zA-Z0-9]*)?\\u0007)", // OSC … BEL
|
|
13
|
-
"(?:[\\u001B\\u009B][[\\]()#;?]*(?:(?:\\d{1,4}(?:;\\d{0,4})*)?[0-9A-ORZcf-nqry=><~]))", // CSI/private
|
|
14
|
-
"[\\u001B\\u009B][=>]", // keypad mode toggles
|
|
15
|
-
].join("|"),
|
|
16
|
-
"g",
|
|
17
|
-
);
|
|
18
|
-
|
|
19
|
-
/** Strip ANSI/VT escape codes from terminal output. */
|
|
20
|
-
export function stripAnsi(text) {
|
|
21
|
-
return String(text).replace(ANSI, "");
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
// A Claude plan OAuth token minted by `claude setup-token`. Prefix `sk-ant-oat`
|
|
25
|
-
// then the version + payload; the char class is exactly the plan's spec.
|
|
26
|
-
const OAT = /sk-ant-oat[0-9a-zA-Z_-]+/;
|
|
27
|
-
|
|
28
|
-
/**
|
|
29
|
-
* Pull the first `sk-ant-oat…` token out of (already ANSI-stripped) text.
|
|
30
|
-
* Returns the token string, or null when none is present.
|
|
31
|
-
*/
|
|
32
|
-
export function extractOatToken(text) {
|
|
33
|
-
const match = OAT.exec(stripAnsi(text));
|
|
34
|
-
return match ? match[0] : null;
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
/**
|
|
38
|
-
* Remove ALL internal whitespace from a single pasted token. Terminals wrap the
|
|
39
|
-
* 108-char Claude token across lines, so a copy carries a newline (often plus a
|
|
40
|
-
* leading space); the provider then reports only "Invalid auth token". Callers
|
|
41
|
-
* paste one token, so collapsing every space/newline/tab is always safe here.
|
|
42
|
-
*/
|
|
43
|
-
export function stripInternalWhitespace(value) {
|
|
44
|
-
return String(value).replace(/\s+/g, "");
|
|
45
|
-
}
|