openalmanac 0.4.0 → 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 (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 +496 -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
package/dist/setup.js DELETED
@@ -1,1216 +0,0 @@
1
- import { readFileSync, writeFileSync, mkdirSync, existsSync, cpSync } from "fs";
2
- import { homedir } from "os";
3
- import { join, dirname } from "path";
4
- import { fileURLToPath } from "url";
5
- import { spawnSync } from "child_process";
6
- import { performLogin } from "./login-core.js";
7
- import { getAuthStatus } from "./auth.js";
8
- import { MCP_TOOL_GROUPS, toClaudePermissionName, } from "./tool-registry.js";
9
- import { EXAMPLE_PROMPT } from "./onboarding-copy.js";
10
- // MCP-side permission groups come from the shared tool registry so adding a
11
- // new MCP tool can never silently leave it un-grouped — see
12
- // `src/tool-registry.ts` for the contract and `test/tool-registry.test.ts`
13
- // for the drift check that enforces it.
14
- function mcpGroupToPermissionGroup(group) {
15
- return {
16
- name: group.name,
17
- description: group.description,
18
- tools: group.tools.map(toClaudePermissionName),
19
- };
20
- }
21
- // Built-in Claude Code tool groups — not MCP tools, so they stay defined
22
- // here. The user opts into them in the same TUI checkbox screen.
23
- const CLAUDE_BUILTIN_TOOL_GROUPS = [
24
- {
25
- name: "Local Files",
26
- description: "read & edit pages in ~/.openalmanac",
27
- tools: [
28
- "Read(~/.openalmanac/**)",
29
- "Write(~/.openalmanac/**)",
30
- "Edit(~/.openalmanac/**)",
31
- ],
32
- },
33
- {
34
- name: "Web Access",
35
- description: "web search & fetch used during research",
36
- tools: ["WebSearch", "WebFetch"],
37
- },
38
- ];
39
- const TOOL_GROUPS = [
40
- ...MCP_TOOL_GROUPS.map(mcpGroupToPermissionGroup),
41
- ...CLAUDE_BUILTIN_TOOL_GROUPS,
42
- ];
43
- const AGENTS = [
44
- { name: "Claude Code", supported: true },
45
- { name: "Codex", supported: false },
46
- { name: "Cursor", supported: false },
47
- { name: "Windsurf", supported: false },
48
- ];
49
- /* ── ANSI helpers ───────────────────────────────────────────────── */
50
- const RST = "\x1b[0m";
51
- const BOLD = "\x1b[1m";
52
- const DIM = "\x1b[2m";
53
- const WHITE_BOLD = "\x1b[1;37m";
54
- const BLUE = "\x1b[38;5;75m"; // blue for accents
55
- const BLUE_DIM = "\x1b[38;5;69m"; // slightly deeper blue for boxes
56
- // Interactive TUI accent
57
- const ACCENT = "\x1b[38;5;252m"; // silver
58
- const ACCENT_BG = "\x1b[48;5;252m\x1b[38;5;16m"; // badge: silver bg, black text
59
- // Banner gradient: white → silver
60
- const GRADIENT = [
61
- "\x1b[38;5;255m",
62
- "\x1b[38;5;253m",
63
- "\x1b[38;5;251m",
64
- "\x1b[38;5;249m",
65
- "\x1b[38;5;246m",
66
- "\x1b[38;5;243m",
67
- ];
68
- /* ── ASCII banner ───────────────────────────────────────────────── */
69
- const LOGO_LINES = [
70
- " \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",
71
- "\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",
72
- "\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 ",
73
- "\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 ",
74
- "\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",
75
- "\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",
76
- ];
77
- function printBanner(subtitle = "Write and publish pages with your AI agent") {
78
- process.stdout.write("\n");
79
- for (let i = 0; i < LOGO_LINES.length; i++) {
80
- process.stdout.write(`${GRADIENT[i]}${LOGO_LINES[i]}${RST}\n`);
81
- }
82
- process.stdout.write(`\n${WHITE_BOLD} ${subtitle}${RST}\n`);
83
- }
84
- function renderHeader(mode = "default") {
85
- printBanner(mode === "reddit"
86
- ? "Turn any subreddit into a published wiki"
87
- : "Write and publish pages with your AI agent");
88
- }
89
- function printBadge() {
90
- process.stdout.write(`\n ${ACCENT_BG} almanac ${RST}\n`);
91
- }
92
- /* ── Step indicators ────────────────────────────────────────────── */
93
- const BAR = ` ${DIM}\u2502${RST}`;
94
- function stepDone(msg) {
95
- process.stdout.write(` ${BLUE}\u25c7${RST} ${msg}\n`);
96
- }
97
- function stepActive(msg) {
98
- process.stdout.write(` ${BLUE}\u25c6${RST} ${msg}\n`);
99
- }
100
- /* ── Helpers ────────────────────────────────────────────────────── */
101
- // Strip ANSI codes to measure visible length
102
- const vis = (s) => s.replace(/\x1b\[[0-9;]*m/g, "").length;
103
- const w = (s) => process.stdout.write(s + "\n");
104
- /* ── File helpers ───────────────────────────────────────────────── */
105
- const CLAUDE_DIR = join(homedir(), ".claude");
106
- const CLAUDE_JSON = join(homedir(), ".claude.json"); // Claude Code user-scoped MCP config
107
- const CLAUDE_CODE_MCP = join(CLAUDE_DIR, "mcp.json"); // Claude Code local MCP config
108
- const SETTINGS_JSON = join(CLAUDE_DIR, "settings.json");
109
- const CODEX_CONFIG = join(homedir(), ".codex", "config.toml");
110
- const CURSOR_MCP_JSON = join(homedir(), ".cursor", "mcp.json");
111
- const OPENCODE_DIR = join(homedir(), ".config", "opencode");
112
- const OPENCODE_JSON = join(OPENCODE_DIR, "opencode.json");
113
- const OPENCODE_JSONC = join(OPENCODE_DIR, "opencode.jsonc");
114
- const WINDSURF_MCP_JSON = join(homedir(), ".codeium", "mcp_config.json");
115
- function ensureDir(dir) {
116
- if (!existsSync(dir))
117
- mkdirSync(dir, { recursive: true });
118
- }
119
- function readJson(path) {
120
- try {
121
- return JSON.parse(readFileSync(path, "utf-8"));
122
- }
123
- catch {
124
- return {};
125
- }
126
- }
127
- function writeJson(path, data) {
128
- writeFileSync(path, JSON.stringify(data, null, 2) + "\n");
129
- }
130
- /* ── Step 1 — MCP server ───────────────────────────────────────── */
131
- const ALMANAC_MCP_ENTRY = { command: "npx", args: ["-y", "openalmanac@latest"] };
132
- const SUPPORTED_CLIENT_IDS = [
133
- "claude-code",
134
- "claude-desktop",
135
- "codex",
136
- "cursor",
137
- "opencode",
138
- "windsurf",
139
- ];
140
- const SUPPORTED_CLIENTS = {
141
- "claude-code": {
142
- id: "claude-code",
143
- name: "Claude Code",
144
- selectionLabel: "Claude Code",
145
- detect: () => hasCommand("claude") || existsSync(CLAUDE_JSON) || existsSync(CLAUDE_DIR),
146
- configure: (mode) => {
147
- const snippets = [
148
- {
149
- path: CLAUDE_JSON,
150
- content: jsonSnippet({
151
- mcpServers: {
152
- almanac: { ...ALMANAC_MCP_ENTRY },
153
- },
154
- }),
155
- },
156
- {
157
- path: CLAUDE_CODE_MCP,
158
- content: jsonSnippet({
159
- mcpServers: {
160
- almanac: { ...ALMANAC_MCP_ENTRY },
161
- },
162
- }),
163
- },
164
- ];
165
- const changedPrimary = configureJsonMcpFile(CLAUDE_JSON, mode);
166
- const changedSecondary = configureJsonMcpFile(CLAUDE_CODE_MCP, mode);
167
- return { changed: changedPrimary || changedSecondary, snippets };
168
- },
169
- supportsPermissions: true,
170
- },
171
- "claude-desktop": {
172
- id: "claude-desktop",
173
- name: "Claude Desktop",
174
- selectionLabel: "Claude Desktop",
175
- detect: () => {
176
- const path = getClaudeDesktopConfigPath();
177
- return Boolean(path && (existsSync(path) || isClaudeDesktopInstalled()));
178
- },
179
- configure: (mode) => {
180
- const path = getClaudeDesktopConfigPath();
181
- if (!path)
182
- return { changed: false, snippets: [] };
183
- return {
184
- changed: configureJsonMcpFile(path, mode),
185
- snippets: [
186
- {
187
- path,
188
- content: jsonSnippet({
189
- mcpServers: {
190
- almanac: { ...ALMANAC_MCP_ENTRY },
191
- },
192
- }),
193
- },
194
- ],
195
- };
196
- },
197
- },
198
- codex: {
199
- id: "codex",
200
- name: "Codex",
201
- selectionLabel: "Codex",
202
- detect: () => hasCommand("codex") || existsSync(CODEX_CONFIG) || existsSync(join(homedir(), ".codex")),
203
- configure: (mode) => ({
204
- changed: configureCodexToml(CODEX_CONFIG, mode),
205
- snippets: [
206
- {
207
- path: CODEX_CONFIG,
208
- content: codexSnippet(),
209
- },
210
- ],
211
- }),
212
- },
213
- cursor: {
214
- id: "cursor",
215
- name: "Cursor",
216
- selectionLabel: "Cursor",
217
- detect: () => hasCommand("cursor-agent") ||
218
- existsSync(CURSOR_MCP_JSON) ||
219
- existsSync(join(homedir(), ".cursor")),
220
- configure: (mode) => ({
221
- changed: configureJsonMcpFile(CURSOR_MCP_JSON, mode),
222
- snippets: [
223
- {
224
- path: CURSOR_MCP_JSON,
225
- content: jsonSnippet({
226
- mcpServers: {
227
- almanac: { ...ALMANAC_MCP_ENTRY },
228
- },
229
- }),
230
- },
231
- ],
232
- }),
233
- },
234
- opencode: {
235
- id: "opencode",
236
- name: "OpenCode",
237
- selectionLabel: "OpenCode",
238
- detect: () => hasCommand("opencode") ||
239
- existsSync(OPENCODE_JSON) ||
240
- existsSync(OPENCODE_JSONC) ||
241
- existsSync(OPENCODE_DIR),
242
- configure: (mode) => {
243
- const path = getOpenCodeConfigPath();
244
- return {
245
- changed: configureOpenCodeFile(path, mode),
246
- snippets: [
247
- {
248
- path,
249
- content: jsonSnippet({
250
- "$schema": "https://opencode.ai/config.json",
251
- mcp: {
252
- almanac: {
253
- type: "local",
254
- command: [ALMANAC_MCP_ENTRY.command, ...ALMANAC_MCP_ENTRY.args],
255
- enabled: true,
256
- },
257
- },
258
- }),
259
- },
260
- ],
261
- };
262
- },
263
- },
264
- windsurf: {
265
- id: "windsurf",
266
- name: "Windsurf",
267
- selectionLabel: "Windsurf",
268
- detect: () => hasCommand("windsurf") ||
269
- existsSync(WINDSURF_MCP_JSON) ||
270
- existsSync(join(homedir(), ".codeium")),
271
- configure: (mode) => ({
272
- changed: configureJsonMcpFile(WINDSURF_MCP_JSON, mode),
273
- snippets: [
274
- {
275
- path: WINDSURF_MCP_JSON,
276
- content: jsonSnippet({
277
- mcpServers: {
278
- almanac: { ...ALMANAC_MCP_ENTRY },
279
- },
280
- }),
281
- },
282
- ],
283
- }),
284
- },
285
- };
286
- function parseSetupArgs(argv) {
287
- const options = {
288
- all: false,
289
- clients: [],
290
- dryRun: false,
291
- print: false,
292
- yes: false,
293
- };
294
- for (let i = 0; i < argv.length; i++) {
295
- const arg = argv[i];
296
- if (arg === "--all") {
297
- options.all = true;
298
- continue;
299
- }
300
- if (arg === "--print") {
301
- options.print = true;
302
- continue;
303
- }
304
- if (arg === "--dry-run") {
305
- options.dryRun = true;
306
- continue;
307
- }
308
- if (arg === "--yes" || arg === "-y") {
309
- options.yes = true;
310
- continue;
311
- }
312
- if (arg === "--client") {
313
- const value = argv[i + 1];
314
- if (!value) {
315
- throw new Error("Missing value for --client");
316
- }
317
- i++;
318
- options.clients.push(...parseClientList(value));
319
- continue;
320
- }
321
- if (arg.startsWith("--client=")) {
322
- options.clients.push(...parseClientList(arg.slice("--client=".length)));
323
- continue;
324
- }
325
- throw new Error(`Unknown setup flag: ${arg}`);
326
- }
327
- if (options.all && options.clients.length > 0) {
328
- throw new Error("Use either --all or --client, not both");
329
- }
330
- options.clients = Array.from(new Set(options.clients));
331
- return options;
332
- }
333
- function parseClientList(value) {
334
- return value
335
- .split(",")
336
- .map((part) => part.trim().toLowerCase())
337
- .filter(Boolean)
338
- .map((part) => normalizeClientId(part));
339
- }
340
- function normalizeClientId(value) {
341
- const aliases = {
342
- claude: "claude-code",
343
- "claude-code": "claude-code",
344
- "claude-desktop": "claude-desktop",
345
- desktop: "claude-desktop",
346
- codex: "codex",
347
- cursor: "cursor",
348
- opencode: "opencode",
349
- windsurf: "windsurf",
350
- };
351
- const normalized = aliases[value];
352
- if (!normalized) {
353
- throw new Error(`Unsupported client "${value}". Supported clients: ${SUPPORTED_CLIENT_IDS.join(", ")}`);
354
- }
355
- return normalized;
356
- }
357
- function hasCommand(command) {
358
- const checker = process.platform === "win32" ? "where" : "which";
359
- const result = spawnSync(checker, [command], { stdio: "ignore" });
360
- return result.status === 0;
361
- }
362
- function getClaudeDesktopConfigPath() {
363
- if (process.platform === "darwin") {
364
- return join(homedir(), "Library", "Application Support", "Claude", "claude_desktop_config.json");
365
- }
366
- if (process.platform === "linux") {
367
- return join(homedir(), ".config", "Claude", "claude_desktop_config.json");
368
- }
369
- if (process.platform === "win32") {
370
- const appData = process.env.APPDATA;
371
- if (!appData)
372
- return null;
373
- return join(appData, "Claude", "claude_desktop_config.json");
374
- }
375
- return null;
376
- }
377
- function isClaudeDesktopInstalled() {
378
- if (process.platform === "darwin") {
379
- return (existsSync("/Applications/Claude.app") ||
380
- existsSync(join(homedir(), "Applications", "Claude.app")));
381
- }
382
- if (process.platform === "linux") {
383
- return (existsSync("/usr/share/applications/claude.desktop") ||
384
- existsSync(join(homedir(), ".local", "share", "applications", "claude.desktop")) ||
385
- existsSync("/opt/Claude/claude"));
386
- }
387
- if (process.platform === "win32") {
388
- const localAppData = process.env.LOCALAPPDATA;
389
- const programFiles = process.env.ProgramFiles;
390
- return Boolean((localAppData &&
391
- existsSync(join(localAppData, "Programs", "Claude", "Claude.exe"))) ||
392
- (programFiles && existsSync(join(programFiles, "Claude", "Claude.exe"))));
393
- }
394
- return false;
395
- }
396
- function jsonSnippet(data) {
397
- return JSON.stringify(data, null, 2);
398
- }
399
- function getOpenCodeConfigPath() {
400
- if (existsSync(OPENCODE_JSON))
401
- return OPENCODE_JSON;
402
- if (existsSync(OPENCODE_JSONC))
403
- return OPENCODE_JSONC;
404
- return OPENCODE_JSON;
405
- }
406
- function codexSnippet() {
407
- return [
408
- "[mcp_servers.almanac]",
409
- `command = ${tomlString(ALMANAC_MCP_ENTRY.command)}`,
410
- `args = ${tomlArray(ALMANAC_MCP_ENTRY.args)}`,
411
- ].join("\n");
412
- }
413
- function isAlmanacCurrent(server) {
414
- return (server?.command === "npx" &&
415
- JSON.stringify(server.args) === JSON.stringify(ALMANAC_MCP_ENTRY.args));
416
- }
417
- function isOpenCodeAlmanacCurrent(entry) {
418
- return (entry?.type === "local" &&
419
- entry?.enabled === true &&
420
- JSON.stringify(entry.command) ===
421
- JSON.stringify([ALMANAC_MCP_ENTRY.command, ...ALMANAC_MCP_ENTRY.args]));
422
- }
423
- function configureJsonMcpFile(path, mode) {
424
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
425
- const json = readJson(path);
426
- if (!json.mcpServers)
427
- json.mcpServers = {};
428
- if (isAlmanacCurrent(json.mcpServers.almanac)) {
429
- return false;
430
- }
431
- if (mode === "apply") {
432
- ensureDir(dirname(path));
433
- json.mcpServers.almanac = { ...ALMANAC_MCP_ENTRY };
434
- writeJson(path, json);
435
- }
436
- return true;
437
- }
438
- function configureCodexToml(path, mode) {
439
- const current = readToml(path);
440
- const next = upsertCodexServer(current);
441
- if (current.trim() === next.trim()) {
442
- return false;
443
- }
444
- if (mode === "apply") {
445
- ensureDir(dirname(path));
446
- writeFileSync(path, next.endsWith("\n") ? next : next + "\n");
447
- }
448
- return true;
449
- }
450
- function configureOpenCodeFile(path, mode) {
451
- const current = readJsonWithComments(path);
452
- if (!current.$schema) {
453
- current.$schema = "https://opencode.ai/config.json";
454
- }
455
- const mcp = isRecord(current.mcp) ? current.mcp : {};
456
- if (!isRecord(current.mcp)) {
457
- current.mcp = mcp;
458
- }
459
- if (isOpenCodeAlmanacCurrent(mcp.almanac)) {
460
- return false;
461
- }
462
- mcp.almanac = {
463
- type: "local",
464
- command: [ALMANAC_MCP_ENTRY.command, ...ALMANAC_MCP_ENTRY.args],
465
- enabled: true,
466
- };
467
- if (mode === "apply") {
468
- ensureDir(dirname(path));
469
- writeJson(path, current);
470
- }
471
- return true;
472
- }
473
- function readToml(path) {
474
- try {
475
- return readFileSync(path, "utf-8");
476
- }
477
- catch {
478
- return "";
479
- }
480
- }
481
- function readJsonWithComments(path) {
482
- try {
483
- const raw = readFileSync(path, "utf-8");
484
- const parsed = JSON.parse(stripJsonComments(raw));
485
- return isRecord(parsed) ? parsed : {};
486
- }
487
- catch {
488
- return {};
489
- }
490
- }
491
- function stripJsonComments(input) {
492
- let result = "";
493
- let inString = false;
494
- let inLineComment = false;
495
- let inBlockComment = false;
496
- let escaped = false;
497
- for (let i = 0; i < input.length; i++) {
498
- const char = input[i];
499
- const next = input[i + 1];
500
- if (inLineComment) {
501
- if (char === "\n") {
502
- inLineComment = false;
503
- result += char;
504
- }
505
- continue;
506
- }
507
- if (inBlockComment) {
508
- if (char === "*" && next === "/") {
509
- inBlockComment = false;
510
- i++;
511
- }
512
- continue;
513
- }
514
- if (inString) {
515
- result += char;
516
- if (escaped) {
517
- escaped = false;
518
- }
519
- else if (char === "\\") {
520
- escaped = true;
521
- }
522
- else if (char === "\"") {
523
- inString = false;
524
- }
525
- continue;
526
- }
527
- if (char === "/" && next === "/") {
528
- inLineComment = true;
529
- i++;
530
- continue;
531
- }
532
- if (char === "/" && next === "*") {
533
- inBlockComment = true;
534
- i++;
535
- continue;
536
- }
537
- if (char === "\"") {
538
- inString = true;
539
- }
540
- result += char;
541
- }
542
- return result;
543
- }
544
- function isRecord(value) {
545
- return typeof value === "object" && value !== null && !Array.isArray(value);
546
- }
547
- function upsertCodexServer(content) {
548
- const sectionName = "mcp_servers.almanac";
549
- const header = `[${sectionName}]`;
550
- const nextBody = [
551
- `command = ${tomlString(ALMANAC_MCP_ENTRY.command)}`,
552
- `args = ${tomlArray(ALMANAC_MCP_ENTRY.args)}`,
553
- ];
554
- const lines = content === "" ? [] : content.split(/\r?\n/);
555
- const start = lines.findIndex((line) => line.trim() === header);
556
- if (start === -1) {
557
- const prefix = content.trimEnd();
558
- const block = [header, ...nextBody].join("\n");
559
- return prefix === "" ? block + "\n" : `${prefix}\n\n${block}\n`;
560
- }
561
- let end = lines.length;
562
- for (let i = start + 1; i < lines.length; i++) {
563
- if (/^\s*\[.+\]\s*$/.test(lines[i])) {
564
- end = i;
565
- break;
566
- }
567
- }
568
- const existingBody = lines.slice(start + 1, end);
569
- const preserved = existingBody.filter((line) => {
570
- const trimmed = line.trim();
571
- return !trimmed.startsWith("command =") && !trimmed.startsWith("args =");
572
- });
573
- const replacement = [header, ...nextBody, ...preserved];
574
- const updated = [...lines.slice(0, start), ...replacement, ...lines.slice(end)];
575
- return updated.join("\n").replace(/\n{3,}/g, "\n\n").replace(/\s+$/, "") + "\n";
576
- }
577
- function tomlString(value) {
578
- return JSON.stringify(value);
579
- }
580
- function tomlArray(values) {
581
- return `[${values.map((value) => tomlString(value)).join(", ")}]`;
582
- }
583
- function detectClients() {
584
- return SUPPORTED_CLIENT_IDS.map((id) => SUPPORTED_CLIENTS[id]).filter((client) => client.detect());
585
- }
586
- function resolveClients(options) {
587
- if (options.clients.length > 0) {
588
- return options.clients.map((id) => SUPPORTED_CLIENTS[id]);
589
- }
590
- const detected = detectClients();
591
- if (options.all)
592
- return detected;
593
- return detected;
594
- }
595
- function applyClientSetup(clients, mode) {
596
- const configured = [];
597
- const alreadyConfigured = [];
598
- for (const client of clients) {
599
- const result = client.configure(mode);
600
- if (result.changed) {
601
- configured.push(client.name);
602
- }
603
- else {
604
- alreadyConfigured.push(client.name);
605
- }
606
- }
607
- return { configured, alreadyConfigured };
608
- }
609
- function printSetupPlan(clients, options) {
610
- const heading = options.dryRun ? "Dry run" : "OpenAlmanac MCP setup";
611
- process.stdout.write(`${heading}\n\n`);
612
- if (clients.length === 0) {
613
- process.stdout.write("No supported clients detected. Use --client <name> to force a target or --print to inspect supported snippets.\n");
614
- if (options.print) {
615
- process.stdout.write("\nSupported clients:\n");
616
- for (const id of SUPPORTED_CLIENT_IDS) {
617
- process.stdout.write(`- ${SUPPORTED_CLIENTS[id].name}\n`);
618
- }
619
- }
620
- return;
621
- }
622
- const mode = options.print ? "print" : "dry-run";
623
- for (const client of clients) {
624
- const result = client.configure(mode);
625
- const status = result.changed
626
- ? options.print
627
- ? "snippet"
628
- : "would configure"
629
- : "already configured";
630
- process.stdout.write(`- ${client.name}: ${status}\n`);
631
- if (options.print) {
632
- for (const snippet of result.snippets) {
633
- process.stdout.write(` Path: ${snippet.path}\n`);
634
- process.stdout.write(`${indentBlock(snippet.content, " ")}\n`);
635
- }
636
- }
637
- }
638
- }
639
- function indentBlock(content, prefix) {
640
- return content
641
- .split("\n")
642
- .map((line) => `${prefix}${line}`)
643
- .join("\n");
644
- }
645
- /* ── Step 2 — Permissions ──────────────────────────────────────── */
646
- function configurePermissions(tools) {
647
- ensureDir(CLAUDE_DIR);
648
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
649
- const settings = readJson(SETTINGS_JSON);
650
- if (!settings.permissions)
651
- settings.permissions = {};
652
- if (!Array.isArray(settings.permissions.allow))
653
- settings.permissions.allow = [];
654
- const existing = new Set(settings.permissions.allow);
655
- let added = 0;
656
- for (const t of tools) {
657
- if (!existing.has(t)) {
658
- settings.permissions.allow.push(t);
659
- added++;
660
- }
661
- }
662
- if (added > 0)
663
- writeJson(SETTINGS_JSON, settings);
664
- return tools.length;
665
- }
666
- /* ── Client selection screen ────────────────────────────────────── */
667
- function renderClientSelect(clients, selected, cursor, mode = "default") {
668
- process.stdout.write("\x1b[2J\x1b[H");
669
- renderHeader(mode);
670
- printBadge();
671
- w("");
672
- stepActive(`Select where to install Almanac`);
673
- w(BAR);
674
- for (let i = 0; i < clients.length; i++) {
675
- const client = clients[i];
676
- const arrow = i === cursor ? `${BLUE}\u276f${RST}` : " ";
677
- const check = selected[i] ? `${BLUE}\u2713${RST}` : " ";
678
- const label = i === cursor ? `${BOLD}${client.selectionLabel ?? client.name}${RST}` : client.selectionLabel ?? client.name;
679
- w(` ${DIM}\u2502${RST} ${arrow} [${check}] ${label}`);
680
- }
681
- w(BAR);
682
- 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}`);
683
- w("");
684
- }
685
- function runClientSelect(clients, mode = "default") {
686
- return new Promise((resolve) => {
687
- const selected = clients.map(() => true);
688
- let cursor = 0;
689
- renderClientSelect(clients, selected, cursor, mode);
690
- process.stdin.setRawMode(true);
691
- process.stdin.resume();
692
- process.stdin.setEncoding("utf-8");
693
- const cleanup = () => {
694
- process.stdin.removeListener("data", onData);
695
- process.stdin.setRawMode(false);
696
- process.stdin.pause();
697
- };
698
- const onData = (key) => {
699
- if (key === "\x03" || key === "q") {
700
- cleanup();
701
- process.stdout.write("\x1b[2J\x1b[H");
702
- console.log("\n Setup cancelled.\n");
703
- process.exit(0);
704
- }
705
- if (key === "\x1b[A" || key === "k") {
706
- cursor = (cursor - 1 + clients.length) % clients.length;
707
- }
708
- else if (key === "\x1b[B" || key === "j") {
709
- cursor = (cursor + 1) % clients.length;
710
- }
711
- else if (key === " ") {
712
- selected[cursor] = !selected[cursor];
713
- }
714
- else if (key === "a") {
715
- const all = selected.every(Boolean);
716
- selected.fill(!all);
717
- }
718
- else if (key === "\r" || key === "\n") {
719
- cleanup();
720
- const chosen = clients.filter((_, index) => selected[index]);
721
- if (chosen.length === 0) {
722
- console.log("\n Select at least one client.\n");
723
- process.exit(1);
724
- }
725
- resolve(chosen);
726
- return;
727
- }
728
- renderClientSelect(clients, selected, cursor, mode);
729
- };
730
- process.stdin.on("data", onData);
731
- });
732
- }
733
- async function runAgentSelect(mode = "default") {
734
- const [client] = await runClientSelect([SUPPORTED_CLIENTS["claude-code"]], mode);
735
- return client.name;
736
- }
737
- /* ── Login step ─────────────────────────────────────────────────── */
738
- function loginLabel(result) {
739
- if (result.status === "already")
740
- return `Logged in as ${WHITE_BOLD}${result.name}${RST}`;
741
- if (result.status === "done")
742
- return `Logged in`;
743
- return `Login ${DIM}skipped${RST}`;
744
- }
745
- function waitForKey(prompt) {
746
- return new Promise((resolve) => {
747
- process.stdin.setRawMode(true);
748
- process.stdin.resume();
749
- process.stdin.setEncoding("utf-8");
750
- w(prompt);
751
- const onData = (key) => {
752
- process.stdin.removeListener("data", onData);
753
- process.stdin.setRawMode(false);
754
- process.stdin.pause();
755
- if (key === "\x03") {
756
- process.stdout.write("\x1b[2J\x1b[H");
757
- console.log("\n Setup cancelled.\n");
758
- process.exit(0);
759
- }
760
- resolve(key);
761
- };
762
- process.stdin.on("data", onData);
763
- });
764
- }
765
- async function runLoginStep(agent, mcpChanged, toolCount, mode = "default") {
766
- const label = agent.includes(",") ? "Clients" : "Agent";
767
- const priorSteps = () => {
768
- stepDone(`${label} \u2192 ${WHITE_BOLD}${agent}${RST}`);
769
- w(BAR);
770
- stepDone(`MCP server ${mcpChanged ? "configured" : `${DIM}already configured${RST}`}`);
771
- if (toolCount !== null) {
772
- w(BAR);
773
- stepDone(`${BLUE}${toolCount}${RST} tool${toolCount !== 1 ? "s" : ""} allowed`);
774
- }
775
- w(BAR);
776
- };
777
- function renderLoginChoice(name, cursor) {
778
- process.stdout.write("\x1b[2J\x1b[H");
779
- renderHeader(mode);
780
- printBadge();
781
- w("");
782
- priorSteps();
783
- stepActive(`Already logged in as ${WHITE_BOLD}${name}${RST}`);
784
- w(BAR);
785
- const options = [
786
- `Continue as ${BOLD}${name}${RST}`,
787
- `Login with a different account`,
788
- ];
789
- for (let i = 0; i < options.length; i++) {
790
- const arrow = i === cursor ? `${BLUE}\u276f${RST}` : " ";
791
- const label = i === cursor ? options[i] : `${DIM}${options[i]}${RST}`;
792
- w(` ${DIM}\u2502${RST} ${arrow} ${label}`);
793
- }
794
- w(BAR);
795
- w(` ${DIM}\u2502${RST} ${BLUE}${BOLD}[\u2191\u2193]${RST} move ${BLUE}${BOLD}[enter]${RST} confirm`);
796
- w("");
797
- }
798
- function runLoginChoice(name) {
799
- return new Promise((resolve) => {
800
- let cursor = 0;
801
- renderLoginChoice(name, cursor);
802
- process.stdin.setRawMode(true);
803
- process.stdin.resume();
804
- process.stdin.setEncoding("utf-8");
805
- const cleanup = () => {
806
- process.stdin.removeListener("data", onData);
807
- process.stdin.setRawMode(false);
808
- process.stdin.pause();
809
- };
810
- const onData = (key) => {
811
- if (key === "\x03" || key === "q") {
812
- cleanup();
813
- process.stdout.write("\x1b[2J\x1b[H");
814
- console.log("\n Setup cancelled.\n");
815
- process.exit(0);
816
- }
817
- if (key === "\x1b[A" || key === "k")
818
- cursor = cursor === 0 ? 1 : 0;
819
- else if (key === "\x1b[B" || key === "j")
820
- cursor = cursor === 0 ? 1 : 0;
821
- else if (key === "\r" || key === "\n") {
822
- cleanup();
823
- resolve(cursor === 0); // 0 = keep, 1 = new account
824
- return;
825
- }
826
- renderLoginChoice(name, cursor);
827
- };
828
- process.stdin.on("data", onData);
829
- });
830
- }
831
- // Check if already logged in
832
- let forceNew = false;
833
- const auth = await getAuthStatus();
834
- if (auth.loggedIn) {
835
- const keepAccount = await runLoginChoice(auth.name);
836
- if (keepAccount) {
837
- return { status: "already", name: auth.name };
838
- }
839
- forceNew = true;
840
- }
841
- // Show prompt before opening browser
842
- process.stdout.write("\x1b[2J\x1b[H");
843
- renderHeader(mode);
844
- printBadge();
845
- w("");
846
- priorSteps();
847
- stepActive(`Login to Almanac`);
848
- w(BAR);
849
- w(` ${DIM}\u2502${RST} This will open ${WHITE_BOLD}almanac${RST} in your browser`);
850
- w(` ${DIM}\u2502${RST} to connect your account.`);
851
- w(BAR);
852
- await waitForKey(` ${DIM}\u2502${RST} ${BLUE}${BOLD}[enter]${RST} continue`);
853
- // Show waiting state with cancel/retry hint
854
- const renderWaiting = () => {
855
- process.stdout.write("\x1b[2J\x1b[H");
856
- renderHeader(mode);
857
- printBadge();
858
- w("");
859
- priorSteps();
860
- stepActive(`Waiting for login\u2026 ${DIM}complete in browser${RST}`);
861
- w(BAR);
862
- w(` ${DIM}\u2502${RST} ${BLUE}${BOLD}[r]${RST} retry ${DIM}[q] cancel setup${RST}`);
863
- w("");
864
- };
865
- // Loop: attempt login, let user retry if it fails/times out
866
- // eslint-disable-next-line no-constant-condition
867
- while (true) {
868
- renderWaiting();
869
- // AbortController so we can kill the HTTP server on retry/cancel
870
- const controller = new AbortController();
871
- // Race login against keypress
872
- const loginPromise = performLogin({ signal: controller.signal, forceNew }).then((result) => result.status === "already_logged_in"
873
- ? { status: "already", name: result.name }
874
- : { status: "done" }, () => ({ status: "skipped" }));
875
- let keyOnData = null;
876
- const keyPromise = new Promise((resolve) => {
877
- process.stdin.setRawMode(true);
878
- process.stdin.resume();
879
- process.stdin.setEncoding("utf-8");
880
- keyOnData = (key) => {
881
- process.stdin.removeListener("data", keyOnData);
882
- process.stdin.setRawMode(false);
883
- process.stdin.pause();
884
- resolve(key);
885
- };
886
- process.stdin.on("data", keyOnData);
887
- });
888
- const result = await Promise.race([
889
- loginPromise.then((r) => ({ type: "login", result: r })),
890
- keyPromise.then((k) => ({ type: "key", key: k })),
891
- ]);
892
- // Clean up stdin listener if login won
893
- if (result.type === "login") {
894
- if (keyOnData)
895
- process.stdin.removeListener("data", keyOnData);
896
- try {
897
- process.stdin.setRawMode(false);
898
- }
899
- catch { /* already off */ }
900
- process.stdin.pause();
901
- return result.result;
902
- }
903
- // Key won — abort the login HTTP server
904
- controller.abort();
905
- // Handle keypress
906
- if (result.key === "\x03" || result.key === "q") {
907
- process.stdout.write("\x1b[2J\x1b[H");
908
- console.log("\n Setup cancelled.\n");
909
- process.exit(0);
910
- }
911
- if (result.key === "r") {
912
- // Retry — loop continues
913
- continue;
914
- }
915
- }
916
- }
917
- /* ── Tool permissions TUI ───────────────────────────────────────── */
918
- const MAX_NAME = Math.max(...TOOL_GROUPS.map((g) => g.name.length));
919
- function renderToolSelect(selected, cursor, clientsLabel, mcpChanged, mode = "default") {
920
- process.stdout.write("\x1b[2J\x1b[H");
921
- renderHeader(mode);
922
- printBadge();
923
- w("");
924
- stepDone(`Clients \u2192 ${WHITE_BOLD}${clientsLabel}${RST}`);
925
- w(BAR);
926
- stepDone(`MCP server ${mcpChanged ? "configured" : `${DIM}already configured${RST}`}`);
927
- w(BAR);
928
- stepActive(`Select Claude Code tool permissions ${DIM}deselect any you'd rather approve manually${RST}`);
929
- w(BAR);
930
- for (let i = 0; i < TOOL_GROUPS.length; i++) {
931
- const arrow = i === cursor ? `${BLUE}\u276f${RST}` : " ";
932
- const check = selected[i] ? `${BLUE}\u2713${RST}` : " ";
933
- const pad = TOOL_GROUPS[i].name.padEnd(MAX_NAME + 2);
934
- const name = i === cursor ? `${BOLD}${pad}${RST}` : pad;
935
- const desc = `${DIM}${TOOL_GROUPS[i].description}${RST}`;
936
- w(` ${DIM}\u2502${RST} ${arrow} [${check}] ${name} ${desc}`);
937
- }
938
- w(BAR);
939
- 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}`);
940
- w("");
941
- }
942
- function runToolSelect(clientsLabel, mcpChanged, mode = "default") {
943
- return new Promise((resolve) => {
944
- const selected = TOOL_GROUPS.map(() => true);
945
- let cursor = 0;
946
- renderToolSelect(selected, cursor, clientsLabel, mcpChanged, mode);
947
- process.stdin.setRawMode(true);
948
- process.stdin.resume();
949
- process.stdin.setEncoding("utf-8");
950
- const cleanup = () => {
951
- process.stdin.removeListener("data", onData);
952
- process.stdin.setRawMode(false);
953
- process.stdin.pause();
954
- };
955
- const onData = (key) => {
956
- if (key === "\x03" || key === "q") {
957
- cleanup();
958
- process.stdout.write("\x1b[2J\x1b[H");
959
- console.log("\n Setup cancelled.\n");
960
- process.exit(0);
961
- }
962
- if (key === "\x1b[A" || key === "k")
963
- cursor = (cursor - 1 + TOOL_GROUPS.length) % TOOL_GROUPS.length;
964
- else if (key === "\x1b[B" || key === "j")
965
- cursor = (cursor + 1) % TOOL_GROUPS.length;
966
- else if (key === " ")
967
- selected[cursor] = !selected[cursor];
968
- else if (key === "a") {
969
- const all = selected.every(Boolean);
970
- selected.fill(!all);
971
- }
972
- else if (key === "\r" || key === "\n") {
973
- cleanup();
974
- const tools = [];
975
- for (let i = 0; i < TOOL_GROUPS.length; i++) {
976
- if (selected[i])
977
- tools.push(...TOOL_GROUPS[i].tools);
978
- }
979
- resolve(tools);
980
- return;
981
- }
982
- renderToolSelect(selected, cursor, clientsLabel, mcpChanged, mode);
983
- };
984
- process.stdin.on("data", onData);
985
- });
986
- }
987
- /* ── Result screen ──────────────────────────────────────────────── */
988
- function printResult(clientsLabel, loginResult, configured, alreadyConfigured, toolCount) {
989
- process.stdout.write("\x1b[2J\x1b[H");
990
- printBanner();
991
- printBadge();
992
- w("");
993
- stepDone(`Clients \u2192 ${WHITE_BOLD}${clientsLabel}${RST}`);
994
- w(BAR);
995
- stepDone(`Configured \u2192 ${configured.length > 0 ? configured.join(", ") : `${DIM}none${RST}`}`);
996
- w(BAR);
997
- stepDone(`Already configured \u2192 ${alreadyConfigured.length > 0 ? alreadyConfigured.join(", ") : `${DIM}none${RST}`}`);
998
- w(BAR);
999
- if (toolCount > 0) {
1000
- stepDone(`${BLUE}${toolCount}${RST} tool${toolCount !== 1 ? "s" : ""} allowed in ${DIM}~/.claude/settings.json${RST}`);
1001
- w(BAR);
1002
- }
1003
- stepDone(loginLabel(loginResult));
1004
- w(BAR);
1005
- stepDone(`${BLUE}Setup complete${RST}`);
1006
- w("");
1007
- // Next steps box
1008
- const innerW = 62;
1009
- const row = (content) => {
1010
- const padding = Math.max(0, innerW - vis(content));
1011
- return ` ${BLUE_DIM}\u2502${RST}${content}${" ".repeat(padding)}${BLUE_DIM}\u2502${RST}`;
1012
- };
1013
- const empty = row("");
1014
- const nextSteps = getNextSteps(clientsLabel);
1015
- w(` ${BLUE_DIM}\u256d${"─".repeat(innerW)}\u256e${RST}`);
1016
- w(empty);
1017
- w(row(` ${WHITE_BOLD}Next steps${RST}`));
1018
- w(empty);
1019
- for (let i = 0; i < nextSteps.length; i++) {
1020
- w(row(` ${BLUE}${i + 1}.${RST} ${nextSteps[i]}`));
1021
- }
1022
- w(empty);
1023
- w(` ${BLUE_DIM}\u2570${"─".repeat(innerW)}\u256f${RST}`);
1024
- w("");
1025
- }
1026
- function getNextSteps(clientsLabel) {
1027
- const exampleLine = `${BLUE}"${EXAMPLE_PROMPT}"${RST}`;
1028
- if (clientsLabel === "Claude Code") {
1029
- return [
1030
- `Type ${WHITE_BOLD}claude${RST} to start Claude Code`,
1031
- `Say ${exampleLine}`,
1032
- ];
1033
- }
1034
- if (clientsLabel === "Codex") {
1035
- return [
1036
- `Type ${WHITE_BOLD}codex${RST} to start Codex`,
1037
- `Ask ${exampleLine}`,
1038
- ];
1039
- }
1040
- if (clientsLabel === "Cursor") {
1041
- return [
1042
- `Open ${WHITE_BOLD}Cursor${RST} in your project`,
1043
- `Ask ${exampleLine}`,
1044
- ];
1045
- }
1046
- if (clientsLabel === "OpenCode") {
1047
- return [
1048
- `Type ${WHITE_BOLD}opencode${RST} to start OpenCode`,
1049
- `Ask ${exampleLine}`,
1050
- ];
1051
- }
1052
- if (clientsLabel === "Windsurf") {
1053
- return [
1054
- `Open ${WHITE_BOLD}Windsurf${RST} in your project`,
1055
- `Ask ${exampleLine}`,
1056
- ];
1057
- }
1058
- if (clientsLabel === "Claude Desktop") {
1059
- return [
1060
- `Open ${WHITE_BOLD}Claude Desktop${RST}`,
1061
- `Ask ${exampleLine}`,
1062
- ];
1063
- }
1064
- return [
1065
- `Open one of your configured agents in this project`,
1066
- `Ask ${exampleLine}`,
1067
- ];
1068
- }
1069
- /* ── Entry point ────────────────────────────────────────────────── */
1070
- export async function runSetup() {
1071
- const options = parseSetupArgs(process.argv.slice(3));
1072
- let clients = resolveClients(options);
1073
- if (options.print || options.dryRun) {
1074
- printSetupPlan(clients, options);
1075
- process.exit(0);
1076
- }
1077
- if (clients.length === 0) {
1078
- printSetupPlan(clients, options);
1079
- process.exit(0);
1080
- }
1081
- const skipTui = options.yes;
1082
- const interactive = process.stdin.isTTY && !skipTui;
1083
- if (interactive && options.clients.length === 0) {
1084
- clients = await runClientSelect(clients);
1085
- }
1086
- const clientsLabel = clients.map((client) => client.name).join(", ");
1087
- const setupSummary = applyClientSetup(clients, "apply");
1088
- const permissionClient = clients.find((client) => client.supportsPermissions);
1089
- let tools = [];
1090
- if (permissionClient) {
1091
- const mcpChanged = setupSummary.configured.includes(permissionClient.name);
1092
- if (interactive) {
1093
- tools = await runToolSelect(clientsLabel, mcpChanged);
1094
- }
1095
- else {
1096
- tools = TOOL_GROUPS.flatMap((g) => g.tools);
1097
- }
1098
- }
1099
- const count = tools.length > 0 ? configurePermissions(tools) : 0;
1100
- const permissionCount = permissionClient ? count : null;
1101
- let loginResult;
1102
- if (interactive) {
1103
- loginResult = await runLoginStep(clientsLabel, setupSummary.configured.length > 0, permissionCount);
1104
- }
1105
- else {
1106
- try {
1107
- const result = await performLogin();
1108
- loginResult =
1109
- result.status === "already_logged_in"
1110
- ? { status: "already", name: result.name }
1111
- : { status: "done" };
1112
- }
1113
- catch {
1114
- loginResult = { status: "skipped" };
1115
- }
1116
- }
1117
- printResult(clientsLabel, loginResult, setupSummary.configured, setupSummary.alreadyConfigured, count);
1118
- process.exit(0);
1119
- }
1120
- /* ── Skill installation ────────────────────────────────────────── */
1121
- function getPackageSkillsDir() {
1122
- const thisFile = fileURLToPath(import.meta.url);
1123
- // dist/setup.js → package root → skills/
1124
- return join(dirname(thisFile), "..", "skills");
1125
- }
1126
- function installSkill(skillName) {
1127
- const src = join(getPackageSkillsDir(), skillName);
1128
- if (!existsSync(src)) {
1129
- throw new Error(`Skill "${skillName}" not found in package at ${src}`);
1130
- }
1131
- const dest = join(homedir(), ".claude", "skills", skillName);
1132
- // Always overwrite to ensure latest version
1133
- mkdirSync(dirname(dest), { recursive: true });
1134
- cpSync(src, dest, { recursive: true, force: true });
1135
- return true;
1136
- }
1137
- /* ── Reddit-specific tool groups ───────────────────────────────── */
1138
- const REDDIT_EXTRA_TOOLS = [
1139
- "Bash(node */ingest.js *)",
1140
- ];
1141
- /* ── Reddit result screen ──────────────────────────────────────── */
1142
- function printRedditResult(agent, loginResult, mcpChanged, toolCount) {
1143
- process.stdout.write("\x1b[2J\x1b[H");
1144
- renderHeader("reddit");
1145
- printBadge();
1146
- w("");
1147
- stepDone(`Agent \u2192 ${WHITE_BOLD}${agent}${RST}`);
1148
- w(BAR);
1149
- stepDone(`MCP server ${mcpChanged ? "configured" : `${DIM}already configured${RST}`}`);
1150
- w(BAR);
1151
- stepDone(`${BLUE}${toolCount}${RST} tool${toolCount !== 1 ? "s" : ""} allowed`);
1152
- w(BAR);
1153
- stepDone(loginLabel(loginResult));
1154
- w(BAR);
1155
- stepDone(`${BLUE}/reddit-wiki${RST} skill installed`);
1156
- w(BAR);
1157
- stepDone(`${BLUE}Setup complete${RST}`);
1158
- w("");
1159
- // Next steps box
1160
- const innerW = 62;
1161
- const row = (content) => {
1162
- const padding = Math.max(0, innerW - vis(content));
1163
- return ` ${BLUE_DIM}\u2502${RST}${content}${" ".repeat(padding)}${BLUE_DIM}\u2502${RST}`;
1164
- };
1165
- const empty = row("");
1166
- w(` ${BLUE_DIM}\u256d${"─".repeat(innerW)}\u256e${RST}`);
1167
- w(empty);
1168
- w(row(` ${WHITE_BOLD}Next steps${RST}`));
1169
- w(empty);
1170
- w(row(` ${BLUE}1.${RST} Type ${WHITE_BOLD}claude${RST} to start Claude Code`));
1171
- w(empty);
1172
- w(` ${BLUE_DIM}\u2570${"─".repeat(innerW)}\u256f${RST}`);
1173
- w("");
1174
- }
1175
- /* ── Reddit entry point ────────────────────────────────────────── */
1176
- export async function runRedditSetup() {
1177
- const skipTui = process.argv.includes("--yes") || process.argv.includes("-y");
1178
- const interactive = process.stdin.isTTY && !skipTui;
1179
- let agent = "Claude Code";
1180
- if (interactive) {
1181
- agent = await runAgentSelect("reddit");
1182
- }
1183
- const claudeSetup = SUPPORTED_CLIENTS["claude-code"].configure("apply");
1184
- const mcpChanged = claudeSetup.changed;
1185
- let tools;
1186
- if (interactive) {
1187
- tools = await runToolSelect(agent, mcpChanged, "reddit");
1188
- }
1189
- else {
1190
- tools = TOOL_GROUPS.flatMap((g) => g.tools);
1191
- }
1192
- // Add reddit-specific tool permissions
1193
- tools = [...tools, ...REDDIT_EXTRA_TOOLS];
1194
- const count = configurePermissions(tools);
1195
- // Login step
1196
- let loginResult;
1197
- if (interactive) {
1198
- loginResult = await runLoginStep(agent, mcpChanged, count, "reddit");
1199
- }
1200
- else {
1201
- try {
1202
- const result = await performLogin();
1203
- loginResult =
1204
- result.status === "already_logged_in"
1205
- ? { status: "already", name: result.name }
1206
- : { status: "done" };
1207
- }
1208
- catch {
1209
- loginResult = { status: "skipped" };
1210
- }
1211
- }
1212
- // Install the reddit-wiki skill
1213
- installSkill("reddit-wiki");
1214
- printRedditResult(agent, loginResult, mcpChanged, count);
1215
- process.exit(0);
1216
- }