openalmanac 0.4.0 → 0.4.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (37) hide show
  1. package/dist/cli.js +1 -1
  2. package/dist/instructions.d.ts +1 -0
  3. package/dist/instructions.js +150 -0
  4. package/dist/onboarding-copy.d.ts +1 -1
  5. package/dist/onboarding-copy.js +6 -6
  6. package/dist/openalmanac_mcp-0.3.1-py3-none-any.whl +0 -0
  7. package/dist/openalmanac_mcp-0.3.1.tar.gz +0 -0
  8. package/dist/openalmanac_mcp-0.3.2-py3-none-any.whl +0 -0
  9. package/dist/openalmanac_mcp-0.3.2.tar.gz +0 -0
  10. package/dist/server.js +3 -149
  11. package/dist/setup/clients.d.ts +10 -0
  12. package/dist/setup/clients.js +291 -0
  13. package/dist/setup/config-files.d.ts +43 -0
  14. package/dist/setup/config-files.js +257 -0
  15. package/dist/setup/index.d.ts +2 -0
  16. package/dist/setup/index.js +55 -0
  17. package/dist/setup/permissions.d.ts +3 -0
  18. package/dist/setup/permissions.js +52 -0
  19. package/dist/{setup.d.ts → setup/reddit.d.ts} +0 -1
  20. package/dist/setup/reddit.js +69 -0
  21. package/dist/setup/tui.d.ts +7 -0
  22. package/dist/setup/tui.js +517 -0
  23. package/dist/setup/types.d.ts +43 -0
  24. package/dist/setup/types.js +1 -0
  25. package/dist/tool-registry.js +1 -1
  26. package/dist/tools/{pages.js → pages/index.js} +8 -164
  27. package/dist/tools/pages/publish-format.d.ts +48 -0
  28. package/dist/tools/pages/publish-format.js +92 -0
  29. package/dist/tools/pages/workspace.d.ts +7 -0
  30. package/dist/tools/pages/workspace.js +14 -0
  31. package/dist/tools/pages/writing-guide.d.ts +1 -0
  32. package/dist/tools/pages/writing-guide.js +56 -0
  33. package/package.json +1 -1
  34. package/dist/setup.js +0 -1216
  35. package/dist/validate.d.ts +0 -971
  36. package/dist/validate.js +0 -154
  37. /package/dist/tools/{pages.d.ts → pages/index.d.ts} +0 -0
@@ -0,0 +1,52 @@
1
+ import { MCP_TOOL_GROUPS, toClaudePermissionName } from "../tool-registry.js";
2
+ import { CLAUDE_DIR, SETTINGS_JSON, ensureDir, readJson, writeJson } from "./config-files.js";
3
+ function mcpGroupToPermissionGroup(group) {
4
+ return {
5
+ name: group.name,
6
+ description: group.description,
7
+ tools: group.tools.map(toClaudePermissionName),
8
+ };
9
+ }
10
+ // Built-in Claude Code tool groups — not MCP tools, so they stay defined
11
+ // here. The user opts into them in the same TUI checkbox screen.
12
+ const CLAUDE_BUILTIN_TOOL_GROUPS = [
13
+ {
14
+ name: "Local Files",
15
+ description: "read & edit pages in ~/.openalmanac",
16
+ tools: [
17
+ "Read(~/.openalmanac/**)",
18
+ "Write(~/.openalmanac/**)",
19
+ "Edit(~/.openalmanac/**)",
20
+ ],
21
+ },
22
+ {
23
+ name: "Web Access",
24
+ description: "web search & fetch used during research",
25
+ tools: ["WebSearch", "WebFetch"],
26
+ },
27
+ ];
28
+ export const TOOL_GROUPS = [
29
+ ...MCP_TOOL_GROUPS.map(mcpGroupToPermissionGroup),
30
+ ...CLAUDE_BUILTIN_TOOL_GROUPS,
31
+ ];
32
+ export function configurePermissions(tools) {
33
+ ensureDir(CLAUDE_DIR);
34
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
35
+ const settings = readJson(SETTINGS_JSON);
36
+ if (!settings.permissions)
37
+ settings.permissions = {};
38
+ if (!Array.isArray(settings.permissions.allow))
39
+ settings.permissions.allow = [];
40
+ const existing = new Set(settings.permissions.allow);
41
+ let added = 0;
42
+ for (const t of tools) {
43
+ if (!existing.has(t)) {
44
+ settings.permissions.allow.push(t);
45
+ added++;
46
+ }
47
+ }
48
+ if (added > 0)
49
+ writeJson(SETTINGS_JSON, settings);
50
+ return tools.length;
51
+ }
52
+ /* ── Client selection screen ────────────────────────────────────── */
@@ -1,2 +1 @@
1
- export declare function runSetup(): Promise<void>;
2
1
  export declare function runRedditSetup(): Promise<void>;
@@ -0,0 +1,69 @@
1
+ import { cpSync, existsSync, mkdirSync } from "fs";
2
+ import { homedir } from "os";
3
+ import { dirname, join } from "path";
4
+ import { fileURLToPath } from "url";
5
+ import { performLogin } from "../login-core.js";
6
+ import { SUPPORTED_CLIENTS } from "./clients.js";
7
+ import { TOOL_GROUPS, configurePermissions } from "./permissions.js";
8
+ import { printRedditResult, runAgentSelect, runLoginStep, runToolSelect } from "./tui.js";
9
+ function getPackageSkillsDir() {
10
+ const thisFile = fileURLToPath(import.meta.url);
11
+ // dist/setup.js → package root → skills/
12
+ return join(dirname(thisFile), "..", "skills");
13
+ }
14
+ function installSkill(skillName) {
15
+ const src = join(getPackageSkillsDir(), skillName);
16
+ if (!existsSync(src)) {
17
+ throw new Error(`Skill "${skillName}" not found in package at ${src}`);
18
+ }
19
+ const dest = join(homedir(), ".claude", "skills", skillName);
20
+ // Always overwrite to ensure latest version
21
+ mkdirSync(dirname(dest), { recursive: true });
22
+ cpSync(src, dest, { recursive: true, force: true });
23
+ return true;
24
+ }
25
+ /* ── Reddit-specific tool groups ───────────────────────────────── */
26
+ const REDDIT_EXTRA_TOOLS = [
27
+ "Bash(node */ingest.js *)",
28
+ ];
29
+ export async function runRedditSetup() {
30
+ const skipTui = process.argv.includes("--yes") || process.argv.includes("-y");
31
+ const interactive = process.stdin.isTTY && !skipTui;
32
+ let agent = "Claude Code";
33
+ if (interactive) {
34
+ agent = await runAgentSelect("reddit");
35
+ }
36
+ const claudeSetup = SUPPORTED_CLIENTS["claude-code"].configure("apply");
37
+ const mcpChanged = claudeSetup.changed;
38
+ let tools;
39
+ if (interactive) {
40
+ tools = await runToolSelect(agent, mcpChanged, "reddit");
41
+ }
42
+ else {
43
+ tools = TOOL_GROUPS.flatMap((g) => g.tools);
44
+ }
45
+ // Add reddit-specific tool permissions
46
+ tools = [...tools, ...REDDIT_EXTRA_TOOLS];
47
+ const count = configurePermissions(tools);
48
+ // Login step
49
+ let loginResult;
50
+ if (interactive) {
51
+ loginResult = await runLoginStep(agent, mcpChanged, count, "reddit");
52
+ }
53
+ else {
54
+ try {
55
+ const result = await performLogin();
56
+ loginResult =
57
+ result.status === "already_logged_in"
58
+ ? { status: "already", name: result.name }
59
+ : { status: "done" };
60
+ }
61
+ catch {
62
+ loginResult = { status: "skipped" };
63
+ }
64
+ }
65
+ // Install the reddit-wiki skill
66
+ installSkill("reddit-wiki");
67
+ printRedditResult(agent, loginResult, mcpChanged, count);
68
+ process.exit(0);
69
+ }
@@ -0,0 +1,7 @@
1
+ import type { LoginStepResult, SetupClient, SetupMode } from "./types.js";
2
+ export declare function runClientSelect(clients: SetupClient[], mode?: SetupMode): Promise<SetupClient[]>;
3
+ export declare function runAgentSelect(mode?: SetupMode): Promise<string>;
4
+ export declare function runLoginStep(agent: string, mcpChanged: boolean, toolCount: number | null, mode?: SetupMode): Promise<LoginStepResult>;
5
+ export declare function runToolSelect(clientsLabel: string, mcpChanged: boolean, mode?: SetupMode): Promise<string[]>;
6
+ export declare function printResult(clientsLabel: string, loginResult: LoginStepResult, configured: string[], alreadyConfigured: string[], toolCount: number): void;
7
+ export declare function printRedditResult(agent: string, loginResult: LoginStepResult, mcpChanged: boolean, toolCount: number): void;
@@ -0,0 +1,517 @@
1
+ import { performLogin } from "../login-core.js";
2
+ import { getAuthStatus } from "../auth.js";
3
+ import { EXAMPLE_PROMPT } from "../onboarding-copy.js";
4
+ import { SUPPORTED_CLIENTS } from "./clients.js";
5
+ import { TOOL_GROUPS } from "./permissions.js";
6
+ const RST = "\x1b[0m";
7
+ const BOLD = "\x1b[1m";
8
+ const DIM = "\x1b[2m";
9
+ const WHITE_BOLD = "\x1b[1;37m";
10
+ const BLUE = "\x1b[38;5;75m"; // blue for accents
11
+ const BLUE_DIM = "\x1b[38;5;69m"; // slightly deeper blue for boxes
12
+ // Interactive TUI accent
13
+ const ACCENT = "\x1b[38;5;252m"; // silver
14
+ const ACCENT_BG = "\x1b[48;5;252m\x1b[38;5;16m"; // badge: silver bg, black text
15
+ // Banner gradient: white → silver
16
+ const GRADIENT = [
17
+ "\x1b[38;5;255m",
18
+ "\x1b[38;5;253m",
19
+ "\x1b[38;5;251m",
20
+ "\x1b[38;5;249m",
21
+ "\x1b[38;5;246m",
22
+ "\x1b[38;5;243m",
23
+ ];
24
+ /* ── ASCII banner ───────────────────────────────────────────────── */
25
+ const LOGO_LINES = [
26
+ " \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",
27
+ "\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",
28
+ "\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 ",
29
+ "\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 ",
30
+ "\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",
31
+ "\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",
32
+ ];
33
+ function printBanner(subtitle = "Write and publish pages with your AI agent") {
34
+ process.stdout.write("\n");
35
+ for (let i = 0; i < LOGO_LINES.length; i++) {
36
+ process.stdout.write(`${GRADIENT[i]}${LOGO_LINES[i]}${RST}\n`);
37
+ }
38
+ process.stdout.write(`\n${WHITE_BOLD} ${subtitle}${RST}\n`);
39
+ }
40
+ function renderHeader(mode = "default") {
41
+ printBanner(mode === "reddit"
42
+ ? "Turn any subreddit into a published wiki"
43
+ : "Write and publish pages with your AI agent");
44
+ }
45
+ function printBadge() {
46
+ process.stdout.write(`\n ${ACCENT_BG} almanac ${RST}\n`);
47
+ }
48
+ /* ── Step indicators ────────────────────────────────────────────── */
49
+ const BAR = ` ${DIM}\u2502${RST}`;
50
+ function stepDone(msg) {
51
+ process.stdout.write(` ${BLUE}\u25c7${RST} ${msg}\n`);
52
+ }
53
+ function stepActive(msg) {
54
+ process.stdout.write(` ${BLUE}\u25c6${RST} ${msg}\n`);
55
+ }
56
+ /* ── Helpers ────────────────────────────────────────────────────── */
57
+ // Strip ANSI codes to measure visible length
58
+ const vis = (s) => s.replace(/\x1b\[[0-9;]*m/g, "").length;
59
+ const w = (s) => process.stdout.write(s + "\n");
60
+ function getBoxInnerWidth(contents, minWidth = 62) {
61
+ const terminalWidth = process.stdout.columns ?? 80;
62
+ const available = Math.max(40, terminalWidth - 6);
63
+ const widest = contents.reduce((max, content) => Math.max(max, vis(content)), 0);
64
+ return Math.min(Math.max(minWidth, widest), available);
65
+ }
66
+ function boxRow(content, innerW) {
67
+ const padding = Math.max(0, innerW - vis(content));
68
+ return ` ${BLUE_DIM}\u2502${RST}${content}${" ".repeat(padding)}${BLUE_DIM}\u2502${RST}`;
69
+ }
70
+ function wrapWithPrefixes(text, firstPrefix, nextPrefix, innerW) {
71
+ const words = text.split(" ").filter(Boolean);
72
+ if (words.length === 0)
73
+ return [firstPrefix];
74
+ const lines = [];
75
+ let prefix = firstPrefix;
76
+ let line = prefix;
77
+ let hasWord = false;
78
+ for (const word of words) {
79
+ const candidate = hasWord ? `${line} ${word}` : `${prefix}${word}`;
80
+ if (hasWord && vis(candidate) > innerW) {
81
+ lines.push(line);
82
+ prefix = nextPrefix;
83
+ line = `${prefix}${word}`;
84
+ hasWord = true;
85
+ continue;
86
+ }
87
+ line = candidate;
88
+ hasWord = true;
89
+ }
90
+ lines.push(line);
91
+ return lines;
92
+ }
93
+ function renderNextStepsBox(lines) {
94
+ const header = ` ${WHITE_BOLD}Next steps${RST}`;
95
+ const innerW = getBoxInnerWidth([header, ...lines]);
96
+ const empty = boxRow("", innerW);
97
+ w(` ${BLUE_DIM}\u256d${"─".repeat(innerW)}\u256e${RST}`);
98
+ w(empty);
99
+ w(boxRow(header, innerW));
100
+ w(empty);
101
+ for (const line of lines) {
102
+ w(boxRow(line, innerW));
103
+ }
104
+ w(empty);
105
+ w(` ${BLUE_DIM}\u2570${"─".repeat(innerW)}\u256f${RST}`);
106
+ w("");
107
+ }
108
+ function renderClientSelect(clients, selected, cursor, mode = "default") {
109
+ process.stdout.write("\x1b[2J\x1b[H");
110
+ renderHeader(mode);
111
+ printBadge();
112
+ w("");
113
+ stepActive(`Select where to install Almanac`);
114
+ w(BAR);
115
+ for (let i = 0; i < clients.length; i++) {
116
+ const client = clients[i];
117
+ const arrow = i === cursor ? `${BLUE}\u276f${RST}` : " ";
118
+ const check = selected[i] ? `${BLUE}\u2713${RST}` : " ";
119
+ const label = i === cursor ? `${BOLD}${client.selectionLabel ?? client.name}${RST}` : client.selectionLabel ?? client.name;
120
+ w(` ${DIM}\u2502${RST} ${arrow} [${check}] ${label}`);
121
+ }
122
+ w(BAR);
123
+ 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}`);
124
+ w("");
125
+ }
126
+ export function runClientSelect(clients, mode = "default") {
127
+ return new Promise((resolve) => {
128
+ const selected = clients.map(() => true);
129
+ let cursor = 0;
130
+ renderClientSelect(clients, selected, cursor, mode);
131
+ process.stdin.setRawMode(true);
132
+ process.stdin.resume();
133
+ process.stdin.setEncoding("utf-8");
134
+ const cleanup = () => {
135
+ process.stdin.removeListener("data", onData);
136
+ process.stdin.setRawMode(false);
137
+ process.stdin.pause();
138
+ };
139
+ const onData = (key) => {
140
+ if (key === "\x03" || key === "q") {
141
+ cleanup();
142
+ process.stdout.write("\x1b[2J\x1b[H");
143
+ console.log("\n Setup cancelled.\n");
144
+ process.exit(0);
145
+ }
146
+ if (key === "\x1b[A" || key === "k") {
147
+ cursor = (cursor - 1 + clients.length) % clients.length;
148
+ }
149
+ else if (key === "\x1b[B" || key === "j") {
150
+ cursor = (cursor + 1) % clients.length;
151
+ }
152
+ else if (key === " ") {
153
+ selected[cursor] = !selected[cursor];
154
+ }
155
+ else if (key === "a") {
156
+ const all = selected.every(Boolean);
157
+ selected.fill(!all);
158
+ }
159
+ else if (key === "\r" || key === "\n") {
160
+ cleanup();
161
+ const chosen = clients.filter((_, index) => selected[index]);
162
+ if (chosen.length === 0) {
163
+ console.log("\n Select at least one client.\n");
164
+ process.exit(1);
165
+ }
166
+ resolve(chosen);
167
+ return;
168
+ }
169
+ renderClientSelect(clients, selected, cursor, mode);
170
+ };
171
+ process.stdin.on("data", onData);
172
+ });
173
+ }
174
+ export async function runAgentSelect(mode = "default") {
175
+ const [client] = await runClientSelect([SUPPORTED_CLIENTS["claude-code"]], mode);
176
+ return client.name;
177
+ }
178
+ function loginLabel(result) {
179
+ if (result.status === "already")
180
+ return `Logged in as ${WHITE_BOLD}${result.name}${RST}`;
181
+ if (result.status === "done")
182
+ return `Logged in`;
183
+ return `Login ${DIM}skipped${RST}`;
184
+ }
185
+ function waitForKey(prompt) {
186
+ return new Promise((resolve) => {
187
+ process.stdin.setRawMode(true);
188
+ process.stdin.resume();
189
+ process.stdin.setEncoding("utf-8");
190
+ w(prompt);
191
+ const onData = (key) => {
192
+ process.stdin.removeListener("data", onData);
193
+ process.stdin.setRawMode(false);
194
+ process.stdin.pause();
195
+ if (key === "\x03") {
196
+ process.stdout.write("\x1b[2J\x1b[H");
197
+ console.log("\n Setup cancelled.\n");
198
+ process.exit(0);
199
+ }
200
+ resolve(key);
201
+ };
202
+ process.stdin.on("data", onData);
203
+ });
204
+ }
205
+ export async function runLoginStep(agent, mcpChanged, toolCount, mode = "default") {
206
+ const label = agent.includes(",") ? "Clients" : "Agent";
207
+ const priorSteps = () => {
208
+ stepDone(`${label} \u2192 ${WHITE_BOLD}${agent}${RST}`);
209
+ w(BAR);
210
+ stepDone(`MCP server ${mcpChanged ? "configured" : `${DIM}already configured${RST}`}`);
211
+ if (toolCount !== null) {
212
+ w(BAR);
213
+ stepDone(`${BLUE}${toolCount}${RST} tool${toolCount !== 1 ? "s" : ""} allowed`);
214
+ }
215
+ w(BAR);
216
+ };
217
+ function renderLoginChoice(name, cursor) {
218
+ process.stdout.write("\x1b[2J\x1b[H");
219
+ renderHeader(mode);
220
+ printBadge();
221
+ w("");
222
+ priorSteps();
223
+ stepActive(`Already logged in as ${WHITE_BOLD}${name}${RST}`);
224
+ w(BAR);
225
+ const options = [
226
+ `Continue as ${BOLD}${name}${RST}`,
227
+ `Login with a different account`,
228
+ ];
229
+ for (let i = 0; i < options.length; i++) {
230
+ const arrow = i === cursor ? `${BLUE}\u276f${RST}` : " ";
231
+ const label = i === cursor ? options[i] : `${DIM}${options[i]}${RST}`;
232
+ w(` ${DIM}\u2502${RST} ${arrow} ${label}`);
233
+ }
234
+ w(BAR);
235
+ w(` ${DIM}\u2502${RST} ${BLUE}${BOLD}[\u2191\u2193]${RST} move ${BLUE}${BOLD}[enter]${RST} confirm`);
236
+ w("");
237
+ }
238
+ function runLoginChoice(name) {
239
+ return new Promise((resolve) => {
240
+ let cursor = 0;
241
+ renderLoginChoice(name, cursor);
242
+ process.stdin.setRawMode(true);
243
+ process.stdin.resume();
244
+ process.stdin.setEncoding("utf-8");
245
+ const cleanup = () => {
246
+ process.stdin.removeListener("data", onData);
247
+ process.stdin.setRawMode(false);
248
+ process.stdin.pause();
249
+ };
250
+ const onData = (key) => {
251
+ if (key === "\x03" || key === "q") {
252
+ cleanup();
253
+ process.stdout.write("\x1b[2J\x1b[H");
254
+ console.log("\n Setup cancelled.\n");
255
+ process.exit(0);
256
+ }
257
+ if (key === "\x1b[A" || key === "k")
258
+ cursor = cursor === 0 ? 1 : 0;
259
+ else if (key === "\x1b[B" || key === "j")
260
+ cursor = cursor === 0 ? 1 : 0;
261
+ else if (key === "\r" || key === "\n") {
262
+ cleanup();
263
+ resolve(cursor === 0); // 0 = keep, 1 = new account
264
+ return;
265
+ }
266
+ renderLoginChoice(name, cursor);
267
+ };
268
+ process.stdin.on("data", onData);
269
+ });
270
+ }
271
+ // Check if already logged in
272
+ let forceNew = false;
273
+ const auth = await getAuthStatus();
274
+ if (auth.loggedIn) {
275
+ const keepAccount = await runLoginChoice(auth.name);
276
+ if (keepAccount) {
277
+ return { status: "already", name: auth.name };
278
+ }
279
+ forceNew = true;
280
+ }
281
+ // Show prompt before opening browser
282
+ process.stdout.write("\x1b[2J\x1b[H");
283
+ renderHeader(mode);
284
+ printBadge();
285
+ w("");
286
+ priorSteps();
287
+ stepActive(`Login to Almanac`);
288
+ w(BAR);
289
+ w(` ${DIM}\u2502${RST} This will open ${WHITE_BOLD}almanac${RST} in your browser`);
290
+ w(` ${DIM}\u2502${RST} to connect your account.`);
291
+ w(BAR);
292
+ await waitForKey(` ${DIM}\u2502${RST} ${BLUE}${BOLD}[enter]${RST} continue`);
293
+ // Show waiting state with cancel/retry hint
294
+ const renderWaiting = () => {
295
+ process.stdout.write("\x1b[2J\x1b[H");
296
+ renderHeader(mode);
297
+ printBadge();
298
+ w("");
299
+ priorSteps();
300
+ stepActive(`Waiting for login\u2026 ${DIM}complete in browser${RST}`);
301
+ w(BAR);
302
+ w(` ${DIM}\u2502${RST} ${BLUE}${BOLD}[r]${RST} retry ${DIM}[q] cancel setup${RST}`);
303
+ w("");
304
+ };
305
+ // Loop: attempt login, let user retry if it fails/times out
306
+ // eslint-disable-next-line no-constant-condition
307
+ while (true) {
308
+ renderWaiting();
309
+ // AbortController so we can kill the HTTP server on retry/cancel
310
+ const controller = new AbortController();
311
+ // Race login against keypress
312
+ const loginPromise = performLogin({ signal: controller.signal, forceNew }).then((result) => result.status === "already_logged_in"
313
+ ? { status: "already", name: result.name }
314
+ : { status: "done" }, () => ({ status: "skipped" }));
315
+ let keyOnData = null;
316
+ const keyPromise = new Promise((resolve) => {
317
+ process.stdin.setRawMode(true);
318
+ process.stdin.resume();
319
+ process.stdin.setEncoding("utf-8");
320
+ keyOnData = (key) => {
321
+ process.stdin.removeListener("data", keyOnData);
322
+ process.stdin.setRawMode(false);
323
+ process.stdin.pause();
324
+ resolve(key);
325
+ };
326
+ process.stdin.on("data", keyOnData);
327
+ });
328
+ const result = await Promise.race([
329
+ loginPromise.then((r) => ({ type: "login", result: r })),
330
+ keyPromise.then((k) => ({ type: "key", key: k })),
331
+ ]);
332
+ // Clean up stdin listener if login won
333
+ if (result.type === "login") {
334
+ if (keyOnData)
335
+ process.stdin.removeListener("data", keyOnData);
336
+ try {
337
+ process.stdin.setRawMode(false);
338
+ }
339
+ catch { /* already off */ }
340
+ process.stdin.pause();
341
+ return result.result;
342
+ }
343
+ // Key won — abort the login HTTP server
344
+ controller.abort();
345
+ // Handle keypress
346
+ if (result.key === "\x03" || result.key === "q") {
347
+ process.stdout.write("\x1b[2J\x1b[H");
348
+ console.log("\n Setup cancelled.\n");
349
+ process.exit(0);
350
+ }
351
+ if (result.key === "r") {
352
+ // Retry — loop continues
353
+ continue;
354
+ }
355
+ }
356
+ }
357
+ const MAX_NAME = Math.max(...TOOL_GROUPS.map((g) => g.name.length));
358
+ function renderToolSelect(selected, cursor, clientsLabel, mcpChanged, mode = "default") {
359
+ process.stdout.write("\x1b[2J\x1b[H");
360
+ renderHeader(mode);
361
+ printBadge();
362
+ w("");
363
+ stepDone(`Clients \u2192 ${WHITE_BOLD}${clientsLabel}${RST}`);
364
+ w(BAR);
365
+ stepDone(`MCP server ${mcpChanged ? "configured" : `${DIM}already configured${RST}`}`);
366
+ w(BAR);
367
+ stepActive(`Select Claude Code tool permissions ${DIM}deselect any you'd rather approve manually${RST}`);
368
+ w(BAR);
369
+ for (let i = 0; i < TOOL_GROUPS.length; i++) {
370
+ const arrow = i === cursor ? `${BLUE}\u276f${RST}` : " ";
371
+ const check = selected[i] ? `${BLUE}\u2713${RST}` : " ";
372
+ const pad = TOOL_GROUPS[i].name.padEnd(MAX_NAME + 2);
373
+ const name = i === cursor ? `${BOLD}${pad}${RST}` : pad;
374
+ const desc = `${DIM}${TOOL_GROUPS[i].description}${RST}`;
375
+ w(` ${DIM}\u2502${RST} ${arrow} [${check}] ${name} ${desc}`);
376
+ }
377
+ w(BAR);
378
+ 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}`);
379
+ w("");
380
+ }
381
+ export function runToolSelect(clientsLabel, mcpChanged, mode = "default") {
382
+ return new Promise((resolve) => {
383
+ const selected = TOOL_GROUPS.map(() => true);
384
+ let cursor = 0;
385
+ renderToolSelect(selected, cursor, clientsLabel, mcpChanged, mode);
386
+ process.stdin.setRawMode(true);
387
+ process.stdin.resume();
388
+ process.stdin.setEncoding("utf-8");
389
+ const cleanup = () => {
390
+ process.stdin.removeListener("data", onData);
391
+ process.stdin.setRawMode(false);
392
+ process.stdin.pause();
393
+ };
394
+ const onData = (key) => {
395
+ if (key === "\x03" || key === "q") {
396
+ cleanup();
397
+ process.stdout.write("\x1b[2J\x1b[H");
398
+ console.log("\n Setup cancelled.\n");
399
+ process.exit(0);
400
+ }
401
+ if (key === "\x1b[A" || key === "k")
402
+ cursor = (cursor - 1 + TOOL_GROUPS.length) % TOOL_GROUPS.length;
403
+ else if (key === "\x1b[B" || key === "j")
404
+ cursor = (cursor + 1) % TOOL_GROUPS.length;
405
+ else if (key === " ")
406
+ selected[cursor] = !selected[cursor];
407
+ else if (key === "a") {
408
+ const all = selected.every(Boolean);
409
+ selected.fill(!all);
410
+ }
411
+ else if (key === "\r" || key === "\n") {
412
+ cleanup();
413
+ const tools = [];
414
+ for (let i = 0; i < TOOL_GROUPS.length; i++) {
415
+ if (selected[i])
416
+ tools.push(...TOOL_GROUPS[i].tools);
417
+ }
418
+ resolve(tools);
419
+ return;
420
+ }
421
+ renderToolSelect(selected, cursor, clientsLabel, mcpChanged, mode);
422
+ };
423
+ process.stdin.on("data", onData);
424
+ });
425
+ }
426
+ /* ── Result screen ──────────────────────────────────────────────── */
427
+ export function printResult(clientsLabel, loginResult, configured, alreadyConfigured, toolCount) {
428
+ process.stdout.write("\x1b[2J\x1b[H");
429
+ printBanner();
430
+ printBadge();
431
+ w("");
432
+ stepDone(`Clients \u2192 ${WHITE_BOLD}${clientsLabel}${RST}`);
433
+ w(BAR);
434
+ stepDone(`Configured \u2192 ${configured.length > 0 ? configured.join(", ") : `${DIM}none${RST}`}`);
435
+ w(BAR);
436
+ stepDone(`Already configured \u2192 ${alreadyConfigured.length > 0 ? alreadyConfigured.join(", ") : `${DIM}none${RST}`}`);
437
+ w(BAR);
438
+ if (toolCount > 0) {
439
+ stepDone(`${BLUE}${toolCount}${RST} tool${toolCount !== 1 ? "s" : ""} allowed in ${DIM}~/.claude/settings.json${RST}`);
440
+ w(BAR);
441
+ }
442
+ stepDone(loginLabel(loginResult));
443
+ w(BAR);
444
+ stepDone(`${BLUE}Setup complete${RST}`);
445
+ w("");
446
+ const nextSteps = getNextSteps(clientsLabel);
447
+ const nextStepLines = nextSteps.flatMap((step, i) => wrapWithPrefixes(step, ` ${BLUE}${i + 1}.${RST} `, " ", 62));
448
+ renderNextStepsBox(nextStepLines);
449
+ }
450
+ function getNextSteps(clientsLabel) {
451
+ const exampleLine = `${BLUE}"${EXAMPLE_PROMPT}"${RST}`;
452
+ if (clientsLabel === "Claude Code") {
453
+ return [
454
+ `Type ${WHITE_BOLD}claude${RST} to start Claude Code`,
455
+ `Ask ${exampleLine}`,
456
+ ];
457
+ }
458
+ if (clientsLabel === "Codex") {
459
+ return [
460
+ `Type ${WHITE_BOLD}codex${RST} to start Codex`,
461
+ `Ask ${exampleLine}`,
462
+ ];
463
+ }
464
+ if (clientsLabel === "Cursor") {
465
+ return [
466
+ `Open ${WHITE_BOLD}Cursor${RST} in your project`,
467
+ `Ask ${exampleLine}`,
468
+ ];
469
+ }
470
+ if (clientsLabel === "OpenCode") {
471
+ return [
472
+ `Type ${WHITE_BOLD}opencode${RST} to start OpenCode`,
473
+ `Ask ${exampleLine}`,
474
+ ];
475
+ }
476
+ if (clientsLabel === "Windsurf") {
477
+ return [
478
+ `Open ${WHITE_BOLD}Windsurf${RST} in your project`,
479
+ `Ask ${exampleLine}`,
480
+ ];
481
+ }
482
+ if (clientsLabel === "Claude Desktop") {
483
+ return [
484
+ `Open ${WHITE_BOLD}Claude Desktop${RST}`,
485
+ `Ask ${exampleLine}`,
486
+ ];
487
+ }
488
+ const formattedClients = clientsLabel.split(", ").join(" / ");
489
+ const projectHint = clientsLabel.includes("Claude Desktop") ? "" : " in this project";
490
+ return [
491
+ `Open ${WHITE_BOLD}${formattedClients}${RST}${projectHint}`,
492
+ `Ask ${exampleLine}`,
493
+ ];
494
+ }
495
+ /* ── Entry point ────────────────────────────────────────────────── */
496
+ export function printRedditResult(agent, loginResult, mcpChanged, toolCount) {
497
+ process.stdout.write("\x1b[2J\x1b[H");
498
+ renderHeader("reddit");
499
+ printBadge();
500
+ w("");
501
+ stepDone(`Agent \u2192 ${WHITE_BOLD}${agent}${RST}`);
502
+ w(BAR);
503
+ stepDone(`MCP server ${mcpChanged ? "configured" : `${DIM}already configured${RST}`}`);
504
+ w(BAR);
505
+ stepDone(`${BLUE}${toolCount}${RST} tool${toolCount !== 1 ? "s" : ""} allowed`);
506
+ w(BAR);
507
+ stepDone(loginLabel(loginResult));
508
+ w(BAR);
509
+ stepDone(`${BLUE}/reddit-wiki${RST} skill installed`);
510
+ w(BAR);
511
+ stepDone(`${BLUE}Setup complete${RST}`);
512
+ w("");
513
+ renderNextStepsBox([
514
+ ` ${BLUE}1.${RST} Type ${WHITE_BOLD}claude${RST} to start Claude Code`,
515
+ ]);
516
+ }
517
+ /* ── Reddit entry point ────────────────────────────────────────── */