glenn-code 1.0.12 → 1.0.14
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/adapters/mcps/stdio-server.js +131 -83
- package/package.json +1 -1
|
@@ -2,19 +2,19 @@
|
|
|
2
2
|
// Glenn Code - Bundled and minified
|
|
3
3
|
// (c) DNM Lab - All rights reserved
|
|
4
4
|
|
|
5
|
-
import{McpServer as
|
|
6
|
-
`;console.error(`[Stdio MCP Server] ${
|
|
5
|
+
import{McpServer as v}from"@modelcontextprotocol/sdk/server/mcp.js";import{StdioServerTransport as O}from"@modelcontextprotocol/sdk/server/stdio.js";import{z as d}from"zod";import*as g from"fs";import*as _ from"path";import{execFileSync as D}from"child_process";var E=process.env.API_URL||"http://localhost:5338",i=process.env.PROJECT_ID,b=process.env.PROJECT_KEY,T=process.env.WORKSPACE_DIR||process.cwd(),k="/tmp/agent-logs",C=_.join(k,"stdio-mcp-server.log");try{g.existsSync(k)||g.mkdirSync(k,{recursive:!0})}catch{}function s(t,...e){let r=`[${new Date().toISOString()}] ${t} ${e.map(o=>typeof o=="object"?JSON.stringify(o):String(o)).join(" ")}
|
|
6
|
+
`;console.error(`[Stdio MCP Server] ${t}`,...e);try{g.appendFileSync(C,r,"utf8")}catch{}}s("Starting...");s("API_URL:",E);s("PROJECT_ID:",i||"(not set)");s("PROJECT_KEY:",b?`${b.slice(0,12)}...`:"(not set)");s("WORKSPACE_DIR:",T);s("Process cwd:",process.cwd());s("__filename equivalent:",import.meta.url);s("Log file:",C);function I(t){return t.toLowerCase().replace(/[^a-z0-9]+/g,"-").replace(/^-|-$/g,"")}function h(t){return t&&t.replace(/[\uD800-\uDBFF](?![\uDC00-\uDFFF])|(?<![\uD800-\uDBFF])[\uDC00-\uDFFF]/g,"\uFFFD")}function $(t){if(typeof t=="string")return h(t);if(Array.isArray(t))return t.map(e=>$(e));if(t&&typeof t=="object"){let e={};for(let[n,r]of Object.entries(t))e[n]=$(r);return e}return t}async function l(t,e){let n=`${E}${t}`,r={"Content-Type":"application/json"};return b&&(r["X-Project-Key"]=b),fetch(n,{...e,headers:{...r,...e?.headers}})}var p=new v({name:"agent-planning",version:"1.0.0"});p.tool("save_specification","Save a specification. First write content to a temp file with Write tool, then call this with the file path.",{name:d.string().describe("Specification name"),filePath:d.string().describe("Path to file containing the spec content")},async({name:t,filePath:e})=>{if(s("save_specification called",{name:t,filePath:e,projectId:i||"(not set)"}),!i)return s("save_specification failed: PROJECT_ID not set"),{content:[{type:"text",text:"\u274C PROJECT_ID not set"}]};let n=_.resolve(e);if(!g.existsSync(n))return s("save_specification failed: file not found",{resolvedPath:n}),{content:[{type:"text",text:`\u274C File not found: ${e}. Use Write tool first to create the file.`}]};let r=g.readFileSync(n,"utf-8"),o=I(t);s("save_specification reading file",{resolvedPath:n,contentLength:r.length,slug:o});try{let c=await l(`/api/projects/${i}/specifications`,{method:"POST",body:JSON.stringify({slug:o,name:t,content:r})});if(!c.ok){let u=await c.text();return s("save_specification failed:",{status:c.status,error:u}),{content:[{type:"text",text:`\u274C Failed to save: ${u}`}]}}let a=await c.json();return a.success?(s("save_specification succeeded:",{name:t,slug:o}),{content:[{type:"text",text:`\u2705 Saved specification: ${o}.spec.md`}]}):(s("save_specification failed:",{result:a}),{content:[{type:"text",text:`\u274C ${a.error||"Failed to save"}`}]})}catch(c){return s("save_specification error:",c instanceof Error?c.message:String(c),c),{content:[{type:"text",text:`\u274C Error: ${c instanceof Error?c.message:String(c)}`}]}}});p.tool("read_specification","Read a specification's content",{name:d.string().describe("Specification name")},async({name:t})=>{if(!i)return{content:[{type:"text",text:"\u274C PROJECT_ID not set"}]};let e=I(t);try{let n=await l(`/api/projects/${i}/specifications/${e}`);if(!n.ok)return{content:[{type:"text",text:`\u274C Specification "${t}" not found.`}]};let r=await n.json();return!r.success||!r.content?{content:[{type:"text",text:`\u274C ${r.error||"Specification not found"}`}]}:{content:[{type:"text",text:h(r.content)}]}}catch(n){return{content:[{type:"text",text:`\u274C Error: ${n instanceof Error?n.message:String(n)}`}]}}});p.tool("list_specifications","List all specifications",{},async()=>{if(!i)return{content:[{type:"text",text:"\u274C PROJECT_ID not set"}]};try{let t=await l(`/api/projects/${i}/specifications`);if(!t.ok)return{content:[{type:"text",text:"\u{1F4CB} No specifications found."}]};let e=await t.json();return!e.success||!e.specifications||e.specifications.length===0?{content:[{type:"text",text:"\u{1F4CB} No specifications found."}]}:{content:[{type:"text",text:`\u{1F4CB} Specifications:
|
|
7
7
|
|
|
8
|
-
${
|
|
9
|
-
`)}`}]}}catch(
|
|
8
|
+
${e.specifications.map(r=>`- ${r.name} (${r.slug}.spec.md)`).join(`
|
|
9
|
+
`)}`}]}}catch(t){return{content:[{type:"text",text:`\u274C Error: ${t instanceof Error?t.message:String(t)}`}]}}});p.tool("delete_specification","Delete a specification",{name:d.string().describe("Specification name")},async({name:t})=>{if(!i)return{content:[{type:"text",text:"\u274C PROJECT_ID not set"}]};let e=I(t);try{let n=await l(`/api/projects/${i}/specifications/${e}`,{method:"DELETE"});if(!n.ok)return{content:[{type:"text",text:`\u274C Specification "${t}" not found.`}]};let r=await n.json();return r.success?{content:[{type:"text",text:`\u{1F5D1}\uFE0F Deleted: ${e}.spec.md`}]}:{content:[{type:"text",text:`\u274C ${r.error||"Failed to delete"}`}]}}catch(n){return{content:[{type:"text",text:`\u274C Error: ${n instanceof Error?n.message:String(n)}`}]}}});var N=d.object({id:d.string().describe("Unique identifier for the question"),question:d.string().describe("The question to ask the user"),options:d.array(d.string()).optional().describe("Predefined options the user can choose from"),allowFreeText:d.boolean().optional().default(!0).describe("Allow user to type a custom answer"),multiSelect:d.boolean().optional().default(!1).describe("Allow selecting multiple options")}),j=null;p.tool("start_question_session",`Start a conversational question session to gather requirements from the user.
|
|
10
10
|
|
|
11
11
|
Call this BEFORE asking any questions. It initializes the session context.
|
|
12
12
|
|
|
13
13
|
After calling this, use ask_question to ask individual questions one at a time.
|
|
14
|
-
Each question should build on the previous answer (leading questions approach).`,{purpose:
|
|
15
|
-
Purpose: ${
|
|
14
|
+
Each question should build on the previous answer (leading questions approach).`,{purpose:d.string().describe("What you're trying to understand/gather (e.g., 'understand feature requirements')")},async({purpose:t})=>(s("start_question_session called",{purpose:t,projectId:i||"(not set)"}),j=t,{content:[{type:"text",text:`\u2705 Question session started.
|
|
15
|
+
Purpose: ${t}
|
|
16
16
|
|
|
17
|
-
Now call ask_question to ask your first question. Ask ONE question at a time, and let the user's answer guide your next question.`}]}));
|
|
17
|
+
Now call ask_question to ask your first question. Ask ONE question at a time, and let the user's answer guide your next question.`}]}));p.tool("ask_question",`Ask a SINGLE question to the user and wait for their answer.
|
|
18
18
|
|
|
19
19
|
This tool will:
|
|
20
20
|
1. Display the question in the UI
|
|
@@ -31,33 +31,33 @@ Parameters:
|
|
|
31
31
|
- question: The question text
|
|
32
32
|
- options: Optional predefined choices (user can still type custom answer)
|
|
33
33
|
- allowFreeText: Let user type custom answer (default: true)
|
|
34
|
-
- multiSelect: Allow multiple selections (default: false)`,{question:
|
|
34
|
+
- multiSelect: Allow multiple selections (default: false)`,{question:d.string().describe("The question to ask"),options:d.array(d.string()).optional().describe("Predefined answer options"),allowFreeText:d.boolean().optional().default(!0),multiSelect:d.boolean().optional().default(!1)},async({question:t,options:e,allowFreeText:n,multiSelect:r})=>{if(s("ask_question called",{question:t,projectId:i||"(not set)"}),!i)return{content:[{type:"text",text:"\u274C PROJECT_ID not set"}]};let c={id:`q_${Date.now()}`,question:t,options:e||[],allowFreeText:n??!0,multiSelect:r??!1};try{let a=await l(`/api/projects/${i}/questions/ask`,{method:"POST",body:JSON.stringify({question:c,sessionContext:j?{purpose:j}:null})});if(!a.ok){let u=await a.text();return s("ask_question failed:",{status:a.status,error:u}),{content:[{type:"text",text:`\u274C Failed to ask question: ${u}`}]}}return s("ask_question succeeded"),{content:[{type:"text",text:`\u2705 Question sent to user. STOP HERE and wait for their answer.
|
|
35
35
|
|
|
36
|
-
The user's answer will be provided in the next message. Based on their answer, decide what to ask next (or proceed with implementation if you have enough info).`}]}}catch(
|
|
36
|
+
The user's answer will be provided in the next message. Based on their answer, decide what to ask next (or proceed with implementation if you have enough info).`}]}}catch(a){return s("ask_question error:",a instanceof Error?a.message:String(a)),{content:[{type:"text",text:`\u274C Error: ${a instanceof Error?a.message:String(a)}`}]}}});p.tool("end_question_session",`End the current question session and summarize what you learned.
|
|
37
37
|
|
|
38
38
|
Call this when:
|
|
39
39
|
1. You have gathered enough information to proceed
|
|
40
40
|
2. The user said "start implementing", "that's enough", "let's go", etc.
|
|
41
41
|
3. You've asked enough questions (aim for 3-7, don't exhaust the user!)
|
|
42
42
|
|
|
43
|
-
After ending the session, you can proceed with implementation.`,{summary:
|
|
43
|
+
After ending the session, you can proceed with implementation.`,{summary:d.string().describe("Brief summary of what you learned from the questions")},async({summary:t})=>{if(s("end_question_session called",{summary:t,projectId:i||"(not set)"}),!i)return{content:[{type:"text",text:"\u274C PROJECT_ID not set"}]};try{await l(`/api/projects/${i}/questions/end-session`,{method:"POST",body:JSON.stringify({summary:t})})}catch(e){s("end_question_session error (non-fatal):",e instanceof Error?e.message:String(e))}return j=null,{content:[{type:"text",text:`\u2705 Question session ended.
|
|
44
44
|
|
|
45
|
-
Summary: ${
|
|
45
|
+
Summary: ${t}
|
|
46
46
|
|
|
47
|
-
You may now proceed with implementation based on what you learned.`}]}});
|
|
47
|
+
You may now proceed with implementation based on what you learned.`}]}});p.tool("report_learning",`Report a critical insight that would save significant time if added to CLAUDE.md.
|
|
48
48
|
Use this when you discover something important about the codebase or workflow.
|
|
49
49
|
|
|
50
50
|
Categories:
|
|
51
51
|
- missing_instruction: Something that should be documented in CLAUDE.md
|
|
52
52
|
- time_saver: Workflow tip that saves significant time
|
|
53
53
|
- gotcha: Common mistake or pitfall to avoid
|
|
54
|
-
- workflow_tip: Best practice you discovered`,{category:
|
|
54
|
+
- workflow_tip: Best practice you discovered`,{category:d.enum(["missing_instruction","time_saver","gotcha","workflow_tip"]).describe("Category of the learning"),description:d.string().describe("The insight - write it as you'd want it in CLAUDE.md"),file:d.string().optional().describe("Relevant file path if applicable")},async({category:t,description:e,file:n})=>{if(s("report_learning called",{category:t,projectId:i||"(not set)"}),!i)return s("report_learning failed: PROJECT_ID not set"),{content:[{type:"text",text:"\u274C PROJECT_ID not set"}]};try{let r=await l(`/api/projects/${i}/insights`,{method:"POST",body:JSON.stringify({type:"Learning",category:t,description:e,file:n})});if(!r.ok){let o=await r.text();return s("report_learning failed:",{status:r.status,error:o}),{content:[{type:"text",text:`\u26A0\uFE0F Learning noted locally (API error): ${t}`}]}}return s("report_learning succeeded:",{category:t}),{content:[{type:"text",text:`\u2705 Learning recorded: ${t}`}]}}catch(r){return s("report_learning error:",r instanceof Error?r.message:String(r)),{content:[{type:"text",text:`\u26A0\uFE0F Learning noted locally (error): ${t}`}]}}});p.tool("report_bug",`Report a bug, issue, or enhancement that needs human attention.
|
|
55
55
|
|
|
56
56
|
Categories:
|
|
57
57
|
- bug: Code bug found in the application
|
|
58
58
|
- scaffold_improvement: Enhancement needed for scaffold tool
|
|
59
59
|
- agent_env: Issue with agent environment or setup
|
|
60
|
-
- code_generation: Problem with generated code patterns`,{category:
|
|
60
|
+
- code_generation: Problem with generated code patterns`,{category:d.enum(["bug","scaffold_improvement","agent_env","code_generation"]).describe("Category of the bug/issue"),description:d.string().describe("Clear description of the issue"),file:d.string().optional().describe("File where the issue was found")},async({category:t,description:e,file:n})=>{if(s("report_bug called",{category:t,projectId:i||"(not set)"}),!i)return s("report_bug failed: PROJECT_ID not set"),{content:[{type:"text",text:"\u274C PROJECT_ID not set"}]};try{let r=await l(`/api/projects/${i}/insights`,{method:"POST",body:JSON.stringify({type:"Bug",category:t,description:e,file:n})});if(!r.ok){let o=await r.text();return s("report_bug failed:",{status:r.status,error:o}),{content:[{type:"text",text:`\u26A0\uFE0F Bug noted locally (API error): ${t}`}]}}return s("report_bug succeeded:",{category:t}),{content:[{type:"text",text:`\u{1F41B} Bug reported: ${t}`}]}}catch(r){return s("report_bug error:",r instanceof Error?r.message:String(r)),{content:[{type:"text",text:`\u26A0\uFE0F Bug noted locally (error): ${t}`}]}}});p.tool("report_debug_insight",`\u{1F6D1} DEBUG MODE TOOL: Report unexpected behavior and signal that you're stopping.
|
|
61
61
|
|
|
62
62
|
Use this when DEBUG MODE is active and you encounter:
|
|
63
63
|
- Scaffold failures or unexpected output
|
|
@@ -66,97 +66,97 @@ Use this when DEBUG MODE is active and you encounter:
|
|
|
66
66
|
- Anything that deviates from expected flow
|
|
67
67
|
- Errors you don't understand
|
|
68
68
|
|
|
69
|
-
After calling this, you MUST stop and wait for user feedback.`,{category:
|
|
69
|
+
After calling this, you MUST stop and wait for user feedback.`,{category:d.enum(["scaffold_failure","build_error","unexpected_behavior","weird_error"]).describe("Category of the debug issue"),description:d.string().describe("What went wrong - be specific"),context:d.string().optional().describe("What you were trying to do when this happened"),errorOutput:d.string().optional().describe("The actual error message or output")},async({category:t,description:e,context:n,errorOutput:r})=>{if(s("report_debug_insight called",{category:t,projectId:i||"(not set)"}),!i)return s("report_debug_insight failed: PROJECT_ID not set"),{content:[{type:"text",text:"\u274C PROJECT_ID not set"}]};try{let o=[e,n?`
|
|
70
70
|
|
|
71
|
-
**Context:** ${
|
|
71
|
+
**Context:** ${n}`:"",r?`
|
|
72
72
|
|
|
73
73
|
**Error Output:**
|
|
74
74
|
\`\`\`
|
|
75
|
-
${
|
|
76
|
-
\`\`\``:""].join(""),c=await
|
|
77
|
-
|
|
78
|
-
You MUST now stop and say: "\u{1F6D1} DEBUG: I've hit an issue and reported it. Please review and tell me how to proceed."`}]}}catch(o){return
|
|
79
|
-
Use get_column_cards to fetch cards for a specific column.`,{},async()=>{if(
|
|
80
|
-
|
|
81
|
-
`;for(let o of
|
|
82
|
-
`;return
|
|
83
|
-
Use get_card_details for full card info including description and subtasks.`,{column:
|
|
84
|
-
|
|
85
|
-
`;for(let
|
|
86
|
-
id: ${
|
|
87
|
-
`}return c+="\nUse `get_card_details` with card ID for full info.",
|
|
88
|
-
`;if(
|
|
89
|
-
`,
|
|
90
|
-
`,o.dueDate&&(
|
|
91
|
-
`),
|
|
92
|
-
|
|
93
|
-
`,o.description&&(
|
|
94
|
-
${
|
|
95
|
-
|
|
96
|
-
`),o.subtasks&&o.subtasks.length>0){
|
|
97
|
-
`;for(let
|
|
98
|
-
`}}return
|
|
75
|
+
${r}
|
|
76
|
+
\`\`\``:""].join(""),c=await l(`/api/projects/${i}/insights`,{method:"POST",body:JSON.stringify({type:"DebugInsight",category:t,description:o})});if(!c.ok){let a=await c.text();return s("report_debug_insight failed:",{status:c.status,error:a}),{content:[{type:"text",text:"\u{1F6D1} Debug insight recorded locally (API error). STOP and wait for user."}]}}return s("report_debug_insight succeeded:",{category:t}),{content:[{type:"text",text:`\u{1F6D1} Debug insight reported: ${t}
|
|
77
|
+
|
|
78
|
+
You MUST now stop and say: "\u{1F6D1} DEBUG: I've hit an issue and reported it. Please review and tell me how to proceed."`}]}}catch(o){return s("report_debug_insight error:",o instanceof Error?o.message:String(o)),{content:[{type:"text",text:"\u{1F6D1} Debug insight recorded locally (error). STOP and wait for user."}]}}});p.tool("get_kanban_board",`Get kanban board overview - columns with card counts only.
|
|
79
|
+
Use get_column_cards to fetch cards for a specific column.`,{},async()=>{if(s("get_kanban_board called",{projectId:i||"(not set)"}),!i)return{content:[{type:"text",text:"\u274C PROJECT_ID not set"}]};try{let t=await l(`/api/kanban-boards/by-project/${i}/full`);if(!t.ok)return t.status===404?{content:[{type:"text",text:"\u{1F4CB} No kanban board found for this project."}]}:{content:[{type:"text",text:`\u274C Failed to get board: ${await t.text()}`}]};let e=await t.json(),n=$(e),r=`\u{1F4CB} **${n.title}**
|
|
80
|
+
|
|
81
|
+
`;for(let o of n.columns)r+=`- **${o.title}**: ${o.cards.length} cards (id: ${o.id})
|
|
82
|
+
`;return r+="\nUse `get_column_cards` with column name to see cards.",s("get_kanban_board succeeded"),{content:[{type:"text",text:r}]}}catch(t){return s("get_kanban_board error:",t instanceof Error?t.message:String(t)),{content:[{type:"text",text:`\u274C Error: ${t instanceof Error?t.message:String(t)}`}]}}});p.tool("get_column_cards",`Get cards in a specific column. Returns card titles, priorities, and IDs.
|
|
83
|
+
Use get_card_details for full card info including description and subtasks.`,{column:d.string().describe("Column name (e.g., 'Todo', 'In Progress', 'Done')")},async({column:t})=>{if(s("get_column_cards called",{column:t,projectId:i||"(not set)"}),!i)return{content:[{type:"text",text:"\u274C PROJECT_ID not set"}]};try{let e=await l(`/api/kanban-boards/by-project/${i}/full`);if(!e.ok)return{content:[{type:"text",text:"\u274C Failed to get board"}]};let n=await e.json(),r=$(n),o=r.columns.find(a=>a.title.toLowerCase()===t.toLowerCase());if(!o){let a=r.columns.map(u=>u.title).join(", ");return{content:[{type:"text",text:`\u274C Column "${t}" not found. Available: ${a}`}]}}if(o.cards.length===0)return{content:[{type:"text",text:`\u{1F4CB} **${o.title}**: No cards`}]};let c=`\u{1F4CB} **${o.title}** (${o.cards.length} cards)
|
|
84
|
+
|
|
85
|
+
`;for(let a of o.cards){let u=a.priority!=="Medium"?` [${a.priority}]`:"",f=a.subtasks?.length||0,m=f>0?` (${f} subtasks)`:"";c+=`- **${h(a.title)}**${u}${m}
|
|
86
|
+
id: ${a.id}
|
|
87
|
+
`}return c+="\nUse `get_card_details` with card ID for full info.",s("get_column_cards succeeded",{column:o.title,cardCount:o.cards.length}),{content:[{type:"text",text:c}]}}catch(e){return s("get_column_cards error:",e instanceof Error?e.message:String(e)),{content:[{type:"text",text:`\u274C Error: ${e instanceof Error?e.message:String(e)}`}]}}});p.tool("get_card_details","Get full details of a specific card including description and subtasks.",{cardId:d.string().describe("Card ID (GUID)")},async({cardId:t})=>{if(s("get_card_details called",{cardId:t,projectId:i||"(not set)"}),!i)return{content:[{type:"text",text:"\u274C PROJECT_ID not set"}]};try{let e=await l(`/api/kanban-boards/by-project/${i}/full`);if(!e.ok)return{content:[{type:"text",text:"\u274C Failed to get board"}]};let n=await e.json(),r=$(n),o=null,c="";for(let u of r.columns){let f=u.cards.find(m=>m.id===t);if(f){o=f,c=u.title;break}}if(!o)return{content:[{type:"text",text:`\u274C Card not found: ${t}`}]};let a=`\u{1F4CB} **${h(o.title)}**
|
|
88
|
+
`;if(a+=`Column: ${c}
|
|
89
|
+
`,a+=`Priority: ${o.priority}
|
|
90
|
+
`,o.dueDate&&(a+=`Due: ${new Date(o.dueDate).toLocaleDateString()}
|
|
91
|
+
`),a+=`ID: ${o.id}
|
|
92
|
+
|
|
93
|
+
`,o.description&&(a+=`**Description:**
|
|
94
|
+
${h(o.description)}
|
|
95
|
+
|
|
96
|
+
`),o.subtasks&&o.subtasks.length>0){a+=`**Subtasks:**
|
|
97
|
+
`;for(let u of o.subtasks){let f=u.isCompleted?"\u2705":"\u2B1C";a+=`${f} ${h(u.title)} (id: ${u.id})
|
|
98
|
+
`}}return s("get_card_details succeeded",{cardId:t}),{content:[{type:"text",text:a}]}}catch(e){return s("get_card_details error:",e instanceof Error?e.message:String(e)),{content:[{type:"text",text:`\u274C Error: ${e instanceof Error?e.message:String(e)}`}]}}});p.tool("create_kanban_card",`Create a new card on the kanban board.
|
|
99
99
|
Column names are typically: Backlog, Todo, In Progress, Done
|
|
100
|
-
The tool will match column names case-insensitively.`,{title:
|
|
101
|
-
Use get_kanban_board first to get the card ID.`,{cardId:
|
|
100
|
+
The tool will match column names case-insensitively.`,{title:d.string().describe("Card title (max 200 chars)"),column:d.string().describe("Column name (e.g., 'Backlog', 'Todo', 'In Progress', 'Done')"),description:d.string().optional().describe("Card description (max 2000 chars)"),priority:d.enum(["Low","Medium","High","Urgent"]).optional().describe("Priority level (default: Medium)"),dueDate:d.string().optional().describe("Due date in ISO format (e.g., '2024-12-31')")},async({title:t,column:e,description:n,priority:r,dueDate:o})=>{if(s("create_kanban_card called",{title:t,column:e,projectId:i||"(not set)"}),!i)return{content:[{type:"text",text:"\u274C PROJECT_ID not set"}]};try{let c=await l(`/api/projects/${i}/kanban/cards`,{method:"POST",body:JSON.stringify({title:t,column:e,description:n,priority:r||"Medium",dueDate:o?new Date(o).toISOString():void 0})}),a=await c.json();return!c.ok||!a.success?(s("create_kanban_card failed:",{result:a}),{content:[{type:"text",text:`\u274C ${a.error||"Failed to create card"}`}]}):(s("create_kanban_card succeeded:",{id:a.card.id}),{content:[{type:"text",text:`\u2705 Created card "${a.card.title}" in ${a.card.columnName} (id: ${a.card.id})`}]})}catch(c){return s("create_kanban_card error:",c instanceof Error?c.message:String(c)),{content:[{type:"text",text:`\u274C Error: ${c instanceof Error?c.message:String(c)}`}]}}});p.tool("update_kanban_card",`Update an existing kanban card's properties.
|
|
101
|
+
Use get_kanban_board first to get the card ID.`,{cardId:d.string().describe("Card ID (GUID from get_kanban_board)"),title:d.string().optional().describe("New title"),description:d.string().optional().describe("New description"),priority:d.enum(["Low","Medium","High","Urgent"]).optional().describe("New priority"),dueDate:d.string().optional().describe("New due date in ISO format")},async({cardId:t,title:e,description:n,priority:r,dueDate:o})=>{if(s("update_kanban_card called",{cardId:t,projectId:i||"(not set)"}),!i)return{content:[{type:"text",text:"\u274C PROJECT_ID not set"}]};try{let c=await l(`/api/projects/${i}/kanban/cards/${t}`,{method:"PUT",body:JSON.stringify({title:e,description:n,priority:r,dueDate:o?new Date(o).toISOString():void 0})}),a=await c.json();return!c.ok||!a.success?(s("update_kanban_card failed:",{result:a}),{content:[{type:"text",text:`\u274C ${a.error||"Failed to update card"}`}]}):(s("update_kanban_card succeeded:",{id:a.card.id}),{content:[{type:"text",text:`\u2705 Updated card "${a.card.title}"`}]})}catch(c){return s("update_kanban_card error:",c instanceof Error?c.message:String(c)),{content:[{type:"text",text:`\u274C Error: ${c instanceof Error?c.message:String(c)}`}]}}});p.tool("move_kanban_card",`Move a card to a different column.
|
|
102
102
|
Use get_kanban_board first to get the card ID.
|
|
103
|
-
Column names (EXACT, case-sensitive): "Backlog", "Todo", "In Progress", "Done"`,{cardId:
|
|
104
|
-
Use get_kanban_board first to get the card ID.`,{cardId:
|
|
105
|
-
Use get_kanban_board first to get the card ID.`,{cardId:
|
|
106
|
-
Use get_kanban_board first to get the subtask ID.`,{subtaskId:
|
|
107
|
-
Use get_kanban_board first to get the subtask ID.`,{subtaskId:
|
|
103
|
+
Column names (EXACT, case-sensitive): "Backlog", "Todo", "In Progress", "Done"`,{cardId:d.string().describe("Card ID (GUID from get_kanban_board)"),column:d.string().describe("Target column name"),position:d.number().optional().describe("Position in column (0 = top, omit for bottom)")},async({cardId:t,column:e,position:n})=>{if(s("move_kanban_card called",{cardId:t,column:e,projectId:i||"(not set)"}),!i)return{content:[{type:"text",text:"\u274C PROJECT_ID not set"}]};try{let r=await l(`/api/projects/${i}/kanban/cards/${t}/move`,{method:"PUT",body:JSON.stringify({column:e,position:n})}),o=await r.json();return!r.ok||!o.success?(s("move_kanban_card failed:",{result:o}),{content:[{type:"text",text:`\u274C ${o.error||"Failed to move card"}`}]}):(s("move_kanban_card succeeded:",{id:o.card.id,column:o.card.columnName}),{content:[{type:"text",text:`\u2705 Moved card "${o.card.title}" to ${o.card.columnName}`}]})}catch(r){return s("move_kanban_card error:",r instanceof Error?r.message:String(r)),{content:[{type:"text",text:`\u274C Error: ${r instanceof Error?r.message:String(r)}`}]}}});p.tool("delete_kanban_card",`Delete a kanban card.
|
|
104
|
+
Use get_kanban_board first to get the card ID.`,{cardId:d.string().describe("Card ID (GUID from get_kanban_board)")},async({cardId:t})=>{if(s("delete_kanban_card called",{cardId:t,projectId:i||"(not set)"}),!i)return{content:[{type:"text",text:"\u274C PROJECT_ID not set"}]};try{let e=await l(`/api/projects/${i}/kanban/cards/${t}`,{method:"DELETE"}),n=await e.json();return!e.ok||!n.success?(s("delete_kanban_card failed:",{result:n}),{content:[{type:"text",text:`\u274C ${n.error||"Failed to delete card"}`}]}):(s("delete_kanban_card succeeded:",{cardId:t}),{content:[{type:"text",text:"\u{1F5D1}\uFE0F Deleted card"}]})}catch(e){return s("delete_kanban_card error:",e instanceof Error?e.message:String(e)),{content:[{type:"text",text:`\u274C Error: ${e instanceof Error?e.message:String(e)}`}]}}});p.tool("create_subtask",`Add a subtask to a kanban card.
|
|
105
|
+
Use get_kanban_board first to get the card ID.`,{cardId:d.string().describe("Parent card ID (GUID from get_kanban_board)"),title:d.string().describe("Subtask title (max 200 chars)")},async({cardId:t,title:e})=>{if(s("create_subtask called",{cardId:t,title:e,projectId:i||"(not set)"}),!i)return{content:[{type:"text",text:"\u274C PROJECT_ID not set"}]};try{let n=await l(`/api/projects/${i}/kanban/cards/${t}/subtasks`,{method:"POST",body:JSON.stringify({title:e})}),r=await n.json();return!n.ok||!r.success?(s("create_subtask failed:",{result:r}),{content:[{type:"text",text:`\u274C ${r.error||"Failed to create subtask"}`}]}):(s("create_subtask succeeded:",{id:r.subtask.id}),{content:[{type:"text",text:`\u2705 Created subtask "${r.subtask.title}" (id: ${r.subtask.id})`}]})}catch(n){return s("create_subtask error:",n instanceof Error?n.message:String(n)),{content:[{type:"text",text:`\u274C Error: ${n instanceof Error?n.message:String(n)}`}]}}});p.tool("toggle_subtask",`Toggle a subtask's completion status.
|
|
106
|
+
Use get_kanban_board first to get the subtask ID.`,{subtaskId:d.string().describe("Subtask ID (GUID from get_kanban_board)")},async({subtaskId:t})=>{if(s("toggle_subtask called",{subtaskId:t,projectId:i||"(not set)"}),!i)return{content:[{type:"text",text:"\u274C PROJECT_ID not set"}]};try{let e=await l(`/api/projects/${i}/kanban/subtasks/${t}/toggle`,{method:"PUT"}),n=await e.json();if(!e.ok||!n.success)return s("toggle_subtask failed:",{result:n}),{content:[{type:"text",text:`\u274C ${n.error||"Failed to toggle subtask"}`}]};let r=n.subtask.isCompleted?"completed \u2705":"uncompleted \u2B1C";return s("toggle_subtask succeeded:",{id:n.subtask.id,isCompleted:n.subtask.isCompleted}),{content:[{type:"text",text:`\u2705 Subtask "${n.subtask.title}" marked as ${r}`}]}}catch(e){return s("toggle_subtask error:",e instanceof Error?e.message:String(e)),{content:[{type:"text",text:`\u274C Error: ${e instanceof Error?e.message:String(e)}`}]}}});p.tool("delete_subtask",`Delete a subtask.
|
|
107
|
+
Use get_kanban_board first to get the subtask ID.`,{subtaskId:d.string().describe("Subtask ID (GUID from get_kanban_board)")},async({subtaskId:t})=>{if(s("delete_subtask called",{subtaskId:t,projectId:i||"(not set)"}),!i)return{content:[{type:"text",text:"\u274C PROJECT_ID not set"}]};try{let e=await l(`/api/projects/${i}/kanban/subtasks/${t}`,{method:"DELETE"}),n=await e.json();return!e.ok||!n.success?(s("delete_subtask failed:",{result:n}),{content:[{type:"text",text:`\u274C ${n.error||"Failed to delete subtask"}`}]}):(s("delete_subtask succeeded:",{subtaskId:t}),{content:[{type:"text",text:"\u{1F5D1}\uFE0F Deleted subtask"}]})}catch(e){return s("delete_subtask error:",e instanceof Error?e.message:String(e)),{content:[{type:"text",text:`\u274C Error: ${e instanceof Error?e.message:String(e)}`}]}}});p.tool("get_test_suites",`Get all test suites for the current project.
|
|
108
108
|
Returns a list of test suites with their names, descriptions, and test counts.
|
|
109
|
-
Use this to find test suites before running them.`,{},async()=>{if(
|
|
109
|
+
Use this to find test suites before running them.`,{},async()=>{if(s("get_test_suites called",{projectId:i||"(not set)"}),!i)return{content:[{type:"text",text:"\u274C PROJECT_ID not set"}]};try{let t=await l(`/api/projects/${i}/tests/suites`),e=await t.json();if(!t.ok||!e.success)return s("get_test_suites failed:",{result:e}),{content:[{type:"text",text:`\u274C ${e.error||"Failed to get test suites"}`}]};let n=e.suites||[];return n.length===0?{content:[{type:"text",text:"\u{1F4CB} No test suites found for this project."}]}:{content:[{type:"text",text:`\u{1F4CB} Test Suites:
|
|
110
110
|
|
|
111
|
-
${
|
|
111
|
+
${n.map(o=>`\u2022 ${o.name} (${o.testCount} tests) - ID: ${o.id}${o.lastRunStatus?` [Last: ${o.lastRunStatus}]`:""}${o.description?`
|
|
112
112
|
${o.description}`:""}`).join(`
|
|
113
|
-
`)}`}]}}catch(
|
|
113
|
+
`)}`}]}}catch(t){return s("get_test_suites error:",t instanceof Error?t.message:String(t)),{content:[{type:"text",text:`\u274C Error: ${t instanceof Error?t.message:String(t)}`}]}}});p.tool("get_test_suite_details",`Get detailed information about a test suite including all its tests.
|
|
114
114
|
Returns the suite with all test definitions ordered by position.
|
|
115
|
-
Use this to get the tests before running a suite.`,{suiteId:
|
|
116
|
-
ID: ${
|
|
117
|
-
Instructions: ${
|
|
115
|
+
Use this to get the tests before running a suite.`,{suiteId:d.string().describe("Test suite ID (GUID)")},async({suiteId:t})=>{if(s("get_test_suite_details called",{suiteId:t,projectId:i||"(not set)"}),!i)return{content:[{type:"text",text:"\u274C PROJECT_ID not set"}]};try{let e=await l(`/api/projects/${i}/tests/suites/${t}`),n=await e.json();if(!e.ok||!n.success)return s("get_test_suite_details failed:",{result:n}),{content:[{type:"text",text:`\u274C ${n.error||"Failed to get test suite"}`}]};let r=n.suite,o=r.tests||[],c=o.map((a,u)=>`${u+1}. ${a.name}${a.lastRunStatus?` [${a.lastRunStatus}]`:""}
|
|
116
|
+
ID: ${a.id}
|
|
117
|
+
Instructions: ${a.instructions.substring(0,200)}${a.instructions.length>200?"...":""}`).join(`
|
|
118
118
|
|
|
119
|
-
`);return{content:[{type:"text",text:`\u{1F4CB} Test Suite: ${
|
|
120
|
-
${
|
|
119
|
+
`);return{content:[{type:"text",text:`\u{1F4CB} Test Suite: ${r.name}
|
|
120
|
+
${r.description?`Description: ${r.description}
|
|
121
121
|
`:""}
|
|
122
122
|
Tests (${o.length}):
|
|
123
123
|
|
|
124
|
-
${c}`}]}}catch(
|
|
125
|
-
Returns the test name and full instructions for the AI to execute.`,{testId:
|
|
126
|
-
Suite: ${
|
|
124
|
+
${c}`}]}}catch(e){return s("get_test_suite_details error:",e instanceof Error?e.message:String(e)),{content:[{type:"text",text:`\u274C Error: ${e instanceof Error?e.message:String(e)}`}]}}});p.tool("get_test_details",`Get detailed information about a single test.
|
|
125
|
+
Returns the test name and full instructions for the AI to execute.`,{testId:d.string().describe("Test ID (GUID)")},async({testId:t})=>{if(s("get_test_details called",{testId:t,projectId:i||"(not set)"}),!i)return{content:[{type:"text",text:"\u274C PROJECT_ID not set"}]};try{let e=await l(`/api/projects/${i}/tests/${t}`),n=await e.json();if(!e.ok||!n.success)return s("get_test_details failed:",{result:n}),{content:[{type:"text",text:`\u274C ${n.error||"Failed to get test"}`}]};let r=n.test;return{content:[{type:"text",text:`\u{1F4DD} Test: ${r.name}
|
|
126
|
+
Suite: ${r.testSuiteName}
|
|
127
127
|
|
|
128
128
|
Instructions:
|
|
129
|
-
${
|
|
129
|
+
${r.instructions}`}]}}catch(e){return s("get_test_details error:",e instanceof Error?e.message:String(e)),{content:[{type:"text",text:`\u274C Error: ${e instanceof Error?e.message:String(e)}`}]}}});p.tool("run_test",`Start a test run for a single test.
|
|
130
130
|
Creates a new test run record and returns the test details for execution.
|
|
131
131
|
After calling this, execute the test using Playwright MCP tools, then call report_test_status to update the result.
|
|
132
132
|
|
|
133
133
|
The test instructions contain human-readable steps that you should follow using
|
|
134
|
-
Playwright MCP tools (browser_navigate, browser_click, browser_type, browser_snapshot, etc.).`,{testId:
|
|
134
|
+
Playwright MCP tools (browser_navigate, browser_click, browser_type, browser_snapshot, etc.).`,{testId:d.string().describe("Test ID to run (GUID)")},async({testId:t})=>{if(s("run_test called",{testId:t,projectId:i||"(not set)"}),!i)return{content:[{type:"text",text:"\u274C PROJECT_ID not set"}]};try{let e=await l(`/api/projects/${i}/tests/runs`,{method:"POST",body:JSON.stringify({testId:t})}),n=await e.json();if(!e.ok||!n.success)return s("run_test failed:",{result:n}),{content:[{type:"text",text:`\u274C ${n.error||"Failed to start test run"}`}]};let r=n.test;return{content:[{type:"text",text:`\u{1F680} Started test run!
|
|
135
135
|
|
|
136
|
-
Run ID: ${
|
|
137
|
-
Test: ${
|
|
136
|
+
Run ID: ${n.runId}
|
|
137
|
+
Test: ${r.name}
|
|
138
138
|
|
|
139
139
|
Instructions to execute:
|
|
140
|
-
${
|
|
140
|
+
${r.instructions}
|
|
141
141
|
|
|
142
|
-
\u26A0\uFE0F Remember to call report_test_status with run ID "${
|
|
142
|
+
\u26A0\uFE0F Remember to call report_test_status with run ID "${n.runId}" when done.`}]}}catch(e){return s("run_test error:",e instanceof Error?e.message:String(e)),{content:[{type:"text",text:`\u274C Error: ${e instanceof Error?e.message:String(e)}`}]}}});p.tool("run_test_suite",`Start a test run for an entire test suite.
|
|
143
143
|
Creates a suite-level test run record and returns all tests to execute in order.
|
|
144
144
|
Execute each test using Playwright MCP tools, call report_test_status for each test result,
|
|
145
|
-
then call report_test_status with the suite summary at the end.`,{suiteId:
|
|
145
|
+
then call report_test_status with the suite summary at the end.`,{suiteId:d.string().describe("Test suite ID to run (GUID)")},async({suiteId:t})=>{if(s("run_test_suite called",{suiteId:t,projectId:i||"(not set)"}),!i)return{content:[{type:"text",text:"\u274C PROJECT_ID not set"}]};try{let e=await l(`/api/projects/${i}/tests/runs`,{method:"POST",body:JSON.stringify({testSuiteId:t})}),n=await e.json();if(!e.ok||!n.success)return s("run_test_suite failed:",{result:n}),{content:[{type:"text",text:`\u274C ${n.error||"Failed to start test suite run"}`}]};let o=(n.tests||[]).map((c,a)=>`${a+1}. ${c.name}
|
|
146
146
|
ID: ${c.id}
|
|
147
147
|
Instructions: ${c.instructions}`).join(`
|
|
148
148
|
|
|
149
149
|
`);return{content:[{type:"text",text:`\u{1F680} Started test suite run!
|
|
150
150
|
|
|
151
|
-
Run ID: ${
|
|
152
|
-
Suite: ${
|
|
153
|
-
Total Tests: ${
|
|
151
|
+
Run ID: ${n.runId}
|
|
152
|
+
Suite: ${n.suiteName}
|
|
153
|
+
Total Tests: ${n.totalTests}
|
|
154
154
|
|
|
155
155
|
Tests to execute:
|
|
156
156
|
|
|
157
157
|
${o}
|
|
158
158
|
|
|
159
|
-
\u26A0\uFE0F Execute each test in order, then call report_test_status with run ID "${
|
|
159
|
+
\u26A0\uFE0F Execute each test in order, then call report_test_status with run ID "${n.runId}" to update progress.`}]}}catch(e){return s("run_test_suite error:",e instanceof Error?e.message:String(e)),{content:[{type:"text",text:`\u274C Error: ${e instanceof Error?e.message:String(e)}`}]}}});p.tool("report_test_status",`Report the status of a test run.
|
|
160
160
|
Call this after executing a test to update its status.
|
|
161
161
|
For suite runs, call with passed/failed counts as you complete each test.
|
|
162
162
|
|
|
@@ -164,15 +164,15 @@ Status values:
|
|
|
164
164
|
- "running": Test is in progress
|
|
165
165
|
- "passed": Test completed successfully
|
|
166
166
|
- "failed": Test failed
|
|
167
|
-
- "cancelled": Test was cancelled`,{runId:
|
|
168
|
-
Completed at: ${
|
|
167
|
+
- "cancelled": Test was cancelled`,{runId:d.string().describe("Test run ID (GUID from run_test or run_test_suite)"),status:d.enum(["running","passed","failed","cancelled"]).describe("New status"),resultSummary:d.string().optional().describe("Summary of test results (what was verified, any issues)"),errorMessage:d.string().optional().describe("Short error message if test failed"),errorDetails:d.string().optional().describe("Full error details/stack trace if failed"),passedTests:d.number().optional().describe("For suite runs: count of passed tests so far"),failedTests:d.number().optional().describe("For suite runs: count of failed tests so far")},async({runId:t,status:e,resultSummary:n,errorMessage:r,errorDetails:o,passedTests:c,failedTests:a})=>{if(s("report_test_status called",{runId:t,status:e,projectId:i||"(not set)"}),!i)return{content:[{type:"text",text:"\u274C PROJECT_ID not set"}]};try{let u=await l(`/api/projects/${i}/tests/runs/${t}`,{method:"PUT",body:JSON.stringify({status:e,resultSummary:n,errorMessage:r,errorDetails:o,passedTests:c,failedTests:a})}),f=await u.json();return!u.ok||!f.success?(s("report_test_status failed:",{result:f}),{content:[{type:"text",text:`\u274C ${f.error||"Failed to update test status"}`}]}):{content:[{type:"text",text:`${e==="passed"?"\u2705":e==="failed"?"\u274C":e==="cancelled"?"\u{1F6AB}":"\u{1F504}"} Test run updated: ${e.toUpperCase()}${f.endedAt?`
|
|
168
|
+
Completed at: ${f.endedAt}`:""}`}]}}catch(u){return s("report_test_status error:",u instanceof Error?u.message:String(u)),{content:[{type:"text",text:`\u274C Error: ${u instanceof Error?u.message:String(u)}`}]}}});p.tool("create_test_suite",`Create a new test suite for organizing tests.
|
|
169
169
|
A test suite is a logical grouping of related tests (e.g., "Login Tests", "Checkout Flow").
|
|
170
170
|
|
|
171
|
-
Use this to create a container for related tests before adding individual tests to it.`,{name:
|
|
171
|
+
Use this to create a container for related tests before adding individual tests to it.`,{name:d.string().describe("Name of the test suite (e.g., 'Login Tests', 'Checkout Flow')"),description:d.string().optional().describe("Optional description of what this suite tests")},async({name:t,description:e})=>{if(s("create_test_suite called",{name:t,projectId:i||"(not set)"}),!i)return{content:[{type:"text",text:"\u274C PROJECT_ID not set"}]};try{let n=await l(`/api/projects/${i}/tests/suites`,{method:"POST",body:JSON.stringify({name:t,description:e})}),r=await n.json();return!n.ok||!r.success?(s("create_test_suite failed:",{result:r}),{content:[{type:"text",text:`\u274C ${r.error||"Failed to create test suite"}`}]}):{content:[{type:"text",text:`\u2705 Created test suite "${r.name}"
|
|
172
172
|
|
|
173
|
-
Suite ID: ${
|
|
173
|
+
Suite ID: ${r.suiteId}
|
|
174
174
|
|
|
175
|
-
You can now add tests to this suite using create_test.`}]}}catch(
|
|
175
|
+
You can now add tests to this suite using create_test.`}]}}catch(n){return s("create_test_suite error:",n instanceof Error?n.message:String(n)),{content:[{type:"text",text:`\u274C Error: ${n instanceof Error?n.message:String(n)}`}]}}});p.tool("create_test",`Create a new test within a test suite.
|
|
176
176
|
The test should have clear, step-by-step instructions that can be executed using Playwright.
|
|
177
177
|
|
|
178
178
|
Write instructions that are:
|
|
@@ -186,19 +186,19 @@ Example instructions:
|
|
|
186
186
|
3. Enter "password123" in the password field
|
|
187
187
|
4. Click the "Sign In" button
|
|
188
188
|
5. Verify the URL changes to /dashboard
|
|
189
|
-
6. Verify a welcome message is displayed`,{suiteId:
|
|
189
|
+
6. Verify a welcome message is displayed`,{suiteId:d.string().describe("Test suite ID to add the test to (GUID)"),name:d.string().describe("Name of the test (e.g., 'Login with valid credentials')"),instructions:d.string().describe("Step-by-step instructions for the AI to execute this test using Playwright")},async({suiteId:t,name:e,instructions:n})=>{if(s("create_test called",{suiteId:t,name:e,projectId:i||"(not set)"}),!i)return{content:[{type:"text",text:"\u274C PROJECT_ID not set"}]};try{let r=await l(`/api/projects/${i}/tests/suites/${t}/tests`,{method:"POST",body:JSON.stringify({name:e,instructions:n})}),o=await r.json();return!r.ok||!o.success?(s("create_test failed:",{result:o}),{content:[{type:"text",text:`\u274C ${o.error||"Failed to create test"}`}]}):{content:[{type:"text",text:`\u2705 Created test "${o.name}" in suite "${o.suiteName}"
|
|
190
190
|
|
|
191
191
|
Test ID: ${o.testId}
|
|
192
192
|
|
|
193
|
-
You can run this test using run_test with the test ID.`}]}}catch(
|
|
193
|
+
You can run this test using run_test with the test ID.`}]}}catch(r){return s("create_test error:",r instanceof Error?r.message:String(r)),{content:[{type:"text",text:`\u274C Error: ${r instanceof Error?r.message:String(r)}`}]}}});p.tool("update_test",`Update an existing test's name or instructions.
|
|
194
194
|
Use this to fix test instructions that are outdated or incorrect.
|
|
195
195
|
|
|
196
196
|
Common reasons to update:
|
|
197
197
|
- Selector changed (element moved or renamed)
|
|
198
198
|
- Flow changed (new steps added, steps removed)
|
|
199
|
-
- Clarify ambiguous instructions`,{testId:
|
|
199
|
+
- Clarify ambiguous instructions`,{testId:d.string().describe("Test ID to update (GUID)"),name:d.string().optional().describe("New name for the test (optional)"),instructions:d.string().optional().describe("New instructions for the test (optional)")},async({testId:t,name:e,instructions:n})=>{if(s("update_test called",{testId:t,projectId:i||"(not set)"}),!i)return{content:[{type:"text",text:"\u274C PROJECT_ID not set"}]};if(!e&&!n)return{content:[{type:"text",text:"\u274C Provide at least name or instructions to update"}]};try{let r=await l(`/api/projects/${i}/tests/${t}`,{method:"PUT",body:JSON.stringify({name:e,instructions:n})}),o=await r.json();return!r.ok||!o.success?(s("update_test failed:",{result:o}),{content:[{type:"text",text:`\u274C ${o.error||"Failed to update test"}`}]}):{content:[{type:"text",text:`\u2705 Updated test "${o.name}"
|
|
200
200
|
|
|
201
|
-
Test ID: ${o.testId}`}]}}catch(
|
|
201
|
+
Test ID: ${o.testId}`}]}}catch(r){return s("update_test error:",r instanceof Error?r.message:String(r)),{content:[{type:"text",text:`\u274C Error: ${r instanceof Error?r.message:String(r)}`}]}}});p.tool("create_command",`Create a reusable command/prompt that can be saved to the user's command library.
|
|
202
202
|
Commands are reusable prompts or instructions that users can quickly insert into chat.
|
|
203
203
|
|
|
204
204
|
Use this when the user asks you to:
|
|
@@ -206,13 +206,13 @@ Use this when the user asks you to:
|
|
|
206
206
|
- Save a prompt as a command
|
|
207
207
|
- Add a reusable instruction to their library
|
|
208
208
|
|
|
209
|
-
The command will be saved to the user's personal command library.`,{name:
|
|
209
|
+
The command will be saved to the user's personal command library.`,{name:d.string().describe("Command name (short, descriptive)"),content:d.string().describe("The actual prompt/instruction content"),description:d.string().optional().describe("Brief description of what the command does"),category:d.string().optional().describe("Category (e.g., 'Code Review', 'Refactoring', 'Testing')"),tags:d.string().optional().describe("Comma-separated tags for searchability")},async({name:t,content:e,description:n,category:r,tags:o})=>{s("create_command called",{name:t,projectId:i||"(not set)"});try{let c=await l("/api/commands",{method:"POST",body:JSON.stringify({name:t,content:e,description:n,category:r,tags:o})});if(!c.ok){let u=await c.text();return s("create_command failed:",{status:c.status,error:u}),{content:[{type:"text",text:`\u274C Failed to create command: ${u}`}]}}let a=await c.json();return s("create_command succeeded:",{id:a.id,name:a.name}),{content:[{type:"text",text:`\u2705 Created command "${a.name}" (id: ${a.id})
|
|
210
210
|
|
|
211
|
-
You can find it in the Commands section or use "/" in chat to access it.`}]}}catch(c){return
|
|
211
|
+
You can find it in the Commands section or use "/" in chat to access it.`}]}}catch(c){return s("create_command error:",c instanceof Error?c.message:String(c)),{content:[{type:"text",text:`\u274C Error: ${c instanceof Error?c.message:String(c)}`}]}}});p.tool("get_project_prompt",`Get the current project prompt/description that defines this project's context.
|
|
212
212
|
Returns the project-specific prompt that gets injected into your system prompt.
|
|
213
|
-
This describes what the project is about, its tech stack, patterns, and key rules.`,{},async()=>{if(
|
|
213
|
+
This describes what the project is about, its tech stack, patterns, and key rules.`,{},async()=>{if(s("get_project_prompt called",{projectId:i||"(not set)"}),!i)return{content:[{type:"text",text:"\u274C PROJECT_ID not set"}]};try{let t=await l(`/api/projects/${i}/prompt`,{method:"GET"}),e=await t.json();return t.ok?(s("get_project_prompt succeeded:",{hasPrompt:!!e.prompt}),e.prompt?{content:[{type:"text",text:`\u{1F4CB} **Project Prompt:**
|
|
214
214
|
|
|
215
|
-
${
|
|
215
|
+
${h(e.prompt)}`}]}:{content:[{type:"text",text:"\u{1F4DD} No project prompt set. Use update_project_prompt to set one."}]}):(s("get_project_prompt failed:",{result:e}),{content:[{type:"text",text:`\u274C ${e.error||"Failed to get project prompt"}`}]})}catch(t){return s("get_project_prompt error:",t instanceof Error?t.message:String(t)),{content:[{type:"text",text:`\u274C Error: ${t instanceof Error?t.message:String(t)}`}]}}});p.tool("update_project_prompt",`Update the project prompt/description. Use this to define or refine the project context.
|
|
216
216
|
The prompt should describe:
|
|
217
217
|
- What the project is about (overview)
|
|
218
218
|
- Technology stack
|
|
@@ -221,4 +221,52 @@ The prompt should describe:
|
|
|
221
221
|
- Critical rules and gotchas
|
|
222
222
|
|
|
223
223
|
This prompt will be injected into your system prompt for all future conversations.
|
|
224
|
-
Changes are synced in real-time via SignalR.`,{prompt:
|
|
224
|
+
Changes are synced in real-time via SignalR.`,{prompt:d.string().describe("The new project prompt content. Use XML-style sections for organization.")},async({prompt:t})=>{if(s("update_project_prompt called",{projectId:i||"(not set)",promptLength:t.length}),!i)return{content:[{type:"text",text:"\u274C PROJECT_ID not set"}]};try{let e=await l(`/api/projects/${i}/prompt`,{method:"PUT",body:JSON.stringify({prompt:t})}),n=await e.json();return e.ok?(s("update_project_prompt succeeded:",{updatedAt:n.updatedAt}),{content:[{type:"text",text:`\u2705 Project prompt updated (${t.length} chars). It will be used in your next conversation.`}]}):(s("update_project_prompt failed:",{result:n}),{content:[{type:"text",text:`\u274C ${n.error||"Failed to update project prompt"}`}]})}catch(e){return s("update_project_prompt error:",e instanceof Error?e.message:String(e)),{content:[{type:"text",text:`\u274C Error: ${e instanceof Error?e.message:String(e)}`}]}}});p.tool("reference_projects_list",`List all imported reference projects for the current project.
|
|
225
|
+
Reference projects are customer prototypes (Lovable, Bolt, etc.) imported via zip upload.
|
|
226
|
+
Use this to discover available reference projects before exploring their contents.`,{},async()=>{if(s("reference_projects_list called",{projectId:i||"(not set)"}),!i)return{content:[{type:"text",text:"\u274C PROJECT_ID not set"}]};try{let t=await l(`/api/projects/${i}/reference-projects`);if(!t.ok){let r=await t.text();return s("reference_projects_list failed:",{status:t.status,error:r}),{content:[{type:"text",text:`\u274C Failed to list reference projects: ${r}`}]}}let e=await t.json();if(!e||e.length===0)return{content:[{type:"text",text:"\u{1F4C1} No reference projects found. Upload a zip via the UI first."}]};let n=`\u{1F4C1} **Reference Projects:**
|
|
227
|
+
|
|
228
|
+
`;for(let r of e)n+=`\u2022 **${h(r.name)}**
|
|
229
|
+
`,n+=` ID: ${r.id}
|
|
230
|
+
`,n+=` Status: ${r.status}
|
|
231
|
+
`,n+=` Files: ${r.fileCount}
|
|
232
|
+
`,r.techStack&&(n+=` Tech: ${r.techStack}
|
|
233
|
+
`),r.specificationSlug&&(n+=` Spec: ${r.specificationSlug}
|
|
234
|
+
`),r.notes&&(n+=` Notes: ${h(r.notes).substring(0,100)}${r.notes.length>100?"...":""}
|
|
235
|
+
`),n+=`
|
|
236
|
+
`;return n+="Use `reference_project_tree` with an ID to explore file structure.",s("reference_projects_list succeeded",{count:e.length}),{content:[{type:"text",text:n}]}}catch(t){return s("reference_projects_list error:",t instanceof Error?t.message:String(t)),{content:[{type:"text",text:`\u274C Error: ${t instanceof Error?t.message:String(t)}`}]}}});p.tool("reference_project_tree",`Get the file tree structure of a reference project.
|
|
237
|
+
Returns a hierarchical view of all files and directories.
|
|
238
|
+
Use this to understand the project structure before reading specific files.`,{referenceProjectId:d.string().describe("Reference project ID (GUID)")},async({referenceProjectId:t})=>{if(s("reference_project_tree called",{referenceProjectId:t,projectId:i||"(not set)"}),!i)return{content:[{type:"text",text:"\u274C PROJECT_ID not set"}]};try{let c=function(f,m=""){let x="",y=f.type==="directory"?"\u{1F4C1}":"\u{1F4C4}",S=f.size?` (${a(f.size)})`:"";if(x+=`${m}${y} ${f.name}${S}
|
|
239
|
+
`,f.children&&f.children.length>0)for(let w of f.children)x+=c(w,m+" ");return x},a=function(f){return f<1024?`${f}B`:f<1024*1024?`${(f/1024).toFixed(1)}KB`:`${(f/1024/1024).toFixed(1)}MB`};var e=c,n=a;let r=await l(`/api/projects/${i}/reference-projects/${t}/tree`);if(!r.ok){if(r.status===404)return{content:[{type:"text",text:"\u274C Reference project not found"}]};let f=await r.text();return s("reference_project_tree failed:",{status:r.status,error:f}),{content:[{type:"text",text:`\u274C Failed to get file tree: ${f}`}]}}let o=await r.json(),u=c(o.root);return s("reference_project_tree succeeded",{referenceProjectId:t}),{content:[{type:"text",text:`\u{1F4C1} **File Tree:**
|
|
240
|
+
|
|
241
|
+
${u}
|
|
242
|
+
Use \`reference_project_read_file\` with a path to read file contents.`}]}}catch(r){return s("reference_project_tree error:",r instanceof Error?r.message:String(r)),{content:[{type:"text",text:`\u274C Error: ${r instanceof Error?r.message:String(r)}`}]}}});p.tool("reference_project_read_file",`Read the content of a specific file from a reference project.
|
|
243
|
+
Use reference_project_tree first to find the file path.
|
|
244
|
+
Binary files will show a placeholder instead of content.
|
|
245
|
+
Maximum file size: 1MB.`,{referenceProjectId:d.string().describe("Reference project ID (GUID)"),path:d.string().describe("Relative file path within the project (e.g., 'src/App.tsx', 'package.json')")},async({referenceProjectId:t,path:e})=>{if(s("reference_project_read_file called",{referenceProjectId:t,filePath:e,projectId:i||"(not set)"}),!i)return{content:[{type:"text",text:"\u274C PROJECT_ID not set"}]};try{let c=function(a){return a<1024?`${a}B`:a<1024*1024?`${(a/1024).toFixed(1)}KB`:`${(a/1024/1024).toFixed(1)}MB`};var n=c;let r=await l(`/api/projects/${i}/reference-projects/${t}/file?path=${encodeURIComponent(e)}`);if(!r.ok){if(r.status===404)return{content:[{type:"text",text:`\u274C File not found: ${e}`}]};if(r.status===400)return{content:[{type:"text",text:`\u274C ${(await r.json()).error||"Invalid file path"}`}]};let a=await r.text();return s("reference_project_read_file failed:",{status:r.status,error:a}),{content:[{type:"text",text:`\u274C Failed to read file: ${a}`}]}}let o=await r.json();return o.isBinary?{content:[{type:"text",text:`\u{1F4C4} **${e}** (binary file, ${c(o.size)})
|
|
246
|
+
|
|
247
|
+
[Binary content not displayed]`}]}:(s("reference_project_read_file succeeded",{referenceProjectId:t,filePath:e}),{content:[{type:"text",text:`\u{1F4C4} **${e}** (${c(o.size)})
|
|
248
|
+
|
|
249
|
+
\`\`\`
|
|
250
|
+
${h(o.content)}
|
|
251
|
+
\`\`\``}]})}catch(r){return s("reference_project_read_file error:",r instanceof Error?r.message:String(r)),{content:[{type:"text",text:`\u274C Error: ${r instanceof Error?r.message:String(r)}`}]}}});p.tool("reference_project_update_status",`Update a reference project's status and/or specification slug.
|
|
252
|
+
Use this after completing analysis to mark progress.
|
|
253
|
+
|
|
254
|
+
Status values:
|
|
255
|
+
- Imported: Initial state after upload
|
|
256
|
+
- Analyzing: Analysis in progress
|
|
257
|
+
- Analyzed: Analysis complete, spec saved
|
|
258
|
+
- Migrating: Migration in progress
|
|
259
|
+
- Done: All work complete
|
|
260
|
+
|
|
261
|
+
The specificationSlug links to a saved specification (use save_specification first).`,{referenceProjectId:d.string().describe("Reference project ID (GUID)"),status:d.enum(["Imported","Analyzing","Analyzed","Migrating","Done"]).optional().describe("New status"),specificationSlug:d.string().optional().describe("Slug of the linked specification (e.g., 'ref-lovable-prototype')"),notes:d.string().optional().describe("Optional notes about the project")},async({referenceProjectId:t,status:e,specificationSlug:n,notes:r})=>{if(s("reference_project_update_status called",{referenceProjectId:t,status:e,specificationSlug:n,projectId:i||"(not set)"}),!i)return{content:[{type:"text",text:"\u274C PROJECT_ID not set"}]};if(!e&&!n&&r===void 0)return{content:[{type:"text",text:"\u274C Provide at least status, specificationSlug, or notes to update"}]};try{let o={};e&&(o.status=e),n&&(o.specificationSlug=n),r!==void 0&&(o.notes=r);let c=await l(`/api/projects/${i}/reference-projects/${t}`,{method:"PATCH",body:JSON.stringify(o)});if(!c.ok){if(c.status===404)return{content:[{type:"text",text:"\u274C Reference project not found"}]};let u=await c.text();return s("reference_project_update_status failed:",{status:c.status,error:u}),{content:[{type:"text",text:`\u274C Failed to update: ${u}`}]}}let a=await c.json();return s("reference_project_update_status succeeded",{referenceProjectId:t,status:a.status}),{content:[{type:"text",text:`\u2705 Updated reference project "${a.name}"
|
|
262
|
+
|
|
263
|
+
Status: ${a.status}${a.specificationSlug?`
|
|
264
|
+
Spec: ${a.specificationSlug}`:""}`}]}}catch(o){return s("reference_project_update_status error:",o instanceof Error?o.message:String(o)),{content:[{type:"text",text:`\u274C Error: ${o instanceof Error?o.message:String(o)}`}]}}});p.tool("reference_project_download",`Download a reference project to the local filesystem for exploration.
|
|
265
|
+
This downloads and extracts the project files to a local directory.
|
|
266
|
+
After downloading, you can use Read, Glob, Grep tools to explore the codebase freely.
|
|
267
|
+
|
|
268
|
+
Returns the local path where files are extracted.`,{referenceProjectId:d.string().describe("Reference project ID (GUID)"),targetDir:d.string().optional().describe("Optional target directory (defaults to ./ref-projects/{id})")},async({referenceProjectId:t,targetDir:e})=>{if(s("reference_project_download called",{referenceProjectId:t,targetDir:e,projectId:i||"(not set)"}),!i)return{content:[{type:"text",text:"\u274C PROJECT_ID not set"}]};if(!/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(t))return{content:[{type:"text",text:"\u274C Invalid reference project ID format (expected GUID)"}]};try{let r;if(e){if(r=_.resolve(e),r.includes(".."))return{content:[{type:"text",text:"\u274C Invalid target directory (path traversal not allowed)"}]}}else r=_.join(T,"ref-projects",t);g.mkdirSync(r,{recursive:!0});let o=`${E}/api/projects/${i}/reference-projects/${t}/download`,c={};b&&(c["X-Project-Key"]=b),s("reference_project_download fetching",{url:o});let a=await fetch(o,{headers:c});if(!a.ok){if(a.status===404)return{content:[{type:"text",text:"\u274C Reference project not found"}]};let y=await a.text();return s("reference_project_download failed:",{status:a.status,error:y}),{content:[{type:"text",text:`\u274C Failed to download: ${y}`}]}}let u=_.join("/tmp",`ref-project-${t}.zip`),f=await a.arrayBuffer();g.writeFileSync(u,Buffer.from(f)),s("reference_project_download extracting",{zipPath:u,extractPath:r});try{D("unzip",["-o","-q",u,"-d",r],{stdio:"pipe"})}catch(y){try{D("ditto",["-x","-k",u,r],{stdio:"pipe"})}catch{return s("reference_project_download extract failed:",y),{content:[{type:"text",text:"\u274C Failed to extract zip. Make sure 'unzip' is installed."}]}}}try{g.unlinkSync(u)}catch{}let m=0,x=y=>{let S=g.readdirSync(y,{withFileTypes:!0});for(let w of S)w.isDirectory()?x(_.join(y,w.name)):m++};return x(r),s("reference_project_download succeeded",{extractPath:r,fileCount:m}),{content:[{type:"text",text:`\u2705 Downloaded reference project to: ${r}
|
|
269
|
+
|
|
270
|
+
${m} files extracted.
|
|
271
|
+
|
|
272
|
+
You can now use Read, Glob, Grep to explore the codebase freely.`}]}}catch(r){return s("reference_project_download error:",r instanceof Error?r.message:String(r)),{content:[{type:"text",text:`\u274C Error: ${r instanceof Error?r.message:String(r)}`}]}}});async function P(){try{s("Initializing transport...");let t=new O;s("Connecting to transport..."),await p.connect(t),s("Started successfully")}catch(t){s("Fatal error during startup:",t instanceof Error?t.message:String(t),t),process.exit(1)}}P().catch(t=>{s("Unhandled error:",t instanceof Error?t.message:String(t),t),process.exit(1)});
|