@tday-com/mcp 0.3.1 → 0.4.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/README.md +79 -67
- package/dist/cli.js +781 -63
- package/package.json +19 -11
- package/dist/api-client.js +0 -170
- package/dist/config.js +0 -62
- package/dist/download-safety.js +0 -73
- package/dist/installer-agents.js +0 -199
- package/dist/installer-callback.js +0 -84
- package/dist/installer-slash-command.js +0 -321
- package/dist/installer.js +0 -150
- package/dist/package-info.js +0 -23
- package/dist/prompts.js +0 -36
- package/dist/result-format.js +0 -130
- package/dist/server.js +0 -43
- package/dist/tool-schemas.js +0 -175
- package/dist/tools-account.js +0 -30
- package/dist/tools-features.js +0 -39
- package/dist/tools-launch.js +0 -358
- package/dist/tools.js +0 -275
package/package.json
CHANGED
|
@@ -1,16 +1,17 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tday-com/mcp",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "MCP server and CLI
|
|
3
|
+
"version": "0.4.2",
|
|
4
|
+
"description": "Record narrated product demos with your coding agent: the tday MCP server and CLI for Claude Code, Cursor, Codex, VS Code and Gemini CLI. Only rendered UI and metadata leave your machine.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"mcp",
|
|
7
7
|
"model-context-protocol",
|
|
8
8
|
"tday",
|
|
9
|
-
"
|
|
10
|
-
"
|
|
11
|
-
"
|
|
12
|
-
"
|
|
13
|
-
"cursor"
|
|
9
|
+
"demo-video",
|
|
10
|
+
"product-demo",
|
|
11
|
+
"screen-recording",
|
|
12
|
+
"claude-code",
|
|
13
|
+
"cursor",
|
|
14
|
+
"codex"
|
|
14
15
|
],
|
|
15
16
|
"homepage": "https://tday.com",
|
|
16
17
|
"license": "SEE LICENSE IN LICENSE",
|
|
@@ -26,21 +27,28 @@
|
|
|
26
27
|
"LICENSE"
|
|
27
28
|
],
|
|
28
29
|
"engines": {
|
|
29
|
-
"node": ">=18"
|
|
30
|
+
"node": ">=18.17"
|
|
30
31
|
},
|
|
31
32
|
"scripts": {
|
|
32
|
-
"build": "rm -rf dist &&
|
|
33
|
-
"dev": "tsc -p tsconfig.build.json --watch",
|
|
33
|
+
"build": "rm -rf dist && bun build src/cli.ts --target node --outdir dist --format esm --minify --external playwright-core --external @modelcontextprotocol/sdk --external zod --external postcss --external parse5 --external pngjs --external @lydell/node-pty --external @xterm/xterm && chmod +x dist/cli.js && bun ../capture/scripts/check-bundle.ts dist/cli.js",
|
|
34
34
|
"typecheck": "tsc -p tsconfig.json --noEmit",
|
|
35
35
|
"test": "bun test",
|
|
36
36
|
"prepublishOnly": "bun run build",
|
|
37
37
|
"start": "node dist/cli.js serve"
|
|
38
38
|
},
|
|
39
39
|
"dependencies": {
|
|
40
|
-
"@
|
|
40
|
+
"@lydell/node-pty": "^1.1.0",
|
|
41
|
+
"@modelcontextprotocol/sdk": "^1.30.0",
|
|
42
|
+
"@xterm/xterm": "^6.0.0",
|
|
43
|
+
"parse5": "^7.3.0",
|
|
44
|
+
"playwright-core": "^1.62.1",
|
|
45
|
+
"pngjs": "^7.0.0",
|
|
46
|
+
"postcss": "^8.5.26",
|
|
41
47
|
"zod": "^4.6.6"
|
|
42
48
|
},
|
|
43
49
|
"devDependencies": {
|
|
50
|
+
"@markup-one/capture": "0.0.1",
|
|
51
|
+
"@markup-one/capture-format": "0.0.1",
|
|
44
52
|
"@types/node": "^26.1.2",
|
|
45
53
|
"typescript": "^5.6.3"
|
|
46
54
|
},
|
package/dist/api-client.js
DELETED
|
@@ -1,170 +0,0 @@
|
|
|
1
|
-
export async function api(config, path, init = {}) {
|
|
2
|
-
if (!config.token) {
|
|
3
|
-
throw new Error("Not authenticated. Run `npx -y @tday-com/mcp install` to link this MCP server to your tday account in the browser, then restart your AI agent.");
|
|
4
|
-
}
|
|
5
|
-
const controller = new AbortController();
|
|
6
|
-
const timeoutMs = init.method === "POST" ? 120_000 : 30_000;
|
|
7
|
-
const timeout = setTimeout(() => controller.abort(), timeoutMs);
|
|
8
|
-
if (typeof timeout === "object" && "unref" in timeout)
|
|
9
|
-
timeout.unref();
|
|
10
|
-
const callerSignal = init.signal;
|
|
11
|
-
if (callerSignal?.aborted)
|
|
12
|
-
controller.abort(callerSignal.reason);
|
|
13
|
-
callerSignal?.addEventListener("abort", () => controller.abort(callerSignal.reason), {
|
|
14
|
-
once: true,
|
|
15
|
-
});
|
|
16
|
-
try {
|
|
17
|
-
const res = await fetch(`${config.apiBaseUrl}${path}`, {
|
|
18
|
-
...init,
|
|
19
|
-
signal: controller.signal,
|
|
20
|
-
headers: {
|
|
21
|
-
accept: "application/json",
|
|
22
|
-
"content-type": "application/json",
|
|
23
|
-
authorization: `Bearer ${config.token}`,
|
|
24
|
-
...init.headers,
|
|
25
|
-
},
|
|
26
|
-
});
|
|
27
|
-
const text = await res.text();
|
|
28
|
-
if (!res.ok) {
|
|
29
|
-
throw new Error(formatHttpError(res.status, path, text, res.statusText));
|
|
30
|
-
}
|
|
31
|
-
if (!text)
|
|
32
|
-
return null;
|
|
33
|
-
try {
|
|
34
|
-
return JSON.parse(text);
|
|
35
|
-
}
|
|
36
|
-
catch {
|
|
37
|
-
return text;
|
|
38
|
-
}
|
|
39
|
-
}
|
|
40
|
-
catch (err) {
|
|
41
|
-
if (err instanceof Error && err.name === "AbortError") {
|
|
42
|
-
if (callerSignal?.aborted)
|
|
43
|
-
throw err;
|
|
44
|
-
throw new Error(`tday request timed out after ${timeoutMs / 1000}s: ${path}`);
|
|
45
|
-
}
|
|
46
|
-
throw err;
|
|
47
|
-
}
|
|
48
|
-
finally {
|
|
49
|
-
clearTimeout(timeout);
|
|
50
|
-
}
|
|
51
|
-
}
|
|
52
|
-
function formatHttpError(status, path, text, statusText) {
|
|
53
|
-
if (status >= 500) {
|
|
54
|
-
return `tday ${status} ${path}. tday is temporarily unable to complete the request; retry shortly. If a design or launch video was already queued, keep polling it (tday_poll_generation or tday_poll_launch_video) rather than starting another.`;
|
|
55
|
-
}
|
|
56
|
-
const body = formatErrorBody(text) || statusText;
|
|
57
|
-
const prefix = `tday ${status} ${path}: ${body}`;
|
|
58
|
-
const code = errorCode(text);
|
|
59
|
-
const links = refusalLinks(text);
|
|
60
|
-
const linkLine = [
|
|
61
|
-
links.usage ? `See what the organisation has spent: ${links.usage}` : null,
|
|
62
|
-
links.billing ? `The organisation's owner can change the plan at ${links.billing}` : null,
|
|
63
|
-
]
|
|
64
|
-
.filter(Boolean)
|
|
65
|
-
.join(" ");
|
|
66
|
-
if (code && PLAN_CODES.has(code) && status !== 402) {
|
|
67
|
-
return `${prefix}. ${PLAN_REFUSAL_LEADS[code] ?? "The organisation's plan does not include this."}${linkLine ? ` ${linkLine}` : ""} Run tday_whoami to see what the plan includes and who owns it.`;
|
|
68
|
-
}
|
|
69
|
-
switch (status) {
|
|
70
|
-
case 400:
|
|
71
|
-
return `${prefix}. Check the tool arguments and retry with values returned by tday list/get tools.`;
|
|
72
|
-
case 401:
|
|
73
|
-
return `${prefix}. This MCP server's tday link is missing, expired (links last 90 days) or was disconnected. Run \`npx -y @tday-com/mcp install\` to sign in and relink this machine, then restart your AI agent and retry tday_whoami.`;
|
|
74
|
-
case 402:
|
|
75
|
-
return `${prefix}. Nothing was started and no credits were spent. Do not retry until the balance changes.${linkLine ? ` ${linkLine}` : ""} Run tday_whoami for each organisation's balance and who owns it; tday_get_credit_prices has what things cost.`;
|
|
76
|
-
case 403:
|
|
77
|
-
return `${prefix}. The linked tday account, or this connection, is not allowed to do that. Use tday_whoami to verify the account, then list brands/designs again.`;
|
|
78
|
-
case 404:
|
|
79
|
-
return `${prefix}. Check that the ID came from a tday_* list/get tool and belongs to the linked account.`;
|
|
80
|
-
case 429:
|
|
81
|
-
return `${prefix}. MCP/API generation is rate limited per account. Wait for the limit window to pass (see the Retry-After time), then retry. Do not retry in a loop. Run tday_whoami to check the plan and credit balance.`;
|
|
82
|
-
default:
|
|
83
|
-
return prefix;
|
|
84
|
-
}
|
|
85
|
-
}
|
|
86
|
-
const PLAN_CODES = new Set(["insufficient_credits", "plan_locked", "brand_plan_locked", "format_locked", "brand_limit_reached"]);
|
|
87
|
-
const PLAN_REFUSAL_LEADS = {
|
|
88
|
-
plan_locked: "This brand is locked by the organisation's plan.",
|
|
89
|
-
brand_plan_locked: "This brand is locked by the organisation's plan.",
|
|
90
|
-
format_locked: "The organisation's plan does not include this size; name another or omit it.",
|
|
91
|
-
brand_limit_reached: "The organisation's plan does not include another brand.",
|
|
92
|
-
};
|
|
93
|
-
function refusalLinks(text) {
|
|
94
|
-
try {
|
|
95
|
-
const parsed = JSON.parse(text);
|
|
96
|
-
return { usage: tdayUrl(parsed.links?.usage), billing: tdayUrl(parsed.links?.billing) };
|
|
97
|
-
}
|
|
98
|
-
catch {
|
|
99
|
-
return { usage: null, billing: null };
|
|
100
|
-
}
|
|
101
|
-
}
|
|
102
|
-
function tdayUrl(value) {
|
|
103
|
-
if (typeof value !== "string" || value.length > 500)
|
|
104
|
-
return null;
|
|
105
|
-
try {
|
|
106
|
-
const url = new URL(value);
|
|
107
|
-
const local = url.hostname === "localhost" || url.hostname === "127.0.0.1";
|
|
108
|
-
const tday = url.protocol === "https:" && (url.hostname === "tday.com" || url.hostname.endsWith(".tday.com"));
|
|
109
|
-
return tday || local ? url.toString() : null;
|
|
110
|
-
}
|
|
111
|
-
catch {
|
|
112
|
-
return null;
|
|
113
|
-
}
|
|
114
|
-
}
|
|
115
|
-
function errorCode(text) {
|
|
116
|
-
if (!text)
|
|
117
|
-
return null;
|
|
118
|
-
try {
|
|
119
|
-
const parsed = JSON.parse(text);
|
|
120
|
-
return typeof parsed.code === "string" ? parsed.code : null;
|
|
121
|
-
}
|
|
122
|
-
catch {
|
|
123
|
-
return null;
|
|
124
|
-
}
|
|
125
|
-
}
|
|
126
|
-
function formatErrorBody(text) {
|
|
127
|
-
if (!text)
|
|
128
|
-
return "";
|
|
129
|
-
try {
|
|
130
|
-
const parsed = JSON.parse(text);
|
|
131
|
-
const message = parsed.error ?? parsed.message;
|
|
132
|
-
if (typeof message === "string")
|
|
133
|
-
return message.slice(0, 300);
|
|
134
|
-
}
|
|
135
|
-
catch {
|
|
136
|
-
}
|
|
137
|
-
return "";
|
|
138
|
-
}
|
|
139
|
-
export function normalizeWebsite(raw) {
|
|
140
|
-
const candidate = raw.match(/^https?:\/\//i) ? raw : `https://${raw}`;
|
|
141
|
-
let url;
|
|
142
|
-
try {
|
|
143
|
-
url = new URL(candidate);
|
|
144
|
-
}
|
|
145
|
-
catch {
|
|
146
|
-
throw new Error(`Invalid website URL: "${raw}"`);
|
|
147
|
-
}
|
|
148
|
-
if (url.protocol !== "http:" && url.protocol !== "https:") {
|
|
149
|
-
throw new Error(`Website URL must use http or https (got ${url.protocol})`);
|
|
150
|
-
}
|
|
151
|
-
if (!url.hostname || !url.hostname.includes(".")) {
|
|
152
|
-
throw new Error("Website must include a valid domain, for example stripe.com");
|
|
153
|
-
}
|
|
154
|
-
return url.href;
|
|
155
|
-
}
|
|
156
|
-
export function firstImageUrl(generation) {
|
|
157
|
-
return (generation?.imageUrl ??
|
|
158
|
-
generation?.masterImageUrl ??
|
|
159
|
-
generation?.panels?.find((panel) => panel.imageUrl)?.imageUrl ??
|
|
160
|
-
null);
|
|
161
|
-
}
|
|
162
|
-
export function query(params) {
|
|
163
|
-
const search = new URLSearchParams();
|
|
164
|
-
for (const [key, value] of Object.entries(params)) {
|
|
165
|
-
if (value !== undefined)
|
|
166
|
-
search.set(key, String(value));
|
|
167
|
-
}
|
|
168
|
-
const encoded = search.toString();
|
|
169
|
-
return encoded ? `?${encoded}` : "";
|
|
170
|
-
}
|
package/dist/config.js
DELETED
|
@@ -1,62 +0,0 @@
|
|
|
1
|
-
import { readFile } from "node:fs/promises";
|
|
2
|
-
import { homedir } from "node:os";
|
|
3
|
-
import { join } from "node:path";
|
|
4
|
-
const CONFIG_PATH = join(homedir(), ".tday", "config.json");
|
|
5
|
-
const DEFAULT_WEB_URL = "https://tday.com";
|
|
6
|
-
export async function loadConfig() {
|
|
7
|
-
const envApiBaseUrl = process.env.TDAY_API_URL;
|
|
8
|
-
const envWebBaseUrl = process.env.TDAY_WEB_URL;
|
|
9
|
-
const envToken = process.env.TDAY_TOKEN ?? null;
|
|
10
|
-
const defaultApiBaseUrl = normalizeBaseUrl(envApiBaseUrl ?? DEFAULT_WEB_URL);
|
|
11
|
-
const defaultWebBaseUrl = normalizeBaseUrl(envWebBaseUrl ?? deriveWebBaseUrl(defaultApiBaseUrl));
|
|
12
|
-
if (envToken) {
|
|
13
|
-
return {
|
|
14
|
-
apiBaseUrl: defaultApiBaseUrl,
|
|
15
|
-
webBaseUrl: defaultWebBaseUrl,
|
|
16
|
-
token: envToken,
|
|
17
|
-
};
|
|
18
|
-
}
|
|
19
|
-
const fileConfig = await readFirstConfig([CONFIG_PATH]);
|
|
20
|
-
const apiBaseUrl = normalizeBaseUrl(fileConfig?.apiBaseUrl ?? defaultApiBaseUrl);
|
|
21
|
-
const webBaseUrl = normalizeBaseUrl(fileConfig?.webBaseUrl ?? envWebBaseUrl ?? deriveWebBaseUrl(apiBaseUrl));
|
|
22
|
-
return {
|
|
23
|
-
apiBaseUrl,
|
|
24
|
-
webBaseUrl,
|
|
25
|
-
token: fileConfig?.token ?? null,
|
|
26
|
-
};
|
|
27
|
-
}
|
|
28
|
-
export function configPath() {
|
|
29
|
-
return CONFIG_PATH;
|
|
30
|
-
}
|
|
31
|
-
async function readFirstConfig(paths) {
|
|
32
|
-
for (const path of paths) {
|
|
33
|
-
try {
|
|
34
|
-
const raw = await readFile(path, "utf8");
|
|
35
|
-
const parsed = JSON.parse(raw);
|
|
36
|
-
if (parsed && typeof parsed === "object")
|
|
37
|
-
return parsed;
|
|
38
|
-
}
|
|
39
|
-
catch {
|
|
40
|
-
}
|
|
41
|
-
}
|
|
42
|
-
return null;
|
|
43
|
-
}
|
|
44
|
-
function normalizeBaseUrl(raw) {
|
|
45
|
-
const trimmed = raw.trim().replace(/\/+$/, "");
|
|
46
|
-
return trimmed.replace(/\/api$/i, "");
|
|
47
|
-
}
|
|
48
|
-
function deriveWebBaseUrl(apiBaseUrl) {
|
|
49
|
-
try {
|
|
50
|
-
const url = new URL(apiBaseUrl);
|
|
51
|
-
if (url.hostname.startsWith("api.")) {
|
|
52
|
-
url.hostname = url.hostname.slice(4);
|
|
53
|
-
}
|
|
54
|
-
url.pathname = "";
|
|
55
|
-
url.search = "";
|
|
56
|
-
url.hash = "";
|
|
57
|
-
return normalizeBaseUrl(url.toString());
|
|
58
|
-
}
|
|
59
|
-
catch {
|
|
60
|
-
return DEFAULT_WEB_URL;
|
|
61
|
-
}
|
|
62
|
-
}
|
package/dist/download-safety.js
DELETED
|
@@ -1,73 +0,0 @@
|
|
|
1
|
-
import { lstat, mkdir, realpath } from "node:fs/promises";
|
|
2
|
-
import { dirname, isAbsolute, resolve, relative, sep } from "node:path";
|
|
3
|
-
export const ALLOWED_IMAGE_HOSTS = new Set([
|
|
4
|
-
"cdn.tday.com",
|
|
5
|
-
"video-cdn.communities.one",
|
|
6
|
-
"communities.one",
|
|
7
|
-
"media.brand.dev",
|
|
8
|
-
"asset.brandfetch.io",
|
|
9
|
-
"cdn.brandfetch.io",
|
|
10
|
-
"tday.com",
|
|
11
|
-
"www.tday.com",
|
|
12
|
-
"app.tday.com",
|
|
13
|
-
]);
|
|
14
|
-
export const MAX_IMAGE_BYTES = 50 * 1024 * 1024;
|
|
15
|
-
export function validateImageUrl(raw) {
|
|
16
|
-
let url;
|
|
17
|
-
try {
|
|
18
|
-
url = new URL(raw);
|
|
19
|
-
}
|
|
20
|
-
catch {
|
|
21
|
-
throw new Error(`Invalid image URL: "${raw}"`);
|
|
22
|
-
}
|
|
23
|
-
if (url.protocol !== "https:") {
|
|
24
|
-
throw new Error(`Image URL must use HTTPS (got ${url.protocol})`);
|
|
25
|
-
}
|
|
26
|
-
if (!ALLOWED_IMAGE_HOSTS.has(url.hostname)) {
|
|
27
|
-
throw new Error(`Image host "${url.hostname}" is not allowed. Expected one of: ${[...ALLOWED_IMAGE_HOSTS].join(", ")}`);
|
|
28
|
-
}
|
|
29
|
-
return url;
|
|
30
|
-
}
|
|
31
|
-
export function sandboxFilePath(raw) {
|
|
32
|
-
const cwd = process.cwd();
|
|
33
|
-
const resolved = resolve(cwd, raw);
|
|
34
|
-
const rel = relative(cwd, resolved);
|
|
35
|
-
if (rel === ".." || rel.startsWith(`..${sep}`) || isAbsolute(rel) || resolve(cwd, rel) !== resolved) {
|
|
36
|
-
throw new Error(`File path must be inside the working directory. "${raw}" resolves outside of ${cwd}`);
|
|
37
|
-
}
|
|
38
|
-
return resolved;
|
|
39
|
-
}
|
|
40
|
-
export async function prepareOutputFilePath(raw, options = {}) {
|
|
41
|
-
const filePath = sandboxFilePath(raw);
|
|
42
|
-
const parentDir = dirname(filePath);
|
|
43
|
-
await mkdir(parentDir, { recursive: true });
|
|
44
|
-
const cwdReal = await realpath(process.cwd());
|
|
45
|
-
const parentReal = await realpath(parentDir);
|
|
46
|
-
if (!isInsideDirectory(cwdReal, parentReal)) {
|
|
47
|
-
throw new Error(`File path parent directory must stay inside the working directory and cannot traverse symlinks outside of ${process.cwd()}`);
|
|
48
|
-
}
|
|
49
|
-
try {
|
|
50
|
-
const existing = await lstat(filePath);
|
|
51
|
-
if (existing.isSymbolicLink()) {
|
|
52
|
-
throw new Error("Refusing to write to a symbolic link.");
|
|
53
|
-
}
|
|
54
|
-
if (!existing.isFile()) {
|
|
55
|
-
throw new Error("Refusing to write because the target path exists and is not a regular file.");
|
|
56
|
-
}
|
|
57
|
-
if (!options.overwrite) {
|
|
58
|
-
throw new Error("File already exists. Choose a new filePath or pass overwrite: true.");
|
|
59
|
-
}
|
|
60
|
-
}
|
|
61
|
-
catch (err) {
|
|
62
|
-
if (!isNodeError(err) || err.code !== "ENOENT")
|
|
63
|
-
throw err;
|
|
64
|
-
}
|
|
65
|
-
return filePath;
|
|
66
|
-
}
|
|
67
|
-
function isInsideDirectory(base, target) {
|
|
68
|
-
const rel = relative(base, target);
|
|
69
|
-
return rel === "" || (!rel.startsWith("..") && !isAbsolute(rel));
|
|
70
|
-
}
|
|
71
|
-
function isNodeError(err) {
|
|
72
|
-
return err instanceof Error && "code" in err;
|
|
73
|
-
}
|
package/dist/installer-agents.js
DELETED
|
@@ -1,199 +0,0 @@
|
|
|
1
|
-
import { existsSync, mkdirSync, readFileSync, renameSync, writeFileSync } from "node:fs";
|
|
2
|
-
import { homedir } from "node:os";
|
|
3
|
-
import { join, dirname } from "node:path";
|
|
4
|
-
import { pinnedCaptureSpec, pinnedPackageSpec } from "./package-info.js";
|
|
5
|
-
export const SERVER_NAME = "tday";
|
|
6
|
-
export const CAPTURE_SERVER_NAME = "tday-capture";
|
|
7
|
-
export function mcpCommand() {
|
|
8
|
-
return {
|
|
9
|
-
command: "npx",
|
|
10
|
-
args: ["-y", pinnedPackageSpec(), "serve"],
|
|
11
|
-
};
|
|
12
|
-
}
|
|
13
|
-
export function captureCommand() {
|
|
14
|
-
return {
|
|
15
|
-
command: "npx",
|
|
16
|
-
args: ["-y", pinnedCaptureSpec(), "mcp"],
|
|
17
|
-
};
|
|
18
|
-
}
|
|
19
|
-
export function serverEntries(servers) {
|
|
20
|
-
return {
|
|
21
|
-
[SERVER_NAME]: mcpCommand(),
|
|
22
|
-
...(servers.capture ? { [CAPTURE_SERVER_NAME]: captureCommand() } : {}),
|
|
23
|
-
};
|
|
24
|
-
}
|
|
25
|
-
function serverList(servers) {
|
|
26
|
-
return servers.capture ? `"${SERVER_NAME}" and "${CAPTURE_SERVER_NAME}" MCP servers` : `"${SERVER_NAME}" MCP server`;
|
|
27
|
-
}
|
|
28
|
-
const plainEntry = (server) => ({ ...server });
|
|
29
|
-
const vscodeEntry = (server) => ({ type: "stdio", ...server });
|
|
30
|
-
function claudeCodeTarget(home) {
|
|
31
|
-
const configPath = join(home, ".claude.json");
|
|
32
|
-
return {
|
|
33
|
-
id: "claude-code",
|
|
34
|
-
name: "Claude Code",
|
|
35
|
-
configPath,
|
|
36
|
-
installDir: home,
|
|
37
|
-
requireFile: true,
|
|
38
|
-
missing: `No Claude Code config found at ${configPath}. Install Claude Code first, then re-run \`tday install\`.`,
|
|
39
|
-
key: "mcpServers",
|
|
40
|
-
entry: plainEntry,
|
|
41
|
-
};
|
|
42
|
-
}
|
|
43
|
-
function cursorTarget(home) {
|
|
44
|
-
const installDir = join(home, ".cursor");
|
|
45
|
-
return {
|
|
46
|
-
id: "cursor",
|
|
47
|
-
name: "Cursor",
|
|
48
|
-
configPath: join(installDir, "mcp.json"),
|
|
49
|
-
installDir,
|
|
50
|
-
missing: `No Cursor install detected (${installDir} missing).`,
|
|
51
|
-
key: "mcpServers",
|
|
52
|
-
entry: plainEntry,
|
|
53
|
-
};
|
|
54
|
-
}
|
|
55
|
-
function geminiTarget(home) {
|
|
56
|
-
const installDir = join(home, ".gemini");
|
|
57
|
-
return {
|
|
58
|
-
id: "gemini-cli",
|
|
59
|
-
name: "Gemini CLI",
|
|
60
|
-
configPath: join(installDir, "settings.json"),
|
|
61
|
-
installDir,
|
|
62
|
-
missing: `No Gemini CLI config directory detected (${installDir} missing).`,
|
|
63
|
-
key: "mcpServers",
|
|
64
|
-
entry: plainEntry,
|
|
65
|
-
};
|
|
66
|
-
}
|
|
67
|
-
export function vscodeTargets(home, platform = process.platform, env = process.env) {
|
|
68
|
-
const base = platform === "darwin"
|
|
69
|
-
? join(home, "Library", "Application Support")
|
|
70
|
-
: platform === "win32"
|
|
71
|
-
? env.APPDATA || join(home, "AppData", "Roaming")
|
|
72
|
-
: env.XDG_CONFIG_HOME || join(home, ".config");
|
|
73
|
-
return [
|
|
74
|
-
{ id: "vscode", name: "VS Code", app: "Code" },
|
|
75
|
-
{ id: "vscode-insiders", name: "VS Code Insiders", app: "Code - Insiders" },
|
|
76
|
-
].map(({ id, name, app }) => {
|
|
77
|
-
const installDir = join(base, app, "User");
|
|
78
|
-
return {
|
|
79
|
-
id: id,
|
|
80
|
-
name,
|
|
81
|
-
configPath: join(installDir, "mcp.json"),
|
|
82
|
-
installDir,
|
|
83
|
-
missing: `No ${name} install detected (${installDir} missing).`,
|
|
84
|
-
key: "servers",
|
|
85
|
-
entry: vscodeEntry,
|
|
86
|
-
};
|
|
87
|
-
});
|
|
88
|
-
}
|
|
89
|
-
function jsonTargets(home, env) {
|
|
90
|
-
return [claudeCodeTarget(home), cursorTarget(home), geminiTarget(home), ...vscodeTargets(home, process.platform, env)];
|
|
91
|
-
}
|
|
92
|
-
function patchJson(target, servers) {
|
|
93
|
-
const { id, name, configPath } = target;
|
|
94
|
-
const skipped = (message) => ({ id, name, action: "skipped", configPath, message });
|
|
95
|
-
if (!existsSync(target.installDir) || (target.requireFile && !existsSync(configPath)))
|
|
96
|
-
return skipped(target.missing);
|
|
97
|
-
let config = {};
|
|
98
|
-
if (existsSync(configPath)) {
|
|
99
|
-
let raw;
|
|
100
|
-
try {
|
|
101
|
-
raw = readFileSync(configPath, "utf8");
|
|
102
|
-
}
|
|
103
|
-
catch (err) {
|
|
104
|
-
return skipped(`Could not read ${configPath}: ${err.message}`);
|
|
105
|
-
}
|
|
106
|
-
try {
|
|
107
|
-
config = raw.trim() ? JSON.parse(raw) : {};
|
|
108
|
-
}
|
|
109
|
-
catch (err) {
|
|
110
|
-
return skipped(`Existing ${configPath} is not valid JSON (${err.message}). Fix it or patch manually.`);
|
|
111
|
-
}
|
|
112
|
-
if (!isPlainRecord(config))
|
|
113
|
-
return skipped(`Existing ${configPath} is not a JSON object. Fix it or patch manually.`);
|
|
114
|
-
}
|
|
115
|
-
const existing = config[target.key];
|
|
116
|
-
const entries = isPlainRecord(existing) ? existing : {};
|
|
117
|
-
for (const [server, entry] of Object.entries(serverEntries(servers)))
|
|
118
|
-
entries[server] = target.entry(entry);
|
|
119
|
-
config[target.key] = entries;
|
|
120
|
-
writeFileAtomic(configPath, JSON.stringify(config, null, 2) + "\n");
|
|
121
|
-
return { id, name, action: "patched", configPath, message: `Added ${serverList(servers)} to ${configPath}.` };
|
|
122
|
-
}
|
|
123
|
-
function previewJson(target, servers) {
|
|
124
|
-
const { id, name, configPath } = target;
|
|
125
|
-
if (!existsSync(target.installDir) || (target.requireFile && !existsSync(configPath))) {
|
|
126
|
-
return { id, name, action: "skipped", configPath, message: target.missing };
|
|
127
|
-
}
|
|
128
|
-
return { id, name, action: "patched", configPath, message: `Would add ${serverList(servers)} to ${configPath}.` };
|
|
129
|
-
}
|
|
130
|
-
export function codexBlock(servers) {
|
|
131
|
-
return Object.entries(serverEntries(servers))
|
|
132
|
-
.map(([server, entry]) => [
|
|
133
|
-
``,
|
|
134
|
-
`[mcp_servers."${server}"]`,
|
|
135
|
-
`command = "${entry.command}"`,
|
|
136
|
-
`args = [${entry.args.map((arg) => `"${arg}"`).join(", ")}]`,
|
|
137
|
-
].join("\n"))
|
|
138
|
-
.join("\n")
|
|
139
|
-
.concat("\n");
|
|
140
|
-
}
|
|
141
|
-
function patchCodex(home, servers) {
|
|
142
|
-
const codexDir = join(home, ".codex");
|
|
143
|
-
const configPath = join(codexDir, "config.toml");
|
|
144
|
-
if (!existsSync(codexDir)) {
|
|
145
|
-
return {
|
|
146
|
-
id: "codex",
|
|
147
|
-
name: "Codex",
|
|
148
|
-
action: "skipped",
|
|
149
|
-
configPath,
|
|
150
|
-
message: `No Codex install detected (${codexDir} missing).`,
|
|
151
|
-
};
|
|
152
|
-
}
|
|
153
|
-
return {
|
|
154
|
-
id: "codex",
|
|
155
|
-
name: "Codex",
|
|
156
|
-
action: "manual",
|
|
157
|
-
configPath,
|
|
158
|
-
message: `Append the following block to ${configPath}:\n${codexBlock(servers)}\n` +
|
|
159
|
-
`(Codex uses TOML; we don't auto-edit it to avoid corrupting your config.)`,
|
|
160
|
-
};
|
|
161
|
-
}
|
|
162
|
-
function previewCodex(home) {
|
|
163
|
-
const codexPath = join(home, ".codex", "config.toml");
|
|
164
|
-
return existsSync(join(home, ".codex"))
|
|
165
|
-
? { id: "codex", name: "Codex", action: "manual", configPath: codexPath, message: `Would print TOML block for ${codexPath} (manual append).` }
|
|
166
|
-
: { id: "codex", name: "Codex", action: "skipped", configPath: codexPath, message: `No Codex install detected.` };
|
|
167
|
-
}
|
|
168
|
-
export function patchAllAgents(servers = { capture: false }, home = homedir(), env = process.env) {
|
|
169
|
-
const [claude, cursor, gemini, ...vscode] = jsonTargets(home, env);
|
|
170
|
-
return [
|
|
171
|
-
patchJson(claude, servers),
|
|
172
|
-
patchJson(cursor, servers),
|
|
173
|
-
patchJson(gemini, servers),
|
|
174
|
-
patchCodex(home, servers),
|
|
175
|
-
...vscode.map((target) => patchJson(target, servers)),
|
|
176
|
-
];
|
|
177
|
-
}
|
|
178
|
-
export function previewAllAgents(servers = { capture: false }, home = homedir(), env = process.env) {
|
|
179
|
-
const [claude, cursor, gemini, ...vscode] = jsonTargets(home, env);
|
|
180
|
-
return [
|
|
181
|
-
previewJson(claude, servers),
|
|
182
|
-
previewJson(cursor, servers),
|
|
183
|
-
previewJson(gemini, servers),
|
|
184
|
-
previewCodex(home),
|
|
185
|
-
...vscode.map((target) => previewJson(target, servers)),
|
|
186
|
-
];
|
|
187
|
-
}
|
|
188
|
-
function isPlainRecord(value) {
|
|
189
|
-
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
190
|
-
}
|
|
191
|
-
function writeFileAtomic(path, contents) {
|
|
192
|
-
const dir = dirname(path);
|
|
193
|
-
if (!existsSync(dir)) {
|
|
194
|
-
mkdirSync(dir, { recursive: true });
|
|
195
|
-
}
|
|
196
|
-
const tmp = `${path}.tmp-${process.pid}-${Date.now()}`;
|
|
197
|
-
writeFileSync(tmp, contents, { encoding: "utf8", mode: 0o600 });
|
|
198
|
-
renameSync(tmp, path);
|
|
199
|
-
}
|
|
@@ -1,84 +0,0 @@
|
|
|
1
|
-
import http from "node:http";
|
|
2
|
-
export async function startCallbackServer(options) {
|
|
3
|
-
let resolve;
|
|
4
|
-
let reject;
|
|
5
|
-
const done = new Promise((res, rej) => {
|
|
6
|
-
resolve = res;
|
|
7
|
-
reject = rej;
|
|
8
|
-
});
|
|
9
|
-
const server = http.createServer((req, res) => {
|
|
10
|
-
res.setHeader("access-control-allow-origin", "*");
|
|
11
|
-
res.setHeader("access-control-allow-methods", "POST, OPTIONS");
|
|
12
|
-
res.setHeader("access-control-allow-headers", "content-type");
|
|
13
|
-
if (req.method === "OPTIONS") {
|
|
14
|
-
res.statusCode = 204;
|
|
15
|
-
res.end();
|
|
16
|
-
return;
|
|
17
|
-
}
|
|
18
|
-
if (req.url && new URL(req.url, "http://127.0.0.1").pathname !== "/cb") {
|
|
19
|
-
res.statusCode = 404;
|
|
20
|
-
res.end("not found");
|
|
21
|
-
return;
|
|
22
|
-
}
|
|
23
|
-
if (req.method !== "POST") {
|
|
24
|
-
res.statusCode = 405;
|
|
25
|
-
res.end("method not allowed");
|
|
26
|
-
return;
|
|
27
|
-
}
|
|
28
|
-
let body = "";
|
|
29
|
-
req.on("data", (chunk) => {
|
|
30
|
-
body += chunk.toString();
|
|
31
|
-
if (body.length > 8_192) {
|
|
32
|
-
res.statusCode = 413;
|
|
33
|
-
res.end("payload too large");
|
|
34
|
-
req.destroy();
|
|
35
|
-
}
|
|
36
|
-
});
|
|
37
|
-
req.on("end", () => {
|
|
38
|
-
let parsed;
|
|
39
|
-
try {
|
|
40
|
-
parsed = JSON.parse(body);
|
|
41
|
-
}
|
|
42
|
-
catch {
|
|
43
|
-
res.statusCode = 400;
|
|
44
|
-
res.end("invalid json");
|
|
45
|
-
return;
|
|
46
|
-
}
|
|
47
|
-
if (!parsed ||
|
|
48
|
-
typeof parsed !== "object" ||
|
|
49
|
-
typeof parsed.token !== "string" ||
|
|
50
|
-
typeof parsed.state !== "string") {
|
|
51
|
-
res.statusCode = 400;
|
|
52
|
-
res.end("missing authorization payload or state");
|
|
53
|
-
return;
|
|
54
|
-
}
|
|
55
|
-
const payload = parsed;
|
|
56
|
-
if (payload.state !== options.expectedState) {
|
|
57
|
-
res.statusCode = 400;
|
|
58
|
-
res.end("state mismatch");
|
|
59
|
-
return;
|
|
60
|
-
}
|
|
61
|
-
res.statusCode = 200;
|
|
62
|
-
res.setHeader("content-type", "application/json");
|
|
63
|
-
res.end(JSON.stringify({ ok: true }));
|
|
64
|
-
resolve(payload);
|
|
65
|
-
});
|
|
66
|
-
});
|
|
67
|
-
await new Promise((res, rej) => {
|
|
68
|
-
server.once("error", rej);
|
|
69
|
-
server.listen(0, "127.0.0.1", () => res());
|
|
70
|
-
});
|
|
71
|
-
const address = server.address();
|
|
72
|
-
if (!address || typeof address === "string") {
|
|
73
|
-
server.close();
|
|
74
|
-
throw new Error("failed to bind callback server");
|
|
75
|
-
}
|
|
76
|
-
const timer = setTimeout(() => {
|
|
77
|
-
reject(new Error(`Authorization timed out after ${options.timeoutMs / 1000}s`));
|
|
78
|
-
}, options.timeoutMs);
|
|
79
|
-
done.finally(() => {
|
|
80
|
-
clearTimeout(timer);
|
|
81
|
-
server.close();
|
|
82
|
-
});
|
|
83
|
-
return { port: address.port, done };
|
|
84
|
-
}
|