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.
- package/dist/setup.js +508 -71
- 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
|
|
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
|
|
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
|
|
174
|
-
if (!
|
|
175
|
-
|
|
176
|
-
if (
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
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
|
-
|
|
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
|
-
/* ──
|
|
205
|
-
function
|
|
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
|
|
564
|
+
stepActive(`Select where to install Almanac`);
|
|
211
565
|
w(BAR);
|
|
212
|
-
for (
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
}
|
|
216
|
-
|
|
217
|
-
|
|
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
|
|
577
|
+
function runClientSelect(clients, mode = "default") {
|
|
225
578
|
return new Promise((resolve) => {
|
|
226
|
-
|
|
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 === "\
|
|
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
|
|
245
|
-
|
|
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(
|
|
660
|
+
stepDone(`${label} \u2192 ${WHITE_BOLD}${agent}${RST}`);
|
|
283
661
|
w(BAR);
|
|
284
662
|
stepDone(`MCP server ${mcpChanged ? "configured" : `${DIM}already configured${RST}`}`);
|
|
285
|
-
|
|
286
|
-
|
|
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,
|
|
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(`
|
|
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(
|
|
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,
|
|
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,
|
|
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(
|
|
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(`
|
|
885
|
+
stepDone(`Clients \u2192 ${WHITE_BOLD}${clientsLabel}${RST}`);
|
|
506
886
|
w(BAR);
|
|
507
|
-
stepDone(`
|
|
887
|
+
stepDone(`Configured \u2192 ${configured.length > 0 ? configured.join(", ") : `${DIM}none${RST}`}`);
|
|
508
888
|
w(BAR);
|
|
509
|
-
stepDone(
|
|
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
|
-
|
|
527
|
-
|
|
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
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
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
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
tools = await runToolSelect(agent, mcpChanged);
|
|
962
|
+
if (clients.length === 0) {
|
|
963
|
+
printSetupPlan(clients, options);
|
|
964
|
+
process.exit(0);
|
|
544
965
|
}
|
|
545
|
-
|
|
546
|
-
|
|
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
|
|
549
|
-
|
|
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(
|
|
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(
|
|
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
|
|
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");
|