diff --git a/.claude-plugin/marketplace.json b/.claude-plugin/marketplace.json index c0623d1..1390ca3 100644 --- a/.claude-plugin/marketplace.json +++ b/.claude-plugin/marketplace.json @@ -8,13 +8,13 @@ { "name": "cuey", "source": "./plugins/cuey", - "description": "Run Cuey's multi-model comparison and return a synthesized answer.", - "version": "0.3.3", + "description": "Run Cuey's multi-model comparison over prompts and attached Excel workbooks.", + "version": "0.4.0", "author": { "name": "TensorBlock" }, "category": "productivity", - "tags": ["comparison", "verification", "multi-model"] + "tags": ["comparison", "verification", "multi-model", "excel", "spreadsheet"] } ] } diff --git a/plugins/cuey/.claude-plugin/plugin.json b/plugins/cuey/.claude-plugin/plugin.json index 32c9f22..afc7637 100644 --- a/plugins/cuey/.claude-plugin/plugin.json +++ b/plugins/cuey/.claude-plugin/plugin.json @@ -1,10 +1,10 @@ { "name": "cuey", - "version": "0.3.3", - "description": "Ask Cuey runs a three-model comparison and returns a synthesized answer with the original model responses available in Cuey.", + "version": "0.4.0", + "description": "Ask Cuey runs a three-model comparison over prompts and attached Excel workbooks, then returns a synthesized answer.", "author": { "name": "TensorBlock", "url": "https://tensorblock.co" }, - "keywords": ["cuey", "comparison", "verification", "multi-model"] + "keywords": ["cuey", "comparison", "verification", "multi-model", "excel", "spreadsheet"] } diff --git a/plugins/cuey/mcp/src/ask-cuey-client.mjs b/plugins/cuey/mcp/src/ask-cuey-client.mjs index 4a67fe0..0fc71c6 100644 --- a/plugins/cuey/mcp/src/ask-cuey-client.mjs +++ b/plugins/cuey/mcp/src/ask-cuey-client.mjs @@ -48,6 +48,7 @@ const DEFAULT_TIMEOUT_MS = 120000; const DEFAULT_SYNTHESIS_TIMEOUT_MS = 90000; const DEFAULT_SYNTHESIS_ATTEMPTS = 2; const MAX_ERROR_MESSAGE_LENGTH = 260; +const MAX_SPREADSHEET_CONTEXT_LENGTH = 180000; function cleanBaseUrl(value) { return String(value || DEFAULT_API_BASE_URL).replace(/\/+$/, ""); @@ -85,6 +86,18 @@ function normalizeReasoningLevel(value) { return normalized === "advanced" ? "advanced" : "standard"; } +function normalizeSpreadsheet(input) { + if (!input || typeof input !== "object") return null; + const filename = String(input.filename || input.fileName || "").trim(); + const documentId = String(input.documentId || input.document_id || "").trim(); + let context = String(input.context || input.workbookContext || input.workbook_context || "").trim(); + if (context.length > MAX_SPREADSHEET_CONTEXT_LENGTH) { + context = `${context.slice(0, MAX_SPREADSHEET_CONTEXT_LENGTH).trimEnd()}\n\n[Workbook context truncated by Cuey]`; + } + if (!filename && !context && !documentId) return null; + return { filename, context, documentId }; +} + function requestHeaders({ token, anonymousId, json = true } = {}) { const headers = {}; if (json) headers["Content-Type"] = "application/json"; @@ -194,9 +207,10 @@ function extractReasoningText(json) { return typeof content === "string" ? content.trim() : ""; } -export function buildAskCueyMessages({ question, context, mode } = {}) { +export function buildAskCueyMessages({ question, context, mode, spreadsheet } = {}) { const q = String(question || "").trim(); const ctx = String(context || "").trim(); + const workbook = normalizeSpreadsheet(spreadsheet); const normalizedMode = String(mode || "ask").trim().toLowerCase() || "ask"; const userParts = []; if (ctx) { @@ -204,6 +218,13 @@ export function buildAskCueyMessages({ question, context, mode } = {}) { userParts.push(ctx); userParts.push(""); } + if (workbook?.context) { + userParts.push("Attached Excel workbook:"); + if (workbook.filename) userParts.push(`Filename: ${workbook.filename}`); + userParts.push("Workbook context:"); + userParts.push(workbook.context); + userParts.push(""); + } userParts.push(`Cuey mode: ${normalizedMode}`); userParts.push(""); userParts.push("Question:"); @@ -223,6 +244,7 @@ export function normalizeCueyRequest(input = {}) { mode: String(input.mode || "ask").trim().toLowerCase() || "ask", question, context: String(input.context || "").trim(), + spreadsheet: normalizeSpreadsheet(input.spreadsheet), models: normalizeModels(input.models, reasoningLevel), reasoningLevel, source: String(input.source || "claude_skill").trim() || "claude_skill", @@ -288,10 +310,22 @@ export function buildSynthesisRequest({ cueyMessageId, candidateResponses, metad } export async function writeLatestAskCueyResult(result, resultPath = LATEST_RESULT_PATH) { + const request = result?.request + ? { + ...result.request, + spreadsheet: result.request.spreadsheet + ? { + filename: result.request.spreadsheet.filename || "", + documentId: result.request.spreadsheet.documentId || "", + hasContext: Boolean(result.request.spreadsheet.context), + } + : null, + } + : null; const payload = { schemaVersion: 1, writtenAt: new Date().toISOString(), - request: result?.request || null, + request, cueyMessageId: result?.cueyMessageId || null, synthesis: result?.synthesis || null, candidates: (result?.candidates || []).map((candidate) => ({ @@ -498,6 +532,15 @@ export async function runAskCuey(input = {}, options = {}) { cuey_source: request.source, cuey_mode: request.mode, reasoning_level: request.reasoningLevel, + ...(request.spreadsheet?.filename + ? { spreadsheet_filename: request.spreadsheet.filename } + : {}), + ...(request.spreadsheet?.context + ? { has_spreadsheet_context: true } + : {}), + ...(request.spreadsheet?.documentId + ? { spreadsheet_document_id: request.spreadsheet.documentId } + : {}), }; const candidateSettled = await Promise.allSettled( diff --git a/plugins/cuey/mcp/src/server.mjs b/plugins/cuey/mcp/src/server.mjs index 88d3415..d48f018 100644 --- a/plugins/cuey/mcp/src/server.mjs +++ b/plugins/cuey/mcp/src/server.mjs @@ -7,12 +7,12 @@ import { } from "./ask-cuey-client.mjs"; const SERVER_NAME = "cuey-claude-mcp"; -const SERVER_VERSION = "0.3.3"; +const SERVER_VERSION = "0.4.0"; const tools = [ { name: "ask_cuey", - description: "Run Ask Cuey multi-model fanout and synthesis. Call this tool whenever the user explicitly asks to use Cuey, asks for a Cuey comparison, or the message instructs you to use the Cuey MCP tool. The text content is the final synthesis: return it verbatim without adding analysis, commentary, or a separate answer.", + description: "Run Ask Cuey multi-model fanout and synthesis, optionally using structured context extracted from an attached Excel workbook. Call this tool whenever the user explicitly asks to use Cuey, asks for a Cuey comparison, or the message instructs you to use the Cuey MCP tool. The text content is the final synthesis: return it verbatim without adding analysis, commentary, or a separate answer.", inputSchema: { type: "object", properties: { @@ -29,6 +29,24 @@ const tools = [ type: "string", description: "Relevant Claude-visible context to include.", }, + spreadsheet: { + type: "object", + description: "Structured context extracted from the Excel workbook attached to the current Claude request.", + properties: { + filename: { + type: "string", + description: "Original .xlsx filename.", + }, + context: { + type: "string", + description: "Workbook sheet names, used ranges, values, and formulas relevant to the question.", + }, + documentId: { + type: "string", + description: "Optional Cuey document ID when the workbook was uploaded through Cuey.", + }, + }, + }, models: { type: "array", items: { type: "string" }, diff --git a/plugins/cuey/mcp/tests/ask-cuey-client.test.mjs b/plugins/cuey/mcp/tests/ask-cuey-client.test.mjs index aad21e2..326f987 100644 --- a/plugins/cuey/mcp/tests/ask-cuey-client.test.mjs +++ b/plugins/cuey/mcp/tests/ask-cuey-client.test.mjs @@ -70,6 +70,39 @@ test("buildAskCueyMessages includes relevant context and question", () => { assert.match(messages[1].content, /Cuey mode: verify/); }); +test("normalizeCueyRequest preserves structured Excel workbook context", () => { + const request = normalizeCueyRequest({ + question: "Summarize this workbook", + spreadsheet: { + filename: "forecast.xlsx", + context: "Sheets: Revenue, Assumptions\nRevenue!A1:B2\nYear | ARR\n2026 | 12000000", + document_id: "doc-123", + }, + }); + + assert.deepEqual(request.spreadsheet, { + filename: "forecast.xlsx", + context: "Sheets: Revenue, Assumptions\nRevenue!A1:B2\nYear | ARR\n2026 | 12000000", + documentId: "doc-123", + }); +}); + +test("buildAskCueyMessages labels attached Excel context separately", () => { + const messages = buildAskCueyMessages({ + question: "What changed year over year?", + mode: "summarize", + spreadsheet: { + filename: "forecast.xlsx", + context: "Sheet Revenue, used range A1:B2\nYear | ARR\n2026 | 12000000", + }, + }); + + assert.match(messages[1].content, /Attached Excel workbook:/); + assert.match(messages[1].content, /Filename: forecast\.xlsx/); + assert.match(messages[1].content, /Sheet Revenue, used range A1:B2/); + assert.match(messages[1].content, /What changed year over year\?/); +}); + test("buildCandidateRequest sends anonymous id for public requests", () => { const request = buildCandidateRequest({ apiBaseUrl: "https://staging-api.cuey.io/api/cuey", @@ -290,3 +323,35 @@ test("writeLatestAskCueyResult stores original model answers for the overlay", a await rm(dir, { recursive: true, force: true }); } }); + +test("writeLatestAskCueyResult records workbook presence without persisting cell context", async () => { + const dir = await mkdtemp(path.join(tmpdir(), "cuey-result-")); + const resultPath = path.join(dir, "latest-ask-cuey-result.json"); + try { + await writeLatestAskCueyResult( + { + request: { + question: "Summarize this workbook", + spreadsheet: { + filename: "forecast.xlsx", + context: "private workbook cells", + documentId: "doc-123", + }, + }, + synthesis: { answer: "Revenue increased." }, + candidates: [], + }, + resultPath, + ); + + const parsed = JSON.parse(await readFile(resultPath, "utf8")); + assert.deepEqual(parsed.request.spreadsheet, { + filename: "forecast.xlsx", + documentId: "doc-123", + hasContext: true, + }); + assert.doesNotMatch(await readFile(resultPath, "utf8"), /private workbook cells/); + } finally { + await rm(dir, { recursive: true, force: true }); + } +}); diff --git a/plugins/cuey/skills/cuey/SKILL.md b/plugins/cuey/skills/cuey/SKILL.md index 85a04b7..b4c13e1 100644 --- a/plugins/cuey/skills/cuey/SKILL.md +++ b/plugins/cuey/skills/cuey/SKILL.md @@ -6,7 +6,17 @@ argument-hint: # Cuey -When invoked, call the local MCP tool `cuey-plugin:ask_cuey` immediately. Do not inspect files, use bash, recall memory, search, or answer directly before calling the tool. +When invoked, call the local MCP tool `cuey-plugin:ask_cuey`. Do not use bash, recall memory, search, or answer directly before calling the tool. + +If the current request includes an Excel `.xlsx` attachment, read that attachment first with Claude's available file or spreadsheet capability. Build compact workbook context containing: + +- workbook filename; +- every sheet name; +- the used range and headers for each relevant sheet; +- cell values and formulas relevant to the user's question; +- any omitted sheets or ranges when the workbook is too large to include fully. + +Do not infer missing cells or formulas. For a small workbook, include all populated cells. For a large workbook, prioritize the sheets and ranges relevant to `$ARGUMENTS` and clearly record the selection in the workbook context. Send this payload: @@ -15,6 +25,10 @@ Send this payload: "mode": "ask | compare | verify | summarize", "question": "$ARGUMENTS", "context": "only relevant prior conversation context", + "spreadsheet": { + "filename": "attached workbook filename, or empty when none", + "context": "structured workbook context extracted from the attached .xlsx, or empty when none" + }, "models": ["grok-4.5-reasoning", "gpt-5.6-sol", "claude-opus-4-8"], "reasoningLevel": "standard", "source": "claude_plugin" @@ -23,6 +37,8 @@ Send this payload: Choose `compare` for comparisons, `verify` for risk or correctness checks, `summarize` for summaries, and `ask` otherwise. +When there is no Excel attachment, omit `spreadsheet`. Never substitute a filename-only description for workbook content. + After a successful call: 1. Return the first text item from the MCP result exactly as the complete answer.