titan-agent 6.4.3 → 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.
- package/dist/agent/agent.js +61 -46
- package/dist/agent/agent.js.map +1 -1
- package/dist/agent/agentLessons.js.map +1 -1
- package/dist/agent/agentLoop.js +5 -1
- package/dist/agent/agentLoop.js.map +1 -1
- package/dist/agent/orchestrator.js +5 -14
- package/dist/agent/orchestrator.js.map +1 -1
- package/dist/agent/pipeline.js +11 -6
- package/dist/agent/pipeline.js.map +1 -1
- package/dist/agent/somaInitiative.js.map +1 -1
- package/dist/agent/toolRunner.js +35 -0
- package/dist/agent/toolRunner.js.map +1 -1
- package/dist/channels/discord.js +10 -6
- package/dist/channels/discord.js.map +1 -1
- package/dist/channels/slack.js +7 -2
- package/dist/channels/slack.js.map +1 -1
- package/dist/channels/telegram.js +1 -1
- package/dist/channels/telegram.js.map +1 -1
- package/dist/config/schema.js +6 -0
- package/dist/config/schema.js.map +1 -1
- package/dist/gateway/openai-compat.js +69 -10
- package/dist/gateway/openai-compat.js.map +1 -1
- package/dist/gateway/server.js +79 -0
- package/dist/gateway/server.js.map +1 -1
- package/dist/migrations/runner.js.map +1 -1
- package/dist/migrations/safeRun.js.map +1 -1
- package/dist/providers/anthropic.js +7 -4
- package/dist/providers/anthropic.js.map +1 -1
- package/dist/providers/base.js +8 -1
- package/dist/providers/base.js.map +1 -1
- package/dist/providers/google.js +7 -4
- package/dist/providers/google.js.map +1 -1
- package/dist/providers/ollama.js +8 -7
- package/dist/providers/ollama.js.map +1 -1
- package/dist/providers/openai.js +7 -4
- package/dist/providers/openai.js.map +1 -1
- package/dist/providers/openai_compat.js +7 -4
- package/dist/providers/openai_compat.js.map +1 -1
- package/dist/providers/router.js +7 -3
- package/dist/providers/router.js.map +1 -1
- package/dist/skills/builtin/backup.js.map +1 -1
- package/dist/skills/builtin/browser.js +11 -21
- package/dist/skills/builtin/browser.js.map +1 -1
- package/dist/skills/builtin/calendar.js +6 -2
- package/dist/skills/builtin/calendar.js.map +1 -1
- package/dist/skills/builtin/canvas_spaces.js.map +1 -1
- package/dist/skills/builtin/cron.js +7 -6
- package/dist/skills/builtin/cron.js.map +1 -1
- package/dist/skills/builtin/data_analysis.js +1 -1
- package/dist/skills/builtin/data_analysis.js.map +1 -1
- package/dist/skills/builtin/jira_linear.js +6 -4
- package/dist/skills/builtin/jira_linear.js.map +1 -1
- package/dist/skills/builtin/web_browser.js +6 -0
- package/dist/skills/builtin/web_browser.js.map +1 -1
- package/dist/skills/builtin/web_fetch.js +1 -0
- package/dist/skills/builtin/web_fetch.js.map +1 -1
- package/dist/storage/starterSpaces.js.map +1 -1
- package/dist/utils/constants.js +1 -1
- package/dist/utils/constants.js.map +1 -1
- package/package.json +1 -1
- package/ui/dist/sw.js +1 -1
|
@@ -71,27 +71,17 @@ ${links.join("\n")}`);
|
|
|
71
71
|
});
|
|
72
72
|
});
|
|
73
73
|
}
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
case "
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
return `
|
|
83
|
-
|
|
84
|
-
|
|
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
|
|
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":"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","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
|
|
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":[]}
|
|
@@ -41,12 +41,13 @@ async function executePrompt(prompt, allowedTools) {
|
|
|
41
41
|
try {
|
|
42
42
|
const { processMessage } = await import("../../agent/agent.js");
|
|
43
43
|
const sessionId = `cron-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 8)}`;
|
|
44
|
-
const
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
44
|
+
const normalized = Array.isArray(allowedTools) ? allowedTools : typeof allowedTools === "string" && allowedTools.length > 0 ? allowedTools.split(",").map((s) => s.trim()).filter(Boolean) : void 0;
|
|
45
|
+
const result = await processMessage(
|
|
46
|
+
prompt,
|
|
47
|
+
sessionId,
|
|
48
|
+
"cron",
|
|
49
|
+
normalized && normalized.length > 0 ? { allowedTools: normalized } : void 0
|
|
50
|
+
);
|
|
50
51
|
const summary = (result.content || "").slice(0, 500);
|
|
51
52
|
const tools = (result.toolsUsed || []).join(", ");
|
|
52
53
|
return `[tool-mode] used tools: ${tools || "(none)"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../src/skills/builtin/cron.ts"],"sourcesContent":["/**\n * TITAN — Cron Skill (Built-in)\n * Schedule and manage automated tasks using node-cron.\n * Jobs are persisted to the DB and re-scheduled on gateway boot via initCronScheduler().\n */\nimport * as cron from 'node-cron';\nimport { exec } from 'child_process';\nimport { registerSkill } from '../registry.js';\nimport { getDb } from '../../memory/memory.js';\nimport { v4 as uuid } from 'uuid';\nimport logger from '../../utils/logger.js';\n\nconst COMPONENT = 'Cron';\n\n// ─── Active task handles ────────────────────────────────────────\n// Maps job ID → node-cron ScheduledTask so we can stop/restart them.\nconst activeTasks = new Map<string, ReturnType<typeof cron.schedule>>();\n\n// ─── Shell execution helper ─────────────────────────────────────\n\n/** Execute a shell command and return its output (stdout + stderr). */\nfunction executeCommand(command: string, timeout: number = 60000): Promise<string> {\n return new Promise((resolve) => {\n const proc = exec(\n command,\n {\n timeout,\n maxBuffer: 1024 * 1024 * 5, // 5 MB\n shell: '/bin/bash',\n },\n (error, stdout, stderr) => {\n let output = '';\n if (stdout) output += stdout;\n if (stderr) output += (output ? '\\n' : '') + `[stderr] ${stderr}`;\n if (error) {\n if (error.killed) {\n output += (output ? '\\n' : '') + `[timed out after ${timeout}ms]`;\n } else {\n output += (output ? '\\n' : '') + `[exit code: ${error.code}]`;\n }\n }\n // Truncate very long output so we don't bloat logs\n if (output.length > 10000) {\n output = output.slice(0, 5000) + '\\n... [truncated] ...\\n' + output.slice(-5000);\n }\n resolve(output || '(no output)');\n },\n );\n void proc; // satisfy TS — proc is managed by exec callback\n });\n}\n\n/**\n * v6.0.5 — Send the command as an LLM prompt to the agent loop. This is\n * the `mode: \"tool\"` path. Previously the scheduler ignored the mode flag\n * and ALL jobs (including tool-mode ones with English prompts) were\n * shoved through bash, which mangled FB posts among other things.\n */\nasync function executePrompt(prompt: string, allowedTools?: string[]): Promise<string> {\n try {\n const { processMessage } = await import('../../agent/agent.js');\n // sessionId 'cron-<rand>' so the agent loop doesn't try to recall\n // chat history. userId 'cron' identifies the surface in logs.\n const sessionId = `cron-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 8)}`;\n const result = await processMessage(prompt, sessionId, 'cron', {\n channel: 'cron',\n // allowedTools is plumbed but not yet honored end-to-end —\n // logged here so a future hardening pass can enforce it.\n ...(allowedTools && allowedTools.length > 0 ? { allowedTools } : {}),\n } as Parameters<typeof processMessage>[3]);\n const summary = (result.content || '').slice(0, 500);\n const tools = (result.toolsUsed || []).join(', ');\n return `[tool-mode] used tools: ${tools || '(none)'}\\n${summary}`;\n } catch (err) {\n return `[tool-mode error] ${(err as Error).message}`;\n }\n}\n\n/**\n * v6.0.5 — Defensive guard for shell-mode jobs. Refuses commands that\n * clearly look like English prose / LLM prompts rather than shell.\n *\n * The trigger for this guard: FB autopilot cron jobs were created with\n * the agent prompt as the `command` field but with `mode` left at the\n * default (\"shell\"). Because the scheduler also ignored `mode`, those\n * prompts went straight into `/bin/bash -c \"You are TITAN...\"` every day,\n * resulting in mangled / broken output. We catch the \"looks like a\n * prompt\" case BEFORE bash sees it so the only fix needed is the\n * scheduler honoring mode.\n *\n * Heuristic: starts with words like \"you are\", \"post a\", \"write a\",\n * \"build a\", or contains \"Use the X tool\" — strong signals of an LLM\n * prompt accidentally routed here. Conservative; real shell commands\n * (cd, ls, git, npm, python, node, /bin/, ./, ~, $(...) , etc.) never\n * match.\n */\nfunction looksLikeLlmPrompt(cmd: string): boolean {\n const head = cmd.trim().slice(0, 200).toLowerCase();\n const promptStarts = [\n 'you are ', 'you are titan',\n 'post a ', 'post an ',\n 'write a ', 'write an ',\n 'build a ', 'build me ', 'build the ',\n 'create a ', 'create me ',\n 'generate a ', 'generate an ',\n 'compose a ', 'compose an ',\n 'draft a ', 'draft an ',\n ];\n if (promptStarts.some(p => head.startsWith(p))) return true;\n if (/\\buse the (?:[a-z_]+) tool\\b/i.test(cmd)) return true;\n if (/\\bunder \\d+ words\\b/i.test(cmd)) return true;\n return false;\n}\n\n// ─── Scheduler helpers ──────────────────────────────────────────\n\n/**\n * Start a node-cron task for a persisted job. Returns the task handle.\n *\n * v6.0.5 — Now honors `mode` (shell vs tool). Previously this signature\n * was `(jobId, schedule, command)` and ALL jobs were bash-executed\n * regardless of `record.mode`. That bug mangled the FB autopilot cron\n * jobs (English prompts routed to /bin/bash). The signature now takes\n * a record-shaped arg so callers don't have to remember to pass mode +\n * allowedTools separately, and forgetting the mode flag silently\n * defaults to shell at most, never bypasses it.\n */\ninterface CronJobLike {\n id: string;\n name?: string;\n schedule: string;\n command: string;\n mode?: 'shell' | 'tool';\n allowedTools?: string[];\n}\n\nfunction scheduleJob(job: CronJobLike): ReturnType<typeof cron.schedule> | null {\n if (!cron.validate(job.schedule)) {\n logger.warn(COMPONENT, `Invalid cron expression for job ${job.id}: \"${job.schedule}\"`);\n return null;\n }\n\n const mode: 'shell' | 'tool' = job.mode === 'tool' ? 'tool' : 'shell';\n const task = cron.schedule(job.schedule, async () => {\n logger.info(COMPONENT, `Running cron job ${job.id} (mode=${mode}, name=${job.name ?? '(unnamed)'}): ${job.command.slice(0, 120)}`);\n\n const store = getDb();\n const record = store.cronJobs.find((j) => j.id === job.id);\n if (record) record.last_run = new Date().toISOString();\n\n try {\n let output: string;\n if (mode === 'tool') {\n output = await executePrompt(job.command, job.allowedTools);\n } else if (looksLikeLlmPrompt(job.command)) {\n // Defensive guard for \"shell\" mode that's actually an LLM\n // prompt. Refuse instead of letting bash mangle it.\n output = `[refused] This shell-mode cron job looks like an LLM prompt, not a shell command. Re-create the job with mode:\"tool\" so it routes to the agent loop. Command head: ${job.command.slice(0, 80)}…`;\n logger.warn(COMPONENT, `Cron job ${job.id}: REFUSED to bash-execute LLM-prompt-shaped command`);\n } else {\n output = await executeCommand(job.command);\n }\n logger.info(COMPONENT, `Cron job ${job.id} completed:\\n${output.slice(0, 500)}`);\n } catch (err) {\n logger.error(COMPONENT, `Cron job ${job.id} failed: ${(err as Error).message}`);\n }\n });\n\n return task;\n}\n\n/** Stop and remove a task from the active map. */\nfunction stopAndRemoveTask(jobId: string): void {\n const task = activeTasks.get(jobId);\n if (task) {\n task.stop();\n activeTasks.delete(jobId);\n logger.debug(COMPONENT, `Stopped cron task: ${jobId}`);\n }\n}\n\n// ─── Public: init scheduler on gateway boot ─────────────────────\n\n/**\n * Called once from server.ts on startup.\n * Reads all enabled cron jobs from the DB and schedules them.\n */\nexport function initCronScheduler(): void {\n const store = getDb();\n let scheduled = 0;\n let skipped = 0;\n\n for (const job of store.cronJobs) {\n if (!job.enabled) { skipped++; continue; }\n\n const task = scheduleJob(job);\n if (task) {\n activeTasks.set(job.id, task);\n scheduled++;\n logger.debug(COMPONENT, `Scheduled: \"${job.name}\" (${job.schedule}, mode=${job.mode ?? 'shell'})`);\n } else {\n skipped++;\n }\n }\n\n logger.info(COMPONENT, `Cron scheduler initialised — ${scheduled} active, ${skipped} skipped`);\n}\n\n// ─── Skill registration ─────────────────────────────────────────\n\nexport function registerCronSkill(): void {\n registerSkill(\n { name: 'cron', description: 'Schedule and manage recurring tasks using cron expressions. USE THIS WHEN Tony says: \"schedule this to run every X\", \"set up a recurring task\", \"run this daily at 9am\", \"run this every 5 minutes\", \"add a cron job\", \"automate X on a schedule\". WORKFLOW: action=create with name, schedule (cron expression), and shell command — action=list to see all jobs — action=enable/disable to toggle — action=delete to remove.', version: '1.1.0', source: 'bundled', enabled: true },\n {\n name: 'cron',\n description: `Schedule, list, toggle, or delete recurring jobs. Jobs persist across restarts and run via node-cron in-process. Each job has a name, a cron expression, a command, and an execution mode (shell or tool).\n\nUSE WHEN: \"schedule this to run every X\" / \"set up a recurring task\" / \"run this daily at 9am\" / \"every 5 minutes do X\" / \"add a cron job\" / \"automate X on a schedule\" / \"stop that cron job\".\n\nDO NOT USE FOR:\n- One-off tasks → just run them now via shell or the appropriate tool.\n- Tasks triggered by events (file change, webhook) → use event_triggers / webhook respectively.\n- Tasks that need TITAN to \"wake up\" and decide what to do → set mode:\"tool\" so the cron sends a message to the agent loop instead of running a bare shell.\n\nParameters:\n- action (string, required) — one of: \"create\", \"list\", \"delete\", \"enable\", \"disable\".\n- name (string, required for create) — human-readable label.\n- schedule (string, required for create) — cron expression. \"0 9 * * *\" = daily at 9am, \"*/5 * * * *\" = every 5 minutes, \"0 0 * * 0\" = Sunday midnight. Five fields, no seconds.\n- command (string, required for create) — shell command OR message to TITAN (depending on mode).\n- mode (string, optional) — \"shell\" (default; runs as raw shell) or \"tool\" (sent as a message to the agent loop, gets all tools).\n- allowedTools (string, optional) — comma-separated tool allowlist for mode:\"tool\" jobs.\n- jobId (string, required for delete/enable/disable) — id from list.\n\nReturns:\n- create → \"Created job: <id>\"\n- list → table of all jobs with status, next-run, last-run.\n- delete/enable/disable → confirmation with the affected jobId.\n\nErrors:\n- \"Invalid cron expression\" — five fields required, no seconds. Test at crontab.guru.\n- \"Job not found: <id>\" — call list to discover the right id.\n- \"Tool not in allowlist\" (mode:\"tool\") — extend allowedTools or remove the constraint.`,\n parameters: {\n type: 'object',\n properties: {\n action: {\n type: 'string',\n enum: ['create', 'list', 'delete', 'enable', 'disable'],\n description: 'Action to perform',\n },\n name: { type: 'string', description: 'Human-readable name for the cron job' },\n schedule: {\n type: 'string',\n description: 'Cron schedule expression (e.g., \"0 9 * * *\" for daily at 9 am, \"*/5 * * * *\" every 5 min)',\n },\n command: { type: 'string', description: 'Shell command or task description to execute when the job runs' },\n jobId: { type: 'string', description: 'Job ID (required for delete/enable/disable)' },\n mode: { type: 'string', enum: ['shell', 'tool'], description: 'Execution mode: \"shell\" (default) runs a shell command, \"tool\" sends the command as a message to TITAN for tool-based execution' },\n allowedTools: { type: 'string', description: 'Comma-separated list of allowed tool names for tool-mode jobs (e.g., \"web_search,write_file,shell\")' },\n },\n required: ['action'],\n },\n execute: async (args) => {\n const action = args.action as string;\n const store = getDb();\n\n switch (action) {\n // ── Create ─────────────────────────────────\n case 'create': {\n const name = args.name as string;\n const schedule = args.schedule as string;\n const command = args.command as string;\n\n if (!name || !schedule || !command) {\n return 'Error: name, schedule, and command are required for create';\n }\n\n if (!cron.validate(schedule)) {\n return `Error: \"${schedule}\" is not a valid cron expression. Example: \"0 9 * * *\" (daily at 9 am)`;\n }\n\n const mode = (args.mode as string) === 'tool' ? 'tool' : 'shell';\n const allowedToolsStr = args.allowedTools as string | undefined;\n const allowedTools = allowedToolsStr ? allowedToolsStr.split(',').map(t => t.trim()).filter(Boolean) : undefined;\n\n const id = uuid();\n const record = {\n id,\n name,\n schedule,\n command,\n mode: mode as 'shell' | 'tool',\n allowedTools,\n enabled: true,\n created_at: new Date().toISOString(),\n };\n store.cronJobs.push(record);\n\n // Schedule immediately so it runs without a restart.\n // v6.0.5 — Pass the WHOLE record so scheduleJob can\n // dispatch on mode + honor allowedTools.\n const task = scheduleJob(record);\n if (task) {\n activeTasks.set(id, task);\n }\n\n logger.info(COMPONENT, `Created and scheduled cron job: ${name} (${schedule}, mode=${mode})`);\n return `Created cron job \"${name}\" (ID: ${id})\\nSchedule: ${schedule}\\nMode: ${mode}\\nCommand: ${command}\\nStatus: Active and running`;\n }\n\n // ── List ──────────────────────────────────\n case 'list': {\n if (store.cronJobs.length === 0) return 'No cron jobs configured.';\n\n return store.cronJobs.map((j) => {\n const running = activeTasks.has(j.id) ? '(running)' : '(not scheduled)';\n const lastRun = j.last_run ? `\\n Last run: ${j.last_run}` : '';\n return [\n `• ${j.name} [${j.enabled ? '✅ enabled' : '❌ disabled'}] ${running}`,\n ` ID: ${j.id}`,\n ` Schedule: ${j.schedule}`,\n ` Command: ${j.command}`,\n lastRun,\n ].filter(Boolean).join('\\n');\n }).join('\\n\\n');\n }\n\n // ── Delete ────────────────────────────────\n case 'delete': {\n const jobId = args.jobId as string;\n if (!jobId) return 'Error: jobId is required';\n\n const job = store.cronJobs.find((j) => j.id === jobId);\n if (!job) return `Error: no cron job found with ID: ${jobId}`;\n\n // Stop the running task first\n stopAndRemoveTask(jobId);\n\n store.cronJobs = store.cronJobs.filter((j) => j.id !== jobId);\n logger.info(COMPONENT, `Deleted cron job: ${job.name} (${jobId})`);\n return `Deleted cron job \"${job.name}\" (ID: ${jobId})`;\n }\n\n // ── Enable ────────────────────────────────\n case 'enable': {\n const eId = args.jobId as string;\n if (!eId) return 'Error: jobId is required';\n\n const job = store.cronJobs.find((j) => j.id === eId);\n if (!job) return `Error: no cron job found with ID: ${eId}`;\n\n job.enabled = true;\n\n // Start scheduling if not already active.\n // v6.0.5 — Pass the whole record so mode is honored.\n if (!activeTasks.has(eId)) {\n const task = scheduleJob(job);\n if (task) {\n activeTasks.set(eId, task);\n }\n }\n\n logger.info(COMPONENT, `Enabled cron job: ${job.name} (${eId})`);\n return `Enabled cron job \"${job.name}\" — it will now run on schedule (${job.schedule})`;\n }\n\n // ── Disable ───────────────────────────────\n case 'disable': {\n const dId = args.jobId as string;\n if (!dId) return 'Error: jobId is required';\n\n const job = store.cronJobs.find((j) => j.id === dId);\n if (!job) return `Error: no cron job found with ID: ${dId}`;\n\n job.enabled = false;\n\n // Stop the running task\n stopAndRemoveTask(dId);\n\n logger.info(COMPONENT, `Disabled cron job: ${job.name} (${dId})`);\n return `Disabled cron job \"${job.name}\" — it will no longer run until re-enabled`;\n }\n\n default:\n return `Unknown action: ${action}. Valid actions: create, list, delete, enable, disable`;\n }\n },\n },\n );\n}\n"],"mappings":"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","names":[]}
|
|
1
|
+
{"version":3,"sources":["../../../src/skills/builtin/cron.ts"],"sourcesContent":["/**\n * TITAN — Cron Skill (Built-in)\n * Schedule and manage automated tasks using node-cron.\n * Jobs are persisted to the DB and re-scheduled on gateway boot via initCronScheduler().\n */\nimport * as cron from 'node-cron';\nimport { exec } from 'child_process';\nimport { registerSkill } from '../registry.js';\nimport { getDb } from '../../memory/memory.js';\nimport { v4 as uuid } from 'uuid';\nimport logger from '../../utils/logger.js';\n\nconst COMPONENT = 'Cron';\n\n// ─── Active task handles ────────────────────────────────────────\n// Maps job ID → node-cron ScheduledTask so we can stop/restart them.\nconst activeTasks = new Map<string, ReturnType<typeof cron.schedule>>();\n\n// ─── Shell execution helper ─────────────────────────────────────\n\n/** Execute a shell command and return its output (stdout + stderr). */\nfunction executeCommand(command: string, timeout: number = 60000): Promise<string> {\n return new Promise((resolve) => {\n const proc = exec(\n command,\n {\n timeout,\n maxBuffer: 1024 * 1024 * 5, // 5 MB\n shell: '/bin/bash',\n },\n (error, stdout, stderr) => {\n let output = '';\n if (stdout) output += stdout;\n if (stderr) output += (output ? '\\n' : '') + `[stderr] ${stderr}`;\n if (error) {\n if (error.killed) {\n output += (output ? '\\n' : '') + `[timed out after ${timeout}ms]`;\n } else {\n output += (output ? '\\n' : '') + `[exit code: ${error.code}]`;\n }\n }\n // Truncate very long output so we don't bloat logs\n if (output.length > 10000) {\n output = output.slice(0, 5000) + '\\n... [truncated] ...\\n' + output.slice(-5000);\n }\n resolve(output || '(no output)');\n },\n );\n void proc; // satisfy TS — proc is managed by exec callback\n });\n}\n\n/**\n * v6.0.5 — Send the command as an LLM prompt to the agent loop. This is\n * the `mode: \"tool\"` path. Previously the scheduler ignored the mode flag\n * and ALL jobs (including tool-mode ones with English prompts) were\n * shoved through bash, which mangled FB posts among other things.\n */\nasync function executePrompt(prompt: string, allowedTools?: string[] | string): Promise<string> {\n try {\n const { processMessage } = await import('../../agent/agent.js');\n // sessionId 'cron-<rand>' so the agent loop doesn't try to recall\n // chat history. userId 'cron' identifies the surface in logs.\n const sessionId = `cron-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 8)}`;\n // v6.4.4: normalize allowedTools. Legacy DB entries (anything created\n // before this ship) store this as a comma-separated string like\n // \"fb_post,fb_read_feed\" instead of an array. Accept both shapes so we\n // don't need a DB migration.\n const normalized: string[] | undefined = Array.isArray(allowedTools)\n ? allowedTools\n : (typeof allowedTools === 'string' && allowedTools.length > 0\n ? allowedTools.split(',').map(s => s.trim()).filter(Boolean)\n : undefined);\n // v6.4.4: allowedTools is now honored end-to-end. processMessage's\n // overrides type accepts it; when present, the agent loop short-\n // circuits the persona / brain / toolSearch / dangerous filter\n // chain and binds EXACTLY these tools. This is what cron jobs\n // like fb-* need to make the model actually call fb_post instead\n // of hallucinating <tool_call name=\"fb_post\"> in the response text.\n // (channel='cron' is already passed as the 3rd positional arg above.)\n const result = await processMessage(prompt, sessionId, 'cron',\n normalized && normalized.length > 0 ? { allowedTools: normalized } : undefined);\n const summary = (result.content || '').slice(0, 500);\n const tools = (result.toolsUsed || []).join(', ');\n return `[tool-mode] used tools: ${tools || '(none)'}\\n${summary}`;\n } catch (err) {\n return `[tool-mode error] ${(err as Error).message}`;\n }\n}\n\n/**\n * v6.0.5 — Defensive guard for shell-mode jobs. Refuses commands that\n * clearly look like English prose / LLM prompts rather than shell.\n *\n * The trigger for this guard: FB autopilot cron jobs were created with\n * the agent prompt as the `command` field but with `mode` left at the\n * default (\"shell\"). Because the scheduler also ignored `mode`, those\n * prompts went straight into `/bin/bash -c \"You are TITAN...\"` every day,\n * resulting in mangled / broken output. We catch the \"looks like a\n * prompt\" case BEFORE bash sees it so the only fix needed is the\n * scheduler honoring mode.\n *\n * Heuristic: starts with words like \"you are\", \"post a\", \"write a\",\n * \"build a\", or contains \"Use the X tool\" — strong signals of an LLM\n * prompt accidentally routed here. Conservative; real shell commands\n * (cd, ls, git, npm, python, node, /bin/, ./, ~, $(...) , etc.) never\n * match.\n */\nfunction looksLikeLlmPrompt(cmd: string): boolean {\n const head = cmd.trim().slice(0, 200).toLowerCase();\n const promptStarts = [\n 'you are ', 'you are titan',\n 'post a ', 'post an ',\n 'write a ', 'write an ',\n 'build a ', 'build me ', 'build the ',\n 'create a ', 'create me ',\n 'generate a ', 'generate an ',\n 'compose a ', 'compose an ',\n 'draft a ', 'draft an ',\n ];\n if (promptStarts.some(p => head.startsWith(p))) return true;\n if (/\\buse the (?:[a-z_]+) tool\\b/i.test(cmd)) return true;\n if (/\\bunder \\d+ words\\b/i.test(cmd)) return true;\n return false;\n}\n\n// ─── Scheduler helpers ──────────────────────────────────────────\n\n/**\n * Start a node-cron task for a persisted job. Returns the task handle.\n *\n * v6.0.5 — Now honors `mode` (shell vs tool). Previously this signature\n * was `(jobId, schedule, command)` and ALL jobs were bash-executed\n * regardless of `record.mode`. That bug mangled the FB autopilot cron\n * jobs (English prompts routed to /bin/bash). The signature now takes\n * a record-shaped arg so callers don't have to remember to pass mode +\n * allowedTools separately, and forgetting the mode flag silently\n * defaults to shell at most, never bypasses it.\n */\ninterface CronJobLike {\n id: string;\n name?: string;\n schedule: string;\n command: string;\n mode?: 'shell' | 'tool';\n allowedTools?: string[];\n}\n\nfunction scheduleJob(job: CronJobLike): ReturnType<typeof cron.schedule> | null {\n if (!cron.validate(job.schedule)) {\n logger.warn(COMPONENT, `Invalid cron expression for job ${job.id}: \"${job.schedule}\"`);\n return null;\n }\n\n const mode: 'shell' | 'tool' = job.mode === 'tool' ? 'tool' : 'shell';\n const task = cron.schedule(job.schedule, async () => {\n logger.info(COMPONENT, `Running cron job ${job.id} (mode=${mode}, name=${job.name ?? '(unnamed)'}): ${job.command.slice(0, 120)}`);\n\n const store = getDb();\n const record = store.cronJobs.find((j) => j.id === job.id);\n if (record) record.last_run = new Date().toISOString();\n\n try {\n let output: string;\n if (mode === 'tool') {\n output = await executePrompt(job.command, job.allowedTools);\n } else if (looksLikeLlmPrompt(job.command)) {\n // Defensive guard for \"shell\" mode that's actually an LLM\n // prompt. Refuse instead of letting bash mangle it.\n output = `[refused] This shell-mode cron job looks like an LLM prompt, not a shell command. Re-create the job with mode:\"tool\" so it routes to the agent loop. Command head: ${job.command.slice(0, 80)}…`;\n logger.warn(COMPONENT, `Cron job ${job.id}: REFUSED to bash-execute LLM-prompt-shaped command`);\n } else {\n output = await executeCommand(job.command);\n }\n logger.info(COMPONENT, `Cron job ${job.id} completed:\\n${output.slice(0, 500)}`);\n } catch (err) {\n logger.error(COMPONENT, `Cron job ${job.id} failed: ${(err as Error).message}`);\n }\n });\n\n return task;\n}\n\n/** Stop and remove a task from the active map. */\nfunction stopAndRemoveTask(jobId: string): void {\n const task = activeTasks.get(jobId);\n if (task) {\n task.stop();\n activeTasks.delete(jobId);\n logger.debug(COMPONENT, `Stopped cron task: ${jobId}`);\n }\n}\n\n// ─── Public: init scheduler on gateway boot ─────────────────────\n\n/**\n * Called once from server.ts on startup.\n * Reads all enabled cron jobs from the DB and schedules them.\n */\nexport function initCronScheduler(): void {\n const store = getDb();\n let scheduled = 0;\n let skipped = 0;\n\n for (const job of store.cronJobs) {\n if (!job.enabled) { skipped++; continue; }\n\n const task = scheduleJob(job);\n if (task) {\n activeTasks.set(job.id, task);\n scheduled++;\n logger.debug(COMPONENT, `Scheduled: \"${job.name}\" (${job.schedule}, mode=${job.mode ?? 'shell'})`);\n } else {\n skipped++;\n }\n }\n\n logger.info(COMPONENT, `Cron scheduler initialised — ${scheduled} active, ${skipped} skipped`);\n}\n\n// ─── Skill registration ─────────────────────────────────────────\n\nexport function registerCronSkill(): void {\n registerSkill(\n { name: 'cron', description: 'Schedule and manage recurring tasks using cron expressions. USE THIS WHEN Tony says: \"schedule this to run every X\", \"set up a recurring task\", \"run this daily at 9am\", \"run this every 5 minutes\", \"add a cron job\", \"automate X on a schedule\". WORKFLOW: action=create with name, schedule (cron expression), and shell command — action=list to see all jobs — action=enable/disable to toggle — action=delete to remove.', version: '1.1.0', source: 'bundled', enabled: true },\n {\n name: 'cron',\n description: `Schedule, list, toggle, or delete recurring jobs. Jobs persist across restarts and run via node-cron in-process. Each job has a name, a cron expression, a command, and an execution mode (shell or tool).\n\nUSE WHEN: \"schedule this to run every X\" / \"set up a recurring task\" / \"run this daily at 9am\" / \"every 5 minutes do X\" / \"add a cron job\" / \"automate X on a schedule\" / \"stop that cron job\".\n\nDO NOT USE FOR:\n- One-off tasks → just run them now via shell or the appropriate tool.\n- Tasks triggered by events (file change, webhook) → use event_triggers / webhook respectively.\n- Tasks that need TITAN to \"wake up\" and decide what to do → set mode:\"tool\" so the cron sends a message to the agent loop instead of running a bare shell.\n\nParameters:\n- action (string, required) — one of: \"create\", \"list\", \"delete\", \"enable\", \"disable\".\n- name (string, required for create) — human-readable label.\n- schedule (string, required for create) — cron expression. \"0 9 * * *\" = daily at 9am, \"*/5 * * * *\" = every 5 minutes, \"0 0 * * 0\" = Sunday midnight. Five fields, no seconds.\n- command (string, required for create) — shell command OR message to TITAN (depending on mode).\n- mode (string, optional) — \"shell\" (default; runs as raw shell) or \"tool\" (sent as a message to the agent loop, gets all tools).\n- allowedTools (string, optional) — comma-separated tool allowlist for mode:\"tool\" jobs.\n- jobId (string, required for delete/enable/disable) — id from list.\n\nReturns:\n- create → \"Created job: <id>\"\n- list → table of all jobs with status, next-run, last-run.\n- delete/enable/disable → confirmation with the affected jobId.\n\nErrors:\n- \"Invalid cron expression\" — five fields required, no seconds. Test at crontab.guru.\n- \"Job not found: <id>\" — call list to discover the right id.\n- \"Tool not in allowlist\" (mode:\"tool\") — extend allowedTools or remove the constraint.`,\n parameters: {\n type: 'object',\n properties: {\n action: {\n type: 'string',\n enum: ['create', 'list', 'delete', 'enable', 'disable'],\n description: 'Action to perform',\n },\n name: { type: 'string', description: 'Human-readable name for the cron job' },\n schedule: {\n type: 'string',\n description: 'Cron schedule expression (e.g., \"0 9 * * *\" for daily at 9 am, \"*/5 * * * *\" every 5 min)',\n },\n command: { type: 'string', description: 'Shell command or task description to execute when the job runs' },\n jobId: { type: 'string', description: 'Job ID (required for delete/enable/disable)' },\n mode: { type: 'string', enum: ['shell', 'tool'], description: 'Execution mode: \"shell\" (default) runs a shell command, \"tool\" sends the command as a message to TITAN for tool-based execution' },\n allowedTools: { type: 'string', description: 'Comma-separated list of allowed tool names for tool-mode jobs (e.g., \"web_search,write_file,shell\")' },\n },\n required: ['action'],\n },\n execute: async (args) => {\n const action = args.action as string;\n const store = getDb();\n\n switch (action) {\n // ── Create ─────────────────────────────────\n case 'create': {\n const name = args.name as string;\n const schedule = args.schedule as string;\n const command = args.command as string;\n\n if (!name || !schedule || !command) {\n return 'Error: name, schedule, and command are required for create';\n }\n\n if (!cron.validate(schedule)) {\n return `Error: \"${schedule}\" is not a valid cron expression. Example: \"0 9 * * *\" (daily at 9 am)`;\n }\n\n const mode = (args.mode as string) === 'tool' ? 'tool' : 'shell';\n const allowedToolsStr = args.allowedTools as string | undefined;\n const allowedTools = allowedToolsStr ? allowedToolsStr.split(',').map(t => t.trim()).filter(Boolean) : undefined;\n\n const id = uuid();\n const record = {\n id,\n name,\n schedule,\n command,\n mode: mode as 'shell' | 'tool',\n allowedTools,\n enabled: true,\n created_at: new Date().toISOString(),\n };\n store.cronJobs.push(record);\n\n // Schedule immediately so it runs without a restart.\n // v6.0.5 — Pass the WHOLE record so scheduleJob can\n // dispatch on mode + honor allowedTools.\n const task = scheduleJob(record);\n if (task) {\n activeTasks.set(id, task);\n }\n\n logger.info(COMPONENT, `Created and scheduled cron job: ${name} (${schedule}, mode=${mode})`);\n return `Created cron job \"${name}\" (ID: ${id})\\nSchedule: ${schedule}\\nMode: ${mode}\\nCommand: ${command}\\nStatus: Active and running`;\n }\n\n // ── List ──────────────────────────────────\n case 'list': {\n if (store.cronJobs.length === 0) return 'No cron jobs configured.';\n\n return store.cronJobs.map((j) => {\n const running = activeTasks.has(j.id) ? '(running)' : '(not scheduled)';\n const lastRun = j.last_run ? `\\n Last run: ${j.last_run}` : '';\n return [\n `• ${j.name} [${j.enabled ? '✅ enabled' : '❌ disabled'}] ${running}`,\n ` ID: ${j.id}`,\n ` Schedule: ${j.schedule}`,\n ` Command: ${j.command}`,\n lastRun,\n ].filter(Boolean).join('\\n');\n }).join('\\n\\n');\n }\n\n // ── Delete ────────────────────────────────\n case 'delete': {\n const jobId = args.jobId as string;\n if (!jobId) return 'Error: jobId is required';\n\n const job = store.cronJobs.find((j) => j.id === jobId);\n if (!job) return `Error: no cron job found with ID: ${jobId}`;\n\n // Stop the running task first\n stopAndRemoveTask(jobId);\n\n store.cronJobs = store.cronJobs.filter((j) => j.id !== jobId);\n logger.info(COMPONENT, `Deleted cron job: ${job.name} (${jobId})`);\n return `Deleted cron job \"${job.name}\" (ID: ${jobId})`;\n }\n\n // ── Enable ────────────────────────────────\n case 'enable': {\n const eId = args.jobId as string;\n if (!eId) return 'Error: jobId is required';\n\n const job = store.cronJobs.find((j) => j.id === eId);\n if (!job) return `Error: no cron job found with ID: ${eId}`;\n\n job.enabled = true;\n\n // Start scheduling if not already active.\n // v6.0.5 — Pass the whole record so mode is honored.\n if (!activeTasks.has(eId)) {\n const task = scheduleJob(job);\n if (task) {\n activeTasks.set(eId, task);\n }\n }\n\n logger.info(COMPONENT, `Enabled cron job: ${job.name} (${eId})`);\n return `Enabled cron job \"${job.name}\" — it will now run on schedule (${job.schedule})`;\n }\n\n // ── Disable ───────────────────────────────\n case 'disable': {\n const dId = args.jobId as string;\n if (!dId) return 'Error: jobId is required';\n\n const job = store.cronJobs.find((j) => j.id === dId);\n if (!job) return `Error: no cron job found with ID: ${dId}`;\n\n job.enabled = false;\n\n // Stop the running task\n stopAndRemoveTask(dId);\n\n logger.info(COMPONENT, `Disabled cron job: ${job.name} (${dId})`);\n return `Disabled cron job \"${job.name}\" — it will no longer run until re-enabled`;\n }\n\n default:\n return `Unknown action: ${action}. Valid actions: create, list, delete, enable, disable`;\n }\n },\n },\n );\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*(
|
|
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();
|