openalmanac 0.3.1 → 0.3.3
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/cli.js +0 -0
- package/dist/setup.js +651 -71
- package/dist/tools/articles.d.ts +2 -0
- package/dist/tools/articles.js +401 -0
- package/dist/tools/communities.d.ts +2 -0
- package/dist/tools/communities.js +127 -0
- package/dist/tools/people.d.ts +2 -0
- package/dist/tools/people.js +20 -0
- package/dist/tools/research.js +33 -27
- 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,15 @@ 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 OPENCODE_DIR = join(homedir(), ".config", "opencode");
|
|
141
|
+
const OPENCODE_JSON = join(OPENCODE_DIR, "opencode.json");
|
|
142
|
+
const OPENCODE_JSONC = join(OPENCODE_DIR, "opencode.jsonc");
|
|
143
|
+
const WINDSURF_MCP_JSON = join(homedir(), ".codeium", "mcp_config.json");
|
|
137
144
|
function ensureDir(dir) {
|
|
138
145
|
if (!existsSync(dir))
|
|
139
146
|
mkdirSync(dir, { recursive: true });
|
|
@@ -151,34 +158,518 @@ function writeJson(path, data) {
|
|
|
151
158
|
}
|
|
152
159
|
/* ── Step 1 — MCP server ───────────────────────────────────────── */
|
|
153
160
|
const ALMANAC_MCP_ENTRY = { command: "npx", args: ["-y", "openalmanac@latest"] };
|
|
161
|
+
const SUPPORTED_CLIENT_IDS = [
|
|
162
|
+
"claude-code",
|
|
163
|
+
"claude-desktop",
|
|
164
|
+
"codex",
|
|
165
|
+
"cursor",
|
|
166
|
+
"opencode",
|
|
167
|
+
"windsurf",
|
|
168
|
+
];
|
|
169
|
+
const SUPPORTED_CLIENTS = {
|
|
170
|
+
"claude-code": {
|
|
171
|
+
id: "claude-code",
|
|
172
|
+
name: "Claude Code",
|
|
173
|
+
selectionLabel: "Claude Code",
|
|
174
|
+
detect: () => hasCommand("claude") || existsSync(CLAUDE_JSON) || existsSync(CLAUDE_DIR),
|
|
175
|
+
configure: (mode) => {
|
|
176
|
+
const snippets = [
|
|
177
|
+
{
|
|
178
|
+
path: CLAUDE_JSON,
|
|
179
|
+
content: jsonSnippet({
|
|
180
|
+
mcpServers: {
|
|
181
|
+
almanac: { ...ALMANAC_MCP_ENTRY },
|
|
182
|
+
},
|
|
183
|
+
}),
|
|
184
|
+
},
|
|
185
|
+
{
|
|
186
|
+
path: CLAUDE_CODE_MCP,
|
|
187
|
+
content: jsonSnippet({
|
|
188
|
+
mcpServers: {
|
|
189
|
+
almanac: { ...ALMANAC_MCP_ENTRY },
|
|
190
|
+
},
|
|
191
|
+
}),
|
|
192
|
+
},
|
|
193
|
+
];
|
|
194
|
+
const changedPrimary = configureJsonMcpFile(CLAUDE_JSON, mode);
|
|
195
|
+
const changedSecondary = configureJsonMcpFile(CLAUDE_CODE_MCP, mode);
|
|
196
|
+
return { changed: changedPrimary || changedSecondary, snippets };
|
|
197
|
+
},
|
|
198
|
+
supportsPermissions: true,
|
|
199
|
+
},
|
|
200
|
+
"claude-desktop": {
|
|
201
|
+
id: "claude-desktop",
|
|
202
|
+
name: "Claude Desktop",
|
|
203
|
+
selectionLabel: "Claude Desktop",
|
|
204
|
+
detect: () => {
|
|
205
|
+
const path = getClaudeDesktopConfigPath();
|
|
206
|
+
return Boolean(path && (existsSync(path) || isClaudeDesktopInstalled()));
|
|
207
|
+
},
|
|
208
|
+
configure: (mode) => {
|
|
209
|
+
const path = getClaudeDesktopConfigPath();
|
|
210
|
+
if (!path)
|
|
211
|
+
return { changed: false, snippets: [] };
|
|
212
|
+
return {
|
|
213
|
+
changed: configureJsonMcpFile(path, mode),
|
|
214
|
+
snippets: [
|
|
215
|
+
{
|
|
216
|
+
path,
|
|
217
|
+
content: jsonSnippet({
|
|
218
|
+
mcpServers: {
|
|
219
|
+
almanac: { ...ALMANAC_MCP_ENTRY },
|
|
220
|
+
},
|
|
221
|
+
}),
|
|
222
|
+
},
|
|
223
|
+
],
|
|
224
|
+
};
|
|
225
|
+
},
|
|
226
|
+
},
|
|
227
|
+
codex: {
|
|
228
|
+
id: "codex",
|
|
229
|
+
name: "Codex",
|
|
230
|
+
selectionLabel: "Codex",
|
|
231
|
+
detect: () => hasCommand("codex") || existsSync(CODEX_CONFIG) || existsSync(join(homedir(), ".codex")),
|
|
232
|
+
configure: (mode) => ({
|
|
233
|
+
changed: configureCodexToml(CODEX_CONFIG, mode),
|
|
234
|
+
snippets: [
|
|
235
|
+
{
|
|
236
|
+
path: CODEX_CONFIG,
|
|
237
|
+
content: codexSnippet(),
|
|
238
|
+
},
|
|
239
|
+
],
|
|
240
|
+
}),
|
|
241
|
+
},
|
|
242
|
+
cursor: {
|
|
243
|
+
id: "cursor",
|
|
244
|
+
name: "Cursor",
|
|
245
|
+
selectionLabel: "Cursor",
|
|
246
|
+
detect: () => hasCommand("cursor-agent") ||
|
|
247
|
+
existsSync(CURSOR_MCP_JSON) ||
|
|
248
|
+
existsSync(join(homedir(), ".cursor")),
|
|
249
|
+
configure: (mode) => ({
|
|
250
|
+
changed: configureJsonMcpFile(CURSOR_MCP_JSON, mode),
|
|
251
|
+
snippets: [
|
|
252
|
+
{
|
|
253
|
+
path: CURSOR_MCP_JSON,
|
|
254
|
+
content: jsonSnippet({
|
|
255
|
+
mcpServers: {
|
|
256
|
+
almanac: { ...ALMANAC_MCP_ENTRY },
|
|
257
|
+
},
|
|
258
|
+
}),
|
|
259
|
+
},
|
|
260
|
+
],
|
|
261
|
+
}),
|
|
262
|
+
},
|
|
263
|
+
opencode: {
|
|
264
|
+
id: "opencode",
|
|
265
|
+
name: "OpenCode",
|
|
266
|
+
selectionLabel: "OpenCode",
|
|
267
|
+
detect: () => hasCommand("opencode") ||
|
|
268
|
+
existsSync(OPENCODE_JSON) ||
|
|
269
|
+
existsSync(OPENCODE_JSONC) ||
|
|
270
|
+
existsSync(OPENCODE_DIR),
|
|
271
|
+
configure: (mode) => {
|
|
272
|
+
const path = getOpenCodeConfigPath();
|
|
273
|
+
return {
|
|
274
|
+
changed: configureOpenCodeFile(path, mode),
|
|
275
|
+
snippets: [
|
|
276
|
+
{
|
|
277
|
+
path,
|
|
278
|
+
content: jsonSnippet({
|
|
279
|
+
"$schema": "https://opencode.ai/config.json",
|
|
280
|
+
mcp: {
|
|
281
|
+
almanac: {
|
|
282
|
+
type: "local",
|
|
283
|
+
command: [ALMANAC_MCP_ENTRY.command, ...ALMANAC_MCP_ENTRY.args],
|
|
284
|
+
enabled: true,
|
|
285
|
+
},
|
|
286
|
+
},
|
|
287
|
+
}),
|
|
288
|
+
},
|
|
289
|
+
],
|
|
290
|
+
};
|
|
291
|
+
},
|
|
292
|
+
},
|
|
293
|
+
windsurf: {
|
|
294
|
+
id: "windsurf",
|
|
295
|
+
name: "Windsurf",
|
|
296
|
+
selectionLabel: "Windsurf",
|
|
297
|
+
detect: () => hasCommand("windsurf") ||
|
|
298
|
+
existsSync(WINDSURF_MCP_JSON) ||
|
|
299
|
+
existsSync(join(homedir(), ".codeium")),
|
|
300
|
+
configure: (mode) => ({
|
|
301
|
+
changed: configureJsonMcpFile(WINDSURF_MCP_JSON, mode),
|
|
302
|
+
snippets: [
|
|
303
|
+
{
|
|
304
|
+
path: WINDSURF_MCP_JSON,
|
|
305
|
+
content: jsonSnippet({
|
|
306
|
+
mcpServers: {
|
|
307
|
+
almanac: { ...ALMANAC_MCP_ENTRY },
|
|
308
|
+
},
|
|
309
|
+
}),
|
|
310
|
+
},
|
|
311
|
+
],
|
|
312
|
+
}),
|
|
313
|
+
},
|
|
314
|
+
};
|
|
315
|
+
function parseSetupArgs(argv) {
|
|
316
|
+
const options = {
|
|
317
|
+
all: false,
|
|
318
|
+
clients: [],
|
|
319
|
+
dryRun: false,
|
|
320
|
+
print: false,
|
|
321
|
+
yes: false,
|
|
322
|
+
};
|
|
323
|
+
for (let i = 0; i < argv.length; i++) {
|
|
324
|
+
const arg = argv[i];
|
|
325
|
+
if (arg === "--all") {
|
|
326
|
+
options.all = true;
|
|
327
|
+
continue;
|
|
328
|
+
}
|
|
329
|
+
if (arg === "--print") {
|
|
330
|
+
options.print = true;
|
|
331
|
+
continue;
|
|
332
|
+
}
|
|
333
|
+
if (arg === "--dry-run") {
|
|
334
|
+
options.dryRun = true;
|
|
335
|
+
continue;
|
|
336
|
+
}
|
|
337
|
+
if (arg === "--yes" || arg === "-y") {
|
|
338
|
+
options.yes = true;
|
|
339
|
+
continue;
|
|
340
|
+
}
|
|
341
|
+
if (arg === "--client") {
|
|
342
|
+
const value = argv[i + 1];
|
|
343
|
+
if (!value) {
|
|
344
|
+
throw new Error("Missing value for --client");
|
|
345
|
+
}
|
|
346
|
+
i++;
|
|
347
|
+
options.clients.push(...parseClientList(value));
|
|
348
|
+
continue;
|
|
349
|
+
}
|
|
350
|
+
if (arg.startsWith("--client=")) {
|
|
351
|
+
options.clients.push(...parseClientList(arg.slice("--client=".length)));
|
|
352
|
+
continue;
|
|
353
|
+
}
|
|
354
|
+
throw new Error(`Unknown setup flag: ${arg}`);
|
|
355
|
+
}
|
|
356
|
+
if (options.all && options.clients.length > 0) {
|
|
357
|
+
throw new Error("Use either --all or --client, not both");
|
|
358
|
+
}
|
|
359
|
+
options.clients = Array.from(new Set(options.clients));
|
|
360
|
+
return options;
|
|
361
|
+
}
|
|
362
|
+
function parseClientList(value) {
|
|
363
|
+
return value
|
|
364
|
+
.split(",")
|
|
365
|
+
.map((part) => part.trim().toLowerCase())
|
|
366
|
+
.filter(Boolean)
|
|
367
|
+
.map((part) => normalizeClientId(part));
|
|
368
|
+
}
|
|
369
|
+
function normalizeClientId(value) {
|
|
370
|
+
const aliases = {
|
|
371
|
+
claude: "claude-code",
|
|
372
|
+
"claude-code": "claude-code",
|
|
373
|
+
"claude-desktop": "claude-desktop",
|
|
374
|
+
desktop: "claude-desktop",
|
|
375
|
+
codex: "codex",
|
|
376
|
+
cursor: "cursor",
|
|
377
|
+
opencode: "opencode",
|
|
378
|
+
windsurf: "windsurf",
|
|
379
|
+
};
|
|
380
|
+
const normalized = aliases[value];
|
|
381
|
+
if (!normalized) {
|
|
382
|
+
throw new Error(`Unsupported client "${value}". Supported clients: ${SUPPORTED_CLIENT_IDS.join(", ")}`);
|
|
383
|
+
}
|
|
384
|
+
return normalized;
|
|
385
|
+
}
|
|
386
|
+
function hasCommand(command) {
|
|
387
|
+
const checker = process.platform === "win32" ? "where" : "which";
|
|
388
|
+
const result = spawnSync(checker, [command], { stdio: "ignore" });
|
|
389
|
+
return result.status === 0;
|
|
390
|
+
}
|
|
391
|
+
function getClaudeDesktopConfigPath() {
|
|
392
|
+
if (process.platform === "darwin") {
|
|
393
|
+
return join(homedir(), "Library", "Application Support", "Claude", "claude_desktop_config.json");
|
|
394
|
+
}
|
|
395
|
+
if (process.platform === "linux") {
|
|
396
|
+
return join(homedir(), ".config", "Claude", "claude_desktop_config.json");
|
|
397
|
+
}
|
|
398
|
+
if (process.platform === "win32") {
|
|
399
|
+
const appData = process.env.APPDATA;
|
|
400
|
+
if (!appData)
|
|
401
|
+
return null;
|
|
402
|
+
return join(appData, "Claude", "claude_desktop_config.json");
|
|
403
|
+
}
|
|
404
|
+
return null;
|
|
405
|
+
}
|
|
406
|
+
function isClaudeDesktopInstalled() {
|
|
407
|
+
if (process.platform === "darwin") {
|
|
408
|
+
return (existsSync("/Applications/Claude.app") ||
|
|
409
|
+
existsSync(join(homedir(), "Applications", "Claude.app")));
|
|
410
|
+
}
|
|
411
|
+
if (process.platform === "linux") {
|
|
412
|
+
return (existsSync("/usr/share/applications/claude.desktop") ||
|
|
413
|
+
existsSync(join(homedir(), ".local", "share", "applications", "claude.desktop")) ||
|
|
414
|
+
existsSync("/opt/Claude/claude"));
|
|
415
|
+
}
|
|
416
|
+
if (process.platform === "win32") {
|
|
417
|
+
const localAppData = process.env.LOCALAPPDATA;
|
|
418
|
+
const programFiles = process.env.ProgramFiles;
|
|
419
|
+
return Boolean((localAppData &&
|
|
420
|
+
existsSync(join(localAppData, "Programs", "Claude", "Claude.exe"))) ||
|
|
421
|
+
(programFiles && existsSync(join(programFiles, "Claude", "Claude.exe"))));
|
|
422
|
+
}
|
|
423
|
+
return false;
|
|
424
|
+
}
|
|
425
|
+
function jsonSnippet(data) {
|
|
426
|
+
return JSON.stringify(data, null, 2);
|
|
427
|
+
}
|
|
428
|
+
function getOpenCodeConfigPath() {
|
|
429
|
+
if (existsSync(OPENCODE_JSON))
|
|
430
|
+
return OPENCODE_JSON;
|
|
431
|
+
if (existsSync(OPENCODE_JSONC))
|
|
432
|
+
return OPENCODE_JSONC;
|
|
433
|
+
return OPENCODE_JSON;
|
|
434
|
+
}
|
|
435
|
+
function codexSnippet() {
|
|
436
|
+
return [
|
|
437
|
+
"[mcp_servers.almanac]",
|
|
438
|
+
`command = ${tomlString(ALMANAC_MCP_ENTRY.command)}`,
|
|
439
|
+
`args = ${tomlArray(ALMANAC_MCP_ENTRY.args)}`,
|
|
440
|
+
].join("\n");
|
|
441
|
+
}
|
|
154
442
|
function isAlmanacCurrent(server) {
|
|
155
443
|
return (server?.command === "npx" &&
|
|
156
444
|
JSON.stringify(server.args) === JSON.stringify(ALMANAC_MCP_ENTRY.args));
|
|
157
445
|
}
|
|
158
|
-
function
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
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);
|
|
446
|
+
function isOpenCodeAlmanacCurrent(entry) {
|
|
447
|
+
return (entry?.type === "local" &&
|
|
448
|
+
entry?.enabled === true &&
|
|
449
|
+
JSON.stringify(entry.command) ===
|
|
450
|
+
JSON.stringify([ALMANAC_MCP_ENTRY.command, ...ALMANAC_MCP_ENTRY.args]));
|
|
451
|
+
}
|
|
452
|
+
function configureJsonMcpFile(path, mode) {
|
|
172
453
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
173
|
-
const
|
|
174
|
-
if (!
|
|
175
|
-
|
|
176
|
-
if (
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
454
|
+
const json = readJson(path);
|
|
455
|
+
if (!json.mcpServers)
|
|
456
|
+
json.mcpServers = {};
|
|
457
|
+
if (isAlmanacCurrent(json.mcpServers.almanac)) {
|
|
458
|
+
return false;
|
|
459
|
+
}
|
|
460
|
+
if (mode === "apply") {
|
|
461
|
+
ensureDir(dirname(path));
|
|
462
|
+
json.mcpServers.almanac = { ...ALMANAC_MCP_ENTRY };
|
|
463
|
+
writeJson(path, json);
|
|
464
|
+
}
|
|
465
|
+
return true;
|
|
466
|
+
}
|
|
467
|
+
function configureCodexToml(path, mode) {
|
|
468
|
+
const current = readToml(path);
|
|
469
|
+
const next = upsertCodexServer(current);
|
|
470
|
+
if (current.trim() === next.trim()) {
|
|
471
|
+
return false;
|
|
472
|
+
}
|
|
473
|
+
if (mode === "apply") {
|
|
474
|
+
ensureDir(dirname(path));
|
|
475
|
+
writeFileSync(path, next.endsWith("\n") ? next : next + "\n");
|
|
476
|
+
}
|
|
477
|
+
return true;
|
|
478
|
+
}
|
|
479
|
+
function configureOpenCodeFile(path, mode) {
|
|
480
|
+
const current = readJsonWithComments(path);
|
|
481
|
+
if (!current.$schema) {
|
|
482
|
+
current.$schema = "https://opencode.ai/config.json";
|
|
483
|
+
}
|
|
484
|
+
const mcp = isRecord(current.mcp) ? current.mcp : {};
|
|
485
|
+
if (!isRecord(current.mcp)) {
|
|
486
|
+
current.mcp = mcp;
|
|
487
|
+
}
|
|
488
|
+
if (isOpenCodeAlmanacCurrent(mcp.almanac)) {
|
|
489
|
+
return false;
|
|
490
|
+
}
|
|
491
|
+
mcp.almanac = {
|
|
492
|
+
type: "local",
|
|
493
|
+
command: [ALMANAC_MCP_ENTRY.command, ...ALMANAC_MCP_ENTRY.args],
|
|
494
|
+
enabled: true,
|
|
495
|
+
};
|
|
496
|
+
if (mode === "apply") {
|
|
497
|
+
ensureDir(dirname(path));
|
|
498
|
+
writeJson(path, current);
|
|
499
|
+
}
|
|
500
|
+
return true;
|
|
501
|
+
}
|
|
502
|
+
function readToml(path) {
|
|
503
|
+
try {
|
|
504
|
+
return readFileSync(path, "utf-8");
|
|
505
|
+
}
|
|
506
|
+
catch {
|
|
507
|
+
return "";
|
|
180
508
|
}
|
|
181
|
-
|
|
509
|
+
}
|
|
510
|
+
function readJsonWithComments(path) {
|
|
511
|
+
try {
|
|
512
|
+
const raw = readFileSync(path, "utf-8");
|
|
513
|
+
const parsed = JSON.parse(stripJsonComments(raw));
|
|
514
|
+
return isRecord(parsed) ? parsed : {};
|
|
515
|
+
}
|
|
516
|
+
catch {
|
|
517
|
+
return {};
|
|
518
|
+
}
|
|
519
|
+
}
|
|
520
|
+
function stripJsonComments(input) {
|
|
521
|
+
let result = "";
|
|
522
|
+
let inString = false;
|
|
523
|
+
let inLineComment = false;
|
|
524
|
+
let inBlockComment = false;
|
|
525
|
+
let escaped = false;
|
|
526
|
+
for (let i = 0; i < input.length; i++) {
|
|
527
|
+
const char = input[i];
|
|
528
|
+
const next = input[i + 1];
|
|
529
|
+
if (inLineComment) {
|
|
530
|
+
if (char === "\n") {
|
|
531
|
+
inLineComment = false;
|
|
532
|
+
result += char;
|
|
533
|
+
}
|
|
534
|
+
continue;
|
|
535
|
+
}
|
|
536
|
+
if (inBlockComment) {
|
|
537
|
+
if (char === "*" && next === "/") {
|
|
538
|
+
inBlockComment = false;
|
|
539
|
+
i++;
|
|
540
|
+
}
|
|
541
|
+
continue;
|
|
542
|
+
}
|
|
543
|
+
if (inString) {
|
|
544
|
+
result += char;
|
|
545
|
+
if (escaped) {
|
|
546
|
+
escaped = false;
|
|
547
|
+
}
|
|
548
|
+
else if (char === "\\") {
|
|
549
|
+
escaped = true;
|
|
550
|
+
}
|
|
551
|
+
else if (char === "\"") {
|
|
552
|
+
inString = false;
|
|
553
|
+
}
|
|
554
|
+
continue;
|
|
555
|
+
}
|
|
556
|
+
if (char === "/" && next === "/") {
|
|
557
|
+
inLineComment = true;
|
|
558
|
+
i++;
|
|
559
|
+
continue;
|
|
560
|
+
}
|
|
561
|
+
if (char === "/" && next === "*") {
|
|
562
|
+
inBlockComment = true;
|
|
563
|
+
i++;
|
|
564
|
+
continue;
|
|
565
|
+
}
|
|
566
|
+
if (char === "\"") {
|
|
567
|
+
inString = true;
|
|
568
|
+
}
|
|
569
|
+
result += char;
|
|
570
|
+
}
|
|
571
|
+
return result;
|
|
572
|
+
}
|
|
573
|
+
function isRecord(value) {
|
|
574
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
575
|
+
}
|
|
576
|
+
function upsertCodexServer(content) {
|
|
577
|
+
const sectionName = "mcp_servers.almanac";
|
|
578
|
+
const header = `[${sectionName}]`;
|
|
579
|
+
const nextBody = [
|
|
580
|
+
`command = ${tomlString(ALMANAC_MCP_ENTRY.command)}`,
|
|
581
|
+
`args = ${tomlArray(ALMANAC_MCP_ENTRY.args)}`,
|
|
582
|
+
];
|
|
583
|
+
const lines = content === "" ? [] : content.split(/\r?\n/);
|
|
584
|
+
const start = lines.findIndex((line) => line.trim() === header);
|
|
585
|
+
if (start === -1) {
|
|
586
|
+
const prefix = content.trimEnd();
|
|
587
|
+
const block = [header, ...nextBody].join("\n");
|
|
588
|
+
return prefix === "" ? block + "\n" : `${prefix}\n\n${block}\n`;
|
|
589
|
+
}
|
|
590
|
+
let end = lines.length;
|
|
591
|
+
for (let i = start + 1; i < lines.length; i++) {
|
|
592
|
+
if (/^\s*\[.+\]\s*$/.test(lines[i])) {
|
|
593
|
+
end = i;
|
|
594
|
+
break;
|
|
595
|
+
}
|
|
596
|
+
}
|
|
597
|
+
const existingBody = lines.slice(start + 1, end);
|
|
598
|
+
const preserved = existingBody.filter((line) => {
|
|
599
|
+
const trimmed = line.trim();
|
|
600
|
+
return !trimmed.startsWith("command =") && !trimmed.startsWith("args =");
|
|
601
|
+
});
|
|
602
|
+
const replacement = [header, ...nextBody, ...preserved];
|
|
603
|
+
const updated = [...lines.slice(0, start), ...replacement, ...lines.slice(end)];
|
|
604
|
+
return updated.join("\n").replace(/\n{3,}/g, "\n\n").replace(/\s+$/, "") + "\n";
|
|
605
|
+
}
|
|
606
|
+
function tomlString(value) {
|
|
607
|
+
return JSON.stringify(value);
|
|
608
|
+
}
|
|
609
|
+
function tomlArray(values) {
|
|
610
|
+
return `[${values.map((value) => tomlString(value)).join(", ")}]`;
|
|
611
|
+
}
|
|
612
|
+
function detectClients() {
|
|
613
|
+
return SUPPORTED_CLIENT_IDS.map((id) => SUPPORTED_CLIENTS[id]).filter((client) => client.detect());
|
|
614
|
+
}
|
|
615
|
+
function resolveClients(options) {
|
|
616
|
+
if (options.clients.length > 0) {
|
|
617
|
+
return options.clients.map((id) => SUPPORTED_CLIENTS[id]);
|
|
618
|
+
}
|
|
619
|
+
const detected = detectClients();
|
|
620
|
+
if (options.all)
|
|
621
|
+
return detected;
|
|
622
|
+
return detected;
|
|
623
|
+
}
|
|
624
|
+
function applyClientSetup(clients, mode) {
|
|
625
|
+
const configured = [];
|
|
626
|
+
const alreadyConfigured = [];
|
|
627
|
+
for (const client of clients) {
|
|
628
|
+
const result = client.configure(mode);
|
|
629
|
+
if (result.changed) {
|
|
630
|
+
configured.push(client.name);
|
|
631
|
+
}
|
|
632
|
+
else {
|
|
633
|
+
alreadyConfigured.push(client.name);
|
|
634
|
+
}
|
|
635
|
+
}
|
|
636
|
+
return { configured, alreadyConfigured };
|
|
637
|
+
}
|
|
638
|
+
function printSetupPlan(clients, options) {
|
|
639
|
+
const heading = options.dryRun ? "Dry run" : "OpenAlmanac MCP setup";
|
|
640
|
+
process.stdout.write(`${heading}\n\n`);
|
|
641
|
+
if (clients.length === 0) {
|
|
642
|
+
process.stdout.write("No supported clients detected. Use --client <name> to force a target or --print to inspect supported snippets.\n");
|
|
643
|
+
if (options.print) {
|
|
644
|
+
process.stdout.write("\nSupported clients:\n");
|
|
645
|
+
for (const id of SUPPORTED_CLIENT_IDS) {
|
|
646
|
+
process.stdout.write(`- ${SUPPORTED_CLIENTS[id].name}\n`);
|
|
647
|
+
}
|
|
648
|
+
}
|
|
649
|
+
return;
|
|
650
|
+
}
|
|
651
|
+
const mode = options.print ? "print" : "dry-run";
|
|
652
|
+
for (const client of clients) {
|
|
653
|
+
const result = client.configure(mode);
|
|
654
|
+
const status = result.changed
|
|
655
|
+
? options.print
|
|
656
|
+
? "snippet"
|
|
657
|
+
: "would configure"
|
|
658
|
+
: "already configured";
|
|
659
|
+
process.stdout.write(`- ${client.name}: ${status}\n`);
|
|
660
|
+
if (options.print) {
|
|
661
|
+
for (const snippet of result.snippets) {
|
|
662
|
+
process.stdout.write(` Path: ${snippet.path}\n`);
|
|
663
|
+
process.stdout.write(`${indentBlock(snippet.content, " ")}\n`);
|
|
664
|
+
}
|
|
665
|
+
}
|
|
666
|
+
}
|
|
667
|
+
}
|
|
668
|
+
function indentBlock(content, prefix) {
|
|
669
|
+
return content
|
|
670
|
+
.split("\n")
|
|
671
|
+
.map((line) => `${prefix}${line}`)
|
|
672
|
+
.join("\n");
|
|
182
673
|
}
|
|
183
674
|
/* ── Step 2 — Permissions ──────────────────────────────────────── */
|
|
184
675
|
function configurePermissions(tools) {
|
|
@@ -201,29 +692,30 @@ function configurePermissions(tools) {
|
|
|
201
692
|
writeJson(SETTINGS_JSON, settings);
|
|
202
693
|
return tools.length;
|
|
203
694
|
}
|
|
204
|
-
/* ──
|
|
205
|
-
function
|
|
695
|
+
/* ── Client selection screen ────────────────────────────────────── */
|
|
696
|
+
function renderClientSelect(clients, selected, cursor, mode = "default") {
|
|
206
697
|
process.stdout.write("\x1b[2J\x1b[H");
|
|
207
698
|
renderHeader(mode);
|
|
208
699
|
printBadge();
|
|
209
700
|
w("");
|
|
210
|
-
stepActive(`Select
|
|
701
|
+
stepActive(`Select where to install Almanac`);
|
|
211
702
|
w(BAR);
|
|
212
|
-
for (
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
}
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
}
|
|
703
|
+
for (let i = 0; i < clients.length; i++) {
|
|
704
|
+
const client = clients[i];
|
|
705
|
+
const arrow = i === cursor ? `${BLUE}\u276f${RST}` : " ";
|
|
706
|
+
const check = selected[i] ? `${BLUE}\u2713${RST}` : " ";
|
|
707
|
+
const label = i === cursor ? `${BOLD}${client.selectionLabel ?? client.name}${RST}` : client.selectionLabel ?? client.name;
|
|
708
|
+
w(` ${DIM}\u2502${RST} ${arrow} [${check}] ${label}`);
|
|
219
709
|
}
|
|
220
710
|
w(BAR);
|
|
221
|
-
w(` ${DIM}\u2502${RST} ${BLUE}${BOLD}[enter]${RST} confirm ${DIM}[q] quit${RST}`);
|
|
711
|
+
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
712
|
w("");
|
|
223
713
|
}
|
|
224
|
-
function
|
|
714
|
+
function runClientSelect(clients, mode = "default") {
|
|
225
715
|
return new Promise((resolve) => {
|
|
226
|
-
|
|
716
|
+
const selected = clients.map(() => true);
|
|
717
|
+
let cursor = 0;
|
|
718
|
+
renderClientSelect(clients, selected, cursor, mode);
|
|
227
719
|
process.stdin.setRawMode(true);
|
|
228
720
|
process.stdin.resume();
|
|
229
721
|
process.stdin.setEncoding("utf-8");
|
|
@@ -239,16 +731,38 @@ function runAgentSelect(mode = "default") {
|
|
|
239
731
|
console.log("\n Setup cancelled.\n");
|
|
240
732
|
process.exit(0);
|
|
241
733
|
}
|
|
242
|
-
if (key === "\
|
|
734
|
+
if (key === "\x1b[A" || key === "k") {
|
|
735
|
+
cursor = (cursor - 1 + clients.length) % clients.length;
|
|
736
|
+
}
|
|
737
|
+
else if (key === "\x1b[B" || key === "j") {
|
|
738
|
+
cursor = (cursor + 1) % clients.length;
|
|
739
|
+
}
|
|
740
|
+
else if (key === " ") {
|
|
741
|
+
selected[cursor] = !selected[cursor];
|
|
742
|
+
}
|
|
743
|
+
else if (key === "a") {
|
|
744
|
+
const all = selected.every(Boolean);
|
|
745
|
+
selected.fill(!all);
|
|
746
|
+
}
|
|
747
|
+
else if (key === "\r" || key === "\n") {
|
|
243
748
|
cleanup();
|
|
244
|
-
const
|
|
245
|
-
|
|
749
|
+
const chosen = clients.filter((_, index) => selected[index]);
|
|
750
|
+
if (chosen.length === 0) {
|
|
751
|
+
console.log("\n Select at least one client.\n");
|
|
752
|
+
process.exit(1);
|
|
753
|
+
}
|
|
754
|
+
resolve(chosen);
|
|
246
755
|
return;
|
|
247
756
|
}
|
|
757
|
+
renderClientSelect(clients, selected, cursor, mode);
|
|
248
758
|
};
|
|
249
759
|
process.stdin.on("data", onData);
|
|
250
760
|
});
|
|
251
761
|
}
|
|
762
|
+
async function runAgentSelect(mode = "default") {
|
|
763
|
+
const [client] = await runClientSelect([SUPPORTED_CLIENTS["claude-code"]], mode);
|
|
764
|
+
return client.name;
|
|
765
|
+
}
|
|
252
766
|
/* ── Login step ─────────────────────────────────────────────────── */
|
|
253
767
|
function loginLabel(result) {
|
|
254
768
|
if (result.status === "already")
|
|
@@ -278,12 +792,15 @@ function waitForKey(prompt) {
|
|
|
278
792
|
});
|
|
279
793
|
}
|
|
280
794
|
async function runLoginStep(agent, mcpChanged, toolCount, mode = "default") {
|
|
795
|
+
const label = agent.includes(",") ? "Clients" : "Agent";
|
|
281
796
|
const priorSteps = () => {
|
|
282
|
-
stepDone(
|
|
797
|
+
stepDone(`${label} \u2192 ${WHITE_BOLD}${agent}${RST}`);
|
|
283
798
|
w(BAR);
|
|
284
799
|
stepDone(`MCP server ${mcpChanged ? "configured" : `${DIM}already configured${RST}`}`);
|
|
285
|
-
|
|
286
|
-
|
|
800
|
+
if (toolCount !== null) {
|
|
801
|
+
w(BAR);
|
|
802
|
+
stepDone(`${BLUE}${toolCount}${RST} tool${toolCount !== 1 ? "s" : ""} allowed`);
|
|
803
|
+
}
|
|
287
804
|
w(BAR);
|
|
288
805
|
};
|
|
289
806
|
function renderLoginChoice(name, cursor) {
|
|
@@ -428,16 +945,16 @@ async function runLoginStep(agent, mcpChanged, toolCount, mode = "default") {
|
|
|
428
945
|
}
|
|
429
946
|
/* ── Tool permissions TUI ───────────────────────────────────────── */
|
|
430
947
|
const MAX_NAME = Math.max(...TOOL_GROUPS.map((g) => g.name.length));
|
|
431
|
-
function renderToolSelect(selected, cursor,
|
|
948
|
+
function renderToolSelect(selected, cursor, clientsLabel, mcpChanged, mode = "default") {
|
|
432
949
|
process.stdout.write("\x1b[2J\x1b[H");
|
|
433
950
|
renderHeader(mode);
|
|
434
951
|
printBadge();
|
|
435
952
|
w("");
|
|
436
|
-
stepDone(`
|
|
953
|
+
stepDone(`Clients \u2192 ${WHITE_BOLD}${clientsLabel}${RST}`);
|
|
437
954
|
w(BAR);
|
|
438
955
|
stepDone(`MCP server ${mcpChanged ? "configured" : `${DIM}already configured${RST}`}`);
|
|
439
956
|
w(BAR);
|
|
440
|
-
stepActive(`Select tool permissions ${DIM}deselect any you'd rather approve manually${RST}`);
|
|
957
|
+
stepActive(`Select Claude Code tool permissions ${DIM}deselect any you'd rather approve manually${RST}`);
|
|
441
958
|
w(BAR);
|
|
442
959
|
for (let i = 0; i < TOOL_GROUPS.length; i++) {
|
|
443
960
|
const arrow = i === cursor ? `${BLUE}\u276f${RST}` : " ";
|
|
@@ -451,11 +968,11 @@ function renderToolSelect(selected, cursor, agent, mcpChanged, mode = "default")
|
|
|
451
968
|
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
969
|
w("");
|
|
453
970
|
}
|
|
454
|
-
function runToolSelect(
|
|
971
|
+
function runToolSelect(clientsLabel, mcpChanged, mode = "default") {
|
|
455
972
|
return new Promise((resolve) => {
|
|
456
973
|
const selected = TOOL_GROUPS.map(() => true);
|
|
457
974
|
let cursor = 0;
|
|
458
|
-
renderToolSelect(selected, cursor,
|
|
975
|
+
renderToolSelect(selected, cursor, clientsLabel, mcpChanged, mode);
|
|
459
976
|
process.stdin.setRawMode(true);
|
|
460
977
|
process.stdin.resume();
|
|
461
978
|
process.stdin.setEncoding("utf-8");
|
|
@@ -491,23 +1008,27 @@ function runToolSelect(agent, mcpChanged, mode = "default") {
|
|
|
491
1008
|
resolve(tools);
|
|
492
1009
|
return;
|
|
493
1010
|
}
|
|
494
|
-
renderToolSelect(selected, cursor,
|
|
1011
|
+
renderToolSelect(selected, cursor, clientsLabel, mcpChanged, mode);
|
|
495
1012
|
};
|
|
496
1013
|
process.stdin.on("data", onData);
|
|
497
1014
|
});
|
|
498
1015
|
}
|
|
499
1016
|
/* ── Result screen ──────────────────────────────────────────────── */
|
|
500
|
-
function printResult(
|
|
1017
|
+
function printResult(clientsLabel, loginResult, configured, alreadyConfigured, toolCount) {
|
|
501
1018
|
process.stdout.write("\x1b[2J\x1b[H");
|
|
502
1019
|
printBanner();
|
|
503
1020
|
printBadge();
|
|
504
1021
|
w("");
|
|
505
|
-
stepDone(`
|
|
1022
|
+
stepDone(`Clients \u2192 ${WHITE_BOLD}${clientsLabel}${RST}`);
|
|
506
1023
|
w(BAR);
|
|
507
|
-
stepDone(`
|
|
1024
|
+
stepDone(`Configured \u2192 ${configured.length > 0 ? configured.join(", ") : `${DIM}none${RST}`}`);
|
|
508
1025
|
w(BAR);
|
|
509
|
-
stepDone(
|
|
1026
|
+
stepDone(`Already configured \u2192 ${alreadyConfigured.length > 0 ? alreadyConfigured.join(", ") : `${DIM}none${RST}`}`);
|
|
510
1027
|
w(BAR);
|
|
1028
|
+
if (toolCount > 0) {
|
|
1029
|
+
stepDone(`${BLUE}${toolCount}${RST} tool${toolCount !== 1 ? "s" : ""} allowed in ${DIM}~/.claude/settings.json${RST}`);
|
|
1030
|
+
w(BAR);
|
|
1031
|
+
}
|
|
511
1032
|
stepDone(loginLabel(loginResult));
|
|
512
1033
|
w(BAR);
|
|
513
1034
|
stepDone(`${BLUE}Setup complete${RST}`);
|
|
@@ -519,37 +1040,95 @@ function printResult(agent, loginResult, mcpChanged, toolCount) {
|
|
|
519
1040
|
return ` ${BLUE_DIM}\u2502${RST}${content}${" ".repeat(padding)}${BLUE_DIM}\u2502${RST}`;
|
|
520
1041
|
};
|
|
521
1042
|
const empty = row("");
|
|
1043
|
+
const nextSteps = getNextSteps(clientsLabel);
|
|
522
1044
|
w(` ${BLUE_DIM}\u256d${"─".repeat(innerW)}\u256e${RST}`);
|
|
523
1045
|
w(empty);
|
|
524
1046
|
w(row(` ${WHITE_BOLD}Next steps${RST}`));
|
|
525
1047
|
w(empty);
|
|
526
|
-
|
|
527
|
-
|
|
1048
|
+
for (let i = 0; i < nextSteps.length; i++) {
|
|
1049
|
+
w(row(` ${BLUE}${i + 1}.${RST} ${nextSteps[i]}`));
|
|
1050
|
+
}
|
|
528
1051
|
w(empty);
|
|
529
1052
|
w(` ${BLUE_DIM}\u2570${"─".repeat(innerW)}\u256f${RST}`);
|
|
530
1053
|
w("");
|
|
531
1054
|
}
|
|
1055
|
+
function getNextSteps(clientsLabel) {
|
|
1056
|
+
if (clientsLabel === "Claude Code") {
|
|
1057
|
+
return [
|
|
1058
|
+
`Type ${WHITE_BOLD}claude${RST} to start Claude Code`,
|
|
1059
|
+
`Say ${BLUE}"Let's explore <topic> using Almanac"${RST}`,
|
|
1060
|
+
];
|
|
1061
|
+
}
|
|
1062
|
+
if (clientsLabel === "Codex") {
|
|
1063
|
+
return [
|
|
1064
|
+
`Type ${WHITE_BOLD}codex${RST} to start Codex`,
|
|
1065
|
+
`Ask ${BLUE}"Let's explore <topic> using Almanac"${RST}`,
|
|
1066
|
+
];
|
|
1067
|
+
}
|
|
1068
|
+
if (clientsLabel === "Cursor") {
|
|
1069
|
+
return [
|
|
1070
|
+
`Open ${WHITE_BOLD}Cursor${RST} in your project`,
|
|
1071
|
+
`Ask ${BLUE}"Let's explore <topic> using Almanac"${RST}`,
|
|
1072
|
+
];
|
|
1073
|
+
}
|
|
1074
|
+
if (clientsLabel === "OpenCode") {
|
|
1075
|
+
return [
|
|
1076
|
+
`Type ${WHITE_BOLD}opencode${RST} to start OpenCode`,
|
|
1077
|
+
`Ask ${BLUE}"Let's explore <topic> using Almanac"${RST}`,
|
|
1078
|
+
];
|
|
1079
|
+
}
|
|
1080
|
+
if (clientsLabel === "Windsurf") {
|
|
1081
|
+
return [
|
|
1082
|
+
`Open ${WHITE_BOLD}Windsurf${RST} in your project`,
|
|
1083
|
+
`Ask ${BLUE}"Let's explore <topic> using Almanac"${RST}`,
|
|
1084
|
+
];
|
|
1085
|
+
}
|
|
1086
|
+
if (clientsLabel === "Claude Desktop") {
|
|
1087
|
+
return [
|
|
1088
|
+
`Open ${WHITE_BOLD}Claude Desktop${RST}`,
|
|
1089
|
+
`Ask ${BLUE}"Let's explore <topic> using Almanac"${RST}`,
|
|
1090
|
+
];
|
|
1091
|
+
}
|
|
1092
|
+
return [
|
|
1093
|
+
`Open one of your configured agents in this project`,
|
|
1094
|
+
`Ask ${BLUE}"Let's explore <topic> using Almanac"${RST}`,
|
|
1095
|
+
];
|
|
1096
|
+
}
|
|
532
1097
|
/* ── Entry point ────────────────────────────────────────────────── */
|
|
533
1098
|
export async function runSetup() {
|
|
534
|
-
const
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
1099
|
+
const options = parseSetupArgs(process.argv.slice(3));
|
|
1100
|
+
let clients = resolveClients(options);
|
|
1101
|
+
if (options.print || options.dryRun) {
|
|
1102
|
+
printSetupPlan(clients, options);
|
|
1103
|
+
process.exit(0);
|
|
539
1104
|
}
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
tools = await runToolSelect(agent, mcpChanged);
|
|
1105
|
+
if (clients.length === 0) {
|
|
1106
|
+
printSetupPlan(clients, options);
|
|
1107
|
+
process.exit(0);
|
|
544
1108
|
}
|
|
545
|
-
|
|
546
|
-
|
|
1109
|
+
const skipTui = options.yes;
|
|
1110
|
+
const interactive = process.stdin.isTTY && !skipTui;
|
|
1111
|
+
if (interactive && options.clients.length === 0) {
|
|
1112
|
+
clients = await runClientSelect(clients);
|
|
547
1113
|
}
|
|
548
|
-
const
|
|
549
|
-
|
|
1114
|
+
const clientsLabel = clients.map((client) => client.name).join(", ");
|
|
1115
|
+
const setupSummary = applyClientSetup(clients, "apply");
|
|
1116
|
+
const permissionClient = clients.find((client) => client.supportsPermissions);
|
|
1117
|
+
let tools = [];
|
|
1118
|
+
if (permissionClient) {
|
|
1119
|
+
const mcpChanged = setupSummary.configured.includes(permissionClient.name);
|
|
1120
|
+
if (interactive) {
|
|
1121
|
+
tools = await runToolSelect(clientsLabel, mcpChanged);
|
|
1122
|
+
}
|
|
1123
|
+
else {
|
|
1124
|
+
tools = TOOL_GROUPS.flatMap((g) => g.tools);
|
|
1125
|
+
}
|
|
1126
|
+
}
|
|
1127
|
+
const count = tools.length > 0 ? configurePermissions(tools) : 0;
|
|
1128
|
+
const permissionCount = permissionClient ? count : null;
|
|
550
1129
|
let loginResult;
|
|
551
1130
|
if (interactive) {
|
|
552
|
-
loginResult = await runLoginStep(
|
|
1131
|
+
loginResult = await runLoginStep(clientsLabel, setupSummary.configured.length > 0, permissionCount);
|
|
553
1132
|
}
|
|
554
1133
|
else {
|
|
555
1134
|
try {
|
|
@@ -563,7 +1142,7 @@ export async function runSetup() {
|
|
|
563
1142
|
loginResult = { status: "skipped" };
|
|
564
1143
|
}
|
|
565
1144
|
}
|
|
566
|
-
printResult(
|
|
1145
|
+
printResult(clientsLabel, loginResult, setupSummary.configured, setupSummary.alreadyConfigured, count);
|
|
567
1146
|
process.exit(0);
|
|
568
1147
|
}
|
|
569
1148
|
/* ── Skill installation ────────────────────────────────────────── */
|
|
@@ -629,7 +1208,8 @@ export async function runRedditSetup() {
|
|
|
629
1208
|
if (interactive) {
|
|
630
1209
|
agent = await runAgentSelect("reddit");
|
|
631
1210
|
}
|
|
632
|
-
const
|
|
1211
|
+
const claudeSetup = SUPPORTED_CLIENTS["claude-code"].configure("apply");
|
|
1212
|
+
const mcpChanged = claudeSetup.changed;
|
|
633
1213
|
let tools;
|
|
634
1214
|
if (interactive) {
|
|
635
1215
|
tools = await runToolSelect(agent, mcpChanged, "reddit");
|