openalmanac 0.3.6 → 0.4.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/auth.d.ts +2 -2
- package/dist/auth.js +2 -2
- package/dist/cli.js +1 -1
- package/dist/instructions.d.ts +1 -0
- package/dist/instructions.js +150 -0
- package/dist/login-core.js +2 -1
- package/dist/onboarding-copy.d.ts +1 -0
- package/dist/onboarding-copy.js +14 -0
- package/dist/openalmanac_mcp-0.3.1-py3-none-any.whl +0 -0
- package/dist/openalmanac_mcp-0.3.1.tar.gz +0 -0
- package/dist/openalmanac_mcp-0.3.2-py3-none-any.whl +0 -0
- package/dist/openalmanac_mcp-0.3.2.tar.gz +0 -0
- package/dist/server.js +5 -150
- package/dist/setup/clients.d.ts +10 -0
- package/dist/setup/clients.js +291 -0
- package/dist/setup/config-files.d.ts +43 -0
- package/dist/setup/config-files.js +257 -0
- package/dist/setup/index.d.ts +2 -0
- package/dist/setup/index.js +55 -0
- package/dist/setup/permissions.d.ts +3 -0
- package/dist/setup/permissions.js +52 -0
- package/dist/{setup.d.ts → setup/reddit.d.ts} +0 -1
- package/dist/setup/reddit.js +69 -0
- package/dist/setup/tui.d.ts +7 -0
- package/dist/setup/tui.js +496 -0
- package/dist/setup/types.d.ts +43 -0
- package/dist/setup/types.js +1 -0
- package/dist/tool-registry.d.ts +11 -0
- package/dist/tool-registry.js +148 -0
- package/dist/tools/auth.js +1 -1
- package/dist/tools/{pages.js → pages/index.js} +39 -202
- package/dist/tools/pages/publish-format.d.ts +48 -0
- package/dist/tools/pages/publish-format.js +92 -0
- package/dist/tools/pages/workspace.d.ts +7 -0
- package/dist/tools/pages/workspace.js +14 -0
- package/dist/tools/pages/writing-guide.d.ts +1 -0
- package/dist/tools/pages/writing-guide.js +56 -0
- package/dist/tools/research.js +16 -15
- package/package.json +15 -6
- package/skills/reddit-wiki/SKILL.md +46 -46
- package/dist/setup.js +0 -1243
- package/dist/validate.d.ts +0 -971
- package/dist/validate.js +0 -154
- /package/dist/tools/{pages.d.ts → pages/index.d.ts} +0 -0
|
@@ -0,0 +1,291 @@
|
|
|
1
|
+
import { existsSync } from "fs";
|
|
2
|
+
import { homedir } from "os";
|
|
3
|
+
import { join } from "path";
|
|
4
|
+
import { ALMANAC_MCP_ENTRY, CLAUDE_CODE_MCP, CLAUDE_DIR, CLAUDE_JSON, CODEX_CONFIG, CURSOR_MCP_JSON, OPENCODE_DIR, OPENCODE_JSON, OPENCODE_JSONC, WINDSURF_MCP_JSON, codexSnippet, configureCodexToml, configureJsonMcpFile, configureOpenCodeFile, getClaudeDesktopConfigPath, getOpenCodeConfigPath, hasCommand, isClaudeDesktopInstalled, jsonSnippet, } from "./config-files.js";
|
|
5
|
+
export const SUPPORTED_CLIENT_IDS = [
|
|
6
|
+
"claude-code",
|
|
7
|
+
"claude-desktop",
|
|
8
|
+
"codex",
|
|
9
|
+
"cursor",
|
|
10
|
+
"opencode",
|
|
11
|
+
"windsurf",
|
|
12
|
+
];
|
|
13
|
+
export const SUPPORTED_CLIENTS = {
|
|
14
|
+
"claude-code": {
|
|
15
|
+
id: "claude-code",
|
|
16
|
+
name: "Claude Code",
|
|
17
|
+
selectionLabel: "Claude Code",
|
|
18
|
+
detect: () => hasCommand("claude") || existsSync(CLAUDE_JSON) || existsSync(CLAUDE_DIR),
|
|
19
|
+
configure: (mode) => {
|
|
20
|
+
const snippets = [
|
|
21
|
+
{
|
|
22
|
+
path: CLAUDE_JSON,
|
|
23
|
+
content: jsonSnippet({
|
|
24
|
+
mcpServers: {
|
|
25
|
+
almanac: { ...ALMANAC_MCP_ENTRY },
|
|
26
|
+
},
|
|
27
|
+
}),
|
|
28
|
+
},
|
|
29
|
+
{
|
|
30
|
+
path: CLAUDE_CODE_MCP,
|
|
31
|
+
content: jsonSnippet({
|
|
32
|
+
mcpServers: {
|
|
33
|
+
almanac: { ...ALMANAC_MCP_ENTRY },
|
|
34
|
+
},
|
|
35
|
+
}),
|
|
36
|
+
},
|
|
37
|
+
];
|
|
38
|
+
const changedPrimary = configureJsonMcpFile(CLAUDE_JSON, mode);
|
|
39
|
+
const changedSecondary = configureJsonMcpFile(CLAUDE_CODE_MCP, mode);
|
|
40
|
+
return { changed: changedPrimary || changedSecondary, snippets };
|
|
41
|
+
},
|
|
42
|
+
supportsPermissions: true,
|
|
43
|
+
},
|
|
44
|
+
"claude-desktop": {
|
|
45
|
+
id: "claude-desktop",
|
|
46
|
+
name: "Claude Desktop",
|
|
47
|
+
selectionLabel: "Claude Desktop",
|
|
48
|
+
detect: () => {
|
|
49
|
+
const path = getClaudeDesktopConfigPath();
|
|
50
|
+
return Boolean(path && (existsSync(path) || isClaudeDesktopInstalled()));
|
|
51
|
+
},
|
|
52
|
+
configure: (mode) => {
|
|
53
|
+
const path = getClaudeDesktopConfigPath();
|
|
54
|
+
if (!path)
|
|
55
|
+
return { changed: false, snippets: [] };
|
|
56
|
+
return {
|
|
57
|
+
changed: configureJsonMcpFile(path, mode),
|
|
58
|
+
snippets: [
|
|
59
|
+
{
|
|
60
|
+
path,
|
|
61
|
+
content: jsonSnippet({
|
|
62
|
+
mcpServers: {
|
|
63
|
+
almanac: { ...ALMANAC_MCP_ENTRY },
|
|
64
|
+
},
|
|
65
|
+
}),
|
|
66
|
+
},
|
|
67
|
+
],
|
|
68
|
+
};
|
|
69
|
+
},
|
|
70
|
+
},
|
|
71
|
+
codex: {
|
|
72
|
+
id: "codex",
|
|
73
|
+
name: "Codex",
|
|
74
|
+
selectionLabel: "Codex",
|
|
75
|
+
detect: () => hasCommand("codex") || existsSync(CODEX_CONFIG) || existsSync(join(homedir(), ".codex")),
|
|
76
|
+
configure: (mode) => ({
|
|
77
|
+
changed: configureCodexToml(CODEX_CONFIG, mode),
|
|
78
|
+
snippets: [
|
|
79
|
+
{
|
|
80
|
+
path: CODEX_CONFIG,
|
|
81
|
+
content: codexSnippet(),
|
|
82
|
+
},
|
|
83
|
+
],
|
|
84
|
+
}),
|
|
85
|
+
},
|
|
86
|
+
cursor: {
|
|
87
|
+
id: "cursor",
|
|
88
|
+
name: "Cursor",
|
|
89
|
+
selectionLabel: "Cursor",
|
|
90
|
+
detect: () => hasCommand("cursor-agent") ||
|
|
91
|
+
existsSync(CURSOR_MCP_JSON) ||
|
|
92
|
+
existsSync(join(homedir(), ".cursor")),
|
|
93
|
+
configure: (mode) => ({
|
|
94
|
+
changed: configureJsonMcpFile(CURSOR_MCP_JSON, mode),
|
|
95
|
+
snippets: [
|
|
96
|
+
{
|
|
97
|
+
path: CURSOR_MCP_JSON,
|
|
98
|
+
content: jsonSnippet({
|
|
99
|
+
mcpServers: {
|
|
100
|
+
almanac: { ...ALMANAC_MCP_ENTRY },
|
|
101
|
+
},
|
|
102
|
+
}),
|
|
103
|
+
},
|
|
104
|
+
],
|
|
105
|
+
}),
|
|
106
|
+
},
|
|
107
|
+
opencode: {
|
|
108
|
+
id: "opencode",
|
|
109
|
+
name: "OpenCode",
|
|
110
|
+
selectionLabel: "OpenCode",
|
|
111
|
+
detect: () => hasCommand("opencode") ||
|
|
112
|
+
existsSync(OPENCODE_JSON) ||
|
|
113
|
+
existsSync(OPENCODE_JSONC) ||
|
|
114
|
+
existsSync(OPENCODE_DIR),
|
|
115
|
+
configure: (mode) => {
|
|
116
|
+
const path = getOpenCodeConfigPath();
|
|
117
|
+
return {
|
|
118
|
+
changed: configureOpenCodeFile(path, mode),
|
|
119
|
+
snippets: [
|
|
120
|
+
{
|
|
121
|
+
path,
|
|
122
|
+
content: jsonSnippet({
|
|
123
|
+
"$schema": "https://opencode.ai/config.json",
|
|
124
|
+
mcp: {
|
|
125
|
+
almanac: {
|
|
126
|
+
type: "local",
|
|
127
|
+
command: [ALMANAC_MCP_ENTRY.command, ...ALMANAC_MCP_ENTRY.args],
|
|
128
|
+
enabled: true,
|
|
129
|
+
},
|
|
130
|
+
},
|
|
131
|
+
}),
|
|
132
|
+
},
|
|
133
|
+
],
|
|
134
|
+
};
|
|
135
|
+
},
|
|
136
|
+
},
|
|
137
|
+
windsurf: {
|
|
138
|
+
id: "windsurf",
|
|
139
|
+
name: "Windsurf",
|
|
140
|
+
selectionLabel: "Windsurf",
|
|
141
|
+
detect: () => hasCommand("windsurf") ||
|
|
142
|
+
existsSync(WINDSURF_MCP_JSON) ||
|
|
143
|
+
existsSync(join(homedir(), ".codeium")),
|
|
144
|
+
configure: (mode) => ({
|
|
145
|
+
changed: configureJsonMcpFile(WINDSURF_MCP_JSON, mode),
|
|
146
|
+
snippets: [
|
|
147
|
+
{
|
|
148
|
+
path: WINDSURF_MCP_JSON,
|
|
149
|
+
content: jsonSnippet({
|
|
150
|
+
mcpServers: {
|
|
151
|
+
almanac: { ...ALMANAC_MCP_ENTRY },
|
|
152
|
+
},
|
|
153
|
+
}),
|
|
154
|
+
},
|
|
155
|
+
],
|
|
156
|
+
}),
|
|
157
|
+
},
|
|
158
|
+
};
|
|
159
|
+
export function parseSetupArgs(argv) {
|
|
160
|
+
const options = {
|
|
161
|
+
all: false,
|
|
162
|
+
clients: [],
|
|
163
|
+
dryRun: false,
|
|
164
|
+
print: false,
|
|
165
|
+
yes: false,
|
|
166
|
+
};
|
|
167
|
+
for (let i = 0; i < argv.length; i++) {
|
|
168
|
+
const arg = argv[i];
|
|
169
|
+
if (arg === "--all") {
|
|
170
|
+
options.all = true;
|
|
171
|
+
continue;
|
|
172
|
+
}
|
|
173
|
+
if (arg === "--print") {
|
|
174
|
+
options.print = true;
|
|
175
|
+
continue;
|
|
176
|
+
}
|
|
177
|
+
if (arg === "--dry-run") {
|
|
178
|
+
options.dryRun = true;
|
|
179
|
+
continue;
|
|
180
|
+
}
|
|
181
|
+
if (arg === "--yes" || arg === "-y") {
|
|
182
|
+
options.yes = true;
|
|
183
|
+
continue;
|
|
184
|
+
}
|
|
185
|
+
if (arg === "--client") {
|
|
186
|
+
const value = argv[i + 1];
|
|
187
|
+
if (!value) {
|
|
188
|
+
throw new Error("Missing value for --client");
|
|
189
|
+
}
|
|
190
|
+
i++;
|
|
191
|
+
options.clients.push(...parseClientList(value));
|
|
192
|
+
continue;
|
|
193
|
+
}
|
|
194
|
+
if (arg.startsWith("--client=")) {
|
|
195
|
+
options.clients.push(...parseClientList(arg.slice("--client=".length)));
|
|
196
|
+
continue;
|
|
197
|
+
}
|
|
198
|
+
throw new Error(`Unknown setup flag: ${arg}`);
|
|
199
|
+
}
|
|
200
|
+
if (options.all && options.clients.length > 0) {
|
|
201
|
+
throw new Error("Use either --all or --client, not both");
|
|
202
|
+
}
|
|
203
|
+
options.clients = Array.from(new Set(options.clients));
|
|
204
|
+
return options;
|
|
205
|
+
}
|
|
206
|
+
export function parseClientList(value) {
|
|
207
|
+
return value
|
|
208
|
+
.split(",")
|
|
209
|
+
.map((part) => part.trim().toLowerCase())
|
|
210
|
+
.filter(Boolean)
|
|
211
|
+
.map((part) => normalizeClientId(part));
|
|
212
|
+
}
|
|
213
|
+
export function normalizeClientId(value) {
|
|
214
|
+
const aliases = {
|
|
215
|
+
claude: "claude-code",
|
|
216
|
+
"claude-code": "claude-code",
|
|
217
|
+
"claude-desktop": "claude-desktop",
|
|
218
|
+
desktop: "claude-desktop",
|
|
219
|
+
codex: "codex",
|
|
220
|
+
cursor: "cursor",
|
|
221
|
+
opencode: "opencode",
|
|
222
|
+
windsurf: "windsurf",
|
|
223
|
+
};
|
|
224
|
+
const normalized = aliases[value];
|
|
225
|
+
if (!normalized) {
|
|
226
|
+
throw new Error(`Unsupported client "${value}". Supported clients: ${SUPPORTED_CLIENT_IDS.join(", ")}`);
|
|
227
|
+
}
|
|
228
|
+
return normalized;
|
|
229
|
+
}
|
|
230
|
+
export function detectClients() {
|
|
231
|
+
return SUPPORTED_CLIENT_IDS.map((id) => SUPPORTED_CLIENTS[id]).filter((client) => client.detect());
|
|
232
|
+
}
|
|
233
|
+
export function resolveClients(options) {
|
|
234
|
+
if (options.clients.length > 0) {
|
|
235
|
+
return options.clients.map((id) => SUPPORTED_CLIENTS[id]);
|
|
236
|
+
}
|
|
237
|
+
const detected = detectClients();
|
|
238
|
+
if (options.all)
|
|
239
|
+
return detected;
|
|
240
|
+
return detected;
|
|
241
|
+
}
|
|
242
|
+
export function applyClientSetup(clients, mode) {
|
|
243
|
+
const configured = [];
|
|
244
|
+
const alreadyConfigured = [];
|
|
245
|
+
for (const client of clients) {
|
|
246
|
+
const result = client.configure(mode);
|
|
247
|
+
if (result.changed) {
|
|
248
|
+
configured.push(client.name);
|
|
249
|
+
}
|
|
250
|
+
else {
|
|
251
|
+
alreadyConfigured.push(client.name);
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
return { configured, alreadyConfigured };
|
|
255
|
+
}
|
|
256
|
+
export function printSetupPlan(clients, options) {
|
|
257
|
+
const heading = options.dryRun ? "Dry run" : "OpenAlmanac MCP setup";
|
|
258
|
+
process.stdout.write(`${heading}\n\n`);
|
|
259
|
+
if (clients.length === 0) {
|
|
260
|
+
process.stdout.write("No supported clients detected. Use --client <name> to force a target or --print to inspect supported snippets.\n");
|
|
261
|
+
if (options.print) {
|
|
262
|
+
process.stdout.write("\nSupported clients:\n");
|
|
263
|
+
for (const id of SUPPORTED_CLIENT_IDS) {
|
|
264
|
+
process.stdout.write(`- ${SUPPORTED_CLIENTS[id].name}\n`);
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
return;
|
|
268
|
+
}
|
|
269
|
+
const mode = options.print ? "print" : "dry-run";
|
|
270
|
+
for (const client of clients) {
|
|
271
|
+
const result = client.configure(mode);
|
|
272
|
+
const status = result.changed
|
|
273
|
+
? options.print
|
|
274
|
+
? "snippet"
|
|
275
|
+
: "would configure"
|
|
276
|
+
: "already configured";
|
|
277
|
+
process.stdout.write(`- ${client.name}: ${status}\n`);
|
|
278
|
+
if (options.print) {
|
|
279
|
+
for (const snippet of result.snippets) {
|
|
280
|
+
process.stdout.write(` Path: ${snippet.path}\n`);
|
|
281
|
+
process.stdout.write(`${indentBlock(snippet.content, " ")}\n`);
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
function indentBlock(content, prefix) {
|
|
287
|
+
return content
|
|
288
|
+
.split("\n")
|
|
289
|
+
.map((line) => `${prefix}${line}`)
|
|
290
|
+
.join("\n");
|
|
291
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import type { ConfigureMode } from "./types.js";
|
|
2
|
+
export declare const CLAUDE_DIR: string;
|
|
3
|
+
export declare const CLAUDE_JSON: string;
|
|
4
|
+
export declare const CLAUDE_CODE_MCP: string;
|
|
5
|
+
export declare const SETTINGS_JSON: string;
|
|
6
|
+
export declare const CODEX_CONFIG: string;
|
|
7
|
+
export declare const CURSOR_MCP_JSON: string;
|
|
8
|
+
export declare const OPENCODE_DIR: string;
|
|
9
|
+
export declare const OPENCODE_JSON: string;
|
|
10
|
+
export declare const OPENCODE_JSONC: string;
|
|
11
|
+
export declare const WINDSURF_MCP_JSON: string;
|
|
12
|
+
export declare function ensureDir(dir: string): void;
|
|
13
|
+
export declare function readJson(path: string): Record<string, unknown>;
|
|
14
|
+
export declare function writeJson(path: string, data: Record<string, unknown>): void;
|
|
15
|
+
export declare const ALMANAC_MCP_ENTRY: {
|
|
16
|
+
readonly command: "npx";
|
|
17
|
+
readonly args: readonly ["-y", "openalmanac@latest"];
|
|
18
|
+
};
|
|
19
|
+
export declare function hasCommand(command: string): boolean;
|
|
20
|
+
export declare function getClaudeDesktopConfigPath(): string | null;
|
|
21
|
+
export declare function isClaudeDesktopInstalled(): boolean;
|
|
22
|
+
export declare function jsonSnippet(data: Record<string, unknown>): string;
|
|
23
|
+
export declare function getOpenCodeConfigPath(): string;
|
|
24
|
+
export declare function codexSnippet(): string;
|
|
25
|
+
export declare function isAlmanacCurrent(server: {
|
|
26
|
+
command?: string;
|
|
27
|
+
args?: string[];
|
|
28
|
+
} | undefined): boolean;
|
|
29
|
+
export declare function isOpenCodeAlmanacCurrent(entry: {
|
|
30
|
+
type?: string;
|
|
31
|
+
command?: string[];
|
|
32
|
+
enabled?: boolean;
|
|
33
|
+
} | undefined): boolean;
|
|
34
|
+
export declare function configureJsonMcpFile(path: string, mode: ConfigureMode): boolean;
|
|
35
|
+
export declare function configureCodexToml(path: string, mode: ConfigureMode): boolean;
|
|
36
|
+
export declare function configureOpenCodeFile(path: string, mode: ConfigureMode): boolean;
|
|
37
|
+
export declare function readToml(path: string): string;
|
|
38
|
+
export declare function readJsonWithComments(path: string): Record<string, unknown>;
|
|
39
|
+
export declare function stripJsonComments(input: string): string;
|
|
40
|
+
export declare function isRecord(value: unknown): value is Record<string, unknown>;
|
|
41
|
+
export declare function upsertCodexServer(content: string): string;
|
|
42
|
+
export declare function tomlString(value: string): string;
|
|
43
|
+
export declare function tomlArray(values: readonly string[]): string;
|
|
@@ -0,0 +1,257 @@
|
|
|
1
|
+
import { readFileSync, writeFileSync, mkdirSync, existsSync } from "fs";
|
|
2
|
+
import { homedir } from "os";
|
|
3
|
+
import { join, dirname } from "path";
|
|
4
|
+
import { spawnSync } from "child_process";
|
|
5
|
+
export const CLAUDE_DIR = join(homedir(), ".claude");
|
|
6
|
+
export const CLAUDE_JSON = join(homedir(), ".claude.json"); // Claude Code user-scoped MCP config
|
|
7
|
+
export const CLAUDE_CODE_MCP = join(CLAUDE_DIR, "mcp.json"); // Claude Code local MCP config
|
|
8
|
+
export const SETTINGS_JSON = join(CLAUDE_DIR, "settings.json");
|
|
9
|
+
export const CODEX_CONFIG = join(homedir(), ".codex", "config.toml");
|
|
10
|
+
export const CURSOR_MCP_JSON = join(homedir(), ".cursor", "mcp.json");
|
|
11
|
+
export const OPENCODE_DIR = join(homedir(), ".config", "opencode");
|
|
12
|
+
export const OPENCODE_JSON = join(OPENCODE_DIR, "opencode.json");
|
|
13
|
+
export const OPENCODE_JSONC = join(OPENCODE_DIR, "opencode.jsonc");
|
|
14
|
+
export const WINDSURF_MCP_JSON = join(homedir(), ".codeium", "mcp_config.json");
|
|
15
|
+
export function ensureDir(dir) {
|
|
16
|
+
if (!existsSync(dir))
|
|
17
|
+
mkdirSync(dir, { recursive: true });
|
|
18
|
+
}
|
|
19
|
+
export function readJson(path) {
|
|
20
|
+
try {
|
|
21
|
+
return JSON.parse(readFileSync(path, "utf-8"));
|
|
22
|
+
}
|
|
23
|
+
catch {
|
|
24
|
+
return {};
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
export function writeJson(path, data) {
|
|
28
|
+
writeFileSync(path, JSON.stringify(data, null, 2) + "\n");
|
|
29
|
+
}
|
|
30
|
+
/* ── Step 1 — MCP server ───────────────────────────────────────── */
|
|
31
|
+
export const ALMANAC_MCP_ENTRY = { command: "npx", args: ["-y", "openalmanac@latest"] };
|
|
32
|
+
export function hasCommand(command) {
|
|
33
|
+
const checker = process.platform === "win32" ? "where" : "which";
|
|
34
|
+
const result = spawnSync(checker, [command], { stdio: "ignore" });
|
|
35
|
+
return result.status === 0;
|
|
36
|
+
}
|
|
37
|
+
export function getClaudeDesktopConfigPath() {
|
|
38
|
+
if (process.platform === "darwin") {
|
|
39
|
+
return join(homedir(), "Library", "Application Support", "Claude", "claude_desktop_config.json");
|
|
40
|
+
}
|
|
41
|
+
if (process.platform === "linux") {
|
|
42
|
+
return join(homedir(), ".config", "Claude", "claude_desktop_config.json");
|
|
43
|
+
}
|
|
44
|
+
if (process.platform === "win32") {
|
|
45
|
+
const appData = process.env.APPDATA;
|
|
46
|
+
if (!appData)
|
|
47
|
+
return null;
|
|
48
|
+
return join(appData, "Claude", "claude_desktop_config.json");
|
|
49
|
+
}
|
|
50
|
+
return null;
|
|
51
|
+
}
|
|
52
|
+
export function isClaudeDesktopInstalled() {
|
|
53
|
+
if (process.platform === "darwin") {
|
|
54
|
+
return (existsSync("/Applications/Claude.app") ||
|
|
55
|
+
existsSync(join(homedir(), "Applications", "Claude.app")));
|
|
56
|
+
}
|
|
57
|
+
if (process.platform === "linux") {
|
|
58
|
+
return (existsSync("/usr/share/applications/claude.desktop") ||
|
|
59
|
+
existsSync(join(homedir(), ".local", "share", "applications", "claude.desktop")) ||
|
|
60
|
+
existsSync("/opt/Claude/claude"));
|
|
61
|
+
}
|
|
62
|
+
if (process.platform === "win32") {
|
|
63
|
+
const localAppData = process.env.LOCALAPPDATA;
|
|
64
|
+
const programFiles = process.env.ProgramFiles;
|
|
65
|
+
return Boolean((localAppData &&
|
|
66
|
+
existsSync(join(localAppData, "Programs", "Claude", "Claude.exe"))) ||
|
|
67
|
+
(programFiles && existsSync(join(programFiles, "Claude", "Claude.exe"))));
|
|
68
|
+
}
|
|
69
|
+
return false;
|
|
70
|
+
}
|
|
71
|
+
export function jsonSnippet(data) {
|
|
72
|
+
return JSON.stringify(data, null, 2);
|
|
73
|
+
}
|
|
74
|
+
export function getOpenCodeConfigPath() {
|
|
75
|
+
if (existsSync(OPENCODE_JSON))
|
|
76
|
+
return OPENCODE_JSON;
|
|
77
|
+
if (existsSync(OPENCODE_JSONC))
|
|
78
|
+
return OPENCODE_JSONC;
|
|
79
|
+
return OPENCODE_JSON;
|
|
80
|
+
}
|
|
81
|
+
export function codexSnippet() {
|
|
82
|
+
return [
|
|
83
|
+
"[mcp_servers.almanac]",
|
|
84
|
+
`command = ${tomlString(ALMANAC_MCP_ENTRY.command)}`,
|
|
85
|
+
`args = ${tomlArray(ALMANAC_MCP_ENTRY.args)}`,
|
|
86
|
+
].join("\n");
|
|
87
|
+
}
|
|
88
|
+
export function isAlmanacCurrent(server) {
|
|
89
|
+
return (server?.command === "npx" &&
|
|
90
|
+
JSON.stringify(server.args) === JSON.stringify(ALMANAC_MCP_ENTRY.args));
|
|
91
|
+
}
|
|
92
|
+
export function isOpenCodeAlmanacCurrent(entry) {
|
|
93
|
+
return (entry?.type === "local" &&
|
|
94
|
+
entry?.enabled === true &&
|
|
95
|
+
JSON.stringify(entry.command) ===
|
|
96
|
+
JSON.stringify([ALMANAC_MCP_ENTRY.command, ...ALMANAC_MCP_ENTRY.args]));
|
|
97
|
+
}
|
|
98
|
+
export function configureJsonMcpFile(path, mode) {
|
|
99
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
100
|
+
const json = readJson(path);
|
|
101
|
+
if (!json.mcpServers)
|
|
102
|
+
json.mcpServers = {};
|
|
103
|
+
if (isAlmanacCurrent(json.mcpServers.almanac)) {
|
|
104
|
+
return false;
|
|
105
|
+
}
|
|
106
|
+
if (mode === "apply") {
|
|
107
|
+
ensureDir(dirname(path));
|
|
108
|
+
json.mcpServers.almanac = { ...ALMANAC_MCP_ENTRY };
|
|
109
|
+
writeJson(path, json);
|
|
110
|
+
}
|
|
111
|
+
return true;
|
|
112
|
+
}
|
|
113
|
+
export function configureCodexToml(path, mode) {
|
|
114
|
+
const current = readToml(path);
|
|
115
|
+
const next = upsertCodexServer(current);
|
|
116
|
+
if (current.trim() === next.trim()) {
|
|
117
|
+
return false;
|
|
118
|
+
}
|
|
119
|
+
if (mode === "apply") {
|
|
120
|
+
ensureDir(dirname(path));
|
|
121
|
+
writeFileSync(path, next.endsWith("\n") ? next : next + "\n");
|
|
122
|
+
}
|
|
123
|
+
return true;
|
|
124
|
+
}
|
|
125
|
+
export function configureOpenCodeFile(path, mode) {
|
|
126
|
+
const current = readJsonWithComments(path);
|
|
127
|
+
if (!current.$schema) {
|
|
128
|
+
current.$schema = "https://opencode.ai/config.json";
|
|
129
|
+
}
|
|
130
|
+
const mcp = isRecord(current.mcp) ? current.mcp : {};
|
|
131
|
+
if (!isRecord(current.mcp)) {
|
|
132
|
+
current.mcp = mcp;
|
|
133
|
+
}
|
|
134
|
+
if (isOpenCodeAlmanacCurrent(mcp.almanac)) {
|
|
135
|
+
return false;
|
|
136
|
+
}
|
|
137
|
+
mcp.almanac = {
|
|
138
|
+
type: "local",
|
|
139
|
+
command: [ALMANAC_MCP_ENTRY.command, ...ALMANAC_MCP_ENTRY.args],
|
|
140
|
+
enabled: true,
|
|
141
|
+
};
|
|
142
|
+
if (mode === "apply") {
|
|
143
|
+
ensureDir(dirname(path));
|
|
144
|
+
writeJson(path, current);
|
|
145
|
+
}
|
|
146
|
+
return true;
|
|
147
|
+
}
|
|
148
|
+
export function readToml(path) {
|
|
149
|
+
try {
|
|
150
|
+
return readFileSync(path, "utf-8");
|
|
151
|
+
}
|
|
152
|
+
catch {
|
|
153
|
+
return "";
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
export function readJsonWithComments(path) {
|
|
157
|
+
try {
|
|
158
|
+
const raw = readFileSync(path, "utf-8");
|
|
159
|
+
const parsed = JSON.parse(stripJsonComments(raw));
|
|
160
|
+
return isRecord(parsed) ? parsed : {};
|
|
161
|
+
}
|
|
162
|
+
catch {
|
|
163
|
+
return {};
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
export function stripJsonComments(input) {
|
|
167
|
+
let result = "";
|
|
168
|
+
let inString = false;
|
|
169
|
+
let inLineComment = false;
|
|
170
|
+
let inBlockComment = false;
|
|
171
|
+
let escaped = false;
|
|
172
|
+
for (let i = 0; i < input.length; i++) {
|
|
173
|
+
const char = input[i];
|
|
174
|
+
const next = input[i + 1];
|
|
175
|
+
if (inLineComment) {
|
|
176
|
+
if (char === "\n") {
|
|
177
|
+
inLineComment = false;
|
|
178
|
+
result += char;
|
|
179
|
+
}
|
|
180
|
+
continue;
|
|
181
|
+
}
|
|
182
|
+
if (inBlockComment) {
|
|
183
|
+
if (char === "*" && next === "/") {
|
|
184
|
+
inBlockComment = false;
|
|
185
|
+
i++;
|
|
186
|
+
}
|
|
187
|
+
continue;
|
|
188
|
+
}
|
|
189
|
+
if (inString) {
|
|
190
|
+
result += char;
|
|
191
|
+
if (escaped) {
|
|
192
|
+
escaped = false;
|
|
193
|
+
}
|
|
194
|
+
else if (char === "\\") {
|
|
195
|
+
escaped = true;
|
|
196
|
+
}
|
|
197
|
+
else if (char === "\"") {
|
|
198
|
+
inString = false;
|
|
199
|
+
}
|
|
200
|
+
continue;
|
|
201
|
+
}
|
|
202
|
+
if (char === "/" && next === "/") {
|
|
203
|
+
inLineComment = true;
|
|
204
|
+
i++;
|
|
205
|
+
continue;
|
|
206
|
+
}
|
|
207
|
+
if (char === "/" && next === "*") {
|
|
208
|
+
inBlockComment = true;
|
|
209
|
+
i++;
|
|
210
|
+
continue;
|
|
211
|
+
}
|
|
212
|
+
if (char === "\"") {
|
|
213
|
+
inString = true;
|
|
214
|
+
}
|
|
215
|
+
result += char;
|
|
216
|
+
}
|
|
217
|
+
return result;
|
|
218
|
+
}
|
|
219
|
+
export function isRecord(value) {
|
|
220
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
221
|
+
}
|
|
222
|
+
export function upsertCodexServer(content) {
|
|
223
|
+
const sectionName = "mcp_servers.almanac";
|
|
224
|
+
const header = `[${sectionName}]`;
|
|
225
|
+
const nextBody = [
|
|
226
|
+
`command = ${tomlString(ALMANAC_MCP_ENTRY.command)}`,
|
|
227
|
+
`args = ${tomlArray(ALMANAC_MCP_ENTRY.args)}`,
|
|
228
|
+
];
|
|
229
|
+
const lines = content === "" ? [] : content.split(/\r?\n/);
|
|
230
|
+
const start = lines.findIndex((line) => line.trim() === header);
|
|
231
|
+
if (start === -1) {
|
|
232
|
+
const prefix = content.trimEnd();
|
|
233
|
+
const block = [header, ...nextBody].join("\n");
|
|
234
|
+
return prefix === "" ? block + "\n" : `${prefix}\n\n${block}\n`;
|
|
235
|
+
}
|
|
236
|
+
let end = lines.length;
|
|
237
|
+
for (let i = start + 1; i < lines.length; i++) {
|
|
238
|
+
if (/^\s*\[.+\]\s*$/.test(lines[i])) {
|
|
239
|
+
end = i;
|
|
240
|
+
break;
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
const existingBody = lines.slice(start + 1, end);
|
|
244
|
+
const preserved = existingBody.filter((line) => {
|
|
245
|
+
const trimmed = line.trim();
|
|
246
|
+
return !trimmed.startsWith("command =") && !trimmed.startsWith("args =");
|
|
247
|
+
});
|
|
248
|
+
const replacement = [header, ...nextBody, ...preserved];
|
|
249
|
+
const updated = [...lines.slice(0, start), ...replacement, ...lines.slice(end)];
|
|
250
|
+
return updated.join("\n").replace(/\n{3,}/g, "\n\n").replace(/\s+$/, "") + "\n";
|
|
251
|
+
}
|
|
252
|
+
export function tomlString(value) {
|
|
253
|
+
return JSON.stringify(value);
|
|
254
|
+
}
|
|
255
|
+
export function tomlArray(values) {
|
|
256
|
+
return `[${values.map((value) => tomlString(value)).join(", ")}]`;
|
|
257
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { performLogin } from "../login-core.js";
|
|
2
|
+
import { applyClientSetup, parseSetupArgs, printSetupPlan, resolveClients, } from "./clients.js";
|
|
3
|
+
import { TOOL_GROUPS, configurePermissions } from "./permissions.js";
|
|
4
|
+
import { printResult, runClientSelect, runLoginStep, runToolSelect } from "./tui.js";
|
|
5
|
+
export { runRedditSetup } from "./reddit.js";
|
|
6
|
+
export async function runSetup() {
|
|
7
|
+
const options = parseSetupArgs(process.argv.slice(3));
|
|
8
|
+
let clients = resolveClients(options);
|
|
9
|
+
if (options.print || options.dryRun) {
|
|
10
|
+
printSetupPlan(clients, options);
|
|
11
|
+
process.exit(0);
|
|
12
|
+
}
|
|
13
|
+
if (clients.length === 0) {
|
|
14
|
+
printSetupPlan(clients, options);
|
|
15
|
+
process.exit(0);
|
|
16
|
+
}
|
|
17
|
+
const skipTui = options.yes;
|
|
18
|
+
const interactive = process.stdin.isTTY && !skipTui;
|
|
19
|
+
if (interactive && options.clients.length === 0) {
|
|
20
|
+
clients = await runClientSelect(clients);
|
|
21
|
+
}
|
|
22
|
+
const clientsLabel = clients.map((client) => client.name).join(", ");
|
|
23
|
+
const setupSummary = applyClientSetup(clients, "apply");
|
|
24
|
+
const permissionClient = clients.find((client) => client.supportsPermissions);
|
|
25
|
+
let tools = [];
|
|
26
|
+
if (permissionClient) {
|
|
27
|
+
const mcpChanged = setupSummary.configured.includes(permissionClient.name);
|
|
28
|
+
if (interactive) {
|
|
29
|
+
tools = await runToolSelect(clientsLabel, mcpChanged);
|
|
30
|
+
}
|
|
31
|
+
else {
|
|
32
|
+
tools = TOOL_GROUPS.flatMap((g) => g.tools);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
const count = tools.length > 0 ? configurePermissions(tools) : 0;
|
|
36
|
+
const permissionCount = permissionClient ? count : null;
|
|
37
|
+
let loginResult;
|
|
38
|
+
if (interactive) {
|
|
39
|
+
loginResult = await runLoginStep(clientsLabel, setupSummary.configured.length > 0, permissionCount);
|
|
40
|
+
}
|
|
41
|
+
else {
|
|
42
|
+
try {
|
|
43
|
+
const result = await performLogin();
|
|
44
|
+
loginResult =
|
|
45
|
+
result.status === "already_logged_in"
|
|
46
|
+
? { status: "already", name: result.name }
|
|
47
|
+
: { status: "done" };
|
|
48
|
+
}
|
|
49
|
+
catch {
|
|
50
|
+
loginResult = { status: "skipped" };
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
printResult(clientsLabel, loginResult, setupSummary.configured, setupSummary.alreadyConfigured, count);
|
|
54
|
+
process.exit(0);
|
|
55
|
+
}
|