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.
- package/dist/auth.d.ts +2 -2
- package/dist/auth.js +2 -2
- package/dist/cli.js +1 -1
- package/dist/instructions.d.ts +1 -0
- package/dist/instructions.js +150 -0
- package/dist/login-core.js +2 -1
- package/dist/onboarding-copy.d.ts +1 -0
- package/dist/onboarding-copy.js +14 -0
- package/dist/openalmanac_mcp-0.3.1-py3-none-any.whl +0 -0
- package/dist/openalmanac_mcp-0.3.1.tar.gz +0 -0
- package/dist/openalmanac_mcp-0.3.2-py3-none-any.whl +0 -0
- package/dist/openalmanac_mcp-0.3.2.tar.gz +0 -0
- package/dist/server.js +5 -150
- package/dist/setup/clients.d.ts +10 -0
- package/dist/setup/clients.js +291 -0
- package/dist/setup/config-files.d.ts +43 -0
- package/dist/setup/config-files.js +257 -0
- package/dist/setup/index.d.ts +2 -0
- package/dist/setup/index.js +55 -0
- package/dist/setup/permissions.d.ts +3 -0
- package/dist/setup/permissions.js +52 -0
- package/dist/{setup.d.ts → setup/reddit.d.ts} +0 -1
- package/dist/setup/reddit.js +69 -0
- package/dist/setup/tui.d.ts +7 -0
- package/dist/setup/tui.js +496 -0
- package/dist/setup/types.d.ts +43 -0
- package/dist/setup/types.js +1 -0
- package/dist/tool-registry.d.ts +11 -0
- package/dist/tool-registry.js +148 -0
- package/dist/tools/auth.js +1 -1
- package/dist/tools/{pages.js → pages/index.js} +39 -202
- package/dist/tools/pages/publish-format.d.ts +48 -0
- package/dist/tools/pages/publish-format.js +92 -0
- package/dist/tools/pages/workspace.d.ts +7 -0
- package/dist/tools/pages/workspace.js +14 -0
- package/dist/tools/pages/writing-guide.d.ts +1 -0
- package/dist/tools/pages/writing-guide.js +56 -0
- package/dist/tools/research.js +16 -15
- package/package.json +15 -6
- package/skills/reddit-wiki/SKILL.md +46 -46
- package/dist/setup.js +0 -1243
- package/dist/validate.d.ts +0 -971
- package/dist/validate.js +0 -154
- /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 ────────────────────────────────────── */
|
|
@@ -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 {};
|