@tday-com/mcp 0.0.0-stage → 0.2.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 +105 -2
- package/dist/api-client.d.ts +23 -0
- package/dist/api-client.js +187 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +78 -0
- package/dist/config.d.ts +7 -0
- package/dist/config.js +66 -0
- package/dist/demo-import.d.ts +6 -0
- package/dist/demo-import.js +101 -0
- package/dist/download-safety.d.ts +7 -0
- package/dist/download-safety.js +76 -0
- package/dist/installer-agents.d.ts +17 -0
- package/dist/installer-agents.js +283 -0
- package/dist/installer-callback.d.ts +13 -0
- package/dist/installer-callback.js +91 -0
- package/dist/installer-slash-command.d.ts +12 -0
- package/dist/installer-slash-command.js +262 -0
- package/dist/installer.d.ts +2 -0
- package/dist/installer.js +127 -0
- package/dist/observability.d.ts +1 -0
- package/dist/observability.js +77 -0
- package/dist/package-info.d.ts +11 -0
- package/dist/package-info.js +17 -0
- package/dist/result-format.d.ts +4 -0
- package/dist/result-format.js +154 -0
- package/dist/server.d.ts +1 -0
- package/dist/server.js +37 -0
- package/dist/tool-schemas.d.ts +460 -0
- package/dist/tool-schemas.js +189 -0
- package/dist/tools-account.d.ts +6 -0
- package/dist/tools-account.js +34 -0
- package/dist/tools-demo.d.ts +11 -0
- package/dist/tools-demo.js +57 -0
- package/dist/tools-knowledge.d.ts +4 -0
- package/dist/tools-knowledge.js +111 -0
- package/dist/tools-launch.d.ts +4 -0
- package/dist/tools-launch.js +365 -0
- package/dist/tools.d.ts +15 -0
- package/dist/tools.js +296 -0
- package/package.json +58 -4
|
@@ -0,0 +1,262 @@
|
|
|
1
|
+
import { existsSync, mkdirSync, unlinkSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { homedir } from "node:os";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
const legacyCommandFilenames = [
|
|
5
|
+
"markup.md",
|
|
6
|
+
"markup-design.md",
|
|
7
|
+
"markup-brand.md",
|
|
8
|
+
"markup-credits.md",
|
|
9
|
+
"markup-ideas.md",
|
|
10
|
+
];
|
|
11
|
+
const commands = [
|
|
12
|
+
{
|
|
13
|
+
filename: "tday.md",
|
|
14
|
+
body: `---
|
|
15
|
+
name: tday
|
|
16
|
+
description: Interact with your tday.com account. Generate designs, manage brands, check credits.
|
|
17
|
+
---
|
|
18
|
+
|
|
19
|
+
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.
|
|
20
|
+
|
|
21
|
+
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."
|
|
22
|
+
|
|
23
|
+
**Tools available:**
|
|
24
|
+
- \`tday_whoami\` .. every organisation you belong to: plan, credits, what the plan includes
|
|
25
|
+
- \`tday_get_credit_prices\` .. what each thing costs, to quote before starting
|
|
26
|
+
- \`tday_get_usage\` .. what an organisation has spent this period
|
|
27
|
+
- \`tday_list_brands\` / \`tday_get_brand\` .. brand library
|
|
28
|
+
- \`tday_create_brand\` .. create brand from a website URL
|
|
29
|
+
- \`tday_list_designs\` / \`tday_get_design\` .. design history
|
|
30
|
+
- \`tday_generate_design\` .. create a new design from a prompt
|
|
31
|
+
- \`tday_poll_generation\` .. poll generation status until done
|
|
32
|
+
- \`tday_download_image\` .. save a generated image to a local file
|
|
33
|
+
|
|
34
|
+
**When generating a design:**
|
|
35
|
+
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\`.
|
|
36
|
+
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).
|
|
37
|
+
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.
|
|
38
|
+
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.
|
|
39
|
+
|
|
40
|
+
**When creating a brand:**
|
|
41
|
+
- Accept bare domains (stripe.com) or full URLs.
|
|
42
|
+
- Call \`tday_create_brand\` with { website }. It auto-extracts colors, fonts, logos.
|
|
43
|
+
- Show what was extracted: color count, font families, logo count.
|
|
44
|
+
|
|
45
|
+
**Formatting:** Keep responses tight. Show URLs as clickable markdown links. No em dashes.
|
|
46
|
+
|
|
47
|
+
**STRICT RULES (violations break user trust):**
|
|
48
|
+
- ONLY use tday_* MCP tools. NEVER use Bash, Read, Glob, Grep, Write, Edit, Agent, or any other tool.
|
|
49
|
+
- NEVER mention: localhost, dev server, bun, npm, API keys, Gemini, workers, queues, pipelines, cold starts, rate limits, backend, rendering engine, or any implementation detail.
|
|
50
|
+
- NEVER suggest: checking logs, restarting servers, debugging infrastructure, investigating code, or any developer action.
|
|
51
|
+
- NEVER say: "stuck", "hanging", "slow", "taking longer than usual", "seems to be stuck", "might be an issue".
|
|
52
|
+
- If a design is still generating: just say "Still creating your design..." and keep polling. That's it.
|
|
53
|
+
- If a design failed: say "Generation didn't complete. Try a different prompt or check back later at [editorUrl]."
|
|
54
|
+
- 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.
|
|
55
|
+
- NEVER offer to investigate code, look at the codebase, or step outside the tday.com scope.
|
|
56
|
+
- NEVER display raw imageUrl or CDN links. They are internal infrastructure URLs. Only show the editorUrl. Use designId + generationId for \`tday_download_image\`.
|
|
57
|
+
- You are the public face of tday.com. Speak like a polished product, not a developer debugging a side project.
|
|
58
|
+
`,
|
|
59
|
+
},
|
|
60
|
+
{
|
|
61
|
+
filename: "tday-design.md",
|
|
62
|
+
body: `---
|
|
63
|
+
name: tday-design
|
|
64
|
+
description: Generate a design with tday.com. Pass a brand name and prompt, or just start and you'll be asked.
|
|
65
|
+
---
|
|
66
|
+
|
|
67
|
+
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.
|
|
68
|
+
|
|
69
|
+
This is a focused workflow: resolve brand, generate, poll, deliver.
|
|
70
|
+
|
|
71
|
+
**Parse the user's input ($ARGUMENTS):**
|
|
72
|
+
- If two quoted strings like \`"Acme" "Instagram post for summer sale"\`: first is brand name, second is the design prompt.
|
|
73
|
+
- If one string: treat it as the design prompt and ask which brand to use (or pick the most recent one).
|
|
74
|
+
- If empty: ask the user what they want to design and for which brand.
|
|
75
|
+
|
|
76
|
+
**Steps:**
|
|
77
|
+
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.
|
|
78
|
+
2. **Generate.** Call \`tday_generate_design\` with { prompt, brandId, name }. Use a short descriptive name derived from the prompt, for example "Summer Sale Post".
|
|
79
|
+
3. **Poll with progress.** Call \`tday_poll_generation\` repeatedly. Do NOT use sleep or Bash between polls. Just call the tool again directly.
|
|
80
|
+
|
|
81
|
+
**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:
|
|
82
|
+
\`\`\`
|
|
83
|
+
◐ Crafting layout...
|
|
84
|
+
◓ Rendering visuals...
|
|
85
|
+
◑ AI reviewing composition...
|
|
86
|
+
◒ Polishing details...
|
|
87
|
+
\`\`\`
|
|
88
|
+
Map the API status to a human phrase:
|
|
89
|
+
- pending -> "Queued, waiting for a slot..."
|
|
90
|
+
- ideating -> "Brainstorming the concept..."
|
|
91
|
+
- generating -> "Crafting the layout..."
|
|
92
|
+
- visualising -> "Rendering visuals..."
|
|
93
|
+
- reviewing -> "AI reviewing the composition..."
|
|
94
|
+
- refining -> "Polishing the details..."
|
|
95
|
+
|
|
96
|
+
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.
|
|
97
|
+
|
|
98
|
+
4. **Deliver.** When status is \`completed\`:
|
|
99
|
+
\`\`\`
|
|
100
|
+
✓ Design ready!
|
|
101
|
+
|
|
102
|
+
Open in editor: <editorUrl>
|
|
103
|
+
\`\`\`
|
|
104
|
+
NEVER show the raw imageUrl. It is an internal CDN link and looks unprofessional. Only show the editorUrl.
|
|
105
|
+
Then ask: "Save locally?" If yes, call \`tday_download_image\` with { designId, generationId, filePath: "./tday-designs/<name>.png" }.
|
|
106
|
+
|
|
107
|
+
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.
|
|
108
|
+
|
|
109
|
+
**Tone:** Tight. One line per update. No filler. You're a machine that makes designs, not a narrator.
|
|
110
|
+
|
|
111
|
+
**STRICT RULES (violations break user trust):**
|
|
112
|
+
- ONLY use tday_* MCP tools. NEVER use Bash, Read, Glob, Grep, Write, Edit, Agent, or any other tool.
|
|
113
|
+
- NEVER mention: localhost, dev server, bun, npm, API keys, Gemini, workers, queues, pipelines, cold starts, rate limits, backend, rendering engine, or any implementation detail.
|
|
114
|
+
- NEVER suggest: checking logs, restarting servers, debugging infrastructure, investigating code, or any developer action.
|
|
115
|
+
- NEVER say: "stuck", "hanging", "slow", "taking longer than usual", "seems to be stuck", "might be an issue".
|
|
116
|
+
- 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.
|
|
117
|
+
- If a design failed: say "Generation didn't complete. Try a different prompt or check back later at [editorUrl]."
|
|
118
|
+
- NEVER offer to investigate code, look at the codebase, or step outside the tday.com scope.
|
|
119
|
+
- NEVER display raw imageUrl or CDN links. They are internal infrastructure URLs. Only show the editorUrl. Use designId + generationId for \`tday_download_image\`.
|
|
120
|
+
- You are the public face of tday.com. Speak like a polished product, not a developer debugging a side project.
|
|
121
|
+
`,
|
|
122
|
+
},
|
|
123
|
+
{
|
|
124
|
+
filename: "tday-brand.md",
|
|
125
|
+
body: `---
|
|
126
|
+
name: tday-brand
|
|
127
|
+
description: Create a brand from a URL, list your brands, or view brand details.
|
|
128
|
+
---
|
|
129
|
+
|
|
130
|
+
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.
|
|
131
|
+
|
|
132
|
+
**Parse the user's input ($ARGUMENTS):**
|
|
133
|
+
- 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.
|
|
134
|
+
- If it's "list" or empty: list all brands.
|
|
135
|
+
- Otherwise: fuzzy-match it as a brand name and show details.
|
|
136
|
+
|
|
137
|
+
**Creating a brand from URL:**
|
|
138
|
+
1. Call \`tday_create_brand\` with { website: "<the url>" }. The API auto-extracts colors, fonts, and logos from the site.
|
|
139
|
+
2. Show a summary of what was found:
|
|
140
|
+
- Brand name (auto-detected)
|
|
141
|
+
- Colors extracted (show hex values)
|
|
142
|
+
- Fonts found (show family names)
|
|
143
|
+
- Logos detected (count)
|
|
144
|
+
3. Show the link to the brand page on tday.com: \`https://tday.com/brands/<brandId>\`
|
|
145
|
+
4. Suggest: "Run \`/tday-design\` to create your first design with this brand."
|
|
146
|
+
|
|
147
|
+
**Listing brands:**
|
|
148
|
+
1. Call \`tday_list_brands\`.
|
|
149
|
+
2. Show a clean table: name, color count, font count, industry.
|
|
150
|
+
3. If no brands: "No brands yet. Run \`/tday-brand example.com\` to create one from any website."
|
|
151
|
+
|
|
152
|
+
**Viewing a brand:**
|
|
153
|
+
1. Call \`tday_list_brands\`, fuzzy-match the name.
|
|
154
|
+
2. Call \`tday_get_brand\` with the matched id.
|
|
155
|
+
3. Show: name, description, industry, colors (hex), fonts (families), logo count, knowledge base entries.
|
|
156
|
+
|
|
157
|
+
**Tone:** Efficient. Brands are the foundation, designs come next. Guide the user toward their first design.
|
|
158
|
+
|
|
159
|
+
**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.
|
|
160
|
+
`,
|
|
161
|
+
},
|
|
162
|
+
{
|
|
163
|
+
filename: "tday-credits.md",
|
|
164
|
+
body: `---
|
|
165
|
+
name: tday-credits
|
|
166
|
+
description: Check your plan and credit balance.
|
|
167
|
+
---
|
|
168
|
+
|
|
169
|
+
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.
|
|
170
|
+
|
|
171
|
+
Quick plan and credit check for tday.com.
|
|
172
|
+
|
|
173
|
+
1. Call \`tday_whoami\`.
|
|
174
|
+
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.
|
|
175
|
+
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.
|
|
176
|
+
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.
|
|
177
|
+
|
|
178
|
+
Keep it to 3-5 lines per organisation. No filler.
|
|
179
|
+
`,
|
|
180
|
+
},
|
|
181
|
+
{
|
|
182
|
+
filename: "tday-ideas.md",
|
|
183
|
+
body: `---
|
|
184
|
+
name: tday-ideas
|
|
185
|
+
description: Brainstorm design ideas for a brand, then generate the one you pick.
|
|
186
|
+
---
|
|
187
|
+
|
|
188
|
+
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.
|
|
189
|
+
|
|
190
|
+
**Parse the user's input ($ARGUMENTS):**
|
|
191
|
+
- If a brand name is given: resolve it via \`tday_list_brands\`.
|
|
192
|
+
- If empty: ask which brand, or pick the most recent one.
|
|
193
|
+
|
|
194
|
+
**Steps:**
|
|
195
|
+
1. **Understand the brand.** Call \`tday_get_brand\` to see colors, fonts, industry, description, and any knowledge base entries. This gives you creative context.
|
|
196
|
+
2. **Brainstorm.** Suggest 5 design ideas tailored to this brand. Each idea should be:
|
|
197
|
+
- A specific format (Instagram post, LinkedIn banner, event flyer, product card, story ad, etc.)
|
|
198
|
+
- A concrete concept (not generic). Reference the brand's industry, colors, or identity.
|
|
199
|
+
- One line each, numbered.
|
|
200
|
+
|
|
201
|
+
Example for a fintech brand:
|
|
202
|
+
1. Instagram carousel, "3 Steps to Smarter Savings", using brand blues with data visualization style
|
|
203
|
+
2. LinkedIn banner, founder quote with geometric brand pattern overlay
|
|
204
|
+
3. Product launch card, new feature announcement with screenshot mockup
|
|
205
|
+
4. Story ad, "Did you know?" stat with bold typography on dark background
|
|
206
|
+
5. Event flyer, upcoming webinar with speaker photo placeholder and brand gradients
|
|
207
|
+
|
|
208
|
+
3. **Ask:** "Pick a number (or describe your own idea) and I'll generate it."
|
|
209
|
+
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).
|
|
210
|
+
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.
|
|
211
|
+
|
|
212
|
+
**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.
|
|
213
|
+
|
|
214
|
+
**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, Gemini, 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.
|
|
215
|
+
`,
|
|
216
|
+
},
|
|
217
|
+
];
|
|
218
|
+
/** A command's text by filename, without installing anything (tests read it without touching ~/.claude). */
|
|
219
|
+
export function slashCommandBody(filename) {
|
|
220
|
+
return commands.find((command) => command.filename === filename)?.body;
|
|
221
|
+
}
|
|
222
|
+
export function installSlashCommands() {
|
|
223
|
+
const claudeDir = join(homedir(), ".claude");
|
|
224
|
+
if (!existsSync(claudeDir)) {
|
|
225
|
+
return commands.map((c) => ({
|
|
226
|
+
path: join(claudeDir, "commands", c.filename),
|
|
227
|
+
installed: false,
|
|
228
|
+
}));
|
|
229
|
+
}
|
|
230
|
+
const commandsDir = join(claudeDir, "commands");
|
|
231
|
+
if (!existsSync(commandsDir)) {
|
|
232
|
+
mkdirSync(commandsDir, { recursive: true });
|
|
233
|
+
}
|
|
234
|
+
cleanupLegacySlashCommands(commandsDir);
|
|
235
|
+
return commands.map((cmd) => {
|
|
236
|
+
const path = join(commandsDir, cmd.filename);
|
|
237
|
+
writeFileSync(path, cmd.body, { encoding: "utf8", mode: 0o644 });
|
|
238
|
+
return { path, installed: true };
|
|
239
|
+
});
|
|
240
|
+
}
|
|
241
|
+
export function cleanupLegacySlashCommands(commandsDir) {
|
|
242
|
+
const removed = [];
|
|
243
|
+
for (const filename of legacyCommandFilenames) {
|
|
244
|
+
const path = join(commandsDir, filename);
|
|
245
|
+
try {
|
|
246
|
+
if (existsSync(path)) {
|
|
247
|
+
unlinkSync(path);
|
|
248
|
+
removed.push(path);
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
catch {
|
|
252
|
+
// Best-effort cleanup. Installing current tday commands should still proceed.
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
return removed;
|
|
256
|
+
}
|
|
257
|
+
// Backward-compat alias for installer.ts
|
|
258
|
+
export function installSlashCommand() {
|
|
259
|
+
const results = installSlashCommands();
|
|
260
|
+
const firstInstalled = results.find((r) => r.installed);
|
|
261
|
+
return firstInstalled ?? { path: results[0]?.path ?? "", installed: false };
|
|
262
|
+
}
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
import { randomBytes } from "node:crypto";
|
|
2
|
+
import { chmodSync, existsSync, mkdirSync, writeFileSync } from "node:fs";
|
|
3
|
+
import { homedir } from "node:os";
|
|
4
|
+
import { spawn } from "node:child_process";
|
|
5
|
+
import { dirname, join } from "node:path";
|
|
6
|
+
import { startCallbackServer } from "./installer-callback.js";
|
|
7
|
+
import { patchAllAgents, previewAllAgents } from "./installer-agents.js";
|
|
8
|
+
import { installSlashCommands } from "./installer-slash-command.js";
|
|
9
|
+
const WEB_URL = (process.env.TDAY_WEB_URL ?? "https://tday.com").replace(/\/+$/, "");
|
|
10
|
+
const API_URL = (process.env.TDAY_API_URL ?? WEB_URL).replace(/\/+$/, "").replace(/\/api$/i, "");
|
|
11
|
+
const TIMEOUT_MS = 5 * 60 * 1000;
|
|
12
|
+
export async function runInstaller(args) {
|
|
13
|
+
const skipBrowser = args.includes("--no-browser");
|
|
14
|
+
const dryRun = args.includes("--dry-run");
|
|
15
|
+
console.log("tday MCP installer\n");
|
|
16
|
+
if (dryRun) {
|
|
17
|
+
console.log("Dry run - no files will be written, no browser opened.\n");
|
|
18
|
+
const results = previewAllAgents();
|
|
19
|
+
for (const r of results) {
|
|
20
|
+
console.log(` [${r.action}] ${r.name} - ${r.message}`);
|
|
21
|
+
}
|
|
22
|
+
return;
|
|
23
|
+
}
|
|
24
|
+
const state = randomBytes(16).toString("hex");
|
|
25
|
+
const clientName = detectClientLabel();
|
|
26
|
+
console.log("1. Starting local callback listener…");
|
|
27
|
+
const { port, done } = await startCallbackServer({
|
|
28
|
+
expectedState: state,
|
|
29
|
+
timeoutMs: TIMEOUT_MS,
|
|
30
|
+
});
|
|
31
|
+
const callbackUrl = `http://127.0.0.1:${port}/cb`;
|
|
32
|
+
const authUrl = `${WEB_URL}/cli/auth` +
|
|
33
|
+
`?callback=${encodeURIComponent(callbackUrl)}` +
|
|
34
|
+
`&state=${encodeURIComponent(state)}` +
|
|
35
|
+
`&client=${encodeURIComponent(clientName)}`;
|
|
36
|
+
console.log("2. Sign in to tday in your browser to authorize this MCP connection:\n");
|
|
37
|
+
console.log(` ${authUrl}\n`);
|
|
38
|
+
if (!skipBrowser) {
|
|
39
|
+
openBrowser(authUrl);
|
|
40
|
+
}
|
|
41
|
+
console.log("3. Waiting for authorization (timeout 5 min)…\n");
|
|
42
|
+
let token;
|
|
43
|
+
try {
|
|
44
|
+
const result = await done;
|
|
45
|
+
token = result.token;
|
|
46
|
+
}
|
|
47
|
+
catch (err) {
|
|
48
|
+
console.error(`\nAuthorization failed: ${err.message}`);
|
|
49
|
+
process.exit(1);
|
|
50
|
+
}
|
|
51
|
+
console.log("4. Verifying account link…");
|
|
52
|
+
const ok = await verifyToken(token);
|
|
53
|
+
if (!ok) {
|
|
54
|
+
console.error(`Account link verification failed against ${API_URL}/api/billing/info. Nothing was saved.`);
|
|
55
|
+
process.exit(1);
|
|
56
|
+
}
|
|
57
|
+
saveConfig(token);
|
|
58
|
+
console.log(` Saved secure local connection to ${configPath()} (chmod 0600)\n`);
|
|
59
|
+
console.log("5. Patching installed AI agents (Claude Code, Cursor, Gemini CLI, Codex)…");
|
|
60
|
+
const results = patchAllAgents();
|
|
61
|
+
for (const r of results) {
|
|
62
|
+
const tag = r.action === "patched" ? "+" : r.action === "manual" ? "!" : "-";
|
|
63
|
+
console.log(` ${tag} ${r.name}: ${r.message}`);
|
|
64
|
+
}
|
|
65
|
+
console.log("\n6. Installing Claude Code slash commands...");
|
|
66
|
+
const cmds = installSlashCommands();
|
|
67
|
+
for (const cmd of cmds) {
|
|
68
|
+
console.log(cmd.installed ? ` + ${cmd.path}` : ` - Skipped (Claude Code not found)`);
|
|
69
|
+
}
|
|
70
|
+
console.log(`\nDone. Restart your AI agent. In Claude Code, type / to see: /tday, /tday-design, /tday-brand, /tday-credits, /tday-ideas`);
|
|
71
|
+
}
|
|
72
|
+
// --------------------------------------------------------------------------
|
|
73
|
+
// helpers
|
|
74
|
+
// --------------------------------------------------------------------------
|
|
75
|
+
function configPath() {
|
|
76
|
+
return join(homedir(), ".tday", "config.json");
|
|
77
|
+
}
|
|
78
|
+
function saveConfig(token) {
|
|
79
|
+
const path = configPath();
|
|
80
|
+
const dir = dirname(path);
|
|
81
|
+
if (!existsSync(dir))
|
|
82
|
+
mkdirSync(dir, { recursive: true, mode: 0o700 });
|
|
83
|
+
chmodSync(dir, 0o700);
|
|
84
|
+
writeFileSync(path, JSON.stringify({ token, apiBaseUrl: API_URL, webBaseUrl: WEB_URL }, null, 2) + "\n", { encoding: "utf8", mode: 0o600 });
|
|
85
|
+
chmodSync(path, 0o600);
|
|
86
|
+
}
|
|
87
|
+
async function verifyToken(token) {
|
|
88
|
+
try {
|
|
89
|
+
const res = await fetch(`${API_URL}/api/billing/info`, {
|
|
90
|
+
headers: { authorization: `Bearer ${token}` },
|
|
91
|
+
});
|
|
92
|
+
return res.ok;
|
|
93
|
+
}
|
|
94
|
+
catch {
|
|
95
|
+
return false;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
function detectClientLabel() {
|
|
99
|
+
if (process.env.TDAY_CLIENT_LABEL)
|
|
100
|
+
return process.env.TDAY_CLIENT_LABEL;
|
|
101
|
+
if (process.env.TERM_PROGRAM === "Claude Code")
|
|
102
|
+
return "claude-code";
|
|
103
|
+
if (process.env.CURSOR_TRACE_ID)
|
|
104
|
+
return "cursor";
|
|
105
|
+
return "tday-cli";
|
|
106
|
+
}
|
|
107
|
+
export function openBrowser(url) {
|
|
108
|
+
const platform = process.platform;
|
|
109
|
+
const warn = () => console.warn("Could not open the browser. Open the printed URL manually to continue.");
|
|
110
|
+
try {
|
|
111
|
+
let child;
|
|
112
|
+
if (platform === "darwin") {
|
|
113
|
+
child = spawn("open", [url], { detached: true, stdio: "ignore" });
|
|
114
|
+
}
|
|
115
|
+
else if (platform === "win32") {
|
|
116
|
+
child = spawn("cmd", ["/c", "start", "", url], { detached: true, stdio: "ignore" });
|
|
117
|
+
}
|
|
118
|
+
else {
|
|
119
|
+
child = spawn("xdg-open", [url], { detached: true, stdio: "ignore" });
|
|
120
|
+
}
|
|
121
|
+
child.once("error", warn);
|
|
122
|
+
child.unref();
|
|
123
|
+
}
|
|
124
|
+
catch {
|
|
125
|
+
warn();
|
|
126
|
+
}
|
|
127
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function startObservability(): void;
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { NodeSDK } from "@opentelemetry/sdk-node";
|
|
2
|
+
import { OTLPTraceExporter } from "@opentelemetry/exporter-trace-otlp-http";
|
|
3
|
+
import { OTLPLogExporter } from "@opentelemetry/exporter-logs-otlp-http";
|
|
4
|
+
import { OTLPMetricExporter } from "@opentelemetry/exporter-metrics-otlp-http";
|
|
5
|
+
import { PeriodicExportingMetricReader } from "@opentelemetry/sdk-metrics";
|
|
6
|
+
import { BatchLogRecordProcessor } from "@opentelemetry/sdk-logs";
|
|
7
|
+
import { getNodeAutoInstrumentations } from "@opentelemetry/auto-instrumentations-node";
|
|
8
|
+
import { resourceFromAttributes } from "@opentelemetry/resources";
|
|
9
|
+
import { ATTR_SERVICE_NAME, ATTR_SERVICE_VERSION, } from "@opentelemetry/semantic-conventions";
|
|
10
|
+
const SUPERLOG_ENDPOINT = "https://intake.superlog.sh";
|
|
11
|
+
const SUPERLOG_PUBLIC_TOKEN = "sl_public_IY6nDv2SVznCdynG-aIoRhUdDK1yRNFRBOB8TDagtEc";
|
|
12
|
+
const headers = { Authorization: `Bearer ${SUPERLOG_PUBLIC_TOKEN}` };
|
|
13
|
+
// Telemetry leaves the user's machine, so it is OFF unless they opt in with
|
|
14
|
+
// TDAY_MCP_TELEMETRY=1. It used to switch on whenever NODE_ENV was "production", which many
|
|
15
|
+
// shells set, without the user knowing. When on, it sends traces/metrics/logs of this process
|
|
16
|
+
// (tool calls and their outbound HTTP to tday) to our collector, never the token or file
|
|
17
|
+
// contents: auto-instrumentation records URLs and timings, and fs instrumentation is disabled.
|
|
18
|
+
const TELEMETRY_OPT_IN = process.env.TDAY_MCP_TELEMETRY === "1";
|
|
19
|
+
const resource = resourceFromAttributes({
|
|
20
|
+
[ATTR_SERVICE_NAME]: "tday-mcp",
|
|
21
|
+
[ATTR_SERVICE_VERSION]: "0.0.1",
|
|
22
|
+
});
|
|
23
|
+
let started = false;
|
|
24
|
+
export function startObservability() {
|
|
25
|
+
if (!TELEMETRY_OPT_IN)
|
|
26
|
+
return;
|
|
27
|
+
if (started)
|
|
28
|
+
return;
|
|
29
|
+
started = true;
|
|
30
|
+
const sdk = new NodeSDK({
|
|
31
|
+
resource,
|
|
32
|
+
traceExporter: new OTLPTraceExporter({
|
|
33
|
+
url: `${SUPERLOG_ENDPOINT}/v1/traces`,
|
|
34
|
+
headers,
|
|
35
|
+
}),
|
|
36
|
+
metricReader: new PeriodicExportingMetricReader({
|
|
37
|
+
exporter: new OTLPMetricExporter({
|
|
38
|
+
url: `${SUPERLOG_ENDPOINT}/v1/metrics`,
|
|
39
|
+
headers,
|
|
40
|
+
}),
|
|
41
|
+
exportIntervalMillis: 30000,
|
|
42
|
+
}),
|
|
43
|
+
logRecordProcessors: [
|
|
44
|
+
// @opentelemetry/sdk-logs 0.222 moved the exporter from a positional
|
|
45
|
+
// argument into BatchLogRecordProcessorOptions, matching the shape
|
|
46
|
+
// PeriodicExportingMetricReader already uses above.
|
|
47
|
+
new BatchLogRecordProcessor({
|
|
48
|
+
exporter: new OTLPLogExporter({
|
|
49
|
+
url: `${SUPERLOG_ENDPOINT}/v1/logs`,
|
|
50
|
+
headers,
|
|
51
|
+
}),
|
|
52
|
+
}),
|
|
53
|
+
],
|
|
54
|
+
instrumentations: [
|
|
55
|
+
getNodeAutoInstrumentations({
|
|
56
|
+
"@opentelemetry/instrumentation-fs": { enabled: false },
|
|
57
|
+
}),
|
|
58
|
+
],
|
|
59
|
+
});
|
|
60
|
+
sdk.start();
|
|
61
|
+
const shutdown = async () => {
|
|
62
|
+
try {
|
|
63
|
+
await sdk.shutdown();
|
|
64
|
+
}
|
|
65
|
+
catch {
|
|
66
|
+
// ignore
|
|
67
|
+
}
|
|
68
|
+
};
|
|
69
|
+
const shutdownAndExit = (signal) => async () => {
|
|
70
|
+
await shutdown();
|
|
71
|
+
// Re-raise default termination behavior so the process actually exits.
|
|
72
|
+
process.kill(process.pid, signal);
|
|
73
|
+
};
|
|
74
|
+
process.once("SIGTERM", shutdownAndExit("SIGTERM"));
|
|
75
|
+
process.once("SIGINT", shutdownAndExit("SIGINT"));
|
|
76
|
+
process.once("beforeExit", shutdown);
|
|
77
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export declare const PACKAGE_NAME = "@tday-com/mcp";
|
|
2
|
+
/** This build's own version, read from the package.json it shipped in (dist/ and src/ both sit one level down). */
|
|
3
|
+
export declare function packageVersion(): string;
|
|
4
|
+
/**
|
|
5
|
+
* What an agent's MCP config launches: this exact version, not whatever `latest` is on the day
|
|
6
|
+
* the agent next starts. `npx -y <name>` re-resolves on every launch, so a bad publish (a
|
|
7
|
+
* compromised maintainer account, a mistaken tag) would reach every installed agent, and the
|
|
8
|
+
* token in ~/.tday/config.json with it, without anyone running anything. Pinned, an update only
|
|
9
|
+
* arrives when someone re-runs `install`, which writes the version it was itself run as.
|
|
10
|
+
*/
|
|
11
|
+
export declare function pinnedPackageSpec(): string;
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { readFileSync } from "node:fs";
|
|
2
|
+
export const PACKAGE_NAME = "@tday-com/mcp";
|
|
3
|
+
/** This build's own version, read from the package.json it shipped in (dist/ and src/ both sit one level down). */
|
|
4
|
+
export function packageVersion() {
|
|
5
|
+
const pkg = JSON.parse(readFileSync(new URL("../package.json", import.meta.url), "utf8"));
|
|
6
|
+
return pkg.version;
|
|
7
|
+
}
|
|
8
|
+
/**
|
|
9
|
+
* What an agent's MCP config launches: this exact version, not whatever `latest` is on the day
|
|
10
|
+
* the agent next starts. `npx -y <name>` re-resolves on every launch, so a bad publish (a
|
|
11
|
+
* compromised maintainer account, a mistaken tag) would reach every installed agent, and the
|
|
12
|
+
* token in ~/.tday/config.json with it, without anyone running anything. Pinned, an update only
|
|
13
|
+
* arrives when someone re-runs `install`, which writes the version it was itself run as.
|
|
14
|
+
*/
|
|
15
|
+
export function pinnedPackageSpec() {
|
|
16
|
+
return `${PACKAGE_NAME}@${packageVersion()}`;
|
|
17
|
+
}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import type { ResponseFormat } from "./tool-schemas.js";
|
|
2
|
+
export declare function stringifyResult(result: unknown): string;
|
|
3
|
+
export declare function toStructuredContent(result: unknown): Record<string, unknown>;
|
|
4
|
+
export declare function formatToolResultText(toolName: string, result: unknown, format: ResponseFormat): string;
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
// Tool-result presentation: JSON stringification, structuredContent shaping,
|
|
2
|
+
// and per-tool markdown rendering.
|
|
3
|
+
const MAX_TEXT_CHARS = 25_000;
|
|
4
|
+
export function stringifyResult(result) {
|
|
5
|
+
return JSON.stringify(result, (_key, value) => (typeof value === "bigint" ? value.toString() : value), 2);
|
|
6
|
+
}
|
|
7
|
+
export function toStructuredContent(result) {
|
|
8
|
+
return isRecord(result) ? result : { result };
|
|
9
|
+
}
|
|
10
|
+
export function formatToolResultText(toolName, result, format) {
|
|
11
|
+
const text = format === "markdown" ? markdownResult(toolName, result) : stringifyResult(result);
|
|
12
|
+
return truncateText(text);
|
|
13
|
+
}
|
|
14
|
+
function isRecord(value) {
|
|
15
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
16
|
+
}
|
|
17
|
+
function truncateText(text) {
|
|
18
|
+
if (text.length <= MAX_TEXT_CHARS)
|
|
19
|
+
return text;
|
|
20
|
+
return `${text.slice(0, MAX_TEXT_CHARS)}\n\n[Response text truncated at ${MAX_TEXT_CHARS} characters. Use structuredContent or narrower list/get calls for the complete data.]`;
|
|
21
|
+
}
|
|
22
|
+
function markdownResult(toolName, result) {
|
|
23
|
+
if (!isRecord(result))
|
|
24
|
+
return String(result);
|
|
25
|
+
switch (toolName) {
|
|
26
|
+
case "tday_whoami":
|
|
27
|
+
return markdownWhoami(result);
|
|
28
|
+
case "tday_list_brands":
|
|
29
|
+
return markdownList("Brands", result.brands, ["id", "name", "role", "status", "industry"]);
|
|
30
|
+
case "tday_list_designs":
|
|
31
|
+
return markdownList("Designs", result.designs, ["id", "name", "brandName", "currentStep", "updatedAt"]);
|
|
32
|
+
case "tday_generate_design":
|
|
33
|
+
return [
|
|
34
|
+
"# Generation queued",
|
|
35
|
+
`- Design ID: ${result.designId ?? "unknown"}`,
|
|
36
|
+
`- Generation ID: ${result.generationId ?? "unknown"}`,
|
|
37
|
+
`- Status: ${result.status ?? "unknown"}`,
|
|
38
|
+
result.editorUrl ? `- Editor: ${result.editorUrl}` : undefined,
|
|
39
|
+
"",
|
|
40
|
+
"Poll with `tday_poll_generation` until status is `completed` or `failed`.",
|
|
41
|
+
].filter(Boolean).join("\n");
|
|
42
|
+
case "tday_poll_generation":
|
|
43
|
+
return [
|
|
44
|
+
"# Generation status",
|
|
45
|
+
`- Design ID: ${result.designId ?? "unknown"}`,
|
|
46
|
+
`- Generation ID: ${result.generationId ?? "unknown"}`,
|
|
47
|
+
`- Status: ${result.status ?? "unknown"}`,
|
|
48
|
+
result.imageUrl ? `- Image URL: ${result.imageUrl}` : undefined,
|
|
49
|
+
result.editorUrl ? `- Editor: ${result.editorUrl}` : undefined,
|
|
50
|
+
result.failureMessage ? `- Failure: ${result.failureMessage}` : undefined,
|
|
51
|
+
].filter(Boolean).join("\n");
|
|
52
|
+
case "tday_search_knowledge_entries":
|
|
53
|
+
return markdownList("Knowledge entries", result.entries, [
|
|
54
|
+
"slug",
|
|
55
|
+
"type",
|
|
56
|
+
"draft",
|
|
57
|
+
"verified",
|
|
58
|
+
"description",
|
|
59
|
+
]);
|
|
60
|
+
case "tday_create_knowledge_entry":
|
|
61
|
+
return [
|
|
62
|
+
result.created ? "# Knowledge entry recorded" : "# Knowledge entry updated",
|
|
63
|
+
`- Slug: ${display(result.slug)}`,
|
|
64
|
+
`- Graph node: ${display(result.node)}`,
|
|
65
|
+
"",
|
|
66
|
+
"Recorded as a draft - it appears in the knowledge graph immediately, marked unreviewed.",
|
|
67
|
+
].join("\n");
|
|
68
|
+
case "tday_download_image":
|
|
69
|
+
return [
|
|
70
|
+
"# Image downloaded",
|
|
71
|
+
`- Saved: ${result.saved ? "yes" : "no"}`,
|
|
72
|
+
`- Path: ${result.path ?? "unknown"}`,
|
|
73
|
+
`- Size: ${result.size ?? 0} bytes`,
|
|
74
|
+
].join("\n");
|
|
75
|
+
default:
|
|
76
|
+
return `\`\`\`json\n${stringifyResult(result)}\n\`\`\``;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
function markdownWhoami(result) {
|
|
80
|
+
const account = isRecord(result.account) ? result.account : {};
|
|
81
|
+
const subscription = isRecord(result.subscription) ? result.subscription : {};
|
|
82
|
+
return [
|
|
83
|
+
"# tday account",
|
|
84
|
+
`- Name: ${display(account.name)}`,
|
|
85
|
+
`- Email: ${display(account.email)}`,
|
|
86
|
+
`- Auth source: ${display(isRecord(result.auth) ? result.auth.source : undefined)}`,
|
|
87
|
+
`- Plan: ${display(subscription.plan ?? subscription.name ?? subscription.tier)}`,
|
|
88
|
+
balanceLine(result),
|
|
89
|
+
].join("\n");
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* The balance line. Credits when the account has them (`credits.enabled`);
|
|
93
|
+
* otherwise the prepaid dollar balance an account carried before credits,
|
|
94
|
+
* which is what `/api/billing/info` reports until credits reach the account.
|
|
95
|
+
* Every number is the API's own: nothing here knows a price or a grant.
|
|
96
|
+
*/
|
|
97
|
+
function balanceLine(result) {
|
|
98
|
+
const credits = isRecord(result.credits) ? result.credits : null;
|
|
99
|
+
if (credits && credits.enabled === true) {
|
|
100
|
+
if (typeof credits.balance !== "number")
|
|
101
|
+
return `- Credits: ${display(credits.balance)}`;
|
|
102
|
+
// An uncapped contract has no grant to count down from, and is never refused.
|
|
103
|
+
if (credits.grant === null)
|
|
104
|
+
return "- Credits: usage is covered by your contract";
|
|
105
|
+
const grant = typeof credits.grant === "number" ? ` of ${formatCount(credits.grant)}` : "";
|
|
106
|
+
const refresh = typeof credits.refreshesAt === "string" ? `, refreshes ${formatDay(credits.refreshesAt)}` : "";
|
|
107
|
+
const low = credits.lowBalance === "low" || credits.lowBalance === "critical" ? " (running low)" : "";
|
|
108
|
+
return `- Credits: ${formatCount(credits.balance)}${grant} left${refresh}${low}`;
|
|
109
|
+
}
|
|
110
|
+
const legacy = isRecord(result.apiBalance) ? result.apiBalance : null;
|
|
111
|
+
if (legacy && typeof legacy.balanceCents === "number") {
|
|
112
|
+
return `- Prepaid balance: $${(legacy.balanceCents / 100).toFixed(2)}`;
|
|
113
|
+
}
|
|
114
|
+
return `- Balance: ${display(result.apiBalance)}`;
|
|
115
|
+
}
|
|
116
|
+
function formatCount(value) {
|
|
117
|
+
return Math.trunc(value).toLocaleString("en-US");
|
|
118
|
+
}
|
|
119
|
+
/** "Oct 1", in UTC like the app's own refusal copy. The raw value when it is not a date. */
|
|
120
|
+
function formatDay(iso) {
|
|
121
|
+
const date = new Date(iso);
|
|
122
|
+
if (Number.isNaN(date.getTime()))
|
|
123
|
+
return iso;
|
|
124
|
+
return date.toLocaleDateString("en-US", { month: "short", day: "numeric", timeZone: "UTC" });
|
|
125
|
+
}
|
|
126
|
+
function markdownList(title, value, columns) {
|
|
127
|
+
const items = Array.isArray(value) ? value.filter(isRecord) : [];
|
|
128
|
+
const lines = [`# ${title}`, ""];
|
|
129
|
+
if (items.length === 0) {
|
|
130
|
+
lines.push("No items returned.");
|
|
131
|
+
return lines.join("\n");
|
|
132
|
+
}
|
|
133
|
+
lines.push(`Returned ${items.length} item${items.length === 1 ? "" : "s"}.`, "");
|
|
134
|
+
lines.push(`| ${columns.join(" | ")} |`);
|
|
135
|
+
lines.push(`| ${columns.map(() => "---").join(" | ")} |`);
|
|
136
|
+
for (const item of items) {
|
|
137
|
+
lines.push(`| ${columns.map((column) => escapeMarkdownCell(display(item[column]))).join(" | ")} |`);
|
|
138
|
+
}
|
|
139
|
+
return lines.join("\n");
|
|
140
|
+
}
|
|
141
|
+
function display(value) {
|
|
142
|
+
if (value === null || value === undefined || value === "")
|
|
143
|
+
return "-";
|
|
144
|
+
if (typeof value === "string" ||
|
|
145
|
+
typeof value === "number" ||
|
|
146
|
+
typeof value === "boolean" ||
|
|
147
|
+
typeof value === "bigint") {
|
|
148
|
+
return String(value);
|
|
149
|
+
}
|
|
150
|
+
return JSON.stringify(value, (_key, v) => (typeof v === "bigint" ? v.toString() : v));
|
|
151
|
+
}
|
|
152
|
+
function escapeMarkdownCell(value) {
|
|
153
|
+
return value.replace(/\\/g, "\\\\").replace(/\|/g, "\\|").replace(/\n/g, " ");
|
|
154
|
+
}
|