openalmanac 0.3.1 → 0.3.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/dist/setup.js +508 -71
  2. package/package.json +1 -1
package/dist/setup.js CHANGED
@@ -2,6 +2,7 @@ import { readFileSync, writeFileSync, mkdirSync, existsSync, cpSync } from "fs";
2
2
  import { homedir } from "os";
3
3
  import { join, dirname } from "path";
4
4
  import { fileURLToPath } from "url";
5
+ import { spawnSync } from "child_process";
5
6
  import { performLogin } from "./login-core.js";
6
7
  import { getAuthStatus } from "./auth.js";
7
8
  const TOOL_GROUPS = [
@@ -131,9 +132,12 @@ const vis = (s) => s.replace(/\x1b\[[0-9;]*m/g, "").length;
131
132
  const w = (s) => process.stdout.write(s + "\n");
132
133
  /* ── File helpers ───────────────────────────────────────────────── */
133
134
  const CLAUDE_DIR = join(homedir(), ".claude");
134
- const CLAUDE_JSON = join(homedir(), ".claude.json"); // Claude Desktop
135
- const CLAUDE_CODE_MCP = join(CLAUDE_DIR, "mcp.json"); // Claude Code
135
+ const CLAUDE_JSON = join(homedir(), ".claude.json"); // Claude Code user-scoped MCP config
136
+ const CLAUDE_CODE_MCP = join(CLAUDE_DIR, "mcp.json"); // Claude Code local MCP config
136
137
  const SETTINGS_JSON = join(CLAUDE_DIR, "settings.json");
138
+ const CODEX_CONFIG = join(homedir(), ".codex", "config.toml");
139
+ const CURSOR_MCP_JSON = join(homedir(), ".cursor", "mcp.json");
140
+ const WINDSURF_MCP_JSON = join(homedir(), ".codeium", "mcp_config.json");
137
141
  function ensureDir(dir) {
138
142
  if (!existsSync(dir))
139
143
  mkdirSync(dir, { recursive: true });
@@ -151,34 +155,384 @@ function writeJson(path, data) {
151
155
  }
152
156
  /* ── Step 1 — MCP server ───────────────────────────────────────── */
153
157
  const ALMANAC_MCP_ENTRY = { command: "npx", args: ["-y", "openalmanac@latest"] };
158
+ const SUPPORTED_CLIENT_IDS = [
159
+ "claude-code",
160
+ "claude-desktop",
161
+ "codex",
162
+ "cursor",
163
+ "windsurf",
164
+ ];
165
+ const SUPPORTED_CLIENTS = {
166
+ "claude-code": {
167
+ id: "claude-code",
168
+ name: "Claude Code",
169
+ selectionLabel: "Claude Code",
170
+ detect: () => hasCommand("claude") || existsSync(CLAUDE_JSON) || existsSync(CLAUDE_DIR),
171
+ configure: (mode) => {
172
+ const snippets = [
173
+ {
174
+ path: CLAUDE_JSON,
175
+ content: jsonSnippet({
176
+ mcpServers: {
177
+ almanac: { ...ALMANAC_MCP_ENTRY },
178
+ },
179
+ }),
180
+ },
181
+ {
182
+ path: CLAUDE_CODE_MCP,
183
+ content: jsonSnippet({
184
+ mcpServers: {
185
+ almanac: { ...ALMANAC_MCP_ENTRY },
186
+ },
187
+ }),
188
+ },
189
+ ];
190
+ const changedPrimary = configureJsonMcpFile(CLAUDE_JSON, mode);
191
+ const changedSecondary = configureJsonMcpFile(CLAUDE_CODE_MCP, mode);
192
+ return { changed: changedPrimary || changedSecondary, snippets };
193
+ },
194
+ supportsPermissions: true,
195
+ },
196
+ "claude-desktop": {
197
+ id: "claude-desktop",
198
+ name: "Claude Desktop",
199
+ selectionLabel: "Claude Desktop",
200
+ detect: () => {
201
+ const path = getClaudeDesktopConfigPath();
202
+ return Boolean(path && (existsSync(path) || isClaudeDesktopInstalled()));
203
+ },
204
+ configure: (mode) => {
205
+ const path = getClaudeDesktopConfigPath();
206
+ if (!path)
207
+ return { changed: false, snippets: [] };
208
+ return {
209
+ changed: configureJsonMcpFile(path, mode),
210
+ snippets: [
211
+ {
212
+ path,
213
+ content: jsonSnippet({
214
+ mcpServers: {
215
+ almanac: { ...ALMANAC_MCP_ENTRY },
216
+ },
217
+ }),
218
+ },
219
+ ],
220
+ };
221
+ },
222
+ },
223
+ codex: {
224
+ id: "codex",
225
+ name: "Codex",
226
+ selectionLabel: "Codex",
227
+ detect: () => hasCommand("codex") || existsSync(CODEX_CONFIG) || existsSync(join(homedir(), ".codex")),
228
+ configure: (mode) => ({
229
+ changed: configureCodexToml(CODEX_CONFIG, mode),
230
+ snippets: [
231
+ {
232
+ path: CODEX_CONFIG,
233
+ content: codexSnippet(),
234
+ },
235
+ ],
236
+ }),
237
+ },
238
+ cursor: {
239
+ id: "cursor",
240
+ name: "Cursor",
241
+ selectionLabel: "Cursor",
242
+ detect: () => hasCommand("cursor-agent") ||
243
+ existsSync(CURSOR_MCP_JSON) ||
244
+ existsSync(join(homedir(), ".cursor")),
245
+ configure: (mode) => ({
246
+ changed: configureJsonMcpFile(CURSOR_MCP_JSON, mode),
247
+ snippets: [
248
+ {
249
+ path: CURSOR_MCP_JSON,
250
+ content: jsonSnippet({
251
+ mcpServers: {
252
+ almanac: { ...ALMANAC_MCP_ENTRY },
253
+ },
254
+ }),
255
+ },
256
+ ],
257
+ }),
258
+ },
259
+ windsurf: {
260
+ id: "windsurf",
261
+ name: "Windsurf",
262
+ selectionLabel: "Windsurf",
263
+ detect: () => hasCommand("windsurf") ||
264
+ existsSync(WINDSURF_MCP_JSON) ||
265
+ existsSync(join(homedir(), ".codeium")),
266
+ configure: (mode) => ({
267
+ changed: configureJsonMcpFile(WINDSURF_MCP_JSON, mode),
268
+ snippets: [
269
+ {
270
+ path: WINDSURF_MCP_JSON,
271
+ content: jsonSnippet({
272
+ mcpServers: {
273
+ almanac: { ...ALMANAC_MCP_ENTRY },
274
+ },
275
+ }),
276
+ },
277
+ ],
278
+ }),
279
+ },
280
+ };
281
+ function parseSetupArgs(argv) {
282
+ const options = {
283
+ all: false,
284
+ clients: [],
285
+ dryRun: false,
286
+ print: false,
287
+ yes: false,
288
+ };
289
+ for (let i = 0; i < argv.length; i++) {
290
+ const arg = argv[i];
291
+ if (arg === "--all") {
292
+ options.all = true;
293
+ continue;
294
+ }
295
+ if (arg === "--print") {
296
+ options.print = true;
297
+ continue;
298
+ }
299
+ if (arg === "--dry-run") {
300
+ options.dryRun = true;
301
+ continue;
302
+ }
303
+ if (arg === "--yes" || arg === "-y") {
304
+ options.yes = true;
305
+ continue;
306
+ }
307
+ if (arg === "--client") {
308
+ const value = argv[i + 1];
309
+ if (!value) {
310
+ throw new Error("Missing value for --client");
311
+ }
312
+ i++;
313
+ options.clients.push(...parseClientList(value));
314
+ continue;
315
+ }
316
+ if (arg.startsWith("--client=")) {
317
+ options.clients.push(...parseClientList(arg.slice("--client=".length)));
318
+ continue;
319
+ }
320
+ throw new Error(`Unknown setup flag: ${arg}`);
321
+ }
322
+ if (options.all && options.clients.length > 0) {
323
+ throw new Error("Use either --all or --client, not both");
324
+ }
325
+ options.clients = Array.from(new Set(options.clients));
326
+ return options;
327
+ }
328
+ function parseClientList(value) {
329
+ return value
330
+ .split(",")
331
+ .map((part) => part.trim().toLowerCase())
332
+ .filter(Boolean)
333
+ .map((part) => normalizeClientId(part));
334
+ }
335
+ function normalizeClientId(value) {
336
+ const aliases = {
337
+ claude: "claude-code",
338
+ "claude-code": "claude-code",
339
+ "claude-desktop": "claude-desktop",
340
+ desktop: "claude-desktop",
341
+ codex: "codex",
342
+ cursor: "cursor",
343
+ windsurf: "windsurf",
344
+ };
345
+ const normalized = aliases[value];
346
+ if (!normalized) {
347
+ throw new Error(`Unsupported client "${value}". Supported clients: ${SUPPORTED_CLIENT_IDS.join(", ")}`);
348
+ }
349
+ return normalized;
350
+ }
351
+ function hasCommand(command) {
352
+ const checker = process.platform === "win32" ? "where" : "which";
353
+ const result = spawnSync(checker, [command], { stdio: "ignore" });
354
+ return result.status === 0;
355
+ }
356
+ function getClaudeDesktopConfigPath() {
357
+ if (process.platform === "darwin") {
358
+ return join(homedir(), "Library", "Application Support", "Claude", "claude_desktop_config.json");
359
+ }
360
+ if (process.platform === "linux") {
361
+ return join(homedir(), ".config", "Claude", "claude_desktop_config.json");
362
+ }
363
+ if (process.platform === "win32") {
364
+ const appData = process.env.APPDATA;
365
+ if (!appData)
366
+ return null;
367
+ return join(appData, "Claude", "claude_desktop_config.json");
368
+ }
369
+ return null;
370
+ }
371
+ function isClaudeDesktopInstalled() {
372
+ if (process.platform === "darwin") {
373
+ return (existsSync("/Applications/Claude.app") ||
374
+ existsSync(join(homedir(), "Applications", "Claude.app")));
375
+ }
376
+ if (process.platform === "linux") {
377
+ return (existsSync("/usr/share/applications/claude.desktop") ||
378
+ existsSync(join(homedir(), ".local", "share", "applications", "claude.desktop")) ||
379
+ existsSync("/opt/Claude/claude"));
380
+ }
381
+ if (process.platform === "win32") {
382
+ const localAppData = process.env.LOCALAPPDATA;
383
+ const programFiles = process.env.ProgramFiles;
384
+ return Boolean((localAppData &&
385
+ existsSync(join(localAppData, "Programs", "Claude", "Claude.exe"))) ||
386
+ (programFiles && existsSync(join(programFiles, "Claude", "Claude.exe"))));
387
+ }
388
+ return false;
389
+ }
390
+ function jsonSnippet(data) {
391
+ return JSON.stringify(data, null, 2);
392
+ }
393
+ function codexSnippet() {
394
+ return [
395
+ "[mcp_servers.almanac]",
396
+ `command = ${tomlString(ALMANAC_MCP_ENTRY.command)}`,
397
+ `args = ${tomlArray(ALMANAC_MCP_ENTRY.args)}`,
398
+ ].join("\n");
399
+ }
154
400
  function isAlmanacCurrent(server) {
155
401
  return (server?.command === "npx" &&
156
402
  JSON.stringify(server.args) === JSON.stringify(ALMANAC_MCP_ENTRY.args));
157
403
  }
158
- function configureMcp() {
159
- let changed = false;
160
- // Claude Desktop — ~/.claude.json
161
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
162
- const desktop = readJson(CLAUDE_JSON);
163
- if (!desktop.mcpServers)
164
- desktop.mcpServers = {};
165
- if (!isAlmanacCurrent(desktop.mcpServers.almanac)) {
166
- desktop.mcpServers.almanac = { ...ALMANAC_MCP_ENTRY };
167
- writeJson(CLAUDE_JSON, desktop);
168
- changed = true;
169
- }
170
- // Claude Code — ~/.claude/mcp.json
171
- ensureDir(CLAUDE_DIR);
404
+ function configureJsonMcpFile(path, mode) {
172
405
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
173
- const code = readJson(CLAUDE_CODE_MCP);
174
- if (!code.mcpServers)
175
- code.mcpServers = {};
176
- if (!isAlmanacCurrent(code.mcpServers.almanac)) {
177
- code.mcpServers.almanac = { ...ALMANAC_MCP_ENTRY };
178
- writeJson(CLAUDE_CODE_MCP, code);
179
- changed = true;
406
+ const json = readJson(path);
407
+ if (!json.mcpServers)
408
+ json.mcpServers = {};
409
+ if (isAlmanacCurrent(json.mcpServers.almanac)) {
410
+ return false;
411
+ }
412
+ if (mode === "apply") {
413
+ ensureDir(dirname(path));
414
+ json.mcpServers.almanac = { ...ALMANAC_MCP_ENTRY };
415
+ writeJson(path, json);
416
+ }
417
+ return true;
418
+ }
419
+ function configureCodexToml(path, mode) {
420
+ const current = readToml(path);
421
+ const next = upsertCodexServer(current);
422
+ if (current.trim() === next.trim()) {
423
+ return false;
424
+ }
425
+ if (mode === "apply") {
426
+ ensureDir(dirname(path));
427
+ writeFileSync(path, next.endsWith("\n") ? next : next + "\n");
428
+ }
429
+ return true;
430
+ }
431
+ function readToml(path) {
432
+ try {
433
+ return readFileSync(path, "utf-8");
434
+ }
435
+ catch {
436
+ return "";
437
+ }
438
+ }
439
+ function upsertCodexServer(content) {
440
+ const sectionName = "mcp_servers.almanac";
441
+ const header = `[${sectionName}]`;
442
+ const nextBody = [
443
+ `command = ${tomlString(ALMANAC_MCP_ENTRY.command)}`,
444
+ `args = ${tomlArray(ALMANAC_MCP_ENTRY.args)}`,
445
+ ];
446
+ const lines = content === "" ? [] : content.split(/\r?\n/);
447
+ const start = lines.findIndex((line) => line.trim() === header);
448
+ if (start === -1) {
449
+ const prefix = content.trimEnd();
450
+ const block = [header, ...nextBody].join("\n");
451
+ return prefix === "" ? block + "\n" : `${prefix}\n\n${block}\n`;
452
+ }
453
+ let end = lines.length;
454
+ for (let i = start + 1; i < lines.length; i++) {
455
+ if (/^\s*\[.+\]\s*$/.test(lines[i])) {
456
+ end = i;
457
+ break;
458
+ }
180
459
  }
181
- return changed;
460
+ const existingBody = lines.slice(start + 1, end);
461
+ const preserved = existingBody.filter((line) => {
462
+ const trimmed = line.trim();
463
+ return !trimmed.startsWith("command =") && !trimmed.startsWith("args =");
464
+ });
465
+ const replacement = [header, ...nextBody, ...preserved];
466
+ const updated = [...lines.slice(0, start), ...replacement, ...lines.slice(end)];
467
+ return updated.join("\n").replace(/\n{3,}/g, "\n\n").replace(/\s+$/, "") + "\n";
468
+ }
469
+ function tomlString(value) {
470
+ return JSON.stringify(value);
471
+ }
472
+ function tomlArray(values) {
473
+ return `[${values.map((value) => tomlString(value)).join(", ")}]`;
474
+ }
475
+ function detectClients() {
476
+ return SUPPORTED_CLIENT_IDS.map((id) => SUPPORTED_CLIENTS[id]).filter((client) => client.detect());
477
+ }
478
+ function resolveClients(options) {
479
+ if (options.clients.length > 0) {
480
+ return options.clients.map((id) => SUPPORTED_CLIENTS[id]);
481
+ }
482
+ const detected = detectClients();
483
+ if (options.all)
484
+ return detected;
485
+ return detected;
486
+ }
487
+ function applyClientSetup(clients, mode) {
488
+ const configured = [];
489
+ const alreadyConfigured = [];
490
+ for (const client of clients) {
491
+ const result = client.configure(mode);
492
+ if (result.changed) {
493
+ configured.push(client.name);
494
+ }
495
+ else {
496
+ alreadyConfigured.push(client.name);
497
+ }
498
+ }
499
+ return { configured, alreadyConfigured };
500
+ }
501
+ function printSetupPlan(clients, options) {
502
+ const heading = options.dryRun ? "Dry run" : "OpenAlmanac MCP setup";
503
+ process.stdout.write(`${heading}\n\n`);
504
+ if (clients.length === 0) {
505
+ process.stdout.write("No supported clients detected. Use --client <name> to force a target or --print to inspect supported snippets.\n");
506
+ if (options.print) {
507
+ process.stdout.write("\nSupported clients:\n");
508
+ for (const id of SUPPORTED_CLIENT_IDS) {
509
+ process.stdout.write(`- ${SUPPORTED_CLIENTS[id].name}\n`);
510
+ }
511
+ }
512
+ return;
513
+ }
514
+ const mode = options.print ? "print" : "dry-run";
515
+ for (const client of clients) {
516
+ const result = client.configure(mode);
517
+ const status = result.changed
518
+ ? options.print
519
+ ? "snippet"
520
+ : "would configure"
521
+ : "already configured";
522
+ process.stdout.write(`- ${client.name}: ${status}\n`);
523
+ if (options.print) {
524
+ for (const snippet of result.snippets) {
525
+ process.stdout.write(` Path: ${snippet.path}\n`);
526
+ process.stdout.write(`${indentBlock(snippet.content, " ")}\n`);
527
+ }
528
+ }
529
+ }
530
+ }
531
+ function indentBlock(content, prefix) {
532
+ return content
533
+ .split("\n")
534
+ .map((line) => `${prefix}${line}`)
535
+ .join("\n");
182
536
  }
183
537
  /* ── Step 2 — Permissions ──────────────────────────────────────── */
184
538
  function configurePermissions(tools) {
@@ -201,29 +555,30 @@ function configurePermissions(tools) {
201
555
  writeJson(SETTINGS_JSON, settings);
202
556
  return tools.length;
203
557
  }
204
- /* ── Agent selection screen ─────────────────────────────────────── */
205
- function renderAgentSelect(_cursor, mode = "default") {
558
+ /* ── Client selection screen ────────────────────────────────────── */
559
+ function renderClientSelect(clients, selected, cursor, mode = "default") {
206
560
  process.stdout.write("\x1b[2J\x1b[H");
207
561
  renderHeader(mode);
208
562
  printBadge();
209
563
  w("");
210
- stepActive(`Select your agent`);
564
+ stepActive(`Select where to install Almanac`);
211
565
  w(BAR);
212
- for (const agent of AGENTS) {
213
- if (agent.supported) {
214
- w(` ${DIM}\u2502${RST} ${BLUE}\u276f${RST} ${BLUE}\u25cf${RST} ${BOLD}${agent.name}${RST}`);
215
- }
216
- else {
217
- w(` ${DIM}\u2502${RST} ${DIM}\u25cb ${agent.name}${" "}coming soon${RST}`);
218
- }
566
+ for (let i = 0; i < clients.length; i++) {
567
+ const client = clients[i];
568
+ const arrow = i === cursor ? `${BLUE}\u276f${RST}` : " ";
569
+ const check = selected[i] ? `${BLUE}\u2713${RST}` : " ";
570
+ const label = i === cursor ? `${BOLD}${client.selectionLabel ?? client.name}${RST}` : client.selectionLabel ?? client.name;
571
+ w(` ${DIM}\u2502${RST} ${arrow} [${check}] ${label}`);
219
572
  }
220
573
  w(BAR);
221
- w(` ${DIM}\u2502${RST} ${BLUE}${BOLD}[enter]${RST} confirm ${DIM}[q] quit${RST}`);
574
+ 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}`);
222
575
  w("");
223
576
  }
224
- function runAgentSelect(mode = "default") {
577
+ function runClientSelect(clients, mode = "default") {
225
578
  return new Promise((resolve) => {
226
- renderAgentSelect(0, mode);
579
+ const selected = clients.map(() => true);
580
+ let cursor = 0;
581
+ renderClientSelect(clients, selected, cursor, mode);
227
582
  process.stdin.setRawMode(true);
228
583
  process.stdin.resume();
229
584
  process.stdin.setEncoding("utf-8");
@@ -239,16 +594,38 @@ function runAgentSelect(mode = "default") {
239
594
  console.log("\n Setup cancelled.\n");
240
595
  process.exit(0);
241
596
  }
242
- if (key === "\r" || key === "\n") {
597
+ if (key === "\x1b[A" || key === "k") {
598
+ cursor = (cursor - 1 + clients.length) % clients.length;
599
+ }
600
+ else if (key === "\x1b[B" || key === "j") {
601
+ cursor = (cursor + 1) % clients.length;
602
+ }
603
+ else if (key === " ") {
604
+ selected[cursor] = !selected[cursor];
605
+ }
606
+ else if (key === "a") {
607
+ const all = selected.every(Boolean);
608
+ selected.fill(!all);
609
+ }
610
+ else if (key === "\r" || key === "\n") {
243
611
  cleanup();
244
- const supported = AGENTS.find((a) => a.supported);
245
- resolve(supported.name);
612
+ const chosen = clients.filter((_, index) => selected[index]);
613
+ if (chosen.length === 0) {
614
+ console.log("\n Select at least one client.\n");
615
+ process.exit(1);
616
+ }
617
+ resolve(chosen);
246
618
  return;
247
619
  }
620
+ renderClientSelect(clients, selected, cursor, mode);
248
621
  };
249
622
  process.stdin.on("data", onData);
250
623
  });
251
624
  }
625
+ async function runAgentSelect(mode = "default") {
626
+ const [client] = await runClientSelect([SUPPORTED_CLIENTS["claude-code"]], mode);
627
+ return client.name;
628
+ }
252
629
  /* ── Login step ─────────────────────────────────────────────────── */
253
630
  function loginLabel(result) {
254
631
  if (result.status === "already")
@@ -278,12 +655,15 @@ function waitForKey(prompt) {
278
655
  });
279
656
  }
280
657
  async function runLoginStep(agent, mcpChanged, toolCount, mode = "default") {
658
+ const label = agent.includes(",") ? "Clients" : "Agent";
281
659
  const priorSteps = () => {
282
- stepDone(`Agent \u2192 ${WHITE_BOLD}${agent}${RST}`);
660
+ stepDone(`${label} \u2192 ${WHITE_BOLD}${agent}${RST}`);
283
661
  w(BAR);
284
662
  stepDone(`MCP server ${mcpChanged ? "configured" : `${DIM}already configured${RST}`}`);
285
- w(BAR);
286
- stepDone(`${BLUE}${toolCount}${RST} tool${toolCount !== 1 ? "s" : ""} allowed`);
663
+ if (toolCount !== null) {
664
+ w(BAR);
665
+ stepDone(`${BLUE}${toolCount}${RST} tool${toolCount !== 1 ? "s" : ""} allowed`);
666
+ }
287
667
  w(BAR);
288
668
  };
289
669
  function renderLoginChoice(name, cursor) {
@@ -428,16 +808,16 @@ async function runLoginStep(agent, mcpChanged, toolCount, mode = "default") {
428
808
  }
429
809
  /* ── Tool permissions TUI ───────────────────────────────────────── */
430
810
  const MAX_NAME = Math.max(...TOOL_GROUPS.map((g) => g.name.length));
431
- function renderToolSelect(selected, cursor, agent, mcpChanged, mode = "default") {
811
+ function renderToolSelect(selected, cursor, clientsLabel, mcpChanged, mode = "default") {
432
812
  process.stdout.write("\x1b[2J\x1b[H");
433
813
  renderHeader(mode);
434
814
  printBadge();
435
815
  w("");
436
- stepDone(`Agent \u2192 ${WHITE_BOLD}${agent}${RST}`);
816
+ stepDone(`Clients \u2192 ${WHITE_BOLD}${clientsLabel}${RST}`);
437
817
  w(BAR);
438
818
  stepDone(`MCP server ${mcpChanged ? "configured" : `${DIM}already configured${RST}`}`);
439
819
  w(BAR);
440
- stepActive(`Select tool permissions ${DIM}deselect any you'd rather approve manually${RST}`);
820
+ stepActive(`Select Claude Code tool permissions ${DIM}deselect any you'd rather approve manually${RST}`);
441
821
  w(BAR);
442
822
  for (let i = 0; i < TOOL_GROUPS.length; i++) {
443
823
  const arrow = i === cursor ? `${BLUE}\u276f${RST}` : " ";
@@ -451,11 +831,11 @@ function renderToolSelect(selected, cursor, agent, mcpChanged, mode = "default")
451
831
  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}`);
452
832
  w("");
453
833
  }
454
- function runToolSelect(agent, mcpChanged, mode = "default") {
834
+ function runToolSelect(clientsLabel, mcpChanged, mode = "default") {
455
835
  return new Promise((resolve) => {
456
836
  const selected = TOOL_GROUPS.map(() => true);
457
837
  let cursor = 0;
458
- renderToolSelect(selected, cursor, agent, mcpChanged, mode);
838
+ renderToolSelect(selected, cursor, clientsLabel, mcpChanged, mode);
459
839
  process.stdin.setRawMode(true);
460
840
  process.stdin.resume();
461
841
  process.stdin.setEncoding("utf-8");
@@ -491,23 +871,27 @@ function runToolSelect(agent, mcpChanged, mode = "default") {
491
871
  resolve(tools);
492
872
  return;
493
873
  }
494
- renderToolSelect(selected, cursor, agent, mcpChanged, mode);
874
+ renderToolSelect(selected, cursor, clientsLabel, mcpChanged, mode);
495
875
  };
496
876
  process.stdin.on("data", onData);
497
877
  });
498
878
  }
499
879
  /* ── Result screen ──────────────────────────────────────────────── */
500
- function printResult(agent, loginResult, mcpChanged, toolCount) {
880
+ function printResult(clientsLabel, loginResult, configured, alreadyConfigured, toolCount) {
501
881
  process.stdout.write("\x1b[2J\x1b[H");
502
882
  printBanner();
503
883
  printBadge();
504
884
  w("");
505
- stepDone(`Agent \u2192 ${WHITE_BOLD}${agent}${RST}`);
885
+ stepDone(`Clients \u2192 ${WHITE_BOLD}${clientsLabel}${RST}`);
506
886
  w(BAR);
507
- stepDone(`MCP server ${mcpChanged ? "configured" : `${DIM}already configured${RST}`}`);
887
+ stepDone(`Configured \u2192 ${configured.length > 0 ? configured.join(", ") : `${DIM}none${RST}`}`);
508
888
  w(BAR);
509
- stepDone(`${BLUE}${toolCount}${RST} tool${toolCount !== 1 ? "s" : ""} allowed in ${DIM}~/.claude/settings.json${RST}`);
889
+ stepDone(`Already configured \u2192 ${alreadyConfigured.length > 0 ? alreadyConfigured.join(", ") : `${DIM}none${RST}`}`);
510
890
  w(BAR);
891
+ if (toolCount > 0) {
892
+ stepDone(`${BLUE}${toolCount}${RST} tool${toolCount !== 1 ? "s" : ""} allowed in ${DIM}~/.claude/settings.json${RST}`);
893
+ w(BAR);
894
+ }
511
895
  stepDone(loginLabel(loginResult));
512
896
  w(BAR);
513
897
  stepDone(`${BLUE}Setup complete${RST}`);
@@ -519,37 +903,89 @@ function printResult(agent, loginResult, mcpChanged, toolCount) {
519
903
  return ` ${BLUE_DIM}\u2502${RST}${content}${" ".repeat(padding)}${BLUE_DIM}\u2502${RST}`;
520
904
  };
521
905
  const empty = row("");
906
+ const nextSteps = getNextSteps(clientsLabel);
522
907
  w(` ${BLUE_DIM}\u256d${"─".repeat(innerW)}\u256e${RST}`);
523
908
  w(empty);
524
909
  w(row(` ${WHITE_BOLD}Next steps${RST}`));
525
910
  w(empty);
526
- w(row(` ${BLUE}1.${RST} Type ${WHITE_BOLD}claude${RST} to start Claude Code`));
527
- w(row(` ${BLUE}2.${RST} Say ${BLUE}"I want to contribute/explore the founders-inc wiki"${RST}`));
911
+ for (let i = 0; i < nextSteps.length; i++) {
912
+ w(row(` ${BLUE}${i + 1}.${RST} ${nextSteps[i]}`));
913
+ }
528
914
  w(empty);
529
915
  w(` ${BLUE_DIM}\u2570${"─".repeat(innerW)}\u256f${RST}`);
530
916
  w("");
531
917
  }
918
+ function getNextSteps(clientsLabel) {
919
+ if (clientsLabel === "Claude Code") {
920
+ return [
921
+ `Type ${WHITE_BOLD}claude${RST} to start Claude Code`,
922
+ `Say ${BLUE}"Let's explore <topic> using Almanac"${RST}`,
923
+ ];
924
+ }
925
+ if (clientsLabel === "Codex") {
926
+ return [
927
+ `Type ${WHITE_BOLD}codex${RST} to start Codex`,
928
+ `Ask ${BLUE}"Let's explore <topic> using Almanac"${RST}`,
929
+ ];
930
+ }
931
+ if (clientsLabel === "Cursor") {
932
+ return [
933
+ `Open ${WHITE_BOLD}Cursor${RST} in your project`,
934
+ `Ask ${BLUE}"Let's explore <topic> using Almanac"${RST}`,
935
+ ];
936
+ }
937
+ if (clientsLabel === "Windsurf") {
938
+ return [
939
+ `Open ${WHITE_BOLD}Windsurf${RST} in your project`,
940
+ `Ask ${BLUE}"Let's explore <topic> using Almanac"${RST}`,
941
+ ];
942
+ }
943
+ if (clientsLabel === "Claude Desktop") {
944
+ return [
945
+ `Open ${WHITE_BOLD}Claude Desktop${RST}`,
946
+ `Ask ${BLUE}"Let's explore <topic> using Almanac"${RST}`,
947
+ ];
948
+ }
949
+ return [
950
+ `Open one of your configured agents in this project`,
951
+ `Ask ${BLUE}"Let's explore <topic> using Almanac"${RST}`,
952
+ ];
953
+ }
532
954
  /* ── Entry point ────────────────────────────────────────────────── */
533
955
  export async function runSetup() {
534
- const skipTui = process.argv.includes("--yes") || process.argv.includes("-y");
535
- const interactive = process.stdin.isTTY && !skipTui;
536
- let agent = "Claude Code";
537
- if (interactive) {
538
- agent = await runAgentSelect();
956
+ const options = parseSetupArgs(process.argv.slice(3));
957
+ let clients = resolveClients(options);
958
+ if (options.print || options.dryRun) {
959
+ printSetupPlan(clients, options);
960
+ process.exit(0);
539
961
  }
540
- const mcpChanged = configureMcp();
541
- let tools;
542
- if (interactive) {
543
- tools = await runToolSelect(agent, mcpChanged);
962
+ if (clients.length === 0) {
963
+ printSetupPlan(clients, options);
964
+ process.exit(0);
544
965
  }
545
- else {
546
- tools = TOOL_GROUPS.flatMap((g) => g.tools);
966
+ const skipTui = options.yes;
967
+ const interactive = process.stdin.isTTY && !skipTui;
968
+ if (interactive && options.clients.length === 0) {
969
+ clients = await runClientSelect(clients);
547
970
  }
548
- const count = configurePermissions(tools);
549
- // Login step (last — so when they return from browser, everything is done)
971
+ const clientsLabel = clients.map((client) => client.name).join(", ");
972
+ const setupSummary = applyClientSetup(clients, "apply");
973
+ const permissionClient = clients.find((client) => client.supportsPermissions);
974
+ let tools = [];
975
+ if (permissionClient) {
976
+ const mcpChanged = setupSummary.configured.includes(permissionClient.name);
977
+ if (interactive) {
978
+ tools = await runToolSelect(clientsLabel, mcpChanged);
979
+ }
980
+ else {
981
+ tools = TOOL_GROUPS.flatMap((g) => g.tools);
982
+ }
983
+ }
984
+ const count = tools.length > 0 ? configurePermissions(tools) : 0;
985
+ const permissionCount = permissionClient ? count : null;
550
986
  let loginResult;
551
987
  if (interactive) {
552
- loginResult = await runLoginStep(agent, mcpChanged, count);
988
+ loginResult = await runLoginStep(clientsLabel, setupSummary.configured.length > 0, permissionCount);
553
989
  }
554
990
  else {
555
991
  try {
@@ -563,7 +999,7 @@ export async function runSetup() {
563
999
  loginResult = { status: "skipped" };
564
1000
  }
565
1001
  }
566
- printResult(agent, loginResult, mcpChanged, count);
1002
+ printResult(clientsLabel, loginResult, setupSummary.configured, setupSummary.alreadyConfigured, count);
567
1003
  process.exit(0);
568
1004
  }
569
1005
  /* ── Skill installation ────────────────────────────────────────── */
@@ -629,7 +1065,8 @@ export async function runRedditSetup() {
629
1065
  if (interactive) {
630
1066
  agent = await runAgentSelect("reddit");
631
1067
  }
632
- const mcpChanged = configureMcp();
1068
+ const claudeSetup = SUPPORTED_CLIENTS["claude-code"].configure("apply");
1069
+ const mcpChanged = claudeSetup.changed;
633
1070
  let tools;
634
1071
  if (interactive) {
635
1072
  tools = await runToolSelect(agent, mcpChanged, "reddit");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "openalmanac",
3
- "version": "0.3.1",
3
+ "version": "0.3.2",
4
4
  "description": "OpenAlmanac — pull, edit, and push articles to the open knowledge base",
5
5
  "type": "module",
6
6
  "bin": {