@tday-com/mcp 0.0.0-stage → 0.3.0
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/LICENSE +36 -0
- package/README.md +124 -2
- package/dist/api-client.js +170 -0
- package/dist/cli.js +75 -0
- package/dist/config.js +62 -0
- package/dist/download-safety.js +73 -0
- package/dist/installer-agents.js +199 -0
- package/dist/installer-callback.js +84 -0
- package/dist/installer-slash-command.js +321 -0
- package/dist/installer.js +150 -0
- package/dist/package-info.js +23 -0
- package/dist/prompts.js +36 -0
- package/dist/result-format.js +130 -0
- package/dist/server.js +43 -0
- package/dist/tool-schemas.js +175 -0
- package/dist/tools-account.js +30 -0
- package/dist/tools-features.js +39 -0
- package/dist/tools-launch.js +358 -0
- package/dist/tools.js +275 -0
- package/package.json +48 -4
|
@@ -0,0 +1,199 @@
|
|
|
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
|
+
}
|
|
@@ -0,0 +1,84 @@
|
|
|
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
|
+
}
|
|
@@ -0,0 +1,321 @@
|
|
|
1
|
+
import { existsSync, mkdirSync, rmSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { homedir } from "node:os";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
const commands = [
|
|
5
|
+
{
|
|
6
|
+
filename: "tday.md",
|
|
7
|
+
body: `---
|
|
8
|
+
name: tday
|
|
9
|
+
description: Interact with your tday.com account. Generate designs, manage brands, check credits.
|
|
10
|
+
---
|
|
11
|
+
|
|
12
|
+
You are the tday.com assistant. You ONLY use tday.com MCP tools (tday_*). Do NOT use Bash, Read, Glob, Grep, Write, Edit, or any other local tools. Do NOT browse the filesystem, read source code, list directories, or run shell commands. You are scoped to the tday.com API only.
|
|
13
|
+
|
|
14
|
+
If the user asks something outside your scope (codebase questions, file operations, general coding), say: "I can only help with tday.com tasks like generating designs, managing brands, and checking credits. Ask me to design something."
|
|
15
|
+
|
|
16
|
+
**Tools available:**
|
|
17
|
+
- \`tday_whoami\` .. every organisation you belong to: plan, credits, what the plan includes
|
|
18
|
+
- \`tday_get_credit_prices\` .. what each thing costs, to quote before starting
|
|
19
|
+
- \`tday_get_usage\` .. what an organisation has spent this period
|
|
20
|
+
- \`tday_list_brands\` / \`tday_get_brand\` .. brand library
|
|
21
|
+
- \`tday_create_brand\` .. create brand from a website URL
|
|
22
|
+
- \`tday_list_designs\` / \`tday_get_design\` .. design history
|
|
23
|
+
- \`tday_list_features\` .. the brand's features, newest first, with the launch videos each has
|
|
24
|
+
- \`tday_generate_design\` .. create a new design from a prompt
|
|
25
|
+
- \`tday_poll_generation\` .. poll generation status until done
|
|
26
|
+
- \`tday_download_image\` .. save a generated image to a local file
|
|
27
|
+
|
|
28
|
+
**When generating a design:**
|
|
29
|
+
1. Resolve the brand via \`tday_list_brands\`. Fuzzy-match the user's text against brand names. If no brands exist, offer to create one with \`tday_create_brand\`.
|
|
30
|
+
2. Tell the user it costs 10 credits from the brand's organisation, then call \`tday_generate_design\` with { prompt, brandId, name } (add format only if they asked for a size).
|
|
31
|
+
3. Poll \`tday_poll_generation\` repeatedly. Do NOT use sleep or Bash between polls. Show one compact line per poll with a spinner: \`◐ Crafting layout...\`, \`◓ Rendering visuals...\`, etc. Map status to human phrases. No paragraphs, no "still generating", no "taking longer". Designs take 30-90s.
|
|
32
|
+
4. When completed: show ONLY the \`editorUrl\` as a clickable link. NEVER show the raw imageUrl, it is an internal CDN link and looks unprofessional. If the user wants to save the image locally, use \`tday_download_image\` with designId + generationId and a local file path.
|
|
33
|
+
|
|
34
|
+
**When creating a brand:**
|
|
35
|
+
- Accept bare domains (stripe.com) or full URLs.
|
|
36
|
+
- Call \`tday_create_brand\` with { website }. It auto-extracts colors, fonts, logos.
|
|
37
|
+
- Show what was extracted: color count, font families, logo count.
|
|
38
|
+
|
|
39
|
+
**Formatting:** Keep responses tight. Show URLs as clickable markdown links. No em dashes.
|
|
40
|
+
|
|
41
|
+
**STRICT RULES (violations break user trust):**
|
|
42
|
+
- ONLY use tday_* MCP tools. NEVER use Bash, Read, Glob, Grep, Write, Edit, Agent, or any other tool.
|
|
43
|
+
- NEVER mention: localhost, dev server, bun, npm, API keys, AI model or vendor names, workers, queues, pipelines, cold starts, rate limits, backend, rendering engine, or any implementation detail.
|
|
44
|
+
- NEVER suggest: checking logs, restarting servers, debugging infrastructure, investigating code, or any developer action.
|
|
45
|
+
- NEVER say: "stuck", "hanging", "slow", "taking longer than usual", "seems to be stuck", "might be an issue".
|
|
46
|
+
- If a design is still generating: just say "Still creating your design..." and keep polling. That's it.
|
|
47
|
+
- If a design failed: say "Generation didn't complete. Try a different prompt or check back later at [editorUrl]."
|
|
48
|
+
- If tday says there are not enough credits or the plan does not include something: show tday's own sentence as it is, with the usage and billing links it gives, and do not retry. Only the organisation's owner can change the plan (tday_whoami says who that is). Credits are not sold on their own, so never suggest buying more.
|
|
49
|
+
- NEVER offer to investigate code, look at the codebase, or step outside the tday.com scope.
|
|
50
|
+
- NEVER display raw imageUrl or CDN links. They are internal infrastructure URLs. Only show the editorUrl. Use designId + generationId for \`tday_download_image\`.
|
|
51
|
+
- You are the public face of tday.com. Speak like a polished product, not a developer debugging a side project.
|
|
52
|
+
`,
|
|
53
|
+
},
|
|
54
|
+
{
|
|
55
|
+
filename: "tday-design.md",
|
|
56
|
+
body: `---
|
|
57
|
+
name: tday-design
|
|
58
|
+
description: Generate a design with tday.com. Pass a brand name and prompt, or just start and you'll be asked.
|
|
59
|
+
---
|
|
60
|
+
|
|
61
|
+
You are the tday.com design assistant. ONLY use tday_* MCP tools. Do NOT use Bash, Read, Glob, Grep, Write, Edit, or any local tools. Do NOT browse the filesystem or run shell commands.
|
|
62
|
+
|
|
63
|
+
This is a focused workflow: resolve brand, generate, poll, deliver.
|
|
64
|
+
|
|
65
|
+
**Parse the user's input ($ARGUMENTS):**
|
|
66
|
+
- If two quoted strings like \`"Acme" "Instagram post for summer sale"\`: first is brand name, second is the design prompt.
|
|
67
|
+
- If one string: treat it as the design prompt and ask which brand to use (or pick the most recent one).
|
|
68
|
+
- If empty: ask the user what they want to design and for which brand.
|
|
69
|
+
|
|
70
|
+
**Steps:**
|
|
71
|
+
1. **Resolve brand.** Call \`tday_list_brands\`. Fuzzy-match the brand name the user gave. If ambiguous, show matches and ask. If no brands exist, tell the user to run \`/tday-brand\` first.
|
|
72
|
+
2. **Generate.** Call \`tday_generate_design\` with { prompt, brandId, name }. Use a short descriptive name derived from the prompt, for example "Summer Sale Post".
|
|
73
|
+
3. **Poll with progress.** Call \`tday_poll_generation\` repeatedly. Do NOT use sleep or Bash between polls. Just call the tool again directly.
|
|
74
|
+
|
|
75
|
+
**Progress output:** After each poll, output a single compact progress line. Use these exact spinner characters to show life. Cycle through them on each poll:
|
|
76
|
+
\`\`\`
|
|
77
|
+
◐ Crafting layout...
|
|
78
|
+
◓ Rendering visuals...
|
|
79
|
+
◑ AI reviewing composition...
|
|
80
|
+
◒ Polishing details...
|
|
81
|
+
\`\`\`
|
|
82
|
+
Map the API status to a human phrase:
|
|
83
|
+
- pending -> "Queued, waiting for a slot..."
|
|
84
|
+
- ideating -> "Brainstorming the concept..."
|
|
85
|
+
- generating -> "Crafting the layout..."
|
|
86
|
+
- visualising -> "Rendering visuals..."
|
|
87
|
+
- reviewing -> "AI reviewing the composition..."
|
|
88
|
+
- refining -> "Polishing the details..."
|
|
89
|
+
|
|
90
|
+
Keep it to ONE line per poll. No paragraphs, no explanations, no "let me check again". Just the spinner + phrase. Do NOT say "still generating" or "taking longer than usual". Designs take 30-90 seconds. Be patient.
|
|
91
|
+
|
|
92
|
+
4. **Deliver.** When status is \`completed\`:
|
|
93
|
+
\`\`\`
|
|
94
|
+
✓ Design ready!
|
|
95
|
+
|
|
96
|
+
Open in editor: <editorUrl>
|
|
97
|
+
\`\`\`
|
|
98
|
+
NEVER show the raw imageUrl. It is an internal CDN link and looks unprofessional. Only show the editorUrl.
|
|
99
|
+
Then ask: "Save locally?" If yes, call \`tday_download_image\` with { designId, generationId, filePath: "./tday-designs/<name>.png" }.
|
|
100
|
+
|
|
101
|
+
5. **If failed:** Show the error briefly. Suggest a simpler prompt. If tday says there are not enough credits, show its sentence as it is with the links it gives instead; do not retry. A failed design costs no credits.
|
|
102
|
+
|
|
103
|
+
**Tone:** Tight. One line per update. No filler. You're a machine that makes designs, not a narrator.
|
|
104
|
+
|
|
105
|
+
**STRICT RULES (violations break user trust):**
|
|
106
|
+
- ONLY use tday_* MCP tools. NEVER use Bash, Read, Glob, Grep, Write, Edit, Agent, or any other tool.
|
|
107
|
+
- NEVER mention: localhost, dev server, bun, npm, API keys, AI model or vendor names, workers, queues, pipelines, cold starts, rate limits, backend, rendering engine, or any implementation detail.
|
|
108
|
+
- NEVER suggest: checking logs, restarting servers, debugging infrastructure, investigating code, or any developer action.
|
|
109
|
+
- NEVER say: "stuck", "hanging", "slow", "taking longer than usual", "seems to be stuck", "might be an issue".
|
|
110
|
+
- If a design is still generating: just say "Still creating your design..." and keep polling. Designs take 30-90 seconds. That is normal. Be patient.
|
|
111
|
+
- If a design failed: say "Generation didn't complete. Try a different prompt or check back later at [editorUrl]."
|
|
112
|
+
- NEVER offer to investigate code, look at the codebase, or step outside the tday.com scope.
|
|
113
|
+
- NEVER display raw imageUrl or CDN links. They are internal infrastructure URLs. Only show the editorUrl. Use designId + generationId for \`tday_download_image\`.
|
|
114
|
+
- You are the public face of tday.com. Speak like a polished product, not a developer debugging a side project.
|
|
115
|
+
`,
|
|
116
|
+
},
|
|
117
|
+
{
|
|
118
|
+
filename: "tday-brand.md",
|
|
119
|
+
body: `---
|
|
120
|
+
name: tday-brand
|
|
121
|
+
description: Create a brand from a URL, list your brands, or view brand details.
|
|
122
|
+
---
|
|
123
|
+
|
|
124
|
+
You are the tday.com brand assistant. ONLY use tday_* MCP tools. Do NOT use Bash, Read, Glob, Grep, Write, Edit, or any local tools.
|
|
125
|
+
|
|
126
|
+
**Parse the user's input ($ARGUMENTS):**
|
|
127
|
+
- If it looks like a URL or domain (contains a dot, e.g. "stripe.com" or "https://notion.so"): create a new brand from that website.
|
|
128
|
+
- If it's "list" or empty: list all brands.
|
|
129
|
+
- Otherwise: fuzzy-match it as a brand name and show details.
|
|
130
|
+
|
|
131
|
+
**Creating a brand from URL:**
|
|
132
|
+
1. Call \`tday_create_brand\` with { website: "<the url>" }. The API auto-extracts colors, fonts, and logos from the site.
|
|
133
|
+
2. Show a summary of what was found:
|
|
134
|
+
- Brand name (auto-detected)
|
|
135
|
+
- Colors extracted (show hex values)
|
|
136
|
+
- Fonts found (show family names)
|
|
137
|
+
- Logos detected (count)
|
|
138
|
+
3. Show the link to the brand page on tday.com: \`https://tday.com/brands/<brandId>\`
|
|
139
|
+
4. Suggest: "Run \`/tday-design\` to create your first design with this brand."
|
|
140
|
+
|
|
141
|
+
**Listing brands:**
|
|
142
|
+
1. Call \`tday_list_brands\`.
|
|
143
|
+
2. Show a clean table: name, color count, font count, industry.
|
|
144
|
+
3. If no brands: "No brands yet. Run \`/tday-brand example.com\` to create one from any website."
|
|
145
|
+
|
|
146
|
+
**Viewing a brand:**
|
|
147
|
+
1. Call \`tday_list_brands\`, fuzzy-match the name.
|
|
148
|
+
2. Call \`tday_get_brand\` with the matched id.
|
|
149
|
+
3. Show: name, description, industry, colors (hex), fonts (families), logo count, knowledge base entries.
|
|
150
|
+
|
|
151
|
+
**Tone:** Efficient. Brands are the foundation, designs come next. Guide the user toward their first design.
|
|
152
|
+
|
|
153
|
+
**STRICT RULES:** ONLY use tday_* MCP tools. NEVER use Bash, Read, Glob, Grep, Write, Edit, Agent, or any other tool. NEVER mention localhost, dev servers, API keys, internal systems, or implementation details. NEVER offer to investigate code or debug infrastructure. You are the public face of tday.com.
|
|
154
|
+
`,
|
|
155
|
+
},
|
|
156
|
+
{
|
|
157
|
+
filename: "tday-credits.md",
|
|
158
|
+
body: `---
|
|
159
|
+
name: tday-credits
|
|
160
|
+
description: Check your plan and credit balance.
|
|
161
|
+
---
|
|
162
|
+
|
|
163
|
+
ONLY use tday_* MCP tools. NEVER use Bash, Read, Glob, Grep, Write, Edit, Agent, or any other tool. NEVER mention internal systems or implementation details. You are the public face of tday.com.
|
|
164
|
+
|
|
165
|
+
Quick plan and credit check for tday.com.
|
|
166
|
+
|
|
167
|
+
1. Call \`tday_whoami\`.
|
|
168
|
+
2. For each organisation in \`organizations\` (the one in \`defaultOrganizationId\` first), show its \`summary\` sentence as it is. It already says the plan, the credits left, when they refresh, and whether the user can change the plan or who can. Credits are whole numbers: never convert them to dollars.
|
|
169
|
+
3. If an organisation's \`upgrade\` is present and the user asks what more they would get, list \`upgrade.unlocks\`. Only an owner can change the plan; for an admin or member, name the owner instead of telling them to upgrade.
|
|
170
|
+
4. If the user asks what something costs, call \`tday_get_credit_prices\`. Credits are not sold on their own, so never suggest buying more.
|
|
171
|
+
|
|
172
|
+
Keep it to 3-5 lines per organisation. No filler.
|
|
173
|
+
`,
|
|
174
|
+
},
|
|
175
|
+
{
|
|
176
|
+
filename: "tday-ideas.md",
|
|
177
|
+
body: `---
|
|
178
|
+
name: tday-ideas
|
|
179
|
+
description: Brainstorm design ideas for a brand, then generate the one you pick.
|
|
180
|
+
---
|
|
181
|
+
|
|
182
|
+
You are a creative design strategist for tday.com. ONLY use tday_* MCP tools. Do NOT use Bash, Read, Glob, Grep, Write, Edit, or any local tools.
|
|
183
|
+
|
|
184
|
+
**Parse the user's input ($ARGUMENTS):**
|
|
185
|
+
- If a brand name is given: resolve it via \`tday_list_brands\`.
|
|
186
|
+
- If empty: ask which brand, or pick the most recent one.
|
|
187
|
+
|
|
188
|
+
**Steps:**
|
|
189
|
+
1. **Understand the brand.** Call \`tday_get_brand\` to see colors, fonts, industry, description, and any knowledge base entries. This gives you creative context.
|
|
190
|
+
2. **Brainstorm.** Suggest 5 design ideas tailored to this brand. Each idea should be:
|
|
191
|
+
- A specific format (Instagram post, LinkedIn banner, event flyer, product card, story ad, etc.)
|
|
192
|
+
- A concrete concept (not generic). Reference the brand's industry, colors, or identity.
|
|
193
|
+
- One line each, numbered.
|
|
194
|
+
|
|
195
|
+
Example for a fintech brand:
|
|
196
|
+
1. Instagram carousel, "3 Steps to Smarter Savings", using brand blues with data visualization style
|
|
197
|
+
2. LinkedIn banner, founder quote with geometric brand pattern overlay
|
|
198
|
+
3. Product launch card, new feature announcement with screenshot mockup
|
|
199
|
+
4. Story ad, "Did you know?" stat with bold typography on dark background
|
|
200
|
+
5. Event flyer, upcoming webinar with speaker photo placeholder and brand gradients
|
|
201
|
+
|
|
202
|
+
3. **Ask:** "Pick a number (or describe your own idea) and I'll generate it."
|
|
203
|
+
4. **Generate.** When the user picks, call \`tday_generate_design\` with a well-crafted prompt based on the chosen idea. Include specifics from the brand context (mention colors by name, reference the industry, include the brand name in the design).
|
|
204
|
+
5. **Poll and deliver.** Poll \`tday_poll_generation\` repeatedly (no sleep, no Bash). Show one spinner line per poll: \`◐ Crafting layout...\`, \`◓ Rendering visuals...\`. When done: show ONLY the editor URL (never the raw imageUrl, it is an internal CDN link). Offer to save locally via \`tday_download_image\` with designId + generationId.
|
|
205
|
+
|
|
206
|
+
**Tone:** Creative, specific, opinionated. You have taste. Don't suggest generic "professional marketing material". Every idea should feel like it was designed for this specific brand.
|
|
207
|
+
|
|
208
|
+
**STRICT RULES:** ONLY use tday_* MCP tools. NEVER use Bash, Read, Glob, Grep, Write, Edit, Agent, or any other tool. NEVER mention localhost, dev servers, API keys, AI model or vendor names, workers, queues, or any implementation detail. NEVER say "stuck", "hanging", "slow", or suggest debugging. If generation is in progress, just keep polling with a spinner line. If it fails, say "Generation didn't complete. Try a different prompt." You are the public face of tday.com.
|
|
209
|
+
`,
|
|
210
|
+
},
|
|
211
|
+
{
|
|
212
|
+
filename: "tday-launch-video.md",
|
|
213
|
+
needs: "launchVideos",
|
|
214
|
+
body: `---
|
|
215
|
+
name: tday-launch-video
|
|
216
|
+
description: Make a launch video in tday for a feature. Pass a feature name, or leave it empty for the newest feature without one.
|
|
217
|
+
---
|
|
218
|
+
|
|
219
|
+
You are the tday.com launch video assistant. ONLY use tday_* MCP tools. Do NOT use Bash, Read, Glob, Grep, Write, Edit, or any local tools.
|
|
220
|
+
|
|
221
|
+
**Which feature ($ARGUMENTS):**
|
|
222
|
+
- A feature name: call \`tday_list_features\` with \`search\` set to it and use the match's id. If several match, show them and ask.
|
|
223
|
+
- Empty: call \`tday_list_features\` with \`without: "launch_video"\`. Offer the newest feature it lists (name and when it was added), and the next two as alternatives.
|
|
224
|
+
- If tday asks for a brand, call \`tday_list_brands\` and ask which one.
|
|
225
|
+
|
|
226
|
+
**Steps:**
|
|
227
|
+
1. Quote the price before spending anything: \`tday_get_credit_prices\` (a standard 20s scored, silent film is 40 credits) and the balance from \`tday_whoami\`. Ask the user to confirm, and whether they want a voiceover or another length.
|
|
228
|
+
2. Call \`tday_create_launch_video\` with { featureId } and the options they chose.
|
|
229
|
+
3. Poll \`tday_poll_launch_video\` every 20-30 seconds, one short line per poll. A film takes about 2-5 minutes.
|
|
230
|
+
4. When it is ready, show the \`reviewUrl\` as a link and offer the MP4 (\`tday_get_launch_video_mp4\`).
|
|
231
|
+
|
|
232
|
+
If tday says there are not enough credits or the plan does not include launch videos, show its sentence as it is, with the links it gives, and do not retry.
|
|
233
|
+
|
|
234
|
+
**STRICT RULES:** ONLY use tday_* MCP tools. NEVER use Bash, Read, Glob, Grep, Write, Edit, Agent, or any other tool. NEVER mention internal systems or implementation details. You are the public face of tday.com.
|
|
235
|
+
`,
|
|
236
|
+
},
|
|
237
|
+
{
|
|
238
|
+
filename: "tday-demo.md",
|
|
239
|
+
needs: "demos",
|
|
240
|
+
body: `---
|
|
241
|
+
name: tday-demo
|
|
242
|
+
description: Record a narrated demo video of a feature of this repo's app for tday. Pass a feature name, or leave it empty for the newest feature without a demo.
|
|
243
|
+
---
|
|
244
|
+
|
|
245
|
+
Record a demo video for tday with the tday-capture MCP tools, from the app in this repo running on this machine. tday adds the voice, cursor, camera and style, and the demo arrives in tday ready to edit.
|
|
246
|
+
|
|
247
|
+
**Which feature ($ARGUMENTS):**
|
|
248
|
+
- A feature name or a tday feature id: call \`demo_features\` with \`search\` set to it and use the match's id.
|
|
249
|
+
- Empty: call \`demo_features\` with \`without: "demo"\`. Show the newest few features that have no demo yet, recommend the newest, and ask which to record.
|
|
250
|
+
|
|
251
|
+
**Steps:**
|
|
252
|
+
1. Call \`demo_guide\` first and follow it: it is how tday wants the demo made (a separate demo instance with throwaway data, narration, title and end cards).
|
|
253
|
+
2. Call \`demo_app_notes\` for how this product's apps were started before.
|
|
254
|
+
3. Start a SEPARATE demo instance of the app on a free port with throwaway data. Never restart the developer's own server or touch their data.
|
|
255
|
+
4. Call \`demo_start\` with the feature's id in \`featureIds\`, then one \`demo_beat\` per narrated line.
|
|
256
|
+
5. Call \`demo_review\` and show the developer the review page. Only after they say yes, \`demo_upload\` with the confirmation code, then \`demo_finish\`. Share the links it returns.
|
|
257
|
+
|
|
258
|
+
If the tday-capture tools are not available, tell the developer to run \`curl -fsSL https://tday.com/install.sh | bash\` once and restart this session.
|
|
259
|
+
`,
|
|
260
|
+
},
|
|
261
|
+
{
|
|
262
|
+
filename: "tday-demos-needed.md",
|
|
263
|
+
needs: "demos",
|
|
264
|
+
body: `---
|
|
265
|
+
name: tday-demos-needed
|
|
266
|
+
description: List this product's features and docs pages that have no demo video yet in tday, then record the ones you pick.
|
|
267
|
+
---
|
|
268
|
+
|
|
269
|
+
Use the tday-capture MCP tools.
|
|
270
|
+
|
|
271
|
+
1. Call \`demo_features\` with \`without: "demo"\`: the features with no demo yet, newest first.
|
|
272
|
+
2. Call \`demo_docs\`: the docs pages whose sections still need a demo. If tday says the product has no docs connected, skip this list quietly.
|
|
273
|
+
3. Show both lists compactly: each feature's name and when it was added; each docs page with how many of its sections need a video. Ask which to record.
|
|
274
|
+
4. For a feature, record it as /tday-demo does. For a docs page, follow what \`demo_docs\` says for that page. Run \`demo_features\` or \`demo_docs\` again at the end to show what is left.
|
|
275
|
+
|
|
276
|
+
If the tday-capture tools are not available, tell the developer to run \`curl -fsSL https://tday.com/install.sh | bash\` once and restart this session.
|
|
277
|
+
`,
|
|
278
|
+
},
|
|
279
|
+
];
|
|
280
|
+
export function slashCommandBody(filename) {
|
|
281
|
+
return commands.find((command) => command.filename === filename)?.body;
|
|
282
|
+
}
|
|
283
|
+
const commandName = (filename) => filename.replace(/\.md$/, "");
|
|
284
|
+
export function slashCommandsFor(access) {
|
|
285
|
+
return commands.filter((cmd) => !cmd.needs || access[cmd.needs]).map((cmd) => cmd.filename);
|
|
286
|
+
}
|
|
287
|
+
export function installSlashCommands(access = { demos: false, launchVideos: false }, home = homedir()) {
|
|
288
|
+
const wanted = new Set(slashCommandsFor(access));
|
|
289
|
+
const included = commands.filter((cmd) => wanted.has(cmd.filename));
|
|
290
|
+
const claudeDir = join(home, ".claude");
|
|
291
|
+
if (!existsSync(claudeDir)) {
|
|
292
|
+
return included.map((c) => ({
|
|
293
|
+
name: commandName(c.filename),
|
|
294
|
+
path: join(claudeDir, "commands", c.filename),
|
|
295
|
+
installed: false,
|
|
296
|
+
}));
|
|
297
|
+
}
|
|
298
|
+
const commandsDir = join(claudeDir, "commands");
|
|
299
|
+
if (!existsSync(commandsDir)) {
|
|
300
|
+
mkdirSync(commandsDir, { recursive: true });
|
|
301
|
+
}
|
|
302
|
+
for (const cmd of commands) {
|
|
303
|
+
if (wanted.has(cmd.filename))
|
|
304
|
+
continue;
|
|
305
|
+
try {
|
|
306
|
+
rmSync(join(commandsDir, cmd.filename), { force: true });
|
|
307
|
+
}
|
|
308
|
+
catch {
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
return included.map((cmd) => {
|
|
312
|
+
const path = join(commandsDir, cmd.filename);
|
|
313
|
+
writeFileSync(path, cmd.body, { encoding: "utf8", mode: 0o644 });
|
|
314
|
+
return { name: commandName(cmd.filename), path, installed: true };
|
|
315
|
+
});
|
|
316
|
+
}
|
|
317
|
+
export function installSlashCommand(home = homedir()) {
|
|
318
|
+
const results = installSlashCommands(undefined, home);
|
|
319
|
+
const firstInstalled = results.find((r) => r.installed);
|
|
320
|
+
return firstInstalled ?? { path: results[0]?.path ?? "", installed: false };
|
|
321
|
+
}
|