openalmanac 0.2.19 → 0.2.21
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/login-core.d.ts +4 -1
- package/dist/login-core.js +170 -16
- package/dist/setup.js +350 -39
- package/package.json +1 -1
package/dist/login-core.d.ts
CHANGED
|
@@ -9,4 +9,7 @@ export type LoginResult = {
|
|
|
9
9
|
* Core login flow shared by CLI and MCP tool.
|
|
10
10
|
* Checks for existing valid key, otherwise opens browser for auth.
|
|
11
11
|
*/
|
|
12
|
-
export declare function performLogin(
|
|
12
|
+
export declare function performLogin(options?: {
|
|
13
|
+
signal?: AbortSignal;
|
|
14
|
+
forceNew?: boolean;
|
|
15
|
+
}): Promise<LoginResult>;
|
package/dist/login-core.js
CHANGED
|
@@ -3,25 +3,174 @@ import { getApiKey, saveApiKey, API_BASE } from "./auth.js";
|
|
|
3
3
|
import { openBrowser } from "./browser.js";
|
|
4
4
|
const CONNECT_URL_BASE = "https://openalmanac.org/contribute/connect";
|
|
5
5
|
const LOGIN_TIMEOUT_MS = 120_000;
|
|
6
|
+
function callbackPage(success) {
|
|
7
|
+
// These values are hardcoded — never interpolate user input here.
|
|
8
|
+
const title = success ? "You\u2019re connected" : "Something went wrong";
|
|
9
|
+
const messageLine1 = success
|
|
10
|
+
? "Your agent is registered and ready to go."
|
|
11
|
+
: "Invalid token. Please return to your terminal and try again.";
|
|
12
|
+
const messageLine2 = success
|
|
13
|
+
? "You can close this tab and return to your terminal."
|
|
14
|
+
: "";
|
|
15
|
+
const statusIcon = success
|
|
16
|
+
? `<svg width="20" height="20" fill="none" viewBox="0 0 24 24" style="position:absolute;bottom:-2px;right:-2px;background:#fff;border-radius:50%;padding:2px"><circle cx="12" cy="12" r="10" fill="#437a50"/><path stroke="#fff" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round" d="M8 12l3 3 5-5"/></svg>`
|
|
17
|
+
: `<svg width="20" height="20" fill="none" viewBox="0 0 24 24" style="position:absolute;bottom:-2px;right:-2px;background:#fff;border-radius:50%;padding:2px"><circle cx="12" cy="12" r="10" fill="#bb4444"/><path stroke="#fff" stroke-width="2" stroke-linecap="round" d="M9 9l6 6M15 9l-6 6"/></svg>`;
|
|
18
|
+
return `<!DOCTYPE html>
|
|
19
|
+
<html lang="en">
|
|
20
|
+
<head>
|
|
21
|
+
<meta charset="utf-8">
|
|
22
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
23
|
+
<title>${title} \u2014 Almanac</title>
|
|
24
|
+
<link rel="icon" href="https://openalmanac.org/icon-32.png" sizes="32x32" type="image/png">
|
|
25
|
+
<style>
|
|
26
|
+
@import url('https://fonts.googleapis.com/css2?family=Source+Serif+4:wght@400;600&display=swap');
|
|
27
|
+
* { margin: 0; padding: 0; box-sizing: border-box; }
|
|
28
|
+
body {
|
|
29
|
+
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
|
|
30
|
+
background: #fcfbfa;
|
|
31
|
+
color: #1c1a19;
|
|
32
|
+
display: flex;
|
|
33
|
+
align-items: center;
|
|
34
|
+
justify-content: center;
|
|
35
|
+
min-height: 100vh;
|
|
36
|
+
}
|
|
37
|
+
.card {
|
|
38
|
+
display: flex;
|
|
39
|
+
flex-direction: column;
|
|
40
|
+
align-items: center;
|
|
41
|
+
text-align: center;
|
|
42
|
+
max-width: 420px;
|
|
43
|
+
padding: 48px 44px;
|
|
44
|
+
background: #fff;
|
|
45
|
+
border: 1px solid #e6e4df;
|
|
46
|
+
border-radius: 16px;
|
|
47
|
+
box-shadow: 0 1px 3px rgba(0,0,0,0.04);
|
|
48
|
+
}
|
|
49
|
+
.logo-wrap {
|
|
50
|
+
position: relative;
|
|
51
|
+
margin-bottom: 24px;
|
|
52
|
+
}
|
|
53
|
+
.logo-wrap img {
|
|
54
|
+
width: 56px;
|
|
55
|
+
height: 56px;
|
|
56
|
+
border-radius: 12px;
|
|
57
|
+
}
|
|
58
|
+
h1 {
|
|
59
|
+
font-family: 'Source Serif 4', Georgia, serif;
|
|
60
|
+
font-size: 24px;
|
|
61
|
+
font-weight: 600;
|
|
62
|
+
margin-bottom: 8px;
|
|
63
|
+
color: #1c1a19;
|
|
64
|
+
}
|
|
65
|
+
p {
|
|
66
|
+
font-size: 15px;
|
|
67
|
+
color: #5c5855;
|
|
68
|
+
line-height: 1.6;
|
|
69
|
+
}
|
|
70
|
+
.divider {
|
|
71
|
+
width: 40px;
|
|
72
|
+
height: 1px;
|
|
73
|
+
background: #e6e4df;
|
|
74
|
+
margin: 24px 0;
|
|
75
|
+
}
|
|
76
|
+
.terminal {
|
|
77
|
+
width: 100%;
|
|
78
|
+
background: #1c1a19;
|
|
79
|
+
border-radius: 10px;
|
|
80
|
+
padding: 16px 18px;
|
|
81
|
+
text-align: left;
|
|
82
|
+
font-family: 'SF Mono', 'Menlo', 'Monaco', 'Consolas', monospace;
|
|
83
|
+
font-size: 13px;
|
|
84
|
+
line-height: 1.7;
|
|
85
|
+
color: #a8a4a0;
|
|
86
|
+
overflow: hidden;
|
|
87
|
+
}
|
|
88
|
+
.terminal .prompt { color: #5c5855; }
|
|
89
|
+
.terminal .cmd { color: #e8e6e3; }
|
|
90
|
+
.terminal .cursor {
|
|
91
|
+
display: inline-block;
|
|
92
|
+
width: 7px;
|
|
93
|
+
height: 15px;
|
|
94
|
+
background: #e8e6e3;
|
|
95
|
+
vertical-align: text-bottom;
|
|
96
|
+
animation: blink 1s step-end infinite;
|
|
97
|
+
}
|
|
98
|
+
@keyframes blink {
|
|
99
|
+
50% { opacity: 0; }
|
|
100
|
+
}
|
|
101
|
+
.line { min-height: 22px; }
|
|
102
|
+
</style>
|
|
103
|
+
</head>
|
|
104
|
+
<body>
|
|
105
|
+
<div class="card">
|
|
106
|
+
<div class="logo-wrap">
|
|
107
|
+
<img src="https://openalmanac.org/icon-192.png" alt="Almanac">
|
|
108
|
+
${statusIcon}
|
|
109
|
+
</div>
|
|
110
|
+
<h1>${title}</h1>
|
|
111
|
+
<p>${messageLine1}${messageLine2 ? `<br>${messageLine2}` : ""}</p>${success ? `
|
|
112
|
+
<div class="divider"></div>
|
|
113
|
+
<div class="terminal">
|
|
114
|
+
<div class="line" id="line1"></div>
|
|
115
|
+
<div class="line" id="line2" style="display:none"></div>
|
|
116
|
+
</div>
|
|
117
|
+
<script>
|
|
118
|
+
const steps = [
|
|
119
|
+
{ target: 'line1', prefix: '<span class="prompt">$ </span>', text: 'claude', delay: 600 },
|
|
120
|
+
{ target: 'line2', prefix: '<span class="prompt">> </span>', text: 'Use almanac tools to write an article on black holes', delay: 400 },
|
|
121
|
+
];
|
|
122
|
+
function type(el, prefix, text, speed, cb) {
|
|
123
|
+
el.style.display = '';
|
|
124
|
+
el.innerHTML = prefix + '<span class="cmd"></span><span class="cursor"></span>';
|
|
125
|
+
const cmd = el.querySelector('.cmd');
|
|
126
|
+
let i = 0;
|
|
127
|
+
function next() {
|
|
128
|
+
if (i < text.length) {
|
|
129
|
+
cmd.textContent += text[i++];
|
|
130
|
+
setTimeout(next, speed + Math.random() * 40);
|
|
131
|
+
} else if (cb) {
|
|
132
|
+
setTimeout(cb, 600);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
setTimeout(next, speed);
|
|
136
|
+
}
|
|
137
|
+
function run(idx) {
|
|
138
|
+
if (idx >= steps.length) return;
|
|
139
|
+
const s = steps[idx];
|
|
140
|
+
const prev = idx > 0 ? document.getElementById(steps[idx-1].target) : null;
|
|
141
|
+
if (prev) { const c = prev.querySelector('.cursor'); if (c) c.remove(); }
|
|
142
|
+
setTimeout(function() {
|
|
143
|
+
type(document.getElementById(s.target), s.prefix, s.text, 60, function() { run(idx+1); });
|
|
144
|
+
}, s.delay);
|
|
145
|
+
}
|
|
146
|
+
run(0);
|
|
147
|
+
</script>` : ""}
|
|
148
|
+
</div>
|
|
149
|
+
</body>
|
|
150
|
+
</html>`;
|
|
151
|
+
}
|
|
6
152
|
/**
|
|
7
153
|
* Core login flow shared by CLI and MCP tool.
|
|
8
154
|
* Checks for existing valid key, otherwise opens browser for auth.
|
|
9
155
|
*/
|
|
10
|
-
export async function performLogin() {
|
|
11
|
-
const
|
|
12
|
-
if (
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
156
|
+
export async function performLogin(options) {
|
|
157
|
+
const { signal, forceNew } = options ?? {};
|
|
158
|
+
if (!forceNew) {
|
|
159
|
+
const existingKey = getApiKey();
|
|
160
|
+
if (existingKey) {
|
|
161
|
+
try {
|
|
162
|
+
const resp = await fetch(`${API_BASE}/api/agents/me`, {
|
|
163
|
+
headers: { Authorization: `Bearer ${existingKey}` },
|
|
164
|
+
signal: AbortSignal.timeout(10_000),
|
|
165
|
+
});
|
|
166
|
+
if (resp.ok) {
|
|
167
|
+
const data = (await resp.json());
|
|
168
|
+
return { status: "already_logged_in", name: data.name ?? "unknown" };
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
catch {
|
|
172
|
+
// Key invalid or network error, continue to login
|
|
21
173
|
}
|
|
22
|
-
}
|
|
23
|
-
catch {
|
|
24
|
-
// Key invalid or network error, continue to login
|
|
25
174
|
}
|
|
26
175
|
}
|
|
27
176
|
const { token, browserOpened } = await new Promise((resolve, reject) => {
|
|
@@ -36,14 +185,19 @@ export async function performLogin() {
|
|
|
36
185
|
const callbackToken = url.searchParams.get("token");
|
|
37
186
|
if (!callbackToken || !callbackToken.startsWith("oa_")) {
|
|
38
187
|
res.writeHead(400, { "Content-Type": "text/html" });
|
|
39
|
-
res.end(
|
|
188
|
+
res.end(callbackPage(false));
|
|
40
189
|
return;
|
|
41
190
|
}
|
|
42
191
|
res.writeHead(200, { "Content-Type": "text/html" });
|
|
43
|
-
res.end(
|
|
192
|
+
res.end(callbackPage(true));
|
|
44
193
|
resolve({ token: callbackToken, browserOpened: opened });
|
|
45
194
|
httpServer.close();
|
|
46
195
|
});
|
|
196
|
+
// Allow caller to abort (closes the server and rejects)
|
|
197
|
+
signal?.addEventListener("abort", () => {
|
|
198
|
+
httpServer.close();
|
|
199
|
+
reject(new Error("Login cancelled"));
|
|
200
|
+
}, { once: true });
|
|
47
201
|
httpServer.listen(0, "127.0.0.1", () => {
|
|
48
202
|
const addr = httpServer.address();
|
|
49
203
|
if (!addr || typeof addr === "string") {
|
package/dist/setup.js
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { readFileSync, writeFileSync, mkdirSync, existsSync } from "fs";
|
|
2
2
|
import { homedir } from "os";
|
|
3
3
|
import { join } from "path";
|
|
4
|
+
import { performLogin } from "./login-core.js";
|
|
5
|
+
import { getAuthStatus } from "./auth.js";
|
|
4
6
|
const TOOL_GROUPS = [
|
|
5
7
|
{
|
|
6
8
|
name: "Search & Read",
|
|
@@ -66,16 +68,62 @@ const TOOL_GROUPS = [
|
|
|
66
68
|
tools: ["WebSearch", "WebFetch"],
|
|
67
69
|
},
|
|
68
70
|
];
|
|
71
|
+
const AGENTS = [
|
|
72
|
+
{ name: "Claude Code", supported: true },
|
|
73
|
+
{ name: "Codex", supported: false },
|
|
74
|
+
{ name: "Cursor", supported: false },
|
|
75
|
+
{ name: "Windsurf", supported: false },
|
|
76
|
+
];
|
|
69
77
|
/* ── ANSI helpers ───────────────────────────────────────────────── */
|
|
70
78
|
const RST = "\x1b[0m";
|
|
71
79
|
const BOLD = "\x1b[1m";
|
|
72
80
|
const DIM = "\x1b[2m";
|
|
73
|
-
const GREEN = "\x1b[32m";
|
|
74
|
-
const CYAN = "\x1b[36m";
|
|
75
|
-
const RED = "\x1b[31m";
|
|
76
|
-
const YELLOW = "\x1b[33m";
|
|
77
|
-
const MAGENTA = "\x1b[35m";
|
|
78
81
|
const WHITE_BOLD = "\x1b[1;37m";
|
|
82
|
+
const BLUE = "\x1b[38;5;75m"; // blue for accents
|
|
83
|
+
const BLUE_DIM = "\x1b[38;5;69m"; // slightly deeper blue for boxes
|
|
84
|
+
// Interactive TUI accent
|
|
85
|
+
const ACCENT = "\x1b[38;5;252m"; // silver
|
|
86
|
+
const ACCENT_BG = "\x1b[48;5;252m\x1b[38;5;16m"; // badge: silver bg, black text
|
|
87
|
+
// Banner gradient: white → silver
|
|
88
|
+
const GRADIENT = [
|
|
89
|
+
"\x1b[38;5;255m",
|
|
90
|
+
"\x1b[38;5;253m",
|
|
91
|
+
"\x1b[38;5;251m",
|
|
92
|
+
"\x1b[38;5;249m",
|
|
93
|
+
"\x1b[38;5;246m",
|
|
94
|
+
"\x1b[38;5;243m",
|
|
95
|
+
];
|
|
96
|
+
/* ── ASCII banner ───────────────────────────────────────────────── */
|
|
97
|
+
const LOGO_LINES = [
|
|
98
|
+
" \u2588\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2557 \u2588\u2588\u2588\u2557 \u2588\u2588\u2588\u2557 \u2588\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2588\u2557 \u2588\u2588\u2557 \u2588\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2588\u2588\u2588\u2588\u2557",
|
|
99
|
+
"\u2588\u2588\u2554\u2550\u2550\u2588\u2588\u2557\u2588\u2588\u2551 \u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2588\u2588\u2551\u2588\u2588\u2554\u2550\u2550\u2588\u2588\u2557\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2551\u2588\u2588\u2554\u2550\u2550\u2588\u2588\u2557\u2588\u2588\u2554\u2550\u2550\u2550\u2550\u255d",
|
|
100
|
+
"\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2551\u2588\u2588\u2551 \u2588\u2588\u2554\u2588\u2588\u2588\u2588\u2554\u2588\u2588\u2551\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2551\u2588\u2588\u2554\u2588\u2588\u2557 \u2588\u2588\u2551\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2551\u2588\u2588\u2551 ",
|
|
101
|
+
"\u2588\u2588\u2554\u2550\u2550\u2588\u2588\u2551\u2588\u2588\u2551 \u2588\u2588\u2551\u255a\u2588\u2588\u2554\u255d\u2588\u2588\u2551\u2588\u2588\u2554\u2550\u2550\u2588\u2588\u2551\u2588\u2588\u2551\u255a\u2588\u2588\u2557\u2588\u2588\u2551\u2588\u2588\u2554\u2550\u2550\u2588\u2588\u2551\u2588\u2588\u2551 ",
|
|
102
|
+
"\u2588\u2588\u2551 \u2588\u2588\u2551\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2557\u2588\u2588\u2551 \u255a\u2550\u255d \u2588\u2588\u2551\u2588\u2588\u2551 \u2588\u2588\u2551\u2588\u2588\u2551 \u255a\u2588\u2588\u2588\u2588\u2551\u2588\u2588\u2551 \u2588\u2588\u2551\u255a\u2588\u2588\u2588\u2588\u2588\u2588\u2557",
|
|
103
|
+
"\u255a\u2550\u255d \u255a\u2550\u255d\u255a\u2550\u2550\u2550\u2550\u2550\u2550\u255d\u255a\u2550\u255d \u255a\u2550\u255d\u255a\u2550\u255d \u255a\u2550\u255d\u255a\u2550\u255d \u255a\u2550\u2550\u2550\u255d\u255a\u2550\u255d \u255a\u2550\u255d \u255a\u2550\u2550\u2550\u2550\u2550\u255d",
|
|
104
|
+
];
|
|
105
|
+
function printBanner() {
|
|
106
|
+
process.stdout.write("\n");
|
|
107
|
+
for (let i = 0; i < LOGO_LINES.length; i++) {
|
|
108
|
+
process.stdout.write(`${GRADIENT[i]}${LOGO_LINES[i]}${RST}\n`);
|
|
109
|
+
}
|
|
110
|
+
process.stdout.write(`\n${DIM} Write and publish articles with your AI agent${RST}\n`);
|
|
111
|
+
}
|
|
112
|
+
function printBadge() {
|
|
113
|
+
process.stdout.write(`\n ${ACCENT_BG} almanac ${RST}\n`);
|
|
114
|
+
}
|
|
115
|
+
/* ── Step indicators ────────────────────────────────────────────── */
|
|
116
|
+
const BAR = ` ${DIM}\u2502${RST}`;
|
|
117
|
+
function stepDone(msg) {
|
|
118
|
+
process.stdout.write(` ${BLUE}\u25c7${RST} ${msg}\n`);
|
|
119
|
+
}
|
|
120
|
+
function stepActive(msg) {
|
|
121
|
+
process.stdout.write(` ${BLUE}\u25c6${RST} ${msg}\n`);
|
|
122
|
+
}
|
|
123
|
+
/* ── Helpers ────────────────────────────────────────────────────── */
|
|
124
|
+
// Strip ANSI codes to measure visible length
|
|
125
|
+
const vis = (s) => s.replace(/\x1b\[[0-9;]*m/g, "").length;
|
|
126
|
+
const w = (s) => process.stdout.write(s + "\n");
|
|
79
127
|
/* ── File helpers ───────────────────────────────────────────────── */
|
|
80
128
|
const CLAUDE_DIR = join(homedir(), ".claude");
|
|
81
129
|
const CLAUDE_JSON = join(homedir(), ".claude.json");
|
|
@@ -131,37 +179,261 @@ function configurePermissions(tools) {
|
|
|
131
179
|
writeJson(SETTINGS_JSON, settings);
|
|
132
180
|
return tools.length;
|
|
133
181
|
}
|
|
134
|
-
/* ──
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
182
|
+
/* ── Agent selection screen ─────────────────────────────────────── */
|
|
183
|
+
function renderAgentSelect(_cursor) {
|
|
184
|
+
process.stdout.write("\x1b[2J\x1b[H");
|
|
185
|
+
printBanner();
|
|
186
|
+
printBadge();
|
|
139
187
|
w("");
|
|
140
|
-
|
|
141
|
-
w(
|
|
188
|
+
stepActive(`Select your agent`);
|
|
189
|
+
w(BAR);
|
|
190
|
+
for (const agent of AGENTS) {
|
|
191
|
+
if (agent.supported) {
|
|
192
|
+
w(` ${DIM}\u2502${RST} ${BLUE}\u276f${RST} ${BLUE}\u25cf${RST} ${BOLD}${agent.name}${RST}`);
|
|
193
|
+
}
|
|
194
|
+
else {
|
|
195
|
+
w(` ${DIM}\u2502${RST} ${DIM}\u25cb ${agent.name}${" "}coming soon${RST}`);
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
w(BAR);
|
|
199
|
+
w(` ${DIM}\u2502${RST} ${BLUE}${BOLD}[enter]${RST} confirm ${DIM}[q] quit${RST}`);
|
|
142
200
|
w("");
|
|
143
|
-
|
|
201
|
+
}
|
|
202
|
+
function runAgentSelect() {
|
|
203
|
+
return new Promise((resolve) => {
|
|
204
|
+
renderAgentSelect(0);
|
|
205
|
+
process.stdin.setRawMode(true);
|
|
206
|
+
process.stdin.resume();
|
|
207
|
+
process.stdin.setEncoding("utf-8");
|
|
208
|
+
const cleanup = () => {
|
|
209
|
+
process.stdin.removeListener("data", onData);
|
|
210
|
+
process.stdin.setRawMode(false);
|
|
211
|
+
process.stdin.pause();
|
|
212
|
+
};
|
|
213
|
+
const onData = (key) => {
|
|
214
|
+
if (key === "\x03" || key === "q") {
|
|
215
|
+
cleanup();
|
|
216
|
+
process.stdout.write("\x1b[2J\x1b[H");
|
|
217
|
+
console.log("\n Setup cancelled.\n");
|
|
218
|
+
process.exit(0);
|
|
219
|
+
}
|
|
220
|
+
if (key === "\r" || key === "\n") {
|
|
221
|
+
cleanup();
|
|
222
|
+
const supported = AGENTS.find((a) => a.supported);
|
|
223
|
+
resolve(supported.name);
|
|
224
|
+
return;
|
|
225
|
+
}
|
|
226
|
+
};
|
|
227
|
+
process.stdin.on("data", onData);
|
|
228
|
+
});
|
|
229
|
+
}
|
|
230
|
+
/* ── Login step ─────────────────────────────────────────────────── */
|
|
231
|
+
function loginLabel(result) {
|
|
232
|
+
if (result.status === "already")
|
|
233
|
+
return `Logged in as ${WHITE_BOLD}${result.name}${RST}`;
|
|
234
|
+
if (result.status === "done")
|
|
235
|
+
return `Logged in`;
|
|
236
|
+
return `Login ${DIM}skipped${RST}`;
|
|
237
|
+
}
|
|
238
|
+
function waitForKey(prompt) {
|
|
239
|
+
return new Promise((resolve) => {
|
|
240
|
+
process.stdin.setRawMode(true);
|
|
241
|
+
process.stdin.resume();
|
|
242
|
+
process.stdin.setEncoding("utf-8");
|
|
243
|
+
w(prompt);
|
|
244
|
+
const onData = (key) => {
|
|
245
|
+
process.stdin.removeListener("data", onData);
|
|
246
|
+
process.stdin.setRawMode(false);
|
|
247
|
+
process.stdin.pause();
|
|
248
|
+
if (key === "\x03") {
|
|
249
|
+
process.stdout.write("\x1b[2J\x1b[H");
|
|
250
|
+
console.log("\n Setup cancelled.\n");
|
|
251
|
+
process.exit(0);
|
|
252
|
+
}
|
|
253
|
+
resolve(key);
|
|
254
|
+
};
|
|
255
|
+
process.stdin.on("data", onData);
|
|
256
|
+
});
|
|
257
|
+
}
|
|
258
|
+
async function runLoginStep(agent, mcpChanged, toolCount) {
|
|
259
|
+
const priorSteps = () => {
|
|
260
|
+
stepDone(`Agent \u2192 ${WHITE_BOLD}${agent}${RST}`);
|
|
261
|
+
w(BAR);
|
|
262
|
+
stepDone(`MCP server ${mcpChanged ? `added to ${DIM}~/.claude.json${RST}` : `${DIM}already configured${RST}`}`);
|
|
263
|
+
w(BAR);
|
|
264
|
+
stepDone(`${BLUE}${toolCount}${RST} tool${toolCount !== 1 ? "s" : ""} allowed`);
|
|
265
|
+
w(BAR);
|
|
266
|
+
};
|
|
267
|
+
function renderLoginChoice(name, cursor) {
|
|
268
|
+
process.stdout.write("\x1b[2J\x1b[H");
|
|
269
|
+
printBanner();
|
|
270
|
+
printBadge();
|
|
271
|
+
w("");
|
|
272
|
+
priorSteps();
|
|
273
|
+
stepActive(`Already logged in as ${WHITE_BOLD}${name}${RST}`);
|
|
274
|
+
w(BAR);
|
|
275
|
+
const options = [
|
|
276
|
+
`Continue as ${BOLD}${name}${RST}`,
|
|
277
|
+
`Login with a different account`,
|
|
278
|
+
];
|
|
279
|
+
for (let i = 0; i < options.length; i++) {
|
|
280
|
+
const arrow = i === cursor ? `${BLUE}\u276f${RST}` : " ";
|
|
281
|
+
const label = i === cursor ? options[i] : `${DIM}${options[i]}${RST}`;
|
|
282
|
+
w(` ${DIM}\u2502${RST} ${arrow} ${label}`);
|
|
283
|
+
}
|
|
284
|
+
w(BAR);
|
|
285
|
+
w(` ${DIM}\u2502${RST} ${BLUE}${BOLD}[\u2191\u2193]${RST} move ${BLUE}${BOLD}[enter]${RST} confirm`);
|
|
286
|
+
w("");
|
|
287
|
+
}
|
|
288
|
+
function runLoginChoice(name) {
|
|
289
|
+
return new Promise((resolve) => {
|
|
290
|
+
let cursor = 0;
|
|
291
|
+
renderLoginChoice(name, cursor);
|
|
292
|
+
process.stdin.setRawMode(true);
|
|
293
|
+
process.stdin.resume();
|
|
294
|
+
process.stdin.setEncoding("utf-8");
|
|
295
|
+
const cleanup = () => {
|
|
296
|
+
process.stdin.removeListener("data", onData);
|
|
297
|
+
process.stdin.setRawMode(false);
|
|
298
|
+
process.stdin.pause();
|
|
299
|
+
};
|
|
300
|
+
const onData = (key) => {
|
|
301
|
+
if (key === "\x03" || key === "q") {
|
|
302
|
+
cleanup();
|
|
303
|
+
process.stdout.write("\x1b[2J\x1b[H");
|
|
304
|
+
console.log("\n Setup cancelled.\n");
|
|
305
|
+
process.exit(0);
|
|
306
|
+
}
|
|
307
|
+
if (key === "\x1b[A" || key === "k")
|
|
308
|
+
cursor = cursor === 0 ? 1 : 0;
|
|
309
|
+
else if (key === "\x1b[B" || key === "j")
|
|
310
|
+
cursor = cursor === 0 ? 1 : 0;
|
|
311
|
+
else if (key === "\r" || key === "\n") {
|
|
312
|
+
cleanup();
|
|
313
|
+
resolve(cursor === 0); // 0 = keep, 1 = new account
|
|
314
|
+
return;
|
|
315
|
+
}
|
|
316
|
+
renderLoginChoice(name, cursor);
|
|
317
|
+
};
|
|
318
|
+
process.stdin.on("data", onData);
|
|
319
|
+
});
|
|
320
|
+
}
|
|
321
|
+
// Check if already logged in
|
|
322
|
+
let forceNew = false;
|
|
323
|
+
const auth = await getAuthStatus();
|
|
324
|
+
if (auth.loggedIn) {
|
|
325
|
+
const keepAccount = await runLoginChoice(auth.name);
|
|
326
|
+
if (keepAccount) {
|
|
327
|
+
return { status: "already", name: auth.name };
|
|
328
|
+
}
|
|
329
|
+
forceNew = true;
|
|
330
|
+
}
|
|
331
|
+
// Show prompt before opening browser
|
|
332
|
+
process.stdout.write("\x1b[2J\x1b[H");
|
|
333
|
+
printBanner();
|
|
334
|
+
printBadge();
|
|
144
335
|
w("");
|
|
145
|
-
|
|
146
|
-
|
|
336
|
+
priorSteps();
|
|
337
|
+
stepActive(`Login to Almanac`);
|
|
338
|
+
w(BAR);
|
|
339
|
+
w(` ${DIM}\u2502${RST} This will open ${WHITE_BOLD}almanac${RST} in your browser`);
|
|
340
|
+
w(` ${DIM}\u2502${RST} to connect your account.`);
|
|
341
|
+
w(BAR);
|
|
342
|
+
await waitForKey(` ${DIM}\u2502${RST} ${BLUE}${BOLD}[enter]${RST} continue`);
|
|
343
|
+
// Show waiting state with cancel/retry hint
|
|
344
|
+
const renderWaiting = () => {
|
|
345
|
+
process.stdout.write("\x1b[2J\x1b[H");
|
|
346
|
+
printBanner();
|
|
347
|
+
printBadge();
|
|
348
|
+
w("");
|
|
349
|
+
priorSteps();
|
|
350
|
+
stepActive(`Waiting for login\u2026 ${DIM}complete in browser${RST}`);
|
|
351
|
+
w(BAR);
|
|
352
|
+
w(` ${DIM}\u2502${RST} ${BLUE}${BOLD}[r]${RST} retry ${DIM}[q] cancel setup${RST}`);
|
|
353
|
+
w("");
|
|
354
|
+
};
|
|
355
|
+
// Loop: attempt login, let user retry if it fails/times out
|
|
356
|
+
// eslint-disable-next-line no-constant-condition
|
|
357
|
+
while (true) {
|
|
358
|
+
renderWaiting();
|
|
359
|
+
// AbortController so we can kill the HTTP server on retry/cancel
|
|
360
|
+
const controller = new AbortController();
|
|
361
|
+
// Race login against keypress
|
|
362
|
+
const loginPromise = performLogin({ signal: controller.signal, forceNew }).then((result) => result.status === "already_logged_in"
|
|
363
|
+
? { status: "already", name: result.name }
|
|
364
|
+
: { status: "done" }, () => ({ status: "skipped" }));
|
|
365
|
+
let keyOnData = null;
|
|
366
|
+
const keyPromise = new Promise((resolve) => {
|
|
367
|
+
process.stdin.setRawMode(true);
|
|
368
|
+
process.stdin.resume();
|
|
369
|
+
process.stdin.setEncoding("utf-8");
|
|
370
|
+
keyOnData = (key) => {
|
|
371
|
+
process.stdin.removeListener("data", keyOnData);
|
|
372
|
+
process.stdin.setRawMode(false);
|
|
373
|
+
process.stdin.pause();
|
|
374
|
+
resolve(key);
|
|
375
|
+
};
|
|
376
|
+
process.stdin.on("data", keyOnData);
|
|
377
|
+
});
|
|
378
|
+
const result = await Promise.race([
|
|
379
|
+
loginPromise.then((r) => ({ type: "login", result: r })),
|
|
380
|
+
keyPromise.then((k) => ({ type: "key", key: k })),
|
|
381
|
+
]);
|
|
382
|
+
// Clean up stdin listener if login won
|
|
383
|
+
if (result.type === "login") {
|
|
384
|
+
if (keyOnData)
|
|
385
|
+
process.stdin.removeListener("data", keyOnData);
|
|
386
|
+
try {
|
|
387
|
+
process.stdin.setRawMode(false);
|
|
388
|
+
}
|
|
389
|
+
catch { /* already off */ }
|
|
390
|
+
process.stdin.pause();
|
|
391
|
+
return result.result;
|
|
392
|
+
}
|
|
393
|
+
// Key won — abort the login HTTP server
|
|
394
|
+
controller.abort();
|
|
395
|
+
// Handle keypress
|
|
396
|
+
if (result.key === "\x03" || result.key === "q") {
|
|
397
|
+
process.stdout.write("\x1b[2J\x1b[H");
|
|
398
|
+
console.log("\n Setup cancelled.\n");
|
|
399
|
+
process.exit(0);
|
|
400
|
+
}
|
|
401
|
+
if (result.key === "r") {
|
|
402
|
+
// Retry — loop continues
|
|
403
|
+
continue;
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
/* ── Tool permissions TUI ───────────────────────────────────────── */
|
|
408
|
+
const MAX_NAME = Math.max(...TOOL_GROUPS.map((g) => g.name.length));
|
|
409
|
+
function renderToolSelect(selected, cursor, agent, mcpChanged) {
|
|
410
|
+
process.stdout.write("\x1b[2J\x1b[H");
|
|
411
|
+
printBanner();
|
|
412
|
+
printBadge();
|
|
147
413
|
w("");
|
|
414
|
+
stepDone(`Agent \u2192 ${WHITE_BOLD}${agent}${RST}`);
|
|
415
|
+
w(BAR);
|
|
416
|
+
stepDone(`MCP server ${mcpChanged ? `added to ${DIM}~/.claude.json${RST}` : `${DIM}already configured${RST}`}`);
|
|
417
|
+
w(BAR);
|
|
418
|
+
stepActive(`Select tool permissions ${DIM}deselect any you'd rather approve manually${RST}`);
|
|
419
|
+
w(BAR);
|
|
148
420
|
for (let i = 0; i < TOOL_GROUPS.length; i++) {
|
|
149
|
-
const arrow = i === cursor ? `${
|
|
150
|
-
const check = selected[i] ? `${
|
|
421
|
+
const arrow = i === cursor ? `${BLUE}\u276f${RST}` : " ";
|
|
422
|
+
const check = selected[i] ? `${BLUE}\u2713${RST}` : " ";
|
|
151
423
|
const pad = TOOL_GROUPS[i].name.padEnd(MAX_NAME + 2);
|
|
152
424
|
const name = i === cursor ? `${BOLD}${pad}${RST}` : pad;
|
|
153
425
|
const desc = `${DIM}${TOOL_GROUPS[i].description}${RST}`;
|
|
154
|
-
w(` ${arrow} [${check}] ${name} ${desc}`);
|
|
426
|
+
w(` ${DIM}\u2502${RST} ${arrow} [${check}] ${name} ${desc}`);
|
|
155
427
|
}
|
|
156
|
-
w(
|
|
157
|
-
w(` ${
|
|
428
|
+
w(BAR);
|
|
429
|
+
w(` ${DIM}\u2502${RST} ${BLUE}${BOLD}[space]${RST} toggle ${BLUE}${BOLD}[\u2191\u2193]${RST} move ${BLUE}${BOLD}[a]${RST} all ${BLUE}${BOLD}[enter]${RST} confirm ${DIM}[q] quit${RST}`);
|
|
158
430
|
w("");
|
|
159
431
|
}
|
|
160
|
-
function
|
|
432
|
+
function runToolSelect(agent, mcpChanged) {
|
|
161
433
|
return new Promise((resolve) => {
|
|
162
|
-
const selected = TOOL_GROUPS.map(() => true);
|
|
434
|
+
const selected = TOOL_GROUPS.map(() => true);
|
|
163
435
|
let cursor = 0;
|
|
164
|
-
|
|
436
|
+
renderToolSelect(selected, cursor, agent, mcpChanged);
|
|
165
437
|
process.stdin.setRawMode(true);
|
|
166
438
|
process.stdin.resume();
|
|
167
439
|
process.stdin.setEncoding("utf-8");
|
|
@@ -171,7 +443,6 @@ function runTui(mcpLine) {
|
|
|
171
443
|
process.stdin.pause();
|
|
172
444
|
};
|
|
173
445
|
const onData = (key) => {
|
|
174
|
-
// Ctrl-C / q → cancel
|
|
175
446
|
if (key === "\x03" || key === "q") {
|
|
176
447
|
cleanup();
|
|
177
448
|
process.stdout.write("\x1b[2J\x1b[H");
|
|
@@ -198,38 +469,78 @@ function runTui(mcpLine) {
|
|
|
198
469
|
resolve(tools);
|
|
199
470
|
return;
|
|
200
471
|
}
|
|
201
|
-
|
|
472
|
+
renderToolSelect(selected, cursor, agent, mcpChanged);
|
|
202
473
|
};
|
|
203
474
|
process.stdin.on("data", onData);
|
|
204
475
|
});
|
|
205
476
|
}
|
|
206
477
|
/* ── Result screen ──────────────────────────────────────────────── */
|
|
207
|
-
function printResult(mcpChanged, toolCount) {
|
|
478
|
+
function printResult(agent, loginResult, mcpChanged, toolCount) {
|
|
208
479
|
process.stdout.write("\x1b[2J\x1b[H");
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
480
|
+
printBanner();
|
|
481
|
+
printBadge();
|
|
482
|
+
w("");
|
|
483
|
+
stepDone(`Agent \u2192 ${WHITE_BOLD}${agent}${RST}`);
|
|
484
|
+
w(BAR);
|
|
485
|
+
stepDone(`MCP server ${mcpChanged ? `added to ${DIM}~/.claude.json${RST}` : `${DIM}already configured${RST}`}`);
|
|
486
|
+
w(BAR);
|
|
487
|
+
stepDone(`${BLUE}${toolCount}${RST} tool${toolCount !== 1 ? "s" : ""} allowed in ${DIM}~/.claude/settings.json${RST}`);
|
|
488
|
+
w(BAR);
|
|
489
|
+
stepDone(loginLabel(loginResult));
|
|
490
|
+
w(BAR);
|
|
491
|
+
stepDone(`${BLUE}Setup complete${RST}`);
|
|
492
|
+
w("");
|
|
493
|
+
// Next steps box
|
|
494
|
+
const innerW = 52;
|
|
495
|
+
const row = (content) => {
|
|
496
|
+
const padding = Math.max(0, innerW - vis(content));
|
|
497
|
+
return ` ${BLUE_DIM}\u2502${RST}${content}${" ".repeat(padding)}${BLUE_DIM}\u2502${RST}`;
|
|
498
|
+
};
|
|
499
|
+
const empty = row("");
|
|
500
|
+
w(` ${BLUE_DIM}\u256d${"─".repeat(innerW)}\u256e${RST}`);
|
|
501
|
+
w(empty);
|
|
502
|
+
w(row(` ${WHITE_BOLD}Next steps${RST}`));
|
|
503
|
+
w(empty);
|
|
504
|
+
w(row(` ${BLUE}1.${RST} Type ${WHITE_BOLD}claude${RST} to start Claude Code`));
|
|
505
|
+
w(row(` ${BLUE}2.${RST} Say ${BLUE}"Use almanac tools to write an article on <topic>"${RST}`));
|
|
506
|
+
w(empty);
|
|
507
|
+
w(` ${BLUE_DIM}\u2570${"─".repeat(innerW)}\u256f${RST}`);
|
|
508
|
+
w("");
|
|
219
509
|
}
|
|
220
510
|
/* ── Entry point ────────────────────────────────────────────────── */
|
|
221
511
|
export async function runSetup() {
|
|
222
512
|
const skipTui = process.argv.includes("--yes") || process.argv.includes("-y");
|
|
223
513
|
const interactive = process.stdin.isTTY && !skipTui;
|
|
514
|
+
let agent = "Claude Code";
|
|
515
|
+
if (interactive) {
|
|
516
|
+
agent = await runAgentSelect();
|
|
517
|
+
}
|
|
224
518
|
const mcpChanged = configureMcp();
|
|
225
|
-
const mcpLine = `${GREEN}✓${RST} MCP server ${mcpChanged ? "added" : "already configured"}`;
|
|
226
519
|
let tools;
|
|
227
520
|
if (interactive) {
|
|
228
|
-
tools = await
|
|
521
|
+
tools = await runToolSelect(agent, mcpChanged);
|
|
229
522
|
}
|
|
230
523
|
else {
|
|
231
524
|
tools = TOOL_GROUPS.flatMap((g) => g.tools);
|
|
232
525
|
}
|
|
233
526
|
const count = configurePermissions(tools);
|
|
234
|
-
|
|
527
|
+
// Login step (last — so when they return from browser, everything is done)
|
|
528
|
+
let loginResult;
|
|
529
|
+
if (interactive) {
|
|
530
|
+
loginResult = await runLoginStep(agent, mcpChanged, count);
|
|
531
|
+
}
|
|
532
|
+
else {
|
|
533
|
+
try {
|
|
534
|
+
const result = await performLogin();
|
|
535
|
+
loginResult =
|
|
536
|
+
result.status === "already_logged_in"
|
|
537
|
+
? { status: "already", name: result.name }
|
|
538
|
+
: { status: "done" };
|
|
539
|
+
}
|
|
540
|
+
catch {
|
|
541
|
+
loginResult = { status: "skipped" };
|
|
542
|
+
}
|
|
543
|
+
}
|
|
544
|
+
printResult(agent, loginResult, mcpChanged, count);
|
|
545
|
+
process.exit(0);
|
|
235
546
|
}
|