openalmanac 0.3.6 → 0.4.1

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 (44) hide show
  1. package/dist/auth.d.ts +2 -2
  2. package/dist/auth.js +2 -2
  3. package/dist/cli.js +1 -1
  4. package/dist/instructions.d.ts +1 -0
  5. package/dist/instructions.js +150 -0
  6. package/dist/login-core.js +2 -1
  7. package/dist/onboarding-copy.d.ts +1 -0
  8. package/dist/onboarding-copy.js +14 -0
  9. package/dist/openalmanac_mcp-0.3.1-py3-none-any.whl +0 -0
  10. package/dist/openalmanac_mcp-0.3.1.tar.gz +0 -0
  11. package/dist/openalmanac_mcp-0.3.2-py3-none-any.whl +0 -0
  12. package/dist/openalmanac_mcp-0.3.2.tar.gz +0 -0
  13. package/dist/server.js +5 -150
  14. package/dist/setup/clients.d.ts +10 -0
  15. package/dist/setup/clients.js +291 -0
  16. package/dist/setup/config-files.d.ts +43 -0
  17. package/dist/setup/config-files.js +257 -0
  18. package/dist/setup/index.d.ts +2 -0
  19. package/dist/setup/index.js +55 -0
  20. package/dist/setup/permissions.d.ts +3 -0
  21. package/dist/setup/permissions.js +52 -0
  22. package/dist/{setup.d.ts → setup/reddit.d.ts} +0 -1
  23. package/dist/setup/reddit.js +69 -0
  24. package/dist/setup/tui.d.ts +7 -0
  25. package/dist/setup/tui.js +496 -0
  26. package/dist/setup/types.d.ts +43 -0
  27. package/dist/setup/types.js +1 -0
  28. package/dist/tool-registry.d.ts +11 -0
  29. package/dist/tool-registry.js +148 -0
  30. package/dist/tools/auth.js +1 -1
  31. package/dist/tools/{pages.js → pages/index.js} +39 -202
  32. package/dist/tools/pages/publish-format.d.ts +48 -0
  33. package/dist/tools/pages/publish-format.js +92 -0
  34. package/dist/tools/pages/workspace.d.ts +7 -0
  35. package/dist/tools/pages/workspace.js +14 -0
  36. package/dist/tools/pages/writing-guide.d.ts +1 -0
  37. package/dist/tools/pages/writing-guide.js +56 -0
  38. package/dist/tools/research.js +16 -15
  39. package/package.json +15 -6
  40. package/skills/reddit-wiki/SKILL.md +46 -46
  41. package/dist/setup.js +0 -1243
  42. package/dist/validate.d.ts +0 -971
  43. package/dist/validate.js +0 -154
  44. /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,496 @@
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 renderClientSelect(clients, selected, cursor, mode = "default") {
61
+ process.stdout.write("\x1b[2J\x1b[H");
62
+ renderHeader(mode);
63
+ printBadge();
64
+ w("");
65
+ stepActive(`Select where to install Almanac`);
66
+ w(BAR);
67
+ for (let i = 0; i < clients.length; i++) {
68
+ const client = clients[i];
69
+ const arrow = i === cursor ? `${BLUE}\u276f${RST}` : " ";
70
+ const check = selected[i] ? `${BLUE}\u2713${RST}` : " ";
71
+ const label = i === cursor ? `${BOLD}${client.selectionLabel ?? client.name}${RST}` : client.selectionLabel ?? client.name;
72
+ w(` ${DIM}\u2502${RST} ${arrow} [${check}] ${label}`);
73
+ }
74
+ w(BAR);
75
+ 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}`);
76
+ w("");
77
+ }
78
+ export function runClientSelect(clients, mode = "default") {
79
+ return new Promise((resolve) => {
80
+ const selected = clients.map(() => true);
81
+ let cursor = 0;
82
+ renderClientSelect(clients, selected, cursor, mode);
83
+ process.stdin.setRawMode(true);
84
+ process.stdin.resume();
85
+ process.stdin.setEncoding("utf-8");
86
+ const cleanup = () => {
87
+ process.stdin.removeListener("data", onData);
88
+ process.stdin.setRawMode(false);
89
+ process.stdin.pause();
90
+ };
91
+ const onData = (key) => {
92
+ if (key === "\x03" || key === "q") {
93
+ cleanup();
94
+ process.stdout.write("\x1b[2J\x1b[H");
95
+ console.log("\n Setup cancelled.\n");
96
+ process.exit(0);
97
+ }
98
+ if (key === "\x1b[A" || key === "k") {
99
+ cursor = (cursor - 1 + clients.length) % clients.length;
100
+ }
101
+ else if (key === "\x1b[B" || key === "j") {
102
+ cursor = (cursor + 1) % clients.length;
103
+ }
104
+ else if (key === " ") {
105
+ selected[cursor] = !selected[cursor];
106
+ }
107
+ else if (key === "a") {
108
+ const all = selected.every(Boolean);
109
+ selected.fill(!all);
110
+ }
111
+ else if (key === "\r" || key === "\n") {
112
+ cleanup();
113
+ const chosen = clients.filter((_, index) => selected[index]);
114
+ if (chosen.length === 0) {
115
+ console.log("\n Select at least one client.\n");
116
+ process.exit(1);
117
+ }
118
+ resolve(chosen);
119
+ return;
120
+ }
121
+ renderClientSelect(clients, selected, cursor, mode);
122
+ };
123
+ process.stdin.on("data", onData);
124
+ });
125
+ }
126
+ export async function runAgentSelect(mode = "default") {
127
+ const [client] = await runClientSelect([SUPPORTED_CLIENTS["claude-code"]], mode);
128
+ return client.name;
129
+ }
130
+ function loginLabel(result) {
131
+ if (result.status === "already")
132
+ return `Logged in as ${WHITE_BOLD}${result.name}${RST}`;
133
+ if (result.status === "done")
134
+ return `Logged in`;
135
+ return `Login ${DIM}skipped${RST}`;
136
+ }
137
+ function waitForKey(prompt) {
138
+ return new Promise((resolve) => {
139
+ process.stdin.setRawMode(true);
140
+ process.stdin.resume();
141
+ process.stdin.setEncoding("utf-8");
142
+ w(prompt);
143
+ const onData = (key) => {
144
+ process.stdin.removeListener("data", onData);
145
+ process.stdin.setRawMode(false);
146
+ process.stdin.pause();
147
+ if (key === "\x03") {
148
+ process.stdout.write("\x1b[2J\x1b[H");
149
+ console.log("\n Setup cancelled.\n");
150
+ process.exit(0);
151
+ }
152
+ resolve(key);
153
+ };
154
+ process.stdin.on("data", onData);
155
+ });
156
+ }
157
+ export async function runLoginStep(agent, mcpChanged, toolCount, mode = "default") {
158
+ const label = agent.includes(",") ? "Clients" : "Agent";
159
+ const priorSteps = () => {
160
+ stepDone(`${label} \u2192 ${WHITE_BOLD}${agent}${RST}`);
161
+ w(BAR);
162
+ stepDone(`MCP server ${mcpChanged ? "configured" : `${DIM}already configured${RST}`}`);
163
+ if (toolCount !== null) {
164
+ w(BAR);
165
+ stepDone(`${BLUE}${toolCount}${RST} tool${toolCount !== 1 ? "s" : ""} allowed`);
166
+ }
167
+ w(BAR);
168
+ };
169
+ function renderLoginChoice(name, cursor) {
170
+ process.stdout.write("\x1b[2J\x1b[H");
171
+ renderHeader(mode);
172
+ printBadge();
173
+ w("");
174
+ priorSteps();
175
+ stepActive(`Already logged in as ${WHITE_BOLD}${name}${RST}`);
176
+ w(BAR);
177
+ const options = [
178
+ `Continue as ${BOLD}${name}${RST}`,
179
+ `Login with a different account`,
180
+ ];
181
+ for (let i = 0; i < options.length; i++) {
182
+ const arrow = i === cursor ? `${BLUE}\u276f${RST}` : " ";
183
+ const label = i === cursor ? options[i] : `${DIM}${options[i]}${RST}`;
184
+ w(` ${DIM}\u2502${RST} ${arrow} ${label}`);
185
+ }
186
+ w(BAR);
187
+ w(` ${DIM}\u2502${RST} ${BLUE}${BOLD}[\u2191\u2193]${RST} move ${BLUE}${BOLD}[enter]${RST} confirm`);
188
+ w("");
189
+ }
190
+ function runLoginChoice(name) {
191
+ return new Promise((resolve) => {
192
+ let cursor = 0;
193
+ renderLoginChoice(name, cursor);
194
+ process.stdin.setRawMode(true);
195
+ process.stdin.resume();
196
+ process.stdin.setEncoding("utf-8");
197
+ const cleanup = () => {
198
+ process.stdin.removeListener("data", onData);
199
+ process.stdin.setRawMode(false);
200
+ process.stdin.pause();
201
+ };
202
+ const onData = (key) => {
203
+ if (key === "\x03" || key === "q") {
204
+ cleanup();
205
+ process.stdout.write("\x1b[2J\x1b[H");
206
+ console.log("\n Setup cancelled.\n");
207
+ process.exit(0);
208
+ }
209
+ if (key === "\x1b[A" || key === "k")
210
+ cursor = cursor === 0 ? 1 : 0;
211
+ else if (key === "\x1b[B" || key === "j")
212
+ cursor = cursor === 0 ? 1 : 0;
213
+ else if (key === "\r" || key === "\n") {
214
+ cleanup();
215
+ resolve(cursor === 0); // 0 = keep, 1 = new account
216
+ return;
217
+ }
218
+ renderLoginChoice(name, cursor);
219
+ };
220
+ process.stdin.on("data", onData);
221
+ });
222
+ }
223
+ // Check if already logged in
224
+ let forceNew = false;
225
+ const auth = await getAuthStatus();
226
+ if (auth.loggedIn) {
227
+ const keepAccount = await runLoginChoice(auth.name);
228
+ if (keepAccount) {
229
+ return { status: "already", name: auth.name };
230
+ }
231
+ forceNew = true;
232
+ }
233
+ // Show prompt before opening browser
234
+ process.stdout.write("\x1b[2J\x1b[H");
235
+ renderHeader(mode);
236
+ printBadge();
237
+ w("");
238
+ priorSteps();
239
+ stepActive(`Login to Almanac`);
240
+ w(BAR);
241
+ w(` ${DIM}\u2502${RST} This will open ${WHITE_BOLD}almanac${RST} in your browser`);
242
+ w(` ${DIM}\u2502${RST} to connect your account.`);
243
+ w(BAR);
244
+ await waitForKey(` ${DIM}\u2502${RST} ${BLUE}${BOLD}[enter]${RST} continue`);
245
+ // Show waiting state with cancel/retry hint
246
+ const renderWaiting = () => {
247
+ process.stdout.write("\x1b[2J\x1b[H");
248
+ renderHeader(mode);
249
+ printBadge();
250
+ w("");
251
+ priorSteps();
252
+ stepActive(`Waiting for login\u2026 ${DIM}complete in browser${RST}`);
253
+ w(BAR);
254
+ w(` ${DIM}\u2502${RST} ${BLUE}${BOLD}[r]${RST} retry ${DIM}[q] cancel setup${RST}`);
255
+ w("");
256
+ };
257
+ // Loop: attempt login, let user retry if it fails/times out
258
+ // eslint-disable-next-line no-constant-condition
259
+ while (true) {
260
+ renderWaiting();
261
+ // AbortController so we can kill the HTTP server on retry/cancel
262
+ const controller = new AbortController();
263
+ // Race login against keypress
264
+ const loginPromise = performLogin({ signal: controller.signal, forceNew }).then((result) => result.status === "already_logged_in"
265
+ ? { status: "already", name: result.name }
266
+ : { status: "done" }, () => ({ status: "skipped" }));
267
+ let keyOnData = null;
268
+ const keyPromise = new Promise((resolve) => {
269
+ process.stdin.setRawMode(true);
270
+ process.stdin.resume();
271
+ process.stdin.setEncoding("utf-8");
272
+ keyOnData = (key) => {
273
+ process.stdin.removeListener("data", keyOnData);
274
+ process.stdin.setRawMode(false);
275
+ process.stdin.pause();
276
+ resolve(key);
277
+ };
278
+ process.stdin.on("data", keyOnData);
279
+ });
280
+ const result = await Promise.race([
281
+ loginPromise.then((r) => ({ type: "login", result: r })),
282
+ keyPromise.then((k) => ({ type: "key", key: k })),
283
+ ]);
284
+ // Clean up stdin listener if login won
285
+ if (result.type === "login") {
286
+ if (keyOnData)
287
+ process.stdin.removeListener("data", keyOnData);
288
+ try {
289
+ process.stdin.setRawMode(false);
290
+ }
291
+ catch { /* already off */ }
292
+ process.stdin.pause();
293
+ return result.result;
294
+ }
295
+ // Key won — abort the login HTTP server
296
+ controller.abort();
297
+ // Handle keypress
298
+ if (result.key === "\x03" || result.key === "q") {
299
+ process.stdout.write("\x1b[2J\x1b[H");
300
+ console.log("\n Setup cancelled.\n");
301
+ process.exit(0);
302
+ }
303
+ if (result.key === "r") {
304
+ // Retry — loop continues
305
+ continue;
306
+ }
307
+ }
308
+ }
309
+ const MAX_NAME = Math.max(...TOOL_GROUPS.map((g) => g.name.length));
310
+ function renderToolSelect(selected, cursor, clientsLabel, mcpChanged, mode = "default") {
311
+ process.stdout.write("\x1b[2J\x1b[H");
312
+ renderHeader(mode);
313
+ printBadge();
314
+ w("");
315
+ stepDone(`Clients \u2192 ${WHITE_BOLD}${clientsLabel}${RST}`);
316
+ w(BAR);
317
+ stepDone(`MCP server ${mcpChanged ? "configured" : `${DIM}already configured${RST}`}`);
318
+ w(BAR);
319
+ stepActive(`Select Claude Code tool permissions ${DIM}deselect any you'd rather approve manually${RST}`);
320
+ w(BAR);
321
+ for (let i = 0; i < TOOL_GROUPS.length; i++) {
322
+ const arrow = i === cursor ? `${BLUE}\u276f${RST}` : " ";
323
+ const check = selected[i] ? `${BLUE}\u2713${RST}` : " ";
324
+ const pad = TOOL_GROUPS[i].name.padEnd(MAX_NAME + 2);
325
+ const name = i === cursor ? `${BOLD}${pad}${RST}` : pad;
326
+ const desc = `${DIM}${TOOL_GROUPS[i].description}${RST}`;
327
+ w(` ${DIM}\u2502${RST} ${arrow} [${check}] ${name} ${desc}`);
328
+ }
329
+ w(BAR);
330
+ 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}`);
331
+ w("");
332
+ }
333
+ export function runToolSelect(clientsLabel, mcpChanged, mode = "default") {
334
+ return new Promise((resolve) => {
335
+ const selected = TOOL_GROUPS.map(() => true);
336
+ let cursor = 0;
337
+ renderToolSelect(selected, cursor, clientsLabel, mcpChanged, mode);
338
+ process.stdin.setRawMode(true);
339
+ process.stdin.resume();
340
+ process.stdin.setEncoding("utf-8");
341
+ const cleanup = () => {
342
+ process.stdin.removeListener("data", onData);
343
+ process.stdin.setRawMode(false);
344
+ process.stdin.pause();
345
+ };
346
+ const onData = (key) => {
347
+ if (key === "\x03" || key === "q") {
348
+ cleanup();
349
+ process.stdout.write("\x1b[2J\x1b[H");
350
+ console.log("\n Setup cancelled.\n");
351
+ process.exit(0);
352
+ }
353
+ if (key === "\x1b[A" || key === "k")
354
+ cursor = (cursor - 1 + TOOL_GROUPS.length) % TOOL_GROUPS.length;
355
+ else if (key === "\x1b[B" || key === "j")
356
+ cursor = (cursor + 1) % TOOL_GROUPS.length;
357
+ else if (key === " ")
358
+ selected[cursor] = !selected[cursor];
359
+ else if (key === "a") {
360
+ const all = selected.every(Boolean);
361
+ selected.fill(!all);
362
+ }
363
+ else if (key === "\r" || key === "\n") {
364
+ cleanup();
365
+ const tools = [];
366
+ for (let i = 0; i < TOOL_GROUPS.length; i++) {
367
+ if (selected[i])
368
+ tools.push(...TOOL_GROUPS[i].tools);
369
+ }
370
+ resolve(tools);
371
+ return;
372
+ }
373
+ renderToolSelect(selected, cursor, clientsLabel, mcpChanged, mode);
374
+ };
375
+ process.stdin.on("data", onData);
376
+ });
377
+ }
378
+ /* ── Result screen ──────────────────────────────────────────────── */
379
+ export function printResult(clientsLabel, loginResult, configured, alreadyConfigured, toolCount) {
380
+ process.stdout.write("\x1b[2J\x1b[H");
381
+ printBanner();
382
+ printBadge();
383
+ w("");
384
+ stepDone(`Clients \u2192 ${WHITE_BOLD}${clientsLabel}${RST}`);
385
+ w(BAR);
386
+ stepDone(`Configured \u2192 ${configured.length > 0 ? configured.join(", ") : `${DIM}none${RST}`}`);
387
+ w(BAR);
388
+ stepDone(`Already configured \u2192 ${alreadyConfigured.length > 0 ? alreadyConfigured.join(", ") : `${DIM}none${RST}`}`);
389
+ w(BAR);
390
+ if (toolCount > 0) {
391
+ stepDone(`${BLUE}${toolCount}${RST} tool${toolCount !== 1 ? "s" : ""} allowed in ${DIM}~/.claude/settings.json${RST}`);
392
+ w(BAR);
393
+ }
394
+ stepDone(loginLabel(loginResult));
395
+ w(BAR);
396
+ stepDone(`${BLUE}Setup complete${RST}`);
397
+ w("");
398
+ // Next steps box
399
+ const innerW = 62;
400
+ const row = (content) => {
401
+ const padding = Math.max(0, innerW - vis(content));
402
+ return ` ${BLUE_DIM}\u2502${RST}${content}${" ".repeat(padding)}${BLUE_DIM}\u2502${RST}`;
403
+ };
404
+ const empty = row("");
405
+ const nextSteps = getNextSteps(clientsLabel);
406
+ w(` ${BLUE_DIM}\u256d${"─".repeat(innerW)}\u256e${RST}`);
407
+ w(empty);
408
+ w(row(` ${WHITE_BOLD}Next steps${RST}`));
409
+ w(empty);
410
+ for (let i = 0; i < nextSteps.length; i++) {
411
+ w(row(` ${BLUE}${i + 1}.${RST} ${nextSteps[i]}`));
412
+ }
413
+ w(empty);
414
+ w(` ${BLUE_DIM}\u2570${"─".repeat(innerW)}\u256f${RST}`);
415
+ w("");
416
+ }
417
+ function getNextSteps(clientsLabel) {
418
+ const exampleLine = `${BLUE}"${EXAMPLE_PROMPT}"${RST}`;
419
+ if (clientsLabel === "Claude Code") {
420
+ return [
421
+ `Type ${WHITE_BOLD}claude${RST} to start Claude Code`,
422
+ `Ask ${exampleLine}`,
423
+ ];
424
+ }
425
+ if (clientsLabel === "Codex") {
426
+ return [
427
+ `Type ${WHITE_BOLD}codex${RST} to start Codex`,
428
+ `Ask ${exampleLine}`,
429
+ ];
430
+ }
431
+ if (clientsLabel === "Cursor") {
432
+ return [
433
+ `Open ${WHITE_BOLD}Cursor${RST} in your project`,
434
+ `Ask ${exampleLine}`,
435
+ ];
436
+ }
437
+ if (clientsLabel === "OpenCode") {
438
+ return [
439
+ `Type ${WHITE_BOLD}opencode${RST} to start OpenCode`,
440
+ `Ask ${exampleLine}`,
441
+ ];
442
+ }
443
+ if (clientsLabel === "Windsurf") {
444
+ return [
445
+ `Open ${WHITE_BOLD}Windsurf${RST} in your project`,
446
+ `Ask ${exampleLine}`,
447
+ ];
448
+ }
449
+ if (clientsLabel === "Claude Desktop") {
450
+ return [
451
+ `Open ${WHITE_BOLD}Claude Desktop${RST}`,
452
+ `Ask ${exampleLine}`,
453
+ ];
454
+ }
455
+ const formattedClients = clientsLabel.split(", ").join(" / ");
456
+ const projectHint = clientsLabel.includes("Claude Desktop") ? "" : " in this project";
457
+ return [
458
+ `Open ${WHITE_BOLD}${formattedClients}${RST}${projectHint}`,
459
+ `Ask ${exampleLine}`,
460
+ ];
461
+ }
462
+ /* ── Entry point ────────────────────────────────────────────────── */
463
+ export function printRedditResult(agent, loginResult, mcpChanged, toolCount) {
464
+ process.stdout.write("\x1b[2J\x1b[H");
465
+ renderHeader("reddit");
466
+ printBadge();
467
+ w("");
468
+ stepDone(`Agent \u2192 ${WHITE_BOLD}${agent}${RST}`);
469
+ w(BAR);
470
+ stepDone(`MCP server ${mcpChanged ? "configured" : `${DIM}already configured${RST}`}`);
471
+ w(BAR);
472
+ stepDone(`${BLUE}${toolCount}${RST} tool${toolCount !== 1 ? "s" : ""} allowed`);
473
+ w(BAR);
474
+ stepDone(loginLabel(loginResult));
475
+ w(BAR);
476
+ stepDone(`${BLUE}/reddit-wiki${RST} skill installed`);
477
+ w(BAR);
478
+ stepDone(`${BLUE}Setup complete${RST}`);
479
+ w("");
480
+ // Next steps box
481
+ const innerW = 62;
482
+ const row = (content) => {
483
+ const padding = Math.max(0, innerW - vis(content));
484
+ return ` ${BLUE_DIM}\u2502${RST}${content}${" ".repeat(padding)}${BLUE_DIM}\u2502${RST}`;
485
+ };
486
+ const empty = row("");
487
+ w(` ${BLUE_DIM}\u256d${"─".repeat(innerW)}\u256e${RST}`);
488
+ w(empty);
489
+ w(row(` ${WHITE_BOLD}Next steps${RST}`));
490
+ w(empty);
491
+ w(row(` ${BLUE}1.${RST} Type ${WHITE_BOLD}claude${RST} to start Claude Code`));
492
+ w(empty);
493
+ w(` ${BLUE_DIM}\u2570${"─".repeat(innerW)}\u256f${RST}`);
494
+ w("");
495
+ }
496
+ /* ── Reddit entry point ────────────────────────────────────────── */
@@ -0,0 +1,43 @@
1
+ export interface ToolGroup {
2
+ name: string;
3
+ description: string;
4
+ tools: string[];
5
+ }
6
+ export type SetupMode = "default" | "reddit";
7
+ export type ClientId = "claude-code" | "claude-desktop" | "codex" | "cursor" | "opencode" | "windsurf";
8
+ export interface SetupOptions {
9
+ all: boolean;
10
+ clients: ClientId[];
11
+ dryRun: boolean;
12
+ print: boolean;
13
+ yes: boolean;
14
+ }
15
+ export interface SetupClient {
16
+ id: ClientId;
17
+ name: string;
18
+ detect: () => boolean;
19
+ configure: (mode: ConfigureMode) => ClientConfigureResult;
20
+ supportsPermissions?: boolean;
21
+ selectionLabel?: string;
22
+ }
23
+ export interface ClientConfigureResult {
24
+ changed: boolean;
25
+ snippets: ConfigSnippet[];
26
+ }
27
+ export interface ConfigSnippet {
28
+ path: string;
29
+ content: string;
30
+ }
31
+ export type ConfigureMode = "apply" | "dry-run" | "print";
32
+ export interface SetupRunSummary {
33
+ configured: string[];
34
+ alreadyConfigured: string[];
35
+ }
36
+ export type LoginStepResult = {
37
+ status: "already";
38
+ name: string;
39
+ } | {
40
+ status: "done";
41
+ } | {
42
+ status: "skipped";
43
+ };
@@ -0,0 +1 @@
1
+ export {};