titan-agent 6.4.4 → 6.5.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.
Files changed (55) hide show
  1. package/dist/agent/agentLessons.js.map +1 -1
  2. package/dist/agent/agentLoop.js +5 -1
  3. package/dist/agent/agentLoop.js.map +1 -1
  4. package/dist/agent/orchestrator.js +5 -14
  5. package/dist/agent/orchestrator.js.map +1 -1
  6. package/dist/agent/somaInitiative.js.map +1 -1
  7. package/dist/agent/toolRunner.js +35 -0
  8. package/dist/agent/toolRunner.js.map +1 -1
  9. package/dist/channels/discord.js +10 -6
  10. package/dist/channels/discord.js.map +1 -1
  11. package/dist/channels/slack.js +7 -2
  12. package/dist/channels/slack.js.map +1 -1
  13. package/dist/channels/telegram.js +1 -1
  14. package/dist/channels/telegram.js.map +1 -1
  15. package/dist/config/schema.js +6 -0
  16. package/dist/config/schema.js.map +1 -1
  17. package/dist/gateway/openai-compat.js +69 -10
  18. package/dist/gateway/openai-compat.js.map +1 -1
  19. package/dist/gateway/server.js +79 -0
  20. package/dist/gateway/server.js.map +1 -1
  21. package/dist/migrations/runner.js.map +1 -1
  22. package/dist/migrations/safeRun.js.map +1 -1
  23. package/dist/providers/anthropic.js +7 -4
  24. package/dist/providers/anthropic.js.map +1 -1
  25. package/dist/providers/base.js +8 -1
  26. package/dist/providers/base.js.map +1 -1
  27. package/dist/providers/google.js +7 -4
  28. package/dist/providers/google.js.map +1 -1
  29. package/dist/providers/ollama.js +8 -7
  30. package/dist/providers/ollama.js.map +1 -1
  31. package/dist/providers/openai.js +7 -4
  32. package/dist/providers/openai.js.map +1 -1
  33. package/dist/providers/openai_compat.js +7 -4
  34. package/dist/providers/openai_compat.js.map +1 -1
  35. package/dist/providers/router.js +7 -3
  36. package/dist/providers/router.js.map +1 -1
  37. package/dist/skills/builtin/backup.js.map +1 -1
  38. package/dist/skills/builtin/browser.js +11 -21
  39. package/dist/skills/builtin/browser.js.map +1 -1
  40. package/dist/skills/builtin/calendar.js +6 -2
  41. package/dist/skills/builtin/calendar.js.map +1 -1
  42. package/dist/skills/builtin/canvas_spaces.js.map +1 -1
  43. package/dist/skills/builtin/data_analysis.js +1 -1
  44. package/dist/skills/builtin/data_analysis.js.map +1 -1
  45. package/dist/skills/builtin/jira_linear.js +6 -4
  46. package/dist/skills/builtin/jira_linear.js.map +1 -1
  47. package/dist/skills/builtin/web_browser.js +6 -0
  48. package/dist/skills/builtin/web_browser.js.map +1 -1
  49. package/dist/skills/builtin/web_fetch.js +1 -0
  50. package/dist/skills/builtin/web_fetch.js.map +1 -1
  51. package/dist/storage/starterSpaces.js.map +1 -1
  52. package/dist/utils/constants.js +1 -1
  53. package/dist/utils/constants.js.map +1 -1
  54. package/package.json +1 -1
  55. package/ui/dist/sw.js +1 -1
@@ -71,27 +71,17 @@ ${links.join("\n")}`);
71
71
  });
72
72
  });
73
73
  }
74
- case "evaluate": {
75
- const script = args.script;
76
- if (!script) return "Error: script is required";
77
- return `Note: Full CDP browser evaluation requires a running browser session. Script queued: ${script.slice(0, 200)}`;
78
- }
79
- case "screenshot": {
80
- const url = args.url;
81
- if (!url) return "Error: url is required";
82
- return `Screenshot capture requires CDP connection to a running Chromium instance. Target: ${url}`;
83
- }
84
- case "click": {
85
- const selector = args.selector;
86
- if (!selector) return "Error: selector is required";
87
- return `Click action requires CDP connection. Selector: ${selector}`;
88
- }
89
- case "type": {
90
- const selector = args.selector;
91
- const text = args.text;
92
- if (!selector || !text) return "Error: selector and text are required";
93
- return `Type action requires CDP connection. Selector: ${selector}, Text: ${text}`;
94
- }
74
+ // v6.5 — these were non-functional stubs that echoed the args
75
+ // back as if the action were queued, misleading the model into
76
+ // believing the click/type/evaluate/screenshot actually happened.
77
+ // This lightweight 'browser' tool has no CDP/browser session, so
78
+ // redirect to the real Playwright-backed tools instead of faking.
79
+ case "evaluate":
80
+ case "click":
81
+ case "type":
82
+ return `Error: the '${action}' action is not supported by the lightweight 'browser' tool (it has no live browser session). Use 'browser_auto_nav' \u2014 real Playwright that supports click/fill/evaluate action sequences.`;
83
+ case "screenshot":
84
+ return `Error: the 'screenshot' action is not supported by the lightweight 'browser' tool. Use 'browser_screenshot' \u2014 real Playwright screenshots.`;
95
85
  default:
96
86
  return `Unknown browser action: ${action}`;
97
87
  }
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../src/skills/builtin/browser.ts"],"sourcesContent":["/**\n * TITAN — Browser Control Skill (Built-in)\n * CDP-based browser automation: navigate, snapshot, click, type, evaluate.\n */\nimport { registerSkill } from '../registry.js';\nimport { execFile } from 'child_process';\nimport logger from '../../utils/logger.js';\n\nconst COMPONENT = 'Browser';\n\nexport function registerBrowserSkill(): void {\n registerSkill(\n { name: 'browser', description: 'Browser control and web automation', version: '1.0.0', source: 'bundled', enabled: true },\n {\n name: 'browser',\n description: 'CDP-based browser control — navigate, snapshot, click, type, evaluate JS, and extract page content.\\n\\nUSE THIS WHEN Tony says: \"take a screenshot of X\" / \"navigate to X\" / \"click X on that page\" / \"extract links from X\" / \"run JavaScript on X\"\\n\\nACTIONS:\\n- navigate: go to a URL and extract its text\\n- snapshot/extract: get title, description, and links from a page\\n- click: click an element by CSS selector (requires live CDP session)\\n- type: type text into an element (requires live CDP session)\\n- evaluate: run JavaScript in the page context\\n- screenshot: capture a visual screenshot\\n\\nNOTE: Full interactive actions (click, type, evaluate, screenshot) require a connected Chromium browser via CDP. For most browsing tasks, prefer browse_url or web_read.',\n parameters: {\n type: 'object',\n properties: {\n action: {\n type: 'string',\n enum: ['navigate', 'snapshot', 'click', 'type', 'evaluate', 'extract', 'screenshot'],\n description: 'Browser action to perform',\n },\n url: { type: 'string', description: 'URL to navigate to (for navigate action)' },\n selector: { type: 'string', description: 'CSS selector for click/type actions' },\n text: { type: 'string', description: 'Text to type (for type action)' },\n script: { type: 'string', description: 'JavaScript code to evaluate (for evaluate action)' },\n },\n required: ['action'],\n },\n execute: async (args) => {\n const action = args.action as string;\n\n switch (action) {\n case 'navigate': {\n const url = args.url as string;\n if (!url) return 'Error: url is required';\n logger.info(COMPONENT, `Navigating to: ${url}`);\n // Use execFile to avoid shell injection via URL\n return new Promise<string>((resolve) => {\n execFile('curl', ['-sL', '--max-time', '15', url], { timeout: 20000, maxBuffer: 10 * 1024 * 1024 }, (err, stdout) => {\n if (err) {\n resolve(`Error fetching ${url}: ${err.message}`);\n return;\n }\n const raw = stdout.slice(0, 50000);\n // Strip HTML tags for text content\n const text = raw\n .replace(/<script[\\s\\S]*?<\\/script>/gi, '')\n .replace(/<style[\\s\\S]*?<\\/style>/gi, '')\n .replace(/<[^>]*>/g, ' ')\n .replace(/\\s+/g, ' ')\n .trim();\n resolve(`Page content from ${url}:\\n${text.slice(0, 20000)}`);\n });\n });\n }\n case 'snapshot':\n case 'extract': {\n const url = args.url as string;\n if (!url) return 'Error: url is required for snapshot/extract';\n return new Promise<string>((resolve) => {\n execFile('curl', ['-sL', '--max-time', '15', url], { timeout: 20000, maxBuffer: 10 * 1024 * 1024 }, (err, stdout) => {\n if (err) {\n resolve(`Error: ${err.message}`);\n return;\n }\n const raw = stdout.slice(0, 50000);\n // Extract title + meta description + links\n const title = raw.match(/<title[^>]*>(.*?)<\\/title>/i)?.[1] || 'No title';\n const desc = raw.match(/<meta[^>]*name=\"description\"[^>]*content=\"([^\"]*)\"[^>]*>/i)?.[1] || '';\n const links: string[] = [];\n const linkRegex = /<a[^>]*href=\"([^\"]*)\"[^>]*>(.*?)<\\/a>/gi;\n let m;\n while ((m = linkRegex.exec(raw)) !== null && links.length < 20) {\n const text = m[2].replace(/<[^>]*>/g, '').trim();\n if (text && m[1] && !m[1].startsWith('#')) links.push(` ${text}: ${m[1]}`);\n }\n resolve(`Page: ${title}\\nDescription: ${desc}\\nLinks:\\n${links.join('\\n')}`);\n });\n });\n }\n case 'evaluate': {\n const script = args.script as string;\n if (!script) return 'Error: script is required';\n return `Note: Full CDP browser evaluation requires a running browser session. Script queued: ${script.slice(0, 200)}`;\n }\n case 'screenshot': {\n const url = args.url as string;\n if (!url) return 'Error: url is required';\n return `Screenshot capture requires CDP connection to a running Chromium instance. Target: ${url}`;\n }\n case 'click': {\n const selector = args.selector as string;\n if (!selector) return 'Error: selector is required';\n return `Click action requires CDP connection. Selector: ${selector}`;\n }\n case 'type': {\n const selector = args.selector as string;\n const text = args.text as string;\n if (!selector || !text) return 'Error: selector and text are required';\n return `Type action requires CDP connection. Selector: ${selector}, Text: ${text}`;\n }\n default:\n return `Unknown browser action: ${action}`;\n }\n },\n },\n );\n}\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../../../src/skills/builtin/browser.ts"],"sourcesContent":["/**\n * TITAN — Browser Control Skill (Built-in)\n * CDP-based browser automation: navigate, snapshot, click, type, evaluate.\n */\nimport { registerSkill } from '../registry.js';\nimport { execFile } from 'child_process';\nimport logger from '../../utils/logger.js';\n\nconst COMPONENT = 'Browser';\n\nexport function registerBrowserSkill(): void {\n registerSkill(\n { name: 'browser', description: 'Browser control and web automation', version: '1.0.0', source: 'bundled', enabled: true },\n {\n name: 'browser',\n description: 'CDP-based browser control — navigate, snapshot, click, type, evaluate JS, and extract page content.\\n\\nUSE THIS WHEN Tony says: \"take a screenshot of X\" / \"navigate to X\" / \"click X on that page\" / \"extract links from X\" / \"run JavaScript on X\"\\n\\nACTIONS:\\n- navigate: go to a URL and extract its text\\n- snapshot/extract: get title, description, and links from a page\\n- click: click an element by CSS selector (requires live CDP session)\\n- type: type text into an element (requires live CDP session)\\n- evaluate: run JavaScript in the page context\\n- screenshot: capture a visual screenshot\\n\\nNOTE: Full interactive actions (click, type, evaluate, screenshot) require a connected Chromium browser via CDP. For most browsing tasks, prefer browse_url or web_read.',\n parameters: {\n type: 'object',\n properties: {\n action: {\n type: 'string',\n enum: ['navigate', 'snapshot', 'click', 'type', 'evaluate', 'extract', 'screenshot'],\n description: 'Browser action to perform',\n },\n url: { type: 'string', description: 'URL to navigate to (for navigate action)' },\n selector: { type: 'string', description: 'CSS selector for click/type actions' },\n text: { type: 'string', description: 'Text to type (for type action)' },\n script: { type: 'string', description: 'JavaScript code to evaluate (for evaluate action)' },\n },\n required: ['action'],\n },\n execute: async (args) => {\n const action = args.action as string;\n\n switch (action) {\n case 'navigate': {\n const url = args.url as string;\n if (!url) return 'Error: url is required';\n logger.info(COMPONENT, `Navigating to: ${url}`);\n // Use execFile to avoid shell injection via URL\n return new Promise<string>((resolve) => {\n execFile('curl', ['-sL', '--max-time', '15', url], { timeout: 20000, maxBuffer: 10 * 1024 * 1024 }, (err, stdout) => {\n if (err) {\n resolve(`Error fetching ${url}: ${err.message}`);\n return;\n }\n const raw = stdout.slice(0, 50000);\n // Strip HTML tags for text content\n const text = raw\n .replace(/<script[\\s\\S]*?<\\/script>/gi, '')\n .replace(/<style[\\s\\S]*?<\\/style>/gi, '')\n .replace(/<[^>]*>/g, ' ')\n .replace(/\\s+/g, ' ')\n .trim();\n resolve(`Page content from ${url}:\\n${text.slice(0, 20000)}`);\n });\n });\n }\n case 'snapshot':\n case 'extract': {\n const url = args.url as string;\n if (!url) return 'Error: url is required for snapshot/extract';\n return new Promise<string>((resolve) => {\n execFile('curl', ['-sL', '--max-time', '15', url], { timeout: 20000, maxBuffer: 10 * 1024 * 1024 }, (err, stdout) => {\n if (err) {\n resolve(`Error: ${err.message}`);\n return;\n }\n const raw = stdout.slice(0, 50000);\n // Extract title + meta description + links\n const title = raw.match(/<title[^>]*>(.*?)<\\/title>/i)?.[1] || 'No title';\n const desc = raw.match(/<meta[^>]*name=\"description\"[^>]*content=\"([^\"]*)\"[^>]*>/i)?.[1] || '';\n const links: string[] = [];\n const linkRegex = /<a[^>]*href=\"([^\"]*)\"[^>]*>(.*?)<\\/a>/gi;\n let m;\n while ((m = linkRegex.exec(raw)) !== null && links.length < 20) {\n const text = m[2].replace(/<[^>]*>/g, '').trim();\n if (text && m[1] && !m[1].startsWith('#')) links.push(` ${text}: ${m[1]}`);\n }\n resolve(`Page: ${title}\\nDescription: ${desc}\\nLinks:\\n${links.join('\\n')}`);\n });\n });\n }\n // v6.5 these were non-functional stubs that echoed the args\n // back as if the action were queued, misleading the model into\n // believing the click/type/evaluate/screenshot actually happened.\n // This lightweight 'browser' tool has no CDP/browser session, so\n // redirect to the real Playwright-backed tools instead of faking.\n case 'evaluate':\n case 'click':\n case 'type':\n return `Error: the '${action}' action is not supported by the lightweight 'browser' tool (it has no live browser session). Use 'browser_auto_nav' real Playwright that supports click/fill/evaluate action sequences.`;\n case 'screenshot':\n return `Error: the 'screenshot' action is not supported by the lightweight 'browser' tool. Use 'browser_screenshot' real Playwright screenshots.`;\n default:\n return `Unknown browser action: ${action}`;\n }\n },\n },\n );\n}\n"],"mappings":";AAIA,SAAS,qBAAqB;AAC9B,SAAS,gBAAgB;AACzB,OAAO,YAAY;AAEnB,MAAM,YAAY;AAEX,SAAS,uBAA6B;AACzC;AAAA,IACI,EAAE,MAAM,WAAW,aAAa,sCAAsC,SAAS,SAAS,QAAQ,WAAW,SAAS,KAAK;AAAA,IACzH;AAAA,MACI,MAAM;AAAA,MACN,aAAa;AAAA,MACb,YAAY;AAAA,QACR,MAAM;AAAA,QACN,YAAY;AAAA,UACR,QAAQ;AAAA,YACJ,MAAM;AAAA,YACN,MAAM,CAAC,YAAY,YAAY,SAAS,QAAQ,YAAY,WAAW,YAAY;AAAA,YACnF,aAAa;AAAA,UACjB;AAAA,UACA,KAAK,EAAE,MAAM,UAAU,aAAa,2CAA2C;AAAA,UAC/E,UAAU,EAAE,MAAM,UAAU,aAAa,sCAAsC;AAAA,UAC/E,MAAM,EAAE,MAAM,UAAU,aAAa,iCAAiC;AAAA,UACtE,QAAQ,EAAE,MAAM,UAAU,aAAa,oDAAoD;AAAA,QAC/F;AAAA,QACA,UAAU,CAAC,QAAQ;AAAA,MACvB;AAAA,MACA,SAAS,OAAO,SAAS;AACrB,cAAM,SAAS,KAAK;AAEpB,gBAAQ,QAAQ;AAAA,UACZ,KAAK,YAAY;AACb,kBAAM,MAAM,KAAK;AACjB,gBAAI,CAAC,IAAK,QAAO;AACjB,mBAAO,KAAK,WAAW,kBAAkB,GAAG,EAAE;AAE9C,mBAAO,IAAI,QAAgB,CAAC,YAAY;AACpC,uBAAS,QAAQ,CAAC,OAAO,cAAc,MAAM,GAAG,GAAG,EAAE,SAAS,KAAO,WAAW,KAAK,OAAO,KAAK,GAAG,CAAC,KAAK,WAAW;AACjH,oBAAI,KAAK;AACL,0BAAQ,kBAAkB,GAAG,KAAK,IAAI,OAAO,EAAE;AAC/C;AAAA,gBACJ;AACA,sBAAM,MAAM,OAAO,MAAM,GAAG,GAAK;AAEjC,sBAAM,OAAO,IACR,QAAQ,+BAA+B,EAAE,EACzC,QAAQ,6BAA6B,EAAE,EACvC,QAAQ,YAAY,GAAG,EACvB,QAAQ,QAAQ,GAAG,EACnB,KAAK;AACV,wBAAQ,qBAAqB,GAAG;AAAA,EAAM,KAAK,MAAM,GAAG,GAAK,CAAC,EAAE;AAAA,cAChE,CAAC;AAAA,YACL,CAAC;AAAA,UACL;AAAA,UACA,KAAK;AAAA,UACL,KAAK,WAAW;AACZ,kBAAM,MAAM,KAAK;AACjB,gBAAI,CAAC,IAAK,QAAO;AACjB,mBAAO,IAAI,QAAgB,CAAC,YAAY;AACpC,uBAAS,QAAQ,CAAC,OAAO,cAAc,MAAM,GAAG,GAAG,EAAE,SAAS,KAAO,WAAW,KAAK,OAAO,KAAK,GAAG,CAAC,KAAK,WAAW;AACjH,oBAAI,KAAK;AACL,0BAAQ,UAAU,IAAI,OAAO,EAAE;AAC/B;AAAA,gBACJ;AACA,sBAAM,MAAM,OAAO,MAAM,GAAG,GAAK;AAEjC,sBAAM,QAAQ,IAAI,MAAM,6BAA6B,IAAI,CAAC,KAAK;AAC/D,sBAAM,OAAO,IAAI,MAAM,2DAA2D,IAAI,CAAC,KAAK;AAC5F,sBAAM,QAAkB,CAAC;AACzB,sBAAM,YAAY;AAClB,oBAAI;AACJ,wBAAQ,IAAI,UAAU,KAAK,GAAG,OAAO,QAAQ,MAAM,SAAS,IAAI;AAC5D,wBAAM,OAAO,EAAE,CAAC,EAAE,QAAQ,YAAY,EAAE,EAAE,KAAK;AAC/C,sBAAI,QAAQ,EAAE,CAAC,KAAK,CAAC,EAAE,CAAC,EAAE,WAAW,GAAG,EAAG,OAAM,KAAK,KAAK,IAAI,KAAK,EAAE,CAAC,CAAC,EAAE;AAAA,gBAC9E;AACA,wBAAQ,SAAS,KAAK;AAAA,eAAkB,IAAI;AAAA;AAAA,EAAa,MAAM,KAAK,IAAI,CAAC,EAAE;AAAA,cAC/E,CAAC;AAAA,YACL,CAAC;AAAA,UACL;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,UAMA,KAAK;AAAA,UACL,KAAK;AAAA,UACL,KAAK;AACD,mBAAO,eAAe,MAAM;AAAA,UAChC,KAAK;AACD,mBAAO;AAAA,UACX;AACI,mBAAO,2BAA2B,MAAM;AAAA,QAChD;AAAA,MACJ;AAAA,IACJ;AAAA,EACJ;AACJ;","names":[]}
@@ -61,10 +61,14 @@ Setup instructions:
61
61
  switch (action) {
62
62
  case "list":
63
63
  return await handleCalendarList(apiKey, calendarId, args);
64
+ // v6.5 — create/delete need Google OAuth, not an API key (an
65
+ // API key only authorizes public reads → 401/403), so these
66
+ // never worked. Disabled honestly until OAuth is wired (tracked
67
+ // follow-up) instead of returning an opaque API error.
68
+ // handleCalendarCreate/Delete are kept for when OAuth lands.
64
69
  case "create":
65
- return await handleCalendarCreate(apiKey, calendarId, args);
66
70
  case "delete":
67
- return await handleCalendarDelete(apiKey, calendarId, args);
71
+ return `Error: calendar '${action}' requires Google OAuth, which TITAN does not yet support (an API key only authorizes public read access). This is a tracked follow-up. The 'list' action works for public/shared calendars.`;
68
72
  default:
69
73
  return `Unknown action: ${action}`;
70
74
  }
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../src/skills/builtin/calendar.ts"],"sourcesContent":["/**\n * TITAN — Calendar Skill (Built-in)\n * Integrate with Google Calendar via REST API.\n * Requires GOOGLE_CALENDAR_API_KEY and GOOGLE_CALENDAR_ID environment variables.\n */\nimport { registerSkill } from '../registry.js';\nimport logger from '../../utils/logger.js';\n\nconst COMPONENT = 'Calendar';\n\n/**\n * Get the calendar ID from environment or use 'primary'\n * For 'primary' calendar, OAuth is required; for service account calendars, API key works.\n */\nfunction getCalendarId(): string {\n return process.env.GOOGLE_CALENDAR_ID || 'primary';\n}\n\n/**\n * Get the API key from environment\n */\nfunction getApiKey(): string | null {\n return process.env.GOOGLE_CALENDAR_API_KEY || null;\n}\n\n/**\n * Parse ISO string to UTC RFC3339 format for Google Calendar API\n */\nfunction toRfc3339(isoString: string): string {\n const date = new Date(isoString);\n return date.toISOString();\n}\n\n/**\n * Validate ISO datetime string\n */\nfunction isValidIsoDate(dateStr: string): boolean {\n try {\n const date = new Date(dateStr);\n return !isNaN(date.getTime());\n } catch {\n return false;\n }\n}\n\nexport function registerCalendarSkill(): void {\n registerSkill(\n { name: 'calendar', description: 'Google Calendar integration for scheduling and event management. USE THIS WHEN Tony says: \"schedule a meeting\", \"add event\", \"what\\'s on my calendar\", \"what do I have today/this week\", \"am I free on X\", \"cancel meeting\", \"delete event\". WORKFLOW: Before creating an event, always call action=list first to check for conflicts. Then call action=create with the confirmed details. RULES: Never create duplicate events — check first.', version: '1.0.0', source: 'bundled', enabled: true },\n {\n name: 'calendar',\n description: 'Manages Google Calendar events: list upcoming events, create new events, or delete events. USE THIS WHEN Tony says: \"schedule a meeting\", \"add event\", \"what\\'s on my calendar\", \"what do I have today/this week\", \"am I free on X\", \"cancel meeting\", \"delete event\". WORKFLOW: 1) Call action=list to check existing events and avoid conflicts. 2) Call action=create with confirmed title, startTime, and endTime. RULES: Always check calendar before adding — never create duplicate events.',\n parameters: {\n type: 'object',\n properties: {\n action: { type: 'string', enum: ['list', 'create', 'delete'], description: 'Action to perform' },\n days: { type: 'number', description: 'Number of days to look ahead (default: 7)' },\n maxResults: { type: 'number', description: 'Max events to return (default: 10)' },\n title: { type: 'string', description: 'Event title (required for create)' },\n startTime: { type: 'string', description: 'Event start time in ISO format (required for create)' },\n endTime: { type: 'string', description: 'Event end time in ISO format (required for create)' },\n description: { type: 'string', description: 'Event description' },\n location: { type: 'string', description: 'Event location' },\n eventId: { type: 'string', description: 'Event ID (required for delete)' },\n },\n required: ['action'],\n },\n execute: async (args) => {\n const action = args.action as string;\n const apiKey = getApiKey();\n const calendarId = getCalendarId();\n\n if (!apiKey) {\n return `Error: GOOGLE_CALENDAR_API_KEY not configured.\\n\\nSetup instructions:\\n1. Go to https://console.cloud.google.com/\\n2. Create a new project or select existing\\n3. Enable Google Calendar API\\n4. Create an API key or Service Account\\n5. Set environment variable: export GOOGLE_CALENDAR_API_KEY=\"your-api-key\"\\n6. Optionally set: export GOOGLE_CALENDAR_ID=\"calendar-id\"\\n (If not set, 'primary' calendar is used, which requires OAuth)`;\n }\n\n try {\n switch (action) {\n case 'list':\n return await handleCalendarList(apiKey, calendarId, args);\n case 'create':\n return await handleCalendarCreate(apiKey, calendarId, args);\n case 'delete':\n return await handleCalendarDelete(apiKey, calendarId, args);\n default:\n return `Unknown action: ${action}`;\n }\n } catch (e) {\n const errorMsg = (e as Error).message;\n logger.error(COMPONENT, `Calendar operation failed: ${errorMsg}`);\n return `Error: ${errorMsg}`;\n }\n },\n },\n );\n}\n\n/**\n * Handle calendar_list action\n */\nasync function handleCalendarList(apiKey: string, calendarId: string, args: Record<string, unknown>): Promise<string> {\n const days = Math.min((args.days as number) || 7, 365); // Cap at 1 year\n const maxResults = Math.min((args.maxResults as number) || 10, 250); // Cap at Google's max\n\n const now = new Date();\n const timeMin = now.toISOString();\n const timeMax = new Date(now.getTime() + days * 24 * 60 * 60 * 1000).toISOString();\n\n const url = new URL(`https://www.googleapis.com/calendar/v3/calendars/${encodeURIComponent(calendarId)}/events`);\n url.searchParams.append('key', apiKey);\n url.searchParams.append('timeMin', timeMin);\n url.searchParams.append('timeMax', timeMax);\n url.searchParams.append('maxResults', String(maxResults));\n url.searchParams.append('orderBy', 'startTime');\n url.searchParams.append('singleEvents', 'true');\n\n const response = await fetch(url.toString(), {\n method: 'GET',\n signal: AbortSignal.timeout(10000),\n });\n\n if (!response.ok) {\n const error = await response.text();\n throw new Error(`Google Calendar API error: ${response.status} - ${error}`);\n }\n\n const data = (await response.json()) as { items?: Array<{ id: string; summary?: string; start?: { dateTime?: string; date?: string }; end?: { dateTime?: string; date?: string }; description?: string; location?: string }> };\n\n if (!data.items || data.items.length === 0) {\n return `No events found in the next ${days} days.`;\n }\n\n const eventLines = data.items.map((event) => {\n const startStr = event.start?.dateTime || event.start?.date || 'Unknown';\n const endStr = event.end?.dateTime || event.end?.date || 'Unknown';\n const title = event.summary || 'Untitled';\n const location = event.location ? ` at ${event.location}` : '';\n const description = event.description ? `\\n Description: ${event.description.slice(0, 100)}...` : '';\n return `• ${title}\\n ID: ${event.id}\\n Start: ${startStr}\\n End: ${endStr}${location}${description}`;\n });\n\n return `Events for the next ${days} days:\\n\\n${eventLines.join('\\n\\n')}`;\n}\n\n/**\n * Handle calendar_create action\n */\nasync function handleCalendarCreate(apiKey: string, calendarId: string, args: Record<string, unknown>): Promise<string> {\n const title = args.title as string | undefined;\n const startTime = args.startTime as string | undefined;\n const endTime = args.endTime as string | undefined;\n\n if (!title || !startTime || !endTime) {\n return 'Error: title, startTime, and endTime are required for create action.';\n }\n\n if (!isValidIsoDate(startTime) || !isValidIsoDate(endTime)) {\n return 'Error: startTime and endTime must be valid ISO 8601 datetime strings (e.g., 2026-03-15T10:00:00Z).';\n }\n\n const start = new Date(startTime);\n const end = new Date(endTime);\n if (start >= end) {\n return 'Error: startTime must be before endTime.';\n }\n\n const eventBody = {\n summary: title,\n start: {\n dateTime: toRfc3339(startTime),\n timeZone: 'UTC',\n },\n end: {\n dateTime: toRfc3339(endTime),\n timeZone: 'UTC',\n },\n description: (args.description as string) || '',\n location: (args.location as string) || '',\n };\n\n const url = `https://www.googleapis.com/calendar/v3/calendars/${encodeURIComponent(calendarId)}/events?key=${encodeURIComponent(apiKey)}`;\n\n const response = await fetch(url, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n },\n body: JSON.stringify(eventBody),\n signal: AbortSignal.timeout(10000),\n });\n\n if (!response.ok) {\n const error = await response.text();\n throw new Error(`Google Calendar API error: ${response.status} - ${error}`);\n }\n\n const data = (await response.json()) as { id: string; htmlLink?: string };\n\n logger.info(COMPONENT, `Created event: ${title} (${data.id})`);\n return `Created event \"${title}\"\\n Event ID: ${data.id}\\n Start: ${startTime}\\n End: ${endTime}${data.htmlLink ? `\\n Link: ${data.htmlLink}` : ''}`;\n}\n\n/**\n * Handle calendar_delete action\n */\nasync function handleCalendarDelete(apiKey: string, calendarId: string, args: Record<string, unknown>): Promise<string> {\n const eventId = args.eventId as string | undefined;\n\n if (!eventId) {\n return 'Error: eventId is required for delete action.';\n }\n\n const url = `https://www.googleapis.com/calendar/v3/calendars/${encodeURIComponent(calendarId)}/events/${encodeURIComponent(eventId)}?key=${encodeURIComponent(apiKey)}`;\n\n const response = await fetch(url, {\n method: 'DELETE',\n signal: AbortSignal.timeout(10000),\n });\n\n if (response.status === 404) {\n return `Error: Event not found (ID: ${eventId}).`;\n }\n\n if (!response.ok) {\n const error = await response.text();\n throw new Error(`Google Calendar API error: ${response.status} - ${error}`);\n }\n\n logger.info(COMPONENT, `Deleted event: ${eventId}`);\n return `Deleted event: ${eventId}`;\n}\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../../../src/skills/builtin/calendar.ts"],"sourcesContent":["/**\n * TITAN — Calendar Skill (Built-in)\n * Integrate with Google Calendar via REST API.\n * Requires GOOGLE_CALENDAR_API_KEY and GOOGLE_CALENDAR_ID environment variables.\n */\nimport { registerSkill } from '../registry.js';\nimport logger from '../../utils/logger.js';\n\nconst COMPONENT = 'Calendar';\n\n/**\n * Get the calendar ID from environment or use 'primary'\n * For 'primary' calendar, OAuth is required; for service account calendars, API key works.\n */\nfunction getCalendarId(): string {\n return process.env.GOOGLE_CALENDAR_ID || 'primary';\n}\n\n/**\n * Get the API key from environment\n */\nfunction getApiKey(): string | null {\n return process.env.GOOGLE_CALENDAR_API_KEY || null;\n}\n\n/**\n * Parse ISO string to UTC RFC3339 format for Google Calendar API\n */\nfunction toRfc3339(isoString: string): string {\n const date = new Date(isoString);\n return date.toISOString();\n}\n\n/**\n * Validate ISO datetime string\n */\nfunction isValidIsoDate(dateStr: string): boolean {\n try {\n const date = new Date(dateStr);\n return !isNaN(date.getTime());\n } catch {\n return false;\n }\n}\n\nexport function registerCalendarSkill(): void {\n registerSkill(\n { name: 'calendar', description: 'Google Calendar integration for scheduling and event management. USE THIS WHEN Tony says: \"schedule a meeting\", \"add event\", \"what\\'s on my calendar\", \"what do I have today/this week\", \"am I free on X\", \"cancel meeting\", \"delete event\". WORKFLOW: Before creating an event, always call action=list first to check for conflicts. Then call action=create with the confirmed details. RULES: Never create duplicate events — check first.', version: '1.0.0', source: 'bundled', enabled: true },\n {\n name: 'calendar',\n description: 'Manages Google Calendar events: list upcoming events, create new events, or delete events. USE THIS WHEN Tony says: \"schedule a meeting\", \"add event\", \"what\\'s on my calendar\", \"what do I have today/this week\", \"am I free on X\", \"cancel meeting\", \"delete event\". WORKFLOW: 1) Call action=list to check existing events and avoid conflicts. 2) Call action=create with confirmed title, startTime, and endTime. RULES: Always check calendar before adding — never create duplicate events.',\n parameters: {\n type: 'object',\n properties: {\n action: { type: 'string', enum: ['list', 'create', 'delete'], description: 'Action to perform' },\n days: { type: 'number', description: 'Number of days to look ahead (default: 7)' },\n maxResults: { type: 'number', description: 'Max events to return (default: 10)' },\n title: { type: 'string', description: 'Event title (required for create)' },\n startTime: { type: 'string', description: 'Event start time in ISO format (required for create)' },\n endTime: { type: 'string', description: 'Event end time in ISO format (required for create)' },\n description: { type: 'string', description: 'Event description' },\n location: { type: 'string', description: 'Event location' },\n eventId: { type: 'string', description: 'Event ID (required for delete)' },\n },\n required: ['action'],\n },\n execute: async (args) => {\n const action = args.action as string;\n const apiKey = getApiKey();\n const calendarId = getCalendarId();\n\n if (!apiKey) {\n return `Error: GOOGLE_CALENDAR_API_KEY not configured.\\n\\nSetup instructions:\\n1. Go to https://console.cloud.google.com/\\n2. Create a new project or select existing\\n3. Enable Google Calendar API\\n4. Create an API key or Service Account\\n5. Set environment variable: export GOOGLE_CALENDAR_API_KEY=\"your-api-key\"\\n6. Optionally set: export GOOGLE_CALENDAR_ID=\"calendar-id\"\\n (If not set, 'primary' calendar is used, which requires OAuth)`;\n }\n\n try {\n switch (action) {\n case 'list':\n return await handleCalendarList(apiKey, calendarId, args);\n // v6.5 — create/delete need Google OAuth, not an API key (an\n // API key only authorizes public reads → 401/403), so these\n // never worked. Disabled honestly until OAuth is wired (tracked\n // follow-up) instead of returning an opaque API error.\n // handleCalendarCreate/Delete are kept for when OAuth lands.\n case 'create':\n case 'delete':\n return `Error: calendar '${action}' requires Google OAuth, which TITAN does not yet support (an API key only authorizes public read access). This is a tracked follow-up. The 'list' action works for public/shared calendars.`;\n default:\n return `Unknown action: ${action}`;\n }\n } catch (e) {\n const errorMsg = (e as Error).message;\n logger.error(COMPONENT, `Calendar operation failed: ${errorMsg}`);\n return `Error: ${errorMsg}`;\n }\n },\n },\n );\n}\n\n/**\n * Handle calendar_list action\n */\nasync function handleCalendarList(apiKey: string, calendarId: string, args: Record<string, unknown>): Promise<string> {\n const days = Math.min((args.days as number) || 7, 365); // Cap at 1 year\n const maxResults = Math.min((args.maxResults as number) || 10, 250); // Cap at Google's max\n\n const now = new Date();\n const timeMin = now.toISOString();\n const timeMax = new Date(now.getTime() + days * 24 * 60 * 60 * 1000).toISOString();\n\n const url = new URL(`https://www.googleapis.com/calendar/v3/calendars/${encodeURIComponent(calendarId)}/events`);\n url.searchParams.append('key', apiKey);\n url.searchParams.append('timeMin', timeMin);\n url.searchParams.append('timeMax', timeMax);\n url.searchParams.append('maxResults', String(maxResults));\n url.searchParams.append('orderBy', 'startTime');\n url.searchParams.append('singleEvents', 'true');\n\n const response = await fetch(url.toString(), {\n method: 'GET',\n signal: AbortSignal.timeout(10000),\n });\n\n if (!response.ok) {\n const error = await response.text();\n throw new Error(`Google Calendar API error: ${response.status} - ${error}`);\n }\n\n const data = (await response.json()) as { items?: Array<{ id: string; summary?: string; start?: { dateTime?: string; date?: string }; end?: { dateTime?: string; date?: string }; description?: string; location?: string }> };\n\n if (!data.items || data.items.length === 0) {\n return `No events found in the next ${days} days.`;\n }\n\n const eventLines = data.items.map((event) => {\n const startStr = event.start?.dateTime || event.start?.date || 'Unknown';\n const endStr = event.end?.dateTime || event.end?.date || 'Unknown';\n const title = event.summary || 'Untitled';\n const location = event.location ? ` at ${event.location}` : '';\n const description = event.description ? `\\n Description: ${event.description.slice(0, 100)}...` : '';\n return `• ${title}\\n ID: ${event.id}\\n Start: ${startStr}\\n End: ${endStr}${location}${description}`;\n });\n\n return `Events for the next ${days} days:\\n\\n${eventLines.join('\\n\\n')}`;\n}\n\n/**\n * Handle calendar_create action\n */\nasync function handleCalendarCreate(apiKey: string, calendarId: string, args: Record<string, unknown>): Promise<string> {\n const title = args.title as string | undefined;\n const startTime = args.startTime as string | undefined;\n const endTime = args.endTime as string | undefined;\n\n if (!title || !startTime || !endTime) {\n return 'Error: title, startTime, and endTime are required for create action.';\n }\n\n if (!isValidIsoDate(startTime) || !isValidIsoDate(endTime)) {\n return 'Error: startTime and endTime must be valid ISO 8601 datetime strings (e.g., 2026-03-15T10:00:00Z).';\n }\n\n const start = new Date(startTime);\n const end = new Date(endTime);\n if (start >= end) {\n return 'Error: startTime must be before endTime.';\n }\n\n const eventBody = {\n summary: title,\n start: {\n dateTime: toRfc3339(startTime),\n timeZone: 'UTC',\n },\n end: {\n dateTime: toRfc3339(endTime),\n timeZone: 'UTC',\n },\n description: (args.description as string) || '',\n location: (args.location as string) || '',\n };\n\n const url = `https://www.googleapis.com/calendar/v3/calendars/${encodeURIComponent(calendarId)}/events?key=${encodeURIComponent(apiKey)}`;\n\n const response = await fetch(url, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n },\n body: JSON.stringify(eventBody),\n signal: AbortSignal.timeout(10000),\n });\n\n if (!response.ok) {\n const error = await response.text();\n throw new Error(`Google Calendar API error: ${response.status} - ${error}`);\n }\n\n const data = (await response.json()) as { id: string; htmlLink?: string };\n\n logger.info(COMPONENT, `Created event: ${title} (${data.id})`);\n return `Created event \"${title}\"\\n Event ID: ${data.id}\\n Start: ${startTime}\\n End: ${endTime}${data.htmlLink ? `\\n Link: ${data.htmlLink}` : ''}`;\n}\n\n/**\n * Handle calendar_delete action\n */\nasync function handleCalendarDelete(apiKey: string, calendarId: string, args: Record<string, unknown>): Promise<string> {\n const eventId = args.eventId as string | undefined;\n\n if (!eventId) {\n return 'Error: eventId is required for delete action.';\n }\n\n const url = `https://www.googleapis.com/calendar/v3/calendars/${encodeURIComponent(calendarId)}/events/${encodeURIComponent(eventId)}?key=${encodeURIComponent(apiKey)}`;\n\n const response = await fetch(url, {\n method: 'DELETE',\n signal: AbortSignal.timeout(10000),\n });\n\n if (response.status === 404) {\n return `Error: Event not found (ID: ${eventId}).`;\n }\n\n if (!response.ok) {\n const error = await response.text();\n throw new Error(`Google Calendar API error: ${response.status} - ${error}`);\n }\n\n logger.info(COMPONENT, `Deleted event: ${eventId}`);\n return `Deleted event: ${eventId}`;\n}\n"],"mappings":"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","names":[]}
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../src/skills/builtin/canvas_spaces.ts"],"sourcesContent":["/**\n * TITAN — Canvas Spaces skill (v6.0 step 3)\n *\n * Why this skill exists\n * ---------------------\n * v6.0's product thesis: \"infinite workspaces that materialize around the\n * user's work.\" The agent must be able to create, switch, list, rename,\n * and archive Spaces in response to user requests. That's what this skill\n * exposes — 5 tools, one per lifecycle operation.\n *\n * Tools:\n * - create_space — make a new Space, optionally make it active\n * - switch_space — change which Space is active\n * - list_spaces — what workspaces does the user have?\n * - rename_space — change a Space's name (icon/color/intent via update)\n * - archive_space — soft-delete a Space (recoverable)\n *\n * Storage: `src/storage/spaces.ts` owns the on-disk format\n * (`~/.titan/spaces.json`). The SPA's `SpaceEngine` syncs to the same\n * file via the gateway's `/api/spaces` endpoints.\n *\n * Reference:\n * - ~/.claude/projects/-Users-michaelelliott/memory/titan-v6-living-canvas.md\n * (v6.0 step 3: 5 Space-lifecycle tools)\n * - docs/PIPELINES.md (Pipeline 1 — chat message — explains how the\n * active Space's intent flows into the system prompt)\n */\nimport { registerSkill } from '../registry.js';\nimport {\n createSpace as storageCreateSpace,\n switchSpace as storageSwitchSpace,\n listSpaces as storageListSpaces,\n renameSpace as storageRenameSpace,\n archiveSpace as storageArchiveSpace,\n getActiveSpace as storageGetActiveSpace,\n} from '../../storage/spaces.js';\nimport { getStarterPreset, listStarterPresets } from '../../storage/starterSpaces.js';\nimport logger from '../../utils/logger.js';\n\nconst COMPONENT = 'CanvasSpaces';\n\n// ─── Tool: create_space ────────────────────────────────────────────\n\nconst createHandler = {\n name: 'create_space',\n description: [\n 'Create a new workspace (\"Space\") for the user. A Space is a dedicated canvas with its own widgets, intent, and agent posture. Use this when the user expresses a new domain of work — \"make me a place for tracking my freelance gigs\", \"set up a DJ workspace\", \"I want a Space for my homelab.\"',\n '',\n 'USE CASE: user wants a dedicated environment for a new kind of work.',\n 'ANTI-PATTERN: do NOT call this when the user wants a single widget added to their existing Space — use `create_widget` for that. Spaces are for distinct domains, not individual tools.',\n '',\n 'PARAMETERS:',\n ' • name — REQUIRED. Display name shown in the sidebar.',\n ' • icon — Optional emoji or short string shown beside the name.',\n ' • color — Optional hex color (e.g. \"#3b82f6\") for the sidebar accent.',\n ' • intent — Optional one-sentence brief that tells TITAN what posture to take when this Space is active (e.g. \"you are helping the user manage their freelance business — focus on income, time, invoices\"). Saved as the Space\\'s `agentInstructions` and injected into the system prompt every turn this Space is active.',\n ' • starterWidgets — Optional array of widget specs to pre-pin: `[{name, source|template, format?, w?, h?}, ...]`. Use this to materialize a useful Space on creation rather than handing the user an empty canvas.',\n ' • preset — Optional starter-preset id: `coder`, `dj`, `founder`, `homelab`, or `default`. When supplied, the preset\\'s name/icon/color/intent are used as defaults (overridable by the other args). Use this when the user describes a domain that matches one of the presets — \"set up my coding workspace\" → `preset: \"coder\"`.',\n ' • makeActive — Default true when this is the user\\'s first space; otherwise default false. Pass true to switch to the new Space immediately.',\n '',\n 'RETURNS: \"Created Space \\'<name>\\' (id: <id>) — <N> widgets pre-pinned. <active|not active>.\" on success.',\n 'ERRORS: \"Error: ...\" when name is missing or storage fails.',\n ].join('\\n'),\n parameters: {\n type: 'object',\n properties: {\n name: { type: 'string', description: 'Display name for the new Space.' },\n icon: { type: 'string', description: 'Optional emoji / short string.' },\n color: { type: 'string', description: 'Optional hex accent color.' },\n intent: { type: 'string', description: 'Optional agent-posture brief injected into the system prompt when this Space is active.' },\n starterWidgets: {\n type: 'array',\n description: 'Optional widget specs to pre-pin. Same shape as create_widget arguments.',\n items: { type: 'object' },\n },\n preset: {\n type: 'string',\n enum: ['default', 'coder', 'dj', 'founder', 'homelab'],\n description: 'Optional starter-preset id. Supplies defaults for name/icon/color/intent.',\n },\n makeActive: { type: 'boolean', description: 'Switch to this Space immediately (default false; true when no other spaces).' },\n },\n required: ['name'],\n },\n execute: async (args: Record<string, unknown>): Promise<string> => {\n // Resolve preset first so its values become DEFAULTS the explicit\n // args can override (caller intent wins over preset).\n const presetId = typeof args.preset === 'string' ? args.preset : undefined;\n const preset = presetId ? getStarterPreset(presetId as 'default' | 'coder' | 'dj' | 'founder' | 'homelab') : undefined;\n\n // name is required UNLESS a preset is supplied (preset.name is the default).\n const explicitName = typeof args.name === 'string' ? args.name.trim() : '';\n const name = explicitName || preset?.name || '';\n if (!name) return 'Error: \"name\" is required (or pass a \"preset\" id which supplies one).';\n\n const starter = Array.isArray(args.starterWidgets) ? args.starterWidgets : undefined;\n const makeActive = args.makeActive as boolean | undefined;\n try {\n const space = storageCreateSpace({\n name,\n icon: typeof args.icon === 'string' ? args.icon : preset?.icon,\n color: typeof args.color === 'string' ? args.color : preset?.color,\n agentInstructions: typeof args.intent === 'string'\n ? args.intent\n : preset?.agentInstructions,\n starterWidgets: starter,\n makeActive,\n });\n logger.info(COMPONENT, `create_space → ${space.id} \"${space.name}\" (widgets=${space.widgets.length})`);\n const active = makeActive ? 'now active' : 'not active (use switch_space to jump in)';\n const widgetCount = space.widgets.length;\n return `Created Space \"${space.name}\" (id: ${space.id}) — ${widgetCount} widget(s) pre-pinned. ${active}.`;\n } catch (err) {\n return `Error: create_space failed — ${(err as Error).message}`;\n }\n },\n};\n\n// ─── Tool: switch_space ────────────────────────────────────────────\n\nconst switchHandler = {\n name: 'switch_space',\n description: [\n 'Switch which Space is currently active. The active Space\\'s `intent` flows into the system prompt every turn, so switching changes the agent\\'s posture for the rest of the conversation.',\n '',\n 'USE CASE: user says \"switch to my DJ Space\", \"let\\'s work on freelance for a bit\", \"go to home\".',\n 'ANTI-PATTERN: do NOT switch silently when the user is in the middle of a task in another Space.',\n '',\n 'PARAMETERS:',\n ' • id — REQUIRED. Space id from `list_spaces`.',\n '',\n 'RETURNS: \"Switched to Space \\'<name>\\' (id: <id>). Active intent: <intent or \\'none\\'>.\" on success.',\n 'ERRORS: \"Error: Space \\'<id>\\' not found. ...\" when the id doesn\\'t exist.',\n ].join('\\n'),\n parameters: {\n type: 'object',\n properties: {\n id: { type: 'string', description: 'Space id (run list_spaces to find it).' },\n },\n required: ['id'],\n },\n execute: async (args: Record<string, unknown>): Promise<string> => {\n const id = typeof args.id === 'string' ? args.id.trim() : '';\n if (!id) return 'Error: \"id\" is required. Run list_spaces to find an id.';\n const space = storageSwitchSpace(id);\n if (!space) return `Error: Space \"${id}\" not found. Run list_spaces to see available Spaces.`;\n logger.info(COMPONENT, `switch_space → ${space.id} \"${space.name}\"`);\n const intent = space.agentInstructions\n ? space.agentInstructions.slice(0, 120) + (space.agentInstructions.length > 120 ? '…' : '')\n : 'none';\n return `Switched to Space \"${space.name}\" (id: ${space.id}). Active intent: ${intent}`;\n },\n};\n\n// ─── Tool: list_spaces ─────────────────────────────────────────────\n\nconst listHandler = {\n name: 'list_spaces',\n description: [\n 'List the user\\'s workspaces (Spaces), newest-updated first. Use before `switch_space` to find the id, or when the user asks \"what workspaces do I have?\".',\n '',\n 'PARAMETERS:',\n ' • limit — Optional cap on returned entries (default 20).',\n '',\n 'RETURNS: A markdown table with id, name, widget count, active flag, last-updated. Empty-state message when no Spaces exist.',\n ].join('\\n'),\n parameters: {\n type: 'object',\n properties: {\n limit: { type: 'number', description: 'Max entries to return (default 20).' },\n },\n },\n execute: async (args: Record<string, unknown>): Promise<string> => {\n const limit = Math.max(1, Math.min(100, (args.limit as number) ?? 20));\n const all = storageListSpaces().slice(0, limit);\n const active = storageGetActiveSpace();\n if (all.length === 0) {\n return 'No Spaces yet. Use `create_space` to make one — \"what kind of work would you like a Space for?\"';\n }\n const lines = [\n `${all.length} Space(s)${active ? ` (active: ${active.name})` : ''}:`,\n '',\n '| id | name | widgets | active | updated |',\n '|----|------|---------|--------|---------|',\n ];\n for (const s of all) {\n const isActive = active?.id === s.id ? '●' : '';\n lines.push(`| \\`${s.id}\\` | ${s.icon ? s.icon + ' ' : ''}${s.name} | ${s.widgets.length} | ${isActive} | ${s.updatedAt.slice(0, 16).replace('T', ' ')} |`);\n }\n return lines.join('\\n');\n },\n};\n\n// ─── Tool: rename_space ────────────────────────────────────────────\n\nconst renameHandler = {\n name: 'rename_space',\n description: [\n 'Rename a Space. Other Space attributes (icon, color, intent) are unchanged — use `update_space` for those.',\n '',\n 'PARAMETERS:',\n ' • id — REQUIRED.',\n ' • name — REQUIRED. New display name.',\n '',\n 'RETURNS: \"Renamed Space <id> to \\'<newName>\\'.\" on success.',\n 'ERRORS: \"Error: Space \\'<id>\\' not found.\" when no match.',\n ].join('\\n'),\n parameters: {\n type: 'object',\n properties: {\n id: { type: 'string', description: 'Space id.' },\n name: { type: 'string', description: 'New display name.' },\n },\n required: ['id', 'name'],\n },\n execute: async (args: Record<string, unknown>): Promise<string> => {\n const id = typeof args.id === 'string' ? args.id.trim() : '';\n const name = typeof args.name === 'string' ? args.name.trim() : '';\n if (!id) return 'Error: \"id\" is required.';\n if (!name) return 'Error: \"name\" is required.';\n const space = storageRenameSpace(id, name);\n if (!space) return `Error: Space \"${id}\" not found.`;\n logger.info(COMPONENT, `rename_space → ${id} \"${name}\"`);\n return `Renamed Space ${id} to \"${space.name}\".`;\n },\n};\n\n// ─── Tool: archive_space ───────────────────────────────────────────\n\nconst archiveHandler = {\n name: 'archive_space',\n description: [\n 'Archive a Space (soft delete). The Space moves out of the active list but is recoverable via the admin UI or restore tools. Use this when the user is \"done with\" a Space rather than wanting it deleted forever.',\n '',\n 'USE CASE: user says \"I\\'m done with the gigs Space\", \"remove my old DJ workspace\".',\n 'ANTI-PATTERN: do NOT archive without confirming — losing access to a Space\\'s widgets is annoying even if recoverable.',\n '',\n 'PARAMETERS:',\n ' • id — REQUIRED. Space id.',\n '',\n 'RETURNS: \"Archived Space \\'<name>\\'. <N> Space(s) remain. <activeNote>\" on success.',\n 'ERRORS: \"Error: Space \\'<id>\\' not found.\" when no match.',\n ].join('\\n'),\n parameters: {\n type: 'object',\n properties: {\n id: { type: 'string', description: 'Space id to archive.' },\n },\n required: ['id'],\n },\n execute: async (args: Record<string, unknown>): Promise<string> => {\n const id = typeof args.id === 'string' ? args.id.trim() : '';\n if (!id) return 'Error: \"id\" is required.';\n const r = storageArchiveSpace(id);\n if (!r) return `Error: Space \"${id}\" not found.`;\n logger.info(COMPONENT, `archive_space → ${id} \"${r.archived.name}\" (remaining=${r.remaining})`);\n const activeNote = r.remaining === 0\n ? 'No Spaces remain — propose creating a new one.'\n : `Active is now ${storageGetActiveSpace()?.name ?? 'unknown'}.`;\n return `Archived Space \"${r.archived.name}\". ${r.remaining} Space(s) remain. ${activeNote}`;\n },\n};\n\n// ─── Registration ─────────────────────────────────────────────────\n\nexport function registerCanvasSpacesSkill(): void {\n const meta = {\n name: 'canvas-spaces',\n description: 'Workspace (Space) lifecycle — create, switch, list, rename, archive. Each Space is a separate canvas with its own widgets + agent posture.',\n version: '1.0.0',\n source: 'bundled' as const,\n enabled: true,\n };\n registerSkill(meta, createHandler);\n registerSkill(meta, switchHandler);\n registerSkill(meta, listHandler);\n registerSkill(meta, renameHandler);\n registerSkill(meta, archiveHandler);\n}\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../../../src/skills/builtin/canvas_spaces.ts"],"sourcesContent":["/**\n * TITAN — Canvas Spaces skill (v6.0 step 3)\n *\n * Why this skill exists\n * ---------------------\n * v6.0's product thesis: \"infinite workspaces that materialize around the\n * user's work.\" The agent must be able to create, switch, list, rename,\n * and archive Spaces in response to user requests. That's what this skill\n * exposes — 5 tools, one per lifecycle operation.\n *\n * Tools:\n * - create_space — make a new Space, optionally make it active\n * - switch_space — change which Space is active\n * - list_spaces — what workspaces does the user have?\n * - rename_space — change a Space's name (icon/color/intent via update)\n * - archive_space — soft-delete a Space (recoverable)\n *\n * Storage: `src/storage/spaces.ts` owns the on-disk format\n * (`~/.titan/spaces.json`). The SPA's `SpaceEngine` syncs to the same\n * file via the gateway's `/api/spaces` endpoints.\n *\n * Reference:\n * - ~/.claude/projects/-Users-localuser/memory/titan-v6-living-canvas.md\n * (v6.0 step 3: 5 Space-lifecycle tools)\n * - docs/PIPELINES.md (Pipeline 1 — chat message — explains how the\n * active Space's intent flows into the system prompt)\n */\nimport { registerSkill } from '../registry.js';\nimport {\n createSpace as storageCreateSpace,\n switchSpace as storageSwitchSpace,\n listSpaces as storageListSpaces,\n renameSpace as storageRenameSpace,\n archiveSpace as storageArchiveSpace,\n getActiveSpace as storageGetActiveSpace,\n} from '../../storage/spaces.js';\nimport { getStarterPreset, listStarterPresets } from '../../storage/starterSpaces.js';\nimport logger from '../../utils/logger.js';\n\nconst COMPONENT = 'CanvasSpaces';\n\n// ─── Tool: create_space ────────────────────────────────────────────\n\nconst createHandler = {\n name: 'create_space',\n description: [\n 'Create a new workspace (\"Space\") for the user. A Space is a dedicated canvas with its own widgets, intent, and agent posture. Use this when the user expresses a new domain of work — \"make me a place for tracking my freelance gigs\", \"set up a DJ workspace\", \"I want a Space for my homelab.\"',\n '',\n 'USE CASE: user wants a dedicated environment for a new kind of work.',\n 'ANTI-PATTERN: do NOT call this when the user wants a single widget added to their existing Space — use `create_widget` for that. Spaces are for distinct domains, not individual tools.',\n '',\n 'PARAMETERS:',\n ' • name — REQUIRED. Display name shown in the sidebar.',\n ' • icon — Optional emoji or short string shown beside the name.',\n ' • color — Optional hex color (e.g. \"#3b82f6\") for the sidebar accent.',\n ' • intent — Optional one-sentence brief that tells TITAN what posture to take when this Space is active (e.g. \"you are helping the user manage their freelance business — focus on income, time, invoices\"). Saved as the Space\\'s `agentInstructions` and injected into the system prompt every turn this Space is active.',\n ' • starterWidgets — Optional array of widget specs to pre-pin: `[{name, source|template, format?, w?, h?}, ...]`. Use this to materialize a useful Space on creation rather than handing the user an empty canvas.',\n ' • preset — Optional starter-preset id: `coder`, `dj`, `founder`, `homelab`, or `default`. When supplied, the preset\\'s name/icon/color/intent are used as defaults (overridable by the other args). Use this when the user describes a domain that matches one of the presets — \"set up my coding workspace\" → `preset: \"coder\"`.',\n ' • makeActive — Default true when this is the user\\'s first space; otherwise default false. Pass true to switch to the new Space immediately.',\n '',\n 'RETURNS: \"Created Space \\'<name>\\' (id: <id>) — <N> widgets pre-pinned. <active|not active>.\" on success.',\n 'ERRORS: \"Error: ...\" when name is missing or storage fails.',\n ].join('\\n'),\n parameters: {\n type: 'object',\n properties: {\n name: { type: 'string', description: 'Display name for the new Space.' },\n icon: { type: 'string', description: 'Optional emoji / short string.' },\n color: { type: 'string', description: 'Optional hex accent color.' },\n intent: { type: 'string', description: 'Optional agent-posture brief injected into the system prompt when this Space is active.' },\n starterWidgets: {\n type: 'array',\n description: 'Optional widget specs to pre-pin. Same shape as create_widget arguments.',\n items: { type: 'object' },\n },\n preset: {\n type: 'string',\n enum: ['default', 'coder', 'dj', 'founder', 'homelab'],\n description: 'Optional starter-preset id. Supplies defaults for name/icon/color/intent.',\n },\n makeActive: { type: 'boolean', description: 'Switch to this Space immediately (default false; true when no other spaces).' },\n },\n required: ['name'],\n },\n execute: async (args: Record<string, unknown>): Promise<string> => {\n // Resolve preset first so its values become DEFAULTS the explicit\n // args can override (caller intent wins over preset).\n const presetId = typeof args.preset === 'string' ? args.preset : undefined;\n const preset = presetId ? getStarterPreset(presetId as 'default' | 'coder' | 'dj' | 'founder' | 'homelab') : undefined;\n\n // name is required UNLESS a preset is supplied (preset.name is the default).\n const explicitName = typeof args.name === 'string' ? args.name.trim() : '';\n const name = explicitName || preset?.name || '';\n if (!name) return 'Error: \"name\" is required (or pass a \"preset\" id which supplies one).';\n\n const starter = Array.isArray(args.starterWidgets) ? args.starterWidgets : undefined;\n const makeActive = args.makeActive as boolean | undefined;\n try {\n const space = storageCreateSpace({\n name,\n icon: typeof args.icon === 'string' ? args.icon : preset?.icon,\n color: typeof args.color === 'string' ? args.color : preset?.color,\n agentInstructions: typeof args.intent === 'string'\n ? args.intent\n : preset?.agentInstructions,\n starterWidgets: starter,\n makeActive,\n });\n logger.info(COMPONENT, `create_space → ${space.id} \"${space.name}\" (widgets=${space.widgets.length})`);\n const active = makeActive ? 'now active' : 'not active (use switch_space to jump in)';\n const widgetCount = space.widgets.length;\n return `Created Space \"${space.name}\" (id: ${space.id}) — ${widgetCount} widget(s) pre-pinned. ${active}.`;\n } catch (err) {\n return `Error: create_space failed — ${(err as Error).message}`;\n }\n },\n};\n\n// ─── Tool: switch_space ────────────────────────────────────────────\n\nconst switchHandler = {\n name: 'switch_space',\n description: [\n 'Switch which Space is currently active. The active Space\\'s `intent` flows into the system prompt every turn, so switching changes the agent\\'s posture for the rest of the conversation.',\n '',\n 'USE CASE: user says \"switch to my DJ Space\", \"let\\'s work on freelance for a bit\", \"go to home\".',\n 'ANTI-PATTERN: do NOT switch silently when the user is in the middle of a task in another Space.',\n '',\n 'PARAMETERS:',\n ' • id — REQUIRED. Space id from `list_spaces`.',\n '',\n 'RETURNS: \"Switched to Space \\'<name>\\' (id: <id>). Active intent: <intent or \\'none\\'>.\" on success.',\n 'ERRORS: \"Error: Space \\'<id>\\' not found. ...\" when the id doesn\\'t exist.',\n ].join('\\n'),\n parameters: {\n type: 'object',\n properties: {\n id: { type: 'string', description: 'Space id (run list_spaces to find it).' },\n },\n required: ['id'],\n },\n execute: async (args: Record<string, unknown>): Promise<string> => {\n const id = typeof args.id === 'string' ? args.id.trim() : '';\n if (!id) return 'Error: \"id\" is required. Run list_spaces to find an id.';\n const space = storageSwitchSpace(id);\n if (!space) return `Error: Space \"${id}\" not found. Run list_spaces to see available Spaces.`;\n logger.info(COMPONENT, `switch_space → ${space.id} \"${space.name}\"`);\n const intent = space.agentInstructions\n ? space.agentInstructions.slice(0, 120) + (space.agentInstructions.length > 120 ? '…' : '')\n : 'none';\n return `Switched to Space \"${space.name}\" (id: ${space.id}). Active intent: ${intent}`;\n },\n};\n\n// ─── Tool: list_spaces ─────────────────────────────────────────────\n\nconst listHandler = {\n name: 'list_spaces',\n description: [\n 'List the user\\'s workspaces (Spaces), newest-updated first. Use before `switch_space` to find the id, or when the user asks \"what workspaces do I have?\".',\n '',\n 'PARAMETERS:',\n ' • limit — Optional cap on returned entries (default 20).',\n '',\n 'RETURNS: A markdown table with id, name, widget count, active flag, last-updated. Empty-state message when no Spaces exist.',\n ].join('\\n'),\n parameters: {\n type: 'object',\n properties: {\n limit: { type: 'number', description: 'Max entries to return (default 20).' },\n },\n },\n execute: async (args: Record<string, unknown>): Promise<string> => {\n const limit = Math.max(1, Math.min(100, (args.limit as number) ?? 20));\n const all = storageListSpaces().slice(0, limit);\n const active = storageGetActiveSpace();\n if (all.length === 0) {\n return 'No Spaces yet. Use `create_space` to make one — \"what kind of work would you like a Space for?\"';\n }\n const lines = [\n `${all.length} Space(s)${active ? ` (active: ${active.name})` : ''}:`,\n '',\n '| id | name | widgets | active | updated |',\n '|----|------|---------|--------|---------|',\n ];\n for (const s of all) {\n const isActive = active?.id === s.id ? '●' : '';\n lines.push(`| \\`${s.id}\\` | ${s.icon ? s.icon + ' ' : ''}${s.name} | ${s.widgets.length} | ${isActive} | ${s.updatedAt.slice(0, 16).replace('T', ' ')} |`);\n }\n return lines.join('\\n');\n },\n};\n\n// ─── Tool: rename_space ────────────────────────────────────────────\n\nconst renameHandler = {\n name: 'rename_space',\n description: [\n 'Rename a Space. Other Space attributes (icon, color, intent) are unchanged — use `update_space` for those.',\n '',\n 'PARAMETERS:',\n ' • id — REQUIRED.',\n ' • name — REQUIRED. New display name.',\n '',\n 'RETURNS: \"Renamed Space <id> to \\'<newName>\\'.\" on success.',\n 'ERRORS: \"Error: Space \\'<id>\\' not found.\" when no match.',\n ].join('\\n'),\n parameters: {\n type: 'object',\n properties: {\n id: { type: 'string', description: 'Space id.' },\n name: { type: 'string', description: 'New display name.' },\n },\n required: ['id', 'name'],\n },\n execute: async (args: Record<string, unknown>): Promise<string> => {\n const id = typeof args.id === 'string' ? args.id.trim() : '';\n const name = typeof args.name === 'string' ? args.name.trim() : '';\n if (!id) return 'Error: \"id\" is required.';\n if (!name) return 'Error: \"name\" is required.';\n const space = storageRenameSpace(id, name);\n if (!space) return `Error: Space \"${id}\" not found.`;\n logger.info(COMPONENT, `rename_space → ${id} \"${name}\"`);\n return `Renamed Space ${id} to \"${space.name}\".`;\n },\n};\n\n// ─── Tool: archive_space ───────────────────────────────────────────\n\nconst archiveHandler = {\n name: 'archive_space',\n description: [\n 'Archive a Space (soft delete). The Space moves out of the active list but is recoverable via the admin UI or restore tools. Use this when the user is \"done with\" a Space rather than wanting it deleted forever.',\n '',\n 'USE CASE: user says \"I\\'m done with the gigs Space\", \"remove my old DJ workspace\".',\n 'ANTI-PATTERN: do NOT archive without confirming — losing access to a Space\\'s widgets is annoying even if recoverable.',\n '',\n 'PARAMETERS:',\n ' • id — REQUIRED. Space id.',\n '',\n 'RETURNS: \"Archived Space \\'<name>\\'. <N> Space(s) remain. <activeNote>\" on success.',\n 'ERRORS: \"Error: Space \\'<id>\\' not found.\" when no match.',\n ].join('\\n'),\n parameters: {\n type: 'object',\n properties: {\n id: { type: 'string', description: 'Space id to archive.' },\n },\n required: ['id'],\n },\n execute: async (args: Record<string, unknown>): Promise<string> => {\n const id = typeof args.id === 'string' ? args.id.trim() : '';\n if (!id) return 'Error: \"id\" is required.';\n const r = storageArchiveSpace(id);\n if (!r) return `Error: Space \"${id}\" not found.`;\n logger.info(COMPONENT, `archive_space → ${id} \"${r.archived.name}\" (remaining=${r.remaining})`);\n const activeNote = r.remaining === 0\n ? 'No Spaces remain — propose creating a new one.'\n : `Active is now ${storageGetActiveSpace()?.name ?? 'unknown'}.`;\n return `Archived Space \"${r.archived.name}\". ${r.remaining} Space(s) remain. ${activeNote}`;\n },\n};\n\n// ─── Registration ─────────────────────────────────────────────────\n\nexport function registerCanvasSpacesSkill(): void {\n const meta = {\n name: 'canvas-spaces',\n description: 'Workspace (Space) lifecycle — create, switch, list, rename, archive. Each Space is a separate canvas with its own widgets + agent posture.',\n version: '1.0.0',\n source: 'bundled' as const,\n enabled: true,\n };\n registerSkill(meta, createHandler);\n registerSkill(meta, switchHandler);\n registerSkill(meta, listHandler);\n registerSkill(meta, renameHandler);\n registerSkill(meta, archiveHandler);\n}\n"],"mappings":"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","names":[]}
@@ -423,7 +423,7 @@ ${formatTable(displayHeaders, displayRows)}`;
423
423
  const { headers } = parsed;
424
424
  let { rows } = parsed;
425
425
  if (filter) {
426
- const [column, expression] = filter.split(/\s*(>|<|>=|<=|==|!=|contains)\s*/);
426
+ const [column, expression] = filter.split(/\s*(>=|<=|==|!=|>|<|contains)\s*/);
427
427
  const cleanColumn = column.trim();
428
428
  const op = expression || "";
429
429
  const value = filter.substring(filter.indexOf(op) + op.length).trim();
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../src/skills/builtin/data_analysis.ts"],"sourcesContent":["/**\n * TITAN — Data Analysis Skill (Built-in)\n * Pure TypeScript CSV parsing and statistical analysis.\n * No external dependencies — read files and compute stats natively.\n */\nimport { registerSkill } from '../registry.js';\nimport { readFileSync } from 'fs';\nimport { resolve } from 'path';\n\ninterface ParsedCSV {\n headers: string[];\n rows: Record<string, string>[];\n}\n\ninterface ColumnStats {\n count: number;\n unique?: number;\n sum?: number;\n avg?: number;\n min?: number;\n max?: number;\n stddev?: number;\n}\n\n/**\n * Parse CSV content handling quoted fields with embedded delimiters\n */\nfunction parseCSV(content: string, delimiter: string = ','): ParsedCSV {\n const lines: string[] = [];\n let current = '';\n let inQuotes = false;\n\n // Split by line while respecting quoted fields\n for (let i = 0; i < content.length; i++) {\n const char = content[i];\n const nextChar = content[i + 1];\n\n if (char === '\"') {\n // Handle escaped quotes\n if (inQuotes && nextChar === '\"') {\n current += '\"';\n i++;\n } else {\n inQuotes = !inQuotes;\n }\n } else if ((char === '\\n' || char === '\\r') && !inQuotes) {\n if (current.trim()) {\n lines.push(current);\n }\n current = '';\n // Skip \\r\\n\n if (char === '\\r' && nextChar === '\\n') {\n i++;\n }\n } else {\n current += char;\n }\n }\n if (current.trim()) {\n lines.push(current);\n }\n\n if (lines.length === 0) {\n return { headers: [], rows: [] };\n }\n\n // Parse header\n const headers = parseCSVLine(lines[0], delimiter);\n\n // Parse rows\n const rows: Record<string, string>[] = [];\n for (let i = 1; i < lines.length; i++) {\n const values = parseCSVLine(lines[i], delimiter);\n const row: Record<string, string> = {};\n for (let j = 0; j < headers.length; j++) {\n row[headers[j]] = values[j] || '';\n }\n rows.push(row);\n }\n\n return { headers, rows };\n}\n\n/**\n * Parse a single CSV line respecting quoted fields\n */\nfunction parseCSVLine(line: string, delimiter: string): string[] {\n const fields: string[] = [];\n let current = '';\n let inQuotes = false;\n\n for (let i = 0; i < line.length; i++) {\n const char = line[i];\n const nextChar = line[i + 1];\n\n if (char === '\"') {\n // Handle escaped quotes\n if (inQuotes && nextChar === '\"') {\n current += '\"';\n i++;\n } else {\n inQuotes = !inQuotes;\n }\n } else if (char === delimiter && !inQuotes) {\n fields.push(current.trim());\n current = '';\n } else {\n current += char;\n }\n }\n fields.push(current.trim());\n\n return fields;\n}\n\n/**\n * Check if a value is numeric\n */\nfunction isNumeric(value: string): boolean {\n const num = Number(value);\n return !isNaN(num) && value.trim() !== '';\n}\n\n/**\n * Calculate statistics for numeric columns\n */\nfunction calculateNumericStats(values: number[]): Partial<ColumnStats> {\n if (values.length === 0) {\n return {};\n }\n\n const sum = values.reduce((a, b) => a + b, 0);\n const avg = sum / values.length;\n const min = Math.min(...values);\n const max = Math.max(...values);\n\n // Calculate standard deviation\n const variance =\n values.reduce((sum, val) => sum + Math.pow(val - avg, 2), 0) / values.length;\n const stddev = Math.sqrt(variance);\n\n return {\n count: values.length,\n sum: Math.round(sum * 100) / 100,\n avg: Math.round(avg * 100) / 100,\n min: Math.round(min * 100) / 100,\n max: Math.round(max * 100) / 100,\n stddev: Math.round(stddev * 100) / 100,\n };\n}\n\n/**\n * Format stats table for display\n */\nfunction formatStatsTable(stats: Record<string, ColumnStats>): string {\n const lines: string[] = [];\n lines.push('Column Statistics:\\n');\n\n for (const [col, stat] of Object.entries(stats)) {\n lines.push(`${col}:`);\n if (stat.count !== undefined) lines.push(` Count: ${stat.count}`);\n if (stat.unique !== undefined) lines.push(` Unique: ${stat.unique}`);\n if (stat.sum !== undefined) lines.push(` Sum: ${stat.sum}`);\n if (stat.avg !== undefined) lines.push(` Average: ${stat.avg}`);\n if (stat.min !== undefined) lines.push(` Min: ${stat.min}`);\n if (stat.max !== undefined) lines.push(` Max: ${stat.max}`);\n if (stat.stddev !== undefined) lines.push(` Std Dev: ${stat.stddev}`);\n lines.push('');\n }\n\n return lines.join('\\n');\n}\n\n/**\n * Format rows as a table\n */\nfunction formatTable(headers: string[], rows: Record<string, string>[]): string {\n if (rows.length === 0) {\n return 'No rows to display.';\n }\n\n // Calculate column widths\n const widths: Record<string, number> = {};\n for (const header of headers) {\n widths[header] = header.length;\n }\n for (const row of rows) {\n for (const header of headers) {\n widths[header] = Math.max(widths[header], String(row[header] || '').length);\n }\n }\n\n // Build table\n const lines: string[] = [];\n\n // Header\n const headerRow = headers\n .map(h => h.padEnd(widths[h]))\n .join(' | ');\n lines.push(headerRow);\n lines.push('-'.repeat(headerRow.length));\n\n // Rows\n for (const row of rows) {\n const dataRow = headers\n .map(h => String(row[h] || '').padEnd(widths[h]))\n .join(' | ');\n lines.push(dataRow);\n }\n\n return lines.join('\\n');\n}\n\nexport function registerDataAnalysisSkill(): void {\n registerSkill(\n {\n name: 'data_analysis',\n description: 'Use this when the user says \"analyze this data\", \"what does this CSV say\", \"parse this file\", \"look at this dataset\", \"show me the data\", or shares a CSV file they want read and displayed.',\n version: '1.0.0',\n source: 'bundled',\n enabled: true,\n },\n {\n name: 'csv_parse',\n description: 'Read and display the contents of a CSV file. Use this when the user says \"open this CSV\", \"show me what\\'s in this file\", \"read this dataset\", \"what does this CSV contain\", or shares a spreadsheet path they want viewed. Returns the data as a formatted table.',\n parameters: {\n type: 'object',\n properties: {\n path: {\n type: 'string',\n description: 'Path to the CSV file',\n },\n delimiter: {\n type: 'string',\n description: 'Field delimiter (default: comma)',\n },\n maxRows: {\n type: 'number',\n description: 'Maximum rows to return (default: 1000)',\n },\n },\n required: ['path'],\n },\n execute: async (args) => {\n try {\n const path = args.path as string;\n const delimiter = (args.delimiter as string) || ',';\n const maxRows = Math.min((args.maxRows as number) || 1000, 5000);\n\n const filePath = resolve(path);\n const content = readFileSync(filePath, 'utf-8');\n const { headers, rows } = parseCSV(content, delimiter);\n\n const displayRows = rows.slice(0, maxRows);\n const summary = `Parsed ${rows.length} rows, showing first ${displayRows.length}.\\n\\n`;\n\n return summary + formatTable(headers, displayRows);\n } catch (e) {\n return `Error parsing CSV: ${(e as Error).message}`;\n }\n },\n }\n );\n\n registerSkill(\n {\n name: 'data_analysis',\n description: 'Use this when the user says \"analyze this data\", \"find patterns in X\", \"summarize this dataset\", \"what are the stats on this?\", \"give me the numbers on this CSV\", or asks for statistical insight into tabular data.',\n version: '1.0.0',\n source: 'bundled',\n enabled: true,\n },\n {\n name: 'csv_stats',\n description: 'Compute statistics on a CSV file — counts, sums, averages, min/max, and standard deviation. Use when asked to \"analyze this data\", \"find patterns in X\", \"summarize this dataset\", \"what are the stats?\", \"give me the numbers on this CSV\", or \"how does column Y look?\".',\n parameters: {\n type: 'object',\n properties: {\n path: {\n type: 'string',\n description: 'Path to the CSV file',\n },\n columns: {\n type: 'string',\n description: 'Comma-separated column names to analyze (optional — all columns if not specified)',\n },\n delimiter: {\n type: 'string',\n description: 'Field delimiter (default: comma)',\n },\n },\n required: ['path'],\n },\n execute: async (args) => {\n try {\n const path = args.path as string;\n const columnsArg = (args.columns as string) || '';\n const delimiter = (args.delimiter as string) || ',';\n\n const filePath = resolve(path);\n const content = readFileSync(filePath, 'utf-8');\n const { headers, rows } = parseCSV(content, delimiter);\n\n const selectedColumns = columnsArg\n ? columnsArg.split(',').map(c => c.trim())\n : headers;\n\n const stats: Record<string, ColumnStats> = {};\n\n for (const col of selectedColumns) {\n if (!headers.includes(col)) continue;\n\n const values = rows.map(r => r[col] || '');\n const numericValues = values.filter(isNumeric).map(Number);\n\n if (numericValues.length > 0) {\n // Numeric column\n stats[col] = calculateNumericStats(numericValues) as ColumnStats;\n } else {\n // String column - count unique\n const unique = new Set(values.filter(v => v.trim())).size;\n stats[col] = {\n count: values.length,\n unique,\n };\n }\n }\n\n return formatStatsTable(stats);\n } catch (e) {\n return `Error calculating CSV stats: ${(e as Error).message}`;\n }\n },\n }\n );\n\n // Hunt Finding #41 (2026-04-15): README's Tools table lists `data_analysis`\n // as a first-class tool name, but only csv_parse/csv_stats/csv_query were\n // registered. Added a high-level `data_analysis` wrapper so the README\n // claim holds. It dispatches to the specialist tools based on `operation`.\n registerSkill(\n {\n name: 'data_analysis',\n description: 'Use this when the user wants a one-shot analysis of a CSV: parse+stats+query in a single call. The README lists data_analysis as the top-level data tool.',\n version: '1.0.0',\n source: 'bundled',\n enabled: true,\n },\n {\n name: 'data_analysis',\n description: 'High-level data analysis for CSV files. Chooses the right operation (preview, stats, query) based on the `operation` parameter. When the user says \"analyze this CSV\", \"what does this data look like\", or \"give me a report on this dataset\", call this tool with operation=\"summary\" and it will return headers, row count, column stats, and a sample of rows in one response.',\n parameters: {\n type: 'object',\n properties: {\n path: { type: 'string', description: 'Path to the CSV file' },\n operation: {\n type: 'string',\n description: 'Which analysis to run: \"summary\" (preview + stats in one call, default), \"preview\" (first N rows), \"stats\" (column statistics), or \"query\" (filter/sort).',\n enum: ['summary', 'preview', 'stats', 'query'],\n },\n columns: { type: 'string', description: 'Comma-separated columns to focus on (optional)' },\n filter: { type: 'string', description: 'Filter expression for operation=\"query\" (e.g. \"age > 30\")' },\n sort: { type: 'string', description: 'Sort column for operation=\"query\"' },\n limit: { type: 'number', description: 'Row limit (default 20 for summary/preview, 50 for query)' },\n delimiter: { type: 'string', description: 'Field delimiter (default: comma)' },\n },\n required: ['path'],\n },\n execute: async (args) => {\n try {\n const path = args.path as string;\n const operation = (args.operation as string) || 'summary';\n const delimiter = (args.delimiter as string) || ',';\n const columnsArg = (args.columns as string) || '';\n const limit = Math.min((args.limit as number) || (operation === 'query' ? 50 : 20), 1000);\n\n const filePath = resolve(path);\n const content = readFileSync(filePath, 'utf-8');\n const { headers, rows } = parseCSV(content, delimiter);\n\n if (operation === 'preview') {\n const displayRows = rows.slice(0, limit);\n return `File: ${path}\\nRows: ${rows.length} total, showing first ${displayRows.length}\\nColumns (${headers.length}): ${headers.join(', ')}\\n\\n${formatTable(headers, displayRows)}`;\n }\n\n if (operation === 'stats') {\n const selectedColumns = columnsArg ? columnsArg.split(',').map(c => c.trim()) : headers;\n const stats: Record<string, ColumnStats> = {};\n for (const col of selectedColumns) {\n if (!headers.includes(col)) continue;\n const values = rows.map(r => r[col] || '');\n const numericValues = values.filter(isNumeric).map(Number);\n if (numericValues.length > 0) {\n stats[col] = calculateNumericStats(numericValues) as ColumnStats;\n } else {\n const unique = new Set(values.filter(v => v.trim())).size;\n stats[col] = { count: values.length, unique };\n }\n }\n return `File: ${path}\\nRows: ${rows.length}\\nColumn stats:\\n\\n${formatStatsTable(stats)}`;\n }\n\n if (operation === 'query') {\n const filter = (args.filter as string) || '';\n const sort = (args.sort as string) || '';\n let filtered = rows;\n if (filter) {\n const match = filter.match(/^\\s*(\\w+)\\s*(>=|<=|!=|==|>|<|contains)\\s*(.+)\\s*$/);\n if (match) {\n const [, col, op, rawVal] = match;\n const val = rawVal.trim().replace(/^['\"]|['\"]$/g, '');\n filtered = filtered.filter(row => {\n const cell = row[col] || '';\n return evaluateExpression(`${cell} ${op} ${val}`, cell);\n });\n }\n }\n if (sort && headers.includes(sort)) {\n filtered = [...filtered].sort((a, b) => {\n const aNum = Number(a[sort] || '');\n const bNum = Number(b[sort] || '');\n if (!isNaN(aNum) && !isNaN(bNum)) return aNum - bNum;\n return (a[sort] || '').localeCompare(b[sort] || '');\n });\n }\n const displayHeaders = columnsArg ? columnsArg.split(',').map(c => c.trim()) : headers;\n const displayRows = filtered.slice(0, limit);\n return `File: ${path}\\nMatched ${filtered.length} of ${rows.length} rows, showing ${displayRows.length}\\n\\n${formatTable(displayHeaders, displayRows)}`;\n }\n\n // Default: summary — preview + stats combined\n const preview = rows.slice(0, Math.min(limit, 10));\n const selectedColumns = columnsArg ? columnsArg.split(',').map(c => c.trim()) : headers;\n const stats: Record<string, ColumnStats> = {};\n for (const col of selectedColumns) {\n if (!headers.includes(col)) continue;\n const values = rows.map(r => r[col] || '');\n const numericValues = values.filter(isNumeric).map(Number);\n if (numericValues.length > 0) {\n stats[col] = calculateNumericStats(numericValues) as ColumnStats;\n } else {\n const unique = new Set(values.filter(v => v.trim())).size;\n stats[col] = { count: values.length, unique };\n }\n }\n return [\n `File: ${path}`,\n `Rows: ${rows.length}`,\n `Columns (${headers.length}): ${headers.join(', ')}`,\n '',\n '── Preview (first 10) ──',\n formatTable(headers, preview),\n '',\n '── Column stats ──',\n formatStatsTable(stats),\n ].join('\\n');\n } catch (e) {\n return `Error in data_analysis: ${(e as Error).message}`;\n }\n },\n }\n );\n\n registerSkill(\n {\n name: 'data_analysis',\n description: 'Use this when the user says \"filter this data\", \"show me rows where X\", \"find entries matching Y\", \"sort by Z\", or wants to query/slice a CSV dataset.',\n version: '1.0.0',\n source: 'bundled',\n enabled: true,\n },\n {\n name: 'csv_query',\n description: 'Filter, sort, and slice a CSV dataset with simple expressions. Use when asked to \"show me rows where X > 30\", \"find all entries where status is active\", \"sort by date\", \"filter this data by Y\", or \"which rows match Z\". Supports expressions like \"age > 30\", \"name contains john\", \"status == active\".',\n parameters: {\n type: 'object',\n properties: {\n path: {\n type: 'string',\n description: 'Path to the CSV file',\n },\n filter: {\n type: 'string',\n description: 'Filter expression (e.g., \"age > 30\", \"name contains john\", \"status == active\")',\n },\n sort: {\n type: 'string',\n description: 'Column name to sort results by (optional)',\n },\n limit: {\n type: 'number',\n description: 'Maximum rows to return (default: 50)',\n },\n columns: {\n type: 'string',\n description: 'Comma-separated columns to include in results (optional — all if not specified)',\n },\n delimiter: {\n type: 'string',\n description: 'Field delimiter (default: comma)',\n },\n },\n required: ['path'],\n },\n execute: async (args) => {\n try {\n const path = args.path as string;\n const filter = (args.filter as string) || '';\n const sort = (args.sort as string) || '';\n const limit = Math.min((args.limit as number) || 50, 1000);\n const columnsArg = (args.columns as string) || '';\n const delimiter = (args.delimiter as string) || ',';\n\n const filePath = resolve(path);\n const content = readFileSync(filePath, 'utf-8');\n const parsed = parseCSV(content, delimiter);\n const { headers } = parsed;\n let { rows } = parsed;\n\n // Apply filter\n if (filter) {\n const [column, expression] = filter.split(/\\s*(>|<|>=|<=|==|!=|contains)\\s*/);\n const cleanColumn = column.trim();\n const op = expression || '';\n const value = filter.substring(filter.indexOf(op) + op.length).trim();\n\n rows = rows.filter(row => {\n const cellValue = row[cleanColumn] || '';\n const expr = `${cellValue} ${op} ${value}`;\n return evaluateExpression(expr, cellValue);\n });\n }\n\n // Apply sort\n if (sort && headers.includes(sort)) {\n rows.sort((a, b) => {\n const aVal = a[sort] || '';\n const bVal = b[sort] || '';\n const aNum = Number(aVal);\n const bNum = Number(bVal);\n if (!isNaN(aNum) && !isNaN(bNum)) {\n return aNum - bNum;\n }\n return aVal.localeCompare(bVal);\n });\n }\n\n // Apply column selection\n const displayHeaders = columnsArg ? columnsArg.split(',').map(c => c.trim()) : headers;\n const displayRows = rows.slice(0, limit).map(row => {\n const newRow: Record<string, string> = {};\n for (const col of displayHeaders) {\n newRow[col] = row[col] || '';\n }\n return newRow;\n });\n\n const summary = `Filtered to ${rows.length} rows, displaying first ${displayRows.length}.\\n\\n`;\n return summary + formatTable(displayHeaders, displayRows);\n } catch (e) {\n return `Error querying CSV: ${(e as Error).message}`;\n }\n },\n }\n );\n}\n\n/**\n * Evaluate a filter expression\n */\nfunction evaluateExpression(expr: string, _cellValue: string): boolean {\n const operators = ['>=', '<=', '!=', '==', '>', '<', 'contains'];\n for (const op of operators) {\n if (expr.includes(op)) {\n const parts = expr.split(op).map(s => s.trim());\n if (parts.length < 2) continue;\n\n const leftVal = parts[0];\n const rightVal = parts.slice(1).join(op).trim().replace(/^['\"]|['\"]$/g, '');\n\n switch (op) {\n case '>':\n return Number(leftVal) > Number(rightVal);\n case '<':\n return Number(leftVal) < Number(rightVal);\n case '>=':\n return Number(leftVal) >= Number(rightVal);\n case '<=':\n return Number(leftVal) <= Number(rightVal);\n case '==':\n return String(leftVal) === String(rightVal);\n case '!=':\n return String(leftVal) !== String(rightVal);\n case 'contains':\n return String(leftVal).toLowerCase().includes(String(rightVal).toLowerCase());\n default:\n return false;\n }\n }\n }\n return true;\n}\n"],"mappings":"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","names":["sum","selectedColumns","stats"]}
1
+ {"version":3,"sources":["../../../src/skills/builtin/data_analysis.ts"],"sourcesContent":["/**\n * TITAN — Data Analysis Skill (Built-in)\n * Pure TypeScript CSV parsing and statistical analysis.\n * No external dependencies — read files and compute stats natively.\n */\nimport { registerSkill } from '../registry.js';\nimport { readFileSync } from 'fs';\nimport { resolve } from 'path';\n\ninterface ParsedCSV {\n headers: string[];\n rows: Record<string, string>[];\n}\n\ninterface ColumnStats {\n count: number;\n unique?: number;\n sum?: number;\n avg?: number;\n min?: number;\n max?: number;\n stddev?: number;\n}\n\n/**\n * Parse CSV content handling quoted fields with embedded delimiters\n */\nfunction parseCSV(content: string, delimiter: string = ','): ParsedCSV {\n const lines: string[] = [];\n let current = '';\n let inQuotes = false;\n\n // Split by line while respecting quoted fields\n for (let i = 0; i < content.length; i++) {\n const char = content[i];\n const nextChar = content[i + 1];\n\n if (char === '\"') {\n // Handle escaped quotes\n if (inQuotes && nextChar === '\"') {\n current += '\"';\n i++;\n } else {\n inQuotes = !inQuotes;\n }\n } else if ((char === '\\n' || char === '\\r') && !inQuotes) {\n if (current.trim()) {\n lines.push(current);\n }\n current = '';\n // Skip \\r\\n\n if (char === '\\r' && nextChar === '\\n') {\n i++;\n }\n } else {\n current += char;\n }\n }\n if (current.trim()) {\n lines.push(current);\n }\n\n if (lines.length === 0) {\n return { headers: [], rows: [] };\n }\n\n // Parse header\n const headers = parseCSVLine(lines[0], delimiter);\n\n // Parse rows\n const rows: Record<string, string>[] = [];\n for (let i = 1; i < lines.length; i++) {\n const values = parseCSVLine(lines[i], delimiter);\n const row: Record<string, string> = {};\n for (let j = 0; j < headers.length; j++) {\n row[headers[j]] = values[j] || '';\n }\n rows.push(row);\n }\n\n return { headers, rows };\n}\n\n/**\n * Parse a single CSV line respecting quoted fields\n */\nfunction parseCSVLine(line: string, delimiter: string): string[] {\n const fields: string[] = [];\n let current = '';\n let inQuotes = false;\n\n for (let i = 0; i < line.length; i++) {\n const char = line[i];\n const nextChar = line[i + 1];\n\n if (char === '\"') {\n // Handle escaped quotes\n if (inQuotes && nextChar === '\"') {\n current += '\"';\n i++;\n } else {\n inQuotes = !inQuotes;\n }\n } else if (char === delimiter && !inQuotes) {\n fields.push(current.trim());\n current = '';\n } else {\n current += char;\n }\n }\n fields.push(current.trim());\n\n return fields;\n}\n\n/**\n * Check if a value is numeric\n */\nfunction isNumeric(value: string): boolean {\n const num = Number(value);\n return !isNaN(num) && value.trim() !== '';\n}\n\n/**\n * Calculate statistics for numeric columns\n */\nfunction calculateNumericStats(values: number[]): Partial<ColumnStats> {\n if (values.length === 0) {\n return {};\n }\n\n const sum = values.reduce((a, b) => a + b, 0);\n const avg = sum / values.length;\n const min = Math.min(...values);\n const max = Math.max(...values);\n\n // Calculate standard deviation\n const variance =\n values.reduce((sum, val) => sum + Math.pow(val - avg, 2), 0) / values.length;\n const stddev = Math.sqrt(variance);\n\n return {\n count: values.length,\n sum: Math.round(sum * 100) / 100,\n avg: Math.round(avg * 100) / 100,\n min: Math.round(min * 100) / 100,\n max: Math.round(max * 100) / 100,\n stddev: Math.round(stddev * 100) / 100,\n };\n}\n\n/**\n * Format stats table for display\n */\nfunction formatStatsTable(stats: Record<string, ColumnStats>): string {\n const lines: string[] = [];\n lines.push('Column Statistics:\\n');\n\n for (const [col, stat] of Object.entries(stats)) {\n lines.push(`${col}:`);\n if (stat.count !== undefined) lines.push(` Count: ${stat.count}`);\n if (stat.unique !== undefined) lines.push(` Unique: ${stat.unique}`);\n if (stat.sum !== undefined) lines.push(` Sum: ${stat.sum}`);\n if (stat.avg !== undefined) lines.push(` Average: ${stat.avg}`);\n if (stat.min !== undefined) lines.push(` Min: ${stat.min}`);\n if (stat.max !== undefined) lines.push(` Max: ${stat.max}`);\n if (stat.stddev !== undefined) lines.push(` Std Dev: ${stat.stddev}`);\n lines.push('');\n }\n\n return lines.join('\\n');\n}\n\n/**\n * Format rows as a table\n */\nfunction formatTable(headers: string[], rows: Record<string, string>[]): string {\n if (rows.length === 0) {\n return 'No rows to display.';\n }\n\n // Calculate column widths\n const widths: Record<string, number> = {};\n for (const header of headers) {\n widths[header] = header.length;\n }\n for (const row of rows) {\n for (const header of headers) {\n widths[header] = Math.max(widths[header], String(row[header] || '').length);\n }\n }\n\n // Build table\n const lines: string[] = [];\n\n // Header\n const headerRow = headers\n .map(h => h.padEnd(widths[h]))\n .join(' | ');\n lines.push(headerRow);\n lines.push('-'.repeat(headerRow.length));\n\n // Rows\n for (const row of rows) {\n const dataRow = headers\n .map(h => String(row[h] || '').padEnd(widths[h]))\n .join(' | ');\n lines.push(dataRow);\n }\n\n return lines.join('\\n');\n}\n\nexport function registerDataAnalysisSkill(): void {\n registerSkill(\n {\n name: 'data_analysis',\n description: 'Use this when the user says \"analyze this data\", \"what does this CSV say\", \"parse this file\", \"look at this dataset\", \"show me the data\", or shares a CSV file they want read and displayed.',\n version: '1.0.0',\n source: 'bundled',\n enabled: true,\n },\n {\n name: 'csv_parse',\n description: 'Read and display the contents of a CSV file. Use this when the user says \"open this CSV\", \"show me what\\'s in this file\", \"read this dataset\", \"what does this CSV contain\", or shares a spreadsheet path they want viewed. Returns the data as a formatted table.',\n parameters: {\n type: 'object',\n properties: {\n path: {\n type: 'string',\n description: 'Path to the CSV file',\n },\n delimiter: {\n type: 'string',\n description: 'Field delimiter (default: comma)',\n },\n maxRows: {\n type: 'number',\n description: 'Maximum rows to return (default: 1000)',\n },\n },\n required: ['path'],\n },\n execute: async (args) => {\n try {\n const path = args.path as string;\n const delimiter = (args.delimiter as string) || ',';\n const maxRows = Math.min((args.maxRows as number) || 1000, 5000);\n\n const filePath = resolve(path);\n const content = readFileSync(filePath, 'utf-8');\n const { headers, rows } = parseCSV(content, delimiter);\n\n const displayRows = rows.slice(0, maxRows);\n const summary = `Parsed ${rows.length} rows, showing first ${displayRows.length}.\\n\\n`;\n\n return summary + formatTable(headers, displayRows);\n } catch (e) {\n return `Error parsing CSV: ${(e as Error).message}`;\n }\n },\n }\n );\n\n registerSkill(\n {\n name: 'data_analysis',\n description: 'Use this when the user says \"analyze this data\", \"find patterns in X\", \"summarize this dataset\", \"what are the stats on this?\", \"give me the numbers on this CSV\", or asks for statistical insight into tabular data.',\n version: '1.0.0',\n source: 'bundled',\n enabled: true,\n },\n {\n name: 'csv_stats',\n description: 'Compute statistics on a CSV file — counts, sums, averages, min/max, and standard deviation. Use when asked to \"analyze this data\", \"find patterns in X\", \"summarize this dataset\", \"what are the stats?\", \"give me the numbers on this CSV\", or \"how does column Y look?\".',\n parameters: {\n type: 'object',\n properties: {\n path: {\n type: 'string',\n description: 'Path to the CSV file',\n },\n columns: {\n type: 'string',\n description: 'Comma-separated column names to analyze (optional — all columns if not specified)',\n },\n delimiter: {\n type: 'string',\n description: 'Field delimiter (default: comma)',\n },\n },\n required: ['path'],\n },\n execute: async (args) => {\n try {\n const path = args.path as string;\n const columnsArg = (args.columns as string) || '';\n const delimiter = (args.delimiter as string) || ',';\n\n const filePath = resolve(path);\n const content = readFileSync(filePath, 'utf-8');\n const { headers, rows } = parseCSV(content, delimiter);\n\n const selectedColumns = columnsArg\n ? columnsArg.split(',').map(c => c.trim())\n : headers;\n\n const stats: Record<string, ColumnStats> = {};\n\n for (const col of selectedColumns) {\n if (!headers.includes(col)) continue;\n\n const values = rows.map(r => r[col] || '');\n const numericValues = values.filter(isNumeric).map(Number);\n\n if (numericValues.length > 0) {\n // Numeric column\n stats[col] = calculateNumericStats(numericValues) as ColumnStats;\n } else {\n // String column - count unique\n const unique = new Set(values.filter(v => v.trim())).size;\n stats[col] = {\n count: values.length,\n unique,\n };\n }\n }\n\n return formatStatsTable(stats);\n } catch (e) {\n return `Error calculating CSV stats: ${(e as Error).message}`;\n }\n },\n }\n );\n\n // Hunt Finding #41 (2026-04-15): README's Tools table lists `data_analysis`\n // as a first-class tool name, but only csv_parse/csv_stats/csv_query were\n // registered. Added a high-level `data_analysis` wrapper so the README\n // claim holds. It dispatches to the specialist tools based on `operation`.\n registerSkill(\n {\n name: 'data_analysis',\n description: 'Use this when the user wants a one-shot analysis of a CSV: parse+stats+query in a single call. The README lists data_analysis as the top-level data tool.',\n version: '1.0.0',\n source: 'bundled',\n enabled: true,\n },\n {\n name: 'data_analysis',\n description: 'High-level data analysis for CSV files. Chooses the right operation (preview, stats, query) based on the `operation` parameter. When the user says \"analyze this CSV\", \"what does this data look like\", or \"give me a report on this dataset\", call this tool with operation=\"summary\" and it will return headers, row count, column stats, and a sample of rows in one response.',\n parameters: {\n type: 'object',\n properties: {\n path: { type: 'string', description: 'Path to the CSV file' },\n operation: {\n type: 'string',\n description: 'Which analysis to run: \"summary\" (preview + stats in one call, default), \"preview\" (first N rows), \"stats\" (column statistics), or \"query\" (filter/sort).',\n enum: ['summary', 'preview', 'stats', 'query'],\n },\n columns: { type: 'string', description: 'Comma-separated columns to focus on (optional)' },\n filter: { type: 'string', description: 'Filter expression for operation=\"query\" (e.g. \"age > 30\")' },\n sort: { type: 'string', description: 'Sort column for operation=\"query\"' },\n limit: { type: 'number', description: 'Row limit (default 20 for summary/preview, 50 for query)' },\n delimiter: { type: 'string', description: 'Field delimiter (default: comma)' },\n },\n required: ['path'],\n },\n execute: async (args) => {\n try {\n const path = args.path as string;\n const operation = (args.operation as string) || 'summary';\n const delimiter = (args.delimiter as string) || ',';\n const columnsArg = (args.columns as string) || '';\n const limit = Math.min((args.limit as number) || (operation === 'query' ? 50 : 20), 1000);\n\n const filePath = resolve(path);\n const content = readFileSync(filePath, 'utf-8');\n const { headers, rows } = parseCSV(content, delimiter);\n\n if (operation === 'preview') {\n const displayRows = rows.slice(0, limit);\n return `File: ${path}\\nRows: ${rows.length} total, showing first ${displayRows.length}\\nColumns (${headers.length}): ${headers.join(', ')}\\n\\n${formatTable(headers, displayRows)}`;\n }\n\n if (operation === 'stats') {\n const selectedColumns = columnsArg ? columnsArg.split(',').map(c => c.trim()) : headers;\n const stats: Record<string, ColumnStats> = {};\n for (const col of selectedColumns) {\n if (!headers.includes(col)) continue;\n const values = rows.map(r => r[col] || '');\n const numericValues = values.filter(isNumeric).map(Number);\n if (numericValues.length > 0) {\n stats[col] = calculateNumericStats(numericValues) as ColumnStats;\n } else {\n const unique = new Set(values.filter(v => v.trim())).size;\n stats[col] = { count: values.length, unique };\n }\n }\n return `File: ${path}\\nRows: ${rows.length}\\nColumn stats:\\n\\n${formatStatsTable(stats)}`;\n }\n\n if (operation === 'query') {\n const filter = (args.filter as string) || '';\n const sort = (args.sort as string) || '';\n let filtered = rows;\n if (filter) {\n const match = filter.match(/^\\s*(\\w+)\\s*(>=|<=|!=|==|>|<|contains)\\s*(.+)\\s*$/);\n if (match) {\n const [, col, op, rawVal] = match;\n const val = rawVal.trim().replace(/^['\"]|['\"]$/g, '');\n filtered = filtered.filter(row => {\n const cell = row[col] || '';\n return evaluateExpression(`${cell} ${op} ${val}`, cell);\n });\n }\n }\n if (sort && headers.includes(sort)) {\n filtered = [...filtered].sort((a, b) => {\n const aNum = Number(a[sort] || '');\n const bNum = Number(b[sort] || '');\n if (!isNaN(aNum) && !isNaN(bNum)) return aNum - bNum;\n return (a[sort] || '').localeCompare(b[sort] || '');\n });\n }\n const displayHeaders = columnsArg ? columnsArg.split(',').map(c => c.trim()) : headers;\n const displayRows = filtered.slice(0, limit);\n return `File: ${path}\\nMatched ${filtered.length} of ${rows.length} rows, showing ${displayRows.length}\\n\\n${formatTable(displayHeaders, displayRows)}`;\n }\n\n // Default: summary — preview + stats combined\n const preview = rows.slice(0, Math.min(limit, 10));\n const selectedColumns = columnsArg ? columnsArg.split(',').map(c => c.trim()) : headers;\n const stats: Record<string, ColumnStats> = {};\n for (const col of selectedColumns) {\n if (!headers.includes(col)) continue;\n const values = rows.map(r => r[col] || '');\n const numericValues = values.filter(isNumeric).map(Number);\n if (numericValues.length > 0) {\n stats[col] = calculateNumericStats(numericValues) as ColumnStats;\n } else {\n const unique = new Set(values.filter(v => v.trim())).size;\n stats[col] = { count: values.length, unique };\n }\n }\n return [\n `File: ${path}`,\n `Rows: ${rows.length}`,\n `Columns (${headers.length}): ${headers.join(', ')}`,\n '',\n '── Preview (first 10) ──',\n formatTable(headers, preview),\n '',\n '── Column stats ──',\n formatStatsTable(stats),\n ].join('\\n');\n } catch (e) {\n return `Error in data_analysis: ${(e as Error).message}`;\n }\n },\n }\n );\n\n registerSkill(\n {\n name: 'data_analysis',\n description: 'Use this when the user says \"filter this data\", \"show me rows where X\", \"find entries matching Y\", \"sort by Z\", or wants to query/slice a CSV dataset.',\n version: '1.0.0',\n source: 'bundled',\n enabled: true,\n },\n {\n name: 'csv_query',\n description: 'Filter, sort, and slice a CSV dataset with simple expressions. Use when asked to \"show me rows where X > 30\", \"find all entries where status is active\", \"sort by date\", \"filter this data by Y\", or \"which rows match Z\". Supports expressions like \"age > 30\", \"name contains john\", \"status == active\".',\n parameters: {\n type: 'object',\n properties: {\n path: {\n type: 'string',\n description: 'Path to the CSV file',\n },\n filter: {\n type: 'string',\n description: 'Filter expression (e.g., \"age > 30\", \"name contains john\", \"status == active\")',\n },\n sort: {\n type: 'string',\n description: 'Column name to sort results by (optional)',\n },\n limit: {\n type: 'number',\n description: 'Maximum rows to return (default: 50)',\n },\n columns: {\n type: 'string',\n description: 'Comma-separated columns to include in results (optional — all if not specified)',\n },\n delimiter: {\n type: 'string',\n description: 'Field delimiter (default: comma)',\n },\n },\n required: ['path'],\n },\n execute: async (args) => {\n try {\n const path = args.path as string;\n const filter = (args.filter as string) || '';\n const sort = (args.sort as string) || '';\n const limit = Math.min((args.limit as number) || 50, 1000);\n const columnsArg = (args.columns as string) || '';\n const delimiter = (args.delimiter as string) || ',';\n\n const filePath = resolve(path);\n const content = readFileSync(filePath, 'utf-8');\n const parsed = parseCSV(content, delimiter);\n const { headers } = parsed;\n let { rows } = parsed;\n\n // Apply filter\n if (filter) {\n // v6.5 — two-char operators MUST precede their single-char\n // prefixes in the alternation, else '>=' parses as '>' + '= 30',\n // Number('= 30') is NaN, and the filter silently matches 0 rows.\n const [column, expression] = filter.split(/\\s*(>=|<=|==|!=|>|<|contains)\\s*/);\n const cleanColumn = column.trim();\n const op = expression || '';\n const value = filter.substring(filter.indexOf(op) + op.length).trim();\n\n rows = rows.filter(row => {\n const cellValue = row[cleanColumn] || '';\n const expr = `${cellValue} ${op} ${value}`;\n return evaluateExpression(expr, cellValue);\n });\n }\n\n // Apply sort\n if (sort && headers.includes(sort)) {\n rows.sort((a, b) => {\n const aVal = a[sort] || '';\n const bVal = b[sort] || '';\n const aNum = Number(aVal);\n const bNum = Number(bVal);\n if (!isNaN(aNum) && !isNaN(bNum)) {\n return aNum - bNum;\n }\n return aVal.localeCompare(bVal);\n });\n }\n\n // Apply column selection\n const displayHeaders = columnsArg ? columnsArg.split(',').map(c => c.trim()) : headers;\n const displayRows = rows.slice(0, limit).map(row => {\n const newRow: Record<string, string> = {};\n for (const col of displayHeaders) {\n newRow[col] = row[col] || '';\n }\n return newRow;\n });\n\n const summary = `Filtered to ${rows.length} rows, displaying first ${displayRows.length}.\\n\\n`;\n return summary + formatTable(displayHeaders, displayRows);\n } catch (e) {\n return `Error querying CSV: ${(e as Error).message}`;\n }\n },\n }\n );\n}\n\n/**\n * Evaluate a filter expression\n */\nfunction evaluateExpression(expr: string, _cellValue: string): boolean {\n const operators = ['>=', '<=', '!=', '==', '>', '<', 'contains'];\n for (const op of operators) {\n if (expr.includes(op)) {\n const parts = expr.split(op).map(s => s.trim());\n if (parts.length < 2) continue;\n\n const leftVal = parts[0];\n const rightVal = parts.slice(1).join(op).trim().replace(/^['\"]|['\"]$/g, '');\n\n switch (op) {\n case '>':\n return Number(leftVal) > Number(rightVal);\n case '<':\n return Number(leftVal) < Number(rightVal);\n case '>=':\n return Number(leftVal) >= Number(rightVal);\n case '<=':\n return Number(leftVal) <= Number(rightVal);\n case '==':\n return String(leftVal) === String(rightVal);\n case '!=':\n return String(leftVal) !== String(rightVal);\n case 'contains':\n return String(leftVal).toLowerCase().includes(String(rightVal).toLowerCase());\n default:\n return false;\n }\n }\n }\n return true;\n}\n"],"mappings":"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","names":["sum","selectedColumns","stats"]}
@@ -35,15 +35,17 @@ function registerJiraLinearSkill() {
35
35
  try {
36
36
  if (action === "list") {
37
37
  const query = args.query || "";
38
- const gql = query ? `{ issueSearch(query: "${query}", first: 10) { nodes { id identifier title state { name } assignee { name } priority createdAt } } }` : `{ issues(first: 20, orderBy: updatedAt) { nodes { id identifier title state { name } assignee { name } priority updatedAt } } }`;
39
- const data = await apiCall(graphqlUrl, token, "POST", { query: gql });
38
+ const gql = query ? `query($q: String!) { issueSearch(query: $q, first: 10) { nodes { id identifier title state { name } assignee { name } priority createdAt } } }` : `{ issues(first: 20, orderBy: updatedAt) { nodes { id identifier title state { name } assignee { name } priority updatedAt } } }`;
39
+ const variables = query ? { q: query } : void 0;
40
+ const data = await apiCall(graphqlUrl, token, "POST", { query: gql, variables });
40
41
  const issues = data?.data?.issues?.nodes || data?.data?.issueSearch?.nodes || [];
41
42
  if (issues.length === 0) return "No issues found.";
42
43
  return issues.map((i) => `${i.identifier} [${i.state?.name || "?"}] ${i.title} (${i.assignee?.name || "unassigned"})`).join("\n");
43
44
  }
44
45
  if (action === "create") {
45
- const gql = `mutation { issueCreate(input: { title: "${args.title || "Untitled"}", description: "${args.description || ""}" }) { success issue { id identifier title url } } }`;
46
- const data = await apiCall(graphqlUrl, token, "POST", { query: gql });
46
+ const gql = `mutation($title: String!, $description: String) { issueCreate(input: { title: $title, description: $description }) { success issue { id identifier title url } } }`;
47
+ const variables = { title: args.title || "Untitled", description: args.description || "" };
48
+ const data = await apiCall(graphqlUrl, token, "POST", { query: gql, variables });
47
49
  const issue = data?.data?.issueCreate?.issue;
48
50
  return issue ? `Created: ${issue.identifier} \u2014 ${issue.title} (${issue.url})` : "Failed to create issue";
49
51
  }
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../src/skills/builtin/jira_linear.ts"],"sourcesContent":["/**\n * TITAN — Jira & Linear Integration Skill\n * Pull tasks from PM tools, create issues, update status bidirectionally.\n */\nimport { registerSkill } from '../registry.js';\nimport logger from '../../utils/logger.js';\n\nconst COMPONENT = 'JiraLinear';\n\nasync function apiCall(url: string, token: string, method = 'GET', body?: unknown): Promise<unknown> {\n const res = await fetch(url, {\n method,\n headers: { 'Authorization': `Bearer ${token}`, 'Content-Type': 'application/json' },\n body: body ? JSON.stringify(body) : undefined,\n });\n return res.json();\n}\n\nexport function registerJiraLinearSkill(): void {\n // Linear integration\n registerSkill(\n { name: 'linear_issues', description: 'Manage Linear issues', version: '1.0.0', source: 'bundled', enabled: true },\n {\n name: 'linear_issues',\n description: 'List, create, or update Linear issues.\\nUSE THIS WHEN: \"check Linear\", \"create Linear issue\", \"update ticket\", \"what are my tasks\"\\nRequires LINEAR_API_KEY env var.',\n parameters: {\n type: 'object',\n properties: {\n action: { type: 'string', enum: ['list', 'create', 'update', 'get'], description: 'Action to perform' },\n query: { type: 'string', description: 'Search query or issue ID' },\n title: { type: 'string', description: 'Issue title (for create)' },\n description: { type: 'string', description: 'Issue description (for create/update)' },\n status: { type: 'string', description: 'Status to set (for update)' },\n },\n required: ['action'],\n },\n execute: async (args) => {\n const token = process.env.LINEAR_API_KEY;\n if (!token) return 'Error: LINEAR_API_KEY not set. Set it in your environment to use Linear integration.';\n\n const action = args.action as string;\n const graphqlUrl = 'https://api.linear.app/graphql';\n\n try {\n if (action === 'list') {\n const query = args.query as string || '';\n const gql = query\n ? `{ issueSearch(query: \"${query}\", first: 10) { nodes { id identifier title state { name } assignee { name } priority createdAt } } }`\n : `{ issues(first: 20, orderBy: updatedAt) { nodes { id identifier title state { name } assignee { name } priority updatedAt } } }`;\n const data = await apiCall(graphqlUrl, token, 'POST', { query: gql }) as any;\n const issues = data?.data?.issues?.nodes || data?.data?.issueSearch?.nodes || [];\n if (issues.length === 0) return 'No issues found.';\n return issues.map((i: any) => `${i.identifier} [${i.state?.name || '?'}] ${i.title} (${i.assignee?.name || 'unassigned'})`).join('\\n');\n }\n if (action === 'create') {\n const gql = `mutation { issueCreate(input: { title: \"${args.title || 'Untitled'}\", description: \"${args.description || ''}\" }) { success issue { id identifier title url } } }`;\n const data = await apiCall(graphqlUrl, token, 'POST', { query: gql }) as any;\n const issue = data?.data?.issueCreate?.issue;\n return issue ? `Created: ${issue.identifier} — ${issue.title} (${issue.url})` : 'Failed to create issue';\n }\n return `Action \"${action}\" not fully implemented yet. Available: list, create`;\n } catch (e) {\n return `Linear API error: ${(e as Error).message}`;\n }\n },\n },\n );\n\n // Jira integration\n registerSkill(\n { name: 'jira_issues', description: 'Manage Jira issues', version: '1.0.0', source: 'bundled', enabled: true },\n {\n name: 'jira_issues',\n description: 'List, create, or update Jira issues.\\nUSE THIS WHEN: \"check Jira\", \"create Jira ticket\", \"what Jira issues are assigned to me\"\\nRequires JIRA_URL, JIRA_EMAIL, JIRA_TOKEN env vars.',\n parameters: {\n type: 'object',\n properties: {\n action: { type: 'string', enum: ['list', 'create', 'search', 'get'], description: 'Action to perform' },\n jql: { type: 'string', description: 'JQL query for search (e.g. \"assignee=currentUser() AND status!=Done\")' },\n project: { type: 'string', description: 'Project key (for create)' },\n title: { type: 'string', description: 'Issue summary (for create)' },\n description: { type: 'string', description: 'Issue description (for create)' },\n issueType: { type: 'string', description: 'Issue type: Bug, Task, Story (default: Task)' },\n },\n required: ['action'],\n },\n execute: async (args) => {\n const url = process.env.JIRA_URL;\n const email = process.env.JIRA_EMAIL;\n const token = process.env.JIRA_TOKEN;\n if (!url || !email || !token) return 'Error: Set JIRA_URL, JIRA_EMAIL, JIRA_TOKEN environment variables.';\n\n const auth = Buffer.from(`${email}:${token}`).toString('base64');\n const headers = { 'Authorization': `Basic ${auth}`, 'Content-Type': 'application/json' };\n const action = args.action as string;\n\n try {\n if (action === 'list' || action === 'search') {\n const jql = (args.jql as string) || 'assignee=currentUser() AND status!=Done ORDER BY updated DESC';\n const res = await fetch(`${url}/rest/api/3/search?jql=${encodeURIComponent(jql)}&maxResults=20`, { headers });\n const data = await res.json() as any;\n const issues = data.issues || [];\n if (issues.length === 0) return 'No issues found.';\n return issues.map((i: any) => `${i.key} [${i.fields?.status?.name || '?'}] ${i.fields?.summary} (${i.fields?.assignee?.displayName || 'unassigned'})`).join('\\n');\n }\n if (action === 'create') {\n const body = {\n fields: {\n project: { key: args.project || 'TITAN' },\n summary: args.title || 'Untitled',\n description: { type: 'doc', version: 1, content: [{ type: 'paragraph', content: [{ type: 'text', text: args.description || '' }] }] },\n issuetype: { name: args.issueType || 'Task' },\n },\n };\n const res = await fetch(`${url}/rest/api/3/issue`, { method: 'POST', headers, body: JSON.stringify(body) });\n const data = await res.json() as any;\n return data.key ? `Created: ${data.key} — ${args.title} (${url}/browse/${data.key})` : `Error: ${JSON.stringify(data.errors || data)}`;\n }\n return `Action \"${action}\" supported: list, search, create`;\n } catch (e) {\n return `Jira API error: ${(e as Error).message}`;\n }\n },\n },\n );\n logger.info(COMPONENT, 'Registered Jira + Linear integration skills');\n}\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../../../src/skills/builtin/jira_linear.ts"],"sourcesContent":["/**\n * TITAN — Jira & Linear Integration Skill\n * Pull tasks from PM tools, create issues, update status bidirectionally.\n */\nimport { registerSkill } from '../registry.js';\nimport logger from '../../utils/logger.js';\n\nconst COMPONENT = 'JiraLinear';\n\nasync function apiCall(url: string, token: string, method = 'GET', body?: unknown): Promise<unknown> {\n const res = await fetch(url, {\n method,\n headers: { 'Authorization': `Bearer ${token}`, 'Content-Type': 'application/json' },\n body: body ? JSON.stringify(body) : undefined,\n });\n return res.json();\n}\n\nexport function registerJiraLinearSkill(): void {\n // Linear integration\n registerSkill(\n { name: 'linear_issues', description: 'Manage Linear issues', version: '1.0.0', source: 'bundled', enabled: true },\n {\n name: 'linear_issues',\n description: 'List, create, or update Linear issues.\\nUSE THIS WHEN: \"check Linear\", \"create Linear issue\", \"update ticket\", \"what are my tasks\"\\nRequires LINEAR_API_KEY env var.',\n parameters: {\n type: 'object',\n properties: {\n action: { type: 'string', enum: ['list', 'create', 'update', 'get'], description: 'Action to perform' },\n query: { type: 'string', description: 'Search query or issue ID' },\n title: { type: 'string', description: 'Issue title (for create)' },\n description: { type: 'string', description: 'Issue description (for create/update)' },\n status: { type: 'string', description: 'Status to set (for update)' },\n },\n required: ['action'],\n },\n execute: async (args) => {\n const token = process.env.LINEAR_API_KEY;\n if (!token) return 'Error: LINEAR_API_KEY not set. Set it in your environment to use Linear integration.';\n\n const action = args.action as string;\n const graphqlUrl = 'https://api.linear.app/graphql';\n\n try {\n if (action === 'list') {\n const query = args.query as string || '';\n // v6.5 — use GraphQL variables; never string-interpolate\n // user/LLM-controlled input into the query document. Was a\n // GraphQL-injection risk and broke on any quote in the query.\n const gql = query\n ? `query($q: String!) { issueSearch(query: $q, first: 10) { nodes { id identifier title state { name } assignee { name } priority createdAt } } }`\n : `{ issues(first: 20, orderBy: updatedAt) { nodes { id identifier title state { name } assignee { name } priority updatedAt } } }`;\n const variables = query ? { q: query } : undefined;\n const data = await apiCall(graphqlUrl, token, 'POST', { query: gql, variables }) as any;\n const issues = data?.data?.issues?.nodes || data?.data?.issueSearch?.nodes || [];\n if (issues.length === 0) return 'No issues found.';\n return issues.map((i: any) => `${i.identifier} [${i.state?.name || '?'}] ${i.title} (${i.assignee?.name || 'unassigned'})`).join('\\n');\n }\n if (action === 'create') {\n // v6.5 — GraphQL variables (was injection via title/description)\n const gql = `mutation($title: String!, $description: String) { issueCreate(input: { title: $title, description: $description }) { success issue { id identifier title url } } }`;\n const variables = { title: (args.title as string) || 'Untitled', description: (args.description as string) || '' };\n const data = await apiCall(graphqlUrl, token, 'POST', { query: gql, variables }) as any;\n const issue = data?.data?.issueCreate?.issue;\n return issue ? `Created: ${issue.identifier} — ${issue.title} (${issue.url})` : 'Failed to create issue';\n }\n return `Action \"${action}\" not fully implemented yet. Available: list, create`;\n } catch (e) {\n return `Linear API error: ${(e as Error).message}`;\n }\n },\n },\n );\n\n // Jira integration\n registerSkill(\n { name: 'jira_issues', description: 'Manage Jira issues', version: '1.0.0', source: 'bundled', enabled: true },\n {\n name: 'jira_issues',\n description: 'List, create, or update Jira issues.\\nUSE THIS WHEN: \"check Jira\", \"create Jira ticket\", \"what Jira issues are assigned to me\"\\nRequires JIRA_URL, JIRA_EMAIL, JIRA_TOKEN env vars.',\n parameters: {\n type: 'object',\n properties: {\n action: { type: 'string', enum: ['list', 'create', 'search', 'get'], description: 'Action to perform' },\n jql: { type: 'string', description: 'JQL query for search (e.g. \"assignee=currentUser() AND status!=Done\")' },\n project: { type: 'string', description: 'Project key (for create)' },\n title: { type: 'string', description: 'Issue summary (for create)' },\n description: { type: 'string', description: 'Issue description (for create)' },\n issueType: { type: 'string', description: 'Issue type: Bug, Task, Story (default: Task)' },\n },\n required: ['action'],\n },\n execute: async (args) => {\n const url = process.env.JIRA_URL;\n const email = process.env.JIRA_EMAIL;\n const token = process.env.JIRA_TOKEN;\n if (!url || !email || !token) return 'Error: Set JIRA_URL, JIRA_EMAIL, JIRA_TOKEN environment variables.';\n\n const auth = Buffer.from(`${email}:${token}`).toString('base64');\n const headers = { 'Authorization': `Basic ${auth}`, 'Content-Type': 'application/json' };\n const action = args.action as string;\n\n try {\n if (action === 'list' || action === 'search') {\n const jql = (args.jql as string) || 'assignee=currentUser() AND status!=Done ORDER BY updated DESC';\n const res = await fetch(`${url}/rest/api/3/search?jql=${encodeURIComponent(jql)}&maxResults=20`, { headers });\n const data = await res.json() as any;\n const issues = data.issues || [];\n if (issues.length === 0) return 'No issues found.';\n return issues.map((i: any) => `${i.key} [${i.fields?.status?.name || '?'}] ${i.fields?.summary} (${i.fields?.assignee?.displayName || 'unassigned'})`).join('\\n');\n }\n if (action === 'create') {\n const body = {\n fields: {\n project: { key: args.project || 'TITAN' },\n summary: args.title || 'Untitled',\n description: { type: 'doc', version: 1, content: [{ type: 'paragraph', content: [{ type: 'text', text: args.description || '' }] }] },\n issuetype: { name: args.issueType || 'Task' },\n },\n };\n const res = await fetch(`${url}/rest/api/3/issue`, { method: 'POST', headers, body: JSON.stringify(body) });\n const data = await res.json() as any;\n return data.key ? `Created: ${data.key} — ${args.title} (${url}/browse/${data.key})` : `Error: ${JSON.stringify(data.errors || data)}`;\n }\n return `Action \"${action}\" supported: list, search, create`;\n } catch (e) {\n return `Jira API error: ${(e as Error).message}`;\n }\n },\n },\n );\n logger.info(COMPONENT, 'Registered Jira + Linear integration skills');\n}\n"],"mappings":"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","names":[]}