openalmanac 0.2.18 → 0.2.20
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 +1 -1
- package/dist/login-core.js +154 -3
- package/dist/setup.js +356 -43
- package/package.json +1 -1
package/dist/login-core.d.ts
CHANGED
|
@@ -9,4 +9,4 @@ 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(): Promise<LoginResult>;
|
|
12
|
+
export declare function performLogin(signal?: AbortSignal): Promise<LoginResult>;
|
package/dist/login-core.js
CHANGED
|
@@ -3,11 +3,157 @@ 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() {
|
|
156
|
+
export async function performLogin(signal) {
|
|
11
157
|
const existingKey = getApiKey();
|
|
12
158
|
if (existingKey) {
|
|
13
159
|
try {
|
|
@@ -36,14 +182,19 @@ export async function performLogin() {
|
|
|
36
182
|
const callbackToken = url.searchParams.get("token");
|
|
37
183
|
if (!callbackToken || !callbackToken.startsWith("oa_")) {
|
|
38
184
|
res.writeHead(400, { "Content-Type": "text/html" });
|
|
39
|
-
res.end(
|
|
185
|
+
res.end(callbackPage(false));
|
|
40
186
|
return;
|
|
41
187
|
}
|
|
42
188
|
res.writeHead(200, { "Content-Type": "text/html" });
|
|
43
|
-
res.end(
|
|
189
|
+
res.end(callbackPage(true));
|
|
44
190
|
resolve({ token: callbackToken, browserOpened: opened });
|
|
45
191
|
httpServer.close();
|
|
46
192
|
});
|
|
193
|
+
// Allow caller to abort (closes the server and rejects)
|
|
194
|
+
signal?.addEventListener("abort", () => {
|
|
195
|
+
httpServer.close();
|
|
196
|
+
reject(new Error("Login cancelled"));
|
|
197
|
+
}, { once: true });
|
|
47
198
|
httpServer.listen(0, "127.0.0.1", () => {
|
|
48
199
|
const addr = httpServer.address();
|
|
49
200
|
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",
|
|
@@ -60,20 +62,71 @@ const TOOL_GROUPS = [
|
|
|
60
62
|
"Edit(~/.openalmanac/**)",
|
|
61
63
|
],
|
|
62
64
|
},
|
|
65
|
+
{
|
|
66
|
+
name: "Web Access",
|
|
67
|
+
description: "web search & fetch used during research",
|
|
68
|
+
tools: ["WebSearch", "WebFetch"],
|
|
69
|
+
},
|
|
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 },
|
|
63
76
|
];
|
|
64
77
|
/* ── ANSI helpers ───────────────────────────────────────────────── */
|
|
65
78
|
const RST = "\x1b[0m";
|
|
66
79
|
const BOLD = "\x1b[1m";
|
|
67
80
|
const DIM = "\x1b[2m";
|
|
68
|
-
const GREEN = "\x1b[32m";
|
|
69
|
-
const CYAN = "\x1b[36m";
|
|
70
|
-
const RED = "\x1b[31m";
|
|
71
|
-
const YELLOW = "\x1b[33m";
|
|
72
|
-
const MAGENTA = "\x1b[35m";
|
|
73
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");
|
|
74
127
|
/* ── File helpers ───────────────────────────────────────────────── */
|
|
75
128
|
const CLAUDE_DIR = join(homedir(), ".claude");
|
|
76
|
-
const
|
|
129
|
+
const CLAUDE_JSON = join(homedir(), ".claude.json");
|
|
77
130
|
const SETTINGS_JSON = join(CLAUDE_DIR, "settings.json");
|
|
78
131
|
function ensureDir(dir) {
|
|
79
132
|
if (!existsSync(dir))
|
|
@@ -92,9 +145,8 @@ function writeJson(path, data) {
|
|
|
92
145
|
}
|
|
93
146
|
/* ── Step 1 — MCP server ───────────────────────────────────────── */
|
|
94
147
|
function configureMcp() {
|
|
95
|
-
ensureDir(CLAUDE_DIR);
|
|
96
148
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
97
|
-
const cfg = readJson(
|
|
149
|
+
const cfg = readJson(CLAUDE_JSON);
|
|
98
150
|
if (!cfg.mcpServers)
|
|
99
151
|
cfg.mcpServers = {};
|
|
100
152
|
const cur = cfg.mcpServers.almanac;
|
|
@@ -103,7 +155,7 @@ function configureMcp() {
|
|
|
103
155
|
return false; // already set
|
|
104
156
|
}
|
|
105
157
|
cfg.mcpServers.almanac = { command: "npx", args: ["-y", "openalmanac"] };
|
|
106
|
-
writeJson(
|
|
158
|
+
writeJson(CLAUDE_JSON, cfg);
|
|
107
159
|
return true;
|
|
108
160
|
}
|
|
109
161
|
/* ── Step 2 — Permissions ──────────────────────────────────────── */
|
|
@@ -127,37 +179,259 @@ function configurePermissions(tools) {
|
|
|
127
179
|
writeJson(SETTINGS_JSON, settings);
|
|
128
180
|
return tools.length;
|
|
129
181
|
}
|
|
130
|
-
/* ──
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
182
|
+
/* ── Agent selection screen ─────────────────────────────────────── */
|
|
183
|
+
function renderAgentSelect(_cursor) {
|
|
184
|
+
process.stdout.write("\x1b[2J\x1b[H");
|
|
185
|
+
printBanner();
|
|
186
|
+
printBadge();
|
|
135
187
|
w("");
|
|
136
|
-
|
|
137
|
-
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}`);
|
|
138
200
|
w("");
|
|
139
|
-
|
|
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
|
+
const auth = await getAuthStatus();
|
|
323
|
+
if (auth.loggedIn) {
|
|
324
|
+
const keepAccount = await runLoginChoice(auth.name);
|
|
325
|
+
if (keepAccount) {
|
|
326
|
+
return { status: "already", name: auth.name };
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
// Show prompt before opening browser
|
|
330
|
+
process.stdout.write("\x1b[2J\x1b[H");
|
|
331
|
+
printBanner();
|
|
332
|
+
printBadge();
|
|
140
333
|
w("");
|
|
141
|
-
|
|
142
|
-
|
|
334
|
+
priorSteps();
|
|
335
|
+
stepActive(`Login to Almanac`);
|
|
336
|
+
w(BAR);
|
|
337
|
+
w(` ${DIM}\u2502${RST} This will open ${WHITE_BOLD}almanac${RST} in your browser`);
|
|
338
|
+
w(` ${DIM}\u2502${RST} to connect your account.`);
|
|
339
|
+
w(BAR);
|
|
340
|
+
await waitForKey(` ${DIM}\u2502${RST} ${BLUE}${BOLD}[enter]${RST} continue`);
|
|
341
|
+
// Show waiting state with cancel/retry hint
|
|
342
|
+
const renderWaiting = () => {
|
|
343
|
+
process.stdout.write("\x1b[2J\x1b[H");
|
|
344
|
+
printBanner();
|
|
345
|
+
printBadge();
|
|
346
|
+
w("");
|
|
347
|
+
priorSteps();
|
|
348
|
+
stepActive(`Waiting for login\u2026 ${DIM}complete in browser${RST}`);
|
|
349
|
+
w(BAR);
|
|
350
|
+
w(` ${DIM}\u2502${RST} ${BLUE}${BOLD}[r]${RST} retry ${DIM}[q] cancel setup${RST}`);
|
|
351
|
+
w("");
|
|
352
|
+
};
|
|
353
|
+
// Loop: attempt login, let user retry if it fails/times out
|
|
354
|
+
// eslint-disable-next-line no-constant-condition
|
|
355
|
+
while (true) {
|
|
356
|
+
renderWaiting();
|
|
357
|
+
// AbortController so we can kill the HTTP server on retry/cancel
|
|
358
|
+
const controller = new AbortController();
|
|
359
|
+
// Race login against keypress
|
|
360
|
+
const loginPromise = performLogin(controller.signal).then((result) => result.status === "already_logged_in"
|
|
361
|
+
? { status: "already", name: result.name }
|
|
362
|
+
: { status: "done" }, () => ({ status: "skipped" }));
|
|
363
|
+
let keyOnData = null;
|
|
364
|
+
const keyPromise = new Promise((resolve) => {
|
|
365
|
+
process.stdin.setRawMode(true);
|
|
366
|
+
process.stdin.resume();
|
|
367
|
+
process.stdin.setEncoding("utf-8");
|
|
368
|
+
keyOnData = (key) => {
|
|
369
|
+
process.stdin.removeListener("data", keyOnData);
|
|
370
|
+
process.stdin.setRawMode(false);
|
|
371
|
+
process.stdin.pause();
|
|
372
|
+
resolve(key);
|
|
373
|
+
};
|
|
374
|
+
process.stdin.on("data", keyOnData);
|
|
375
|
+
});
|
|
376
|
+
const result = await Promise.race([
|
|
377
|
+
loginPromise.then((r) => ({ type: "login", result: r })),
|
|
378
|
+
keyPromise.then((k) => ({ type: "key", key: k })),
|
|
379
|
+
]);
|
|
380
|
+
// Clean up stdin listener if login won
|
|
381
|
+
if (result.type === "login") {
|
|
382
|
+
if (keyOnData)
|
|
383
|
+
process.stdin.removeListener("data", keyOnData);
|
|
384
|
+
try {
|
|
385
|
+
process.stdin.setRawMode(false);
|
|
386
|
+
}
|
|
387
|
+
catch { /* already off */ }
|
|
388
|
+
process.stdin.pause();
|
|
389
|
+
return result.result;
|
|
390
|
+
}
|
|
391
|
+
// Key won — abort the login HTTP server
|
|
392
|
+
controller.abort();
|
|
393
|
+
// Handle keypress
|
|
394
|
+
if (result.key === "\x03" || result.key === "q") {
|
|
395
|
+
process.stdout.write("\x1b[2J\x1b[H");
|
|
396
|
+
console.log("\n Setup cancelled.\n");
|
|
397
|
+
process.exit(0);
|
|
398
|
+
}
|
|
399
|
+
if (result.key === "r") {
|
|
400
|
+
// Retry — loop continues
|
|
401
|
+
continue;
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
/* ── Tool permissions TUI ───────────────────────────────────────── */
|
|
406
|
+
const MAX_NAME = Math.max(...TOOL_GROUPS.map((g) => g.name.length));
|
|
407
|
+
function renderToolSelect(selected, cursor, agent, mcpChanged) {
|
|
408
|
+
process.stdout.write("\x1b[2J\x1b[H");
|
|
409
|
+
printBanner();
|
|
410
|
+
printBadge();
|
|
143
411
|
w("");
|
|
412
|
+
stepDone(`Agent \u2192 ${WHITE_BOLD}${agent}${RST}`);
|
|
413
|
+
w(BAR);
|
|
414
|
+
stepDone(`MCP server ${mcpChanged ? `added to ${DIM}~/.claude.json${RST}` : `${DIM}already configured${RST}`}`);
|
|
415
|
+
w(BAR);
|
|
416
|
+
stepActive(`Select tool permissions ${DIM}deselect any you'd rather approve manually${RST}`);
|
|
417
|
+
w(BAR);
|
|
144
418
|
for (let i = 0; i < TOOL_GROUPS.length; i++) {
|
|
145
|
-
const arrow = i === cursor ? `${
|
|
146
|
-
const check = selected[i] ? `${
|
|
419
|
+
const arrow = i === cursor ? `${BLUE}\u276f${RST}` : " ";
|
|
420
|
+
const check = selected[i] ? `${BLUE}\u2713${RST}` : " ";
|
|
147
421
|
const pad = TOOL_GROUPS[i].name.padEnd(MAX_NAME + 2);
|
|
148
422
|
const name = i === cursor ? `${BOLD}${pad}${RST}` : pad;
|
|
149
423
|
const desc = `${DIM}${TOOL_GROUPS[i].description}${RST}`;
|
|
150
|
-
w(` ${arrow} [${check}] ${name} ${desc}`);
|
|
424
|
+
w(` ${DIM}\u2502${RST} ${arrow} [${check}] ${name} ${desc}`);
|
|
151
425
|
}
|
|
152
|
-
w(
|
|
153
|
-
w(` ${
|
|
426
|
+
w(BAR);
|
|
427
|
+
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}`);
|
|
154
428
|
w("");
|
|
155
429
|
}
|
|
156
|
-
function
|
|
430
|
+
function runToolSelect(agent, mcpChanged) {
|
|
157
431
|
return new Promise((resolve) => {
|
|
158
|
-
const selected = TOOL_GROUPS.map(() => true);
|
|
432
|
+
const selected = TOOL_GROUPS.map(() => true);
|
|
159
433
|
let cursor = 0;
|
|
160
|
-
|
|
434
|
+
renderToolSelect(selected, cursor, agent, mcpChanged);
|
|
161
435
|
process.stdin.setRawMode(true);
|
|
162
436
|
process.stdin.resume();
|
|
163
437
|
process.stdin.setEncoding("utf-8");
|
|
@@ -167,7 +441,6 @@ function runTui(mcpLine) {
|
|
|
167
441
|
process.stdin.pause();
|
|
168
442
|
};
|
|
169
443
|
const onData = (key) => {
|
|
170
|
-
// Ctrl-C / q → cancel
|
|
171
444
|
if (key === "\x03" || key === "q") {
|
|
172
445
|
cleanup();
|
|
173
446
|
process.stdout.write("\x1b[2J\x1b[H");
|
|
@@ -194,38 +467,78 @@ function runTui(mcpLine) {
|
|
|
194
467
|
resolve(tools);
|
|
195
468
|
return;
|
|
196
469
|
}
|
|
197
|
-
|
|
470
|
+
renderToolSelect(selected, cursor, agent, mcpChanged);
|
|
198
471
|
};
|
|
199
472
|
process.stdin.on("data", onData);
|
|
200
473
|
});
|
|
201
474
|
}
|
|
202
475
|
/* ── Result screen ──────────────────────────────────────────────── */
|
|
203
|
-
function printResult(mcpChanged, toolCount) {
|
|
476
|
+
function printResult(agent, loginResult, mcpChanged, toolCount) {
|
|
204
477
|
process.stdout.write("\x1b[2J\x1b[H");
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
478
|
+
printBanner();
|
|
479
|
+
printBadge();
|
|
480
|
+
w("");
|
|
481
|
+
stepDone(`Agent \u2192 ${WHITE_BOLD}${agent}${RST}`);
|
|
482
|
+
w(BAR);
|
|
483
|
+
stepDone(`MCP server ${mcpChanged ? `added to ${DIM}~/.claude.json${RST}` : `${DIM}already configured${RST}`}`);
|
|
484
|
+
w(BAR);
|
|
485
|
+
stepDone(`${BLUE}${toolCount}${RST} tool${toolCount !== 1 ? "s" : ""} allowed in ${DIM}~/.claude/settings.json${RST}`);
|
|
486
|
+
w(BAR);
|
|
487
|
+
stepDone(loginLabel(loginResult));
|
|
488
|
+
w(BAR);
|
|
489
|
+
stepDone(`${BLUE}Setup complete${RST}`);
|
|
490
|
+
w("");
|
|
491
|
+
// Next steps box
|
|
492
|
+
const innerW = 52;
|
|
493
|
+
const row = (content) => {
|
|
494
|
+
const padding = Math.max(0, innerW - vis(content));
|
|
495
|
+
return ` ${BLUE_DIM}\u2502${RST}${content}${" ".repeat(padding)}${BLUE_DIM}\u2502${RST}`;
|
|
496
|
+
};
|
|
497
|
+
const empty = row("");
|
|
498
|
+
w(` ${BLUE_DIM}\u256d${"─".repeat(innerW)}\u256e${RST}`);
|
|
499
|
+
w(empty);
|
|
500
|
+
w(row(` ${WHITE_BOLD}Next steps${RST}`));
|
|
501
|
+
w(empty);
|
|
502
|
+
w(row(` ${BLUE}1.${RST} Type ${WHITE_BOLD}claude${RST} to start Claude Code`));
|
|
503
|
+
w(row(` ${BLUE}2.${RST} Say ${BLUE}"Use almanac tools to write an article on <topic>"${RST}`));
|
|
504
|
+
w(empty);
|
|
505
|
+
w(` ${BLUE_DIM}\u2570${"─".repeat(innerW)}\u256f${RST}`);
|
|
506
|
+
w("");
|
|
215
507
|
}
|
|
216
508
|
/* ── Entry point ────────────────────────────────────────────────── */
|
|
217
509
|
export async function runSetup() {
|
|
218
510
|
const skipTui = process.argv.includes("--yes") || process.argv.includes("-y");
|
|
219
511
|
const interactive = process.stdin.isTTY && !skipTui;
|
|
512
|
+
let agent = "Claude Code";
|
|
513
|
+
if (interactive) {
|
|
514
|
+
agent = await runAgentSelect();
|
|
515
|
+
}
|
|
220
516
|
const mcpChanged = configureMcp();
|
|
221
|
-
const mcpLine = `${GREEN}✓${RST} MCP server ${mcpChanged ? "added" : "already configured"}`;
|
|
222
517
|
let tools;
|
|
223
518
|
if (interactive) {
|
|
224
|
-
tools = await
|
|
519
|
+
tools = await runToolSelect(agent, mcpChanged);
|
|
225
520
|
}
|
|
226
521
|
else {
|
|
227
522
|
tools = TOOL_GROUPS.flatMap((g) => g.tools);
|
|
228
523
|
}
|
|
229
524
|
const count = configurePermissions(tools);
|
|
230
|
-
|
|
525
|
+
// Login step (last — so when they return from browser, everything is done)
|
|
526
|
+
let loginResult;
|
|
527
|
+
if (interactive) {
|
|
528
|
+
loginResult = await runLoginStep(agent, mcpChanged, count);
|
|
529
|
+
}
|
|
530
|
+
else {
|
|
531
|
+
try {
|
|
532
|
+
const result = await performLogin();
|
|
533
|
+
loginResult =
|
|
534
|
+
result.status === "already_logged_in"
|
|
535
|
+
? { status: "already", name: result.name }
|
|
536
|
+
: { status: "done" };
|
|
537
|
+
}
|
|
538
|
+
catch {
|
|
539
|
+
loginResult = { status: "skipped" };
|
|
540
|
+
}
|
|
541
|
+
}
|
|
542
|
+
printResult(agent, loginResult, mcpChanged, count);
|
|
543
|
+
process.exit(0);
|
|
231
544
|
}
|